@classytic/arc 2.31.0 → 2.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +1 -1
  2. package/bin/arc.js +58 -17
  3. package/dist/{BaseController-De7GqHck.mjs → BaseController-CaQC5EXX.mjs} +298 -35
  4. package/dist/{EventTransport-2uVV34WU.d.mts → EventTransport-BEVP50ka.d.mts} +16 -0
  5. package/dist/{EventTransport-9mlTgqzE.mjs → EventTransport-tq4CEB-q.mjs} +39 -2
  6. package/dist/{HookSystem-BcmPyq5I.mjs → HookSystem-DqXkMXkB.mjs} +2 -2
  7. package/dist/{ResourceRegistry-qq0mn2s9.mjs → ResourceRegistry-DoALUlOO.mjs} +18 -15
  8. package/dist/{actionPermissions-BE5NwaQk.mjs → actionPermissions-CUeCmpUd.mjs} +1 -1
  9. package/dist/audit/index.d.mts +2 -2
  10. package/dist/audit/index.mjs +2 -2
  11. package/dist/auth/audit.mjs +1 -1
  12. package/dist/auth/index.d.mts +2 -2
  13. package/dist/auth/index.mjs +44 -11
  14. package/dist/auth/redis-session.mjs +1 -1
  15. package/dist/{authorizationDecision-ChvUQGG1.mjs → authorizationDecision-B3mP_QTy.mjs} +2 -2
  16. package/dist/{types-B7XeLEgK.d.mts → base-Csjc2oF-.d.mts} +74 -74
  17. package/dist/{buildHandler-CDrCJzmB.mjs → buildHandler-DLE9xX-V.mjs} +6 -5
  18. package/dist/cache/index.mjs +4 -4
  19. package/dist/{caching-BFHA6Scf.mjs → caching-BcGmvnHh.mjs} +2 -2
  20. package/dist/cleanup/index.d.mts +52 -18
  21. package/dist/cleanup/index.mjs +281 -32
  22. package/dist/cli/commands/describe.d.mts +1 -1
  23. package/dist/cli/commands/describe.mjs +4 -6
  24. package/dist/cli/commands/docs.mjs +3 -3
  25. package/dist/cli/commands/doctor.mjs +1 -1
  26. package/dist/cli/commands/generate.mjs +2 -2
  27. package/dist/cli/commands/init.mjs +2 -2
  28. package/dist/cli/commands/introspect.mjs +2 -2
  29. package/dist/cli/index.mjs +3 -3
  30. package/dist/{constants-D3Dv4LAV.mjs → constants-BxCoxmiN.mjs} +1 -1
  31. package/dist/context/index.mjs +3 -3
  32. package/dist/core/index.d.mts +2 -2
  33. package/dist/core/index.mjs +7 -7
  34. package/dist/{core-BGpIxINN.mjs → core-dxhcHhUD.mjs} +3 -3
  35. package/dist/{createActionRouter-CDXEdasl.mjs → createActionRouter-DVpoA0do.mjs} +14 -11
  36. package/dist/{createAggregationRouter-KrQDzx-k.mjs → createAggregationRouter-B_B48JZ-.mjs} +10 -8
  37. package/dist/{createApp-Df89BEni.mjs → createApp-CLGegqiG.mjs} +287 -62
  38. package/dist/{defineAggregation-DbPPS8RI.mjs → defineAggregation-Nv-DwfOf.mjs} +1 -1
  39. package/dist/{defineResource-Bv6EtN1x.mjs → defineResource-DR9vsUYy.mjs} +318 -35
  40. package/dist/discovery/index.mjs +1 -1
  41. package/dist/docs/index.d.mts +1 -1
  42. package/dist/docs/index.mjs +3 -3
  43. package/dist/{elevation-D2l1mh4g.mjs → elevation-B0tlY8y1.mjs} +4 -4
  44. package/dist/encryption/index.mjs +5 -4
  45. package/dist/{errorHandler-C5NeD6bT.mjs → errorHandler-C7xBqHsI.mjs} +42 -5
  46. package/dist/{errorHandler-CpYUJmPA.d.mts → errorHandler-DI5AN2b5.d.mts} +14 -3
  47. package/dist/{errors-Cj42AntO.mjs → errors-Cd_yy7aW.mjs} +1 -1
  48. package/dist/{eventPlugin-GF5nnBIq.d.mts → eventPlugin-DSNEZWfV.d.mts} +29 -1
  49. package/dist/{eventPlugin-CkffEihm.mjs → eventPlugin-Dar8qRaj.mjs} +68 -11
  50. package/dist/events/index.d.mts +18 -297
  51. package/dist/events/index.mjs +7 -758
  52. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  53. package/dist/events/transports/redis-stream-entry.mjs +14 -10
  54. package/dist/events/transports/redis.d.mts +1 -1
  55. package/dist/events/transports/redis.mjs +1 -1
  56. package/dist/factory/index.d.mts +20 -5
  57. package/dist/factory/index.mjs +131 -8
  58. package/dist/fastify-augmentation-DVsTAy6X.d.mts +179 -0
  59. package/dist/{fields-CB5Cuzk9.mjs → fields-CojeUkS4.mjs} +6 -11
  60. package/dist/{generate-EcA6ui4i.mjs → generate-DiB0nHPR.mjs} +2 -2
  61. package/dist/{headers-3zvE0rM3.mjs → headers-CveNO9gR.mjs} +2 -2
  62. package/dist/{health-DjZYjM7H.mjs → health-CasG2A28.mjs} +1 -1
  63. package/dist/hooks/index.d.mts +1 -1
  64. package/dist/hooks/index.mjs +2 -2
  65. package/dist/idempotency/index.d.mts +45 -1
  66. package/dist/idempotency/index.mjs +95 -11
  67. package/dist/idempotency/redis.mjs +1 -1
  68. package/dist/{index-CEJNZFtX.d.mts → index-B-DvUzsr.d.mts} +82 -3
  69. package/dist/{index-CpzxqePp.d.mts → index-BcR2bFZQ.d.mts} +707 -373
  70. package/dist/{index-Bc2ox8_U.d.mts → index-Brin0ODU.d.mts} +4 -4
  71. package/dist/{index-CFThn1K9.d.mts → index-C5NnMEKF.d.mts} +70 -4
  72. package/dist/{index-C-481Zf2.d.mts → index-ugr1tQlW.d.mts} +17 -1
  73. package/dist/index.d.mts +6 -6
  74. package/dist/index.mjs +11 -11
  75. package/dist/{init-BEiNZBsJ.mjs → init-mdJ-v4Xp.mjs} +24 -16
  76. package/dist/integrations/event-gateway.d.mts +1 -1
  77. package/dist/integrations/event-gateway.mjs +2 -2
  78. package/dist/integrations/index.d.mts +2 -2
  79. package/dist/integrations/index.mjs +1 -1
  80. package/dist/integrations/jobs/index.d.mts +1 -1
  81. package/dist/integrations/jobs/index.mjs +100 -98
  82. package/dist/integrations/mcp/index.d.mts +12 -3
  83. package/dist/integrations/mcp/index.mjs +2 -2
  84. package/dist/integrations/mcp/testing.d.mts +1 -1
  85. package/dist/integrations/mcp/testing.mjs +2 -2
  86. package/dist/integrations/streamline.mjs +3 -3
  87. package/dist/integrations/webhooks.d.mts +124 -1
  88. package/dist/integrations/webhooks.mjs +180 -2
  89. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  90. package/dist/integrations/websocket-redis.mjs +1 -1
  91. package/dist/integrations/websocket.mjs +38 -35
  92. package/dist/{interface-DYzM0q-u.mjs → interface-DznJrm9R.mjs} +1 -1
  93. package/dist/{jsonBody-BYbLDi5D.mjs → jsonBody-CfGH-GxO.mjs} +2 -2
  94. package/dist/{keys-C0_pa1tY.mjs → keys-WeKvY1dE.mjs} +1 -1
  95. package/dist/{loadResources-yawi9tAE.mjs → loadResources-Bj-aSsXi.mjs} +4 -3
  96. package/dist/{loadResourcesFromEntry-BAjYZonx.mjs → loadResourcesFromEntry-DwaxNcE2.mjs} +2 -2
  97. package/dist/logger/index.mjs +1 -1
  98. package/dist/{mcpPlugin-B_es0hiF.mjs → mcpPlugin-aJg5TqHi.mjs} +76 -36
  99. package/dist/{memory-DR6wpa9F.mjs → memory-qYSB7rMQ.mjs} +5 -5
  100. package/dist/{metrics-CUdw_wLB.mjs → metrics-D7uchLjN.mjs} +2 -2
  101. package/dist/middleware/index.d.mts +1 -1
  102. package/dist/middleware/index.mjs +3 -3
  103. package/dist/migrations/index.mjs +3 -3
  104. package/dist/{module-0pFJA6YW.mjs → module-CwKsgZXp.mjs} +1 -1
  105. package/dist/{multipartBody-wOgj_fnj.mjs → multipartBody-4w-S3-PU.mjs} +3 -3
  106. package/dist/{openapi-B2L4js1v.mjs → openapi-BN5cQaO2.mjs} +6 -6
  107. package/dist/outbox-admin/index.d.mts +85 -0
  108. package/dist/outbox-admin/index.mjs +115 -0
  109. package/dist/outbox-module-CXB79bvt.mjs +839 -0
  110. package/dist/outbox-module-D-VFq37T.d.mts +368 -0
  111. package/dist/permissions/index.d.mts +3 -3
  112. package/dist/permissions/index.mjs +6 -6
  113. package/dist/{permissions-L--QDAir.mjs → permissions-BAfwgBxc.mjs} +120 -8
  114. package/dist/{pipe-DRDNsI2T.mjs → pipe-BzzoEhHj.mjs} +3 -5
  115. package/dist/pipeline/index.d.mts +1 -1
  116. package/dist/pipeline/index.mjs +2 -2
  117. package/dist/{pipeline-UPQFatit.mjs → pipeline-q69DZzgG.mjs} +17 -15
  118. package/dist/plugins/index.d.mts +6 -6
  119. package/dist/plugins/index.mjs +16 -16
  120. package/dist/plugins/response-cache.mjs +2 -2
  121. package/dist/plugins/tracing-entry.mjs +2 -2
  122. package/dist/{pluralize-O0FZgESl.mjs → pluralize-B7xf8qZF.mjs} +1 -1
  123. package/dist/{preloadResources-Dz6BjzM6.mjs → preloadResources-BkyqeTDz.mjs} +1 -1
  124. package/dist/{preloadResources-CyoJMSiO.d.mts → preloadResources-g07ldtzh.d.mts} +30 -7
  125. package/dist/presets/filesUpload.d.mts +2 -2
  126. package/dist/presets/filesUpload.mjs +6 -6
  127. package/dist/presets/index.d.mts +2 -2
  128. package/dist/presets/index.mjs +2 -2
  129. package/dist/presets/multiTenant.d.mts +1 -1
  130. package/dist/presets/multiTenant.mjs +3 -3
  131. package/dist/presets/search.d.mts +2 -2
  132. package/dist/presets/search.mjs +2 -2
  133. package/dist/{presets-C4-g4_4P.mjs → presets-D-h0uWtP.mjs} +6 -5
  134. package/dist/projection-BKM0Ui-s.mjs +42 -0
  135. package/dist/{queryCachePlugin-BZr8v46E.mjs → queryCachePlugin-DhsZ0ZIc.mjs} +50 -8
  136. package/dist/{rateLimit-CMPY5Qms.mjs → rateLimit-D4-ASwsI.mjs} +2 -2
  137. package/dist/{rateLimitKey-Dn5JNmkj.mjs → rateLimitKey-XngqVNEV.mjs} +1 -1
  138. package/dist/{realtime-C5NoxWFY.mjs → realtime-CTkv3O59.mjs} +10 -10
  139. package/dist/{redis-stream-qN6A-RFv.d.mts → redis-stream-mwLWlfHG.d.mts} +1 -1
  140. package/dist/{registerRoute-CBN84fc7.mjs → registerRoute-DNnd3vkz.mjs} +10 -8
  141. package/dist/registry/index.d.mts +3 -3
  142. package/dist/registry/index.mjs +3 -3
  143. package/dist/{registry-4213U4zb.mjs → registry-CLxCR5LF.mjs} +2 -2
  144. package/dist/{renewingLease-Dll8mGgc.mjs → renewingLease-BnWtbA2U.mjs} +1 -1
  145. package/dist/{replyHelpers-DIiZKGUt.mjs → replyHelpers-BSBVCwL3.mjs} +2 -2
  146. package/dist/{repositoryFilter-f9eAfq_l.mjs → repositoryFilter-DKny7PDn.mjs} +1 -1
  147. package/dist/{requestContext-2TxXOJ7T.mjs → requestContext-uYE35JxI.mjs} +1 -1
  148. package/dist/{rolldown-runtime-CUsjNX9R.mjs → rolldown-runtime-DNg-KNCk.mjs} +1 -1
  149. package/dist/runtimeCapabilities-Dm88DaTW.mjs +49 -0
  150. package/dist/{schedules-DdtlICTf.mjs → schedules-CAcZgia0.mjs} +3 -3
  151. package/dist/{schemaConverter-D--wdJM-.mjs → schemaConverter-XH1RZRyu.mjs} +1 -1
  152. package/dist/{schemaIR-Bm2enbnh.mjs → schemaIR-CDvclAew.mjs} +2 -2
  153. package/dist/schemas/index.mjs +1 -1
  154. package/dist/scim/index.mjs +2 -2
  155. package/dist/scope/index.mjs +5 -5
  156. package/dist/{scope-DHx5Fous.mjs → scope-BaWcqBvr.mjs} +4 -4
  157. package/dist/{simpleEqualityMatcher-DjQJ7FKA.mjs → simpleEqualityMatcher-Se8VrsaP.mjs} +1 -1
  158. package/dist/{sse-CUTo9y0X.mjs → sse-Cf0jUHsh.mjs} +7 -6
  159. package/dist/{sseStream-B_5XcVVd.mjs → sseStream-Bvpi-UXQ.mjs} +2 -2
  160. package/dist/{store-helpers-D1MP_gRG.mjs → store-helpers-B88p99Z2.mjs} +1 -1
  161. package/dist/{streaming-PSZwi8JV.mjs → streaming-DsT1B5zJ.mjs} +1 -1
  162. package/dist/{subscribe-helpers-C0Tb2aUp.mjs → subscribe-helpers-CLIOHsOK.mjs} +2 -2
  163. package/dist/sync/changelog.mjs +1 -1
  164. package/dist/testing/cleanupStoreContract.d.mts +1 -1
  165. package/dist/testing/cleanupStoreContract.mjs +12 -6
  166. package/dist/testing/index.d.mts +52 -13
  167. package/dist/testing/index.mjs +110 -40
  168. package/dist/testing/outboxStoreContract.d.mts +11 -2
  169. package/dist/testing/outboxStoreContract.mjs +110 -21
  170. package/dist/testing/storageContract.mjs +11 -6
  171. package/dist/{transactionContext-dAfBVUrk.mjs → transactionContext-BYtWnwAS.mjs} +1 -1
  172. package/dist/{typeGuards-B0ryPo3I.mjs → typeGuards-jjKJDN13.mjs} +1 -1
  173. package/dist/types/index.d.mts +3 -3
  174. package/dist/types/index.mjs +1 -1
  175. package/dist/types/storage.mjs +1 -1
  176. package/dist/{types-CBgvkp8O.d.mts → types-BcTumAz_.d.mts} +128 -18
  177. package/dist/{types-CwcUQ610.d.mts → types-BfC83KsP.d.mts} +15 -1
  178. package/dist/{types-Cys0ko9Z.mjs → types-CoqIRrkY.mjs} +4 -4
  179. package/dist/{types-C4QE8qnm.d.mts → types-DVLddu2q.d.mts} +144 -1
  180. package/dist/usage/index.d.mts +1 -1
  181. package/dist/usage/index.mjs +0 -0
  182. package/dist/{userHelpers-ChYJ6CdK.mjs → userHelpers-CWF5pbZD.mjs} +1 -1
  183. package/dist/utils/index.d.mts +57 -5
  184. package/dist/utils/index.mjs +12 -11
  185. package/dist/{utils-vL8BrDE2.mjs → utils-h1V_sT-t.mjs} +38 -7
  186. package/dist/{validate-_KbifpHF.mjs → validate-CdQlz52F.mjs} +2 -2
  187. package/dist/{versioning-D_rhHfZm.d.mts → versioning-DEs-_hFi.d.mts} +1 -1
  188. package/dist/{versioning-CHDTor3P.mjs → versioning-rZbyLVD-.mjs} +4 -4
  189. package/package.json +31 -20
  190. package/skills/arc/SKILL.md +96 -4
  191. package/skills/arc/references/api-reference.md +19 -0
  192. package/skills/arc/references/production.md +2 -2
  193. package/dist/fastify-augmentation-zh0_AYHa.d.mts +0 -85
  194. package/dist/projection-BHw6xlOQ.mjs +0 -18
@@ -1,11 +1,9 @@
1
- /*! @classytic/arc v2.31.0 | MIT | https://classytic.com */
2
- import { t as MemoryEventTransport } from "../EventTransport-9mlTgqzE.mjs";
3
- import { a as createEventRegistry, i as wrapWithSchema, n as subscribeWithSchema, o as defineEvent, r as wrapWithBoundary, t as subscribeWithBoundary } from "../subscribe-helpers-C0Tb2aUp.mjs";
4
- import { i as withRetry, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-CkffEihm.mjs";
5
- import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-D1MP_gRG.mjs";
6
- import { and, anyOf, eq, lte, ne, or } from "@classytic/repo-core/filter";
7
- import { InvalidOutboxEventError, OutboxOwnershipError } from "@classytic/primitives/outbox";
8
- import { update } from "@classytic/repo-core/update";
1
+ /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
+ import { n as MemoryEventTransport } from "../EventTransport-tq4CEB-q.mjs";
3
+ import { a as createEventRegistry, i as wrapWithSchema, n as subscribeWithSchema, o as defineEvent, r as wrapWithBoundary, t as subscribeWithBoundary } from "../subscribe-helpers-CLIOHsOK.mjs";
4
+ import { i as withRetry, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-Dar8qRaj.mjs";
5
+ import { i as exponentialBackoff, n as EventOutbox, r as repositoryAsOutboxStore, t as createOutboxModule } from "../outbox-module-CXB79bvt.mjs";
6
+ import { MemoryOutboxStore } from "@classytic/primitives/memory-outbox";
9
7
  //#region src/events/eventTypes.ts
10
8
  /**
11
9
  * Event Type Constants and Helpers
@@ -57,753 +55,4 @@ const CACHE_EVENTS = Object.freeze({
57
55
  TAG_VERSION_BUMPED: "arc.cache.tag.bumped"
58
56
  });
59
57
  //#endregion
60
- //#region src/events/memory-outbox.ts
61
- /**
62
- * In-memory outbox store — reference implementation for dev / testing.
63
- *
64
- * Supports the full `OutboxStore` capability set (claim/lease, fail/retry,
65
- * dedupe, visibleAt). Production deployments should pass a `repository` to
66
- * `EventOutbox` instead of using this; the memory store exists for unit
67
- * tests and single-process dev where durable persistence is noise.
68
- */
69
- const DEFAULT_LEASE_MS$2 = 3e4;
70
- var MemoryOutboxStore = class {
71
- entries = [];
72
- seenDedupeKeys = /* @__PURE__ */ new Set();
73
- /**
74
- * Drop every entry (including delivered / failed / dead-lettered rows) and
75
- * the dedupe-key memory.
76
- *
77
- * For tests only — it is the in-memory analogue of truncating the table, and
78
- * exists so a suite can isolate cases against ONE store instance (the
79
- * `reset` hook of `runOutboxStoreContract`). Not part of `OutboxStore`; a
80
- * production store must never expose this.
81
- */
82
- clear() {
83
- this.entries.length = 0;
84
- this.seenDedupeKeys.clear();
85
- }
86
- async save(event, options) {
87
- if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
88
- if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
89
- if (options?.dedupeKey) {
90
- if (this.seenDedupeKeys.has(options.dedupeKey)) return;
91
- this.seenDedupeKeys.add(options.dedupeKey);
92
- }
93
- this.entries.push({
94
- event,
95
- status: "pending",
96
- attempts: 0,
97
- visibleAt: options?.visibleAt?.getTime() ?? 0,
98
- leaseOwner: null,
99
- leaseExpiresAt: 0,
100
- deliveredAt: null,
101
- firstFailedAt: null,
102
- lastFailedAt: null,
103
- lastError: null,
104
- dedupeKey: options?.dedupeKey
105
- });
106
- }
107
- async getPending(limit) {
108
- const now = Date.now();
109
- return this.entries.filter((e) => e.status === "pending" && e.visibleAt <= now && (e.leaseOwner === null || e.leaseExpiresAt <= now)).slice(0, limit).map((e) => e.event);
110
- }
111
- async claimPending(options) {
112
- const now = Date.now();
113
- const limit = options?.limit ?? 100;
114
- const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS$2;
115
- const consumerId = options?.consumerId ?? "anonymous";
116
- const typeFilter = options?.types ? new Set(options.types) : null;
117
- const claimed = [];
118
- for (const entry of this.entries) {
119
- if (claimed.length >= limit) break;
120
- if (entry.status !== "pending") continue;
121
- if (entry.visibleAt > now) continue;
122
- if (entry.leaseOwner !== null && entry.leaseExpiresAt > now) continue;
123
- if (typeFilter && !typeFilter.has(entry.event.type)) continue;
124
- entry.leaseOwner = consumerId;
125
- entry.leaseExpiresAt = now + leaseMs;
126
- entry.attempts++;
127
- claimed.push(entry.event);
128
- }
129
- return claimed;
130
- }
131
- async acknowledge(eventId, options) {
132
- const entry = this.entries.find((e) => e.event.meta.id === eventId);
133
- if (!entry) return;
134
- if (entry.status === "delivered") return;
135
- if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
136
- entry.status = "delivered";
137
- entry.deliveredAt = Date.now();
138
- entry.leaseOwner = null;
139
- }
140
- async fail(eventId, error, options) {
141
- const entry = this.entries.find((e) => e.event.meta.id === eventId);
142
- if (!entry) return;
143
- if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
144
- const now = Date.now();
145
- entry.lastError = error;
146
- entry.leaseOwner = null;
147
- entry.leaseExpiresAt = 0;
148
- if (entry.firstFailedAt === null) entry.firstFailedAt = now;
149
- entry.lastFailedAt = now;
150
- if (options?.deadLetter) {
151
- entry.status = "dead_letter";
152
- return;
153
- }
154
- entry.status = "pending";
155
- entry.visibleAt = options?.retryAt ? options.retryAt.getTime() : 0;
156
- }
157
- async getDeadLettered(limit) {
158
- const out = [];
159
- for (const entry of this.entries) {
160
- if (out.length >= limit) break;
161
- if (entry.status !== "dead_letter") continue;
162
- out.push({
163
- event: entry.event,
164
- error: {
165
- message: entry.lastError?.message ?? "unknown",
166
- ...entry.lastError?.code !== void 0 ? { code: entry.lastError.code } : {}
167
- },
168
- attempts: entry.attempts,
169
- firstFailedAt: new Date(entry.firstFailedAt ?? entry.lastFailedAt ?? Date.now()),
170
- lastFailedAt: new Date(entry.lastFailedAt ?? entry.firstFailedAt ?? Date.now())
171
- });
172
- }
173
- return out;
174
- }
175
- async purge(olderThanMs) {
176
- const cutoff = Date.now() - olderThanMs;
177
- let purged = 0;
178
- for (let i = this.entries.length - 1; i >= 0; i--) {
179
- const entry = this.entries[i];
180
- if (!entry) continue;
181
- if (entry.status === "delivered" && entry.deliveredAt !== null && entry.deliveredAt < cutoff) {
182
- if (entry.dedupeKey) this.seenDedupeKeys.delete(entry.dedupeKey);
183
- this.entries.splice(i, 1);
184
- purged++;
185
- }
186
- }
187
- return purged;
188
- }
189
- /** Test helper: inspect entry by id */
190
- _getEntry(eventId) {
191
- return this.entries.find((e) => e.event.meta.id === eventId);
192
- }
193
- };
194
- //#endregion
195
- //#region src/events/outbox/backoff.ts
196
- /**
197
- * Compute a `retryAt` `Date` using exponential backoff with jitter.
198
- *
199
- * This is a convenience helper for store authors implementing
200
- * {@link OutboxStore.fail}: call it to compute the retry visibility window
201
- * based on the event's current attempt count.
202
- *
203
- * Formula: `delay = min(maxMs, baseMs * 2^(attempt - 1)) * (1 + random * jitter)`
204
- *
205
- * @example Basic usage inside a store's `fail()` method
206
- * ```typescript
207
- * async fail(eventId, error, options) {
208
- * const entry = await this.findById(eventId);
209
- * entry.attempts++;
210
- * if (entry.attempts >= MAX_ATTEMPTS) {
211
- * return this.deadLetter(eventId, error);
212
- * }
213
- * const retryAt = exponentialBackoff({ attempt: entry.attempts });
214
- * entry.visibleAt = retryAt;
215
- * await this.update(entry);
216
- * }
217
- * ```
218
- *
219
- * @example Tuning for a faster transport
220
- * ```typescript
221
- * exponentialBackoff({ attempt: 3, baseMs: 250, maxMs: 10_000, jitter: 0.3 });
222
- * // attempt=1 → ~250ms ±30%
223
- * // attempt=2 → ~500ms ±30%
224
- * // attempt=3 → ~1000ms ±30%
225
- * // attempt=10 → capped at 10_000ms
226
- * ```
227
- */
228
- function exponentialBackoff(options) {
229
- const attempt = Math.max(1, Math.floor(options.attempt));
230
- const baseMs = options.baseMs ?? 1e3;
231
- const maxMs = options.maxMs ?? 6e4;
232
- const jitter = Math.max(0, Math.min(1, options.jitter ?? .2));
233
- const now = options.now ?? Date.now();
234
- const exp = baseMs * 2 ** (attempt - 1);
235
- const capped = Math.min(maxMs, exp);
236
- const jittered = jitter > 0 ? capped * (1 + Math.random() * jitter) : capped;
237
- return new Date(now + jittered);
238
- }
239
- //#endregion
240
- //#region src/events/repository-outbox-adapter.ts
241
- /**
242
- * RepositoryLike → OutboxStore adapter.
243
- *
244
- * Maps the `OutboxStore` vocabulary (save / claimPending / acknowledge /
245
- * fail / getDeadLettered / purge) onto arc's own `RepositoryLike` primitives
246
- * (create / getOne / findAll / deleteMany / findOneAndUpdate). `EventOutbox`
247
- * wraps a passed repository with this helper when you use the
248
- * `{ repository }` option; the function is also re-exported from
249
- * `@classytic/arc/events` so consumers can build and decorate the store
250
- * manually (metrics, tracing, multi-transport fan-out).
251
- *
252
- * Portability: filters compose via `@classytic/repo-core/filter` and
253
- * updates via `@classytic/repo-core/update`. The primary-key column name
254
- * is read from `repository.idField` — mongokit defaults to `_id`,
255
- * sqlitekit / pgkit / prismakit to the schema's declared PK. The adapter
256
- * therefore runs on any kit that implements `StandardRepo.findOneAndUpdate`
257
- * + `getOne` + `getAll` + `deleteMany` + `create`.
258
- *
259
- * `fail()` uses a lease-gated read-then-write pair to preserve
260
- * `firstFailedAt` across retries without relying on Mongo's aggregation-
261
- * pipeline `$ifNull`. Leases guarantee single-writer during the failure
262
- * window (`claimPending` filters out non-owned rows), so the two calls are
263
- * safe under concurrent relayers.
264
- *
265
- * Why we DON'T use `StandardRepo.claim()` (repo-core 0.4+) anywhere in this
266
- * adapter — design note for the next reader who wants to "modernize" us:
267
- *
268
- * • `claimPending` (FIFO claim-next loop): `claim(id, ...)` requires the
269
- * candidate id upfront, but the relayer doesn't have it — that's the
270
- * whole point of `findOneAndUpdate({ filter, sort: { createdAt: 1 } })`,
271
- * which finds + claims in a SINGLE round-trip per claimed event.
272
- * Replacing with `getOne(filter, sort) → claim(id, ...)` doubles the
273
- * round-trips, opens a TOCTOU window where the candidate state can
274
- * change between read and CAS (we'd need a retry loop on null-on-race),
275
- * and gains nothing — the existing call is already atomic and
276
- * race-free. Throughput regression with no upside; do not adopt.
277
- *
278
- * • `acknowledge` (pending → delivered): id is in scope, but the current
279
- * filter uses `ne('status', 'delivered')` to allow ack from any
280
- * non-delivered state (defensive; lease invariant means it's pending
281
- * in practice). `claim` requires an exact `from` value, so adopting
282
- * would tighten the source-state predicate from "not delivered" to
283
- * "exactly pending". That's a behavior change — even if the new
284
- * behavior is arguably tighter/safer, the constraint here is "no
285
- * semantic regression". Keep `findOneAndUpdate`.
286
- *
287
- * • `fail` (any → pending|dead_letter): the current `baseFilter` is
288
- * id-only (with optional `leaseOwner` guard) — there's NO source-state
289
- * predicate at all. Adopting `claim` would ADD `from: 'pending'`,
290
- * blocking `fail()` calls against rows in any other state. Same "no
291
- * semantic regression" concern; keep `findOneAndUpdate`. The
292
- * read-then-write pair also exists here to preserve `firstFailedAt`
293
- * portably (see paragraph above) — `claim` doesn't simplify that.
294
- *
295
- * No kit currently ships a filter-based `claimNext`/`claimPending` (Path C
296
- * in the v2.11.x evaluation) — checked mongokit 3.13.0 + sqlitekit 0.3.0.
297
- * If/when one appears, the FIFO loop becomes a 1-call adoption candidate.
298
- */
299
- const DEFAULT_LEASE_MS$1 = 3e4;
300
- const DEFAULT_CLAIM_LIMIT = 100;
301
- const DEFAULT_PURGE_BATCH = 500;
302
- /** Column this adapter reads/writes for the visibility timestamp. */
303
- const DEFAULT_VISIBLE_AT_FIELD = "visibleAt";
304
- function repositoryAsOutboxStore(repository, options) {
305
- const visibleAtField = options?.visibleAtField ?? DEFAULT_VISIBLE_AT_FIELD;
306
- const missing = [];
307
- if (typeof repository.create !== "function") missing.push("create");
308
- if (typeof repository.getOne !== "function") missing.push("getOne");
309
- if (typeof repository.getAll !== "function") missing.push("getAll");
310
- if (typeof repository.deleteMany !== "function") missing.push("deleteMany");
311
- if (typeof repository.findOneAndUpdate !== "function") missing.push("findOneAndUpdate");
312
- if (missing.length > 0) throw new Error(`EventOutbox: repository is missing required methods: ${missing.join(", ")}. mongokit ≥3.10.2 satisfies all five; other kits must implement them to back the outbox.`);
313
- const r = repository;
314
- const idField = repository.idField ?? "_id";
315
- /**
316
- * Unwrap mongokit's pagination envelope ({ data, total, ... }) — some
317
- * kits may return a bare array when pagination is disabled. Handle both.
318
- */
319
- const unwrapDocs = (result) => {
320
- if (Array.isArray(result)) return result;
321
- return result?.data ?? [];
322
- };
323
- const isDuplicateKeyError = createIsDuplicateKeyError(repository);
324
- const safeGetOne = createSafeGetOne(repository);
325
- const isWellFormed = (event) => !!event && typeof event.type === "string" && !!event.meta?.id;
326
- /**
327
- * Filter matching every row that's eligible to be claimed by a relayer:
328
- * status=pending, visible now, and either unleased or under an expired
329
- * lease. Used by `getPending` and `claimPending` — defined once so the
330
- * two code paths stay in lockstep.
331
- */
332
- const claimableFilter = (now) => and(eq("status", "pending"), lte(visibleAtField, now), or(eq("leaseOwner", null), lte("leaseExpiresAt", now)));
333
- return {
334
- /**
335
- * Transactional: `save` forwards `options.session` to the backing
336
- * repository's `create`, so the event row joins the caller's transaction on
337
- * the SAME connection and commits or rolls back with the domain write.
338
- *
339
- * Declared explicitly because `OutboxWriteOptions.session` is best-effort by
340
- * contract — a caller cannot infer atomicity from the presence of an outbox,
341
- * only from this flag. See `OutboxStore.transactionalSave` in
342
- * `@classytic/primitives/outbox`.
343
- */
344
- transactionalSave: true,
345
- async save(event, options) {
346
- if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
347
- if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
348
- const now = /* @__PURE__ */ new Date();
349
- const doc = {
350
- [idField]: event.meta.id,
351
- event,
352
- type: event.type,
353
- status: "pending",
354
- attempts: 0,
355
- [visibleAtField]: options?.visibleAt ?? now,
356
- leaseOwner: null,
357
- leaseExpiresAt: null,
358
- deliveredAt: null,
359
- firstFailedAt: null,
360
- lastFailedAt: null,
361
- lastError: null,
362
- dedupeKey: options?.dedupeKey ?? null,
363
- partitionKey: options?.partitionKey ?? null,
364
- headers: options?.headers ? { ...options.headers } : null,
365
- createdAt: now
366
- };
367
- try {
368
- await r.create(doc, options?.session ? { session: options.session } : void 0);
369
- } catch (err) {
370
- if (isDuplicateKeyError(err)) return;
371
- throw err;
372
- }
373
- },
374
- async getPending(limit) {
375
- const now = /* @__PURE__ */ new Date();
376
- const result = await r.getAll({
377
- filters: claimableFilter(now),
378
- sort: { createdAt: 1 },
379
- page: 1,
380
- limit
381
- });
382
- return unwrapDocs(result).map((d) => d.event).filter(isWellFormed);
383
- },
384
- async claimPending(options) {
385
- const limit = options?.limit ?? DEFAULT_CLAIM_LIMIT;
386
- const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS$1;
387
- const consumerId = options?.consumerId ?? "anonymous";
388
- const typeFilter = options?.types?.length ? anyOf("type", options.types) : null;
389
- const now = /* @__PURE__ */ new Date();
390
- const candidateFilter = typeFilter ? and(claimableFilter(now), typeFilter) : claimableFilter(now);
391
- const candidates = unwrapDocs(await r.getAll({
392
- filters: candidateFilter,
393
- sort: { createdAt: 1 },
394
- page: 1,
395
- limit
396
- }));
397
- if (candidates.length === 0) return [];
398
- return (await Promise.all(candidates.map((candidate) => {
399
- const claimNow = /* @__PURE__ */ new Date();
400
- const leaseExpiresAt = new Date(claimNow.getTime() + leaseMs);
401
- return r.findOneAndUpdate(and(eq(idField, candidate.event.meta.id), claimableFilter(claimNow)), update({
402
- set: {
403
- leaseOwner: consumerId,
404
- leaseExpiresAt
405
- },
406
- inc: { attempts: 1 }
407
- }), { returnDocument: "after" });
408
- }))).filter((doc) => doc !== null).map((doc) => doc.event).filter(isWellFormed);
409
- },
410
- async acknowledge(eventId, options) {
411
- const now = /* @__PURE__ */ new Date();
412
- const baseFilter = and(eq(idField, eventId), ne("status", "delivered"));
413
- const filter = options?.consumerId ? and(baseFilter, eq("leaseOwner", options.consumerId)) : baseFilter;
414
- if (await r.findOneAndUpdate(filter, update({ set: {
415
- status: "delivered",
416
- deliveredAt: now,
417
- leaseOwner: null,
418
- leaseExpiresAt: null
419
- } }), { returnDocument: "after" })) return;
420
- const current = await safeGetOne(eq(idField, eventId));
421
- if (!current) return;
422
- if (current.status === "delivered") return;
423
- if (options?.consumerId && current.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, current.leaseOwner);
424
- },
425
- async fail(eventId, error, options) {
426
- const now = /* @__PURE__ */ new Date();
427
- const targetStatus = options?.deadLetter ? "dead_letter" : "pending";
428
- const visibleAt = options?.retryAt ?? now;
429
- const baseFilter = eq(idField, eventId);
430
- const filter = options?.consumerId ? and(baseFilter, eq("leaseOwner", options.consumerId)) : baseFilter;
431
- const current = await safeGetOne(baseFilter);
432
- if (!current) return;
433
- if (options?.consumerId && current.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, current.leaseOwner);
434
- const errorInfo = error.code ? {
435
- message: error.message,
436
- code: error.code
437
- } : { message: error.message };
438
- const firstFailedAt = current.firstFailedAt ?? now;
439
- if (await r.findOneAndUpdate(filter, update({ set: {
440
- status: targetStatus,
441
- [visibleAtField]: visibleAt,
442
- leaseOwner: null,
443
- leaseExpiresAt: null,
444
- lastFailedAt: now,
445
- lastError: errorInfo,
446
- firstFailedAt
447
- } }), { returnDocument: "after" })) return;
448
- if (options?.consumerId) {
449
- const after = await safeGetOne(baseFilter);
450
- if (after && after.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, after.leaseOwner);
451
- }
452
- },
453
- async getDeadLettered(limit) {
454
- const result = await r.getAll({
455
- filters: eq("status", "dead_letter"),
456
- sort: { [idField]: 1 },
457
- page: 1,
458
- limit
459
- });
460
- return unwrapDocs(result).filter((d) => isWellFormed(d.event)).map((d) => ({
461
- event: d.event,
462
- error: {
463
- message: d.lastError?.message ?? "unknown",
464
- ...d.lastError?.code !== void 0 ? { code: d.lastError.code } : {}
465
- },
466
- attempts: d.attempts,
467
- firstFailedAt: d.firstFailedAt ?? d.lastFailedAt ?? d.createdAt,
468
- lastFailedAt: d.lastFailedAt ?? d.firstFailedAt ?? d.createdAt
469
- }));
470
- },
471
- async purge(olderThanMs) {
472
- const cutoff = new Date(Date.now() - olderThanMs);
473
- let totalDeleted = 0;
474
- for (;;) {
475
- const result = await r.getAll({
476
- filters: and(eq("status", "delivered"), lte("deliveredAt", cutoff)),
477
- sort: { deliveredAt: 1 },
478
- page: 1,
479
- limit: DEFAULT_PURGE_BATCH,
480
- select: idField
481
- });
482
- const batch = unwrapDocs(result);
483
- if (batch.length === 0) break;
484
- const ids = batch.map((d) => d[idField]);
485
- const res = await r.deleteMany(anyOf(idField, ids));
486
- totalDeleted += res.deletedCount ?? 0;
487
- if (batch.length < DEFAULT_PURGE_BATCH) break;
488
- }
489
- return totalDeleted;
490
- }
491
- };
492
- }
493
- //#endregion
494
- //#region src/events/outbox/relay.ts
495
- /** Default outbox retention — delivered events older than this are eligible for purge */
496
- const DEFAULT_OUTBOX_RETENTION_MS = 10080 * 60 * 1e3;
497
- const DEFAULT_LEASE_MS = 3e4;
498
- /**
499
- * Hard cap on the in-process attempt-counter map. Entries are cleared on
500
- * local ack and on dead-letter, but an event that fails HERE and is later
501
- * acked by a DIFFERENT relay worker (lease expiry → reclaim) never drains
502
- * locally — without a cap the map grows for the life of the process in
503
- * multi-worker deployments. Eviction is oldest-write-first; evicting an
504
- * entry only resets its non-authoritative in-process count (the documented
505
- * contract already says counts reset on restart — durable counts live in
506
- * the store).
507
- */
508
- const MAX_TRACKED_ATTEMPTS = 1e4;
509
- var EventOutbox = class {
510
- _store;
511
- _transport;
512
- _batchSize;
513
- _consumerId;
514
- _leaseMs;
515
- _onError;
516
- _usePublishMany;
517
- _sessionProvider;
518
- _traceContextProvider;
519
- _failurePolicy;
520
- /**
521
- * In-process attempt counter per event id. Accurate within this relay
522
- * process; resets on restart. Populated as failures occur and cleared on
523
- * successful ack or dead-letter transition; additionally hard-capped at
524
- * {@link MAX_TRACKED_ATTEMPTS} (oldest-write evicted first) so events
525
- * resolved by another relay worker can't accumulate forever. For durable
526
- * authoritative counts, apps can query the store directly inside
527
- * {@link OutboxFailurePolicy}.
528
- */
529
- _attempts = /* @__PURE__ */ new Map();
530
- constructor(opts) {
531
- if (opts.repository) this._store = repositoryAsOutboxStore(opts.repository);
532
- else if (opts.store) this._store = opts.store;
533
- else throw new Error("EventOutbox: either `repository` or `store` must be provided. Pass a RepositoryLike (mongokit / prismakit) for the common case, or a concrete OutboxStore (memory / custom) for non-repository backends.");
534
- this._transport = opts.transport;
535
- this._batchSize = opts.batchSize ?? 100;
536
- this._consumerId = opts.consumerId ?? `relay-${Math.random().toString(36).slice(2, 10)}`;
537
- this._leaseMs = opts.leaseMs ?? DEFAULT_LEASE_MS;
538
- this._onError = opts.onError;
539
- this._usePublishMany = opts.usePublishMany ?? true;
540
- this._failurePolicy = opts.failurePolicy;
541
- this._sessionProvider = opts.sessionProvider;
542
- this._traceContextProvider = opts.traceContextProvider;
543
- }
544
- /** Unique consumer ID used for lease ownership when the store supports claims */
545
- get consumerId() {
546
- return this._consumerId;
547
- }
548
- /**
549
- * Store event in outbox.
550
- *
551
- * Validates that `event.type` and `event.meta.id` are present — throws
552
- * {@link InvalidOutboxEventError} otherwise, so corrupt rows can never
553
- * be persisted via this API.
554
- *
555
- * Pass `options.session` to participate in a host-managed DB transaction
556
- * (store must support session-aware writes). Other options (`visibleAt`,
557
- * `dedupeKey`, `partitionKey`, `headers`) are forwarded to stores that
558
- * implement them and ignored otherwise.
559
- */
560
- async store(event, options) {
561
- if (!event || typeof event !== "object") throw new InvalidOutboxEventError("event is not an object");
562
- if (!event.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
563
- if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
564
- let effective = options;
565
- if (effective?.session === void 0 && this._sessionProvider) {
566
- const autoSession = this._sessionProvider();
567
- if (autoSession !== void 0) effective = {
568
- ...effective ?? {},
569
- session: autoSession
570
- };
571
- }
572
- if (!effective?.headers?.traceparent && this._traceContextProvider) {
573
- const traceHeaders = this._traceContextProvider();
574
- if (traceHeaders && Object.keys(traceHeaders).length > 0) effective = {
575
- ...effective ?? {},
576
- headers: {
577
- ...traceHeaders,
578
- ...effective?.headers ?? {}
579
- }
580
- };
581
- }
582
- if (effective?.dedupeKey === void 0 && event.meta.idempotencyKey) effective = {
583
- ...effective ?? {},
584
- dedupeKey: event.meta.idempotencyKey
585
- };
586
- await this._store.save(event, effective);
587
- }
588
- _reportError(kind, error, event) {
589
- if (!this._onError) return;
590
- const err = error instanceof Error ? error : new Error(String(error));
591
- try {
592
- this._onError({
593
- kind,
594
- event,
595
- error: err
596
- });
597
- } catch {}
598
- }
599
- /**
600
- * Relay pending events to transport and return the number of successful
601
- * publish+acknowledge pairs.
602
- *
603
- * For richer observability (per-kind counts, publishMany detection, etc.)
604
- * use {@link relayBatch} which returns a {@link RelayResult}. This method
605
- * is the backward-compatible shortcut that returns just the count.
606
- *
607
- * @returns Number of successfully published AND acknowledged events
608
- */
609
- async relay() {
610
- return (await this.relayBatch()).relayed;
611
- }
612
- /**
613
- * Relay a batch of pending events to the transport and return a rich
614
- * {@link RelayResult} describing the outcome of each event.
615
- *
616
- * Behavior summary:
617
- *
618
- * - **Claim path**: uses {@link OutboxStore.claimPending} when the store
619
- * supports it (safe for multi-worker relay) or falls back to
620
- * {@link OutboxStore.getPending} (single-worker only).
621
- *
622
- * - **Publish path**: if the transport implements
623
- * {@link EventTransport.publishMany} and `usePublishMany` is not disabled,
624
- * the entire batch is sent in one call. Otherwise each event is published
625
- * individually. Either way, per-event outcomes are tracked.
626
- *
627
- * - **Failure path**: if the store implements `fail`, per-event failures
628
- * are reported via `store.fail(...)` and the batch continues. Without
629
- * `fail`, the batch stops on the first failure (legacy behavior).
630
- *
631
- * - **Malformed events**: events missing `type` or `meta.id` abort the
632
- * batch — a well-behaved store must never return them (see
633
- * {@link OutboxStore} semantics #6). The error is reported via `onError`.
634
- *
635
- * - **Ownership mismatches**: if `acknowledge`/`fail` throws
636
- * {@link OutboxOwnershipError} (our lease expired and another worker
637
- * claimed the event), the event is NOT counted as relayed. The other
638
- * worker will re-publish — at-least-once semantics preserved.
639
- *
640
- * @returns Per-kind outcome counts for the batch
641
- */
642
- async relayBatch() {
643
- const empty = {
644
- relayed: 0,
645
- attempted: 0,
646
- publishFailed: 0,
647
- ackFailed: 0,
648
- ownershipMismatches: 0,
649
- malformed: 0,
650
- failHookErrors: 0,
651
- deadLettered: 0,
652
- usedPublishMany: false
653
- };
654
- if (!this._transport) return empty;
655
- const claimStartedAt = Date.now();
656
- const pending = this._store.claimPending ? await this._store.claimPending({
657
- limit: this._batchSize,
658
- consumerId: this._consumerId,
659
- leaseMs: this._leaseMs
660
- }) : await this._store.getPending(this._batchSize);
661
- const claimMs = Date.now() - claimStartedAt;
662
- const publishStartedAt = Date.now();
663
- const valid = [];
664
- let malformed = 0;
665
- for (const event of pending) {
666
- if (!event?.type || !event.meta?.id) {
667
- this._reportError("malformed_event", new InvalidOutboxEventError("store returned event missing type or meta.id — batch aborted"), event);
668
- malformed++;
669
- break;
670
- }
671
- valid.push(event);
672
- }
673
- const counts = {
674
- relayed: 0,
675
- publishFailed: 0,
676
- ackFailed: 0,
677
- ownershipMismatches: 0,
678
- failHookErrors: 0,
679
- deadLettered: 0
680
- };
681
- const canPublishMany = this._usePublishMany && typeof this._transport.publishMany === "function";
682
- const canFail = typeof this._store.fail === "function";
683
- let publishOutcomes;
684
- if (canPublishMany && valid.length > 0) try {
685
- const result = await this._transport.publishMany?.(valid);
686
- publishOutcomes = new Map(result);
687
- } catch (batchErr) {
688
- publishOutcomes = /* @__PURE__ */ new Map();
689
- const err = batchErr instanceof Error ? batchErr : new Error(String(batchErr));
690
- for (const ev of valid) publishOutcomes.set(ev.meta.id, err);
691
- }
692
- else {
693
- publishOutcomes = /* @__PURE__ */ new Map();
694
- for (const event of valid) try {
695
- await this._transport.publish(event);
696
- publishOutcomes.set(event.meta.id, null);
697
- } catch (err) {
698
- publishOutcomes.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
699
- if (!canFail) break;
700
- }
701
- }
702
- let stopBatch = false;
703
- for (const event of valid) {
704
- if (stopBatch) break;
705
- const publishErr = publishOutcomes.get(event.meta.id);
706
- if (publishErr instanceof Error) {
707
- counts.publishFailed++;
708
- this._reportError("publish_failed", publishErr, event);
709
- if (!canFail) {
710
- stopBatch = true;
711
- continue;
712
- }
713
- const attempts = (this._attempts.get(event.meta.id) ?? 0) + 1;
714
- this._attempts.delete(event.meta.id);
715
- this._attempts.set(event.meta.id, attempts);
716
- if (this._attempts.size > MAX_TRACKED_ATTEMPTS) {
717
- const oldest = this._attempts.keys().next().value;
718
- if (oldest !== void 0) this._attempts.delete(oldest);
719
- }
720
- let failOpts = { consumerId: this._consumerId };
721
- if (this._failurePolicy) try {
722
- const decision = await this._failurePolicy({
723
- event,
724
- error: publishErr,
725
- attempts
726
- });
727
- failOpts = {
728
- ...failOpts,
729
- ...decision
730
- };
731
- } catch (policyErr) {
732
- this._reportError("fail_failed", policyErr, event);
733
- }
734
- try {
735
- await this._store.fail?.(event.meta.id, normalizeError(publishErr), failOpts);
736
- if (failOpts.deadLetter) {
737
- counts.deadLettered++;
738
- this._attempts.delete(event.meta.id);
739
- }
740
- } catch (failErr) {
741
- if (failErr instanceof OutboxOwnershipError) {
742
- counts.ownershipMismatches++;
743
- this._reportError("ownership_mismatch", failErr, event);
744
- } else {
745
- counts.failHookErrors++;
746
- this._reportError("fail_failed", failErr, event);
747
- }
748
- }
749
- continue;
750
- }
751
- try {
752
- await this._store.acknowledge(event.meta.id, { consumerId: this._consumerId });
753
- counts.relayed++;
754
- this._attempts.delete(event.meta.id);
755
- } catch (ackErr) {
756
- counts.ackFailed++;
757
- if (ackErr instanceof OutboxOwnershipError) {
758
- counts.ownershipMismatches++;
759
- this._reportError("ownership_mismatch", ackErr, event);
760
- } else this._reportError("acknowledge_failed", ackErr, event);
761
- }
762
- }
763
- return {
764
- relayed: counts.relayed,
765
- attempted: valid.length,
766
- publishFailed: counts.publishFailed,
767
- ackFailed: counts.ackFailed,
768
- ownershipMismatches: counts.ownershipMismatches,
769
- malformed,
770
- failHookErrors: counts.failHookErrors,
771
- deadLettered: counts.deadLettered,
772
- usedPublishMany: canPublishMany && valid.length > 0,
773
- claimMs,
774
- publishMs: Date.now() - publishStartedAt
775
- };
776
- }
777
- /**
778
- * Fetch current dead-lettered events as typed {@link DeadLetteredEvent}
779
- * envelopes. Delegates to {@link OutboxStore.getDeadLettered} — returns
780
- * `[]` when the store doesn't implement it.
781
- *
782
- * Pairs with {@link OutboxFailurePolicy}: apps set a policy that routes to
783
- * `deadLetter: true` after N attempts, then read back with this to alert,
784
- * replay, or archive.
785
- */
786
- async getDeadLettered(limit = 100) {
787
- if (!this._store.getDeadLettered) return [];
788
- return this._store.getDeadLettered(limit);
789
- }
790
- /**
791
- * Purge old **delivered** events from the outbox store.
792
- * Delegates to `store.purge()` if implemented; no-op otherwise.
793
- * @param olderThanMs - Remove events delivered more than this many ms ago (default: 7 days)
794
- * @returns Number of purged events, or 0 if store doesn't support purge
795
- */
796
- async purge(olderThanMs = DEFAULT_OUTBOX_RETENTION_MS) {
797
- if (!this._store.purge) return 0;
798
- return this._store.purge(olderThanMs);
799
- }
800
- };
801
- function normalizeError(err) {
802
- if (err instanceof Error) return {
803
- message: err.message,
804
- code: err.code
805
- };
806
- return { message: String(err) };
807
- }
808
- //#endregion
809
- export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, MemoryEventTransport, MemoryOutboxStore, createDeadLetterPublisher, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };
58
+ export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, MemoryEventTransport, MemoryOutboxStore, createDeadLetterPublisher, createEventRegistry, createOutboxModule, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };