How It Works
What actually happens on fire, exec, and fireCallback — and why Postboy behaves the way it does. Understanding the two facts below prevents most real-world surprises.
Fact 1: routing is keyed by the static ID string
Registration stores a Subject (or an executor handler) under the message class's static readonly ID. Dispatch looks the ID up in that store. Class identity plays no role.
Consequences:
A class without its own
static readonly IDfails the internalcheckIdwith"<ClassName> should have a static ID field".Inheriting a parent's ID makes two message types share one route — messages silently cross-talk between the types. Never do it (see Caveats).
Registering the same ID twice overwrites the first registration silently (with a console warning) — there is exactly one route per ID.
Fact 2: the bus is mutated only by infrastructure messages
Since v3.5 the service exposes no mutating convenience methods. Registration, locking, middleware, and namespaces are all performed by executing infrastructure messages:
Anatomy of a fire
The message's ID is checked (
checkId).The middleware pipeline's
Publishstage runs:canHandle→beforefor each applicable middleware. AnInterruptdecision aborts here — the caller gets aCancelErrorand the subject is never touched (see Middleware Cancellation).The registered Subject receives
next(message); every subscriber ofpostboy.sub(Type)gets the instance.afterhooks run.
If the ID is not registered, fire throws. If the type is locked (exec(new LockMessage(Type))), the message is silently dropped — no error, no delivery, but middleware hooks still run. Mute, not error.
Anatomy of an exec
The Execute middleware stage runs, then the registered handler function is invoked synchronously; its return value is the exec result. await-ing it is always a mistake — for asynchronous work use a callback message. An unregistered executor throws.
Anatomy of a fireCallback
fireCallback(message, action?) returns an Observable<T> over the message's internal result subject. The responder completes it via next/finish:
With an
action, the message is dispatched eagerly andactionruns once per emitted value.Without
action, dispatch is lazy — the request is sent only when the returned Observable is subscribed.Since 3.5.3, the message is dispatched at most once per
fireCallbackcall; extra subscriptions to the returned Observable never re-send the request.
Details and patterns: Callbacks.
Teardown
DisconnectMessage completes the registered Subject — subscriber streams end. A registrator's down() sends one DisconnectMessage per recorded ID (Lifecycle Management). postboy.dispose() additionally eliminates every namespace, clears the store, and disposes all middleware.
Next steps
Message Roles — choosing between events, queries, and commands.
Middleware — the staged pipeline in depth.
API reference — exact signatures.