Postboy Help

Executors

An executor is a synchronous command. The caller passes it to postboy.exec(...) and gets the typed result back immediately — no stream, no waiting. Executors extend PostboyExecutor<T>. For results that arrive asynchronously use callbacks; for notifications use events.

When to use executors

Use executors for operations that complete immediately: mapping, formatting, validation, cache lookups, lightweight commands. Do not use them for asynchronous work — exec returns T itself, so a Promise would come back unresolved. Do not use them for broadcasts — subscribers cannot react to an executor.

The contract

import {PostboyExecutor} from '@artstesh/postboy'; export class FormatDateExecutor extends PostboyExecutor<string> { static readonly ID = 'app.date.format'; constructor(public readonly date: Date) { super(); } }

The class states its identity (ID), its input (constructor payload), and its output (T). Read the class and you know the operation.

Execute with exec

const formatted: string = postboy.exec(new FormatDateExecutor(new Date()));

exec is synchronous and returns T directly. Never await it and never treat its result as an Observable. If the operation is asynchronous, model it as a PostboyCallbackMessage<T> and send it with fireCallback — see Callbacks.

Registration

An executor must be registered before use; an unregistered ID throws at the call site.

Direct function via ConnectExecutor:

import {ConnectExecutor} from '@artstesh/postboy'; postboy.exec(new ConnectExecutor(FormatDateExecutor, (e) => Intl.DateTimeFormat('en-US').format(e.date), ));

Handler class via ConnectHandler — for logic that outgrows a lambda:

import {ConnectHandler, PostboyExecutionHandler} from '@artstesh/postboy'; export class ComputeTaxExecutor extends PostboyExecutor<number> { static readonly ID = 'shop.tax.compute'; constructor(public readonly net: number) { super(); } } export class ComputeTaxHandler extends PostboyExecutionHandler<number, ComputeTaxExecutor> { handle(executor: ComputeTaxExecutor): number { return executor.net * 0.2; } } postboy.exec(new ConnectHandler(ComputeTaxExecutor, new ComputeTaxHandler()));

Inside a registrator, use the chainable forms (not deprecated):

class ShopRegistrator extends PostboyAbstractRegistrator { protected _up(): void { this.recordExecutor(FormatDateExecutor, (e) => Intl.DateTimeFormat('en-US').format(e.date), ); this.recordHandler(ComputeTaxExecutor, new ComputeTaxHandler()); } }

Executors vs events vs callbacks

Event

Callback

Executor

Class

PostboyGenericMessage

PostboyCallbackMessage<T>

PostboyExecutor<T>

Verb

fire, sub, once

fireCallback

exec

Result

none

Observable<T>

T, synchronous

Consumers

zero or many subscribers

one responder completes the message

exactly one registered handler

Timing

broadcast, asynchronous

request/response, asynchronous

immediate

Middleware

exec passes through the middleware pipeline like every dispatch. An Interrupt returned from before(...) throws CancelError instead of running the handler — see Middleware.

Pitfalls

  • await postboy.exec(...) — the result is T, not a Promise.

  • Calling exec before registration — throws (TypeError calling an undefined handler).

  • Long-running or asynchronous work inside a handler — it blocks the caller or returns an unresolved Promise.

  • Sharing one ID between two executor classes — the last registration wins.

Next steps

07 September 2026