@eventferry/core 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -247,7 +247,8 @@ var Relay = class {
247
247
  } else {
248
248
  await this.handleFailure(
249
249
  record,
250
- result.error ?? new Error("unknown publish error")
250
+ result.error ?? new Error("unknown publish error"),
251
+ result.errorKind
251
252
  );
252
253
  }
253
254
  }
@@ -256,15 +257,17 @@ var Relay = class {
256
257
  }
257
258
  return batch.length;
258
259
  }
259
- async handleFailure(record, error) {
260
+ async handleFailure(record, error, errorKind) {
260
261
  const attempts = record.attempts + 1;
261
- const retryAt = nextRetryAt(this.retry, attempts);
262
+ const isTerminalKind = errorKind === "fatal" || errorKind === "poison";
263
+ const retryAt = isTerminalKind ? null : nextRetryAt(this.retry, attempts);
262
264
  const willRetry = retryAt !== null;
263
265
  this.hooks.onFailed?.(record, error, willRetry);
264
266
  this.log.warn("publish failed", {
265
267
  recordId: record.id,
266
268
  attempts,
267
269
  willRetry,
270
+ errorKind: errorKind ?? "retriable",
268
271
  error: error.message
269
272
  });
270
273
  if (willRetry) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/backoff.ts","../src/serializer.ts","../src/publishable.ts","../src/relay.ts","../src/errors.ts","../src/registry.ts"],"sourcesContent":["export * from \"./types.js\";\nexport * from \"./backoff.js\";\nexport * from \"./serializer.js\";\nexport * from \"./publishable.js\";\nexport * from \"./relay.js\";\nexport * from \"./standard-schema.js\";\nexport * from \"./errors.js\";\nexport * from \"./registry.js\";\n","/**\n * Status lifecycle of an outbox record.\n *\n * pending -> freshly enqueued, awaiting first publish\n * processing -> claimed by a relay instance, publish in flight\n * done -> successfully published to the broker\n * failed -> publish failed, awaiting retry (next_retry_at)\n * dead -> exhausted retries, routed to DLQ (or parked)\n */\nexport type OutboxStatus = \"pending\" | \"processing\" | \"done\" | \"failed\" | \"dead\";\n\nexport const OUTBOX_STATUS_CODE: Record<OutboxStatus, number> = {\n pending: 0,\n processing: 1,\n done: 2,\n failed: 3,\n dead: 4,\n};\n\nexport const OUTBOX_STATUS_FROM_CODE: Record<number, OutboxStatus> = {\n 0: \"pending\",\n 1: \"processing\",\n 2: \"done\",\n 3: \"failed\",\n 4: \"dead\",\n};\n\n/**\n * A message as enqueued by the application inside its own DB transaction.\n * This is the write-side input — no infra concerns leak in here.\n */\nexport interface OutboxMessageInput {\n /** Logical topic the message will be published to. */\n topic: string;\n /** Type of the aggregate that produced this event (e.g. \"order\"). */\n aggregateType: string;\n /**\n * Identifier of the aggregate instance (e.g. order id).\n * Used as the default partition key to preserve per-aggregate ordering.\n */\n aggregateId: string;\n /** Event payload. Serialized by the configured serializer. */\n payload: unknown;\n /** Optional explicit partition key. Falls back to aggregateId. */\n key?: string;\n /** Optional message headers. */\n headers?: Record<string, string>;\n /**\n * Optional client-supplied message id for idempotency / dedup.\n * If omitted, the store generates one.\n */\n messageId?: string;\n}\n\n/**\n * A persisted outbox record as read back by the relay.\n */\nexport interface OutboxRecord {\n id: string;\n messageId: string;\n topic: string;\n aggregateType: string;\n aggregateId: string;\n key: string | null;\n payload: unknown;\n headers: Record<string, string>;\n traceId: string | null;\n status: OutboxStatus;\n attempts: number;\n nextRetryAt: Date | null;\n createdAt: Date;\n processedAt: Date | null;\n}\n\n/**\n * The message handed to a Publisher after serialization.\n */\nexport interface PublishableMessage {\n topic: string;\n key: string | null;\n /** Serialized payload bytes. */\n value: Buffer;\n headers: Record<string, string>;\n /** Original record id, for correlation in publish results. */\n recordId: string;\n messageId: string;\n}\n\n/**\n * Result of attempting to publish a single message.\n */\nexport interface PublishResult {\n recordId: string;\n ok: boolean;\n error?: Error;\n}\n\n/**\n * Pluggable serializer. Default is JSON; users can swap in\n * Avro / Protobuf / Schema-Registry-backed serializers.\n */\nexport interface Serializer {\n serialize(message: OutboxRecord): Buffer | Promise<Buffer>;\n /** Content-type header value advertised for this serializer. */\n readonly contentType: string;\n}\n\n/**\n * Storage abstraction. Implemented per-database (postgres, mysql, ...).\n * The relay only talks to the store through this interface.\n */\nexport interface OutboxStore {\n /**\n * Atomically claim up to `batchSize` due messages and mark them\n * as `processing`. Implementations MUST be safe under concurrent\n * relay instances (e.g. SELECT ... FOR UPDATE SKIP LOCKED).\n */\n claimBatch(batchSize: number): Promise<OutboxRecord[]>;\n\n /** Mark records as successfully published. */\n markDone(recordIds: string[]): Promise<void>;\n\n /**\n * Mark a record as failed and schedule its next retry.\n * `nextRetryAt` of null + status \"dead\" means terminal.\n */\n markFailed(\n recordId: string,\n nextRetryAt: Date | null,\n status: \"failed\" | \"dead\",\n ): Promise<void>;\n\n /** Best-effort lifecycle hooks; no-op allowed. */\n init?(): Promise<void>;\n close?(): Promise<void>;\n}\n\n/**\n * Broker abstraction. Implemented per-driver (kafkajs, confluent, ...).\n */\nexport interface Publisher {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n /**\n * Publish a batch. Returns a per-message result so the relay can\n * mark partial success. Implementations may use a transactional\n * producer to make the batch atomic.\n */\n publish(messages: PublishableMessage[]): Promise<PublishResult[]>;\n /** Route a permanently-failed message to a dead-letter destination. */\n publishToDlq?(message: PublishableMessage, error: Error): Promise<void>;\n}\n\n/**\n * Backoff strategy for retrying failed publishes.\n */\nexport type BackoffStrategy = \"fixed\" | \"linear\" | \"exponential\";\n\nexport interface RetryConfig {\n maxAttempts: number;\n strategy: BackoffStrategy;\n baseMs: number;\n maxMs: number;\n /** Add random jitter (0..baseMs) to avoid thundering herd. Default true. */\n jitter?: boolean;\n}\n\nexport interface DlqConfig {\n /** Topic to route dead messages to. If absent, dead messages are parked. */\n topic?: string;\n}\n\n/**\n * Optional trace-context propagator. `inject` writes the active trace context\n * into the carrier as W3C `traceparent`/`tracestate` (the shape of an\n * OpenTelemetry TextMapPropagator), so it is persisted with the outbox row and\n * carried to the published message. The library depends on no tracing package;\n * provide a thin adapter over yours (OpenTelemetry, Datadog, …).\n */\nexport interface Tracing {\n inject(carrier: Record<string, string>): void;\n}\n\n/**\n * Minimal structured logger. Console-backed default provided.\n */\nexport interface Logger {\n debug(msg: string, meta?: Record<string, unknown>): void;\n info(msg: string, meta?: Record<string, unknown>): void;\n warn(msg: string, meta?: Record<string, unknown>): void;\n error(msg: string, meta?: Record<string, unknown>): void;\n}\n\n/**\n * A low-latency wake source for the relay. Instead of only waking on the poll\n * interval, the relay claims immediately whenever `onWake` fires. The signal is\n * advisory: it may fire spuriously or be missed entirely, and the relay's\n * polling remains the safety net, so implementations need not deduplicate or\n * guarantee delivery. (e.g. a Postgres LISTEN/NOTIFY waker.)\n */\nexport interface Waker {\n start(onWake: () => void): Promise<void>;\n stop(): Promise<void>;\n}\n\n/**\n * Lifecycle / observability hooks emitted by the relay.\n */\nexport interface RelayHooks {\n onBatchClaimed?(count: number): void;\n onPublished?(result: PublishResult): void;\n onFailed?(record: OutboxRecord, error: Error, willRetry: boolean): void;\n onDead?(record: OutboxRecord, error: Error): void;\n onError?(error: Error): void;\n}\n","import type { RetryConfig } from \"./types.js\";\n\n/**\n * Compute the delay (ms) before the next retry attempt.\n *\n * @param attempt 1-based attempt number that just failed.\n */\nexport function computeBackoff(config: RetryConfig, attempt: number): number {\n const { strategy, baseMs, maxMs } = config;\n const jitter = config.jitter ?? true;\n\n let delay: number;\n switch (strategy) {\n case \"fixed\":\n delay = baseMs;\n break;\n case \"linear\":\n delay = baseMs * attempt;\n break;\n case \"exponential\":\n // base * 2^(attempt-1), capped to avoid overflow before clamping\n delay = baseMs * 2 ** Math.min(attempt - 1, 30);\n break;\n }\n\n delay = Math.min(delay, maxMs);\n\n if (jitter) {\n // Full jitter: random in [0, delay]. Decorrelates concurrent relays.\n delay = Math.random() * delay;\n }\n\n return Math.floor(delay);\n}\n\n/**\n * Resolve when (Date) the next retry should occur, or null if the\n * record has exhausted its attempts and should go dead.\n */\nexport function nextRetryAt(\n config: RetryConfig,\n attempts: number,\n now: Date = new Date(),\n): Date | null {\n if (attempts >= config.maxAttempts) return null;\n const delay = computeBackoff(config, attempts);\n return new Date(now.getTime() + delay);\n}\n","import type { Logger, OutboxRecord, Serializer } from \"./types.js\";\n\n/**\n * Default serializer: JSON-encodes the record payload to UTF-8 bytes.\n */\nexport class JsonSerializer implements Serializer {\n readonly contentType = \"application/json\";\n\n serialize(record: OutboxRecord): Buffer {\n return Buffer.from(JSON.stringify(record.payload), \"utf8\");\n }\n}\n\n/**\n * Minimal console-backed logger. Swap in pino/winston by implementing Logger.\n */\nexport class ConsoleLogger implements Logger {\n constructor(private readonly prefix = \"[outbox]\") {}\n\n private fmt(\n write: (line: string, meta?: Record<string, unknown>) => void,\n level: string,\n msg: string,\n meta?: Record<string, unknown>,\n ): void {\n const line = `${this.prefix} ${level} ${msg}`;\n if (meta) {\n write(line, meta);\n } else {\n write(line);\n }\n }\n\n debug(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.debug, \"DEBUG\", msg, meta);\n }\n info(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.info, \"INFO\", msg, meta);\n }\n warn(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.warn, \"WARN\", msg, meta);\n }\n error(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.error, \"ERROR\", msg, meta);\n }\n}\n\n/** Logger that discards everything. Useful for tests. */\nexport class NoopLogger implements Logger {\n debug(): void {}\n info(): void {}\n warn(): void {}\n error(): void {}\n}\n","import type { OutboxRecord, PublishableMessage, Serializer } from \"./types.js\";\n\n/**\n * Turn a persisted outbox record into a broker-ready message: serialize the\n * payload and attach the standard correlation headers. Shared by the polling\n * `Relay` and the streaming relay so both produce byte-identical messages.\n */\nexport async function buildPublishable(\n record: OutboxRecord,\n serializer: Serializer,\n): Promise<PublishableMessage> {\n const value = await serializer.serialize(record);\n const headers: Record<string, string> = {\n ...record.headers,\n \"content-type\": serializer.contentType,\n \"message-id\": record.messageId,\n \"aggregate-type\": record.aggregateType,\n \"aggregate-id\": record.aggregateId,\n };\n if (record.traceId) headers[\"trace-id\"] = record.traceId;\n\n return {\n topic: record.topic,\n key: record.key ?? record.aggregateId,\n value,\n headers,\n recordId: record.id,\n messageId: record.messageId,\n };\n}\n","import { nextRetryAt } from \"./backoff.js\";\nimport { buildPublishable } from \"./publishable.js\";\nimport { ConsoleLogger, JsonSerializer } from \"./serializer.js\";\nimport type {\n DlqConfig,\n Logger,\n OutboxRecord,\n OutboxStore,\n PublishableMessage,\n Publisher,\n RelayHooks,\n RetryConfig,\n Serializer,\n Waker,\n} from \"./types.js\";\n\nexport interface RelayOptions {\n store: OutboxStore;\n publisher: Publisher;\n /** Messages claimed per poll iteration. Default 100. */\n batchSize?: number;\n /** Idle wait (ms) when a poll returns no work. Default 200. */\n pollIntervalMs?: number;\n retry?: Partial<RetryConfig>;\n dlq?: DlqConfig;\n serializer?: Serializer;\n logger?: Logger;\n hooks?: RelayHooks;\n /**\n * Optional low-latency wake source. When provided, the relay claims as soon as\n * the waker signals new work, instead of waiting out `pollIntervalMs`. Polling\n * stays on as a safety net, so set `pollIntervalMs` longer when using a waker.\n */\n waker?: Waker;\n}\n\nconst DEFAULT_RETRY: RetryConfig = {\n maxAttempts: 5,\n strategy: \"exponential\",\n baseMs: 200,\n maxMs: 30_000,\n jitter: true,\n};\n\n/**\n * The Relay drains the outbox store and publishes messages to the broker.\n *\n * It is safe to run multiple Relay instances concurrently against the same\n * store as long as the store's claimBatch uses a lock-free claim strategy\n * (e.g. SELECT ... FOR UPDATE SKIP LOCKED).\n */\nexport class Relay {\n private readonly store: OutboxStore;\n private readonly publisher: Publisher;\n private readonly batchSize: number;\n private readonly pollIntervalMs: number;\n private readonly retry: RetryConfig;\n private readonly dlq: DlqConfig;\n private readonly serializer: Serializer;\n private readonly log: Logger;\n private readonly hooks: RelayHooks;\n private readonly waker: Waker | null;\n\n private running = false;\n private stopping = false;\n private loopPromise: Promise<void> | null = null;\n\n // Interruptible idle wait: `signal()` wakes a pending wait (or marks one\n // pending if none is in flight, so a wake can't be lost between cycles).\n private wakePending = false;\n private wakeResolver: (() => void) | null = null;\n\n constructor(opts: RelayOptions) {\n this.store = opts.store;\n this.publisher = opts.publisher;\n this.batchSize = opts.batchSize ?? 100;\n this.pollIntervalMs = opts.pollIntervalMs ?? 200;\n this.retry = { ...DEFAULT_RETRY, ...opts.retry };\n this.dlq = opts.dlq ?? {};\n this.serializer = opts.serializer ?? new JsonSerializer();\n this.log = opts.logger ?? new ConsoleLogger();\n this.hooks = opts.hooks ?? {};\n this.waker = opts.waker ?? null;\n }\n\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n this.stopping = false;\n\n await this.store.init?.();\n await this.publisher.connect();\n await this.waker?.start(() => this.signal());\n this.log.info(\"relay started\", {\n batchSize: this.batchSize,\n pollIntervalMs: this.pollIntervalMs,\n waker: this.waker !== null,\n });\n\n this.loopPromise = this.loop();\n }\n\n /**\n * Stop accepting new work, finish the in-flight batch, then disconnect.\n */\n async stop(): Promise<void> {\n if (!this.running) return;\n this.stopping = true;\n this.signal(); // break any in-flight idle wait so shutdown is immediate\n this.log.info(\"relay stopping, draining in-flight batch\");\n await this.loopPromise;\n await this.waker?.stop();\n await this.publisher.disconnect();\n await this.store.close?.();\n this.running = false;\n this.log.info(\"relay stopped\");\n }\n\n private async loop(): Promise<void> {\n while (!this.stopping) {\n try {\n const processed = await this.tick();\n if (processed === 0 && !this.stopping) {\n await this.waitForWork(this.pollIntervalMs);\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n this.log.error(\"relay loop error\", { error: error.message });\n this.hooks.onError?.(error);\n await this.waitForWork(this.pollIntervalMs);\n }\n }\n }\n\n /**\n * Run a single claim+publish cycle. Returns number of records processed.\n * Exposed for tests / manual single-shot draining.\n */\n async tick(): Promise<number> {\n const batch = await this.store.claimBatch(this.batchSize);\n if (batch.length === 0) return 0;\n\n this.hooks.onBatchClaimed?.(batch.length);\n this.log.debug(\"batch claimed\", { count: batch.length });\n\n const messages = await this.toPublishable(batch);\n const recordsById = new Map(batch.map((r) => [r.id, r]));\n\n const results = await this.publisher.publish(messages);\n\n const succeeded: string[] = [];\n for (const result of results) {\n const record = recordsById.get(result.recordId);\n if (!record) continue;\n\n if (result.ok) {\n succeeded.push(record.id);\n this.hooks.onPublished?.(result);\n } else {\n await this.handleFailure(\n record,\n result.error ?? new Error(\"unknown publish error\"),\n );\n }\n }\n\n if (succeeded.length > 0) {\n await this.store.markDone(succeeded);\n }\n\n return batch.length;\n }\n\n private async handleFailure(\n record: OutboxRecord,\n error: Error,\n ): Promise<void> {\n const attempts = record.attempts + 1;\n const retryAt = nextRetryAt(this.retry, attempts);\n const willRetry = retryAt !== null;\n\n this.hooks.onFailed?.(record, error, willRetry);\n this.log.warn(\"publish failed\", {\n recordId: record.id,\n attempts,\n willRetry,\n error: error.message,\n });\n\n if (willRetry) {\n await this.store.markFailed(record.id, retryAt, \"failed\");\n return;\n }\n\n // Terminal: route to DLQ if configured, then mark dead.\n if (this.dlq.topic && this.publisher.publishToDlq) {\n try {\n const msg = (await this.toPublishable([record]))[0];\n if (msg) {\n await this.publisher.publishToDlq(\n {\n ...msg,\n topic: this.dlq.topic,\n // Preserve the original destination so the publisher can record\n // it as a header; otherwise it is lost when we overwrite `topic`.\n headers: { ...msg.headers, \"original-topic\": record.topic },\n },\n error,\n );\n }\n } catch (dlqErr) {\n const e = dlqErr instanceof Error ? dlqErr : new Error(String(dlqErr));\n this.log.error(\"DLQ publish failed\", {\n recordId: record.id,\n error: e.message,\n });\n }\n }\n\n await this.store.markFailed(record.id, null, \"dead\");\n this.hooks.onDead?.(record, error);\n }\n\n private async toPublishable(\n records: OutboxRecord[],\n ): Promise<PublishableMessage[]> {\n const out: PublishableMessage[] = [];\n for (const record of records) {\n out.push(await buildPublishable(record, this.serializer));\n }\n return out;\n }\n\n /** Wake a pending idle wait, or mark one pending so the next wait is skipped. */\n private signal(): void {\n this.wakePending = true;\n this.wakeResolver?.();\n }\n\n /**\n * Idle wait that resolves after `ms`, or early when `signal()` fires. A signal\n * raised while no wait is in flight is remembered (wakePending), so a wake that\n * races a claim cycle is never lost.\n */\n private waitForWork(ms: number): Promise<void> {\n if (this.wakePending) {\n this.wakePending = false;\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n this.wakeResolver = null;\n resolve();\n }, ms);\n this.wakeResolver = () => {\n clearTimeout(timer);\n this.wakeResolver = null;\n this.wakePending = false;\n resolve();\n };\n });\n }\n}\n","import type { StandardSchemaV1 } from \"./standard-schema.js\";\n\n/**\n * Thrown when an outbox payload fails its topic's schema — at `enqueue`\n * (before the DB insert) or at `decode` (malformed bytes / schema mismatch).\n * Carries the topic and the validator's structured issues for diagnostics.\n */\nexport class OutboxValidationError extends Error {\n readonly topic: string;\n readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;\n\n constructor(\n topic: string,\n issues: ReadonlyArray<StandardSchemaV1.Issue>,\n options?: { cause?: unknown },\n ) {\n const first = issues[0]?.message ?? \"unknown validation error\";\n super(`Outbox payload for \"${topic}\" failed validation: ${first}`, options);\n this.name = \"OutboxValidationError\";\n this.topic = topic;\n this.issues = issues;\n }\n}\n","import { OutboxValidationError } from \"./errors.js\";\nimport type { StandardSchemaV1 } from \"./standard-schema.js\";\nimport type { OutboxMessageInput } from \"./types.js\";\n\n/** One topic's contract: the aggregate it belongs to and its payload schema. */\nexport interface TopicDefinition {\n /** Aggregate type stamped on every event of this topic (e.g. \"order\"). */\n readonly aggregateType: string;\n /** Standard Schema for the payload (Zod 3.24+/Valibot/ArkType/…). */\n readonly schema: StandardSchemaV1;\n}\n\n/** A map of topic name -> its definition. The single source of truth. */\nexport type OutboxRegistry = Record<string, TopicDefinition>;\n\n/**\n * Minimal store surface the producer facade needs. `PostgresStore` satisfies\n * this structurally, so the facade stays DB-agnostic and `tx` flows from the\n * concrete store (e.g. a pg client).\n */\nexport interface EnqueueableStore<Tx = unknown> {\n enqueue(tx: Tx, msg: OutboxMessageInput & { traceId?: string }): Promise<string>;\n}\n\ntype TxOf<S> = S extends EnqueueableStore<infer Tx> ? Tx : never;\n\ntype PayloadInput<R extends OutboxRegistry, K extends keyof R> =\n StandardSchemaV1.InferInput<R[K][\"schema\"]>;\ntype PayloadOutput<R extends OutboxRegistry, K extends keyof R> =\n StandardSchemaV1.InferOutput<R[K][\"schema\"]>;\n\n/** The write-side input for a typed enqueue (payload is the schema's input type). */\nexport interface EnqueueInput<R extends OutboxRegistry, K extends keyof R> {\n aggregateId: string;\n payload: PayloadInput<R, K>;\n key?: string;\n headers?: Record<string, string>;\n messageId?: string;\n traceId?: string;\n}\n\n/** Consumer-side facade: validate/decode without a store. */\nexport interface OutboxConsumer<R extends OutboxRegistry> {\n /** Validate an already-parsed value against the topic's schema. */\n validate<K extends keyof R & string>(\n topic: K,\n value: unknown,\n ): Promise<PayloadOutput<R, K>>;\n /** JSON-parse `bytes`, validate against the topic's schema, return the payload. */\n decode<K extends keyof R & string>(\n topic: K,\n bytes: Buffer | Uint8Array | string,\n ): Promise<PayloadOutput<R, K>>;\n}\n\n/** Producer-side facade: adds typed, validated enqueue. */\nexport interface OutboxProducer<R extends OutboxRegistry, Tx>\n extends OutboxConsumer<R> {\n /** Validate `payload`, then enqueue inside the caller's transaction `tx`. */\n enqueue<K extends keyof R & string>(\n tx: Tx,\n topic: K,\n msg: EnqueueInput<R, K>,\n ): Promise<string>;\n}\n\n// Loose shape used inside the implementation; the public types come from overloads.\ninterface LooseEnqueueInput {\n aggregateId: string;\n payload: unknown;\n key?: string;\n headers?: Record<string, string>;\n messageId?: string;\n traceId?: string;\n}\n\nexport function defineOutbox<R extends OutboxRegistry>(\n registry: R,\n): OutboxConsumer<R>;\nexport function defineOutbox<R extends OutboxRegistry, S extends EnqueueableStore>(\n registry: R,\n opts: { store: S },\n): OutboxProducer<R, TxOf<S>>;\nexport function defineOutbox<R extends OutboxRegistry>(\n registry: R,\n opts?: { store: EnqueueableStore },\n): OutboxConsumer<R> | OutboxProducer<R, unknown> {\n const validate = async (topic: string, value: unknown): Promise<unknown> => {\n const def = registry[topic];\n if (!def) {\n throw new OutboxValidationError(topic, [\n { message: `unknown topic \"${topic}\"` },\n ]);\n }\n const result = await def.schema[\"~standard\"].validate(value);\n if (result.issues) throw new OutboxValidationError(topic, result.issues);\n return result.value;\n };\n\n const decode = async (\n topic: string,\n bytes: Buffer | Uint8Array | string,\n ): Promise<unknown> => {\n const text =\n typeof bytes === \"string\" ? bytes : Buffer.from(bytes).toString(\"utf8\");\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err) {\n throw new OutboxValidationError(\n topic,\n [{ message: `invalid JSON: ${(err as Error).message}` }],\n { cause: err },\n );\n }\n return validate(topic, parsed);\n };\n\n const consumer = { validate, decode } as unknown as OutboxConsumer<R>;\n if (!opts?.store) return consumer;\n\n const store = opts.store;\n const enqueue = async (\n tx: unknown,\n topic: string,\n msg: LooseEnqueueInput,\n ): Promise<string> => {\n const def = registry[topic];\n if (!def) {\n throw new OutboxValidationError(topic, [\n { message: `unknown topic \"${topic}\"` },\n ]);\n }\n const payload = await validate(topic, msg.payload);\n return store.enqueue(tx, {\n topic,\n aggregateType: def.aggregateType,\n aggregateId: msg.aggregateId,\n payload,\n key: msg.key,\n headers: msg.headers,\n messageId: msg.messageId,\n traceId: msg.traceId,\n });\n };\n\n return { validate, decode, enqueue } as unknown as OutboxProducer<R, unknown>;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/backoff.ts","../src/serializer.ts","../src/publishable.ts","../src/relay.ts","../src/errors.ts","../src/registry.ts"],"sourcesContent":["export * from \"./types.js\";\nexport * from \"./backoff.js\";\nexport * from \"./serializer.js\";\nexport * from \"./publishable.js\";\nexport * from \"./relay.js\";\nexport * from \"./standard-schema.js\";\nexport * from \"./errors.js\";\nexport * from \"./registry.js\";\n","/**\n * Status lifecycle of an outbox record.\n *\n * pending -> freshly enqueued, awaiting first publish\n * processing -> claimed by a relay instance, publish in flight\n * done -> successfully published to the broker\n * failed -> publish failed, awaiting retry (next_retry_at)\n * dead -> exhausted retries, routed to DLQ (or parked)\n */\nexport type OutboxStatus = \"pending\" | \"processing\" | \"done\" | \"failed\" | \"dead\";\n\nexport const OUTBOX_STATUS_CODE: Record<OutboxStatus, number> = {\n pending: 0,\n processing: 1,\n done: 2,\n failed: 3,\n dead: 4,\n};\n\nexport const OUTBOX_STATUS_FROM_CODE: Record<number, OutboxStatus> = {\n 0: \"pending\",\n 1: \"processing\",\n 2: \"done\",\n 3: \"failed\",\n 4: \"dead\",\n};\n\n/**\n * A message as enqueued by the application inside its own DB transaction.\n * This is the write-side input — no infra concerns leak in here.\n */\nexport interface OutboxMessageInput {\n /** Logical topic the message will be published to. */\n topic: string;\n /** Type of the aggregate that produced this event (e.g. \"order\"). */\n aggregateType: string;\n /**\n * Identifier of the aggregate instance (e.g. order id).\n * Used as the default partition key to preserve per-aggregate ordering.\n */\n aggregateId: string;\n /** Event payload. Serialized by the configured serializer. */\n payload: unknown;\n /** Optional explicit partition key. Falls back to aggregateId. */\n key?: string;\n /** Optional message headers. */\n headers?: Record<string, string>;\n /**\n * Optional client-supplied message id for idempotency / dedup.\n * If omitted, the store generates one.\n */\n messageId?: string;\n}\n\n/**\n * A persisted outbox record as read back by the relay.\n */\nexport interface OutboxRecord {\n id: string;\n messageId: string;\n topic: string;\n aggregateType: string;\n aggregateId: string;\n key: string | null;\n payload: unknown;\n headers: Record<string, string>;\n traceId: string | null;\n status: OutboxStatus;\n attempts: number;\n nextRetryAt: Date | null;\n createdAt: Date;\n processedAt: Date | null;\n}\n\n/**\n * The message handed to a Publisher after serialization.\n */\nexport interface PublishableMessage {\n topic: string;\n key: string | null;\n /** Serialized payload bytes. */\n value: Buffer;\n headers: Record<string, string>;\n /** Original record id, for correlation in publish results. */\n recordId: string;\n messageId: string;\n /**\n * Explicit partition override. When set, the publisher MUST route the\n * record to this exact partition, bypassing the configured partitioner.\n *\n * Use cases:\n * - Compacted topics with application-managed sharding.\n * - Tenant-affinity routing where you compute the partition yourself.\n * - Geo-pinning records to a specific broker.\n *\n * When omitted (the default), the underlying client's partitioner\n * decides — usually a hash of `key`, falling back to sticky round-robin\n * when `key` is null.\n */\n partition?: number;\n}\n\n/**\n * Why a publish failed, in terms the relay can act on. Drivers classify their\n * native errors into one of these buckets; the relay reads `errorKind` to\n * decide whether to retry, short-circuit to the DLQ, or pause polling. The\n * field is optional for backward compatibility — when absent, the relay\n * treats the error as `\"retriable\"`.\n *\n * - `retriable` — transient (broker unreachable, leader election, request\n * timeout); retry per the configured backoff policy. The default for any\n * unclassified error.\n * - `fatal` — the producer or the credentials are broken (fenced epoch,\n * authentication failed, ACL denied). Retrying cannot help; the relay\n * short-circuits straight to the DLQ + `dead` status.\n * - `poison` — the message itself is rejectable by every broker\n * (oversized record, corrupt payload, schema-registry refused encoding).\n * Same handling as `fatal`: DLQ + dead, no retries.\n * - `backpressure` — the *producer's own* outbound buffer is full\n * (librdkafka `__QUEUE_FULL`). The right response is to slow the relay\n * down, not to burn retries. v2.1 treats this as `retriable` for\n * compatibility; smarter handling (pause polling) is planned.\n * - `quota` — the broker is throttling us (`THROTTLING_QUOTA_EXCEEDED`).\n * Back off with longer delays. v2.1 treats as `retriable`; smarter\n * handling (longer backoff) is planned.\n */\nexport type PublishErrorKind =\n | \"retriable\"\n | \"fatal\"\n | \"poison\"\n | \"backpressure\"\n | \"quota\";\n\n/**\n * Result of attempting to publish a single message.\n */\nexport interface PublishResult {\n recordId: string;\n ok: boolean;\n error?: Error;\n /**\n * Optional classification of `error` for relay-level decision-making. Set\n * by publisher implementations that know how to inspect their native error\n * shapes. Absent value is treated as `\"retriable\"` by the relay (the safe\n * default — at worst we retry an error we should have skipped).\n */\n errorKind?: PublishErrorKind;\n}\n\n/**\n * Pluggable serializer. Default is JSON; users can swap in\n * Avro / Protobuf / Schema-Registry-backed serializers.\n */\nexport interface Serializer {\n serialize(message: OutboxRecord): Buffer | Promise<Buffer>;\n /** Content-type header value advertised for this serializer. */\n readonly contentType: string;\n}\n\n/**\n * Storage abstraction. Implemented per-database (postgres, mysql, ...).\n * The relay only talks to the store through this interface.\n */\nexport interface OutboxStore {\n /**\n * Atomically claim up to `batchSize` due messages and mark them\n * as `processing`. Implementations MUST be safe under concurrent\n * relay instances (e.g. SELECT ... FOR UPDATE SKIP LOCKED).\n */\n claimBatch(batchSize: number): Promise<OutboxRecord[]>;\n\n /** Mark records as successfully published. */\n markDone(recordIds: string[]): Promise<void>;\n\n /**\n * Mark a record as failed and schedule its next retry.\n * `nextRetryAt` of null + status \"dead\" means terminal.\n */\n markFailed(\n recordId: string,\n nextRetryAt: Date | null,\n status: \"failed\" | \"dead\",\n ): Promise<void>;\n\n /** Best-effort lifecycle hooks; no-op allowed. */\n init?(): Promise<void>;\n close?(): Promise<void>;\n}\n\n/**\n * Broker abstraction. Implemented per-driver (kafkajs, confluent, ...).\n */\nexport interface Publisher {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n /**\n * Publish a batch. Returns a per-message result so the relay can\n * mark partial success. Implementations may use a transactional\n * producer to make the batch atomic.\n */\n publish(messages: PublishableMessage[]): Promise<PublishResult[]>;\n /** Route a permanently-failed message to a dead-letter destination. */\n publishToDlq?(message: PublishableMessage, error: Error): Promise<void>;\n}\n\n/**\n * Backoff strategy for retrying failed publishes.\n */\nexport type BackoffStrategy = \"fixed\" | \"linear\" | \"exponential\";\n\nexport interface RetryConfig {\n maxAttempts: number;\n strategy: BackoffStrategy;\n baseMs: number;\n maxMs: number;\n /** Add random jitter (0..baseMs) to avoid thundering herd. Default true. */\n jitter?: boolean;\n}\n\nexport interface DlqConfig {\n /** Topic to route dead messages to. If absent, dead messages are parked. */\n topic?: string;\n}\n\n/**\n * Optional trace-context propagator. `inject` writes the active trace context\n * into the carrier as W3C `traceparent`/`tracestate` (the shape of an\n * OpenTelemetry TextMapPropagator), so it is persisted with the outbox row and\n * carried to the published message. The library depends on no tracing package;\n * provide a thin adapter over yours (OpenTelemetry, Datadog, …).\n */\nexport interface Tracing {\n inject(carrier: Record<string, string>): void;\n}\n\n/**\n * Minimal structured logger. Console-backed default provided.\n */\nexport interface Logger {\n debug(msg: string, meta?: Record<string, unknown>): void;\n info(msg: string, meta?: Record<string, unknown>): void;\n warn(msg: string, meta?: Record<string, unknown>): void;\n error(msg: string, meta?: Record<string, unknown>): void;\n}\n\n/**\n * A low-latency wake source for the relay. Instead of only waking on the poll\n * interval, the relay claims immediately whenever `onWake` fires. The signal is\n * advisory: it may fire spuriously or be missed entirely, and the relay's\n * polling remains the safety net, so implementations need not deduplicate or\n * guarantee delivery. (e.g. a Postgres LISTEN/NOTIFY waker.)\n */\nexport interface Waker {\n start(onWake: () => void): Promise<void>;\n stop(): Promise<void>;\n}\n\n/**\n * Lifecycle / observability hooks emitted by the relay.\n */\nexport interface RelayHooks {\n onBatchClaimed?(count: number): void;\n onPublished?(result: PublishResult): void;\n onFailed?(record: OutboxRecord, error: Error, willRetry: boolean): void;\n onDead?(record: OutboxRecord, error: Error): void;\n onError?(error: Error): void;\n}\n","import type { RetryConfig } from \"./types.js\";\n\n/**\n * Compute the delay (ms) before the next retry attempt.\n *\n * @param attempt 1-based attempt number that just failed.\n */\nexport function computeBackoff(config: RetryConfig, attempt: number): number {\n const { strategy, baseMs, maxMs } = config;\n const jitter = config.jitter ?? true;\n\n let delay: number;\n switch (strategy) {\n case \"fixed\":\n delay = baseMs;\n break;\n case \"linear\":\n delay = baseMs * attempt;\n break;\n case \"exponential\":\n // base * 2^(attempt-1), capped to avoid overflow before clamping\n delay = baseMs * 2 ** Math.min(attempt - 1, 30);\n break;\n }\n\n delay = Math.min(delay, maxMs);\n\n if (jitter) {\n // Full jitter: random in [0, delay]. Decorrelates concurrent relays.\n delay = Math.random() * delay;\n }\n\n return Math.floor(delay);\n}\n\n/**\n * Resolve when (Date) the next retry should occur, or null if the\n * record has exhausted its attempts and should go dead.\n */\nexport function nextRetryAt(\n config: RetryConfig,\n attempts: number,\n now: Date = new Date(),\n): Date | null {\n if (attempts >= config.maxAttempts) return null;\n const delay = computeBackoff(config, attempts);\n return new Date(now.getTime() + delay);\n}\n","import type { Logger, OutboxRecord, Serializer } from \"./types.js\";\n\n/**\n * Default serializer: JSON-encodes the record payload to UTF-8 bytes.\n */\nexport class JsonSerializer implements Serializer {\n readonly contentType = \"application/json\";\n\n serialize(record: OutboxRecord): Buffer {\n return Buffer.from(JSON.stringify(record.payload), \"utf8\");\n }\n}\n\n/**\n * Minimal console-backed logger. Swap in pino/winston by implementing Logger.\n */\nexport class ConsoleLogger implements Logger {\n constructor(private readonly prefix = \"[outbox]\") {}\n\n private fmt(\n write: (line: string, meta?: Record<string, unknown>) => void,\n level: string,\n msg: string,\n meta?: Record<string, unknown>,\n ): void {\n const line = `${this.prefix} ${level} ${msg}`;\n if (meta) {\n write(line, meta);\n } else {\n write(line);\n }\n }\n\n debug(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.debug, \"DEBUG\", msg, meta);\n }\n info(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.info, \"INFO\", msg, meta);\n }\n warn(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.warn, \"WARN\", msg, meta);\n }\n error(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.error, \"ERROR\", msg, meta);\n }\n}\n\n/** Logger that discards everything. Useful for tests. */\nexport class NoopLogger implements Logger {\n debug(): void {}\n info(): void {}\n warn(): void {}\n error(): void {}\n}\n","import type { OutboxRecord, PublishableMessage, Serializer } from \"./types.js\";\n\n/**\n * Turn a persisted outbox record into a broker-ready message: serialize the\n * payload and attach the standard correlation headers. Shared by the polling\n * `Relay` and the streaming relay so both produce byte-identical messages.\n */\nexport async function buildPublishable(\n record: OutboxRecord,\n serializer: Serializer,\n): Promise<PublishableMessage> {\n const value = await serializer.serialize(record);\n const headers: Record<string, string> = {\n ...record.headers,\n \"content-type\": serializer.contentType,\n \"message-id\": record.messageId,\n \"aggregate-type\": record.aggregateType,\n \"aggregate-id\": record.aggregateId,\n };\n if (record.traceId) headers[\"trace-id\"] = record.traceId;\n\n return {\n topic: record.topic,\n key: record.key ?? record.aggregateId,\n value,\n headers,\n recordId: record.id,\n messageId: record.messageId,\n };\n}\n","import { nextRetryAt } from \"./backoff.js\";\nimport { buildPublishable } from \"./publishable.js\";\nimport { ConsoleLogger, JsonSerializer } from \"./serializer.js\";\nimport type {\n DlqConfig,\n Logger,\n OutboxRecord,\n OutboxStore,\n PublishableMessage,\n PublishErrorKind,\n Publisher,\n RelayHooks,\n RetryConfig,\n Serializer,\n Waker,\n} from \"./types.js\";\n\nexport interface RelayOptions {\n store: OutboxStore;\n publisher: Publisher;\n /** Messages claimed per poll iteration. Default 100. */\n batchSize?: number;\n /** Idle wait (ms) when a poll returns no work. Default 200. */\n pollIntervalMs?: number;\n retry?: Partial<RetryConfig>;\n dlq?: DlqConfig;\n serializer?: Serializer;\n logger?: Logger;\n hooks?: RelayHooks;\n /**\n * Optional low-latency wake source. When provided, the relay claims as soon as\n * the waker signals new work, instead of waiting out `pollIntervalMs`. Polling\n * stays on as a safety net, so set `pollIntervalMs` longer when using a waker.\n */\n waker?: Waker;\n}\n\nconst DEFAULT_RETRY: RetryConfig = {\n maxAttempts: 5,\n strategy: \"exponential\",\n baseMs: 200,\n maxMs: 30_000,\n jitter: true,\n};\n\n/**\n * The Relay drains the outbox store and publishes messages to the broker.\n *\n * It is safe to run multiple Relay instances concurrently against the same\n * store as long as the store's claimBatch uses a lock-free claim strategy\n * (e.g. SELECT ... FOR UPDATE SKIP LOCKED).\n */\nexport class Relay {\n private readonly store: OutboxStore;\n private readonly publisher: Publisher;\n private readonly batchSize: number;\n private readonly pollIntervalMs: number;\n private readonly retry: RetryConfig;\n private readonly dlq: DlqConfig;\n private readonly serializer: Serializer;\n private readonly log: Logger;\n private readonly hooks: RelayHooks;\n private readonly waker: Waker | null;\n\n private running = false;\n private stopping = false;\n private loopPromise: Promise<void> | null = null;\n\n // Interruptible idle wait: `signal()` wakes a pending wait (or marks one\n // pending if none is in flight, so a wake can't be lost between cycles).\n private wakePending = false;\n private wakeResolver: (() => void) | null = null;\n\n constructor(opts: RelayOptions) {\n this.store = opts.store;\n this.publisher = opts.publisher;\n this.batchSize = opts.batchSize ?? 100;\n this.pollIntervalMs = opts.pollIntervalMs ?? 200;\n this.retry = { ...DEFAULT_RETRY, ...opts.retry };\n this.dlq = opts.dlq ?? {};\n this.serializer = opts.serializer ?? new JsonSerializer();\n this.log = opts.logger ?? new ConsoleLogger();\n this.hooks = opts.hooks ?? {};\n this.waker = opts.waker ?? null;\n }\n\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n this.stopping = false;\n\n await this.store.init?.();\n await this.publisher.connect();\n await this.waker?.start(() => this.signal());\n this.log.info(\"relay started\", {\n batchSize: this.batchSize,\n pollIntervalMs: this.pollIntervalMs,\n waker: this.waker !== null,\n });\n\n this.loopPromise = this.loop();\n }\n\n /**\n * Stop accepting new work, finish the in-flight batch, then disconnect.\n */\n async stop(): Promise<void> {\n if (!this.running) return;\n this.stopping = true;\n this.signal(); // break any in-flight idle wait so shutdown is immediate\n this.log.info(\"relay stopping, draining in-flight batch\");\n await this.loopPromise;\n await this.waker?.stop();\n await this.publisher.disconnect();\n await this.store.close?.();\n this.running = false;\n this.log.info(\"relay stopped\");\n }\n\n private async loop(): Promise<void> {\n while (!this.stopping) {\n try {\n const processed = await this.tick();\n if (processed === 0 && !this.stopping) {\n await this.waitForWork(this.pollIntervalMs);\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n this.log.error(\"relay loop error\", { error: error.message });\n this.hooks.onError?.(error);\n await this.waitForWork(this.pollIntervalMs);\n }\n }\n }\n\n /**\n * Run a single claim+publish cycle. Returns number of records processed.\n * Exposed for tests / manual single-shot draining.\n */\n async tick(): Promise<number> {\n const batch = await this.store.claimBatch(this.batchSize);\n if (batch.length === 0) return 0;\n\n this.hooks.onBatchClaimed?.(batch.length);\n this.log.debug(\"batch claimed\", { count: batch.length });\n\n const messages = await this.toPublishable(batch);\n const recordsById = new Map(batch.map((r) => [r.id, r]));\n\n const results = await this.publisher.publish(messages);\n\n const succeeded: string[] = [];\n for (const result of results) {\n const record = recordsById.get(result.recordId);\n if (!record) continue;\n\n if (result.ok) {\n succeeded.push(record.id);\n this.hooks.onPublished?.(result);\n } else {\n await this.handleFailure(\n record,\n result.error ?? new Error(\"unknown publish error\"),\n result.errorKind,\n );\n }\n }\n\n if (succeeded.length > 0) {\n await this.store.markDone(succeeded);\n }\n\n return batch.length;\n }\n\n private async handleFailure(\n record: OutboxRecord,\n error: Error,\n errorKind?: PublishErrorKind,\n ): Promise<void> {\n const attempts = record.attempts + 1;\n // fatal/poison short-circuit: retrying cannot help (auth denied, fenced\n // epoch, oversized record, schema rejected). Skip the backoff schedule\n // entirely and go straight to DLQ + dead.\n const isTerminalKind = errorKind === \"fatal\" || errorKind === \"poison\";\n const retryAt = isTerminalKind ? null : nextRetryAt(this.retry, attempts);\n const willRetry = retryAt !== null;\n\n this.hooks.onFailed?.(record, error, willRetry);\n this.log.warn(\"publish failed\", {\n recordId: record.id,\n attempts,\n willRetry,\n errorKind: errorKind ?? \"retriable\",\n error: error.message,\n });\n\n if (willRetry) {\n await this.store.markFailed(record.id, retryAt, \"failed\");\n return;\n }\n\n // Terminal: route to DLQ if configured, then mark dead.\n if (this.dlq.topic && this.publisher.publishToDlq) {\n try {\n const msg = (await this.toPublishable([record]))[0];\n if (msg) {\n await this.publisher.publishToDlq(\n {\n ...msg,\n topic: this.dlq.topic,\n // Preserve the original destination so the publisher can record\n // it as a header; otherwise it is lost when we overwrite `topic`.\n headers: { ...msg.headers, \"original-topic\": record.topic },\n },\n error,\n );\n }\n } catch (dlqErr) {\n const e = dlqErr instanceof Error ? dlqErr : new Error(String(dlqErr));\n this.log.error(\"DLQ publish failed\", {\n recordId: record.id,\n error: e.message,\n });\n }\n }\n\n await this.store.markFailed(record.id, null, \"dead\");\n this.hooks.onDead?.(record, error);\n }\n\n private async toPublishable(\n records: OutboxRecord[],\n ): Promise<PublishableMessage[]> {\n const out: PublishableMessage[] = [];\n for (const record of records) {\n out.push(await buildPublishable(record, this.serializer));\n }\n return out;\n }\n\n /** Wake a pending idle wait, or mark one pending so the next wait is skipped. */\n private signal(): void {\n this.wakePending = true;\n this.wakeResolver?.();\n }\n\n /**\n * Idle wait that resolves after `ms`, or early when `signal()` fires. A signal\n * raised while no wait is in flight is remembered (wakePending), so a wake that\n * races a claim cycle is never lost.\n */\n private waitForWork(ms: number): Promise<void> {\n if (this.wakePending) {\n this.wakePending = false;\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n this.wakeResolver = null;\n resolve();\n }, ms);\n this.wakeResolver = () => {\n clearTimeout(timer);\n this.wakeResolver = null;\n this.wakePending = false;\n resolve();\n };\n });\n }\n}\n","import type { StandardSchemaV1 } from \"./standard-schema.js\";\n\n/**\n * Thrown when an outbox payload fails its topic's schema — at `enqueue`\n * (before the DB insert) or at `decode` (malformed bytes / schema mismatch).\n * Carries the topic and the validator's structured issues for diagnostics.\n */\nexport class OutboxValidationError extends Error {\n readonly topic: string;\n readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;\n\n constructor(\n topic: string,\n issues: ReadonlyArray<StandardSchemaV1.Issue>,\n options?: { cause?: unknown },\n ) {\n const first = issues[0]?.message ?? \"unknown validation error\";\n super(`Outbox payload for \"${topic}\" failed validation: ${first}`, options);\n this.name = \"OutboxValidationError\";\n this.topic = topic;\n this.issues = issues;\n }\n}\n","import { OutboxValidationError } from \"./errors.js\";\nimport type { StandardSchemaV1 } from \"./standard-schema.js\";\nimport type { OutboxMessageInput } from \"./types.js\";\n\n/** One topic's contract: the aggregate it belongs to and its payload schema. */\nexport interface TopicDefinition {\n /** Aggregate type stamped on every event of this topic (e.g. \"order\"). */\n readonly aggregateType: string;\n /** Standard Schema for the payload (Zod 3.24+/Valibot/ArkType/…). */\n readonly schema: StandardSchemaV1;\n}\n\n/** A map of topic name -> its definition. The single source of truth. */\nexport type OutboxRegistry = Record<string, TopicDefinition>;\n\n/**\n * Minimal store surface the producer facade needs. `PostgresStore` satisfies\n * this structurally, so the facade stays DB-agnostic and `tx` flows from the\n * concrete store (e.g. a pg client).\n */\nexport interface EnqueueableStore<Tx = unknown> {\n enqueue(tx: Tx, msg: OutboxMessageInput & { traceId?: string }): Promise<string>;\n}\n\ntype TxOf<S> = S extends EnqueueableStore<infer Tx> ? Tx : never;\n\ntype PayloadInput<R extends OutboxRegistry, K extends keyof R> =\n StandardSchemaV1.InferInput<R[K][\"schema\"]>;\ntype PayloadOutput<R extends OutboxRegistry, K extends keyof R> =\n StandardSchemaV1.InferOutput<R[K][\"schema\"]>;\n\n/** The write-side input for a typed enqueue (payload is the schema's input type). */\nexport interface EnqueueInput<R extends OutboxRegistry, K extends keyof R> {\n aggregateId: string;\n payload: PayloadInput<R, K>;\n key?: string;\n headers?: Record<string, string>;\n messageId?: string;\n traceId?: string;\n}\n\n/** Consumer-side facade: validate/decode without a store. */\nexport interface OutboxConsumer<R extends OutboxRegistry> {\n /** Validate an already-parsed value against the topic's schema. */\n validate<K extends keyof R & string>(\n topic: K,\n value: unknown,\n ): Promise<PayloadOutput<R, K>>;\n /** JSON-parse `bytes`, validate against the topic's schema, return the payload. */\n decode<K extends keyof R & string>(\n topic: K,\n bytes: Buffer | Uint8Array | string,\n ): Promise<PayloadOutput<R, K>>;\n}\n\n/** Producer-side facade: adds typed, validated enqueue. */\nexport interface OutboxProducer<R extends OutboxRegistry, Tx>\n extends OutboxConsumer<R> {\n /** Validate `payload`, then enqueue inside the caller's transaction `tx`. */\n enqueue<K extends keyof R & string>(\n tx: Tx,\n topic: K,\n msg: EnqueueInput<R, K>,\n ): Promise<string>;\n}\n\n// Loose shape used inside the implementation; the public types come from overloads.\ninterface LooseEnqueueInput {\n aggregateId: string;\n payload: unknown;\n key?: string;\n headers?: Record<string, string>;\n messageId?: string;\n traceId?: string;\n}\n\nexport function defineOutbox<R extends OutboxRegistry>(\n registry: R,\n): OutboxConsumer<R>;\nexport function defineOutbox<R extends OutboxRegistry, S extends EnqueueableStore>(\n registry: R,\n opts: { store: S },\n): OutboxProducer<R, TxOf<S>>;\nexport function defineOutbox<R extends OutboxRegistry>(\n registry: R,\n opts?: { store: EnqueueableStore },\n): OutboxConsumer<R> | OutboxProducer<R, unknown> {\n const validate = async (topic: string, value: unknown): Promise<unknown> => {\n const def = registry[topic];\n if (!def) {\n throw new OutboxValidationError(topic, [\n { message: `unknown topic \"${topic}\"` },\n ]);\n }\n const result = await def.schema[\"~standard\"].validate(value);\n if (result.issues) throw new OutboxValidationError(topic, result.issues);\n return result.value;\n };\n\n const decode = async (\n topic: string,\n bytes: Buffer | Uint8Array | string,\n ): Promise<unknown> => {\n const text =\n typeof bytes === \"string\" ? bytes : Buffer.from(bytes).toString(\"utf8\");\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err) {\n throw new OutboxValidationError(\n topic,\n [{ message: `invalid JSON: ${(err as Error).message}` }],\n { cause: err },\n );\n }\n return validate(topic, parsed);\n };\n\n const consumer = { validate, decode } as unknown as OutboxConsumer<R>;\n if (!opts?.store) return consumer;\n\n const store = opts.store;\n const enqueue = async (\n tx: unknown,\n topic: string,\n msg: LooseEnqueueInput,\n ): Promise<string> => {\n const def = registry[topic];\n if (!def) {\n throw new OutboxValidationError(topic, [\n { message: `unknown topic \"${topic}\"` },\n ]);\n }\n const payload = await validate(topic, msg.payload);\n return store.enqueue(tx, {\n topic,\n aggregateType: def.aggregateType,\n aggregateId: msg.aggregateId,\n payload,\n key: msg.key,\n headers: msg.headers,\n messageId: msg.messageId,\n traceId: msg.traceId,\n });\n };\n\n return { validate, decode, enqueue } as unknown as OutboxProducer<R, unknown>;\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -67,7 +67,46 @@ interface PublishableMessage {
67
67
  /** Original record id, for correlation in publish results. */
68
68
  recordId: string;
69
69
  messageId: string;
70
+ /**
71
+ * Explicit partition override. When set, the publisher MUST route the
72
+ * record to this exact partition, bypassing the configured partitioner.
73
+ *
74
+ * Use cases:
75
+ * - Compacted topics with application-managed sharding.
76
+ * - Tenant-affinity routing where you compute the partition yourself.
77
+ * - Geo-pinning records to a specific broker.
78
+ *
79
+ * When omitted (the default), the underlying client's partitioner
80
+ * decides — usually a hash of `key`, falling back to sticky round-robin
81
+ * when `key` is null.
82
+ */
83
+ partition?: number;
70
84
  }
85
+ /**
86
+ * Why a publish failed, in terms the relay can act on. Drivers classify their
87
+ * native errors into one of these buckets; the relay reads `errorKind` to
88
+ * decide whether to retry, short-circuit to the DLQ, or pause polling. The
89
+ * field is optional for backward compatibility — when absent, the relay
90
+ * treats the error as `"retriable"`.
91
+ *
92
+ * - `retriable` — transient (broker unreachable, leader election, request
93
+ * timeout); retry per the configured backoff policy. The default for any
94
+ * unclassified error.
95
+ * - `fatal` — the producer or the credentials are broken (fenced epoch,
96
+ * authentication failed, ACL denied). Retrying cannot help; the relay
97
+ * short-circuits straight to the DLQ + `dead` status.
98
+ * - `poison` — the message itself is rejectable by every broker
99
+ * (oversized record, corrupt payload, schema-registry refused encoding).
100
+ * Same handling as `fatal`: DLQ + dead, no retries.
101
+ * - `backpressure` — the *producer's own* outbound buffer is full
102
+ * (librdkafka `__QUEUE_FULL`). The right response is to slow the relay
103
+ * down, not to burn retries. v2.1 treats this as `retriable` for
104
+ * compatibility; smarter handling (pause polling) is planned.
105
+ * - `quota` — the broker is throttling us (`THROTTLING_QUOTA_EXCEEDED`).
106
+ * Back off with longer delays. v2.1 treats as `retriable`; smarter
107
+ * handling (longer backoff) is planned.
108
+ */
109
+ type PublishErrorKind = "retriable" | "fatal" | "poison" | "backpressure" | "quota";
71
110
  /**
72
111
  * Result of attempting to publish a single message.
73
112
  */
@@ -75,6 +114,13 @@ interface PublishResult {
75
114
  recordId: string;
76
115
  ok: boolean;
77
116
  error?: Error;
117
+ /**
118
+ * Optional classification of `error` for relay-level decision-making. Set
119
+ * by publisher implementations that know how to inspect their native error
120
+ * shapes. Absent value is treated as `"retriable"` by the relay (the safe
121
+ * default — at worst we retry an error we should have skipped).
122
+ */
123
+ errorKind?: PublishErrorKind;
78
124
  }
79
125
  /**
80
126
  * Pluggable serializer. Default is JSON; users can swap in
@@ -395,4 +441,4 @@ declare function defineOutbox<R extends OutboxRegistry, S extends EnqueueableSto
395
441
  store: S;
396
442
  }): OutboxProducer<R, TxOf<S>>;
397
443
 
398
- export { type BackoffStrategy, ConsoleLogger, type DlqConfig, type EnqueueInput, type EnqueueableStore, JsonSerializer, type Logger, NoopLogger, OUTBOX_STATUS_CODE, OUTBOX_STATUS_FROM_CODE, type OutboxConsumer, type OutboxMessageInput, type OutboxProducer, type OutboxRecord, type OutboxRegistry, type OutboxStatus, type OutboxStore, OutboxValidationError, type PublishResult, type PublishableMessage, type Publisher, Relay, type RelayHooks, type RelayOptions, type RetryConfig, type Serializer, StandardSchemaV1, type TopicDefinition, type Tracing, type Waker, buildPublishable, computeBackoff, defineOutbox, nextRetryAt };
444
+ export { type BackoffStrategy, ConsoleLogger, type DlqConfig, type EnqueueInput, type EnqueueableStore, JsonSerializer, type Logger, NoopLogger, OUTBOX_STATUS_CODE, OUTBOX_STATUS_FROM_CODE, type OutboxConsumer, type OutboxMessageInput, type OutboxProducer, type OutboxRecord, type OutboxRegistry, type OutboxStatus, type OutboxStore, OutboxValidationError, type PublishErrorKind, type PublishResult, type PublishableMessage, type Publisher, Relay, type RelayHooks, type RelayOptions, type RetryConfig, type Serializer, StandardSchemaV1, type TopicDefinition, type Tracing, type Waker, buildPublishable, computeBackoff, defineOutbox, nextRetryAt };
package/dist/index.d.ts CHANGED
@@ -67,7 +67,46 @@ interface PublishableMessage {
67
67
  /** Original record id, for correlation in publish results. */
68
68
  recordId: string;
69
69
  messageId: string;
70
+ /**
71
+ * Explicit partition override. When set, the publisher MUST route the
72
+ * record to this exact partition, bypassing the configured partitioner.
73
+ *
74
+ * Use cases:
75
+ * - Compacted topics with application-managed sharding.
76
+ * - Tenant-affinity routing where you compute the partition yourself.
77
+ * - Geo-pinning records to a specific broker.
78
+ *
79
+ * When omitted (the default), the underlying client's partitioner
80
+ * decides — usually a hash of `key`, falling back to sticky round-robin
81
+ * when `key` is null.
82
+ */
83
+ partition?: number;
70
84
  }
85
+ /**
86
+ * Why a publish failed, in terms the relay can act on. Drivers classify their
87
+ * native errors into one of these buckets; the relay reads `errorKind` to
88
+ * decide whether to retry, short-circuit to the DLQ, or pause polling. The
89
+ * field is optional for backward compatibility — when absent, the relay
90
+ * treats the error as `"retriable"`.
91
+ *
92
+ * - `retriable` — transient (broker unreachable, leader election, request
93
+ * timeout); retry per the configured backoff policy. The default for any
94
+ * unclassified error.
95
+ * - `fatal` — the producer or the credentials are broken (fenced epoch,
96
+ * authentication failed, ACL denied). Retrying cannot help; the relay
97
+ * short-circuits straight to the DLQ + `dead` status.
98
+ * - `poison` — the message itself is rejectable by every broker
99
+ * (oversized record, corrupt payload, schema-registry refused encoding).
100
+ * Same handling as `fatal`: DLQ + dead, no retries.
101
+ * - `backpressure` — the *producer's own* outbound buffer is full
102
+ * (librdkafka `__QUEUE_FULL`). The right response is to slow the relay
103
+ * down, not to burn retries. v2.1 treats this as `retriable` for
104
+ * compatibility; smarter handling (pause polling) is planned.
105
+ * - `quota` — the broker is throttling us (`THROTTLING_QUOTA_EXCEEDED`).
106
+ * Back off with longer delays. v2.1 treats as `retriable`; smarter
107
+ * handling (longer backoff) is planned.
108
+ */
109
+ type PublishErrorKind = "retriable" | "fatal" | "poison" | "backpressure" | "quota";
71
110
  /**
72
111
  * Result of attempting to publish a single message.
73
112
  */
@@ -75,6 +114,13 @@ interface PublishResult {
75
114
  recordId: string;
76
115
  ok: boolean;
77
116
  error?: Error;
117
+ /**
118
+ * Optional classification of `error` for relay-level decision-making. Set
119
+ * by publisher implementations that know how to inspect their native error
120
+ * shapes. Absent value is treated as `"retriable"` by the relay (the safe
121
+ * default — at worst we retry an error we should have skipped).
122
+ */
123
+ errorKind?: PublishErrorKind;
78
124
  }
79
125
  /**
80
126
  * Pluggable serializer. Default is JSON; users can swap in
@@ -395,4 +441,4 @@ declare function defineOutbox<R extends OutboxRegistry, S extends EnqueueableSto
395
441
  store: S;
396
442
  }): OutboxProducer<R, TxOf<S>>;
397
443
 
398
- export { type BackoffStrategy, ConsoleLogger, type DlqConfig, type EnqueueInput, type EnqueueableStore, JsonSerializer, type Logger, NoopLogger, OUTBOX_STATUS_CODE, OUTBOX_STATUS_FROM_CODE, type OutboxConsumer, type OutboxMessageInput, type OutboxProducer, type OutboxRecord, type OutboxRegistry, type OutboxStatus, type OutboxStore, OutboxValidationError, type PublishResult, type PublishableMessage, type Publisher, Relay, type RelayHooks, type RelayOptions, type RetryConfig, type Serializer, StandardSchemaV1, type TopicDefinition, type Tracing, type Waker, buildPublishable, computeBackoff, defineOutbox, nextRetryAt };
444
+ export { type BackoffStrategy, ConsoleLogger, type DlqConfig, type EnqueueInput, type EnqueueableStore, JsonSerializer, type Logger, NoopLogger, OUTBOX_STATUS_CODE, OUTBOX_STATUS_FROM_CODE, type OutboxConsumer, type OutboxMessageInput, type OutboxProducer, type OutboxRecord, type OutboxRegistry, type OutboxStatus, type OutboxStore, OutboxValidationError, type PublishErrorKind, type PublishResult, type PublishableMessage, type Publisher, Relay, type RelayHooks, type RelayOptions, type RetryConfig, type Serializer, StandardSchemaV1, type TopicDefinition, type Tracing, type Waker, buildPublishable, computeBackoff, defineOutbox, nextRetryAt };
package/dist/index.js CHANGED
@@ -211,7 +211,8 @@ var Relay = class {
211
211
  } else {
212
212
  await this.handleFailure(
213
213
  record,
214
- result.error ?? new Error("unknown publish error")
214
+ result.error ?? new Error("unknown publish error"),
215
+ result.errorKind
215
216
  );
216
217
  }
217
218
  }
@@ -220,15 +221,17 @@ var Relay = class {
220
221
  }
221
222
  return batch.length;
222
223
  }
223
- async handleFailure(record, error) {
224
+ async handleFailure(record, error, errorKind) {
224
225
  const attempts = record.attempts + 1;
225
- const retryAt = nextRetryAt(this.retry, attempts);
226
+ const isTerminalKind = errorKind === "fatal" || errorKind === "poison";
227
+ const retryAt = isTerminalKind ? null : nextRetryAt(this.retry, attempts);
226
228
  const willRetry = retryAt !== null;
227
229
  this.hooks.onFailed?.(record, error, willRetry);
228
230
  this.log.warn("publish failed", {
229
231
  recordId: record.id,
230
232
  attempts,
231
233
  willRetry,
234
+ errorKind: errorKind ?? "retriable",
232
235
  error: error.message
233
236
  });
234
237
  if (willRetry) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/backoff.ts","../src/serializer.ts","../src/publishable.ts","../src/relay.ts","../src/errors.ts","../src/registry.ts"],"sourcesContent":["/**\n * Status lifecycle of an outbox record.\n *\n * pending -> freshly enqueued, awaiting first publish\n * processing -> claimed by a relay instance, publish in flight\n * done -> successfully published to the broker\n * failed -> publish failed, awaiting retry (next_retry_at)\n * dead -> exhausted retries, routed to DLQ (or parked)\n */\nexport type OutboxStatus = \"pending\" | \"processing\" | \"done\" | \"failed\" | \"dead\";\n\nexport const OUTBOX_STATUS_CODE: Record<OutboxStatus, number> = {\n pending: 0,\n processing: 1,\n done: 2,\n failed: 3,\n dead: 4,\n};\n\nexport const OUTBOX_STATUS_FROM_CODE: Record<number, OutboxStatus> = {\n 0: \"pending\",\n 1: \"processing\",\n 2: \"done\",\n 3: \"failed\",\n 4: \"dead\",\n};\n\n/**\n * A message as enqueued by the application inside its own DB transaction.\n * This is the write-side input — no infra concerns leak in here.\n */\nexport interface OutboxMessageInput {\n /** Logical topic the message will be published to. */\n topic: string;\n /** Type of the aggregate that produced this event (e.g. \"order\"). */\n aggregateType: string;\n /**\n * Identifier of the aggregate instance (e.g. order id).\n * Used as the default partition key to preserve per-aggregate ordering.\n */\n aggregateId: string;\n /** Event payload. Serialized by the configured serializer. */\n payload: unknown;\n /** Optional explicit partition key. Falls back to aggregateId. */\n key?: string;\n /** Optional message headers. */\n headers?: Record<string, string>;\n /**\n * Optional client-supplied message id for idempotency / dedup.\n * If omitted, the store generates one.\n */\n messageId?: string;\n}\n\n/**\n * A persisted outbox record as read back by the relay.\n */\nexport interface OutboxRecord {\n id: string;\n messageId: string;\n topic: string;\n aggregateType: string;\n aggregateId: string;\n key: string | null;\n payload: unknown;\n headers: Record<string, string>;\n traceId: string | null;\n status: OutboxStatus;\n attempts: number;\n nextRetryAt: Date | null;\n createdAt: Date;\n processedAt: Date | null;\n}\n\n/**\n * The message handed to a Publisher after serialization.\n */\nexport interface PublishableMessage {\n topic: string;\n key: string | null;\n /** Serialized payload bytes. */\n value: Buffer;\n headers: Record<string, string>;\n /** Original record id, for correlation in publish results. */\n recordId: string;\n messageId: string;\n}\n\n/**\n * Result of attempting to publish a single message.\n */\nexport interface PublishResult {\n recordId: string;\n ok: boolean;\n error?: Error;\n}\n\n/**\n * Pluggable serializer. Default is JSON; users can swap in\n * Avro / Protobuf / Schema-Registry-backed serializers.\n */\nexport interface Serializer {\n serialize(message: OutboxRecord): Buffer | Promise<Buffer>;\n /** Content-type header value advertised for this serializer. */\n readonly contentType: string;\n}\n\n/**\n * Storage abstraction. Implemented per-database (postgres, mysql, ...).\n * The relay only talks to the store through this interface.\n */\nexport interface OutboxStore {\n /**\n * Atomically claim up to `batchSize` due messages and mark them\n * as `processing`. Implementations MUST be safe under concurrent\n * relay instances (e.g. SELECT ... FOR UPDATE SKIP LOCKED).\n */\n claimBatch(batchSize: number): Promise<OutboxRecord[]>;\n\n /** Mark records as successfully published. */\n markDone(recordIds: string[]): Promise<void>;\n\n /**\n * Mark a record as failed and schedule its next retry.\n * `nextRetryAt` of null + status \"dead\" means terminal.\n */\n markFailed(\n recordId: string,\n nextRetryAt: Date | null,\n status: \"failed\" | \"dead\",\n ): Promise<void>;\n\n /** Best-effort lifecycle hooks; no-op allowed. */\n init?(): Promise<void>;\n close?(): Promise<void>;\n}\n\n/**\n * Broker abstraction. Implemented per-driver (kafkajs, confluent, ...).\n */\nexport interface Publisher {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n /**\n * Publish a batch. Returns a per-message result so the relay can\n * mark partial success. Implementations may use a transactional\n * producer to make the batch atomic.\n */\n publish(messages: PublishableMessage[]): Promise<PublishResult[]>;\n /** Route a permanently-failed message to a dead-letter destination. */\n publishToDlq?(message: PublishableMessage, error: Error): Promise<void>;\n}\n\n/**\n * Backoff strategy for retrying failed publishes.\n */\nexport type BackoffStrategy = \"fixed\" | \"linear\" | \"exponential\";\n\nexport interface RetryConfig {\n maxAttempts: number;\n strategy: BackoffStrategy;\n baseMs: number;\n maxMs: number;\n /** Add random jitter (0..baseMs) to avoid thundering herd. Default true. */\n jitter?: boolean;\n}\n\nexport interface DlqConfig {\n /** Topic to route dead messages to. If absent, dead messages are parked. */\n topic?: string;\n}\n\n/**\n * Optional trace-context propagator. `inject` writes the active trace context\n * into the carrier as W3C `traceparent`/`tracestate` (the shape of an\n * OpenTelemetry TextMapPropagator), so it is persisted with the outbox row and\n * carried to the published message. The library depends on no tracing package;\n * provide a thin adapter over yours (OpenTelemetry, Datadog, …).\n */\nexport interface Tracing {\n inject(carrier: Record<string, string>): void;\n}\n\n/**\n * Minimal structured logger. Console-backed default provided.\n */\nexport interface Logger {\n debug(msg: string, meta?: Record<string, unknown>): void;\n info(msg: string, meta?: Record<string, unknown>): void;\n warn(msg: string, meta?: Record<string, unknown>): void;\n error(msg: string, meta?: Record<string, unknown>): void;\n}\n\n/**\n * A low-latency wake source for the relay. Instead of only waking on the poll\n * interval, the relay claims immediately whenever `onWake` fires. The signal is\n * advisory: it may fire spuriously or be missed entirely, and the relay's\n * polling remains the safety net, so implementations need not deduplicate or\n * guarantee delivery. (e.g. a Postgres LISTEN/NOTIFY waker.)\n */\nexport interface Waker {\n start(onWake: () => void): Promise<void>;\n stop(): Promise<void>;\n}\n\n/**\n * Lifecycle / observability hooks emitted by the relay.\n */\nexport interface RelayHooks {\n onBatchClaimed?(count: number): void;\n onPublished?(result: PublishResult): void;\n onFailed?(record: OutboxRecord, error: Error, willRetry: boolean): void;\n onDead?(record: OutboxRecord, error: Error): void;\n onError?(error: Error): void;\n}\n","import type { RetryConfig } from \"./types.js\";\n\n/**\n * Compute the delay (ms) before the next retry attempt.\n *\n * @param attempt 1-based attempt number that just failed.\n */\nexport function computeBackoff(config: RetryConfig, attempt: number): number {\n const { strategy, baseMs, maxMs } = config;\n const jitter = config.jitter ?? true;\n\n let delay: number;\n switch (strategy) {\n case \"fixed\":\n delay = baseMs;\n break;\n case \"linear\":\n delay = baseMs * attempt;\n break;\n case \"exponential\":\n // base * 2^(attempt-1), capped to avoid overflow before clamping\n delay = baseMs * 2 ** Math.min(attempt - 1, 30);\n break;\n }\n\n delay = Math.min(delay, maxMs);\n\n if (jitter) {\n // Full jitter: random in [0, delay]. Decorrelates concurrent relays.\n delay = Math.random() * delay;\n }\n\n return Math.floor(delay);\n}\n\n/**\n * Resolve when (Date) the next retry should occur, or null if the\n * record has exhausted its attempts and should go dead.\n */\nexport function nextRetryAt(\n config: RetryConfig,\n attempts: number,\n now: Date = new Date(),\n): Date | null {\n if (attempts >= config.maxAttempts) return null;\n const delay = computeBackoff(config, attempts);\n return new Date(now.getTime() + delay);\n}\n","import type { Logger, OutboxRecord, Serializer } from \"./types.js\";\n\n/**\n * Default serializer: JSON-encodes the record payload to UTF-8 bytes.\n */\nexport class JsonSerializer implements Serializer {\n readonly contentType = \"application/json\";\n\n serialize(record: OutboxRecord): Buffer {\n return Buffer.from(JSON.stringify(record.payload), \"utf8\");\n }\n}\n\n/**\n * Minimal console-backed logger. Swap in pino/winston by implementing Logger.\n */\nexport class ConsoleLogger implements Logger {\n constructor(private readonly prefix = \"[outbox]\") {}\n\n private fmt(\n write: (line: string, meta?: Record<string, unknown>) => void,\n level: string,\n msg: string,\n meta?: Record<string, unknown>,\n ): void {\n const line = `${this.prefix} ${level} ${msg}`;\n if (meta) {\n write(line, meta);\n } else {\n write(line);\n }\n }\n\n debug(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.debug, \"DEBUG\", msg, meta);\n }\n info(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.info, \"INFO\", msg, meta);\n }\n warn(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.warn, \"WARN\", msg, meta);\n }\n error(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.error, \"ERROR\", msg, meta);\n }\n}\n\n/** Logger that discards everything. Useful for tests. */\nexport class NoopLogger implements Logger {\n debug(): void {}\n info(): void {}\n warn(): void {}\n error(): void {}\n}\n","import type { OutboxRecord, PublishableMessage, Serializer } from \"./types.js\";\n\n/**\n * Turn a persisted outbox record into a broker-ready message: serialize the\n * payload and attach the standard correlation headers. Shared by the polling\n * `Relay` and the streaming relay so both produce byte-identical messages.\n */\nexport async function buildPublishable(\n record: OutboxRecord,\n serializer: Serializer,\n): Promise<PublishableMessage> {\n const value = await serializer.serialize(record);\n const headers: Record<string, string> = {\n ...record.headers,\n \"content-type\": serializer.contentType,\n \"message-id\": record.messageId,\n \"aggregate-type\": record.aggregateType,\n \"aggregate-id\": record.aggregateId,\n };\n if (record.traceId) headers[\"trace-id\"] = record.traceId;\n\n return {\n topic: record.topic,\n key: record.key ?? record.aggregateId,\n value,\n headers,\n recordId: record.id,\n messageId: record.messageId,\n };\n}\n","import { nextRetryAt } from \"./backoff.js\";\nimport { buildPublishable } from \"./publishable.js\";\nimport { ConsoleLogger, JsonSerializer } from \"./serializer.js\";\nimport type {\n DlqConfig,\n Logger,\n OutboxRecord,\n OutboxStore,\n PublishableMessage,\n Publisher,\n RelayHooks,\n RetryConfig,\n Serializer,\n Waker,\n} from \"./types.js\";\n\nexport interface RelayOptions {\n store: OutboxStore;\n publisher: Publisher;\n /** Messages claimed per poll iteration. Default 100. */\n batchSize?: number;\n /** Idle wait (ms) when a poll returns no work. Default 200. */\n pollIntervalMs?: number;\n retry?: Partial<RetryConfig>;\n dlq?: DlqConfig;\n serializer?: Serializer;\n logger?: Logger;\n hooks?: RelayHooks;\n /**\n * Optional low-latency wake source. When provided, the relay claims as soon as\n * the waker signals new work, instead of waiting out `pollIntervalMs`. Polling\n * stays on as a safety net, so set `pollIntervalMs` longer when using a waker.\n */\n waker?: Waker;\n}\n\nconst DEFAULT_RETRY: RetryConfig = {\n maxAttempts: 5,\n strategy: \"exponential\",\n baseMs: 200,\n maxMs: 30_000,\n jitter: true,\n};\n\n/**\n * The Relay drains the outbox store and publishes messages to the broker.\n *\n * It is safe to run multiple Relay instances concurrently against the same\n * store as long as the store's claimBatch uses a lock-free claim strategy\n * (e.g. SELECT ... FOR UPDATE SKIP LOCKED).\n */\nexport class Relay {\n private readonly store: OutboxStore;\n private readonly publisher: Publisher;\n private readonly batchSize: number;\n private readonly pollIntervalMs: number;\n private readonly retry: RetryConfig;\n private readonly dlq: DlqConfig;\n private readonly serializer: Serializer;\n private readonly log: Logger;\n private readonly hooks: RelayHooks;\n private readonly waker: Waker | null;\n\n private running = false;\n private stopping = false;\n private loopPromise: Promise<void> | null = null;\n\n // Interruptible idle wait: `signal()` wakes a pending wait (or marks one\n // pending if none is in flight, so a wake can't be lost between cycles).\n private wakePending = false;\n private wakeResolver: (() => void) | null = null;\n\n constructor(opts: RelayOptions) {\n this.store = opts.store;\n this.publisher = opts.publisher;\n this.batchSize = opts.batchSize ?? 100;\n this.pollIntervalMs = opts.pollIntervalMs ?? 200;\n this.retry = { ...DEFAULT_RETRY, ...opts.retry };\n this.dlq = opts.dlq ?? {};\n this.serializer = opts.serializer ?? new JsonSerializer();\n this.log = opts.logger ?? new ConsoleLogger();\n this.hooks = opts.hooks ?? {};\n this.waker = opts.waker ?? null;\n }\n\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n this.stopping = false;\n\n await this.store.init?.();\n await this.publisher.connect();\n await this.waker?.start(() => this.signal());\n this.log.info(\"relay started\", {\n batchSize: this.batchSize,\n pollIntervalMs: this.pollIntervalMs,\n waker: this.waker !== null,\n });\n\n this.loopPromise = this.loop();\n }\n\n /**\n * Stop accepting new work, finish the in-flight batch, then disconnect.\n */\n async stop(): Promise<void> {\n if (!this.running) return;\n this.stopping = true;\n this.signal(); // break any in-flight idle wait so shutdown is immediate\n this.log.info(\"relay stopping, draining in-flight batch\");\n await this.loopPromise;\n await this.waker?.stop();\n await this.publisher.disconnect();\n await this.store.close?.();\n this.running = false;\n this.log.info(\"relay stopped\");\n }\n\n private async loop(): Promise<void> {\n while (!this.stopping) {\n try {\n const processed = await this.tick();\n if (processed === 0 && !this.stopping) {\n await this.waitForWork(this.pollIntervalMs);\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n this.log.error(\"relay loop error\", { error: error.message });\n this.hooks.onError?.(error);\n await this.waitForWork(this.pollIntervalMs);\n }\n }\n }\n\n /**\n * Run a single claim+publish cycle. Returns number of records processed.\n * Exposed for tests / manual single-shot draining.\n */\n async tick(): Promise<number> {\n const batch = await this.store.claimBatch(this.batchSize);\n if (batch.length === 0) return 0;\n\n this.hooks.onBatchClaimed?.(batch.length);\n this.log.debug(\"batch claimed\", { count: batch.length });\n\n const messages = await this.toPublishable(batch);\n const recordsById = new Map(batch.map((r) => [r.id, r]));\n\n const results = await this.publisher.publish(messages);\n\n const succeeded: string[] = [];\n for (const result of results) {\n const record = recordsById.get(result.recordId);\n if (!record) continue;\n\n if (result.ok) {\n succeeded.push(record.id);\n this.hooks.onPublished?.(result);\n } else {\n await this.handleFailure(\n record,\n result.error ?? new Error(\"unknown publish error\"),\n );\n }\n }\n\n if (succeeded.length > 0) {\n await this.store.markDone(succeeded);\n }\n\n return batch.length;\n }\n\n private async handleFailure(\n record: OutboxRecord,\n error: Error,\n ): Promise<void> {\n const attempts = record.attempts + 1;\n const retryAt = nextRetryAt(this.retry, attempts);\n const willRetry = retryAt !== null;\n\n this.hooks.onFailed?.(record, error, willRetry);\n this.log.warn(\"publish failed\", {\n recordId: record.id,\n attempts,\n willRetry,\n error: error.message,\n });\n\n if (willRetry) {\n await this.store.markFailed(record.id, retryAt, \"failed\");\n return;\n }\n\n // Terminal: route to DLQ if configured, then mark dead.\n if (this.dlq.topic && this.publisher.publishToDlq) {\n try {\n const msg = (await this.toPublishable([record]))[0];\n if (msg) {\n await this.publisher.publishToDlq(\n {\n ...msg,\n topic: this.dlq.topic,\n // Preserve the original destination so the publisher can record\n // it as a header; otherwise it is lost when we overwrite `topic`.\n headers: { ...msg.headers, \"original-topic\": record.topic },\n },\n error,\n );\n }\n } catch (dlqErr) {\n const e = dlqErr instanceof Error ? dlqErr : new Error(String(dlqErr));\n this.log.error(\"DLQ publish failed\", {\n recordId: record.id,\n error: e.message,\n });\n }\n }\n\n await this.store.markFailed(record.id, null, \"dead\");\n this.hooks.onDead?.(record, error);\n }\n\n private async toPublishable(\n records: OutboxRecord[],\n ): Promise<PublishableMessage[]> {\n const out: PublishableMessage[] = [];\n for (const record of records) {\n out.push(await buildPublishable(record, this.serializer));\n }\n return out;\n }\n\n /** Wake a pending idle wait, or mark one pending so the next wait is skipped. */\n private signal(): void {\n this.wakePending = true;\n this.wakeResolver?.();\n }\n\n /**\n * Idle wait that resolves after `ms`, or early when `signal()` fires. A signal\n * raised while no wait is in flight is remembered (wakePending), so a wake that\n * races a claim cycle is never lost.\n */\n private waitForWork(ms: number): Promise<void> {\n if (this.wakePending) {\n this.wakePending = false;\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n this.wakeResolver = null;\n resolve();\n }, ms);\n this.wakeResolver = () => {\n clearTimeout(timer);\n this.wakeResolver = null;\n this.wakePending = false;\n resolve();\n };\n });\n }\n}\n","import type { StandardSchemaV1 } from \"./standard-schema.js\";\n\n/**\n * Thrown when an outbox payload fails its topic's schema — at `enqueue`\n * (before the DB insert) or at `decode` (malformed bytes / schema mismatch).\n * Carries the topic and the validator's structured issues for diagnostics.\n */\nexport class OutboxValidationError extends Error {\n readonly topic: string;\n readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;\n\n constructor(\n topic: string,\n issues: ReadonlyArray<StandardSchemaV1.Issue>,\n options?: { cause?: unknown },\n ) {\n const first = issues[0]?.message ?? \"unknown validation error\";\n super(`Outbox payload for \"${topic}\" failed validation: ${first}`, options);\n this.name = \"OutboxValidationError\";\n this.topic = topic;\n this.issues = issues;\n }\n}\n","import { OutboxValidationError } from \"./errors.js\";\nimport type { StandardSchemaV1 } from \"./standard-schema.js\";\nimport type { OutboxMessageInput } from \"./types.js\";\n\n/** One topic's contract: the aggregate it belongs to and its payload schema. */\nexport interface TopicDefinition {\n /** Aggregate type stamped on every event of this topic (e.g. \"order\"). */\n readonly aggregateType: string;\n /** Standard Schema for the payload (Zod 3.24+/Valibot/ArkType/…). */\n readonly schema: StandardSchemaV1;\n}\n\n/** A map of topic name -> its definition. The single source of truth. */\nexport type OutboxRegistry = Record<string, TopicDefinition>;\n\n/**\n * Minimal store surface the producer facade needs. `PostgresStore` satisfies\n * this structurally, so the facade stays DB-agnostic and `tx` flows from the\n * concrete store (e.g. a pg client).\n */\nexport interface EnqueueableStore<Tx = unknown> {\n enqueue(tx: Tx, msg: OutboxMessageInput & { traceId?: string }): Promise<string>;\n}\n\ntype TxOf<S> = S extends EnqueueableStore<infer Tx> ? Tx : never;\n\ntype PayloadInput<R extends OutboxRegistry, K extends keyof R> =\n StandardSchemaV1.InferInput<R[K][\"schema\"]>;\ntype PayloadOutput<R extends OutboxRegistry, K extends keyof R> =\n StandardSchemaV1.InferOutput<R[K][\"schema\"]>;\n\n/** The write-side input for a typed enqueue (payload is the schema's input type). */\nexport interface EnqueueInput<R extends OutboxRegistry, K extends keyof R> {\n aggregateId: string;\n payload: PayloadInput<R, K>;\n key?: string;\n headers?: Record<string, string>;\n messageId?: string;\n traceId?: string;\n}\n\n/** Consumer-side facade: validate/decode without a store. */\nexport interface OutboxConsumer<R extends OutboxRegistry> {\n /** Validate an already-parsed value against the topic's schema. */\n validate<K extends keyof R & string>(\n topic: K,\n value: unknown,\n ): Promise<PayloadOutput<R, K>>;\n /** JSON-parse `bytes`, validate against the topic's schema, return the payload. */\n decode<K extends keyof R & string>(\n topic: K,\n bytes: Buffer | Uint8Array | string,\n ): Promise<PayloadOutput<R, K>>;\n}\n\n/** Producer-side facade: adds typed, validated enqueue. */\nexport interface OutboxProducer<R extends OutboxRegistry, Tx>\n extends OutboxConsumer<R> {\n /** Validate `payload`, then enqueue inside the caller's transaction `tx`. */\n enqueue<K extends keyof R & string>(\n tx: Tx,\n topic: K,\n msg: EnqueueInput<R, K>,\n ): Promise<string>;\n}\n\n// Loose shape used inside the implementation; the public types come from overloads.\ninterface LooseEnqueueInput {\n aggregateId: string;\n payload: unknown;\n key?: string;\n headers?: Record<string, string>;\n messageId?: string;\n traceId?: string;\n}\n\nexport function defineOutbox<R extends OutboxRegistry>(\n registry: R,\n): OutboxConsumer<R>;\nexport function defineOutbox<R extends OutboxRegistry, S extends EnqueueableStore>(\n registry: R,\n opts: { store: S },\n): OutboxProducer<R, TxOf<S>>;\nexport function defineOutbox<R extends OutboxRegistry>(\n registry: R,\n opts?: { store: EnqueueableStore },\n): OutboxConsumer<R> | OutboxProducer<R, unknown> {\n const validate = async (topic: string, value: unknown): Promise<unknown> => {\n const def = registry[topic];\n if (!def) {\n throw new OutboxValidationError(topic, [\n { message: `unknown topic \"${topic}\"` },\n ]);\n }\n const result = await def.schema[\"~standard\"].validate(value);\n if (result.issues) throw new OutboxValidationError(topic, result.issues);\n return result.value;\n };\n\n const decode = async (\n topic: string,\n bytes: Buffer | Uint8Array | string,\n ): Promise<unknown> => {\n const text =\n typeof bytes === \"string\" ? bytes : Buffer.from(bytes).toString(\"utf8\");\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err) {\n throw new OutboxValidationError(\n topic,\n [{ message: `invalid JSON: ${(err as Error).message}` }],\n { cause: err },\n );\n }\n return validate(topic, parsed);\n };\n\n const consumer = { validate, decode } as unknown as OutboxConsumer<R>;\n if (!opts?.store) return consumer;\n\n const store = opts.store;\n const enqueue = async (\n tx: unknown,\n topic: string,\n msg: LooseEnqueueInput,\n ): Promise<string> => {\n const def = registry[topic];\n if (!def) {\n throw new OutboxValidationError(topic, [\n { message: `unknown topic \"${topic}\"` },\n ]);\n }\n const payload = await validate(topic, msg.payload);\n return store.enqueue(tx, {\n topic,\n aggregateType: def.aggregateType,\n aggregateId: msg.aggregateId,\n payload,\n key: msg.key,\n headers: msg.headers,\n messageId: msg.messageId,\n traceId: msg.traceId,\n });\n };\n\n return { validate, decode, enqueue } as unknown as OutboxProducer<R, unknown>;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/types.ts","../src/backoff.ts","../src/serializer.ts","../src/publishable.ts","../src/relay.ts","../src/errors.ts","../src/registry.ts"],"sourcesContent":["/**\n * Status lifecycle of an outbox record.\n *\n * pending -> freshly enqueued, awaiting first publish\n * processing -> claimed by a relay instance, publish in flight\n * done -> successfully published to the broker\n * failed -> publish failed, awaiting retry (next_retry_at)\n * dead -> exhausted retries, routed to DLQ (or parked)\n */\nexport type OutboxStatus = \"pending\" | \"processing\" | \"done\" | \"failed\" | \"dead\";\n\nexport const OUTBOX_STATUS_CODE: Record<OutboxStatus, number> = {\n pending: 0,\n processing: 1,\n done: 2,\n failed: 3,\n dead: 4,\n};\n\nexport const OUTBOX_STATUS_FROM_CODE: Record<number, OutboxStatus> = {\n 0: \"pending\",\n 1: \"processing\",\n 2: \"done\",\n 3: \"failed\",\n 4: \"dead\",\n};\n\n/**\n * A message as enqueued by the application inside its own DB transaction.\n * This is the write-side input — no infra concerns leak in here.\n */\nexport interface OutboxMessageInput {\n /** Logical topic the message will be published to. */\n topic: string;\n /** Type of the aggregate that produced this event (e.g. \"order\"). */\n aggregateType: string;\n /**\n * Identifier of the aggregate instance (e.g. order id).\n * Used as the default partition key to preserve per-aggregate ordering.\n */\n aggregateId: string;\n /** Event payload. Serialized by the configured serializer. */\n payload: unknown;\n /** Optional explicit partition key. Falls back to aggregateId. */\n key?: string;\n /** Optional message headers. */\n headers?: Record<string, string>;\n /**\n * Optional client-supplied message id for idempotency / dedup.\n * If omitted, the store generates one.\n */\n messageId?: string;\n}\n\n/**\n * A persisted outbox record as read back by the relay.\n */\nexport interface OutboxRecord {\n id: string;\n messageId: string;\n topic: string;\n aggregateType: string;\n aggregateId: string;\n key: string | null;\n payload: unknown;\n headers: Record<string, string>;\n traceId: string | null;\n status: OutboxStatus;\n attempts: number;\n nextRetryAt: Date | null;\n createdAt: Date;\n processedAt: Date | null;\n}\n\n/**\n * The message handed to a Publisher after serialization.\n */\nexport interface PublishableMessage {\n topic: string;\n key: string | null;\n /** Serialized payload bytes. */\n value: Buffer;\n headers: Record<string, string>;\n /** Original record id, for correlation in publish results. */\n recordId: string;\n messageId: string;\n /**\n * Explicit partition override. When set, the publisher MUST route the\n * record to this exact partition, bypassing the configured partitioner.\n *\n * Use cases:\n * - Compacted topics with application-managed sharding.\n * - Tenant-affinity routing where you compute the partition yourself.\n * - Geo-pinning records to a specific broker.\n *\n * When omitted (the default), the underlying client's partitioner\n * decides — usually a hash of `key`, falling back to sticky round-robin\n * when `key` is null.\n */\n partition?: number;\n}\n\n/**\n * Why a publish failed, in terms the relay can act on. Drivers classify their\n * native errors into one of these buckets; the relay reads `errorKind` to\n * decide whether to retry, short-circuit to the DLQ, or pause polling. The\n * field is optional for backward compatibility — when absent, the relay\n * treats the error as `\"retriable\"`.\n *\n * - `retriable` — transient (broker unreachable, leader election, request\n * timeout); retry per the configured backoff policy. The default for any\n * unclassified error.\n * - `fatal` — the producer or the credentials are broken (fenced epoch,\n * authentication failed, ACL denied). Retrying cannot help; the relay\n * short-circuits straight to the DLQ + `dead` status.\n * - `poison` — the message itself is rejectable by every broker\n * (oversized record, corrupt payload, schema-registry refused encoding).\n * Same handling as `fatal`: DLQ + dead, no retries.\n * - `backpressure` — the *producer's own* outbound buffer is full\n * (librdkafka `__QUEUE_FULL`). The right response is to slow the relay\n * down, not to burn retries. v2.1 treats this as `retriable` for\n * compatibility; smarter handling (pause polling) is planned.\n * - `quota` — the broker is throttling us (`THROTTLING_QUOTA_EXCEEDED`).\n * Back off with longer delays. v2.1 treats as `retriable`; smarter\n * handling (longer backoff) is planned.\n */\nexport type PublishErrorKind =\n | \"retriable\"\n | \"fatal\"\n | \"poison\"\n | \"backpressure\"\n | \"quota\";\n\n/**\n * Result of attempting to publish a single message.\n */\nexport interface PublishResult {\n recordId: string;\n ok: boolean;\n error?: Error;\n /**\n * Optional classification of `error` for relay-level decision-making. Set\n * by publisher implementations that know how to inspect their native error\n * shapes. Absent value is treated as `\"retriable\"` by the relay (the safe\n * default — at worst we retry an error we should have skipped).\n */\n errorKind?: PublishErrorKind;\n}\n\n/**\n * Pluggable serializer. Default is JSON; users can swap in\n * Avro / Protobuf / Schema-Registry-backed serializers.\n */\nexport interface Serializer {\n serialize(message: OutboxRecord): Buffer | Promise<Buffer>;\n /** Content-type header value advertised for this serializer. */\n readonly contentType: string;\n}\n\n/**\n * Storage abstraction. Implemented per-database (postgres, mysql, ...).\n * The relay only talks to the store through this interface.\n */\nexport interface OutboxStore {\n /**\n * Atomically claim up to `batchSize` due messages and mark them\n * as `processing`. Implementations MUST be safe under concurrent\n * relay instances (e.g. SELECT ... FOR UPDATE SKIP LOCKED).\n */\n claimBatch(batchSize: number): Promise<OutboxRecord[]>;\n\n /** Mark records as successfully published. */\n markDone(recordIds: string[]): Promise<void>;\n\n /**\n * Mark a record as failed and schedule its next retry.\n * `nextRetryAt` of null + status \"dead\" means terminal.\n */\n markFailed(\n recordId: string,\n nextRetryAt: Date | null,\n status: \"failed\" | \"dead\",\n ): Promise<void>;\n\n /** Best-effort lifecycle hooks; no-op allowed. */\n init?(): Promise<void>;\n close?(): Promise<void>;\n}\n\n/**\n * Broker abstraction. Implemented per-driver (kafkajs, confluent, ...).\n */\nexport interface Publisher {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n /**\n * Publish a batch. Returns a per-message result so the relay can\n * mark partial success. Implementations may use a transactional\n * producer to make the batch atomic.\n */\n publish(messages: PublishableMessage[]): Promise<PublishResult[]>;\n /** Route a permanently-failed message to a dead-letter destination. */\n publishToDlq?(message: PublishableMessage, error: Error): Promise<void>;\n}\n\n/**\n * Backoff strategy for retrying failed publishes.\n */\nexport type BackoffStrategy = \"fixed\" | \"linear\" | \"exponential\";\n\nexport interface RetryConfig {\n maxAttempts: number;\n strategy: BackoffStrategy;\n baseMs: number;\n maxMs: number;\n /** Add random jitter (0..baseMs) to avoid thundering herd. Default true. */\n jitter?: boolean;\n}\n\nexport interface DlqConfig {\n /** Topic to route dead messages to. If absent, dead messages are parked. */\n topic?: string;\n}\n\n/**\n * Optional trace-context propagator. `inject` writes the active trace context\n * into the carrier as W3C `traceparent`/`tracestate` (the shape of an\n * OpenTelemetry TextMapPropagator), so it is persisted with the outbox row and\n * carried to the published message. The library depends on no tracing package;\n * provide a thin adapter over yours (OpenTelemetry, Datadog, …).\n */\nexport interface Tracing {\n inject(carrier: Record<string, string>): void;\n}\n\n/**\n * Minimal structured logger. Console-backed default provided.\n */\nexport interface Logger {\n debug(msg: string, meta?: Record<string, unknown>): void;\n info(msg: string, meta?: Record<string, unknown>): void;\n warn(msg: string, meta?: Record<string, unknown>): void;\n error(msg: string, meta?: Record<string, unknown>): void;\n}\n\n/**\n * A low-latency wake source for the relay. Instead of only waking on the poll\n * interval, the relay claims immediately whenever `onWake` fires. The signal is\n * advisory: it may fire spuriously or be missed entirely, and the relay's\n * polling remains the safety net, so implementations need not deduplicate or\n * guarantee delivery. (e.g. a Postgres LISTEN/NOTIFY waker.)\n */\nexport interface Waker {\n start(onWake: () => void): Promise<void>;\n stop(): Promise<void>;\n}\n\n/**\n * Lifecycle / observability hooks emitted by the relay.\n */\nexport interface RelayHooks {\n onBatchClaimed?(count: number): void;\n onPublished?(result: PublishResult): void;\n onFailed?(record: OutboxRecord, error: Error, willRetry: boolean): void;\n onDead?(record: OutboxRecord, error: Error): void;\n onError?(error: Error): void;\n}\n","import type { RetryConfig } from \"./types.js\";\n\n/**\n * Compute the delay (ms) before the next retry attempt.\n *\n * @param attempt 1-based attempt number that just failed.\n */\nexport function computeBackoff(config: RetryConfig, attempt: number): number {\n const { strategy, baseMs, maxMs } = config;\n const jitter = config.jitter ?? true;\n\n let delay: number;\n switch (strategy) {\n case \"fixed\":\n delay = baseMs;\n break;\n case \"linear\":\n delay = baseMs * attempt;\n break;\n case \"exponential\":\n // base * 2^(attempt-1), capped to avoid overflow before clamping\n delay = baseMs * 2 ** Math.min(attempt - 1, 30);\n break;\n }\n\n delay = Math.min(delay, maxMs);\n\n if (jitter) {\n // Full jitter: random in [0, delay]. Decorrelates concurrent relays.\n delay = Math.random() * delay;\n }\n\n return Math.floor(delay);\n}\n\n/**\n * Resolve when (Date) the next retry should occur, or null if the\n * record has exhausted its attempts and should go dead.\n */\nexport function nextRetryAt(\n config: RetryConfig,\n attempts: number,\n now: Date = new Date(),\n): Date | null {\n if (attempts >= config.maxAttempts) return null;\n const delay = computeBackoff(config, attempts);\n return new Date(now.getTime() + delay);\n}\n","import type { Logger, OutboxRecord, Serializer } from \"./types.js\";\n\n/**\n * Default serializer: JSON-encodes the record payload to UTF-8 bytes.\n */\nexport class JsonSerializer implements Serializer {\n readonly contentType = \"application/json\";\n\n serialize(record: OutboxRecord): Buffer {\n return Buffer.from(JSON.stringify(record.payload), \"utf8\");\n }\n}\n\n/**\n * Minimal console-backed logger. Swap in pino/winston by implementing Logger.\n */\nexport class ConsoleLogger implements Logger {\n constructor(private readonly prefix = \"[outbox]\") {}\n\n private fmt(\n write: (line: string, meta?: Record<string, unknown>) => void,\n level: string,\n msg: string,\n meta?: Record<string, unknown>,\n ): void {\n const line = `${this.prefix} ${level} ${msg}`;\n if (meta) {\n write(line, meta);\n } else {\n write(line);\n }\n }\n\n debug(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.debug, \"DEBUG\", msg, meta);\n }\n info(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.info, \"INFO\", msg, meta);\n }\n warn(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.warn, \"WARN\", msg, meta);\n }\n error(msg: string, meta?: Record<string, unknown>): void {\n // eslint-disable-next-line no-console\n this.fmt(console.error, \"ERROR\", msg, meta);\n }\n}\n\n/** Logger that discards everything. Useful for tests. */\nexport class NoopLogger implements Logger {\n debug(): void {}\n info(): void {}\n warn(): void {}\n error(): void {}\n}\n","import type { OutboxRecord, PublishableMessage, Serializer } from \"./types.js\";\n\n/**\n * Turn a persisted outbox record into a broker-ready message: serialize the\n * payload and attach the standard correlation headers. Shared by the polling\n * `Relay` and the streaming relay so both produce byte-identical messages.\n */\nexport async function buildPublishable(\n record: OutboxRecord,\n serializer: Serializer,\n): Promise<PublishableMessage> {\n const value = await serializer.serialize(record);\n const headers: Record<string, string> = {\n ...record.headers,\n \"content-type\": serializer.contentType,\n \"message-id\": record.messageId,\n \"aggregate-type\": record.aggregateType,\n \"aggregate-id\": record.aggregateId,\n };\n if (record.traceId) headers[\"trace-id\"] = record.traceId;\n\n return {\n topic: record.topic,\n key: record.key ?? record.aggregateId,\n value,\n headers,\n recordId: record.id,\n messageId: record.messageId,\n };\n}\n","import { nextRetryAt } from \"./backoff.js\";\nimport { buildPublishable } from \"./publishable.js\";\nimport { ConsoleLogger, JsonSerializer } from \"./serializer.js\";\nimport type {\n DlqConfig,\n Logger,\n OutboxRecord,\n OutboxStore,\n PublishableMessage,\n PublishErrorKind,\n Publisher,\n RelayHooks,\n RetryConfig,\n Serializer,\n Waker,\n} from \"./types.js\";\n\nexport interface RelayOptions {\n store: OutboxStore;\n publisher: Publisher;\n /** Messages claimed per poll iteration. Default 100. */\n batchSize?: number;\n /** Idle wait (ms) when a poll returns no work. Default 200. */\n pollIntervalMs?: number;\n retry?: Partial<RetryConfig>;\n dlq?: DlqConfig;\n serializer?: Serializer;\n logger?: Logger;\n hooks?: RelayHooks;\n /**\n * Optional low-latency wake source. When provided, the relay claims as soon as\n * the waker signals new work, instead of waiting out `pollIntervalMs`. Polling\n * stays on as a safety net, so set `pollIntervalMs` longer when using a waker.\n */\n waker?: Waker;\n}\n\nconst DEFAULT_RETRY: RetryConfig = {\n maxAttempts: 5,\n strategy: \"exponential\",\n baseMs: 200,\n maxMs: 30_000,\n jitter: true,\n};\n\n/**\n * The Relay drains the outbox store and publishes messages to the broker.\n *\n * It is safe to run multiple Relay instances concurrently against the same\n * store as long as the store's claimBatch uses a lock-free claim strategy\n * (e.g. SELECT ... FOR UPDATE SKIP LOCKED).\n */\nexport class Relay {\n private readonly store: OutboxStore;\n private readonly publisher: Publisher;\n private readonly batchSize: number;\n private readonly pollIntervalMs: number;\n private readonly retry: RetryConfig;\n private readonly dlq: DlqConfig;\n private readonly serializer: Serializer;\n private readonly log: Logger;\n private readonly hooks: RelayHooks;\n private readonly waker: Waker | null;\n\n private running = false;\n private stopping = false;\n private loopPromise: Promise<void> | null = null;\n\n // Interruptible idle wait: `signal()` wakes a pending wait (or marks one\n // pending if none is in flight, so a wake can't be lost between cycles).\n private wakePending = false;\n private wakeResolver: (() => void) | null = null;\n\n constructor(opts: RelayOptions) {\n this.store = opts.store;\n this.publisher = opts.publisher;\n this.batchSize = opts.batchSize ?? 100;\n this.pollIntervalMs = opts.pollIntervalMs ?? 200;\n this.retry = { ...DEFAULT_RETRY, ...opts.retry };\n this.dlq = opts.dlq ?? {};\n this.serializer = opts.serializer ?? new JsonSerializer();\n this.log = opts.logger ?? new ConsoleLogger();\n this.hooks = opts.hooks ?? {};\n this.waker = opts.waker ?? null;\n }\n\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n this.stopping = false;\n\n await this.store.init?.();\n await this.publisher.connect();\n await this.waker?.start(() => this.signal());\n this.log.info(\"relay started\", {\n batchSize: this.batchSize,\n pollIntervalMs: this.pollIntervalMs,\n waker: this.waker !== null,\n });\n\n this.loopPromise = this.loop();\n }\n\n /**\n * Stop accepting new work, finish the in-flight batch, then disconnect.\n */\n async stop(): Promise<void> {\n if (!this.running) return;\n this.stopping = true;\n this.signal(); // break any in-flight idle wait so shutdown is immediate\n this.log.info(\"relay stopping, draining in-flight batch\");\n await this.loopPromise;\n await this.waker?.stop();\n await this.publisher.disconnect();\n await this.store.close?.();\n this.running = false;\n this.log.info(\"relay stopped\");\n }\n\n private async loop(): Promise<void> {\n while (!this.stopping) {\n try {\n const processed = await this.tick();\n if (processed === 0 && !this.stopping) {\n await this.waitForWork(this.pollIntervalMs);\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n this.log.error(\"relay loop error\", { error: error.message });\n this.hooks.onError?.(error);\n await this.waitForWork(this.pollIntervalMs);\n }\n }\n }\n\n /**\n * Run a single claim+publish cycle. Returns number of records processed.\n * Exposed for tests / manual single-shot draining.\n */\n async tick(): Promise<number> {\n const batch = await this.store.claimBatch(this.batchSize);\n if (batch.length === 0) return 0;\n\n this.hooks.onBatchClaimed?.(batch.length);\n this.log.debug(\"batch claimed\", { count: batch.length });\n\n const messages = await this.toPublishable(batch);\n const recordsById = new Map(batch.map((r) => [r.id, r]));\n\n const results = await this.publisher.publish(messages);\n\n const succeeded: string[] = [];\n for (const result of results) {\n const record = recordsById.get(result.recordId);\n if (!record) continue;\n\n if (result.ok) {\n succeeded.push(record.id);\n this.hooks.onPublished?.(result);\n } else {\n await this.handleFailure(\n record,\n result.error ?? new Error(\"unknown publish error\"),\n result.errorKind,\n );\n }\n }\n\n if (succeeded.length > 0) {\n await this.store.markDone(succeeded);\n }\n\n return batch.length;\n }\n\n private async handleFailure(\n record: OutboxRecord,\n error: Error,\n errorKind?: PublishErrorKind,\n ): Promise<void> {\n const attempts = record.attempts + 1;\n // fatal/poison short-circuit: retrying cannot help (auth denied, fenced\n // epoch, oversized record, schema rejected). Skip the backoff schedule\n // entirely and go straight to DLQ + dead.\n const isTerminalKind = errorKind === \"fatal\" || errorKind === \"poison\";\n const retryAt = isTerminalKind ? null : nextRetryAt(this.retry, attempts);\n const willRetry = retryAt !== null;\n\n this.hooks.onFailed?.(record, error, willRetry);\n this.log.warn(\"publish failed\", {\n recordId: record.id,\n attempts,\n willRetry,\n errorKind: errorKind ?? \"retriable\",\n error: error.message,\n });\n\n if (willRetry) {\n await this.store.markFailed(record.id, retryAt, \"failed\");\n return;\n }\n\n // Terminal: route to DLQ if configured, then mark dead.\n if (this.dlq.topic && this.publisher.publishToDlq) {\n try {\n const msg = (await this.toPublishable([record]))[0];\n if (msg) {\n await this.publisher.publishToDlq(\n {\n ...msg,\n topic: this.dlq.topic,\n // Preserve the original destination so the publisher can record\n // it as a header; otherwise it is lost when we overwrite `topic`.\n headers: { ...msg.headers, \"original-topic\": record.topic },\n },\n error,\n );\n }\n } catch (dlqErr) {\n const e = dlqErr instanceof Error ? dlqErr : new Error(String(dlqErr));\n this.log.error(\"DLQ publish failed\", {\n recordId: record.id,\n error: e.message,\n });\n }\n }\n\n await this.store.markFailed(record.id, null, \"dead\");\n this.hooks.onDead?.(record, error);\n }\n\n private async toPublishable(\n records: OutboxRecord[],\n ): Promise<PublishableMessage[]> {\n const out: PublishableMessage[] = [];\n for (const record of records) {\n out.push(await buildPublishable(record, this.serializer));\n }\n return out;\n }\n\n /** Wake a pending idle wait, or mark one pending so the next wait is skipped. */\n private signal(): void {\n this.wakePending = true;\n this.wakeResolver?.();\n }\n\n /**\n * Idle wait that resolves after `ms`, or early when `signal()` fires. A signal\n * raised while no wait is in flight is remembered (wakePending), so a wake that\n * races a claim cycle is never lost.\n */\n private waitForWork(ms: number): Promise<void> {\n if (this.wakePending) {\n this.wakePending = false;\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n this.wakeResolver = null;\n resolve();\n }, ms);\n this.wakeResolver = () => {\n clearTimeout(timer);\n this.wakeResolver = null;\n this.wakePending = false;\n resolve();\n };\n });\n }\n}\n","import type { StandardSchemaV1 } from \"./standard-schema.js\";\n\n/**\n * Thrown when an outbox payload fails its topic's schema — at `enqueue`\n * (before the DB insert) or at `decode` (malformed bytes / schema mismatch).\n * Carries the topic and the validator's structured issues for diagnostics.\n */\nexport class OutboxValidationError extends Error {\n readonly topic: string;\n readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;\n\n constructor(\n topic: string,\n issues: ReadonlyArray<StandardSchemaV1.Issue>,\n options?: { cause?: unknown },\n ) {\n const first = issues[0]?.message ?? \"unknown validation error\";\n super(`Outbox payload for \"${topic}\" failed validation: ${first}`, options);\n this.name = \"OutboxValidationError\";\n this.topic = topic;\n this.issues = issues;\n }\n}\n","import { OutboxValidationError } from \"./errors.js\";\nimport type { StandardSchemaV1 } from \"./standard-schema.js\";\nimport type { OutboxMessageInput } from \"./types.js\";\n\n/** One topic's contract: the aggregate it belongs to and its payload schema. */\nexport interface TopicDefinition {\n /** Aggregate type stamped on every event of this topic (e.g. \"order\"). */\n readonly aggregateType: string;\n /** Standard Schema for the payload (Zod 3.24+/Valibot/ArkType/…). */\n readonly schema: StandardSchemaV1;\n}\n\n/** A map of topic name -> its definition. The single source of truth. */\nexport type OutboxRegistry = Record<string, TopicDefinition>;\n\n/**\n * Minimal store surface the producer facade needs. `PostgresStore` satisfies\n * this structurally, so the facade stays DB-agnostic and `tx` flows from the\n * concrete store (e.g. a pg client).\n */\nexport interface EnqueueableStore<Tx = unknown> {\n enqueue(tx: Tx, msg: OutboxMessageInput & { traceId?: string }): Promise<string>;\n}\n\ntype TxOf<S> = S extends EnqueueableStore<infer Tx> ? Tx : never;\n\ntype PayloadInput<R extends OutboxRegistry, K extends keyof R> =\n StandardSchemaV1.InferInput<R[K][\"schema\"]>;\ntype PayloadOutput<R extends OutboxRegistry, K extends keyof R> =\n StandardSchemaV1.InferOutput<R[K][\"schema\"]>;\n\n/** The write-side input for a typed enqueue (payload is the schema's input type). */\nexport interface EnqueueInput<R extends OutboxRegistry, K extends keyof R> {\n aggregateId: string;\n payload: PayloadInput<R, K>;\n key?: string;\n headers?: Record<string, string>;\n messageId?: string;\n traceId?: string;\n}\n\n/** Consumer-side facade: validate/decode without a store. */\nexport interface OutboxConsumer<R extends OutboxRegistry> {\n /** Validate an already-parsed value against the topic's schema. */\n validate<K extends keyof R & string>(\n topic: K,\n value: unknown,\n ): Promise<PayloadOutput<R, K>>;\n /** JSON-parse `bytes`, validate against the topic's schema, return the payload. */\n decode<K extends keyof R & string>(\n topic: K,\n bytes: Buffer | Uint8Array | string,\n ): Promise<PayloadOutput<R, K>>;\n}\n\n/** Producer-side facade: adds typed, validated enqueue. */\nexport interface OutboxProducer<R extends OutboxRegistry, Tx>\n extends OutboxConsumer<R> {\n /** Validate `payload`, then enqueue inside the caller's transaction `tx`. */\n enqueue<K extends keyof R & string>(\n tx: Tx,\n topic: K,\n msg: EnqueueInput<R, K>,\n ): Promise<string>;\n}\n\n// Loose shape used inside the implementation; the public types come from overloads.\ninterface LooseEnqueueInput {\n aggregateId: string;\n payload: unknown;\n key?: string;\n headers?: Record<string, string>;\n messageId?: string;\n traceId?: string;\n}\n\nexport function defineOutbox<R extends OutboxRegistry>(\n registry: R,\n): OutboxConsumer<R>;\nexport function defineOutbox<R extends OutboxRegistry, S extends EnqueueableStore>(\n registry: R,\n opts: { store: S },\n): OutboxProducer<R, TxOf<S>>;\nexport function defineOutbox<R extends OutboxRegistry>(\n registry: R,\n opts?: { store: EnqueueableStore },\n): OutboxConsumer<R> | OutboxProducer<R, unknown> {\n const validate = async (topic: string, value: unknown): Promise<unknown> => {\n const def = registry[topic];\n if (!def) {\n throw new OutboxValidationError(topic, [\n { message: `unknown topic \"${topic}\"` },\n ]);\n }\n const result = await def.schema[\"~standard\"].validate(value);\n if (result.issues) throw new OutboxValidationError(topic, result.issues);\n return result.value;\n };\n\n const decode = async (\n topic: string,\n bytes: Buffer | Uint8Array | string,\n ): Promise<unknown> => {\n const text =\n typeof bytes === \"string\" ? bytes : Buffer.from(bytes).toString(\"utf8\");\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err) {\n throw new OutboxValidationError(\n topic,\n [{ message: `invalid JSON: ${(err as Error).message}` }],\n { cause: err },\n );\n }\n return validate(topic, parsed);\n };\n\n const consumer = { validate, decode } as unknown as OutboxConsumer<R>;\n if (!opts?.store) return consumer;\n\n const store = opts.store;\n const enqueue = async (\n tx: unknown,\n topic: string,\n msg: LooseEnqueueInput,\n ): Promise<string> => {\n const def = registry[topic];\n if (!def) {\n throw new OutboxValidationError(topic, [\n { message: `unknown topic \"${topic}\"` },\n ]);\n }\n const payload = await validate(topic, msg.payload);\n return store.enqueue(tx, {\n topic,\n aggregateType: def.aggregateType,\n aggregateId: msg.aggregateId,\n payload,\n key: msg.key,\n headers: msg.headers,\n messageId: msg.messageId,\n traceId: msg.traceId,\n });\n };\n\n return { validate, decode, enqueue } as unknown as OutboxProducer<R, unknown>;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventferry/core",
3
- "version": "2.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "DB- and broker-agnostic core for the transactional outbox pattern",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",