@classytic/arc 2.8.0 → 2.8.1

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.
Files changed (69) hide show
  1. package/README.md +10 -1
  2. package/dist/{BaseController-CpMfCXdn.mjs → BaseController-DAGGc5Xn.mjs} +76 -25
  3. package/dist/{EventTransport-n1KBxC_N.d.mts → EventTransport-CLXJUzyT.d.mts} +37 -1
  4. package/dist/{ResourceRegistry-BOtJuRCs.mjs → ResourceRegistry-Dtcojmu8.mjs} +14 -2
  5. package/dist/adapters/index.d.mts +2 -2
  6. package/dist/adapters/index.mjs +1 -1
  7. package/dist/{adapters-BxGgSHjj.mjs → adapters-BBqAVvPK.mjs} +11 -0
  8. package/dist/auth/index.d.mts +1 -1
  9. package/dist/auth/index.mjs +3 -3
  10. package/dist/{betterAuthOpenApi-CHCIuA-p.mjs → betterAuthOpenApi-C5lDyRH2.mjs} +1 -1
  11. package/dist/cli/commands/docs.mjs +2 -2
  12. package/dist/cli/commands/introspect.mjs +1 -1
  13. package/dist/core/index.d.mts +2 -2
  14. package/dist/core/index.mjs +4 -4
  15. package/dist/{core-BfrfxNqO.mjs → core-CrLDuqoT.mjs} +1 -1
  16. package/dist/{createActionRouter-CbkIAaGh.mjs → createActionRouter-Df1BuawX.mjs} +87 -21
  17. package/dist/{createApp-Cy8eUNKQ.mjs → createApp-p2OThysU.mjs} +2 -2
  18. package/dist/{defineResource-CovBXvTB.mjs → defineResource-CqeUltrW.mjs} +19 -7
  19. package/dist/docs/index.d.mts +1 -1
  20. package/dist/docs/index.mjs +1 -1
  21. package/dist/dynamic/index.d.mts +1 -1
  22. package/dist/dynamic/index.mjs +1 -1
  23. package/dist/{errorHandler-BW08lEiy.mjs → errorHandler-Cw34h_om.mjs} +1 -1
  24. package/dist/{errorHandler-BeN-ERN7.d.mts → errorHandler-DJ7OAB2V.d.mts} +1 -1
  25. package/dist/{eventPlugin-CAOWMQS8.d.mts → eventPlugin-Cdjwo0Gv.d.mts} +1 -1
  26. package/dist/{eventPlugin-x4jo3sG0.mjs → eventPlugin-XijlQmlL.mjs} +19 -1
  27. package/dist/events/index.d.mts +399 -28
  28. package/dist/events/index.mjs +345 -29
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/events/transports/redis.d.mts +1 -1
  31. package/dist/factory/index.d.mts +1 -1
  32. package/dist/factory/index.mjs +1 -1
  33. package/dist/hooks/index.d.mts +1 -1
  34. package/dist/{index-BpMhrFgn.d.mts → index-0zj73o2U.d.mts} +1 -1
  35. package/dist/{index-qct60lnl.d.mts → index-DadoLP51.d.mts} +35 -3
  36. package/dist/index.d.mts +4 -4
  37. package/dist/index.mjs +7 -7
  38. package/dist/integrations/event-gateway.d.mts +1 -1
  39. package/dist/integrations/index.d.mts +1 -1
  40. package/dist/integrations/mcp/index.d.mts +2 -2
  41. package/dist/integrations/mcp/index.mjs +1 -1
  42. package/dist/integrations/mcp/testing.d.mts +1 -1
  43. package/dist/integrations/mcp/testing.mjs +1 -1
  44. package/dist/{interface-IJqN3pXK.d.mts → interface-CS6d7HiB.d.mts} +549 -107
  45. package/dist/{openapi-AYLVjqVe.mjs → openapi-q6rNKfZy.mjs} +49 -2
  46. package/dist/org/index.d.mts +1 -1
  47. package/dist/plugins/index.d.mts +2 -2
  48. package/dist/plugins/index.mjs +3 -3
  49. package/dist/plugins/tracing-entry.mjs +1 -1
  50. package/dist/presets/index.d.mts +3 -3
  51. package/dist/presets/multiTenant.d.mts +1 -1
  52. package/dist/{redis-stream-CF1lrKVk.d.mts → redis-stream-BgrYzpeq.d.mts} +1 -1
  53. package/dist/registry/index.d.mts +1 -1
  54. package/dist/registry/index.mjs +1 -1
  55. package/dist/{resourceToTools-C_1SMiCz.mjs → resourceToTools-DNNWnZtx.mjs} +193 -63
  56. package/dist/rpc/index.mjs +1 -1
  57. package/dist/testing/index.d.mts +2 -2
  58. package/dist/testing/index.mjs +1 -1
  59. package/dist/types/index.d.mts +2 -2
  60. package/dist/{types-gUxAIZHp.d.mts → types-BlOuKTPw.d.mts} +4 -4
  61. package/dist/{types-Ct0PUUSp.d.mts → types-D3b7hA00.d.mts} +1 -1
  62. package/dist/utils/index.d.mts +2 -14
  63. package/dist/utils/index.mjs +5 -5
  64. package/dist/{utils-B-l6410F.mjs → utils-7sJ8X83I.mjs} +1 -13
  65. package/package.json +4 -3
  66. /package/dist/{circuitBreaker-l18oRgL5.mjs → circuitBreaker-cmi5XDv5.mjs} +0 -0
  67. /package/dist/{errors-Cg58SLNi.mjs → errors-BF2bIOIS.mjs} +0 -0
  68. /package/dist/{requestContext-xHIKedG6.mjs → requestContext-DYvHl113.mjs} +0 -0
  69. /package/dist/{schemaConverter-Y5EejTnJ.mjs → schemaConverter-OxfCshus.mjs} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { p as isArcError } from "./errors-Cg58SLNi.mjs";
2
+ import { p as isArcError } from "./errors-BF2bIOIS.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/plugins/errorHandler.ts
5
5
  var errorHandler_exports = /* @__PURE__ */ __exportAll({ errorHandlerPlugin: () => errorHandlerPlugin });
@@ -1,4 +1,4 @@
1
- import { t as DomainEvent } from "./EventTransport-n1KBxC_N.mjs";
1
+ import { t as DomainEvent } from "./EventTransport-CLXJUzyT.mjs";
2
2
  import { FastifyInstance, FastifyPluginAsync, FastifyRequest } from "fastify";
3
3
 
4
4
  //#region src/plugins/caching.d.ts
@@ -1,4 +1,4 @@
1
- import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-n1KBxC_N.mjs";
1
+ import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-CLXJUzyT.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/events/defineEvent.d.ts
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { t as requestContext } from "./requestContext-xHIKedG6.mjs";
2
+ import { t as requestContext } from "./requestContext-DYvHl113.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/events/EventTransport.ts
5
5
  /**
@@ -33,6 +33,24 @@ var MemoryEventTransport = class {
33
33
  this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
34
34
  }
35
35
  }
36
+ /**
37
+ * Reference `publishMany` implementation — delegates to `publish()` in order.
38
+ *
39
+ * Production transports (Kafka, Redis pipeline, SQS batch) should override
40
+ * this with a single batched network call. Memory transport has nothing to
41
+ * batch, so we just loop — the loop still returns a proper result map so
42
+ * `EventOutbox.relay` can exercise the batched code path in tests.
43
+ */
44
+ async publishMany(events) {
45
+ const results = /* @__PURE__ */ new Map();
46
+ for (const event of events) try {
47
+ await this.publish(event);
48
+ results.set(event.meta.id, null);
49
+ } catch (err) {
50
+ results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
51
+ }
52
+ return results;
53
+ }
36
54
  async subscribe(pattern, handler) {
37
55
  if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
38
56
  this.handlers.get(pattern)?.add(handler);
@@ -1,7 +1,7 @@
1
- import { a as MemoryEventTransport, i as EventTransport, n as EventHandler, o as MemoryEventTransportOptions, r as EventLogger, s as createEvent, t as DomainEvent } from "../EventTransport-n1KBxC_N.mjs";
2
- import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-CAOWMQS8.mjs";
1
+ import { a as MemoryEventTransport, c as createEvent, i as EventTransport, n as EventHandler, o as MemoryEventTransportOptions, r as EventLogger, s as PublishManyResult, t as DomainEvent } from "../EventTransport-CLXJUzyT.mjs";
2
+ import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-Cdjwo0Gv.mjs";
3
3
  import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
4
- import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-CF1lrKVk.mjs";
4
+ import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-BgrYzpeq.mjs";
5
5
 
6
6
  //#region src/events/eventTypes.d.ts
7
7
  /**
@@ -51,68 +51,439 @@ declare const CACHE_EVENTS: Readonly<{
51
51
  type CacheEvent = (typeof CACHE_EVENTS)[keyof typeof CACHE_EVENTS];
52
52
  //#endregion
53
53
  //#region src/events/outbox.d.ts
54
+ /**
55
+ * **Terminology (v2.8.1+):**
56
+ *
57
+ * Arc's outbox uses **`delivered`** as the canonical term for "event has been
58
+ * successfully published to the transport and marked by `acknowledge()`".
59
+ *
60
+ * - **`acknowledge()`** is the operation that transitions an event to delivered
61
+ * - **`delivered`** is the resulting state
62
+ * - **`deliveredAt`** is the timestamp column/field in every reference implementation
63
+ *
64
+ * Store authors should use `deliveredAt` for their timestamp field. Older
65
+ * drafts of these docs used `acknowledgedAt` — that is a deprecated alias and
66
+ * should not be used in new code.
67
+ */
68
+ /**
69
+ * Options passed to {@link OutboxStore.save} for richer write semantics.
70
+ * Stores may ignore fields they don't support — contract is best-effort.
71
+ */
72
+ interface OutboxWriteOptions {
73
+ /**
74
+ * Host-provided DB session/transaction handle for atomic writes.
75
+ * Typed as `unknown` so stores can accept any backend (mongoose session,
76
+ * pg client, Prisma tx, etc.) without Arc taking a peer dep.
77
+ */
78
+ readonly session?: unknown;
79
+ /** Earliest time the event should be visible to relay workers (for delayed publishing) */
80
+ readonly visibleAt?: Date;
81
+ /** Idempotency key — stores that support it should dedupe on this */
82
+ readonly dedupeKey?: string;
83
+ /** Partition/routing key for sharded transports (Kafka, Redis Streams) */
84
+ readonly partitionKey?: string;
85
+ /** Arbitrary headers propagated to the transport layer */
86
+ readonly headers?: Readonly<Record<string, string>>;
87
+ }
88
+ /**
89
+ * Options for {@link OutboxStore.claimPending} — lease-based work claim.
90
+ * Supports safe multi-worker relay: each worker atomically claims a batch
91
+ * with a lease TTL, preventing duplicate publishes.
92
+ */
93
+ interface OutboxClaimOptions {
94
+ /** Max events to claim (default: batchSize) */
95
+ readonly limit?: number;
96
+ /** Unique identifier for the claiming worker */
97
+ readonly consumerId?: string;
98
+ /** Lease duration in ms — claim is released automatically after this */
99
+ readonly leaseMs?: number;
100
+ /** Only claim events of these types (optional filter) */
101
+ readonly types?: readonly string[];
102
+ }
103
+ /** Options for {@link OutboxStore.acknowledge} */
104
+ interface OutboxAcknowledgeOptions {
105
+ /** Worker identifier — stores may enforce "only owner can ack" */
106
+ readonly consumerId?: string;
107
+ }
108
+ /** Options for {@link OutboxStore.fail} */
109
+ interface OutboxFailOptions {
110
+ /** Worker identifier — stores may enforce "only owner can fail" */
111
+ readonly consumerId?: string;
112
+ /** Schedule retry for a later time (implements backoff) */
113
+ readonly retryAt?: Date;
114
+ /** Move the event to dead-letter state (no further retries) */
115
+ readonly deadLetter?: boolean;
116
+ }
117
+ /** Normalized error info passed to {@link OutboxStore.fail} */
118
+ interface OutboxErrorInfo {
119
+ readonly message: string;
120
+ readonly code?: string;
121
+ }
122
+ /**
123
+ * Thrown by a store when `acknowledge` / `fail` is called by a consumer that
124
+ * does not own the event's current lease.
125
+ *
126
+ * Stores that enforce lease ownership MUST throw this (not silently return)
127
+ * so {@link EventOutbox} can detect the mismatch and avoid over-counting
128
+ * successful deliveries. {@link EventOutbox.relay} catches it and reports
129
+ * via {@link EventOutboxOptions.onError} instead of counting as relayed.
130
+ */
131
+ declare class OutboxOwnershipError extends Error {
132
+ readonly eventId: string;
133
+ readonly attemptedBy: string;
134
+ readonly currentOwner: string | null;
135
+ constructor(eventId: string, attemptedBy: string, currentOwner: string | null);
136
+ }
137
+ /** Thrown by {@link EventOutbox.store} when an event is missing `type` or `meta.id`. */
138
+ declare class InvalidOutboxEventError extends Error {
139
+ constructor(reason: string);
140
+ }
141
+ /**
142
+ * Durable storage contract for the transactional outbox pattern.
143
+ *
144
+ * **Required methods**: `save`, `getPending`, `acknowledge`.
145
+ *
146
+ * **Optional capabilities** — stores opt-in to richer semantics:
147
+ * - `claimPending` — lease-based work claim for multi-worker relay
148
+ * - `fail` — failure tracking with retry scheduling and dead-letter
149
+ * - `purge` — acknowledged event cleanup
150
+ *
151
+ * Arc's {@link EventOutbox} detects capabilities at runtime and degrades
152
+ * gracefully for legacy stores that only implement the required methods.
153
+ *
154
+ * ## Required semantics
155
+ *
156
+ * Implementations MUST honor these contracts — `EventOutbox` depends on them
157
+ * for correctness of at-least-once delivery:
158
+ *
159
+ * 1. **`save` must reject invalid events.** If `event.type` or `event.meta.id`
160
+ * is missing/empty, throw — do not persist. `EventOutbox.store()` validates
161
+ * first, but stores should defend against direct-save code paths.
162
+ *
163
+ * 2. **`claimPending` must be atomic.** Two workers calling `claimPending`
164
+ * concurrently must never receive the same event. Stores backed by SQL/Mongo
165
+ * should use `SELECT ... FOR UPDATE SKIP LOCKED` or `findOneAndUpdate` with
166
+ * `{ leaseOwner: null }` as the match condition.
167
+ *
168
+ * 3. **`acknowledge` / `fail` must throw {@link OutboxOwnershipError} on
169
+ * ownership mismatch.** When `options.consumerId` is provided and does not
170
+ * match the current lease owner, the method MUST throw — never silently
171
+ * no-op. `EventOutbox.relay` relies on this signal to avoid counting
172
+ * hijacked events as successfully relayed.
173
+ *
174
+ * 4. **`acknowledge` on an unknown `eventId` is a no-op.** This keeps relay
175
+ * idempotent when the store has been purged or the event was manually
176
+ * removed. Do NOT throw `OutboxOwnershipError` in this case.
177
+ *
178
+ * 5. **`fail` must deterministically update lease/visibility.** On success,
179
+ * the event MUST become re-claimable (either immediately or at `retryAt`),
180
+ * or transition to dead-letter state. The lease owner should be cleared.
181
+ *
182
+ * 6. **Malformed events must never be returned by `getPending`/`claimPending`.**
183
+ * If the store's underlying storage has corrupt rows, the store is
184
+ * responsible for quarantining them (e.g., direct DB delete, DLQ).
185
+ */
54
186
  interface OutboxStore {
55
- /** Save event to outbox (called within business transaction) */
56
- save(event: DomainEvent): Promise<void>;
57
- /** Get pending (unrelayed) events, ordered FIFO */
187
+ /**
188
+ * Save event to outbox (typically called within a business transaction).
189
+ *
190
+ * MUST reject events missing `type` or `meta.id` — throw rather than persist.
191
+ *
192
+ * @param event - Event to persist
193
+ * @param options - Optional write metadata (session, visibleAt, dedupeKey, etc.)
194
+ */
195
+ save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
196
+ /**
197
+ * Get pending (unrelayed) events, ordered FIFO.
198
+ *
199
+ * This is the legacy/simple pull API. Multi-worker deployments should
200
+ * prefer {@link OutboxStore.claimPending} to avoid duplicate publishes.
201
+ *
202
+ * Events returned by this method MUST be well-formed (valid `type` and
203
+ * `meta.id`). Corrupt rows must be quarantined by the store, not exposed
204
+ * to the relay loop.
205
+ */
58
206
  getPending(limit: number): Promise<DomainEvent[]>;
59
- /** Mark event as successfully relayed */
60
- acknowledge(eventId: string): Promise<void>;
61
207
  /**
62
- * Purge old acknowledged events (optional, DB-agnostic contract).
208
+ * Atomically claim pending events with a lease.
209
+ *
210
+ * When implemented, {@link EventOutbox.relay} prefers this over `getPending`
211
+ * so multi-worker relay is safe: each worker holds an exclusive lease on
212
+ * its batch, and stale leases are automatically recovered.
213
+ *
214
+ * **Atomicity is mandatory**: two concurrent callers must never receive
215
+ * overlapping events. Use `SELECT ... FOR UPDATE SKIP LOCKED` (SQL) or
216
+ * a compound condition on `findOneAndUpdate` (Mongo).
217
+ */
218
+ claimPending?(options?: OutboxClaimOptions): Promise<DomainEvent[]>;
219
+ /**
220
+ * Mark event as successfully relayed.
221
+ *
222
+ * **Ownership contract**: If `options.consumerId` is provided and does not
223
+ * match the current lease owner, this method MUST throw
224
+ * {@link OutboxOwnershipError}. Unknown `eventId` is a no-op.
225
+ *
226
+ * @param eventId - Event ID (from `meta.id`)
227
+ * @param options - Optional ack metadata (consumerId for lease enforcement)
228
+ * @throws {@link OutboxOwnershipError} on ownership mismatch
229
+ */
230
+ acknowledge(eventId: string, options?: OutboxAcknowledgeOptions): Promise<void>;
231
+ /**
232
+ * Record a relay failure. When implemented, {@link EventOutbox.relay} calls
233
+ * this instead of stopping the batch — enables retry scheduling and DLQ.
234
+ *
235
+ * **Ownership contract**: Same as `acknowledge` — MUST throw
236
+ * {@link OutboxOwnershipError} on mismatch. After a successful call, the
237
+ * lease owner MUST be cleared and the event MUST be re-claimable (at
238
+ * `retryAt` if provided) or transitioned to dead-letter.
239
+ *
240
+ * @throws {@link OutboxOwnershipError} on ownership mismatch
241
+ */
242
+ fail?(eventId: string, error: OutboxErrorInfo, options?: OutboxFailOptions): Promise<void>;
243
+ /**
244
+ * Purge old **delivered** events (optional, DB-agnostic contract).
245
+ *
246
+ * Cleanup is scoped to events in the `delivered` state — events still
247
+ * pending, failed, or dead-lettered MUST NOT be removed by purge.
63
248
  *
64
249
  * Arc does **not** ship a concrete implementation — your store owns the
65
250
  * cleanup strategy that fits your database:
66
251
  *
67
- * - **MongoDB:** TTL index on `acknowledgedAt` (automatic, zero-code)
68
- * - **SQL:** Scheduled `DELETE FROM outbox WHERE acknowledged_at < :cutoff`
69
- * - **Redis:** Key expiry (`EXPIRE`) on acknowledged entries
252
+ * - **MongoDB:** TTL index on `deliveredAt` (automatic, zero-code)
253
+ * - **SQL:** Scheduled `DELETE FROM outbox WHERE status = 'delivered' AND delivered_at < :cutoff`
254
+ * - **Redis:** Key expiry (`EXPIRE`) on delivered entries
70
255
  *
71
256
  * Called by {@link EventOutbox.purge}. If not implemented, cleanup is
72
257
  * entirely the app's responsibility via native DB tools.
73
258
  *
74
- * @param olderThanMs - Remove events acknowledged more than this many ms ago
259
+ * @param olderThanMs - Remove events delivered more than this many ms ago
75
260
  * @returns Number of purged events
76
261
  */
77
262
  purge?(olderThanMs: number): Promise<number>;
78
263
  }
264
+ /** Reason codes passed to {@link EventOutboxOptions.onError}. */
265
+ type OutboxRelayErrorKind = "publish_failed" | "acknowledge_failed" | "fail_failed" | "ownership_mismatch" | "malformed_event";
266
+ /**
267
+ * Rich per-batch outcome returned by {@link EventOutbox.relayBatch}.
268
+ *
269
+ * Useful for operational dashboards, alerting thresholds, and test assertions.
270
+ * The simpler {@link EventOutbox.relay} returns just the `relayed` count for
271
+ * backward compatibility.
272
+ */
273
+ interface RelayResult {
274
+ /** Number of events successfully published AND acknowledged */
275
+ readonly relayed: number;
276
+ /** Number of events claimed and attempted in this batch */
277
+ readonly attempted: number;
278
+ /** Number of publish failures (transport rejected the event) */
279
+ readonly publishFailed: number;
280
+ /** Number of acknowledge failures after successful publish (at-least-once replay risk) */
281
+ readonly ackFailed: number;
282
+ /** Number of ownership mismatches (our lease expired mid-flight) */
283
+ readonly ownershipMismatches: number;
284
+ /** Number of malformed events encountered (aborts the batch) */
285
+ readonly malformed: number;
286
+ /** Number of fail() calls that themselves threw (store bugs / contention) */
287
+ readonly failHookErrors: number;
288
+ /** Whether `publishMany` was used (true) or per-event `publish` (false) */
289
+ readonly usedPublishMany: boolean;
290
+ }
291
+ /**
292
+ * Called by {@link EventOutbox.relay} when a non-fatal error occurs during
293
+ * a batch. Used for logging and metrics. Must not throw.
294
+ */
295
+ type OutboxRelayErrorHandler = (info: {
296
+ readonly kind: OutboxRelayErrorKind;
297
+ readonly event?: DomainEvent;
298
+ readonly error: Error;
299
+ }) => void;
79
300
  interface EventOutboxOptions {
80
301
  /** Outbox store for persistence */
81
- store: OutboxStore;
302
+ readonly store: OutboxStore;
82
303
  /** Transport to relay events to (optional — can relay later) */
83
- transport?: EventTransport;
304
+ readonly transport?: EventTransport;
84
305
  /** Max events per relay batch (default: 100) */
85
- batchSize?: number;
306
+ readonly batchSize?: number;
307
+ /**
308
+ * Unique identifier for this relay worker. Used when the store supports
309
+ * `claimPending`/`fail` to enforce lease ownership. Defaults to a random ID.
310
+ */
311
+ readonly consumerId?: string;
312
+ /**
313
+ * Lease duration in ms for claimed events. Only used when the store
314
+ * supports `claimPending`. Default: 30 seconds.
315
+ */
316
+ readonly leaseMs?: number;
317
+ /**
318
+ * Callback for non-fatal errors during relay: publish failures,
319
+ * ownership mismatches, ack/fail errors, malformed events. Use this for
320
+ * logging and metrics. Must not throw — exceptions are swallowed.
321
+ */
322
+ readonly onError?: OutboxRelayErrorHandler;
323
+ /**
324
+ * Enable {@link EventTransport.publishMany} when the transport implements it.
325
+ * Default: `true`. Set to `false` to force per-event `publish()` — useful
326
+ * for transports where strict event-order observability matters more than
327
+ * throughput, or to debug batch-specific issues.
328
+ */
329
+ readonly usePublishMany?: boolean;
86
330
  }
87
331
  declare class EventOutbox {
88
332
  private readonly _store;
89
333
  private readonly _transport?;
90
334
  private readonly _batchSize;
335
+ private readonly _consumerId;
336
+ private readonly _leaseMs;
337
+ private readonly _onError?;
338
+ private readonly _usePublishMany;
91
339
  constructor(opts: EventOutboxOptions);
92
- /** Store event in outbox (call within your DB transaction) */
93
- store(event: DomainEvent): Promise<void>;
340
+ /** Unique consumer ID used for lease ownership when the store supports claims */
341
+ get consumerId(): string;
342
+ /**
343
+ * Store event in outbox.
344
+ *
345
+ * Validates that `event.type` and `event.meta.id` are present — throws
346
+ * {@link InvalidOutboxEventError} otherwise, so corrupt rows can never
347
+ * be persisted via this API.
348
+ *
349
+ * Pass `options.session` to participate in a host-managed DB transaction
350
+ * (store must support session-aware writes). Other options (`visibleAt`,
351
+ * `dedupeKey`, `partitionKey`, `headers`) are forwarded to stores that
352
+ * implement them and ignored otherwise.
353
+ */
354
+ store(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
355
+ private _reportError;
94
356
  /**
95
- * Relay pending events to transport.
357
+ * Relay pending events to transport and return the number of successful
358
+ * publish+acknowledge pairs.
96
359
  *
97
- * Processes events in FIFO order up to `batchSize`. Stops on the first
98
- * transport failure remaining events stay pending for the next relay call.
360
+ * For richer observability (per-kind counts, publishMany detection, etc.)
361
+ * use {@link relayBatch} which returns a {@link RelayResult}. This method
362
+ * is the backward-compatible shortcut that returns just the count.
99
363
  *
100
- * @returns Number of successfully published events in this batch
364
+ * @returns Number of successfully published AND acknowledged events
101
365
  */
102
366
  relay(): Promise<number>;
103
367
  /**
104
- * Purge old acknowledged events from the outbox store.
368
+ * Relay a batch of pending events to the transport and return a rich
369
+ * {@link RelayResult} describing the outcome of each event.
370
+ *
371
+ * Behavior summary:
372
+ *
373
+ * - **Claim path**: uses {@link OutboxStore.claimPending} when the store
374
+ * supports it (safe for multi-worker relay) or falls back to
375
+ * {@link OutboxStore.getPending} (single-worker only).
376
+ *
377
+ * - **Publish path**: if the transport implements
378
+ * {@link EventTransport.publishMany} and `usePublishMany` is not disabled,
379
+ * the entire batch is sent in one call. Otherwise each event is published
380
+ * individually. Either way, per-event outcomes are tracked.
381
+ *
382
+ * - **Failure path**: if the store implements `fail`, per-event failures
383
+ * are reported via `store.fail(...)` and the batch continues. Without
384
+ * `fail`, the batch stops on the first failure (legacy behavior).
385
+ *
386
+ * - **Malformed events**: events missing `type` or `meta.id` abort the
387
+ * batch — a well-behaved store must never return them (see
388
+ * {@link OutboxStore} semantics #6). The error is reported via `onError`.
389
+ *
390
+ * - **Ownership mismatches**: if `acknowledge`/`fail` throws
391
+ * {@link OutboxOwnershipError} (our lease expired and another worker
392
+ * claimed the event), the event is NOT counted as relayed. The other
393
+ * worker will re-publish — at-least-once semantics preserved.
394
+ *
395
+ * @returns Per-kind outcome counts for the batch
396
+ */
397
+ relayBatch(): Promise<RelayResult>;
398
+ /**
399
+ * Purge old **delivered** events from the outbox store.
105
400
  * Delegates to `store.purge()` if implemented; no-op otherwise.
106
- * @param olderThanMs - Remove events acknowledged more than this many ms ago (default: 7 days)
401
+ * @param olderThanMs - Remove events delivered more than this many ms ago (default: 7 days)
107
402
  * @returns Number of purged events, or 0 if store doesn't support purge
108
403
  */
109
404
  purge(olderThanMs?: number): Promise<number>;
110
405
  }
406
+ interface MemoryEntry {
407
+ event: DomainEvent;
408
+ status: "pending" | "delivered" | "dead_letter";
409
+ attempts: number;
410
+ visibleAt: number;
411
+ leaseOwner: string | null;
412
+ leaseExpiresAt: number;
413
+ deliveredAt: number | null;
414
+ lastError: OutboxErrorInfo | null;
415
+ dedupeKey?: string;
416
+ }
417
+ /**
418
+ * In-memory outbox store — reference implementation supporting the full
419
+ * capability set (claim/lease, fail/retry, dedupe, visibleAt).
420
+ *
421
+ * For dev/testing only. Production deployments should use a durable store
422
+ * backed by the app's database.
423
+ */
111
424
  declare class MemoryOutboxStore implements OutboxStore {
112
- private events;
113
- save(event: DomainEvent): Promise<void>;
425
+ private readonly entries;
426
+ private readonly seenDedupeKeys;
427
+ save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
114
428
  getPending(limit: number): Promise<DomainEvent[]>;
115
- acknowledge(eventId: string): Promise<void>;
429
+ claimPending(options?: OutboxClaimOptions): Promise<DomainEvent[]>;
430
+ acknowledge(eventId: string, options?: OutboxAcknowledgeOptions): Promise<void>;
431
+ fail(eventId: string, error: OutboxErrorInfo, options?: OutboxFailOptions): Promise<void>;
432
+ purge(olderThanMs: number): Promise<number>;
433
+ /** Test helper: inspect entry by id */
434
+ _getEntry(eventId: string): Readonly<MemoryEntry> | undefined;
116
435
  }
436
+ /**
437
+ * Options for {@link exponentialBackoff}.
438
+ */
439
+ interface ExponentialBackoffOptions {
440
+ /** Current attempt count (1-indexed — first retry is attempt 1) */
441
+ readonly attempt: number;
442
+ /** Base delay in ms (first retry delay). Default: 1000 (1 second) */
443
+ readonly baseMs?: number;
444
+ /** Maximum delay in ms — caps exponential growth. Default: 60_000 (1 minute) */
445
+ readonly maxMs?: number;
446
+ /**
447
+ * Jitter factor [0–1]. The returned delay is multiplied by
448
+ * `1 + (random * jitter)` to spread retry bursts across workers.
449
+ * Default: 0.2 (±20%). Set to 0 to disable.
450
+ */
451
+ readonly jitter?: number;
452
+ /** Reference time (for deterministic tests). Default: `Date.now()` */
453
+ readonly now?: number;
454
+ }
455
+ /**
456
+ * Compute a `retryAt` `Date` using exponential backoff with jitter.
457
+ *
458
+ * This is a convenience helper for store authors implementing
459
+ * {@link OutboxStore.fail}: call it to compute the retry visibility window
460
+ * based on the event's current attempt count.
461
+ *
462
+ * Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
463
+ *
464
+ * @example Basic usage inside a store's `fail()` method
465
+ * ```typescript
466
+ * async fail(eventId, error, options) {
467
+ * const entry = await this.findById(eventId);
468
+ * entry.attempts++;
469
+ * if (entry.attempts >= MAX_ATTEMPTS) {
470
+ * return this.deadLetter(eventId, error);
471
+ * }
472
+ * const retryAt = exponentialBackoff({ attempt: entry.attempts });
473
+ * entry.visibleAt = retryAt;
474
+ * await this.update(entry);
475
+ * }
476
+ * ```
477
+ *
478
+ * @example Tuning for a faster transport
479
+ * ```typescript
480
+ * exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
481
+ * // attempt=1 → ~250ms ±30%
482
+ * // attempt=2 → ~500ms ±30%
483
+ * // attempt=3 → ~1000ms ±30%
484
+ * // attempt=10 → capped at 10_000ms
485
+ * ```
486
+ */
487
+ declare function exponentialBackoff(options: ExponentialBackoffOptions): Date;
117
488
  //#endregion
118
- export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type CustomValidator, type DomainEvent, type EventDefinitionInput, type EventDefinitionOutput, type EventHandler, type EventLogger, EventOutbox, type EventOutboxOptions, type EventPluginOptions, type EventRegistry, type EventRegistryOptions, type EventSchema, type EventTransport, MemoryEventTransport, type MemoryEventTransportOptions, MemoryOutboxStore, type OutboxStore, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RetryOptions, type ValidationResult, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, withRetry };
489
+ export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type CustomValidator, type DomainEvent, type EventDefinitionInput, type EventDefinitionOutput, type EventHandler, type EventLogger, EventOutbox, type EventOutboxOptions, type EventPluginOptions, type EventRegistry, type EventRegistryOptions, type EventSchema, type EventTransport, type ExponentialBackoffOptions, InvalidOutboxEventError, MemoryEventTransport, type MemoryEventTransportOptions, MemoryOutboxStore, type OutboxAcknowledgeOptions, type OutboxClaimOptions, type OutboxErrorInfo, type OutboxFailOptions, OutboxOwnershipError, type OutboxRelayErrorHandler, type OutboxRelayErrorKind, type OutboxStore, type OutboxWriteOptions, type PublishManyResult, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RelayResult, type RetryOptions, type ValidationResult, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, withRetry };