Postboy Help

Lifecycle Management

Registrators give a group of bus registrations an explicit lifecycle. You subclass PostboyAbstractRegistrator, declare the feature's wiring in the _up() hook, and activate it with up(). A single down() tears the whole group down: it executes a DisconnectMessage for every recorded ID, which completes subscriber streams and removes executor handlers. This topic explains why registrators exist, how the base class works, and when bare Connect* messages are enough.

Why registrators exist

Infrastructure messages wire the bus immediately and permanently:

class LanguageChangedMessage extends PostboyGenericMessage { static readonly ID = 'app.language-changed'; } postboy.exec(new ConnectMessage(LanguageChangedMessage, new Subject<LanguageChangedMessage>()));

This tells the bus what to wire, but not when that wiring should live. Without a lifecycle layer you get:

  • registrations that outlive the feature that created them;

  • subjects that are never completed, so subscribers leak;

  • scattered teardown logic that is easy to skip;

  • overriding registrations when a feature is initialized twice.

A registrator turns wiring into a lifecycle boundary: one class owns the bindings, up() activates them, down() removes them all.

PostboyAbstractRegistrator

Subclass the base class and put every record* call into the protected _up() hook. The base class remembers each recorded ID.

import { IPostboyDependingService, PostboyAbstractRegistrator, PostboyGenericMessage, PostboyService } from '@artstesh/postboy'; class CartItemAddedMessage extends PostboyGenericMessage { static readonly ID = 'cart.item-added'; constructor(public sku: string) { super(); } } class CartRegistrator extends PostboyAbstractRegistrator { protected _up(): void { this.recordSubject(CartItemAddedMessage); } } const cart = new CartRegistrator(postboy, 'cart'); // name argument optional cart.up(); // _up() runs, registrations go live cart.down(); // DisconnectMessage per recorded ID

The up()/down() contract:

  • up() runs _up() first, then calls up() on every attached service. Wiring happens at this moment, not in the constructor.

  • down() calls down() on every attached service (and clears the list), then executes a DisconnectMessage for each recorded ID. Subscriber streams complete, and further fire/sub for those IDs throw until they are registered again.

  • The namespace argument only labels the registrator; it does not affect message routing (see Namespaces).

The record* methods

The record* methods are chainable and — unlike the deprecated record* methods on PostboyService — they are the intended API on registrators. Internally each dispatches the v3.5 Connect* message and remembers the ID for down().

Method

Registers

Use for

recordSubject(type)

plain Subject

one-off events

recordReplay(type, bufferSize = 1)

ReplaySubject

replaying recent events to late subscribers

recordBehavior(type, initial)

BehaviorSubject seeded with a message instance

state: every new subscriber immediately gets the current value

recordWithPipe(type, subject, pipe)

your own Subject transformed by a pipe

shared, debounced, or filtered streams

recordExecutor(type, fn)

executor function

synchronous commands

recordHandler(executor, handler)

PostboyExecutionHandler

class-based command handlers

class CartStateChangedMessage extends PostboyGenericMessage { static readonly ID = 'cart.state-changed'; constructor(public items: string[]) { super(); } } class ApplyDiscountExecutor extends PostboyExecutor<string> { static readonly ID = 'cart.apply-discount'; constructor(public code: string) { super(); } } class CartRegistrator extends PostboyAbstractRegistrator { protected _up(): void { this.recordSubject(CartItemAddedMessage) .recordBehavior(CartStateChangedMessage, new CartStateChangedMessage([])) .recordExecutor(ApplyDiscountExecutor, (e) => `discount:${e.code}`); } }

Dependent services: IPostboyDependingService

A service that subscribes to the feature's messages should not do bus work in its constructor — the messages exist only after _up() has run. Implement IPostboyDependingService (up() required, down() optional) and attach instances with registerServices([...]). The registrator then owns the ordering:

  • up(): registrations first, then each service's up() — services subscribe to a ready bus.

  • down(): each service's down() first, then the disconnects — services stop reacting before their streams complete.

class CartTotalsService implements IPostboyDependingService { private total = 0; constructor(private postboy: PostboyService) {} up(): void { this.postboy.sub(CartItemAddedMessage).subscribe((m) => this.total += 1); } down(): void { this.total = 0; } } class CartRegistrator extends PostboyAbstractRegistrator { constructor(postboy: PostboyService, totals: CartTotalsService) { super(postboy, 'cart'); this.registerServices([totals]); // replaces any previous list } protected _up(): void { /* record* calls */ } }

Registrator or bare Connect* messages?

Use a registrator when the wiring belongs to a unit with a lifetime of its own: feature modules, screens, dynamically enabled subsystems — anything that must start, stop, or start again cleanly, possibly with dependent services.

Bare postboy.exec(new ConnectMessage(...)) (see Registration and Infrastructure Messages) is acceptable when the wiring lives exactly as long as the bus: application-wide messages registered once at startup, prototypes, test setup. The trade-off is that you track IDs and call DisconnectMessage yourself.

Practical example

const store = new CartTotalsService(postboy); const cart = new CartRegistrator(postboy, store); cart.up(); postboy.fire(new CartItemAddedMessage('sku-1')); postboy.sub(CartStateChangedMessage).subscribe((m) => console.log(m.items)); // ['sku-1'] postboy.exec(new ApplyDiscountExecutor('SAVE10')); // 'discount:SAVE10' cart.down(); // store.down(), then all three IDs disconnected, streams complete

Pitfalls

  • Calling up() twice without down() in between. _up() re-registers the same IDs; the later registration overrides the earlier one with only a console warning.

  • Bus work in constructors of dependent services. Subscribe in up(); the messages are guaranteed to exist only after _up() has run.

  • Business logic inside the registrator. Keep it to wiring; put behavior in services and handlers, where it can be tested without the bus.

  • Relying on EliminateNamespace for a directly constructed registrator. The namespace store knows only registrators created via AddNamespace; tear a custom registrator down with its own down().

  • Skipping down(). Subjects are not completed otherwise, and the registrations leak until postboy.dispose().

Next steps

07 September 2026