Skip to content

@btravstack/outbox


@btravstack/outbox / index

index ​

Classes ​

OutboxPublisher ​

Defined in: outbox/src/outbox.ts:106

Extends ​

Constructors ​

Constructor ​
ts
new OutboxPublisher(): OutboxPublisher;

Defined in: di/dist/index.d.mts:16

Returns ​

OutboxPublisher

Inherited from ​
ts
Port("OutboxPublisher")<OutboxPublisherService>.constructor

Properties ​

PropertyModifierTypeInherited fromDefined in
[ID]readonly"OutboxPublisher"Port("OutboxPublisher").[ID]di/dist/index.d.mts:12
[SERVICE]readonlyOutboxPublisherServicePort("OutboxPublisher").[SERVICE]di/dist/index.d.mts:13
portIdreadonly"OutboxPublisher"Port("OutboxPublisher").portIddi/dist/index.d.mts:17

OutboxStore ​

Defined in: outbox/src/outbox.ts:85

Where the relay reads from. An application provides it — from @btravstack/outbox/prisma's prismaOutboxStore, memoryOutboxStore, or an adapter of its own — beside the transaction that writes the rows, which stays the application's.

Extends ​

Constructors ​

Constructor ​
ts
new OutboxStore(): OutboxStore;

Defined in: di/dist/index.d.mts:16

Returns ​

OutboxStore

Inherited from ​
ts
Port("OutboxStore")<OutboxStoreService>.constructor

Properties ​

PropertyModifierTypeInherited fromDefined in
[ID]readonly"OutboxStore"Port("OutboxStore").[ID]di/dist/index.d.mts:12
[SERVICE]readonlyOutboxStoreServicePort("OutboxStore").[SERVICE]di/dist/index.d.mts:13
portIdreadonly"OutboxStore"Port("OutboxStore").portIddi/dist/index.d.mts:17

Type Aliases ​

MemoryOutboxStore ​

ts
type MemoryOutboxStore = OutboxStoreService & object;

Defined in: outbox/src/memory.ts:10

The in-process store, plus the write half an application's in-memory adapter calls.

Type Declaration ​

NameTypeDefined in
append()(message) => voidoutbox/src/memory.ts:11

OutboxAppend ​

ts
type OutboxAppend = Omit<OutboxMessage, "id" | "occurredAt">;

Defined in: outbox/src/memory.ts:7

What an in-memory write appends: the envelope, without the two fields the store assigns.


OutboxMessage ​

ts
type OutboxMessage = object;

Defined in: outbox/src/outbox.ts:15

One committed fact awaiting broadcast: a row of the outbox table, as the relay reads it.

The row IS the envelope. tenantId is whose fact it is, and the unit the relay claims by; kind is which sort of thing changed; subjectId is which one, and the key a reader compacts on; payload is the application's own encoding, handed to the publisher untouched — a null payload is the tombstone, the last word about a subject. id is the outbox sequence, and the order the relay publishes a tenant's facts in.

Properties ​

PropertyModifierTypeDefined in
idreadonlynumberoutbox/src/outbox.ts:16
kindreadonlystringoutbox/src/outbox.ts:18
occurredAtreadonlyDateoutbox/src/outbox.ts:21
payloadreadonlystring | nulloutbox/src/outbox.ts:20
subjectIdreadonlystringoutbox/src/outbox.ts:19
tenantIdreadonlystringoutbox/src/outbox.ts:17

OutboxOptions ​

ts
type OutboxOptions = object;

Defined in: outbox/src/relay.ts:48

What outbox is handed. Each field pins the variable named beside it.

Properties ​

PropertyModifierTypeDescriptionDefined in
clock?readonlyClockWhat the poll sleeps on and the lag is measured against (default: the kernel's systemClock).outbox/src/relay.ts:64
concurrency?readonlynumberHow many tenants hold a claim at once — OUTBOX_CONCURRENCY (default 4).outbox/src/relay.ts:62
maxLagMs?readonlynumberThe oldest pending age /healthz tolerates — OUTBOX_MAX_LAG_MS (default 60_000).outbox/src/relay.ts:54
pollMs?readonlynumberThe idle sleep between sweeps — OUTBOX_POLL_MS (default 200).outbox/src/relay.ts:52
retainMs?readonlynumber | falseHow long a published row is kept before the relay deletes it — OUTBOX_RETAIN_MS (default 0: kept). false pins it off whatever the deployment sets.outbox/src/relay.ts:60
tenants?readonlyreadonly string[]The tenants this relay serves — OUTBOX_TENANTS, comma-separated, required.outbox/src/relay.ts:50

OutboxPublisherService ​

ts
type OutboxPublisherService = object;

Defined in: outbox/src/outbox.ts:102

What "publish" means, which only the application knows: the transport, the contract and the decoding of payload.

Any Err is a message left pending: the relay stops the tenant's batch there, so a later fact never overtakes it, and tries again after its back-off.

Properties ​

PropertyModifierTypeDefined in
publishreadonly(message) => AsyncResult<void, PublishRefused>outbox/src/outbox.ts:103

OutboxStoreService ​

ts
type OutboxStoreService = object;

Defined in: outbox/src/outbox.ts:29

What a store answers for the relay. Every operation promises never: a database that will not answer is a defect the relay observes and retries, not a domain outcome.

Properties ​

PropertyModifierTypeDescriptionDefined in
claimreadonly(tenantId, limit, relay) => AsyncResult<void, never>Claims a tenant's oldest unpublished messages, hands them to relay, and marks published exactly the ids relay answers — all under one claim, so no other caller holding the same store is handed the same tenant until it is released. A tenant another caller has claimed is skipped, not waited for: the call answers Ok without running relay. A defect from relay releases the claim and marks nothing.outbox/src/outbox.ts:58
oldestPendingreadonly(tenantIds) => AsyncResult<readonly object[], never>When each of tenantIds' oldest unpublished message was written, for the tenants that have one — on ONE connection however many tenants are asked about, because the health check asks on every /healthz, and a connection per tenant would queue the pool behind the probe meant to report it.outbox/src/outbox.ts:45
pendingreadonly(tenantId, limit) => AsyncResult<readonly OutboxMessage[], never>A tenant's oldest unpublished messages, in outbox order, at most limit. A read and nothing more — it claims nothing, which is what lets the health check and a spec look without racing the relay.outbox/src/outbox.ts:35
prunereadonly(tenantId, olderThanMs, limit) => AsyncResult<number, never>Deletes a tenant's messages published more than olderThanMs ago, at most limit, and answers how many went. The age is measured on the clock that stamped the publish — the database's own, for a database — so a host clock's skew can neither age a row early nor keep it late. Published rows only: a pending one is never touched. It holds the tenant as a claim does, and a tenant another caller holds is skipped, answering 0: across replicas, one prunes a tenant at a time, and never while it is claimed.outbox/src/outbox.ts:72

PublishRefused ​

ts
type PublishRefused = object;

Defined in: outbox/src/outbox.ts:92

Whatever tagged error the publisher models — a broker that refused, a message the contract rejects. The relay reports it and acts on none of it, so the type asks for a tag and nothing else.

Properties ​

PropertyModifierTypeDefined in
_tagreadonlystringoutbox/src/outbox.ts:92

Functions ​

memoryOutboxStore() ​

ts
function memoryOutboxStore(clock?): MemoryOutboxStore;

Defined in: outbox/src/memory.ts:20

An outbox held in the process: for tests, and for an application whose in-memory repositories need somewhere to record their facts. Its claim is a per-tenant flag, so two relays over ONE instance behave as two replicas over one table do — which is the only sharing an in-process store can have.

Parameters ​

ParameterTypeDefault value
clockClocksystemClock

Returns ​

MemoryOutboxStore


outbox() ​

ts
function outbox<O>(options?): Module<HealthChecks, ConfigInvalid, 
  | OutboxPublisher
  | OutboxStore
  | EnvReading<Unpinned<O, "tenants", "OUTBOX_TENANTS">, 
  | MaybePinned<O, "tenants", "OUTBOX_TENANTS">
  | "OUTBOX_POLL_MS"
  | "OUTBOX_MAX_LAG_MS"
  | "OUTBOX_RETAIN_MS"
  | "OUTBOX_CONCURRENCY">
  | Scope>;

Defined in: outbox/src/relay.ts:275

The outbox relay: the other half of a write that recorded its fact in the same transaction as the row it describes. It claims each tenant's oldest pending messages, hands them to the application's OutboxPublisher in outbox order, marks published what was published, and sleeps.

ts
outbox({ tenants: ["acme"] });

At-least-once, with replicas taking turns. A crash between a publish and its mark re-publishes on the next claim, and so does a claiming session the database ends mid-batch, so a subscriber must tolerate a repeat, keyed by the outbox id. What the claim rules out is replicas racing: a tenant is held by one relay while its claiming session lives and skipped by the rest, so its committed facts go out in outbox order — which is not commit order: an id still in an open transaction surfaces after a higher one that committed.

A refused publish stops the tenant's batch and backs that tenant off, doubling from the poll interval to 30 seconds, so a later fact never overtakes an earlier one. A message the publisher refuses forever therefore holds its tenant's outbox — which is what the health check is for: it reports a tenant whose oldest pending message is older than maxLagMs.

At most concurrency tenants hold a claim or a prune at once, each on one pooled connection for its whole batch; the rest wait their turn in order. The default of 4 leaves most of a 10-connection pool to the publisher and the requests sharing it. A publish that never settles holds its slot, so a publisher that can hang answers Err on a timeout of its own.

Started as the graph builds, before the runtime accepts anything, and stopped when the application scope closes, after the runtime has drained. Every claim and publish is reported to Observers; the module holds no logger of its own.

Type Parameters ​

Type ParameterDefault type
O extends OutboxOptionsRecord<never, never>

Parameters ​

ParameterType
optionsO & OptionsAs<O, OutboxOptions>

Returns ​

Module<HealthChecks, ConfigInvalid, | OutboxPublisher | OutboxStore | EnvReading<Unpinned<O, "tenants", "OUTBOX_TENANTS">, | MaybePinned<O, "tenants", "OUTBOX_TENANTS"> | "OUTBOX_POLL_MS" | "OUTBOX_MAX_LAG_MS" | "OUTBOX_RETAIN_MS" | "OUTBOX_CONCURRENCY"> | Scope>

Released under the MIT License.