Postboy Help

Namespaces

A namespace is a named registrator obtained from the bus rather than constructed by you. postboy.exec(new AddNamespace('space')) creates it and returns the PostboyAbstractRegistrator; you record messages and executors on that registrator, and postboy.exec(new EliminateNamespace('space')) tears everything it recorded down in one step. Because registrations live in a single shared store keyed by message ID, namespaces group lifecycle — they do not give each scope its own copy of a message type.

Creating a namespace

AddNamespace is an infrastructure executor: exec returns the created registrator. Repeating AddNamespace with the same name returns the existing registrator without recreating it.

import { AddNamespace, PostboyGenericMessage, PostboyService } from '@artstesh/postboy'; class FilterAppliedMessage extends PostboyGenericMessage { static readonly ID = 'filter.applied'; constructor(public value: string) { super(); } } const filter = postboy.exec(new AddNamespace('user-filter')); filter.recordSubject(FilterAppliedMessage); postboy.sub(FilterAppliedMessage).subscribe((m) => apply(m.value)); // repeating the call returns the same registrator: postboy.exec(new AddNamespace('user-filter')) === filter; // true

Calling record* on the returned registrator registers immediately and remembers the ID, exactly as on a registrator you subclass yourself (see Lifecycle Management). The returned registrator has an empty _up(), so the record* calls themselves are the whole wiring; there is nothing to activate with up().

Tearing a namespace down

postboy.exec(new EliminateNamespace('user-filter'));

EliminateNamespace calls down() on the namespace's registrator — attached services first, then a DisconnectMessage for each recorded ID — and removes the namespace from the store. Subscriber streams complete, and further fire/sub for the disconnected IDs throw until they are registered again. Eliminating an unknown name does nothing.

postboy.dispose() eliminates every namespace as part of tearing the whole bus down.

The name is free afterwards: a new AddNamespace('user-filter') creates a fresh, empty registrator with no memory of the previous registrations.

Critical semantics: one shared store

Registrations live in a single store keyed by the message class's static ID — not per namespace. Two consequences follow.

1. The same ID under a second namespace overwrites the earlier registration. The store logs a warning and keeps only the last registration. The replaced subject is dropped without being completed, so its subscribers simply stop receiving messages.

const a = postboy.exec(new AddNamespace('scope-a')); const b = postboy.exec(new AddNamespace('scope-b')); a.recordSubject(FilterAppliedMessage); // registered under 'filter.applied' b.recordSubject(FilterAppliedMessage); // overrides — warning logged

Eliminating scope-a afterwards still disconnects 'filter.applied' — the registrator remembers the ID it recorded, not the registration that currently sits under it.

2. There is no per-namespace routing. fire and sub resolve a message class to the one registration currently stored under its ID. Firing FilterAppliedMessage reaches the subscribers of that single registration regardless of which namespace recorded it. You cannot address "the FilterAppliedMessage of scope-a" as opposed to "the one of scope-b".

The rule: namespaces group lifecycle, not message identity. If two features need independent streams of the same kind of event, define two message classes with distinct IDs.

Scoping strategy

  • One namespace per scope. Match namespace boundaries to ownership boundaries: a component instance, a service, a feature module.

  • Use unique names. Namespace names are global. If several instances of the same class need their own namespace, suffix the name with an instance identifier.

  • Eliminate what you add. Pair every AddNamespace with an EliminateNamespace in the owner's destroy hook (ngOnDestroy, componentWillUnmount, a destroy() method). Registrations otherwise live until the bus is disposed.

  • The name is only a label. Read it back from filter.namespace; routing never consults it.

  • Directly created registrators are not namespaces. new MyRegistrator(postboy, 'cart') only labels the registrator; the namespace store knows exclusively what AddNamespace created. EliminateNamespace('cart') ignores it — use the registrator's own down().

Namespace or subclassed registrator?

Namespace

Subclassed registrator

Creation

exec(new AddNamespace(name))

new MyRegistrator(postboy, name?)

Wiring

record* calls at the call site

record* calls centralized in _up()

Dependent services

no

yes — registerServices([...])

Teardown

EliminateNamespace or down()

down()

Use a namespace for small, local scopes wired in place. Use a subclassed registrator when a feature has many bindings, dependent services, or needs a reusable wiring unit.

Pitfalls

  • Two namespaces, one message ID — the second registration replaces the first; see the shared-store rules above. Use distinct message classes per scope.

  • One giant namespace — putting everything into a single namespace defeats scoping: you can only eliminate all of it at once.

  • Forgetting EliminateNamespace — the registrations and their subscribers live on until dispose().

  • Expecting isolation — namespaces never isolate message delivery; they only bundle teardown.

Next steps

07 September 2026