morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
morph::qt::QtWebSocketBackend Class Reference

IBackend implementation that communicates with a RemoteServer over WebSocket. More...

#include <qt_websocket_backend.hpp>

Inheritance diagram for morph::qt::QtWebSocketBackend:
morph::backend::IEnvelopeTransport

Public Types

using Config = QtWebSocketBackendConfig
 Alias for the reconnect configuration struct.

Public Member Functions

 QtWebSocketBackend (QUrl serverUrl, ::morph::model::detail::ActionDispatcher &dispatcher=::morph::model::detail::defaultDispatcher(), ::morph::model::detail::ModelRegistryFactory &registry=::morph::model::detail::defaultRegistry(), std::optional< QSslConfiguration > tls=std::nullopt, Config cfg=Config{})
 Constructs the backend and opens a WebSocket connection to serverUrl.
 QtWebSocketBackend (QUrl serverUrl, std::optional< QSslConfiguration > tls, Config cfg=Config{})
 Constructs the backend without naming the dispatcher/registry pair.
 QtWebSocketBackend (QUrl serverUrl, Config cfg)
 Constructs the backend without naming the dispatcher/registry pair or TLS.
 ~QtWebSocketBackend () override
 Closes the socket and cleans up pending operations.
 QtWebSocketBackend (const QtWebSocketBackend &)=delete
QtWebSocketBackend & operator= (const QtWebSocketBackend &)=delete
 QtWebSocketBackend (QtWebSocketBackend &&)=delete
QtWebSocketBackend & operator= (QtWebSocketBackend &&)=delete
bool waitForConnected (int timeoutMs=5000)
 Pumps the Qt event loop until the socket is connected or timeoutMs elapses.
::morph::async::Completion<::morph::wire::ProtocolNegotiationResult > negotiateProtocolVersion (::morph::exec::IExecutor &replyExec)
 Sends a "hello" envelope to the server and classifies its reply.
::morph::exec::detail::ModelId registerModel (const std::string &typeId, std::function< std::unique_ptr<::morph::model::detail::IModelHolder >()> factory) override
 Refuses: this backend registers only through bindModel.
::morph::async::Completion<::morph::exec::detail::ModelId > bindModel (::morph::backend::detail::BindRequest request, ::morph::exec::IExecutor &cbExec) override
 Acquires a model instance over the wire, settled by the server's reply.
void assignPrimary (::morph::exec::detail::ModelId mid, const std::string &typeId, std::string_view primary) override
 Refuses: this backend promotes only through promoteModel.
::morph::async::Completion<::morph::exec::detail::ModelId > promoteModel (::morph::backend::detail::PromoteRequest request, ::morph::exec::IExecutor &cbExec) override
 Files a live server-side instance under a key, settled by the server's reply.
std::vector< std::string > listInstances (const std::string &typeId) override
 Refuses: this backend lists instances only through instances.
::morph::async::Completion< std::vector< std::string > > instances (const std::string &typeId, ::morph::exec::IExecutor &cbExec) override
 Asks the server for the live shared primary keys of typeId.
::morph::async::Completion<::morph::wire::Envelope > request (::morph::wire::Envelope envelope, ::morph::exec::IExecutor &cbExec) override
 Sends one request envelope of a kind the other verbs do not build (IEnvelopeTransport::request) and settles with its reply.
void deregisterModel (::morph::exec::detail::ModelId mid) override
 Sends a deregister message fire-and-forget (does not wait for a reply).
::morph::async::Completion< std::shared_ptr< void > > execute (::morph::exec::detail::ModelId mid, ::morph::backend::detail::ActionCall call, ::morph::exec::IExecutor *cbExec) override
 Sends an execute message and returns a Completion that resolves on reply.
void notifyBackendChanged () override
 No-op — this backend holds no local model objects.
void cancelPending (const std::exception_ptr &exc) override
 Rejects every in-flight request's Completion with exc — executes, binds (queued ones included), promotes, instance listings and the hello alike.
void setReconnectHandler (std::function< void()> handler, ::morph::exec::IExecutor *exec) override
 Installs the handler Bridge uses to re-register handlers after a reconnect.
void setConnectHandler (const std::function< void()> &handler) override
 Installs a handler invoked on every successful connect, including the first.
void setDisconnectHandler (const std::function< void()> &handler) override
 Installs a handler invoked whenever the socket drops.
void setEventHandler (std::function< void(::morph::wire::EventBody)> handler, ::morph::exec::IExecutor *exec) override
 Installs the handler each server-initiated event is posted to. See IBackend::setEventHandler.
void setSession (::morph::session::Context session) override
 Installs the session stamped onto every control envelope this backend subsequently builds (register, registerShared, attach, assign, deregister). See IBackend::setSession.
Public Member Functions inherited from morph::backend::IEnvelopeTransport
 IEnvelopeTransport (IEnvelopeTransport const &)=delete
IEnvelopeTransport & operator= (IEnvelopeTransport const &)=delete
 IEnvelopeTransport (IEnvelopeTransport &&)=delete
IEnvelopeTransport & operator= (IEnvelopeTransport &&)=delete

Detailed Description

IBackend implementation that communicates with a RemoteServer over WebSocket.

Every request is asynchronous and nothing blocks the Qt thread: a request carries a fresh non-zero callId, its pending entry is filed under it, and the matching reply settles that entry from the textMessageReceived slot. The caller's Completion is delivered on the executor the caller names. The request verbs are execute(), bindModel()/promoteModel() (the structural registration surface, docs/spec/core/backend.md), instances() and negotiateProtocolVersion(). A bind issued before the socket is up leaves its handler unbound; a call made through it is held by the Bridge and dispatched when the bind settles.

The synchronous IBackend verbs that would have to wait for a reply — registerModel(), assignPrimary(), listInstances() — throw std::logic_error naming their completion form: waiting would mean spinning a nested event loop on the Qt thread, which a WASM main thread cannot do at all.

Everything this backend keeps — the socket, the pending tables — belongs to the thread the socket lives on, as Qt's own objects do: every verb is called there (the Bridge's owner is that thread) and every reply slot runs there, so none of it takes a lock. A debug build asserts it at each site. deregisterModel() is fire-and-forget: it sends the message and files only its callId, so the reply can be recognised and dropped.

TLS
Pass a QSslConfiguration to enable wss://. Build it with tlsVerifyingConfig() (CA-verified — the recommended production default) or tlsPinnedConfig() (pinned-certificate — the correct choice for a self-signed deployment), both in qt_tls.hpp. tlsInsecureNoVerify() disables peer verification entirely and is for local development and tests only — see security.md's "Transport security" section.
SSL-less Qt builds (QT_NO_SSL, including the standard Qt-for-WebAssembly build)
The constructor's tls parameter (and the _tls member) does not exist at all when Qt itself was configured without SSL — QSslConfiguration isn't a type Qt provides in that configuration, so there is no value to accept or ignore. wss:// still works on such a build regardless: in a WASM/browser deployment the browser terminates TLS before Qt's QWebSocket ever sees the connection, so the only thing genuinely unavailable is the ability to configure TLS from C++ (client certificates, pinning, etc.) — plain wss:// and ws:// both still connect normally.
Threading
Must be used from the Qt event loop thread. Every verb and the internal message handler are called on that thread.

Definition at line 93 of file qt_websocket_backend.hpp.

Member Typedef Documentation

◆ Config

Alias for the reconnect configuration struct.

Definition at line 96 of file qt_websocket_backend.hpp.

Constructor & Destructor Documentation

◆ QtWebSocketBackend() [1/3]

morph::qt::QtWebSocketBackend::QtWebSocketBackend ( QUrl serverUrl,
::morph::model::detail::ActionDispatcher & dispatcher = ::morph::model::detail::defaultDispatcher(),
::morph::model::detail::ModelRegistryFactory & registry = ::morph::model::detail::defaultRegistry(),
std::optional< QSslConfiguration > tls = std::nullopt,
Config cfg = Config{} )
explicit

Constructs the backend and opens a WebSocket connection to serverUrl.

Parameters
serverUrlws:// or wss:// URL of the remote RemoteServer.
dispatcherAction dispatcher (defaults to the process-level singleton).
registryModel registry (defaults to the process-level singleton).
tlsIf non-null, enables TLS and applies this configuration. Not declared at all on an SSL-less Qt build (QT_NO_SSL) — see the class doc comment's "SSL-less Qt builds" section.
cfgReconnect tuning. Default: enabled, 500ms initial / 30s cap, 2x backoff.

◆ QtWebSocketBackend() [2/3]

morph::qt::QtWebSocketBackend::QtWebSocketBackend ( QUrl serverUrl,
std::optional< QSslConfiguration > tls,
Config cfg = Config{} )
inlineexplicit

Constructs the backend without naming the dispatcher/registry pair.

QtWebSocketBackend never actually uses its dispatcher/registry constructor parameters (model construction is delegated to the server — see registerModel()'s doc comment); the main constructor still accepts them, positioned before tls/cfg, purely for API-shape parity with other backends. That forces a caller who only wants to set cfg (e.g. Config::reconnectEnabled) to spell out morph::model::detail::defaultDispatcher()/defaultRegistry() explicitly to reach the parameters after them — reaching into a detail:: namespace for no functional reason. This overload skips straight to tls/cfg.

Parameters
serverUrlws:// or wss:// URL of the remote RemoteServer.
tlsIf non-null, enables TLS and applies this configuration. Not declared at all on an SSL-less Qt build (QT_NO_SSL) — see the class doc comment's "SSL-less Qt builds" section.
cfgReconnect tuning. Default: enabled, 500ms initial / 30s cap, 2x backoff.

Definition at line 135 of file qt_websocket_backend.hpp.

◆ QtWebSocketBackend() [3/3]

morph::qt::QtWebSocketBackend::QtWebSocketBackend ( QUrl serverUrl,
Config cfg )
inlineexplicit

Constructs the backend without naming the dispatcher/registry pair or TLS.

See the (serverUrl, tls, cfg) overload's doc comment for why this exists. Equivalent to that overload with tls = std::nullopt on an SSL-enabled Qt build, or to the main constructor's defaults on an SSL-less (QT_NO_SSL) build, where there is no tls parameter to skip.

Parameters
serverUrlws:// or wss:// URL of the remote RemoteServer.
cfgReconnect tuning. Default: enabled, 500ms initial / 30s cap, 2x backoff.

Definition at line 148 of file qt_websocket_backend.hpp.

Member Function Documentation

◆ waitForConnected()

bool morph::qt::QtWebSocketBackend::waitForConnected ( int timeoutMs = 5000)

Pumps the Qt event loop until the socket is connected or timeoutMs elapses.

Must be called on the Qt event loop thread after construction. It spins a local QEventLoop, which a WASM main thread cannot do: a WASM client binds without waiting (a bind issued before the socket is up is queued) or reacts to setConnectHandler.

Parameters
timeoutMsMaximum time to wait in milliseconds.
Returns
true if connected before the timeout, false otherwise.

◆ negotiateProtocolVersion()

::morph::async::Completion<::morph::wire::ProtocolNegotiationResult > morph::qt::QtWebSocketBackend::negotiateProtocolVersion ( ::morph::exec::IExecutor & replyExec)
nodiscard

Sends a "hello" envelope to the server and classifies its reply.

Intended to be sent once, after the socket connects and before any bindModel/execute; nothing enforces that ordering. The reply also says whether the server honours "cancel", which is the only way this backend ever sends one; once called, "hello" is re-sent on every reconnect, since a new connection may reach a different server.

Parameters
replyExecExecutor the answer is delivered on. Borrowed: it must outlive the returned Completion.
Returns
A Completion resolved with Negotiated if the server accepted kProtocolVersion, or LegacyPeer if it does not understand "hello"; rejected with a std::runtime_error if the server rejects the version or the socket is not connected, and with backend::DisconnectedError if it drops before the reply.

◆ registerModel()

::morph::exec::detail::ModelId morph::qt::QtWebSocketBackend::registerModel ( const std::string & typeId,
std::function< std::unique_ptr<::morph::model::detail::IModelHolder >()> factory )
override

Refuses: this backend registers only through bindModel.

A registration is a round trip, and answering it synchronously would mean blocking the Qt thread until the reply. bindModel with an empty primary sends the same register envelope and settles a Completion. The model is constructed by the server from its own registry, so this backend never uses a factory.

Parameters
typeIdUnused.
factoryUnused.
Returns
Never returns.
Exceptions
std::logic_erroralways.

◆ bindModel()

::morph::async::Completion<::morph::exec::detail::ModelId > morph::qt::QtWebSocketBackend::bindModel ( ::morph::backend::detail::BindRequest request,
::morph::exec::IExecutor & cbExec )
override

Acquires a model instance over the wire, settled by the server's reply.

The structural registration surface (IBackend::bindModel). The request's shape selects the envelope:

primary current Envelope sent
empty 0 register (private)
empty non-zero deregister of current, then register
non-empty 0 register with shared (register-or-attach)
non-empty non-zero attach, naming current

Each carries a fresh callId from the counter execute() uses and the installed session; request.contextKey rides on every shape, since the server constructs the holder itself and the key is the only channel by which the instance's identity (and so its action log) reaches it.

A private registration issued before the socket has finished connecting is queued rather than failed: exactly the ordering a single-threaded WASM client must use, since it can never wait for the connection to settle. The queued request is sent — with a call-id assigned at that point — the moment connected fires next, first connect included, in FIFO order. If the backend is torn down first, cancelPending rejects each queued entry exactly once. A keyed bind on a disconnected socket rejects immediately instead: it may re-point a live instance, and replaying that against a later connection would attach from a current that connection never issued.

Parameters
requestOwning bind request; moved from. request.factory is unused — the server instantiates the model from request.typeId.
cbExecExecutor the continuation is delivered on. Borrowed: it must outlive the returned Completion.
Returns
A Completion resolved with the bound ModelId, or rejected with the failure.

◆ assignPrimary()

void morph::qt::QtWebSocketBackend::assignPrimary ( ::morph::exec::detail::ModelId mid,
const std::string & typeId,
std::string_view primary )
override

Refuses: this backend promotes only through promoteModel.

Same reason as registerModel: an assign is a round trip.

Parameters
midUnused.
typeIdUnused.
primaryUnused.
Exceptions
std::logic_erroralways.

◆ promoteModel()

::morph::async::Completion<::morph::exec::detail::ModelId > morph::qt::QtWebSocketBackend::promoteModel ( ::morph::backend::detail::PromoteRequest request,
::morph::exec::IExecutor & cbExec )
override

Files a live server-side instance under a key, settled by the server's reply.

The structural counterpart of assignPrimary (IBackend::promoteModel): sends the assign envelope with a fresh callId and returns an unsettled Completion. The documented no-op cases (empty primary, zero mid) resolve with request's mid without sending anything, matching IBackend::promoteModel. A disconnected socket rejects.

Parameters
requestOwning promote request; moved from.
cbExecExecutor the continuation is delivered on. Borrowed: it must outlive the returned Completion.
Returns
A Completion resolved with the promoted ModelId, or rejected with the failure.

◆ listInstances()

std::vector< std::string > morph::qt::QtWebSocketBackend::listInstances ( const std::string & typeId)
override

Refuses: this backend lists instances only through instances.

Same reason as registerModel: an instances request is a round trip.

Parameters
typeIdUnused.
Returns
Never returns.
Exceptions
std::logic_erroralways.

◆ instances()

::morph::async::Completion< std::vector< std::string > > morph::qt::QtWebSocketBackend::instances ( const std::string & typeId,
::morph::exec::IExecutor & cbExec )
override

Asks the server for the live shared primary keys of typeId.

Sends an instances envelope with a fresh callId; the reply settles the returned Completion. A disconnected socket rejects.

Parameters
typeIdString type-id to enumerate.
cbExecExecutor the answer is delivered on. Borrowed: it must outlive the returned Completion.
Returns
A Completion resolved with the canonical key strings of the live shared instances, or rejected with the server's error, a decode failure, or backend::DisconnectedError.

◆ request()

::morph::async::Completion<::morph::wire::Envelope > morph::qt::QtWebSocketBackend::request ( ::morph::wire::Envelope envelope,
::morph::exec::IExecutor & cbExec )
overridevirtual

Sends one request envelope of a kind the other verbs do not build (IEnvelopeTransport::request) and settles with its reply.

The envelope gets a fresh callId and keeps the session it carries. Made while the socket is not connected, it is held and sent at the next connect, the first included, as a private bind is; cancelPending, a drop and the destructor reject what is pending or held. On the socket's thread.

Parameters
envelopeThe request; its callId is replaced.
cbExecExecutor the answer is delivered on. Borrowed: it must outlive the returned Completion.
Returns
A Completion resolved with the reply envelope, ok or err; rejected with backend::DisconnectedError when the connection drops before the reply or the request is cancelled.

Implements morph::backend::IEnvelopeTransport.

◆ deregisterModel()

void morph::qt::QtWebSocketBackend::deregisterModel ( ::morph::exec::detail::ModelId mid)
override

Sends a deregister message fire-and-forget (does not wait for a reply).

Nothing waits for the acknowledgement, so it is safe from a destructor. The server performs no connection-scoped cleanup: an undelivered or lost deregister leaves the model registered on the server indefinitely.

The request carries a real, non-zero callId from the counter every other request uses, filed in _pendingDeregisters only so its reply is recognised and dropped.

Parameters
midId of the model to remove on the server.

◆ execute()

::morph::async::Completion< std::shared_ptr< void > > morph::qt::QtWebSocketBackend::execute ( ::morph::exec::detail::ModelId mid,
::morph::backend::detail::ActionCall call,
::morph::exec::IExecutor * cbExec )
override

Sends an execute message and returns a Completion that resolves on reply.

Assigns a monotonically increasing call-id so that concurrent calls can be matched to their replies. The Completion callbacks are posted via cbExec.

Parameters
midTarget model id on the server.
callBundled action; serializeAction and deserializeResult are used.
cbExecExecutor for delivering the completion callbacks.
Returns
Completion resolved asynchronously when the server reply arrives.

◆ notifyBackendChanged()

void morph::qt::QtWebSocketBackend::notifyBackendChanged ( )
inlineoverride

No-op — this backend holds no local model objects.

Definition at line 337 of file qt_websocket_backend.hpp.

◆ cancelPending()

void morph::qt::QtWebSocketBackend::cancelPending ( const std::exception_ptr & exc)
override

Rejects every in-flight request's Completion with exc — executes, binds (queued ones included), promotes, instance listings and the hello alike.

Called by Bridge::switchBackend() on the outgoing backend, by ~Bridge, and internally when the socket disconnects. Late replies arriving for already-cancelled call ids are dropped silently.

Parameters
excException delivered to every pending completion's error sink.

◆ setReconnectHandler()

void morph::qt::QtWebSocketBackend::setReconnectHandler ( std::function< void()> handler,
::morph::exec::IExecutor * exec )
override

Installs the handler Bridge uses to re-register handlers after a reconnect.

Parameters
handlerCallable posted to exec after every successful reconnect. Pass nullptr to clear.
execExecutor the handler runs on. Borrowed: it must outlive the installation.

◆ setConnectHandler()

void morph::qt::QtWebSocketBackend::setConnectHandler ( const std::function< void()> & handler)
override

Installs a handler invoked on every successful connect, including the first.

Parameters
handlerCallable invoked on the Qt thread after every successful connect (first and subsequent). Pass nullptr to clear.

◆ setDisconnectHandler()

void morph::qt::QtWebSocketBackend::setDisconnectHandler ( const std::function< void()> & handler)
override

Installs a handler invoked whenever the socket drops.

Fires before reconnect scheduling, so an observer sees the disconnected state even when a retry follows immediately.

Parameters
handlerCallable invoked on the Qt thread whenever the connection drops. Pass nullptr to clear.

◆ setEventHandler()

void morph::qt::QtWebSocketBackend::setEventHandler ( std::function< void(::morph::wire::EventBody)> handler,
::morph::exec::IExecutor * exec )
override

Installs the handler each server-initiated event is posted to. See IBackend::setEventHandler.

An event envelope is recognised by its kind before any reply matching, so whatever its callId, it never settles a call. Events are posted to exec in the order they arrived. Callable from any thread: the handler is held under a mutex, and an event already being delivered keeps the handler it was read with.

Parameters
handlerCallable posted to exec once per event. Pass nullptr to clear.
execExecutor the handler is posted to. Borrowed: it must outlive the installation. May be null only when handler is.

◆ setSession()

void morph::qt::QtWebSocketBackend::setSession ( ::morph::session::Context session)
override

Installs the session stamped onto every control envelope this backend subsequently builds (register, registerShared, attach, assign, deregister). See IBackend::setSession.

Parameters
sessionSession to stamp; typically pushed by Bridge::setDefaultSession().

The documentation for this class was generated from the following file: