Skip to content

Events

Events is inseam’s typed, durable, in-process event bus: a transactional outbox written in the same Store batch as the data change that produced it, plus a dispatcher that fans rows out to in-process subscribers. Plugins contribute Listeners (side effects) and Materializers (derived projections) through an Effect-free contract.

emit doesn’t write directly — it appends INSERTs to a caller-supplied BatchBuilder. The caller commits via Store.batch, so the event row and the data row land atomically. Dispatch is asynchronous: subscribers run in their own loops, advance per-origin cursors, and survive crashes by reading from the outbox.

Two surfaces, split by package:

  • @inseam/events-contractEventEnvelope, Listener, Materializer, EventsModule, defineEvents, HandlerResult / MaterializerResult, Cursor, plus the error hierarchy. Zero runtime deps.
  • @inseam/core (Events contribution) — openEventsRegistry, EventsRegistry, EmitHandle, OutboxRow, BatchBuilder, plus the operator-facing halt-recovery actions. Owns migrations, per-origin sequence counter, the dispatcher loop, cursor / dead-letter / halt tables.

Two subscriber roles, two contracts:

  • Listener — side-effecting, at-least-once, sequential per (listener, origin). Failures retry with exponential backoff up to maxAttempts, then dead-letter and advance. Handlers MUST be idempotent on event.id. Return { ok: false, retryable }; never throw.
  • Materializer — owns a projection. Receives ordered, gap-free delivery per origin. The projection write and the cursor advance commit in the same ctx.tx batch. Failures retry; exhaustion halts the materializer on that origin (cursor stays put) until an operator skips, resumes, or rewinds.

Other pieces:

  • defineEvents({ name, listeners, materializers }) — the single EventsModule a plugin contributes.
  • emit (via guarded handle) — appends to a BatchBuilder. Subscribers’ types must use <plugin-id>.* or core.*; anything else is rejected with InvalidEventTypeError.
  • Trust-bearing core types (principal.identifier_verified, pairing.token_*, host.capability_changed, …) — core-only emit. Plugin emit paths are rejected with TrustBearingTypeError.
  • Operator halt recoveryskipHaltedEvent, resumeHaltedMaterializer, rewindMaterializer. On the registry surface for hosts only; not plugin-callable.

Exact signatures: @inseam/events-contract API · arch/events/spec.md.

  • Owning a table? Write a Materializer. Earn ordered, gap-free, cursor-atomic delivery; in exchange, projection writes go through ctx.tx and exhausted failures halt rather than dead-letter.
  • Side effect only? Write a Listener. Pay the at-least-once tax with idempotency keyed on event.id.
  • Emitting from inside a write path? Get a guarded emit handle and append to the same BatchBuilder the data write is using. That’s the only way to keep the event and the row atomic.

Don’t reach for Events as a pub-sub primitive for short-lived in-process messages — that’s what plain function calls are for. Events earns its weight when durability, ordering, and crash-safety matter.

  • LLM summary — dense reference for agents.
  • Storeemit appends to a BatchBuilder that the caller commits via Store.batch.
  • Access — two core materializers ride this bus.
  • Connection — Connection lifecycle events ride this port.
  • arch/events/design.md — why one bus; inbound/outbound/peer-sync as one shape; security defaults.
  • arch/events/spec.md — envelope shape, table schemas, behavior, acceptance criteria.