|
morph 0.1.0
Asynchronous bridge between GUI thread and business models
|
Server-side message handler that owns model instances and dispatches actions. More...
#include <remote.hpp>
Public Types | |
| using | LogProvider = ::morph::backend::LogProvider |
| Alias of morph::backend::LogProvider, the type of ServerConfig::logProvider. | |
Public Member Functions | |
| RemoteServer (::morph::exec::IExecutor &workerPool, ::morph::model::detail::ActionDispatcher &dispatcher=::morph::model::detail::defaultDispatcher(), ::morph::model::detail::ModelRegistryFactory ®istry=::morph::model::detail::defaultRegistry()) | |
Constructs a server backed by workerPool with allow-all authorization and a default ServerConfig. | |
| RemoteServer (::morph::exec::IExecutor &workerPool, std::shared_ptr<::morph::session::IAuthorizer > authorizer, ::morph::model::detail::ActionDispatcher &dispatcher=::morph::model::detail::defaultDispatcher(), ::morph::model::detail::ModelRegistryFactory ®istry=::morph::model::detail::defaultRegistry()) | |
| Constructs a server with a custom authorizer and a default ServerConfig. | |
| RemoteServer (::morph::exec::IExecutor &workerPool, ServerConfig config, ::morph::model::detail::ActionDispatcher &dispatcher=::morph::model::detail::defaultDispatcher(), ::morph::model::detail::ModelRegistryFactory ®istry=::morph::model::detail::defaultRegistry()) | |
Constructs a server with allow-all authorization and config. | |
| RemoteServer (::morph::exec::IExecutor &workerPool, std::shared_ptr<::morph::session::IAuthorizer > authorizer, ServerConfig config, ::morph::model::detail::ActionDispatcher &dispatcher=::morph::model::detail::defaultDispatcher(), ::morph::model::detail::ModelRegistryFactory ®istry=::morph::model::detail::defaultRegistry()) | |
Constructs a server with a custom authorizer and config. | |
| RemoteServer (const RemoteServer &)=delete | |
| RemoteServer & | operator= (const RemoteServer &)=delete |
| RemoteServer (RemoteServer &&)=delete | |
| RemoteServer & | operator= (RemoteServer &&)=delete |
| ~RemoteServer () | |
| Closes the server strand before any other member goes. | |
| void | handle (std::string msg, std::function< void(std::string)> reply) |
Asynchronously processes a JSON Envelope and calls reply with the response. | |
| void | handle (std::string msg, std::function< void(std::string)> reply, ConnectionId cid) |
Like handle(msg, reply), but additionally attributes any register decoded from msg to a connection scope. | |
| std::string | handleInline (const std::string &msg) |
| Processes a control Envelope on the server strand and returns its reply, blocking the caller until it has one. | |
| std::string | handleInline (const std::string &msg, ConnectionId cid) |
Like handleInline(msg), but additionally attributes any register decoded from msg to a connection scope. | |
| ConnectionId | openConnection () |
| Opens a new connection scope and returns its id. | |
| void | closeConnection (ConnectionId cid) |
Reclaims every model still registered under cid, then drops the scope. | |
| void | setConnectionWriter (ConnectionId cid, ConnectionWriter writer) |
Hands the server the transport's writer for connection cid, through which post reaches it. | |
| void | post (ConnectionId cid, std::string envelope) |
Writes envelope to connection cid, unless its reader has fallen behind. | |
| void | connectionDrained (ConnectionId cid) |
Tells the server that connection cid's backlog has fallen below kEventBacklogLow. | |
| bool | eventsNegotiated (ConnectionId cid) const |
Whether connection cid negotiated server-sent events. | |
| detail::WatchTable const & | watchTable () const |
| The topics each open connection watches. | |
| void | fanOut (::morph::effects::Changes changes, ConnectionId caller, std::string_view callerPrincipal) |
Tells every connection watching what changes names that it changed, as one changes event per connection. | |
| ::morph::exec::IExecutor & | strand () noexcept |
| The server strand, as an executor. | |
| PayloadCompleteness | payloadCompleteness () const noexcept |
| Returns the configured payload-completeness rule. | |
| ::morph::async::Completion< HealthStatus > | health (::morph::exec::IExecutor &replyExec) |
| The server's health, answered on the server strand. | |
| void | beginShutdown () |
| Enters shutdown: from now on, register, attach and execute envelopes are rejected with err "server shutting down". deregister is still served so clients can tear down cleanly. attach is refused too, so a client cannot re-attach to a shared instance during the drain window. | |
| ::morph::async::Completion< bool > | drainedWithin (std::chrono::milliseconds deadline, ::morph::exec::IExecutor &replyExec) |
Answers whether every in-flight execute delivers its reply within deadline. | |
Server-side message handler that owns model instances and dispatches actions.
RemoteServer receives JSON envelopes (morph::wire::Envelope) from any transport (WebSocket, in-process simulation, …) and executes the corresponding model operations via an ActionDispatcher. Authorization is delegated to an IAuthorizer that defaults to allow-all.
Definition at line 365 of file remote.hpp.
| using morph::backend::RemoteServer::LogProvider = ::morph::backend::LogProvider |
Alias of morph::backend::LogProvider, the type of ServerConfig::logProvider.
Definition at line 369 of file remote.hpp.
|
inlineexplicit |
Constructs a server backed by workerPool with allow-all authorization and a default ServerConfig.
| workerPool | Pool the server strand and every model strand run on. Borrowed, not owned: it must outlive this server and keep running until teardown completes (see docs/spec/concurrency_and_lifetimes.md, "Destruction ordering"). |
| dispatcher | Action dispatcher; defaults to the process-level singleton. Borrowed: it must outlive this server. |
| registry | Model factory registry; defaults to the process-level singleton. Borrowed: it must outlive this server. |
| morph::model::EffectsRegistrationError | if the process's record and effects declarations are invalid: the server reads them here, so such a server is never constructed. |
Definition at line 386 of file remote.hpp.
|
inline |
Constructs a server with a custom authorizer and a default ServerConfig.
| workerPool | Pool the server's strands run on. Borrowed, on the same terms as the constructor above. |
| authorizer | Authorizer consulted for every envelope that makes an authorization decision; null means allow-all. |
| dispatcher | Action dispatcher; defaults to the process-level singleton. Borrowed: it must outlive this server. |
| registry | Model factory registry; defaults to the process-level singleton. Borrowed: it must outlive this server. |
| morph::model::EffectsRegistrationError | if the process's record and effects declarations are invalid: the server reads them here, so such a server is never constructed. |
Definition at line 407 of file remote.hpp.
|
inline |
Constructs a server with allow-all authorization and config.
| workerPool | Pool the server's strands run on. Borrowed, on the same terms as the first constructor. |
| config | Limits, log provider, health handler, protocol range and payload rule, fixed for the server's life. |
| dispatcher | Action dispatcher; defaults to the process-level singleton. Borrowed: it must outlive this server. |
| registry | Model factory registry; defaults to the process-level singleton. Borrowed: it must outlive this server. |
| std::invalid_argument | if config.minProtocolVersion exceeds config.maxProtocolVersion, or config.services holds a null entry. |
| morph::model::EffectsRegistrationError | if the process's record and effects declarations are invalid: the server reads them here, so such a server is never constructed. |
Definition at line 431 of file remote.hpp.
|
inline |
Constructs a server with a custom authorizer and config.
Calls config.healthHandler, if set, once, before returning, with the initial status (ready, nothing registered, nothing in flight).
| workerPool | Pool the server's strands run on. Borrowed, on the same terms as the first constructor. |
| authorizer | Authorizer consulted for every envelope that makes an authorization decision; null means allow-all. |
| config | Limits, log provider, health handler, protocol range and payload rule, fixed for the server's life. |
| dispatcher | Action dispatcher; defaults to the process-level singleton. Borrowed: it must outlive this server. |
| registry | Model factory registry; defaults to the process-level singleton. Borrowed: it must outlive this server. |
| std::invalid_argument | if config.minProtocolVersion exceeds config.maxProtocolVersion, or config.services holds a null entry. |
| morph::model::EffectsRegistrationError | if the process's record and effects declarations are invalid: the server reads them here, so such a server is never constructed. |
Definition at line 459 of file remote.hpp.
|
inline |
Closes the server strand before any other member goes.
Every task the server posts holds the server, so nothing of it is still queued here; closing waits for a task still running on another thread and returns at once from inside the server's own last task.
Definition at line 493 of file remote.hpp.
|
inline |
Asynchronously processes a JSON Envelope and calls reply with the response.
Decodes on the calling thread and posts the envelope to the server strand. reply is called exactly once, from a pool thread: the server strand's, or, for an execute, the model strand's (or the I/O loop's, when a LimitPolicy::executeTimeout answers first).
Callable from any thread. For one connection, calls made one after the other on one thread are handled in that order.
| msg | JSON-encoded morph::wire::Envelope (via wire::encode). |
| reply | Callback invoked with the JSON-encoded reply envelope. |
Definition at line 510 of file remote.hpp.
|
inline |
Like handle(msg, reply), but additionally attributes any register decoded from msg to a connection scope.
A register processed under a non-zero cid records the new model in that connection's scope, so a later closeConnection(cid) reclaims it. Passing cid == 0 is exactly the unscoped, two-argument handle() — nothing is recorded and nothing is ever cleaned up automatically.
Callable from any thread, on the same terms as handle(msg, reply).
| msg | JSON-encoded morph::wire::Envelope (via wire::encode). |
| reply | Callback invoked with the JSON-encoded reply envelope. |
| cid | Connection scope to attribute a register in msg to; 0 means unscoped. |
Definition at line 528 of file remote.hpp.
|
inline |
Processes a control Envelope on the server strand and returns its reply, blocking the caller until it has one.
For register, deregister, attach, assign, instances, schemas and hello — the synchronous control path SimulatedRemoteBackend uses. Runs inline when the caller is already on the server strand; otherwise posts and waits. execute is rejected up front with an err reply: its reply is produced later, on the model's strand.
Callable from a pool thread, including one inside a running action (a handler registered from an action handler): the server strand then runs on another pool thread while this one waits. So the pool needs a thread free for it — a one-thread pool calling this from inside its own task, or every pool thread blocked here at once, never gets its reply. See docs/spec/concurrency_and_lifetimes.md, "Synchronous re-entry".
| msg | JSON-encoded morph::wire::Envelope (via wire::encode). |
Definition at line 550 of file remote.hpp.
|
inline |
Like handleInline(msg), but additionally attributes any register decoded from msg to a connection scope.
The synchronous counterpart to the scoped handle(msg, reply, cid) overload, on the same terms as the one-argument handleInline. Passing cid == 0 is exactly the unscoped overload above.
| msg | JSON-encoded morph::wire::Envelope (via wire::encode). |
| cid | Connection scope to attribute a register in msg to; 0 means unscoped. |
Definition at line 563 of file remote.hpp.
|
inlinenodiscard |
Opens a new connection scope and returns its id.
Call once per accepted transport connection (e.g. from a WebSocket server's "new connection" callback). The returned id is never 0, so it can always be distinguished from the reserved "unscoped" value. Pass it to the scoped handle(msg, reply, cid) overload for every message received on that connection, and to closeConnection(cid) once the connection is gone.
Callable from any thread. The id is drawn at once; the scope is opened on the server strand, before any handle() the same thread makes next.
Definition at line 603 of file remote.hpp.
|
inline |
Reclaims every model still registered under cid, then drops the scope.
Call once the transport observes the connection is gone (disconnect, close, error). Releases exactly as many references as this connection held, exactly as an explicit deregister would, then drops the scope itself. A private instance (count 1, no directory entry) is erased outright, so a later execute against its id replies err "model not found"; a shared instance another connection is still attached to survives, and an execute against it still succeeds — see InstanceDirectory::release's retained outcome.
Idempotent: cid == 0, an unknown cid, or a cid already closed is a no-op. Deliberately does not consult IAuthorizer — this is the server's own housekeeping in reaction to a transport-level event, not an action attributable to any caller (see docs/spec/core/backend.md).
Safe while a model in the scope has an execute in flight: the model strand's task holds its own shared_ptr to the model holder, so erasing the registry entry here only prevents new lookups (see docs/spec/concurrency_and_lifetimes.md).
Callable from any thread; runs on the server strand, before any handle() the same thread makes next.
| cid | Connection scope to close, as returned by openConnection(). |
Definition at line 637 of file remote.hpp.
|
inline |
Hands the server the transport's writer for connection cid, through which post reaches it.
Replaces any writer cid already has, keeping a resync the connection is owed. Kept only while the connection is open: a writer for 0, for an unknown cid or for one already closed is dropped, and closeConnection(cid) drops it. A writer whose write is empty removes the connection's writer.
Callable from any thread; runs on the server strand, after an openConnection() and before any post the same thread makes next.
| cid | Connection, as returned by openConnection(). |
| writer | The connection's writer and its backlog read. |
Definition at line 657 of file remote.hpp.
|
inline |
Writes envelope to connection cid, unless its reader has fallen behind.
On the server strand, while the connection's backlog() exceeds kEventBacklogHigh, the envelope is dropped and the connection is owed one resync event, which connectionDrained(cid) sends. Until then every later envelope is dropped too, so the resync is the next thing the connection is sent after the hold-back began. Replies do not pass through here and are never held back.
Dropped as well: an envelope larger than wire::kMaxEnvelopeBytes (counted as Metric::eventOversize), and one for a connection with no writer — 0, unknown, or closed. A write or backlog that throws is caught on the strand, and the envelope is not written.
Callable from any thread. Envelopes one thread posts to one connection are written in that order.
| cid | Connection to write to. |
| envelope | The encoded envelope (via wire::encode). |
Definition at line 696 of file remote.hpp.
|
inline |
Tells the server that connection cid's backlog has fallen below kEventBacklogLow.
What a transport calls when it has written enough of what it holds. If post held an event back since the last drain, the connection is sent one resync event (wire::makeEvent(wire::kResyncTopic, "{}")), meaning everything it watches may have changed, and events flow again. Otherwise nothing is sent. The drain, not a later write, sends it, so a connection that only listens still receives it.
Callable from any thread, the transport's included; it only posts to the server strand.
| cid | Connection whose backlog drained. |
Definition at line 721 of file remote.hpp.
|
inlinenodiscard |
Whether connection cid negotiated server-sent events.
Server strand only: call it from a task on strand(); a debug build asserts that. The answer is what the UI service recorded when the connection's ui-hello offered events, and stays until the connection closes; a reconnect is a new connection and negotiates again.
| cid | Connection, as returned by openConnection(). |
Definition at line 737 of file remote.hpp.
|
inlinenodiscard |
The topics each open connection watches.
Server strand only: call it from a task on strand(); a debug build asserts that. Filled by the watch and unwatch kinds, and emptied for a connection when it closes or a write to it fails.
Definition at line 749 of file remote.hpp.
|
inline |
Tells every connection watching what changes names that it changed, as one changes event per connection.
Server strand only: call it from a task on strand(); a debug build asserts that. A successful execute posts its changes here.
A record topic, and the instance topic of a keyed shared instance, go to every connection watching them, caller included: the caller may never see its own reply (it timed out, the client discarded it, or it went to an offline replay). An instance is matched by the directory key it holds now, so one an assign keyed while its execute ran reaches that key's watchers. A private instance's topic goes to every connection whose scope holds that instance, which needs no watch, and to no other. A connection that has not negotiated events is sent nothing.
Each recipient's watches are then re-authorised by the rule that admitted them (reauthorise): a topic whose watch fails is dropped from the table and from the event, and a connection that lost any watch is sent one resync (sendResync), after whatever it had queued. A record registered with morph::record::privateKeys reaches a principal other than callerPrincipal without its keys.
What is left is queued on the connection (detail::ChangeCoalescer), not sent: the queue is merged for ServerConfig::coalesceWindow from its first change, or until detail::kCoalesceKeyLimit keys are queued, and then sent as one changes event through post's backlog hold-back and size cap. Nothing is queued for a connection with no writer, or one held back behind its backlog, which its drain's resync covers.
| changes | What one execute changed, from effects::deriveChanges. |
| caller | Connection the execute arrived on; 0 for unscoped. |
| callerPrincipal | The execute's verified principal; empty when none was. |
Definition at line 786 of file remote.hpp.
|
inlinenodiscardnoexcept |
The server strand, as an executor.
What runningOn(server.strand()) asks about: true inside every task the server runs on its own state, false elsewhere. A task posted to it runs in order with the server's own, never beside one.
Definition at line 821 of file remote.hpp.
|
inlinenodiscardnoexcept |
Returns the configured payload-completeness rule.
Definition at line 825 of file remote.hpp.
|
inlinenodiscard |
The server's health, answered on the server strand.
Posted, so the answer reflects every envelope and verb this thread handed the server before calling. Callable from any thread; the caller attaches to the answer on replyExec, the owner it names.
| replyExec | Executor the answer is delivered on, and the one the caller attaches its callbacks from. Borrowed: it must outlive the returned Completion's delivery. |
replyExec. Definition at line 838 of file remote.hpp.
|
inline |
Enters shutdown: from now on, register, attach and execute envelopes are rejected with err "server shutting down". deregister is still served so clients can tear down cleanly. attach is refused too, so a client cannot re-attach to a shared instance during the drain window.
Posted to the server strand: an envelope this thread hands handle() after this call returns is refused. Idempotent. There is no way back: a restarted service constructs a fresh RemoteServer.
Also flips health()'s ready to false and calls ServerConfig::healthHandler, if set, with the post-shutdown status, on the server strand — what lets an orchestrator stop routing to this server while drainedWithin()'s drain runs.
Definition at line 861 of file remote.hpp.
|
inlinenodiscard |
Answers whether every in-flight execute delivers its reply within deadline.
"In-flight" is one count, kept on the server strand: incremented when an execute is admitted for dispatch and decremented, on the strand, when its reply is sent — on every resolving path (ok, err, or a LimitPolicy::executeTimeout firing first). The same count LimitPolicy::maxInFlightExecutes gates and health()'s inFlight reads. Independent of beginShutdown(): it observes, it does not stop new work from arriving. Callable from any thread; nothing blocks. The caller attaches to the answer on replyExec, the owner it names.
| deadline | Longest time to wait; 0 answers from the count as it stands. |
| replyExec | Executor the answer is delivered on, and the one the caller attaches its callbacks from. Borrowed: it must outlive the returned Completion's delivery. |
deadline elapses first. Its callbacks run on replyExec. Definition at line 892 of file remote.hpp.