@classytic/arc 2.7.7 → 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 (119) hide show
  1. package/README.md +11 -2
  2. package/dist/{BaseController-CpMfCXdn.mjs → BaseController-DAGGc5Xn.mjs} +76 -25
  3. package/dist/{EventTransport-C4VheKeC.d.mts → EventTransport-CLXJUzyT.d.mts} +37 -1
  4. package/dist/{ResourceRegistry-DsHiG9cL.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/audit/index.d.mts +1 -1
  9. package/dist/audit/index.mjs +1 -1
  10. package/dist/audit/mongodb.d.mts +1 -1
  11. package/dist/audit/mongodb.mjs +1 -1
  12. package/dist/auth/index.d.mts +4 -4
  13. package/dist/auth/index.mjs +3 -3
  14. package/dist/auth/redis-session.d.mts +1 -1
  15. package/dist/{betterAuthOpenApi-EkPaMWNM.mjs → betterAuthOpenApi-C5lDyRH2.mjs} +1 -1
  16. package/dist/cache/index.d.mts +2 -2
  17. package/dist/cache/index.mjs +1 -1
  18. package/dist/cli/commands/docs.mjs +2 -2
  19. package/dist/cli/commands/generate.mjs +1 -1
  20. package/dist/cli/commands/introspect.mjs +1 -1
  21. package/dist/core/index.d.mts +2 -2
  22. package/dist/core/index.mjs +4 -3
  23. package/dist/core-CrLDuqoT.mjs +34 -0
  24. package/dist/{core-B_zEeA2b.mjs → createActionRouter-Df1BuawX.mjs} +88 -52
  25. package/dist/{createApp-D7e77m8C.mjs → createApp-p2OThysU.mjs} +10 -10
  26. package/dist/{defineResource-BW2dMCu9.mjs → defineResource-CqeUltrW.mjs} +91 -8
  27. package/dist/docs/index.d.mts +2 -2
  28. package/dist/docs/index.mjs +1 -1
  29. package/dist/dynamic/index.d.mts +2 -2
  30. package/dist/dynamic/index.mjs +1 -1
  31. package/dist/{elevation-By_p2lnn.mjs → elevation-BBGFjzIP.mjs} +1 -1
  32. package/dist/{errorHandler-CH8wk1eD.mjs → errorHandler-Cw34h_om.mjs} +1 -1
  33. package/dist/{errorHandler-pCpEtNd7.d.mts → errorHandler-DJ7OAB2V.d.mts} +1 -1
  34. package/dist/{eventPlugin-CdvUoUna.d.mts → eventPlugin-Cdjwo0Gv.d.mts} +1 -1
  35. package/dist/{eventPlugin-B6U_nCFU.mjs → eventPlugin-XijlQmlL.mjs} +19 -1
  36. package/dist/events/index.d.mts +399 -28
  37. package/dist/events/index.mjs +345 -29
  38. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  39. package/dist/events/transports/redis.d.mts +1 -1
  40. package/dist/factory/index.d.mts +1 -1
  41. package/dist/factory/index.mjs +1 -1
  42. package/dist/hooks/index.d.mts +1 -1
  43. package/dist/hooks/index.mjs +1 -1
  44. package/dist/idempotency/index.d.mts +3 -3
  45. package/dist/idempotency/mongodb.d.mts +1 -1
  46. package/dist/idempotency/redis.d.mts +1 -1
  47. package/dist/{index-C9eYNjGR.d.mts → index-0zj73o2U.d.mts} +1 -1
  48. package/dist/{index-B0extFr4.d.mts → index-CBru2y5Y.d.mts} +3 -3
  49. package/dist/{index-BjShrzoj.d.mts → index-DadoLP51.d.mts} +48 -16
  50. package/dist/index.d.mts +8 -8
  51. package/dist/index.mjs +8 -8
  52. package/dist/integrations/event-gateway.d.mts +1 -1
  53. package/dist/integrations/event-gateway.mjs +1 -1
  54. package/dist/integrations/index.d.mts +1 -1
  55. package/dist/integrations/mcp/index.d.mts +2 -2
  56. package/dist/integrations/mcp/index.mjs +1 -1
  57. package/dist/integrations/mcp/testing.d.mts +1 -1
  58. package/dist/integrations/mcp/testing.mjs +1 -1
  59. package/dist/{interface-B91alUzq.d.mts → interface-CS6d7HiB.d.mts} +693 -110
  60. package/dist/{mongodb-Cgu9F1Nd.d.mts → mongodb-B1eVtFhw.d.mts} +1 -1
  61. package/dist/{mongodb-B7zupyck.d.mts → mongodb-NShVZDMr.d.mts} +1 -1
  62. package/dist/{openapi-D7Z7VODz.mjs → openapi-q6rNKfZy.mjs} +49 -2
  63. package/dist/org/index.d.mts +2 -2
  64. package/dist/permissions/index.d.mts +3 -3
  65. package/dist/plugins/index.d.mts +4 -4
  66. package/dist/plugins/index.mjs +9 -9
  67. package/dist/plugins/tracing-entry.d.mts +1 -1
  68. package/dist/plugins/tracing-entry.mjs +1 -1
  69. package/dist/policies/index.d.mts +1 -1
  70. package/dist/presets/index.d.mts +3 -3
  71. package/dist/presets/multiTenant.d.mts +1 -1
  72. package/dist/{queryCachePlugin-Ckl71mkc.d.mts → queryCachePlugin-BCFVXnxK.d.mts} +1 -1
  73. package/dist/{redis-3TQxm2VZ.d.mts → redis-Bunu3qWg.d.mts} +1 -1
  74. package/dist/{redis-stream-Dag5LFa9.d.mts → redis-stream-BgrYzpeq.d.mts} +1 -1
  75. package/dist/registry/index.d.mts +1 -1
  76. package/dist/registry/index.mjs +2 -2
  77. package/dist/{resourceToTools-BJkoQoUP.mjs → resourceToTools-DNNWnZtx.mjs} +194 -64
  78. package/dist/rpc/index.d.mts +1 -1
  79. package/dist/rpc/index.mjs +1 -1
  80. package/dist/scope/index.d.mts +2 -2
  81. package/dist/scope/index.mjs +1 -1
  82. package/dist/{sse-6W0hjVS_.mjs → sse-CD5Hghpu.mjs} +1 -1
  83. package/dist/testing/index.d.mts +2 -2
  84. package/dist/testing/index.mjs +1 -1
  85. package/dist/types/index.d.mts +5 -5
  86. package/dist/{types-CKB47kiu.d.mts → types-BlOuKTPw.d.mts} +9 -9
  87. package/dist/{types-B4BNthET.d.mts → types-BoaZHr-2.d.mts} +1 -1
  88. package/dist/{types-C5g2oRC7.d.mts → types-D3b7hA00.d.mts} +1 -1
  89. package/dist/utils/index.d.mts +4 -16
  90. package/dist/utils/index.mjs +5 -5
  91. package/dist/{utils-B-l6410F.mjs → utils-7sJ8X83I.mjs} +1 -13
  92. package/package.json +6 -5
  93. package/skills/arc/SKILL.md +23 -4
  94. package/skills/arc/references/integrations.md +1 -1
  95. package/skills/arc/references/mcp.md +2 -0
  96. /package/dist/{HookSystem-BNYKnrXF.mjs → HookSystem-BjFu7zf1.mjs} +0 -0
  97. /package/dist/{caching-5DtLwIqb.mjs → caching-CHH-iHs3.mjs} +0 -0
  98. /package/dist/{circuitBreaker-BBPDt-J_.d.mts → circuitBreaker-BGVoB1hD.d.mts} +0 -0
  99. /package/dist/{circuitBreaker-l18oRgL5.mjs → circuitBreaker-cmi5XDv5.mjs} +0 -0
  100. /package/dist/{elevation-D7WK0RXq.d.mts → elevation-UJO3-NvX.d.mts} +0 -0
  101. /package/dist/{errors-Cg58SLNi.mjs → errors-BF2bIOIS.mjs} +0 -0
  102. /package/dist/{errors-BS6lZvWy.d.mts → errors-BI8kEKsO.d.mts} +0 -0
  103. /package/dist/{externalPaths-iba7jD3d.d.mts → externalPaths-BQ8QijNH.d.mts} +0 -0
  104. /package/dist/{fields-D4nMDqnK.d.mts → fields-DoeDgh2b.d.mts} +0 -0
  105. /package/dist/{interface-CSbZdv_3.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  106. /package/dist/{interface-CG7oRZjX.d.mts → interface-bpoLKKqx.d.mts} +0 -0
  107. /package/dist/{logger-DLg8-Ueg.mjs → logger-CDjpjySd.mjs} +0 -0
  108. /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-DuhiSEZI.mjs} +0 -0
  109. /package/dist/{mongodb-B7X7P1P8.mjs → mongodb-5Ff3w8jy.mjs} +0 -0
  110. /package/dist/{pluralize-Dckfq6US.mjs → pluralize-BneOJkpi.mjs} +0 -0
  111. /package/dist/{queryCachePlugin-CwTpR04-.mjs → queryCachePlugin-D0iIVhW_.mjs} +0 -0
  112. /package/dist/{registry-B3lRFBWo.mjs → registry-B0Wl7uVV.mjs} +0 -0
  113. /package/dist/{replyHelpers-uDUIYh7u.mjs → replyHelpers-CXtJDAZ0.mjs} +0 -0
  114. /package/dist/{requestContext-xHIKedG6.mjs → requestContext-DYvHl113.mjs} +0 -0
  115. /package/dist/{schemaConverter-Y5EejTnJ.mjs → schemaConverter-OxfCshus.mjs} +0 -0
  116. /package/dist/{sessionManager-CEo9jwPI.d.mts → sessionManager-BkzVU8h2.d.mts} +0 -0
  117. /package/dist/{tracing-DEqdGkr-.d.mts → tracing-xqXzWeaf.d.mts} +0 -0
  118. /package/dist/{types--D3vvfdt.d.mts → types-CN6JvmYz.d.mts} +0 -0
  119. /package/dist/{versioning-CdBbFefk.mjs → versioning-CPU_5Xfs.mjs} +0 -0
@@ -1,4 +1,4 @@
1
- import { a as MemoryEventTransport, i as withRetry, o as createEvent, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-B6U_nCFU.mjs";
1
+ import { a as MemoryEventTransport, i as withRetry, o as createEvent, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-XijlQmlL.mjs";
2
2
  //#region src/events/defineEvent.ts
3
3
  /**
4
4
  * defineEvent — Typed Event Definitions with Optional Schema Validation
@@ -224,52 +224,235 @@ const CACHE_EVENTS = Object.freeze({
224
224
  });
225
225
  //#endregion
226
226
  //#region src/events/outbox.ts
227
- /** Default outbox retention — acknowledged events older than this are eligible for purge */
227
+ /** Default outbox retention — delivered events older than this are eligible for purge */
228
228
  const DEFAULT_OUTBOX_RETENTION_MS = 10080 * 60 * 1e3;
229
+ const DEFAULT_LEASE_MS = 3e4;
230
+ /**
231
+ * Thrown by a store when `acknowledge` / `fail` is called by a consumer that
232
+ * does not own the event's current lease.
233
+ *
234
+ * Stores that enforce lease ownership MUST throw this (not silently return)
235
+ * so {@link EventOutbox} can detect the mismatch and avoid over-counting
236
+ * successful deliveries. {@link EventOutbox.relay} catches it and reports
237
+ * via {@link EventOutboxOptions.onError} instead of counting as relayed.
238
+ */
239
+ var OutboxOwnershipError = class extends Error {
240
+ eventId;
241
+ attemptedBy;
242
+ currentOwner;
243
+ constructor(eventId, attemptedBy, currentOwner) {
244
+ super(`Outbox ownership mismatch for event "${eventId}": attempted by "${attemptedBy}", current owner is "${currentOwner ?? "none"}". The lease may have expired and been reclaimed by another worker.`);
245
+ this.name = "OutboxOwnershipError";
246
+ this.eventId = eventId;
247
+ this.attemptedBy = attemptedBy;
248
+ this.currentOwner = currentOwner;
249
+ }
250
+ };
251
+ /** Thrown by {@link EventOutbox.store} when an event is missing `type` or `meta.id`. */
252
+ var InvalidOutboxEventError = class extends Error {
253
+ constructor(reason) {
254
+ super(`Invalid outbox event: ${reason}`);
255
+ this.name = "InvalidOutboxEventError";
256
+ }
257
+ };
229
258
  var EventOutbox = class {
230
259
  _store;
231
260
  _transport;
232
261
  _batchSize;
262
+ _consumerId;
263
+ _leaseMs;
264
+ _onError;
265
+ _usePublishMany;
233
266
  constructor(opts) {
234
267
  this._store = opts.store;
235
268
  this._transport = opts.transport;
236
269
  this._batchSize = opts.batchSize ?? 100;
270
+ this._consumerId = opts.consumerId ?? `relay-${Math.random().toString(36).slice(2, 10)}`;
271
+ this._leaseMs = opts.leaseMs ?? DEFAULT_LEASE_MS;
272
+ this._onError = opts.onError;
273
+ this._usePublishMany = opts.usePublishMany ?? true;
274
+ }
275
+ /** Unique consumer ID used for lease ownership when the store supports claims */
276
+ get consumerId() {
277
+ return this._consumerId;
237
278
  }
238
- /** Store event in outbox (call within your DB transaction) */
239
- async store(event) {
240
- await this._store.save(event);
279
+ /**
280
+ * Store event in outbox.
281
+ *
282
+ * Validates that `event.type` and `event.meta.id` are present — throws
283
+ * {@link InvalidOutboxEventError} otherwise, so corrupt rows can never
284
+ * be persisted via this API.
285
+ *
286
+ * Pass `options.session` to participate in a host-managed DB transaction
287
+ * (store must support session-aware writes). Other options (`visibleAt`,
288
+ * `dedupeKey`, `partitionKey`, `headers`) are forwarded to stores that
289
+ * implement them and ignored otherwise.
290
+ */
291
+ async store(event, options) {
292
+ if (!event || typeof event !== "object") throw new InvalidOutboxEventError("event is not an object");
293
+ if (!event.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
294
+ if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
295
+ await this._store.save(event, options);
296
+ }
297
+ _reportError(kind, error, event) {
298
+ if (!this._onError) return;
299
+ const err = error instanceof Error ? error : new Error(String(error));
300
+ try {
301
+ this._onError({
302
+ kind,
303
+ event,
304
+ error: err
305
+ });
306
+ } catch {}
241
307
  }
242
308
  /**
243
- * Relay pending events to transport.
309
+ * Relay pending events to transport and return the number of successful
310
+ * publish+acknowledge pairs.
244
311
  *
245
- * Processes events in FIFO order up to `batchSize`. Stops on the first
246
- * transport failure remaining events stay pending for the next relay call.
312
+ * For richer observability (per-kind counts, publishMany detection, etc.)
313
+ * use {@link relayBatch} which returns a {@link RelayResult}. This method
314
+ * is the backward-compatible shortcut that returns just the count.
247
315
  *
248
- * @returns Number of successfully published events in this batch
316
+ * @returns Number of successfully published AND acknowledged events
249
317
  */
250
318
  async relay() {
251
- if (!this._transport) return 0;
252
- const pending = await this._store.getPending(this._batchSize);
253
- let relayed = 0;
319
+ return (await this.relayBatch()).relayed;
320
+ }
321
+ /**
322
+ * Relay a batch of pending events to the transport and return a rich
323
+ * {@link RelayResult} describing the outcome of each event.
324
+ *
325
+ * Behavior summary:
326
+ *
327
+ * - **Claim path**: uses {@link OutboxStore.claimPending} when the store
328
+ * supports it (safe for multi-worker relay) or falls back to
329
+ * {@link OutboxStore.getPending} (single-worker only).
330
+ *
331
+ * - **Publish path**: if the transport implements
332
+ * {@link EventTransport.publishMany} and `usePublishMany` is not disabled,
333
+ * the entire batch is sent in one call. Otherwise each event is published
334
+ * individually. Either way, per-event outcomes are tracked.
335
+ *
336
+ * - **Failure path**: if the store implements `fail`, per-event failures
337
+ * are reported via `store.fail(...)` and the batch continues. Without
338
+ * `fail`, the batch stops on the first failure (legacy behavior).
339
+ *
340
+ * - **Malformed events**: events missing `type` or `meta.id` abort the
341
+ * batch — a well-behaved store must never return them (see
342
+ * {@link OutboxStore} semantics #6). The error is reported via `onError`.
343
+ *
344
+ * - **Ownership mismatches**: if `acknowledge`/`fail` throws
345
+ * {@link OutboxOwnershipError} (our lease expired and another worker
346
+ * claimed the event), the event is NOT counted as relayed. The other
347
+ * worker will re-publish — at-least-once semantics preserved.
348
+ *
349
+ * @returns Per-kind outcome counts for the batch
350
+ */
351
+ async relayBatch() {
352
+ const empty = {
353
+ relayed: 0,
354
+ attempted: 0,
355
+ publishFailed: 0,
356
+ ackFailed: 0,
357
+ ownershipMismatches: 0,
358
+ malformed: 0,
359
+ failHookErrors: 0,
360
+ usedPublishMany: false
361
+ };
362
+ if (!this._transport) return empty;
363
+ const pending = this._store.claimPending ? await this._store.claimPending({
364
+ limit: this._batchSize,
365
+ consumerId: this._consumerId,
366
+ leaseMs: this._leaseMs
367
+ }) : await this._store.getPending(this._batchSize);
368
+ const valid = [];
369
+ let malformed = 0;
254
370
  for (const event of pending) {
255
- if (!event.type || !event.meta?.id) {
256
- if (event.meta?.id) await this._store.acknowledge(event.meta.id);
371
+ if (!event || !event.type || !event.meta?.id) {
372
+ this._reportError("malformed_event", new InvalidOutboxEventError("store returned event missing type or meta.id — batch aborted"), event);
373
+ malformed++;
374
+ break;
375
+ }
376
+ valid.push(event);
377
+ }
378
+ const counts = {
379
+ relayed: 0,
380
+ publishFailed: 0,
381
+ ackFailed: 0,
382
+ ownershipMismatches: 0,
383
+ failHookErrors: 0
384
+ };
385
+ const canPublishMany = this._usePublishMany && typeof this._transport.publishMany === "function";
386
+ const canFail = typeof this._store.fail === "function";
387
+ let publishOutcomes;
388
+ if (canPublishMany && valid.length > 0) try {
389
+ const result = await this._transport.publishMany(valid);
390
+ publishOutcomes = new Map(result);
391
+ } catch (batchErr) {
392
+ publishOutcomes = /* @__PURE__ */ new Map();
393
+ const err = batchErr instanceof Error ? batchErr : new Error(String(batchErr));
394
+ for (const ev of valid) publishOutcomes.set(ev.meta.id, err);
395
+ }
396
+ else {
397
+ publishOutcomes = /* @__PURE__ */ new Map();
398
+ for (const event of valid) try {
399
+ await this._transport.publish(event);
400
+ publishOutcomes.set(event.meta.id, null);
401
+ } catch (err) {
402
+ publishOutcomes.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
403
+ if (!canFail) break;
404
+ }
405
+ }
406
+ let stopBatch = false;
407
+ for (const event of valid) {
408
+ if (stopBatch) break;
409
+ const publishErr = publishOutcomes.get(event.meta.id);
410
+ if (publishErr instanceof Error) {
411
+ counts.publishFailed++;
412
+ this._reportError("publish_failed", publishErr, event);
413
+ if (!canFail) {
414
+ stopBatch = true;
415
+ continue;
416
+ }
417
+ try {
418
+ await this._store.fail(event.meta.id, normalizeError(publishErr), { consumerId: this._consumerId });
419
+ } catch (failErr) {
420
+ if (failErr instanceof OutboxOwnershipError) {
421
+ counts.ownershipMismatches++;
422
+ this._reportError("ownership_mismatch", failErr, event);
423
+ } else {
424
+ counts.failHookErrors++;
425
+ this._reportError("fail_failed", failErr, event);
426
+ }
427
+ }
257
428
  continue;
258
429
  }
259
430
  try {
260
- await this._transport.publish(event);
261
- await this._store.acknowledge(event.meta.id);
262
- relayed++;
263
- } catch {
264
- break;
431
+ await this._store.acknowledge(event.meta.id, { consumerId: this._consumerId });
432
+ counts.relayed++;
433
+ } catch (ackErr) {
434
+ counts.ackFailed++;
435
+ if (ackErr instanceof OutboxOwnershipError) {
436
+ counts.ownershipMismatches++;
437
+ this._reportError("ownership_mismatch", ackErr, event);
438
+ } else this._reportError("acknowledge_failed", ackErr, event);
265
439
  }
266
440
  }
267
- return relayed;
441
+ return {
442
+ relayed: counts.relayed,
443
+ attempted: valid.length,
444
+ publishFailed: counts.publishFailed,
445
+ ackFailed: counts.ackFailed,
446
+ ownershipMismatches: counts.ownershipMismatches,
447
+ malformed,
448
+ failHookErrors: counts.failHookErrors,
449
+ usedPublishMany: canPublishMany && valid.length > 0
450
+ };
268
451
  }
269
452
  /**
270
- * Purge old acknowledged events from the outbox store.
453
+ * Purge old **delivered** events from the outbox store.
271
454
  * Delegates to `store.purge()` if implemented; no-op otherwise.
272
- * @param olderThanMs - Remove events acknowledged more than this many ms ago (default: 7 days)
455
+ * @param olderThanMs - Remove events delivered more than this many ms ago (default: 7 days)
273
456
  * @returns Number of purged events, or 0 if store doesn't support purge
274
457
  */
275
458
  async purge(olderThanMs = DEFAULT_OUTBOX_RETENTION_MS) {
@@ -277,17 +460,150 @@ var EventOutbox = class {
277
460
  return this._store.purge(olderThanMs);
278
461
  }
279
462
  };
463
+ function normalizeError(err) {
464
+ if (err instanceof Error) return {
465
+ message: err.message,
466
+ code: err.code
467
+ };
468
+ return { message: String(err) };
469
+ }
470
+ /**
471
+ * In-memory outbox store — reference implementation supporting the full
472
+ * capability set (claim/lease, fail/retry, dedupe, visibleAt).
473
+ *
474
+ * For dev/testing only. Production deployments should use a durable store
475
+ * backed by the app's database.
476
+ */
280
477
  var MemoryOutboxStore = class {
281
- events = [];
282
- async save(event) {
283
- this.events.push(event);
478
+ entries = [];
479
+ seenDedupeKeys = /* @__PURE__ */ new Set();
480
+ async save(event, options) {
481
+ if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
482
+ if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
483
+ if (options?.dedupeKey) {
484
+ if (this.seenDedupeKeys.has(options.dedupeKey)) return;
485
+ this.seenDedupeKeys.add(options.dedupeKey);
486
+ }
487
+ this.entries.push({
488
+ event,
489
+ status: "pending",
490
+ attempts: 0,
491
+ visibleAt: options?.visibleAt?.getTime() ?? 0,
492
+ leaseOwner: null,
493
+ leaseExpiresAt: 0,
494
+ deliveredAt: null,
495
+ lastError: null,
496
+ dedupeKey: options?.dedupeKey
497
+ });
284
498
  }
285
499
  async getPending(limit) {
286
- return this.events.slice(0, limit);
500
+ const now = Date.now();
501
+ return this.entries.filter((e) => e.status === "pending" && e.visibleAt <= now && (e.leaseOwner === null || e.leaseExpiresAt <= now)).slice(0, limit).map((e) => e.event);
287
502
  }
288
- async acknowledge(eventId) {
289
- this.events = this.events.filter((e) => e.meta.id !== eventId);
503
+ async claimPending(options) {
504
+ const now = Date.now();
505
+ const limit = options?.limit ?? 100;
506
+ const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS;
507
+ const consumerId = options?.consumerId ?? "anonymous";
508
+ const typeFilter = options?.types ? new Set(options.types) : null;
509
+ const claimed = [];
510
+ for (const entry of this.entries) {
511
+ if (claimed.length >= limit) break;
512
+ if (entry.status !== "pending") continue;
513
+ if (entry.visibleAt > now) continue;
514
+ if (entry.leaseOwner !== null && entry.leaseExpiresAt > now) continue;
515
+ if (typeFilter && !typeFilter.has(entry.event.type)) continue;
516
+ entry.leaseOwner = consumerId;
517
+ entry.leaseExpiresAt = now + leaseMs;
518
+ entry.attempts++;
519
+ claimed.push(entry.event);
520
+ }
521
+ return claimed;
522
+ }
523
+ async acknowledge(eventId, options) {
524
+ const entry = this.entries.find((e) => e.event.meta.id === eventId);
525
+ if (!entry) return;
526
+ if (entry.status === "delivered") return;
527
+ if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
528
+ entry.status = "delivered";
529
+ entry.deliveredAt = Date.now();
530
+ entry.leaseOwner = null;
531
+ }
532
+ async fail(eventId, error, options) {
533
+ const entry = this.entries.find((e) => e.event.meta.id === eventId);
534
+ if (!entry) return;
535
+ if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
536
+ entry.lastError = error;
537
+ entry.leaseOwner = null;
538
+ entry.leaseExpiresAt = 0;
539
+ if (options?.deadLetter) {
540
+ entry.status = "dead_letter";
541
+ return;
542
+ }
543
+ entry.status = "pending";
544
+ entry.visibleAt = options?.retryAt ? options.retryAt.getTime() : 0;
545
+ }
546
+ async purge(olderThanMs) {
547
+ const cutoff = Date.now() - olderThanMs;
548
+ let purged = 0;
549
+ for (let i = this.entries.length - 1; i >= 0; i--) {
550
+ const entry = this.entries[i];
551
+ if (!entry) continue;
552
+ if (entry.status === "delivered" && entry.deliveredAt !== null && entry.deliveredAt < cutoff) {
553
+ if (entry.dedupeKey) this.seenDedupeKeys.delete(entry.dedupeKey);
554
+ this.entries.splice(i, 1);
555
+ purged++;
556
+ }
557
+ }
558
+ return purged;
559
+ }
560
+ /** Test helper: inspect entry by id */
561
+ _getEntry(eventId) {
562
+ return this.entries.find((e) => e.event.meta.id === eventId);
290
563
  }
291
564
  };
565
+ /**
566
+ * Compute a `retryAt` `Date` using exponential backoff with jitter.
567
+ *
568
+ * This is a convenience helper for store authors implementing
569
+ * {@link OutboxStore.fail}: call it to compute the retry visibility window
570
+ * based on the event's current attempt count.
571
+ *
572
+ * Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
573
+ *
574
+ * @example Basic usage inside a store's `fail()` method
575
+ * ```typescript
576
+ * async fail(eventId, error, options) {
577
+ * const entry = await this.findById(eventId);
578
+ * entry.attempts++;
579
+ * if (entry.attempts >= MAX_ATTEMPTS) {
580
+ * return this.deadLetter(eventId, error);
581
+ * }
582
+ * const retryAt = exponentialBackoff({ attempt: entry.attempts });
583
+ * entry.visibleAt = retryAt;
584
+ * await this.update(entry);
585
+ * }
586
+ * ```
587
+ *
588
+ * @example Tuning for a faster transport
589
+ * ```typescript
590
+ * exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
591
+ * // attempt=1 → ~250ms ±30%
592
+ * // attempt=2 → ~500ms ±30%
593
+ * // attempt=3 → ~1000ms ±30%
594
+ * // attempt=10 → capped at 10_000ms
595
+ * ```
596
+ */
597
+ function exponentialBackoff(options) {
598
+ const attempt = Math.max(1, Math.floor(options.attempt));
599
+ const baseMs = options.baseMs ?? 1e3;
600
+ const maxMs = options.maxMs ?? 6e4;
601
+ const jitter = Math.max(0, Math.min(1, options.jitter ?? .2));
602
+ const now = options.now ?? Date.now();
603
+ const exp = baseMs * 2 ** (attempt - 1);
604
+ const capped = Math.min(maxMs, exp);
605
+ const jittered = jitter > 0 ? capped * (1 + Math.random() * jitter) : capped;
606
+ return new Date(now + jittered);
607
+ }
292
608
  //#endregion
293
- export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, MemoryEventTransport, MemoryOutboxStore, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, withRetry };
609
+ export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, InvalidOutboxEventError, MemoryEventTransport, MemoryOutboxStore, OutboxOwnershipError, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, withRetry };
@@ -1,2 +1,2 @@
1
- import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-Dag5LFa9.mjs";
1
+ import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-BgrYzpeq.mjs";
2
2
  export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
@@ -1,4 +1,4 @@
1
- import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "../../EventTransport-C4VheKeC.mjs";
1
+ import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "../../EventTransport-CLXJUzyT.mjs";
2
2
 
3
3
  //#region src/events/transports/redis.d.ts
4
4
  interface RedisLike {
@@ -1,4 +1,4 @@
1
- import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-CKB47kiu.mjs";
1
+ import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-BlOuKTPw.mjs";
2
2
  import { FastifyInstance } from "fastify";
3
3
 
4
4
  //#region src/factory/createApp.d.ts
@@ -1,4 +1,4 @@
1
- import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-D7e77m8C.mjs";
1
+ import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-p2OThysU.mjs";
2
2
  import { readdir } from "node:fs/promises";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -1,2 +1,2 @@
1
- import { _n as defineHook, an as HookOperation, cn as HookSystem, dn as afterDelete, fn as afterUpdate, gn as createHookSystem, hn as beforeUpdate, in as HookHandler, ln as HookSystemOptions, mn as beforeDelete, nn as DefineHookOptions, on as HookPhase, pn as beforeCreate, rn as HookContext, sn as HookRegistration, un as afterCreate } from "../interface-B91alUzq.mjs";
1
+ import { An as beforeCreate, Cn as HookPhase, Dn as afterCreate, En as HookSystemOptions, Mn as beforeUpdate, Nn as createHookSystem, On as afterDelete, Pn as defineHook, Sn as HookOperation, Tn as HookSystem, bn as HookContext, jn as beforeDelete, kn as afterUpdate, wn as HookRegistration, xn as HookHandler, yn as DefineHookOptions } from "../interface-CS6d7HiB.mjs";
2
2
  export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -1,2 +1,2 @@
1
- import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-BNYKnrXF.mjs";
1
+ import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-BjFu7zf1.mjs";
2
2
  export { HookSystem, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -1,6 +1,6 @@
1
- import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-CSbZdv_3.mjs";
2
- import { n as MongoIdempotencyStoreOptions } from "../mongodb-B7zupyck.mjs";
3
- import { r as RedisIdempotencyStoreOptions, t as RedisClient } from "../redis-3TQxm2VZ.mjs";
1
+ import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-CkkWm5uR.mjs";
2
+ import { n as MongoIdempotencyStoreOptions } from "../mongodb-NShVZDMr.mjs";
3
+ import { r as RedisIdempotencyStoreOptions, t as RedisClient } from "../redis-Bunu3qWg.mjs";
4
4
  import { FastifyPluginAsync } from "fastify";
5
5
 
6
6
  //#region src/idempotency/idempotencyPlugin.d.ts
@@ -1,2 +1,2 @@
1
- import { n as MongoIdempotencyStoreOptions, t as MongoIdempotencyStore } from "../mongodb-B7zupyck.mjs";
1
+ import { n as MongoIdempotencyStoreOptions, t as MongoIdempotencyStore } from "../mongodb-NShVZDMr.mjs";
2
2
  export { MongoIdempotencyStore, type MongoIdempotencyStoreOptions };
@@ -1,2 +1,2 @@
1
- import { n as RedisIdempotencyStore, r as RedisIdempotencyStoreOptions, t as RedisClient } from "../redis-3TQxm2VZ.mjs";
1
+ import { n as RedisIdempotencyStore, r as RedisIdempotencyStoreOptions, t as RedisClient } from "../redis-Bunu3qWg.mjs";
2
2
  export { type RedisClient, RedisIdempotencyStore, type RedisIdempotencyStoreOptions };
@@ -1,4 +1,4 @@
1
- import { G as OpenApiSchemas, Q as QueryParserInterface, Ut as CrudRepository, c as ValidationResult, ft as RouteSchemaOptions, n as AdapterSchemaContext, o as RepositoryLike, q as ParsedQuery, r as DataAdapter, s as SchemaMetadata } from "./interface-B91alUzq.mjs";
1
+ import { Y as OpenApiSchemas, Z as ParsedQuery, Zt as CrudRepository, c as ValidationResult, n as AdapterSchemaContext, nt as QueryParserInterface, o as RepositoryLike, r as DataAdapter, s as SchemaMetadata, vt as RouteSchemaOptions } from "./interface-CS6d7HiB.mjs";
2
2
  import { Model } from "mongoose";
3
3
 
4
4
  //#region src/adapters/mongoose.d.ts
@@ -1,6 +1,6 @@
1
- import { r as RequestScope } from "./types--D3vvfdt.mjs";
2
- import { n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-B4BNthET.mjs";
3
- import { i as CacheStore, t as CacheLogger } from "./interface-CG7oRZjX.mjs";
1
+ import { r as RequestScope } from "./types-CN6JvmYz.mjs";
2
+ import { n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-BoaZHr-2.mjs";
3
+ import { i as CacheStore, t as CacheLogger } from "./interface-bpoLKKqx.mjs";
4
4
  import { FastifyRequest } from "fastify";
5
5
 
6
6
  //#region src/permissions/applyPermissionResult.d.ts
@@ -1,6 +1,6 @@
1
- import { r as RequestScope } from "./types--D3vvfdt.mjs";
2
- import { C as CrudRouterOptions, Ft as IController, It as IControllerResponse, Lt as IRequestContext, Vt as ResourceDefinition, it as RequestWithExtras, k as FastifyWithDecorators, nt as RequestContext, ot as ResourceConfig, u as AnyRecord, x as CrudController } from "./interface-B91alUzq.mjs";
3
- import { t as PermissionCheck } from "./types-B4BNthET.mjs";
1
+ import { r as RequestScope } from "./types-CN6JvmYz.mjs";
2
+ import { D as CrudRouterOptions, Ht as IControllerResponse, N as FastifyWithDecorators, T as CrudController, Ut as IRequestContext, Vt as IController, ct as RequestWithExtras, m as AnyRecord, ot as RequestContext, qt as ResourceDefinition, ut as ResourceConfig } from "./interface-CS6d7HiB.mjs";
3
+ import { t as PermissionCheck } from "./types-BoaZHr-2.mjs";
4
4
  import { FastifyInstance, FastifyReply, FastifyRequest, RouteHandlerMethod } from "fastify";
5
5
 
6
6
  //#region src/constants.d.ts
@@ -60,7 +60,7 @@ declare const RESERVED_QUERY_PARAMS: Readonly<Set<string>>;
60
60
  * @param req - Full Fastify request object
61
61
  * @returns Action result (will be wrapped in success response)
62
62
  */
63
- type ActionHandler<TData = any, TResult = any> = (id: string, data: TData, req: RequestWithExtras) => Promise<TResult>;
63
+ type ActionHandler<TData = Record<string, unknown>, TResult = unknown> = (id: string, data: TData, req: RequestWithExtras) => Promise<TResult>;
64
64
  /**
65
65
  * Action router configuration
66
66
  */
@@ -68,31 +68,63 @@ interface ActionRouterConfig {
68
68
  /**
69
69
  * OpenAPI tag for grouping routes
70
70
  */
71
- tag?: string;
71
+ readonly tag?: string;
72
72
  /**
73
73
  * Action handlers map
74
74
  * @example { approve: (id, data, req) => service.approve(id), ... }
75
75
  */
76
- actions: Record<string, ActionHandler>;
76
+ readonly actions: Record<string, ActionHandler>;
77
77
  /**
78
78
  * Per-action permission checks (PermissionCheck functions)
79
79
  * @example { approve: requireRoles(['admin', 'manager']), cancel: requireRoles(['admin']) }
80
80
  */
81
- actionPermissions?: Record<string, PermissionCheck>;
81
+ readonly actionPermissions?: Record<string, PermissionCheck>;
82
82
  /**
83
- * Per-action JSON schema for body validation
84
- * @example { dispatch: { transport: { type: 'object' } } }
83
+ * Per-action schema for body validation.
84
+ *
85
+ * Accepted shapes per action:
86
+ *
87
+ * 1. **Full JSON Schema object** with `type: 'object'`, `properties`, `required` —
88
+ * used verbatim. Required fields ARE enforced by Fastify's AJV.
89
+ *
90
+ * 2. **Zod v4 schema** — auto-converted via `z.toJSONSchema()`. Required fields
91
+ * ARE enforced.
92
+ *
93
+ * 3. **Legacy field map** `{ fieldName: { type: 'string' } }` — each key becomes
94
+ * a property and is treated as REQUIRED unless the property schema has
95
+ * `nullable: true` or Arc's sentinel `required: false`. Kept for back-compat.
96
+ *
97
+ * All shapes are compiled into a single `oneOf` discriminator body schema
98
+ * so AJV can validate action-specific required fields at the HTTP layer.
99
+ *
100
+ * @example JSON Schema
101
+ * ```ts
102
+ * actionSchemas: {
103
+ * dispatch: {
104
+ * type: 'object',
105
+ * properties: { carrier: { type: 'string' } },
106
+ * required: ['carrier'],
107
+ * },
108
+ * }
109
+ * ```
110
+ *
111
+ * @example Zod v4
112
+ * ```ts
113
+ * actionSchemas: {
114
+ * dispatch: z.object({ carrier: z.string() }),
115
+ * }
116
+ * ```
85
117
  */
86
- actionSchemas?: Record<string, Record<string, any>>;
118
+ readonly actionSchemas?: Record<string, Record<string, unknown>>;
87
119
  /**
88
120
  * Global permission check applied to all actions (if action-specific not defined)
89
121
  */
90
- globalAuth?: PermissionCheck;
122
+ readonly globalAuth?: PermissionCheck;
91
123
  /**
92
124
  * Optional idempotency service
93
125
  * If provided, will handle idempotency-key header
94
126
  */
95
- idempotencyService?: IdempotencyService;
127
+ readonly idempotencyService?: IdempotencyService;
96
128
  /**
97
129
  * Custom error handler for action execution failures
98
130
  * @param error - The error thrown by action handler
@@ -100,7 +132,7 @@ interface ActionRouterConfig {
100
132
  * @param id - The resource ID
101
133
  * @returns Status code and error response
102
134
  */
103
- onError?: (error: Error, action: string, id: string) => {
135
+ readonly onError?: (error: Error, action: string, id: string) => {
104
136
  statusCode: number;
105
137
  error: string;
106
138
  code?: string;
@@ -111,11 +143,11 @@ interface ActionRouterConfig {
111
143
  * Apps can provide their own implementation
112
144
  */
113
145
  interface IdempotencyService {
114
- check(key: string, payload: any): Promise<{
146
+ check(key: string, payload: unknown): Promise<{
115
147
  isNew: boolean;
116
- existingResult?: any;
148
+ existingResult?: unknown;
117
149
  }>;
118
- complete(key: string | undefined, result: any): Promise<void>;
150
+ complete(key: string | undefined, result: unknown): Promise<void>;
119
151
  fail(key: string | undefined, error: Error): Promise<void>;
120
152
  }
121
153
  /**