@classytic/arc 2.8.0 → 2.8.3

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 (117) 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-CinyO7zQ.d.mts} +37 -1
  4. package/dist/{ResourceRegistry-BOtJuRCs.mjs → ResourceRegistry-Dq3_zBQP.mjs} +17 -5
  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-CHCIuA-p.mjs → betterAuthOpenApi-C5lDyRH2.mjs} +1 -1
  16. package/dist/cache/index.d.mts +2 -2
  17. package/dist/cli/commands/describe.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/init.mjs +10 -10
  21. package/dist/cli/commands/introspect.mjs +3 -3
  22. package/dist/core/index.d.mts +3 -3
  23. package/dist/core/index.mjs +5 -5
  24. package/dist/{core-BfrfxNqO.mjs → core-DKSwNSXf.mjs} +1 -1
  25. package/dist/{createActionRouter-CbkIAaGh.mjs → createActionRouter-Df1BuawX.mjs} +87 -21
  26. package/dist/{createApp-Cy8eUNKQ.mjs → createApp-BOYjBgdI.mjs} +16 -7
  27. package/dist/{defineResource-CovBXvTB.mjs → defineResource-Bb_Bdhtw.mjs} +60 -33
  28. package/dist/docs/index.d.mts +2 -2
  29. package/dist/docs/index.mjs +1 -1
  30. package/dist/dynamic/index.d.mts +2 -2
  31. package/dist/dynamic/index.mjs +1 -1
  32. package/dist/{errorHandler-BeN-ERN7.d.mts → errorHandler-CdZDavNH.d.mts} +2 -2
  33. package/dist/{errorHandler-BW08lEiy.mjs → errorHandler-mzqk4cGl.mjs} +1 -1
  34. package/dist/{eventPlugin-CAOWMQS8.d.mts → eventPlugin-CVxlE6De.d.mts} +1 -1
  35. package/dist/{eventPlugin-x4jo3sG0.mjs → eventPlugin-D91S2YF4.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-stream-entry.mjs +3 -1
  40. package/dist/events/transports/redis.d.mts +1 -1
  41. package/dist/factory/index.d.mts +1 -1
  42. package/dist/factory/index.mjs +2 -152
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/idempotency/index.d.mts +3 -3
  45. package/dist/idempotency/mongodb.d.mts +1 -1
  46. package/dist/idempotency/mongodb.mjs +18 -6
  47. package/dist/idempotency/redis.d.mts +1 -1
  48. package/dist/idempotency/redis.mjs +10 -1
  49. package/dist/{index-BpMhrFgn.d.mts → index-BgmMdpm8.d.mts} +1 -1
  50. package/dist/{index-CBru2y5Y.d.mts → index-CSkeivBx.d.mts} +3 -3
  51. package/dist/{index-qct60lnl.d.mts → index-CpTSDqmD.d.mts} +60 -6
  52. package/dist/index.d.mts +8 -8
  53. package/dist/index.mjs +7 -7
  54. package/dist/integrations/event-gateway.d.mts +1 -1
  55. package/dist/integrations/event-gateway.mjs +1 -1
  56. package/dist/integrations/index.d.mts +1 -1
  57. package/dist/integrations/mcp/index.d.mts +2 -2
  58. package/dist/integrations/mcp/index.mjs +1 -1
  59. package/dist/integrations/mcp/testing.d.mts +1 -1
  60. package/dist/integrations/mcp/testing.mjs +1 -1
  61. package/dist/{interface-IJqN3pXK.d.mts → interface-BVuMfeVv.d.mts} +596 -125
  62. package/dist/loadResources-Bksk8ydA.mjs +154 -0
  63. package/dist/{mongodb-B1eVtFhw.d.mts → mongodb-B8U2xaLj.d.mts} +1 -1
  64. package/dist/{mongodb-NShVZDMr.d.mts → mongodb-X7LbEjTN.d.mts} +10 -1
  65. package/dist/{openapi-AYLVjqVe.mjs → openapi-CYCuekCn.mjs} +50 -3
  66. package/dist/org/index.d.mts +2 -2
  67. package/dist/permissions/index.d.mts +3 -3
  68. package/dist/plugins/index.d.mts +5 -5
  69. package/dist/plugins/index.mjs +8 -8
  70. package/dist/plugins/tracing-entry.d.mts +1 -1
  71. package/dist/plugins/tracing-entry.mjs +1 -1
  72. package/dist/policies/index.d.mts +1 -1
  73. package/dist/presets/index.d.mts +3 -3
  74. package/dist/presets/index.mjs +1 -1
  75. package/dist/presets/multiTenant.d.mts +1 -1
  76. package/dist/{presets-BFrGvvjL.mjs → presets-C2xgzW6x.mjs} +10 -18
  77. package/dist/{queryCachePlugin-BCFVXnxK.d.mts → queryCachePlugin-CnTZZTC5.d.mts} +1 -1
  78. package/dist/{redis-stream-CF1lrKVk.d.mts → redis-stream-D54N5oXs.d.mts} +1 -1
  79. package/dist/{redis-Bunu3qWg.d.mts → redis-z3sFr1UP.d.mts} +1 -1
  80. package/dist/registry/index.d.mts +1 -1
  81. package/dist/registry/index.mjs +1 -1
  82. package/dist/{resourceToTools-C_1SMiCz.mjs → resourceToTools-O_HwWXFa.mjs} +194 -64
  83. package/dist/rpc/index.d.mts +1 -1
  84. package/dist/rpc/index.mjs +1 -1
  85. package/dist/scope/index.d.mts +2 -2
  86. package/dist/testing/index.d.mts +2 -2
  87. package/dist/testing/index.mjs +1 -1
  88. package/dist/types/index.d.mts +5 -5
  89. package/dist/{types-gUxAIZHp.d.mts → types-Bg2X42_m.d.mts} +30 -9
  90. package/dist/{types-BoaZHr-2.d.mts → types-CVC4HOKi.d.mts} +1 -1
  91. package/dist/{types-Ct0PUUSp.d.mts → types-CcG4avic.d.mts} +1 -1
  92. package/dist/utils/index.d.mts +43 -17
  93. package/dist/utils/index.mjs +5 -5
  94. package/dist/{utils-B-l6410F.mjs → utils-yYT3HDXt.mjs} +65 -13
  95. package/package.json +10 -9
  96. package/skills/arc/SKILL.md +79 -6
  97. /package/dist/{caching-CHH-iHs3.mjs → caching-CjybdRwx.mjs} +0 -0
  98. /package/dist/{circuitBreaker-BGVoB1hD.d.mts → circuitBreaker-CvXkjfrW.d.mts} +0 -0
  99. /package/dist/{circuitBreaker-l18oRgL5.mjs → circuitBreaker-cmi5XDv5.mjs} +0 -0
  100. /package/dist/{elevation-UJO3-NvX.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  101. /package/dist/{errors-Cg58SLNi.mjs → errors-BF2bIOIS.mjs} +0 -0
  102. /package/dist/{errors-BI8kEKsO.d.mts → errors-Bmn3eZT6.d.mts} +0 -0
  103. /package/dist/{externalPaths-BQ8QijNH.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
  104. /package/dist/{fields-DoeDgh2b.d.mts → fields-DC4So2M2.d.mts} +0 -0
  105. /package/dist/{interface-CkkWm5uR.d.mts → interface-B-pe8fhj.d.mts} +0 -0
  106. /package/dist/{interface-bpoLKKqx.d.mts → interface-DplgQO2e.d.mts} +0 -0
  107. /package/dist/{metrics-DuhiSEZI.mjs → metrics-TuOmguhi.mjs} +0 -0
  108. /package/dist/{mongodb-5Ff3w8jy.mjs → mongodb-B5O6xaW1.mjs} +0 -0
  109. /package/dist/{pluralize-BneOJkpi.mjs → pluralize-A0tWEl1K.mjs} +0 -0
  110. /package/dist/{replyHelpers-CXtJDAZ0.mjs → replyHelpers-BLojtuvR.mjs} +0 -0
  111. /package/dist/{requestContext-xHIKedG6.mjs → requestContext-DYvHl113.mjs} +0 -0
  112. /package/dist/{schemaConverter-Y5EejTnJ.mjs → schemaConverter-OxfCshus.mjs} +0 -0
  113. /package/dist/{sessionManager-BkzVU8h2.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
  114. /package/dist/{sse-CD5Hghpu.mjs → sse-CJpt7LGI.mjs} +0 -0
  115. /package/dist/{tracing-xqXzWeaf.d.mts → tracing-DxjKk7eW.d.mts} +0 -0
  116. /package/dist/{types-CN6JvmYz.d.mts → types-C72d3NDn.d.mts} +0 -0
  117. /package/dist/{versioning-CPU_5Xfs.mjs → versioning-Cm8qoFDg.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-x4jo3sG0.mjs";
1
+ import { a as MemoryEventTransport, i as withRetry, o as createEvent, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-D91S2YF4.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-CF1lrKVk.mjs";
1
+ import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-D54N5oXs.mjs";
2
2
  export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
@@ -118,7 +118,9 @@ var RedisStreamTransport = class {
118
118
  const claimed = await this.redis.xclaim(this.stream, this.group, this.consumer, this.claimTimeoutMs, ...staleIds);
119
119
  for (const [messageId, fields] of claimed) await this.processEntry(messageId, fields);
120
120
  }
121
- } catch {}
121
+ } catch (err) {
122
+ this.logger.error("[RedisStreamTransport] claimPending error:", err);
123
+ }
122
124
  }
123
125
  async processEntry(messageId, fields) {
124
126
  const fieldMap = /* @__PURE__ */ new Map();
@@ -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-CinyO7zQ.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-gUxAIZHp.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-Bg2X42_m.mjs";
2
2
  import { FastifyInstance } from "fastify";
3
3
 
4
4
  //#region src/factory/createApp.d.ts
@@ -1,7 +1,5 @@
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-Cy8eUNKQ.mjs";
2
- import { readdir } from "node:fs/promises";
3
- import { dirname, join, resolve } from "node:path";
4
- import { fileURLToPath, pathToFileURL } from "node:url";
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-BOYjBgdI.mjs";
2
+ import { t as loadResources } from "../loadResources-Bksk8ydA.mjs";
5
3
  //#region src/factory/edge.ts
6
4
  /**
7
5
  * Convert a Fastify app into a Web Standards fetch handler.
@@ -48,152 +46,4 @@ function toFetchHandler(app, options = {}) {
48
46
  };
49
47
  }
50
48
  //#endregion
51
- //#region src/factory/loadResources.ts
52
- /**
53
- * loadResources — Auto-discover resource files from a directory.
54
- *
55
- * Scans for `*.resource.{ts,js,mts,mjs}` files, imports each,
56
- * and collects their default exports. No barrel file needed.
57
- *
58
- * @example
59
- * ```ts
60
- * import { createApp, loadResources } from '@classytic/arc/factory';
61
- *
62
- * // Recommended: import.meta.url — works in both src/ (dev) and dist/ (prod)
63
- * const app = await createApp({
64
- * resources: await loadResources(import.meta.url),
65
- * auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
66
- * });
67
- *
68
- * // Or explicit path (must match runtime layout)
69
- * const app2 = await createApp({
70
- * resources: await loadResources('./src/resources'),
71
- * });
72
- * ```
73
- *
74
- * File convention:
75
- * ```
76
- * src/resources/
77
- * product/product.resource.ts → export default defineResource({ name: 'product', ... })
78
- * order/order.resource.ts → export default defineResource({ name: 'order', ... })
79
- * ```
80
- */
81
- /**
82
- * Scan a directory for resource files and import their default exports.
83
- *
84
- * Accepts a directory path OR `import.meta.url` (file:// URL).
85
- * When given a URL, resolves to the directory containing that file —
86
- * so `loadResources(import.meta.url)` works in both dev (`src/`) and
87
- * production (`dist/`) without path gymnastics.
88
- *
89
- * @param dir - Directory path, or `import.meta.url` (file:// URL resolved to its dirname)
90
- * @param options - Pattern and recursion options
91
- * @returns Array of resource definitions (anything with `.toPlugin()`)
92
- *
93
- * @example
94
- * ```ts
95
- * // Works from both src/ and dist/ — resolves relative to the calling file
96
- * await loadResources(import.meta.url);
97
- *
98
- * // Subdirectory relative to the calling file
99
- * await loadResources(import.meta.url, { suffix: '.resource' });
100
- *
101
- * // Explicit path (must match runtime layout)
102
- * await loadResources('./src/resources');
103
- * ```
104
- */
105
- async function loadResources(dir, options = {}) {
106
- const { suffix = ".resource", recursive = true, exclude, include, silent = false } = options;
107
- const files = await collectFiles(resolve(dir.startsWith("file://") ? dirname(fileURLToPath(dir)) : dir), new RegExp(`${escapeRegex(suffix)}\\.(ts|js|mts|mjs)$`), recursive);
108
- files.sort();
109
- const includeSet = include ? new Set(include) : null;
110
- const excludeSet = exclude ? new Set(exclude) : null;
111
- const skipped = [];
112
- const failed = [];
113
- const isWindowsPath = (p) => /^[a-z]:[\\/]/i.test(p);
114
- const results = await Promise.all(files.map(async (file) => {
115
- let mod;
116
- let primaryError;
117
- try {
118
- mod = await import(pathToFileURL(file).href);
119
- return {
120
- file,
121
- mod
122
- };
123
- } catch (err) {
124
- primaryError = err;
125
- }
126
- if (!isWindowsPath(file)) try {
127
- mod = await import(file);
128
- return {
129
- file,
130
- mod
131
- };
132
- } catch {}
133
- const err = primaryError;
134
- const code = err.code;
135
- const msg = err instanceof Error ? err.message : String(err);
136
- if (code === "ERR_MODULE_NOT_FOUND" && msg.includes(".js")) failed.push(`${file}: ${msg}\n Hint: This file uses .js extension imports (TypeScript ESM convention).
137
- • Production: ensure your build compiles .ts→.js before loadResources() runs.
138
- • Node.js: use tsx, ts-node/esm, or a build step.
139
- • Vitest: nested .js→.ts resolution may fail through dynamic imports.
140
- Workaround: use import.meta.glob to preload resources statically.
141
- See: https://github.com/classytic/arc/blob/main/docs/production-ops/factory.mdx#vitest-limitation`);
142
- else failed.push(`${file}: ${msg}`);
143
- return null;
144
- }));
145
- const resources = [];
146
- for (const result of results) {
147
- if (!result) continue;
148
- let resource = result.mod.default ?? result.mod.resource;
149
- if (!resource || typeof resource.toPlugin !== "function") {
150
- for (const value of Object.values(result.mod)) if (value && typeof value === "object" && typeof value.toPlugin === "function") {
151
- resource = value;
152
- break;
153
- }
154
- }
155
- if (!resource || typeof resource.toPlugin !== "function") {
156
- skipped.push(result.file);
157
- continue;
158
- }
159
- const name = resource.name;
160
- if (name) {
161
- if (includeSet && !includeSet.has(name)) continue;
162
- if (!includeSet && excludeSet?.has(name)) continue;
163
- }
164
- resources.push(resource);
165
- }
166
- const log = silent ? void 0 : options?.logger;
167
- if (log) {
168
- if (failed.length) {
169
- log.warn(`[arc] loadResources: ${failed.length} file(s) failed to import:`);
170
- for (const f of failed) log.warn(` - ${f}`);
171
- }
172
- if (skipped.length) {
173
- log.warn(`[arc] loadResources: ${skipped.length} file(s) skipped (no default export with toPlugin):`);
174
- for (const f of skipped) log.warn(` - ${f}`);
175
- }
176
- }
177
- return resources;
178
- }
179
- async function collectFiles(dir, pattern, recursive) {
180
- const results = [];
181
- let entries;
182
- try {
183
- entries = await readdir(dir, { withFileTypes: true });
184
- } catch {
185
- return results;
186
- }
187
- for (const entry of entries) {
188
- const name = String(entry.name);
189
- const fullPath = join(dir, name);
190
- if (entry.isDirectory() && recursive) results.push(...await collectFiles(fullPath, pattern, recursive));
191
- else if (entry.isFile() && pattern.test(name)) results.push(fullPath);
192
- }
193
- return results;
194
- }
195
- function escapeRegex(str) {
196
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
197
- }
198
- //#endregion
199
49
  export { ArcFactory, createApp, developmentPreset, edgePreset, getPreset, loadResources, productionPreset, testingPreset, toFetchHandler };
@@ -1,2 +1,2 @@
1
- import { Cn as defineHook, Sn as createHookSystem, _n as afterDelete, bn as beforeDelete, cn as DefineHookOptions, dn as HookOperation, fn as HookPhase, gn as afterCreate, hn as HookSystemOptions, ln as HookContext, mn as HookSystem, pn as HookRegistration, un as HookHandler, vn as afterUpdate, xn as beforeUpdate, yn as beforeCreate } from "../interface-IJqN3pXK.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-BVuMfeVv.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,6 +1,6 @@
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";
1
+ import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-B-pe8fhj.mjs";
2
+ import { n as MongoIdempotencyStoreOptions } from "../mongodb-X7LbEjTN.mjs";
3
+ import { r as RedisIdempotencyStoreOptions, t as RedisClient } from "../redis-z3sFr1UP.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-NShVZDMr.mjs";
1
+ import { n as MongoIdempotencyStoreOptions, t as MongoIdempotencyStore } from "../mongodb-X7LbEjTN.mjs";
2
2
  export { MongoIdempotencyStore, type MongoIdempotencyStoreOptions };