Postboy Help

Callbacks

A callback message is an asynchronous request/response. The sender fires it and receives an Observable<T> of the result; a responder subscribes to the message and completes it. Callbacks extend PostboyCallbackMessage<T> and use the fireCallback verb. When no result is needed, use an event; when the result must be synchronous, use an executor.

When to use callbacks

Use callbacks when the sender depends on a response that arrives asynchronously: queries, remote calls, long operations with progress. Do not use them for pure notifications (events) or synchronous lookups (executors).

Anatomy of PostboyCallbackMessage<T>

Member

Purpose

result: Observable<T>

the stream the sender consumes

next(value: T)

emit a partial or interim result

finish(value: T)

emit the final result and complete

complete()

complete the stream without a final value

import {PostboyCallbackMessage} from '@artstesh/postboy'; export class LoadUserQuery extends PostboyCallbackMessage<User | null> { static readonly ID = 'app.user.load'; constructor(public readonly userId: string) { super(); } }

The responder pattern

One side registers the message and answers requests by calling finish:

postboy.exec(new ConnectMessage(LoadUserQuery, new Subject<LoadUserQuery>())); postboy.sub(LoadUserQuery).subscribe((msg) => { msg.finish(cache.get(msg.userId) ?? null); });

The other side asks and consumes the result:

postboy.fireCallback(new LoadUserQuery('u-42')).subscribe((user) => { console.log(user ? user.name : 'not found'); });

The sender does not know whether the responder answered from a cache, an HTTP call, or a timer — only that a User | null arrives.

fireCallback and lazy dispatch

The signature is fireCallback<T>(message, action?): Observable<T>. Without action, the dispatch is lazy: the message is sent only when the returned Observable is subscribed.

const result$ = postboy.fireCallback(new LoadUserQuery('u-42')); // nothing sent yet result$.subscribe((user) => console.log(user)); // request goes out here

Pass action — or subscribe immediately — when the request must be sent right away. The action callback runs on every emitted value:

postboy.fireCallback(new LoadUserQuery('u-42'), (user) => console.log(user));

Progress with next and finish

next emits interim results; finish emits the last one and completes the stream:

export class GenerateReportQuery extends PostboyCallbackMessage<ReportProgress> { static readonly ID = 'report.generate'; } postboy.sub(GenerateReportQuery).subscribe((msg) => { msg.next({done: 1, total: 3}); msg.next({done: 2, total: 3}); msg.finish({done: 3, total: 3}); });

Typed responses

T documents the whole contract. When several outcomes are possible, model them explicitly:

type ProductResult = | { status: 'success'; data: Product } | { status: 'not-found' } | { status: 'error'; code: string }; export class GetProductQuery extends PostboyCallbackMessage<ProductResult> { static readonly ID = 'shop.product.get'; constructor(public readonly productId: string) { super(); } }

The sender can branch exhaustively over result.status — no ambiguous null.

Pitfalls

  • A responder that never calls finish or complete leaves the sender's subscription open forever.

  • Throwing inside the responder does not error the sender's stream — translate failures into the result (see Callback Patterns).

  • fireCallback without action and without a subscription never sends the request.

  • Re-firing the same message instance re-subscribes its action; use a fresh instance per request.

  • Calling next or finish from anywhere other than the responder's sub handler — the sender only subscribes to result.

Next steps

07 September 2026