Realtime in CoolMS is a transport-neutral bus. Business code publishes an intent; a transport — today Centrifugo — delivers it. Nothing in a module's write path knows which transport is configured, or whether one is configured at all.
Three layers, on purpose#
- The bus contract —
publish(channel, payload)andbroadcast(channels, payload), owned by a module that contains no transport. - The transport —
asynchands the message to the bus,synccalls the broker's HTTP API inline,nulldiscards it. One key chooses,coolms_centrifugo.publisher;asyncis the default, and the test environment is pinned tonullso a persistence test never needs a broker running to save a row. A change of transport is a change of configuration, not of code. - Per-module typed wrappers — each consuming module ships its own intent-revealing helper. Listeners and processors inject that, never the raw bus.
The wrapper layer is what makes the bottom layer replaceable. When the contract itself moved modules, no consumer changed, because no consumer had ever named it.
The rule that matters#
A publisher must never throw into the write path.
A realtime message is an optimisation. The user's data is not. If the message broker is down, the correct outcome is a saved record and a missing live update — a page refresh fixes that. The incorrect outcome is a failed save, which no refresh fixes.
So the enqueue seam is guarded, and a transport that cannot deliver degrades to silence. The null publisher is not a testing convenience; it is the same behaviour, chosen deliberately.
Publish after flush#
Messages go out after the database transaction commits. Publishing inside the transaction means announcing a change that may still roll back, and a subscriber that reacts by fetching the record will not find it. "It worked in development" is what this bug says, because a fast local database usually commits before the subscriber gets around to asking.
Channels#
Channels are named per module and declared rather than assembled ad hoc, so a subscription can be authorized before it is granted. A client asks the server for a subscription token; the server decides what that client may listen to. A channel name is not a secret and must never be the only thing standing between a viewer and someone else's data.
Presence#
Presence — who is here, who is typing — is pushed, not polled. It is also the part of the system where flags live in the transport's own configuration rather than the application's, so a presence feature that appears dead is worth checking there before reading any application code.