@classytic/arc 2.32.0 → 2.34.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 (193) hide show
  1. package/README.md +1 -1
  2. package/bin/arc.js +58 -17
  3. package/dist/{BaseController-Dk34dxhI.mjs → BaseController-CbkuhNI2.mjs} +329 -35
  4. package/dist/{EventTransport-2uVV34WU.d.mts → EventTransport-BEVP50ka.d.mts} +16 -0
  5. package/dist/{EventTransport-BV10D0oG.mjs → EventTransport-tq4CEB-q.mjs} +39 -2
  6. package/dist/{HookSystem-vimw5D7Z.mjs → HookSystem-DqXkMXkB.mjs} +2 -2
  7. package/dist/{ResourceRegistry-BVr7PgA0.mjs → ResourceRegistry-DoALUlOO.mjs} +3 -2
  8. package/dist/{actionPermissions-Dti7ebyv.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 +39 -7
  14. package/dist/auth/redis-session.mjs +1 -1
  15. package/dist/{authorizationDecision-__T3EJDA.mjs → authorizationDecision-CMMLWmuZ.mjs} +6 -2
  16. package/dist/{types-B7XeLEgK.d.mts → base-Csjc2oF-.d.mts} +74 -74
  17. package/dist/{buildHandler-Dn7rzUJo.mjs → buildHandler-BrdYrG_1.mjs} +4 -4
  18. package/dist/cache/index.mjs +4 -4
  19. package/dist/{caching-DFJqHyFs.mjs → caching-BcGmvnHh.mjs} +2 -2
  20. package/dist/cleanup/index.d.mts +17 -5
  21. package/dist/cleanup/index.mjs +240 -7
  22. package/dist/cli/commands/describe.d.mts +1 -1
  23. package/dist/cli/commands/describe.mjs +3 -3
  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-J99LGTJI.mjs → constants-BxCoxmiN.mjs} +1 -1
  31. package/dist/context/index.mjs +3 -3
  32. package/dist/core/index.d.mts +3 -3
  33. package/dist/core/index.mjs +9 -8
  34. package/dist/{core-BwwmNSp7.mjs → core-BYKa8wHu.mjs} +3 -3
  35. package/dist/{createActionRouter-DkJZWo0m.mjs → createActionRouter-D6T0jWhS.mjs} +8 -8
  36. package/dist/{createAggregationRouter-B1exOVD8.mjs → createAggregationRouter-QLV2PrWP.mjs} +7 -7
  37. package/dist/{createApp-Bv0m-Q6_.mjs → createApp-zR-DEesr.mjs} +128 -24
  38. package/dist/{defineAggregation-gX4No1gt.mjs → defineAggregation-Nv-DwfOf.mjs} +1 -1
  39. package/dist/{defineResource-XFPPUu2c.mjs → defineResource-ZhuLonLm.mjs} +297 -23
  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-DNXXxgjl.mjs → elevation-B0tlY8y1.mjs} +4 -4
  44. package/dist/encryption/index.mjs +3 -3
  45. package/dist/{errorHandler-B2AZUJUj.mjs → errorHandler-C7xBqHsI.mjs} +42 -5
  46. package/dist/{errorHandler-CpYUJmPA.d.mts → errorHandler-DI5AN2b5.d.mts} +14 -3
  47. package/dist/{errors-Bh1KK5ps.mjs → errors-Cd_yy7aW.mjs} +1 -1
  48. package/dist/{eventPlugin-GF5nnBIq.d.mts → eventPlugin-DSNEZWfV.d.mts} +29 -1
  49. package/dist/{eventPlugin-Dk_TyXeE.mjs → eventPlugin-Dar8qRaj.mjs} +65 -9
  50. package/dist/events/index.d.mts +5 -351
  51. package/dist/events/index.mjs +5 -780
  52. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  53. package/dist/events/transports/redis-stream-entry.mjs +1 -1
  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 +7 -7
  58. package/dist/{fastify-augmentation-BppG5bMT.d.mts → fastify-augmentation-DVsTAy6X.d.mts} +3 -3
  59. package/dist/{fields-DEi65pGj.mjs → fields-CojeUkS4.mjs} +1 -1
  60. package/dist/{generate-CRSG0jTv.mjs → generate-DiB0nHPR.mjs} +2 -2
  61. package/dist/{headers-BXjQYbeC.mjs → headers-CveNO9gR.mjs} +2 -2
  62. package/dist/{health-5XnTa2uo.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 +90 -9
  67. package/dist/idempotency/redis.mjs +1 -1
  68. package/dist/{index-zq6VrjaB.d.mts → index-B-DvUzsr.d.mts} +12 -2
  69. package/dist/{index-CpzxqePp.d.mts → index-BcR2bFZQ.d.mts} +707 -373
  70. package/dist/{index-ZWhLTp_s.d.mts → index-C5NnMEKF.d.mts} +1 -1
  71. package/dist/{index-DXlHwvuE.d.mts → index-CFAJEWCX.d.mts} +18 -5
  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 +13 -12
  75. package/dist/{init-BbfP8aky.mjs → init-mdJ-v4Xp.mjs} +18 -12
  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 +99 -97
  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 +1 -1
  92. package/dist/{interface-BP4IJCBO.mjs → interface-DznJrm9R.mjs} +1 -1
  93. package/dist/{jsonBody-DDSR2HFl.mjs → jsonBody-CfGH-GxO.mjs} +2 -2
  94. package/dist/{keys-B-HBTGXl.mjs → keys-WeKvY1dE.mjs} +1 -1
  95. package/dist/{loadResources-DisEAmkm.mjs → loadResources-Bj-aSsXi.mjs} +2 -2
  96. package/dist/{loadResourcesFromEntry-D1MJp13f.mjs → loadResourcesFromEntry-DwaxNcE2.mjs} +2 -2
  97. package/dist/logger/index.mjs +1 -1
  98. package/dist/{mcpPlugin-CpfVpKHE.mjs → mcpPlugin-DX3PHD7K.mjs} +60 -21
  99. package/dist/{memory-DsuWnj4T.mjs → memory-qYSB7rMQ.mjs} +2 -2
  100. package/dist/{metrics-DdQL8syd.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 +2 -2
  104. package/dist/{module-Br81am6m.mjs → module-CwKsgZXp.mjs} +1 -1
  105. package/dist/{multipartBody-BnkBLN7v.mjs → multipartBody-4w-S3-PU.mjs} +1 -1
  106. package/dist/{openapi-DIRzvxC0.mjs → openapi-CkV5mQ53.mjs} +6 -6
  107. package/dist/outbox-admin/index.d.mts +2 -2
  108. package/dist/outbox-admin/index.mjs +4 -4
  109. package/dist/outbox-module-C1Bs_FXF.d.mts +382 -0
  110. package/dist/outbox-module-D1QfHqO4.mjs +846 -0
  111. package/dist/permissions/index.d.mts +2 -2
  112. package/dist/permissions/index.mjs +5 -5
  113. package/dist/{permissions-Dl_m0NCe.mjs → permissions-CP47jdi7.mjs} +25 -6
  114. package/dist/{pipe-ELHCP3zj.mjs → pipe-BzzoEhHj.mjs} +2 -2
  115. package/dist/pipeline/index.d.mts +1 -1
  116. package/dist/pipeline/index.mjs +2 -2
  117. package/dist/{pipeline-CPA-jx0M.mjs → pipeline-q69DZzgG.mjs} +6 -6
  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-C2ZkKbsn.mjs → pluralize-B7xf8qZF.mjs} +1 -1
  123. package/dist/{preloadResources-CDs87fTw.mjs → preloadResources-BkyqeTDz.mjs} +1 -1
  124. package/dist/{preloadResources-T1hmlTEZ.d.mts → preloadResources-g07ldtzh.d.mts} +30 -7
  125. package/dist/presets/filesUpload.d.mts +2 -2
  126. package/dist/presets/filesUpload.mjs +5 -5
  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-Dntd_pUO.mjs → presets-BAPijK_A.mjs} +4 -4
  134. package/dist/projection-BKM0Ui-s.mjs +42 -0
  135. package/dist/{queryCachePlugin-Dhps-Ldt.mjs → queryCachePlugin-DhsZ0ZIc.mjs} +49 -7
  136. package/dist/{rateLimit-C7mmAGs3.mjs → rateLimit-D4-ASwsI.mjs} +2 -2
  137. package/dist/{rateLimitKey-CccEFBfk.mjs → rateLimitKey-XngqVNEV.mjs} +1 -1
  138. package/dist/{realtime-DVvVZZFz.mjs → realtime-G1TC6Yzc.mjs} +9 -9
  139. package/dist/{redis-stream-qN6A-RFv.d.mts → redis-stream-mwLWlfHG.d.mts} +1 -1
  140. package/dist/{registerRoute-B_QrjYZS.mjs → registerRoute-Ba8v0PCa.mjs} +4 -4
  141. package/dist/registry/index.d.mts +3 -3
  142. package/dist/registry/index.mjs +3 -3
  143. package/dist/{registry-DqUmzA9W.mjs → registry-CLxCR5LF.mjs} +2 -2
  144. package/dist/{renewingLease-BpP9wwj7.mjs → renewingLease-BnWtbA2U.mjs} +1 -1
  145. package/dist/{replyHelpers-DS-P0b32.mjs → replyHelpers-BSBVCwL3.mjs} +2 -2
  146. package/dist/repositoryFilter-BANpz6BX.mjs +78 -0
  147. package/dist/{requestContext-B6vQF-sy.mjs → requestContext-uYE35JxI.mjs} +1 -1
  148. package/dist/{rolldown-runtime-BhflRrBs.mjs → rolldown-runtime-DNg-KNCk.mjs} +1 -1
  149. package/dist/runtimeCapabilities-Dm88DaTW.mjs +49 -0
  150. package/dist/{schedules-C2Q3uv2d.mjs → schedules-CAcZgia0.mjs} +3 -3
  151. package/dist/{schemaConverter-Crjvczi_.mjs → schemaConverter-XH1RZRyu.mjs} +1 -1
  152. package/dist/{schemaIR-CnyKK_1j.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-afz_Ieek.mjs → scope-BaWcqBvr.mjs} +4 -4
  157. package/dist/{simpleEqualityMatcher-C4KiMWUa.mjs → simpleEqualityMatcher-Se8VrsaP.mjs} +1 -1
  158. package/dist/{sse-B9P4Y-UI.mjs → sse-Cf0jUHsh.mjs} +5 -5
  159. package/dist/{sseStream-B3zb_d1h.mjs → sseStream-Bvpi-UXQ.mjs} +2 -2
  160. package/dist/{store-helpers-DmEMHlPQ.mjs → store-helpers-B88p99Z2.mjs} +1 -1
  161. package/dist/{streaming-DPZdQmDz.mjs → streaming-DsT1B5zJ.mjs} +1 -1
  162. package/dist/{subscribe-helpers-sp5xTDVA.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 +3 -1
  166. package/dist/testing/index.d.mts +24 -12
  167. package/dist/testing/index.mjs +24 -12
  168. package/dist/testing/outboxStoreContract.mjs +1 -1
  169. package/dist/testing/storageContract.mjs +1 -1
  170. package/dist/{transactionContext-BvLKvOIL.mjs → transactionContext-BYtWnwAS.mjs} +1 -1
  171. package/dist/{typeGuards-B_5-hKjJ.mjs → typeGuards-jjKJDN13.mjs} +1 -1
  172. package/dist/types/index.d.mts +3 -3
  173. package/dist/types/index.mjs +1 -1
  174. package/dist/types/storage.mjs +1 -1
  175. package/dist/{types-CT5vu6M0.d.mts → types-BcTumAz_.d.mts} +41 -7
  176. package/dist/{types-CwcUQ610.d.mts → types-BfC83KsP.d.mts} +15 -1
  177. package/dist/{types-DXYX7dIR.mjs → types-CoqIRrkY.mjs} +4 -4
  178. package/dist/{types-C4QE8qnm.d.mts → types-DVLddu2q.d.mts} +144 -1
  179. package/dist/usage/index.d.mts +1 -1
  180. package/dist/usage/index.mjs +0 -0
  181. package/dist/{userHelpers-BNsOXzOd.mjs → userHelpers-CWF5pbZD.mjs} +1 -1
  182. package/dist/utils/index.d.mts +57 -5
  183. package/dist/utils/index.mjs +12 -11
  184. package/dist/{utils-BtOha_Sp.mjs → utils-h1V_sT-t.mjs} +38 -7
  185. package/dist/{validate-PMCPR9El.mjs → validate-CdQlz52F.mjs} +2 -2
  186. package/dist/{versioning-D_rhHfZm.d.mts → versioning-DEs-_hFi.d.mts} +1 -1
  187. package/dist/{versioning-CKv2jz_U.mjs → versioning-rZbyLVD-.mjs} +3 -3
  188. package/package.json +17 -16
  189. package/skills/arc/SKILL.md +96 -4
  190. package/skills/arc/references/api-reference.md +19 -0
  191. package/skills/arc/references/production.md +2 -2
  192. package/dist/projection-CjHhM5W1.mjs +0 -18
  193. package/dist/repositoryFilter-DotlxG3L.mjs +0 -45
@@ -1,5 +1,5 @@
1
1
 
2
- import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-2uVV34WU.mjs";
2
+ import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-BEVP50ka.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  //#region src/events/defineEvent.d.ts
5
5
  interface EventSchema {
@@ -227,6 +227,34 @@ declare function createDeadLetterPublisher(events: {
227
227
  interface EventPluginOptions {
228
228
  /** Event transport (default: MemoryEventTransport) */
229
229
  transport?: EventTransport;
230
+ /**
231
+ * Declare that this deployment is ONE process on purpose — the in-memory
232
+ * transport is its intended configuration, not a forgotten default.
233
+ *
234
+ * A single-node host (a small SaaS on one VPS, an on-prem install, a
235
+ * personal deployment that cannot justify Redis) is a legitimate production
236
+ * topology: with one process there are no other instances to broadcast to,
237
+ * so the memory transport is not a compromise — it is exactly sufficient.
238
+ * Without this flag arc cannot tell that apart from the accidental default
239
+ * (a multi-replica deployment that forgot to configure Redis), so it warns
240
+ * on every boot. Setting `singleProcess: true` states the topology and
241
+ * downgrades the warn to a factual info line.
242
+ *
243
+ * What it does NOT change: in-process delivery semantics. Events still
244
+ * vanish with the process — subscribers that were offline missed them. For
245
+ * durable at-least-once delivery on a single node, pair this with the
246
+ * repository-backed outbox (`createOutboxModule` + `repositoryAsOutboxStore`):
247
+ * events are committed to YOUR database and relayed to the in-process
248
+ * subscribers, so a crash between write and delivery replays on restart —
249
+ * no Redis anywhere in that path.
250
+ *
251
+ * Scale-out later by swapping the transport (Redis Streams etc.) and
252
+ * removing this flag; handler code is transport-independent and unchanged.
253
+ *
254
+ * Ignored (with a warn) when a non-memory transport is configured — the
255
+ * declaration would be describing a topology the transport contradicts.
256
+ */
257
+ singleProcess?: boolean;
230
258
  /** Enable event logging (default: false) */
231
259
  logEvents?: boolean;
232
260
  /**
@@ -1,9 +1,10 @@
1
- /*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
2
- import { t as __exportAll } from "./rolldown-runtime-BhflRrBs.mjs";
1
+ /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
+ import { t as __exportAll } from "./rolldown-runtime-DNg-KNCk.mjs";
3
3
  import { arcLog } from "./logger/index.mjs";
4
- import { d as createDomainError } from "./errors-Bh1KK5ps.mjs";
5
- import { n as requestContext } from "./requestContext-B6vQF-sy.mjs";
6
- import { n as createEvent, t as MemoryEventTransport } from "./EventTransport-BV10D0oG.mjs";
4
+ import { d as createDomainError } from "./errors-Cd_yy7aW.mjs";
5
+ import { n as requestContext } from "./requestContext-uYE35JxI.mjs";
6
+ import { n as MemoryEventTransport, r as createEvent, t as ARC_EVENT_TRANSPORT } from "./EventTransport-tq4CEB-q.mjs";
7
+ import { isProductionEnv } from "@classytic/primitives/environment";
7
8
  import fp from "fastify-plugin";
8
9
  //#region src/events/retry.ts
9
10
  /**
@@ -116,14 +117,39 @@ function sleep(ms) {
116
117
  }
117
118
  //#endregion
118
119
  //#region src/events/eventPlugin.ts
120
+ /**
121
+ * Event Plugin
122
+ *
123
+ * Integrates event transport with Fastify.
124
+ * Defaults to in-memory transport; configure durable transport for production.
125
+ *
126
+ * @example
127
+ * // Development (in-memory)
128
+ * await fastify.register(eventPlugin);
129
+ *
130
+ * // Production (Redis)
131
+ * await fastify.register(eventPlugin, {
132
+ * transport: new RedisEventTransport({ url: process.env.REDIS_URL }),
133
+ * });
134
+ */
119
135
  var eventPlugin_exports = /* @__PURE__ */ __exportAll({
120
136
  default: () => eventPlugin_default,
121
137
  eventPlugin: () => eventPlugin
122
138
  });
123
139
  const eventPlugin = async (fastify, opts = {}) => {
124
- const { transport = new MemoryEventTransport(), logEvents = false, failOpen = true, retry: retryOpts, deadLetterQueue: dlqOpts, wal, onPublish, onPublishError, registry, validateMode: rawValidateMode, warnOnDuplicate: rawWarnOnDuplicate } = opts;
140
+ /**
141
+ * Lifecycle ownership: arc closes ONLY the transport it built.
142
+ *
143
+ * A host-supplied transport (a Redis client wrapper, typically) may be
144
+ * shared with other apps in the process or outlive this one — closing it
145
+ * on this app's shutdown severs a connection arc does not own. Read
146
+ * BEFORE the destructure, because the default below is indistinguishable
147
+ * from a host value afterwards.
148
+ */
149
+ const ownsTransport = opts.transport === void 0;
150
+ const { transport = new MemoryEventTransport(), singleProcess = false, logEvents = false, failOpen = true, retry: retryOpts, deadLetterQueue: dlqOpts, wal, onPublish, onPublishError, registry, validateMode: rawValidateMode, warnOnDuplicate: rawWarnOnDuplicate } = opts;
125
151
  const validateMode = rawValidateMode ?? (registry ? "warn" : "off");
126
- const warnOnDuplicate = rawWarnOnDuplicate ?? process.env.NODE_ENV !== "production";
152
+ const warnOnDuplicate = rawWarnOnDuplicate ?? !isProductionEnv(process.env.NODE_ENV);
127
153
  const DUP_WINDOW_MS = 5e3;
128
154
  const recentPublishes = /* @__PURE__ */ new Map();
129
155
  const evictExpiredPublishes = (now) => {
@@ -220,7 +246,32 @@ const eventPlugin = async (fastify, opts = {}) => {
220
246
  transportName: transport.name,
221
247
  registry
222
248
  });
249
+ /**
250
+ * The RAW transport, for machinery that must observe publish failure.
251
+ *
252
+ * `fastify.events` is a REQUEST-FACING FACADE, and two of its properties make
253
+ * it wrong for a relay to publish through:
254
+ *
255
+ * 1. Different signature — `publish(type, payload, meta?)` vs the
256
+ * transport's `publish(event)`. Handing it a `DomainEvent` makes the
257
+ * envelope the `type` argument, which fails the non-empty-string guard.
258
+ * Measured on the default `createOutboxModule()` path: subscribers saw
259
+ * NOTHING and every relay tick reported `publishFailed: 1`, so events
260
+ * retried to dead-letter and never arrived.
261
+ * 2. Fail-open — the facade catches publish errors and only rethrows when
262
+ * `failOpen: false`. That is correct for HTTP (a transport outage must
263
+ * not fail a user's request) and fatal for an outbox: a swallowed error
264
+ * reads as success, the row is acknowledged, and the durability the
265
+ * outbox exists to provide is gone.
266
+ *
267
+ * So the boundary is explicit: **the facade may fail open; the relay must
268
+ * see the truth.** Decorated under a `Symbol.for` key — not part of the
269
+ * public `EventsDecorator` surface, and registry-keyed so it still resolves
270
+ * if a host's graph ends up with two arc copies.
271
+ */
272
+ fastify.decorate(ARC_EVENT_TRANSPORT, transport);
223
273
  fastify.addHook("onClose", async () => {
274
+ if (!ownsTransport) return;
224
275
  try {
225
276
  await transport.close?.();
226
277
  } catch (error) {
@@ -231,8 +282,13 @@ const eventPlugin = async (fastify, opts = {}) => {
231
282
  if (!failOpen) throw error;
232
283
  }
233
284
  });
234
- if (transport.name === "memory") fastify.log?.warn?.("[Arc Events] Using in-memory transport. Events will not persist or scale across instances. For production, configure a durable transport (Redis, RabbitMQ, etc.)");
235
- else fastify.log?.debug?.(`[Arc Events] Using ${transport.name} transport`);
285
+ if (transport.name === "memory") {
286
+ if (singleProcess) fastify.log?.info?.("[Arc Events] In-memory transport, declared single-process. Events are delivered in-process only and do not survive a crash or restart; subscribers in other processes (if any ever exist) will not receive them. For durability on this single node, pair with the repository-backed outbox (createOutboxModule + repositoryAsOutboxStore) — no Redis required. That covers a crash BEFORE publish; to also retry a THROWING handler, construct the transport with `onHandlerError: 'throw'` (by default a handler error is logged and the outbox row is still acknowledged).");
287
+ else fastify.log?.warn?.("[Arc Events] Using in-memory transport. Events will not persist or scale across instances. If this deployment is intentionally a single process, declare it — `eventPlugin, { singleProcess: true }` — and this becomes a supported configuration instead of a warning. Otherwise configure a durable transport (Redis Streams, RabbitMQ, etc.)");
288
+ } else {
289
+ if (singleProcess) fastify.log?.warn?.(`[Arc Events] \`singleProcess: true\` is declared but the '${transport.name}' transport is configured — a cross-instance transport contradicts a single-process declaration. The flag is ignored; remove it or switch back to the memory transport.`);
290
+ fastify.log?.debug?.(`[Arc Events] Using ${transport.name} transport`);
291
+ }
236
292
  };
237
293
  var eventPlugin_default = fp(eventPlugin, {
238
294
  name: "arc-events",
@@ -1,12 +1,10 @@
1
1
 
2
- import { a as EventTransport, i as EventLogger, n as DomainEvent, o as MemoryEventTransport, r as EventHandler, s as MemoryEventTransportOptions, t as DeadLetteredEvent } from "../EventTransport-2uVV34WU.mjs";
3
- import { t as ArcModule } from "../types-CT5vu6M0.mjs";
4
- import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-GF5nnBIq.mjs";
2
+ import { i as EventLogger, n as DomainEvent, o as MemoryEventTransport, r as EventHandler, s as MemoryEventTransportOptions } from "../EventTransport-BEVP50ka.mjs";
3
+ import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-DSNEZWfV.mjs";
4
+ import { a as EventOutbox, c as OutboxRelayErrorKind, i as MemoryOutboxStore, l as RelayResult, n as OutboxModuleOptions, o as EventOutboxOptions, r as createOutboxModule, s as OutboxRelayErrorHandler, t as OutboxModuleExports } from "../outbox-module-C1Bs_FXF.mjs";
5
5
  import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
6
- import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-qN6A-RFv.mjs";
7
- import { FastifyInstance } from "fastify";
8
- import { MemoryOutboxStore } from "@classytic/primitives/memory-outbox";
9
- import { OutboxFailurePolicy, OutboxStore, OutboxWriteOptions } from "@classytic/primitives/outbox";
6
+ import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-mwLWlfHG.mjs";
7
+ import { OutboxStore } from "@classytic/primitives/outbox";
10
8
  import { RepositoryLike } from "@classytic/repo-core/adapter";
11
9
  //#region src/events/eventTypes.d.ts
12
10
  /**
@@ -117,350 +115,6 @@ interface ExponentialBackoffOptions {
117
115
  */
118
116
  declare function exponentialBackoff(options: ExponentialBackoffOptions): Date;
119
117
  //#endregion
120
- //#region src/events/outbox/relay.d.ts
121
- /** Reason codes passed to {@link EventOutboxOptions.onError}. */
122
- type OutboxRelayErrorKind = "publish_failed" | "acknowledge_failed" | "fail_failed" | "ownership_mismatch" | "malformed_event";
123
- /**
124
- * Rich per-batch outcome returned by {@link EventOutbox.relayBatch}.
125
- *
126
- * Useful for operational dashboards, alerting thresholds, and test assertions.
127
- * The simpler {@link EventOutbox.relay} returns just the `relayed` count for
128
- * backward compatibility.
129
- */
130
- interface RelayResult {
131
- /** Number of events successfully published AND acknowledged */
132
- readonly relayed: number;
133
- /** Number of events claimed and attempted in this batch */
134
- readonly attempted: number;
135
- /** Number of publish failures (transport rejected the event) */
136
- readonly publishFailed: number;
137
- /** Number of acknowledge failures after successful publish (at-least-once replay risk) */
138
- readonly ackFailed: number;
139
- /** Number of ownership mismatches (our lease expired mid-flight) */
140
- readonly ownershipMismatches: number;
141
- /** Number of malformed events encountered (aborts the batch) */
142
- readonly malformed: number;
143
- /** Number of fail() calls that themselves threw (store bugs / contention) */
144
- readonly failHookErrors: number;
145
- /**
146
- * Number of events moved to dead-letter state this batch via the configured
147
- * {@link OutboxFailurePolicy}. Zero when no policy is set or no failure
148
- * tripped the `deadLetter` branch.
149
- */
150
- readonly deadLettered: number;
151
- /** Whether `publishMany` was used (true) or per-event `publish` (false) */
152
- readonly usedPublishMany: boolean;
153
- /**
154
- * Wall-clock ms spent claiming this batch from the store. With the
155
- * portable repository adapter this is ~2 DB round trips; a persistently
156
- * high value under low batch sizes points at store latency, not volume.
157
- */
158
- readonly claimMs?: number;
159
- /** Wall-clock ms spent publishing + acknowledging the batch. */
160
- readonly publishMs?: number;
161
- }
162
- /**
163
- * Called by {@link EventOutbox.relay} when a non-fatal error occurs during
164
- * a batch. Used for logging and metrics. Must not throw.
165
- */
166
- type OutboxRelayErrorHandler = (info: {
167
- readonly kind: OutboxRelayErrorKind;
168
- readonly event?: DomainEvent;
169
- readonly error: Error;
170
- }) => void;
171
- interface EventOutboxOptions {
172
- /**
173
- * Repository managing the outbox collection. Arc consumes it directly —
174
- * no wrapper classes. Requires `create`, `getOne`, `findAll`,
175
- * `deleteMany`, and `findOneAndUpdate` from `RepositoryLike` (mongokit
176
- * ≥3.8 satisfies all of these). Takes precedence over `store` when both
177
- * are passed.
178
- *
179
- * Use this for the common path where the outbox lives in your primary
180
- * database. Use `store` for non-repository backends (memory / custom).
181
- */
182
- readonly repository?: RepositoryLike;
183
- /**
184
- * Non-repository outbox store. Use when your backend isn't a repository
185
- * (memory for tests, Kafka, DynamoDB, custom). Ignored if `repository`
186
- * is also passed.
187
- */
188
- readonly store?: OutboxStore;
189
- /** Transport to relay events to (optional — can relay later) */
190
- readonly transport?: EventTransport;
191
- /** Max events per relay batch (default: 100) */
192
- readonly batchSize?: number;
193
- /**
194
- * Unique identifier for this relay worker. Used when the store supports
195
- * `claimPending`/`fail` to enforce lease ownership. Defaults to a random ID.
196
- */
197
- readonly consumerId?: string;
198
- /**
199
- * Lease duration in ms for claimed events. Only used when the store
200
- * supports `claimPending`. Default: 30 seconds.
201
- */
202
- readonly leaseMs?: number;
203
- /**
204
- * Callback for non-fatal errors during relay: publish failures,
205
- * ownership mismatches, ack/fail errors, malformed events. Use this for
206
- * logging and metrics. Must not throw — exceptions are swallowed.
207
- */
208
- readonly onError?: OutboxRelayErrorHandler;
209
- /**
210
- * Enable {@link EventTransport.publishMany} when the transport implements it.
211
- * Default: `true`. Set to `false` to force per-event `publish()` — useful
212
- * for transports where strict event-order observability matters more than
213
- * throughput, or to debug batch-specific issues.
214
- */
215
- readonly usePublishMany?: boolean;
216
- /**
217
- * Retry/DLQ decision policy. When set, {@link EventOutbox.relayBatch}
218
- * invokes this on every failure and uses the returned options for
219
- * `store.fail()`. Centralises the "after N fails, dead-letter" rule so
220
- * apps don't recompute `exponentialBackoff` + escalation thresholds on
221
- * every failure site.
222
- *
223
- * Without a policy, `fail()` is called with `{}` (immediate re-visibility
224
- * on next poll) — legacy behaviour, unchanged.
225
- */
226
- readonly failurePolicy?: OutboxFailurePolicy;
227
- /**
228
- * Unit-of-Work session provider (AsyncLocalStorage integration).
229
- *
230
- * When set, `store()` calls this to get the active DB session and uses
231
- * it as `OutboxWriteOptions.session` — **only when the caller has not
232
- * already passed an explicit `options.session`**. This eliminates the
233
- * boilerplate of threading the transaction handle from the controller
234
- * through every service layer down to the outbox call site.
235
- *
236
- * Wire up with arc's `transactionContext`:
237
- * ```typescript
238
- * import { transactionContext } from '@classytic/arc/context';
239
- *
240
- * const outbox = new EventOutbox({
241
- * store: myOutboxStore,
242
- * sessionProvider: () => transactionContext.get(),
243
- * });
244
- *
245
- * // In your service layer — session is auto-picked up:
246
- * await transactionContext.run(mongooseSession, async () => {
247
- * await orders.insertOne(order, { session: mongooseSession });
248
- * await outbox.store(orderCreatedEvent); // no session arg needed
249
- * });
250
- * ```
251
- */
252
- readonly sessionProvider?: () => unknown;
253
- /**
254
- * W3C Trace Context provider.
255
- *
256
- * When set, `store()` calls this and merges the returned headers into
257
- * `OutboxWriteOptions.headers` — only when `traceparent` is not already
258
- * present in the caller-supplied headers. Downstream relay workers can
259
- * then forward the headers to the transport, preserving the distributed
260
- * trace tree across service boundaries.
261
- *
262
- * Wire up with arc's `getTraceHeaders`:
263
- * ```typescript
264
- * import { getTraceHeaders } from '@classytic/arc/context';
265
- *
266
- * const outbox = new EventOutbox({
267
- * store: myOutboxStore,
268
- * traceContextProvider: getTraceHeaders,
269
- * });
270
- * ```
271
- */
272
- readonly traceContextProvider?: () => Record<string, string> | undefined;
273
- }
274
- declare class EventOutbox {
275
- private readonly _store;
276
- private readonly _transport?;
277
- private readonly _batchSize;
278
- private readonly _consumerId;
279
- private readonly _leaseMs;
280
- private readonly _onError?;
281
- private readonly _usePublishMany;
282
- private readonly _sessionProvider?;
283
- private readonly _traceContextProvider?;
284
- private readonly _failurePolicy?;
285
- /**
286
- * In-process attempt counter per event id. Accurate within this relay
287
- * process; resets on restart. Populated as failures occur and cleared on
288
- * successful ack or dead-letter transition; additionally hard-capped at
289
- * {@link MAX_TRACKED_ATTEMPTS} (oldest-write evicted first) so events
290
- * resolved by another relay worker can't accumulate forever. For durable
291
- * authoritative counts, apps can query the store directly inside
292
- * {@link OutboxFailurePolicy}.
293
- */
294
- private readonly _attempts;
295
- constructor(opts: EventOutboxOptions);
296
- /** Unique consumer ID used for lease ownership when the store supports claims */
297
- get consumerId(): string;
298
- /**
299
- * Store event in outbox.
300
- *
301
- * Validates that `event.type` and `event.meta.id` are present — throws
302
- * {@link InvalidOutboxEventError} otherwise, so corrupt rows can never
303
- * be persisted via this API.
304
- *
305
- * Pass `options.session` to participate in a host-managed DB transaction
306
- * (store must support session-aware writes). Other options (`visibleAt`,
307
- * `dedupeKey`, `partitionKey`, `headers`) are forwarded to stores that
308
- * implement them and ignored otherwise.
309
- */
310
- store(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
311
- private _reportError;
312
- /**
313
- * Relay pending events to transport and return the number of successful
314
- * publish+acknowledge pairs.
315
- *
316
- * For richer observability (per-kind counts, publishMany detection, etc.)
317
- * use {@link relayBatch} which returns a {@link RelayResult}. This method
318
- * is the backward-compatible shortcut that returns just the count.
319
- *
320
- * @returns Number of successfully published AND acknowledged events
321
- */
322
- relay(): Promise<number>;
323
- /**
324
- * Relay a batch of pending events to the transport and return a rich
325
- * {@link RelayResult} describing the outcome of each event.
326
- *
327
- * Behavior summary:
328
- *
329
- * - **Claim path**: uses {@link OutboxStore.claimPending} when the store
330
- * supports it (safe for multi-worker relay) or falls back to
331
- * {@link OutboxStore.getPending} (single-worker only).
332
- *
333
- * - **Publish path**: if the transport implements
334
- * {@link EventTransport.publishMany} and `usePublishMany` is not disabled,
335
- * the entire batch is sent in one call. Otherwise each event is published
336
- * individually. Either way, per-event outcomes are tracked.
337
- *
338
- * - **Failure path**: if the store implements `fail`, per-event failures
339
- * are reported via `store.fail(...)` and the batch continues. Without
340
- * `fail`, the batch stops on the first failure (legacy behavior).
341
- *
342
- * - **Malformed events**: events missing `type` or `meta.id` abort the
343
- * batch — a well-behaved store must never return them (see
344
- * {@link OutboxStore} semantics #6). The error is reported via `onError`.
345
- *
346
- * - **Ownership mismatches**: if `acknowledge`/`fail` throws
347
- * {@link OutboxOwnershipError} (our lease expired and another worker
348
- * claimed the event), the event is NOT counted as relayed. The other
349
- * worker will re-publish — at-least-once semantics preserved.
350
- *
351
- * @returns Per-kind outcome counts for the batch
352
- */
353
- relayBatch(): Promise<RelayResult>;
354
- /**
355
- * Fetch current dead-lettered events as typed {@link DeadLetteredEvent}
356
- * envelopes. Delegates to {@link OutboxStore.getDeadLettered} — returns
357
- * `[]` when the store doesn't implement it.
358
- *
359
- * Pairs with {@link OutboxFailurePolicy}: apps set a policy that routes to
360
- * `deadLetter: true` after N attempts, then read back with this to alert,
361
- * replay, or archive.
362
- */
363
- getDeadLettered(limit?: number): Promise<DeadLetteredEvent[]>;
364
- /**
365
- * Purge old **delivered** events from the outbox store.
366
- * Delegates to `store.purge()` if implemented; no-op otherwise.
367
- * @param olderThanMs - Remove events delivered more than this many ms ago (default: 7 days)
368
- * @returns Number of purged events, or 0 if store doesn't support purge
369
- */
370
- purge(olderThanMs?: number): Promise<number>;
371
- }
372
- //#endregion
373
- //#region src/events/outbox-module.d.ts
374
- interface OutboxModuleOptions {
375
- /**
376
- * The durable store. Supply it directly, or as a factory when it needs the booted app
377
- * (a mongoose connection decorated by an earlier plugin, for instance).
378
- */
379
- store: OutboxStore | ((fastify: FastifyInstance) => OutboxStore);
380
- /**
381
- * Where relayed events are published. Omit to use `fastify.events` — the ordinary case; pass
382
- * one only when the outbox must publish somewhere other than the app's own transport.
383
- */
384
- transport?: EventTransport | ((fastify: FastifyInstance) => EventTransport);
385
- /** Module name, and therefore the `dependsOn` key. Default `'outbox'`. */
386
- name?: string;
387
- /** Relay interval in ms. Default 5000. */
388
- relayEveryMs?: number;
389
- /**
390
- * Rows per relay tick. Left to the store's own default when omitted — a batch size is a
391
- * throughput tuning decision, not something this module should invent.
392
- */
393
- batchSize?: number;
394
- /** Lease duration for a claimed batch, in ms. */
395
- leaseMs?: number;
396
- /**
397
- * Consumer id recorded on the lease. Default `'arc-outbox-relay'`.
398
- *
399
- * Give each relaying process a DISTINCT id when several run: the lease is what stops two
400
- * relays publishing the same event, and it can only do that if they are distinguishable.
401
- */
402
- consumerId?: string;
403
- /**
404
- * Called for every non-fatal relay problem — publish failures, ownership mismatches,
405
- * acknowledge failures.
406
- *
407
- * **Wire this.** Silence here is the failure mode that matters: the relay keeps ticking, rows
408
- * keep failing, and the only symptom is revenue that never posted.
409
- */
410
- onError?: OutboxRelayErrorHandler;
411
- /**
412
- * Retry / dead-letter policy for a failed publish — `({ attempts }) => { retryAt } |
413
- * { deadLetter: true }`.
414
- *
415
- * Omitting it takes `EventOutbox`'s default. Exposed because a real host had one and losing it
416
- * on adoption would be a silent downgrade: without a policy a permanently-failing row either
417
- * retries forever or dead-letters on the first blip, and both are wrong for money. A typical
418
- * shape is exponential backoff up to N attempts, then dead-letter.
419
- */
420
- failurePolicy?: OutboxFailurePolicy;
421
- /**
422
- * Run one relay pass at boot. Default `true` — a process that restarts with a backlog should
423
- * drain it immediately rather than waiting a full interval, because that backlog is precisely
424
- * the events the previous process failed to publish.
425
- */
426
- runOnStart?: boolean;
427
- }
428
- /** What the module exports at `fastify.arc.modules.<name>`. */
429
- interface OutboxModuleExports {
430
- /** The store, for consumers that write events transactionally. */
431
- store: OutboxStore;
432
- /** The relay, for a host that wants to drain on demand (tests, admin actions). */
433
- relay: EventOutbox;
434
- }
435
- /**
436
- * Compose the outbox as a module.
437
- *
438
- * ```ts
439
- * // host
440
- * const model = createOutboxModel(connection, { visibleAtField: 'nextVisibleAt' });
441
- * createApp({ modules: [
442
- * createOutboxModule({
443
- * store: repositoryAsOutboxStore(new Repository(model), { visibleAtField: 'nextVisibleAt' }),
444
- * onError: (err, ctx) => app.log.error({ err, ...ctx }, 'outbox relay'),
445
- * }),
446
- * …
447
- * ]});
448
- *
449
- * // consumer module
450
- * defineModule({
451
- * name: 'order',
452
- * dependsOn: ['outbox'],
453
- * bootstrap: (f) => createOrder({
454
- * outbox: getModuleExports<OutboxModuleExports>(f, 'outbox').store,
455
- * }),
456
- * });
457
- * ```
458
- *
459
- * `dependsOn` is doing real work there: it both orders the bootstraps and makes a missing outbox a
460
- * boot error naming the module that needed it.
461
- */
462
- declare function createOutboxModule(options: OutboxModuleOptions): ArcModule<OutboxModuleExports>;
463
- //#endregion
464
118
  //#region src/events/repository-outbox-adapter.d.ts
465
119
  interface RepositoryOutboxStoreOptions {
466
120
  /**