@classytic/arc 1.1.0 → 2.1.2

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 (322) hide show
  1. package/README.md +247 -794
  2. package/bin/arc.js +91 -52
  3. package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
  4. package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
  5. package/dist/HookSystem-BsGV-j2l.mjs +405 -0
  6. package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
  7. package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
  8. package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
  9. package/dist/adapters/index.d.mts +5 -0
  10. package/dist/adapters/index.mjs +3 -0
  11. package/dist/audit/index.d.mts +82 -0
  12. package/dist/audit/index.d.mts.map +1 -0
  13. package/dist/audit/index.mjs +276 -0
  14. package/dist/audit/index.mjs.map +1 -0
  15. package/dist/audit/mongodb.d.mts +5 -0
  16. package/dist/audit/mongodb.mjs +3 -0
  17. package/dist/audited-C3T5DTUx.mjs +141 -0
  18. package/dist/audited-C3T5DTUx.mjs.map +1 -0
  19. package/dist/auth/index.d.mts +189 -0
  20. package/dist/auth/index.d.mts.map +1 -0
  21. package/dist/auth/index.mjs +1102 -0
  22. package/dist/auth/index.mjs.map +1 -0
  23. package/dist/auth/redis-session.d.mts +44 -0
  24. package/dist/auth/redis-session.d.mts.map +1 -0
  25. package/dist/auth/redis-session.mjs +76 -0
  26. package/dist/auth/redis-session.mjs.map +1 -0
  27. package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
  28. package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
  29. package/dist/cache/index.d.mts +146 -0
  30. package/dist/cache/index.d.mts.map +1 -0
  31. package/dist/cache/index.mjs +92 -0
  32. package/dist/cache/index.mjs.map +1 -0
  33. package/dist/caching-Bl28lYsR.mjs +94 -0
  34. package/dist/caching-Bl28lYsR.mjs.map +1 -0
  35. package/dist/chunk-C7Uep-_p.mjs +20 -0
  36. package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
  37. package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
  38. package/dist/cli/commands/describe.d.mts +19 -0
  39. package/dist/cli/commands/describe.d.mts.map +1 -0
  40. package/dist/cli/commands/describe.mjs +239 -0
  41. package/dist/cli/commands/describe.mjs.map +1 -0
  42. package/dist/cli/commands/docs.d.mts +14 -0
  43. package/dist/cli/commands/docs.d.mts.map +1 -0
  44. package/dist/cli/commands/docs.mjs +53 -0
  45. package/dist/cli/commands/docs.mjs.map +1 -0
  46. package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
  47. package/dist/cli/commands/generate.d.mts.map +1 -0
  48. package/dist/cli/commands/generate.mjs +358 -0
  49. package/dist/cli/commands/generate.mjs.map +1 -0
  50. package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
  51. package/dist/cli/commands/init.d.mts.map +1 -0
  52. package/dist/cli/commands/{init.js → init.mjs} +807 -616
  53. package/dist/cli/commands/init.mjs.map +1 -0
  54. package/dist/cli/commands/introspect.d.mts +11 -0
  55. package/dist/cli/commands/introspect.d.mts.map +1 -0
  56. package/dist/cli/commands/introspect.mjs +76 -0
  57. package/dist/cli/commands/introspect.mjs.map +1 -0
  58. package/dist/cli/index.d.mts +17 -0
  59. package/dist/cli/index.d.mts.map +1 -0
  60. package/dist/cli/index.mjs +157 -0
  61. package/dist/cli/index.mjs.map +1 -0
  62. package/dist/constants-DdXFXQtN.mjs +85 -0
  63. package/dist/constants-DdXFXQtN.mjs.map +1 -0
  64. package/dist/core/index.d.mts +5 -0
  65. package/dist/core/index.mjs +4 -0
  66. package/dist/createApp-CUgNqegw.mjs +560 -0
  67. package/dist/createApp-CUgNqegw.mjs.map +1 -0
  68. package/dist/defineResource-k0_BDn8v.mjs +2197 -0
  69. package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
  70. package/dist/discovery/index.d.mts +47 -0
  71. package/dist/discovery/index.d.mts.map +1 -0
  72. package/dist/discovery/index.mjs +110 -0
  73. package/dist/discovery/index.mjs.map +1 -0
  74. package/dist/docs/index.d.mts +163 -0
  75. package/dist/docs/index.d.mts.map +1 -0
  76. package/dist/docs/index.mjs +73 -0
  77. package/dist/docs/index.mjs.map +1 -0
  78. package/dist/elevation-BRy3yFWT.mjs +113 -0
  79. package/dist/elevation-BRy3yFWT.mjs.map +1 -0
  80. package/dist/elevation-B_2dRLVP.d.mts +88 -0
  81. package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
  82. package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
  83. package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
  84. package/dist/errorHandler-C1okiriz.mjs +109 -0
  85. package/dist/errorHandler-C1okiriz.mjs.map +1 -0
  86. package/dist/errors-B9bZok84.mjs +212 -0
  87. package/dist/errors-B9bZok84.mjs.map +1 -0
  88. package/dist/errors-ChKiFz62.d.mts +125 -0
  89. package/dist/errors-ChKiFz62.d.mts.map +1 -0
  90. package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
  91. package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
  92. package/dist/eventPlugin-DGR_B2on.mjs +230 -0
  93. package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
  94. package/dist/events/index.d.mts +54 -0
  95. package/dist/events/index.d.mts.map +1 -0
  96. package/dist/events/index.mjs +52 -0
  97. package/dist/events/index.mjs.map +1 -0
  98. package/dist/events/transports/redis-stream-entry.d.mts +2 -0
  99. package/dist/events/transports/redis-stream-entry.mjs +178 -0
  100. package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
  101. package/dist/events/transports/redis.d.mts +77 -0
  102. package/dist/events/transports/redis.d.mts.map +1 -0
  103. package/dist/events/transports/redis.mjs +125 -0
  104. package/dist/events/transports/redis.mjs.map +1 -0
  105. package/dist/externalPaths-DlINfKbP.d.mts +51 -0
  106. package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
  107. package/dist/factory/index.d.mts +64 -0
  108. package/dist/factory/index.d.mts.map +1 -0
  109. package/dist/factory/index.mjs +3 -0
  110. package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
  111. package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
  112. package/dist/fields-DyaDVX4J.d.mts +110 -0
  113. package/dist/fields-DyaDVX4J.d.mts.map +1 -0
  114. package/dist/fields-iagOozy0.mjs +115 -0
  115. package/dist/fields-iagOozy0.mjs.map +1 -0
  116. package/dist/hooks/index.d.mts +4 -0
  117. package/dist/hooks/index.mjs +3 -0
  118. package/dist/idempotency/index.d.mts +97 -0
  119. package/dist/idempotency/index.d.mts.map +1 -0
  120. package/dist/idempotency/index.mjs +320 -0
  121. package/dist/idempotency/index.mjs.map +1 -0
  122. package/dist/idempotency/mongodb.d.mts +2 -0
  123. package/dist/idempotency/mongodb.mjs +115 -0
  124. package/dist/idempotency/mongodb.mjs.map +1 -0
  125. package/dist/idempotency/redis.d.mts +2 -0
  126. package/dist/idempotency/redis.mjs +104 -0
  127. package/dist/idempotency/redis.mjs.map +1 -0
  128. package/dist/index.d.mts +261 -0
  129. package/dist/index.d.mts.map +1 -0
  130. package/dist/index.mjs +105 -0
  131. package/dist/index.mjs.map +1 -0
  132. package/dist/integrations/event-gateway.d.mts +47 -0
  133. package/dist/integrations/event-gateway.d.mts.map +1 -0
  134. package/dist/integrations/event-gateway.mjs +44 -0
  135. package/dist/integrations/event-gateway.mjs.map +1 -0
  136. package/dist/integrations/index.d.mts +5 -0
  137. package/dist/integrations/index.mjs +1 -0
  138. package/dist/integrations/jobs.d.mts +104 -0
  139. package/dist/integrations/jobs.d.mts.map +1 -0
  140. package/dist/integrations/jobs.mjs +124 -0
  141. package/dist/integrations/jobs.mjs.map +1 -0
  142. package/dist/integrations/streamline.d.mts +61 -0
  143. package/dist/integrations/streamline.d.mts.map +1 -0
  144. package/dist/integrations/streamline.mjs +126 -0
  145. package/dist/integrations/streamline.mjs.map +1 -0
  146. package/dist/integrations/websocket.d.mts +83 -0
  147. package/dist/integrations/websocket.d.mts.map +1 -0
  148. package/dist/integrations/websocket.mjs +289 -0
  149. package/dist/integrations/websocket.mjs.map +1 -0
  150. package/dist/interface-B01JvPVc.d.mts +78 -0
  151. package/dist/interface-B01JvPVc.d.mts.map +1 -0
  152. package/dist/interface-CZe8IkMf.d.mts +55 -0
  153. package/dist/interface-CZe8IkMf.d.mts.map +1 -0
  154. package/dist/interface-Ch8HU9uM.d.mts +1098 -0
  155. package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
  156. package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
  157. package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
  158. package/dist/keys-BqNejWup.mjs +43 -0
  159. package/dist/keys-BqNejWup.mjs.map +1 -0
  160. package/dist/logger-Df2O2WsW.mjs +79 -0
  161. package/dist/logger-Df2O2WsW.mjs.map +1 -0
  162. package/dist/memory-cQgelFOj.mjs +144 -0
  163. package/dist/memory-cQgelFOj.mjs.map +1 -0
  164. package/dist/migrations/index.d.mts +157 -0
  165. package/dist/migrations/index.d.mts.map +1 -0
  166. package/dist/migrations/index.mjs +261 -0
  167. package/dist/migrations/index.mjs.map +1 -0
  168. package/dist/mongodb-BfJVlUJH.mjs +94 -0
  169. package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
  170. package/dist/mongodb-CGzRbfAK.d.mts +119 -0
  171. package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
  172. package/dist/mongodb-JN-9JA7K.d.mts +72 -0
  173. package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
  174. package/dist/openapi-G3Cw7XuM.mjs +524 -0
  175. package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
  176. package/dist/org/index.d.mts +69 -0
  177. package/dist/org/index.d.mts.map +1 -0
  178. package/dist/org/index.mjs +514 -0
  179. package/dist/org/index.mjs.map +1 -0
  180. package/dist/org/types.d.mts +83 -0
  181. package/dist/org/types.d.mts.map +1 -0
  182. package/dist/org/types.mjs +1 -0
  183. package/dist/permissions/index.d.mts +279 -0
  184. package/dist/permissions/index.d.mts.map +1 -0
  185. package/dist/permissions/index.mjs +579 -0
  186. package/dist/permissions/index.mjs.map +1 -0
  187. package/dist/plugins/index.d.mts +173 -0
  188. package/dist/plugins/index.d.mts.map +1 -0
  189. package/dist/plugins/index.mjs +523 -0
  190. package/dist/plugins/index.mjs.map +1 -0
  191. package/dist/plugins/response-cache.d.mts +88 -0
  192. package/dist/plugins/response-cache.d.mts.map +1 -0
  193. package/dist/plugins/response-cache.mjs +284 -0
  194. package/dist/plugins/response-cache.mjs.map +1 -0
  195. package/dist/plugins/tracing-entry.d.mts +2 -0
  196. package/dist/plugins/tracing-entry.mjs +186 -0
  197. package/dist/plugins/tracing-entry.mjs.map +1 -0
  198. package/dist/pluralize-CEweyOEm.mjs +87 -0
  199. package/dist/pluralize-CEweyOEm.mjs.map +1 -0
  200. package/dist/policies/{index.d.ts → index.d.mts} +204 -169
  201. package/dist/policies/index.d.mts.map +1 -0
  202. package/dist/policies/index.mjs +322 -0
  203. package/dist/policies/index.mjs.map +1 -0
  204. package/dist/presets/{index.d.ts → index.d.mts} +63 -131
  205. package/dist/presets/index.d.mts.map +1 -0
  206. package/dist/presets/index.mjs +144 -0
  207. package/dist/presets/index.mjs.map +1 -0
  208. package/dist/presets/multiTenant.d.mts +25 -0
  209. package/dist/presets/multiTenant.d.mts.map +1 -0
  210. package/dist/presets/multiTenant.mjs +114 -0
  211. package/dist/presets/multiTenant.mjs.map +1 -0
  212. package/dist/presets-BITljm96.mjs +120 -0
  213. package/dist/presets-BITljm96.mjs.map +1 -0
  214. package/dist/presets-DzSMwlKj.d.mts +58 -0
  215. package/dist/presets-DzSMwlKj.d.mts.map +1 -0
  216. package/dist/prisma-DJbMt3yf.mjs +628 -0
  217. package/dist/prisma-DJbMt3yf.mjs.map +1 -0
  218. package/dist/prisma-Dg9GoVdj.d.mts +275 -0
  219. package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
  220. package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
  221. package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
  222. package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
  223. package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
  224. package/dist/redis-D-JAeLtm.d.mts +50 -0
  225. package/dist/redis-D-JAeLtm.d.mts.map +1 -0
  226. package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
  227. package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
  228. package/dist/registry/index.d.mts +12 -0
  229. package/dist/registry/index.d.mts.map +1 -0
  230. package/dist/registry/index.mjs +4 -0
  231. package/dist/requestContext-QQD6ROJc.mjs +56 -0
  232. package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
  233. package/dist/schemaConverter-BwrmWroW.mjs +99 -0
  234. package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
  235. package/dist/schemas/index.d.mts +64 -0
  236. package/dist/schemas/index.d.mts.map +1 -0
  237. package/dist/schemas/index.mjs +83 -0
  238. package/dist/schemas/index.mjs.map +1 -0
  239. package/dist/scope/index.d.mts +22 -0
  240. package/dist/scope/index.d.mts.map +1 -0
  241. package/dist/scope/index.mjs +66 -0
  242. package/dist/scope/index.mjs.map +1 -0
  243. package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
  244. package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
  245. package/dist/sse-B3c3_yZp.mjs +124 -0
  246. package/dist/sse-B3c3_yZp.mjs.map +1 -0
  247. package/dist/testing/index.d.mts +908 -0
  248. package/dist/testing/index.d.mts.map +1 -0
  249. package/dist/testing/index.mjs +1977 -0
  250. package/dist/testing/index.mjs.map +1 -0
  251. package/dist/tracing-Cc7vVQPp.d.mts +71 -0
  252. package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
  253. package/dist/typeGuards-DhMNLuvU.mjs +10 -0
  254. package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
  255. package/dist/types/index.d.mts +947 -0
  256. package/dist/types/index.d.mts.map +1 -0
  257. package/dist/types/index.mjs +15 -0
  258. package/dist/types/index.mjs.map +1 -0
  259. package/dist/types-Beqn1Un7.mjs +39 -0
  260. package/dist/types-Beqn1Un7.mjs.map +1 -0
  261. package/dist/types-CIgB7UUl.d.mts +446 -0
  262. package/dist/types-CIgB7UUl.d.mts.map +1 -0
  263. package/dist/types-aYB4V7uN.d.mts +87 -0
  264. package/dist/types-aYB4V7uN.d.mts.map +1 -0
  265. package/dist/utils/index.d.mts +748 -0
  266. package/dist/utils/index.d.mts.map +1 -0
  267. package/dist/utils/index.mjs +6 -0
  268. package/package.json +194 -68
  269. package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
  270. package/dist/adapters/index.d.ts +0 -237
  271. package/dist/adapters/index.js +0 -668
  272. package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
  273. package/dist/audit/index.d.ts +0 -195
  274. package/dist/audit/index.js +0 -319
  275. package/dist/auth/index.d.ts +0 -47
  276. package/dist/auth/index.js +0 -174
  277. package/dist/cli/commands/docs.d.ts +0 -11
  278. package/dist/cli/commands/docs.js +0 -474
  279. package/dist/cli/commands/generate.js +0 -334
  280. package/dist/cli/commands/introspect.d.ts +0 -8
  281. package/dist/cli/commands/introspect.js +0 -338
  282. package/dist/cli/index.d.ts +0 -4
  283. package/dist/cli/index.js +0 -3269
  284. package/dist/core/index.d.ts +0 -220
  285. package/dist/core/index.js +0 -2786
  286. package/dist/createApp-Ce9wl8W9.d.ts +0 -77
  287. package/dist/docs/index.d.ts +0 -166
  288. package/dist/docs/index.js +0 -658
  289. package/dist/errors-8WIxGS_6.d.ts +0 -122
  290. package/dist/events/index.d.ts +0 -117
  291. package/dist/events/index.js +0 -89
  292. package/dist/factory/index.d.ts +0 -38
  293. package/dist/factory/index.js +0 -1652
  294. package/dist/hooks/index.d.ts +0 -4
  295. package/dist/hooks/index.js +0 -199
  296. package/dist/idempotency/index.d.ts +0 -323
  297. package/dist/idempotency/index.js +0 -500
  298. package/dist/index-B4t03KQ0.d.ts +0 -1366
  299. package/dist/index.d.ts +0 -135
  300. package/dist/index.js +0 -4756
  301. package/dist/migrations/index.d.ts +0 -185
  302. package/dist/migrations/index.js +0 -274
  303. package/dist/org/index.d.ts +0 -129
  304. package/dist/org/index.js +0 -220
  305. package/dist/permissions/index.d.ts +0 -144
  306. package/dist/permissions/index.js +0 -103
  307. package/dist/plugins/index.d.ts +0 -46
  308. package/dist/plugins/index.js +0 -1069
  309. package/dist/policies/index.js +0 -196
  310. package/dist/presets/index.js +0 -384
  311. package/dist/presets/multiTenant.d.ts +0 -39
  312. package/dist/presets/multiTenant.js +0 -112
  313. package/dist/registry/index.d.ts +0 -16
  314. package/dist/registry/index.js +0 -253
  315. package/dist/testing/index.d.ts +0 -618
  316. package/dist/testing/index.js +0 -48020
  317. package/dist/types/index.d.ts +0 -4
  318. package/dist/types/index.js +0 -8
  319. package/dist/types-B99TBmFV.d.ts +0 -76
  320. package/dist/types-BvckRbs2.d.ts +0 -143
  321. package/dist/utils/index.d.ts +0 -679
  322. package/dist/utils/index.js +0 -931
@@ -0,0 +1,230 @@
1
+ import { t as __exportAll } from "./chunk-C7Uep-_p.mjs";
2
+ import { t as requestContext } from "./requestContext-QQD6ROJc.mjs";
3
+ import fp from "fastify-plugin";
4
+
5
+ //#region src/events/EventTransport.ts
6
+ /**
7
+ * In-memory event transport (default)
8
+ * Events are delivered synchronously within the process.
9
+ * Not suitable for multi-instance deployments.
10
+ */
11
+ var MemoryEventTransport = class {
12
+ name = "memory";
13
+ handlers = /* @__PURE__ */ new Map();
14
+ logger;
15
+ constructor(options) {
16
+ this.logger = options?.logger ?? console;
17
+ }
18
+ async publish(event) {
19
+ const exactHandlers = this.handlers.get(event.type) ?? /* @__PURE__ */ new Set();
20
+ const wildcardHandlers = this.handlers.get("*") ?? /* @__PURE__ */ new Set();
21
+ const patternHandlers = /* @__PURE__ */ new Set();
22
+ for (const [pattern, handlers] of this.handlers.entries()) if (pattern.endsWith(".*")) {
23
+ const prefix = pattern.slice(0, -2);
24
+ if (event.type.startsWith(prefix + ".")) handlers.forEach((h) => patternHandlers.add(h));
25
+ }
26
+ const allHandlers = new Set([
27
+ ...exactHandlers,
28
+ ...wildcardHandlers,
29
+ ...patternHandlers
30
+ ]);
31
+ for (const handler of allHandlers) try {
32
+ await handler(event);
33
+ } catch (err) {
34
+ this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
35
+ }
36
+ }
37
+ async subscribe(pattern, handler) {
38
+ if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
39
+ this.handlers.get(pattern).add(handler);
40
+ return () => {
41
+ this.handlers.get(pattern)?.delete(handler);
42
+ };
43
+ }
44
+ async close() {
45
+ this.handlers.clear();
46
+ }
47
+ };
48
+ /**
49
+ * Create a domain event with auto-generated metadata
50
+ */
51
+ function createEvent(type, payload, meta) {
52
+ return {
53
+ type,
54
+ payload,
55
+ meta: {
56
+ id: crypto.randomUUID(),
57
+ timestamp: /* @__PURE__ */ new Date(),
58
+ ...meta
59
+ }
60
+ };
61
+ }
62
+
63
+ //#endregion
64
+ //#region src/events/retry.ts
65
+ /**
66
+ * Wrap an event handler with retry logic and dead letter support.
67
+ *
68
+ * On failure, retries with exponential backoff (with jitter).
69
+ * After all retries exhausted, calls `onDead` callback if provided.
70
+ */
71
+ function withRetry(handler, options = {}) {
72
+ const { maxRetries = 3, backoffMs = 1e3, maxBackoffMs = 3e4, jitter = .1, onDead, name, logger = console } = options;
73
+ const label = name ?? handler.name ?? "anonymous";
74
+ return async (event) => {
75
+ const errors = [];
76
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
77
+ await handler(event);
78
+ return;
79
+ } catch (err) {
80
+ const error = err instanceof Error ? err : new Error(String(err));
81
+ errors.push(error);
82
+ if (attempt < maxRetries) {
83
+ const baseDelay = Math.min(backoffMs * 2 ** attempt, maxBackoffMs);
84
+ const delay = baseDelay + jitter * baseDelay * Math.random();
85
+ logger.warn(`[Arc Events] Handler '${label}' failed for ${event.type} (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${Math.round(delay)}ms: ${error.message}`);
86
+ await sleep(delay);
87
+ }
88
+ }
89
+ logger.error(`[Arc Events] Handler '${label}' permanently failed for ${event.type} after ${maxRetries + 1} attempts. ${errors.length} errors.`);
90
+ if (onDead) try {
91
+ await onDead(event, errors);
92
+ } catch (dlqErr) {
93
+ logger.error("[Arc Events] Dead letter callback failed:", dlqErr);
94
+ }
95
+ };
96
+ }
97
+ /**
98
+ * Create a dead letter publisher that sends failed events to a `$deadLetter` channel.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * import { withRetry, createDeadLetterPublisher } from '@classytic/arc/events';
103
+ *
104
+ * const toDlq = createDeadLetterPublisher(fastify.events);
105
+ *
106
+ * await fastify.events.subscribe('order.created', withRetry(handler, {
107
+ * maxRetries: 3,
108
+ * onDead: toDlq,
109
+ * }));
110
+ *
111
+ * // Monitor dead letters
112
+ * await fastify.events.subscribe('$deadLetter', async (event) => {
113
+ * console.error('Dead letter:', event.payload);
114
+ * await alertOps(event.payload);
115
+ * });
116
+ * ```
117
+ */
118
+ function createDeadLetterPublisher(events) {
119
+ return async (event, errors) => {
120
+ await events.publish("$deadLetter", {
121
+ originalEvent: event,
122
+ errors: errors.map((e) => ({
123
+ message: e.message,
124
+ stack: e.stack
125
+ })),
126
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
127
+ });
128
+ };
129
+ }
130
+ function sleep(ms) {
131
+ return new Promise((resolve) => setTimeout(resolve, ms));
132
+ }
133
+
134
+ //#endregion
135
+ //#region src/events/eventPlugin.ts
136
+ /**
137
+ * Event Plugin
138
+ *
139
+ * Integrates event transport with Fastify.
140
+ * Defaults to in-memory transport; configure durable transport for production.
141
+ *
142
+ * @example
143
+ * // Development (in-memory)
144
+ * await fastify.register(eventPlugin);
145
+ *
146
+ * // Production (Redis)
147
+ * await fastify.register(eventPlugin, {
148
+ * transport: new RedisEventTransport({ url: process.env.REDIS_URL }),
149
+ * });
150
+ */
151
+ var eventPlugin_exports = /* @__PURE__ */ __exportAll({
152
+ default: () => eventPlugin_default,
153
+ eventPlugin: () => eventPlugin
154
+ });
155
+ const eventPlugin = async (fastify, opts = {}) => {
156
+ const { transport = new MemoryEventTransport(), logEvents = false, failOpen = true, retry: retryOpts, deadLetterQueue: dlqOpts, wal, onPublish, onPublishError } = opts;
157
+ fastify.decorate("events", {
158
+ publish: async (type, payload, meta) => {
159
+ const store = requestContext.get();
160
+ const event = createEvent(type, payload, {
161
+ ...store?.requestId && !meta?.correlationId ? { correlationId: store.requestId } : {},
162
+ ...meta
163
+ });
164
+ if (logEvents) fastify.log?.info?.({
165
+ eventType: type,
166
+ eventId: event.meta.id,
167
+ correlationId: event.meta.correlationId
168
+ }, "Publishing event");
169
+ try {
170
+ if (wal) await wal.save(event);
171
+ await transport.publish(event);
172
+ if (wal?.acknowledge) await wal.acknowledge(event.meta.id);
173
+ onPublish?.(event);
174
+ } catch (error) {
175
+ fastify.log?.error?.({
176
+ transport: transport.name,
177
+ eventType: type,
178
+ error
179
+ }, "[Arc Events] Failed to publish event");
180
+ onPublishError?.(event, error);
181
+ if (!failOpen) throw error;
182
+ }
183
+ },
184
+ subscribe: async (pattern, handler) => {
185
+ let wrappedHandler = handler;
186
+ if (retryOpts && pattern !== "$deadLetter") wrappedHandler = withRetry(handler, {
187
+ ...retryOpts,
188
+ onDead: dlqOpts?.store ?? createDeadLetterPublisher(fastify.events),
189
+ logger: fastify.log
190
+ });
191
+ if (logEvents) fastify.log?.info?.({
192
+ pattern,
193
+ retry: !!retryOpts
194
+ }, "Subscribing to events");
195
+ try {
196
+ return await transport.subscribe(pattern, wrappedHandler);
197
+ } catch (error) {
198
+ fastify.log?.error?.({
199
+ transport: transport.name,
200
+ pattern,
201
+ error
202
+ }, "[Arc Events] Failed to subscribe to events");
203
+ if (!failOpen) throw error;
204
+ return () => {};
205
+ }
206
+ },
207
+ transportName: transport.name
208
+ });
209
+ fastify.addHook("onClose", async () => {
210
+ try {
211
+ await transport.close?.();
212
+ } catch (error) {
213
+ fastify.log?.warn?.({
214
+ transport: transport.name,
215
+ error
216
+ }, "[Arc Events] Transport close failed");
217
+ if (!failOpen) throw error;
218
+ }
219
+ });
220
+ 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.)");
221
+ else fastify.log?.debug?.(`[Arc Events] Using ${transport.name} transport`);
222
+ };
223
+ var eventPlugin_default = fp(eventPlugin, {
224
+ name: "arc-events",
225
+ fastify: "5.x"
226
+ });
227
+
228
+ //#endregion
229
+ export { MemoryEventTransport as a, withRetry as i, eventPlugin_exports as n, createEvent as o, createDeadLetterPublisher as r, eventPlugin as t };
230
+ //# sourceMappingURL=eventPlugin-DGR_B2on.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventPlugin-DGR_B2on.mjs","names":[],"sources":["../src/events/EventTransport.ts","../src/events/retry.ts","../src/events/eventPlugin.ts"],"sourcesContent":["/**\n * Event Transport Interface\n *\n * Defines contract for event delivery backends.\n * Implement for durable transports (Redis, RabbitMQ, Kafka, etc.)\n *\n * @example\n * // Redis Pub/Sub implementation\n * class RedisEventTransport implements EventTransport {\n * async publish(event) {\n * await redis.publish(event.type, JSON.stringify(event));\n * }\n * async subscribe(pattern, handler) {\n * redis.psubscribe(pattern);\n * redis.on('pmessage', (p, channel, msg) => handler(JSON.parse(msg)));\n * }\n * }\n */\n\nexport interface DomainEvent<T = unknown> {\n /** Event type (e.g., 'product.created', 'order.shipped') */\n type: string;\n /** Event payload */\n payload: T;\n /** Event metadata */\n meta: {\n /** Unique event ID */\n id: string;\n /** Event timestamp */\n timestamp: Date;\n /** Source resource */\n resource?: string;\n /** Resource ID */\n resourceId?: string;\n /** User who triggered the event */\n userId?: string;\n /** Organization context */\n organizationId?: string;\n /** Correlation ID for tracing */\n correlationId?: string;\n };\n}\n\nexport type EventHandler<T = unknown> = (event: DomainEvent<T>) => void | Promise<void>;\n\n/**\n * Minimal logger interface for event transports.\n * Compatible with `console`, `pino`, `fastify.log`, and any custom logger.\n *\n * @example\n * ```typescript\n * // Use Fastify's logger\n * new MemoryEventTransport({ logger: fastify.log });\n *\n * // Use a custom logger\n * new MemoryEventTransport({ logger: { warn: myWarn, error: myError } });\n *\n * // Default: console (no logger option needed)\n * new MemoryEventTransport();\n * ```\n */\nexport interface EventLogger {\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n}\n\nexport interface EventTransport {\n /** Transport name for logging */\n readonly name: string;\n\n /**\n * Publish an event to the transport\n */\n publish(event: DomainEvent): Promise<void>;\n\n /**\n * Subscribe to events matching a pattern\n * @param pattern - Event type pattern (e.g., 'product.*', '*')\n * @param handler - Handler function\n * @returns Unsubscribe function\n */\n subscribe(pattern: string, handler: EventHandler): Promise<() => void>;\n\n /**\n * Close transport connections\n */\n close?(): Promise<void>;\n}\n\nexport interface MemoryEventTransportOptions {\n /** Logger for error/warning messages (default: console) */\n logger?: EventLogger;\n}\n\n/**\n * In-memory event transport (default)\n * Events are delivered synchronously within the process.\n * Not suitable for multi-instance deployments.\n */\nexport class MemoryEventTransport implements EventTransport {\n readonly name = 'memory';\n private handlers = new Map<string, Set<EventHandler>>();\n private logger: EventLogger;\n\n constructor(options?: MemoryEventTransportOptions) {\n this.logger = options?.logger ?? console;\n }\n\n async publish(event: DomainEvent): Promise<void> {\n // Exact match handlers\n const exactHandlers = this.handlers.get(event.type) ?? new Set();\n\n // Wildcard handlers\n const wildcardHandlers = this.handlers.get('*') ?? new Set();\n\n // Pattern match handlers (e.g., 'product.*' matches 'product.created')\n const patternHandlers = new Set<EventHandler>();\n for (const [pattern, handlers] of this.handlers.entries()) {\n if (pattern.endsWith('.*')) {\n const prefix = pattern.slice(0, -2);\n if (event.type.startsWith(prefix + '.')) {\n handlers.forEach((h) => patternHandlers.add(h));\n }\n }\n }\n\n const allHandlers = new Set([...exactHandlers, ...wildcardHandlers, ...patternHandlers]);\n\n // Execute handlers (catch errors to prevent one handler from blocking others)\n for (const handler of allHandlers) {\n try {\n await handler(event);\n } catch (err) {\n this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);\n }\n }\n }\n\n async subscribe(pattern: string, handler: EventHandler): Promise<() => void> {\n if (!this.handlers.has(pattern)) {\n this.handlers.set(pattern, new Set());\n }\n this.handlers.get(pattern)!.add(handler);\n\n return () => {\n this.handlers.get(pattern)?.delete(handler);\n };\n }\n\n async close(): Promise<void> {\n this.handlers.clear();\n }\n}\n\n/**\n * Create a domain event with auto-generated metadata\n */\nexport function createEvent<T>(\n type: string,\n payload: T,\n meta?: Partial<DomainEvent['meta']>\n): DomainEvent<T> {\n return {\n type,\n payload,\n meta: {\n id: crypto.randomUUID(),\n timestamp: new Date(),\n ...meta,\n },\n };\n}\n\nexport default MemoryEventTransport;\n","/**\n * Event Handler Retry with Dead Letter Queue\n *\n * Transport-agnostic retry wrapper for event handlers.\n * Works with any EventTransport (Memory, Redis Pub/Sub, Redis Streams).\n *\n * @example\n * ```typescript\n * import { withRetry } from '@classytic/arc/events';\n *\n * // Retry up to 3 times with exponential backoff\n * await fastify.events.subscribe('order.created', withRetry(\n * async (event) => {\n * await sendConfirmationEmail(event.payload);\n * },\n * { maxRetries: 3, backoffMs: 1000 }\n * ));\n *\n * // With dead letter callback\n * await fastify.events.subscribe('order.created', withRetry(\n * async (event) => { ... },\n * {\n * maxRetries: 3,\n * onDead: async (event, errors) => {\n * await fastify.events.publish('$deadLetter', { event, errors });\n * },\n * }\n * ));\n * ```\n */\n\nimport type { DomainEvent, EventHandler, EventLogger } from './EventTransport.js';\n\nexport interface RetryOptions {\n /**\n * Max retry attempts (not counting the initial attempt).\n * @default 3\n */\n maxRetries?: number;\n\n /**\n * Initial backoff delay in ms. Doubles on each retry (exponential backoff).\n * @default 1000\n */\n backoffMs?: number;\n\n /**\n * Maximum backoff delay in ms (caps exponential growth).\n * @default 30000\n */\n maxBackoffMs?: number;\n\n /**\n * Jitter factor (0-1). Adds randomness to prevent thundering herd.\n * 0 = no jitter, 1 = full jitter (delay ∈ [0, calculated]).\n * @default 0.1\n */\n jitter?: number;\n\n /**\n * Callback when all retries are exhausted. The event is \"dead\".\n * Use this to publish to a `$deadLetter` channel, log, alert, etc.\n */\n onDead?: (event: DomainEvent, errors: Error[]) => void | Promise<void>;\n\n /**\n * Optional name for logging/debugging.\n */\n name?: string;\n\n /**\n * Logger for retry warnings and error messages (default: console).\n * Pass `fastify.log` to integrate with your application logger.\n */\n logger?: EventLogger;\n}\n\n/**\n * Wrap an event handler with retry logic and dead letter support.\n *\n * On failure, retries with exponential backoff (with jitter).\n * After all retries exhausted, calls `onDead` callback if provided.\n */\nexport function withRetry(\n handler: EventHandler,\n options: RetryOptions = {},\n): EventHandler {\n const {\n maxRetries = 3,\n backoffMs = 1000,\n maxBackoffMs = 30_000,\n jitter = 0.1,\n onDead,\n name,\n logger = console,\n } = options;\n\n const label = name ?? handler.name ?? 'anonymous';\n\n return async (event: DomainEvent): Promise<void> => {\n const errors: Error[] = [];\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n await handler(event);\n return; // Success\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n errors.push(error);\n\n if (attempt < maxRetries) {\n // Calculate delay with exponential backoff + jitter\n const baseDelay = Math.min(backoffMs * 2 ** attempt, maxBackoffMs);\n const jitterAmount = jitter * baseDelay * Math.random();\n const delay = baseDelay + jitterAmount;\n\n logger.warn(\n `[Arc Events] Handler '${label}' failed for ${event.type} ` +\n `(attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${Math.round(delay)}ms: ${error.message}`,\n );\n\n await sleep(delay);\n }\n }\n }\n\n // All retries exhausted — event is dead\n logger.error(\n `[Arc Events] Handler '${label}' permanently failed for ${event.type} ` +\n `after ${maxRetries + 1} attempts. ${errors.length} errors.`,\n );\n\n if (onDead) {\n try {\n await onDead(event, errors);\n } catch (dlqErr) {\n logger.error('[Arc Events] Dead letter callback failed:', dlqErr);\n }\n }\n };\n}\n\n/**\n * Create a dead letter publisher that sends failed events to a `$deadLetter` channel.\n *\n * @example\n * ```typescript\n * import { withRetry, createDeadLetterPublisher } from '@classytic/arc/events';\n *\n * const toDlq = createDeadLetterPublisher(fastify.events);\n *\n * await fastify.events.subscribe('order.created', withRetry(handler, {\n * maxRetries: 3,\n * onDead: toDlq,\n * }));\n *\n * // Monitor dead letters\n * await fastify.events.subscribe('$deadLetter', async (event) => {\n * console.error('Dead letter:', event.payload);\n * await alertOps(event.payload);\n * });\n * ```\n */\nexport function createDeadLetterPublisher(\n events: { publish: <T>(type: string, payload: T, meta?: Record<string, unknown>) => Promise<void> },\n): (event: DomainEvent, errors: Error[]) => Promise<void> {\n return async (event: DomainEvent, errors: Error[]) => {\n await events.publish('$deadLetter', {\n originalEvent: event,\n errors: errors.map((e) => ({\n message: e.message,\n stack: e.stack,\n })),\n failedAt: new Date().toISOString(),\n });\n };\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * Event Plugin\n *\n * Integrates event transport with Fastify.\n * Defaults to in-memory transport; configure durable transport for production.\n *\n * @example\n * // Development (in-memory)\n * await fastify.register(eventPlugin);\n *\n * // Production (Redis)\n * await fastify.register(eventPlugin, {\n * transport: new RedisEventTransport({ url: process.env.REDIS_URL }),\n * });\n */\n\nimport fp from \"fastify-plugin\";\nimport type { FastifyInstance, FastifyPluginAsync } from \"fastify\";\nimport {\n MemoryEventTransport,\n createEvent,\n type EventTransport,\n type DomainEvent,\n type EventHandler,\n} from \"./EventTransport.js\";\nimport {\n withRetry,\n createDeadLetterPublisher,\n type RetryOptions,\n} from \"./retry.js\";\nimport { requestContext } from \"../context/requestContext.js\";\n\nexport interface EventPluginOptions {\n /** Event transport (default: MemoryEventTransport) */\n transport?: EventTransport;\n /** Enable event logging (default: false) */\n logEvents?: boolean;\n /**\n * Fail-open mode for runtime resilience (default: true).\n * - true: publish/subscribe/close errors are logged and suppressed\n * - false: errors are thrown to caller\n */\n failOpen?: boolean;\n /**\n * Write-Ahead Log (WAL) configuration for at-least-once delivery guarantees.\n * If provided, events will be saved to the WAL *before* passing to the transport.\n * After a successful publish, they are acknowledged.\n */\n wal?: {\n save: (event: DomainEvent) => Promise<void>;\n acknowledge?: (eventId: string) => Promise<void>;\n };\n /**\n * Auto-wrap all subscribed handlers with retry logic.\n * When enabled, failed handler invocations are retried with exponential backoff.\n */\n retry?: Pick<\n RetryOptions,\n \"maxRetries\" | \"backoffMs\" | \"maxBackoffMs\" | \"jitter\"\n >;\n /**\n * Dead letter queue for events that exhaust all retries.\n * Requires `retry` to be enabled. If `retry` is set but no custom `store`,\n * failed events are published to the `$deadLetter` event type by default.\n */\n deadLetterQueue?: {\n /** Custom store function. If omitted, publishes to '$deadLetter' event type. */\n store?: (event: DomainEvent, errors: Error[]) => void | Promise<void>;\n };\n /** Callback after successful publish (for metrics/tracking) */\n onPublish?: (event: DomainEvent) => void;\n /** Callback on publish failure (for metrics/alerting) */\n onPublishError?: (event: DomainEvent, error: Error) => void;\n}\n\ndeclare module \"fastify\" {\n interface FastifyInstance {\n events: {\n /** Publish an event */\n publish: <T>(\n type: string,\n payload: T,\n meta?: Partial<DomainEvent[\"meta\"]>,\n ) => Promise<void>;\n /** Subscribe to events */\n subscribe: (\n pattern: string,\n handler: EventHandler,\n ) => Promise<() => void>;\n /** Get transport name */\n transportName: string;\n };\n }\n}\n\nconst eventPlugin: FastifyPluginAsync<EventPluginOptions> = async (\n fastify: FastifyInstance,\n opts: EventPluginOptions = {},\n) => {\n const {\n transport = new MemoryEventTransport(),\n logEvents = false,\n failOpen = true,\n retry: retryOpts,\n deadLetterQueue: dlqOpts,\n wal,\n onPublish,\n onPublishError,\n } = opts;\n\n // Decorate fastify with event utilities\n fastify.decorate(\"events\", {\n publish: async <T>(\n type: string,\n payload: T,\n meta?: Partial<DomainEvent[\"meta\"]>,\n ): Promise<void> => {\n // Auto-inject correlationId from request context if not already set\n const store = requestContext.get();\n const enrichedMeta: Partial<DomainEvent[\"meta\"]> = {\n ...(store?.requestId && !meta?.correlationId\n ? { correlationId: store.requestId }\n : {}),\n ...meta,\n };\n const event = createEvent(type, payload, enrichedMeta);\n\n if (logEvents) {\n fastify.log?.info?.(\n {\n eventType: type,\n eventId: event.meta.id,\n correlationId: event.meta.correlationId,\n },\n \"Publishing event\",\n );\n }\n\n try {\n if (wal) {\n await wal.save(event);\n }\n await transport.publish(event);\n if (wal?.acknowledge) {\n await wal.acknowledge(event.meta.id);\n }\n onPublish?.(event);\n } catch (error) {\n fastify.log?.error?.(\n { transport: transport.name, eventType: type, error },\n \"[Arc Events] Failed to publish event\",\n );\n onPublishError?.(event, error as Error);\n if (!failOpen) throw error;\n }\n },\n\n subscribe: async (\n pattern: string,\n handler: EventHandler,\n ): Promise<() => void> => {\n // Auto-wrap handler with retry if configured (skip for DLQ subscriptions)\n let wrappedHandler = handler;\n if (retryOpts && pattern !== \"$deadLetter\") {\n wrappedHandler = withRetry(handler, {\n ...retryOpts,\n onDead: dlqOpts?.store ?? createDeadLetterPublisher(fastify.events),\n logger: fastify.log as import(\"./EventTransport.js\").EventLogger,\n });\n }\n\n if (logEvents) {\n fastify.log?.info?.(\n { pattern, retry: !!retryOpts },\n \"Subscribing to events\",\n );\n }\n try {\n return await transport.subscribe(pattern, wrappedHandler);\n } catch (error) {\n fastify.log?.error?.(\n { transport: transport.name, pattern, error },\n \"[Arc Events] Failed to subscribe to events\",\n );\n if (!failOpen) throw error;\n return () => {};\n }\n },\n\n transportName: transport.name,\n });\n\n // Cleanup on close\n fastify.addHook(\"onClose\", async () => {\n try {\n await transport.close?.();\n } catch (error) {\n fastify.log?.warn?.(\n { transport: transport.name, error },\n \"[Arc Events] Transport close failed\",\n );\n if (!failOpen) throw error;\n }\n });\n\n // Log transport type\n if (transport.name === \"memory\") {\n fastify.log?.warn?.(\n \"[Arc Events] Using in-memory transport. Events will not persist or scale across instances. \" +\n \"For production, configure a durable transport (Redis, RabbitMQ, etc.)\",\n );\n } else {\n fastify.log?.debug?.(`[Arc Events] Using ${transport.name} transport`);\n }\n};\n\nexport default fp(eventPlugin, {\n name: \"arc-events\",\n fastify: \"5.x\",\n});\n\nexport { eventPlugin };\n"],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import { a as MemoryEventTransport, i as EventTransport, n as EventHandler, o as MemoryEventTransportOptions, r as EventLogger, s as createEvent, t as DomainEvent } from "../EventTransport-BD2U0BTc.mjs";
2
+ import { a as withRetry, i as createDeadLetterPublisher, n as eventPlugin, r as RetryOptions, t as EventPluginOptions } from "../eventPlugin-CTrLH3mt.mjs";
3
+ import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
4
+ import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-Bdh_vUU8.mjs";
5
+
6
+ //#region src/events/eventTypes.d.ts
7
+ /**
8
+ * Event Type Constants and Helpers
9
+ *
10
+ * Provides well-typed event names for CRUD operations and lifecycle events.
11
+ * Use these instead of hand-typing event strings to prevent typos.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { crudEventType, ARC_LIFECYCLE_EVENTS } from '@classytic/arc/events';
16
+ *
17
+ * // Subscribe to product creation events
18
+ * events.subscribe(crudEventType('product', 'created'), handler);
19
+ *
20
+ * // Subscribe to Arc lifecycle events
21
+ * events.subscribe(ARC_LIFECYCLE_EVENTS.READY, handler);
22
+ * ```
23
+ */
24
+ /** Suffixes for auto-emitted CRUD events */
25
+ declare const CRUD_EVENT_SUFFIXES: readonly ["created", "updated", "deleted"];
26
+ /** Type for CRUD event suffixes */
27
+ type CrudEventSuffix = typeof CRUD_EVENT_SUFFIXES[number];
28
+ /**
29
+ * Build a CRUD event type string.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * crudEventType('product', 'created') // 'product.created'
34
+ * crudEventType('order', 'deleted') // 'order.deleted'
35
+ * ```
36
+ */
37
+ declare function crudEventType(resource: string, suffix: CrudEventSuffix): string;
38
+ /** Arc framework lifecycle events — emitted automatically by the framework */
39
+ declare const ARC_LIFECYCLE_EVENTS: Readonly<{
40
+ /** Emitted when a resource plugin is registered */readonly RESOURCE_REGISTERED: "arc.resource.registered"; /** Emitted when Arc is fully ready (all resources registered, onReady fired) */
41
+ readonly READY: "arc.ready";
42
+ }>;
43
+ /** Type for Arc lifecycle event names */
44
+ type ArcLifecycleEvent = typeof ARC_LIFECYCLE_EVENTS[keyof typeof ARC_LIFECYCLE_EVENTS];
45
+ /** Cache-specific event types for observability and external triggers */
46
+ declare const CACHE_EVENTS: Readonly<{
47
+ /** Emitted when a resource's cache version is bumped */readonly VERSION_BUMPED: "arc.cache.version.bumped"; /** Emitted when a tag version is bumped */
48
+ readonly TAG_VERSION_BUMPED: "arc.cache.tag.bumped";
49
+ }>;
50
+ /** Type for cache event names */
51
+ type CacheEvent = typeof CACHE_EVENTS[keyof typeof CACHE_EVENTS];
52
+ //#endregion
53
+ export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type DomainEvent, type EventHandler, type EventLogger, type EventPluginOptions, type EventTransport, MemoryEventTransport, type MemoryEventTransportOptions, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RetryOptions, createDeadLetterPublisher, createEvent, crudEventType, eventPlugin, withRetry };
54
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/events/eventTypes.ts"],"mappings":";;;;;;;;;;;;AAmBA;;;;;AAGA;;;;;AAWA;;cAda,mBAAA;;KAGD,eAAA,UAAyB,mBAAA;;;;;AAgBrC;;;;;iBALgB,aAAA,CAAc,QAAA,UAAkB,MAAA,EAAQ,eAAA;;cAK3C,oBAAA,EAAoB,QAAA;8GAQJ;EAAA;;;KAAjB,iBAAA,UAA2B,oBAAA,cAAkC,oBAAA;;cAG5D,YAAA,EAAY,QAAA;EAAA;;;;KAQb,UAAA,UAAoB,YAAA,cAA0B,YAAA"}
@@ -0,0 +1,52 @@
1
+ import { a as MemoryEventTransport, i as withRetry, o as createEvent, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-DGR_B2on.mjs";
2
+
3
+ //#region src/events/eventTypes.ts
4
+ /**
5
+ * Event Type Constants and Helpers
6
+ *
7
+ * Provides well-typed event names for CRUD operations and lifecycle events.
8
+ * Use these instead of hand-typing event strings to prevent typos.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { crudEventType, ARC_LIFECYCLE_EVENTS } from '@classytic/arc/events';
13
+ *
14
+ * // Subscribe to product creation events
15
+ * events.subscribe(crudEventType('product', 'created'), handler);
16
+ *
17
+ * // Subscribe to Arc lifecycle events
18
+ * events.subscribe(ARC_LIFECYCLE_EVENTS.READY, handler);
19
+ * ```
20
+ */
21
+ /** Suffixes for auto-emitted CRUD events */
22
+ const CRUD_EVENT_SUFFIXES = Object.freeze([
23
+ "created",
24
+ "updated",
25
+ "deleted"
26
+ ]);
27
+ /**
28
+ * Build a CRUD event type string.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * crudEventType('product', 'created') // 'product.created'
33
+ * crudEventType('order', 'deleted') // 'order.deleted'
34
+ * ```
35
+ */
36
+ function crudEventType(resource, suffix) {
37
+ return `${resource}.${suffix}`;
38
+ }
39
+ /** Arc framework lifecycle events — emitted automatically by the framework */
40
+ const ARC_LIFECYCLE_EVENTS = Object.freeze({
41
+ RESOURCE_REGISTERED: "arc.resource.registered",
42
+ READY: "arc.ready"
43
+ });
44
+ /** Cache-specific event types for observability and external triggers */
45
+ const CACHE_EVENTS = Object.freeze({
46
+ VERSION_BUMPED: "arc.cache.version.bumped",
47
+ TAG_VERSION_BUMPED: "arc.cache.tag.bumped"
48
+ });
49
+
50
+ //#endregion
51
+ export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, MemoryEventTransport, createDeadLetterPublisher, createEvent, crudEventType, eventPlugin, withRetry };
52
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/events/eventTypes.ts"],"sourcesContent":["/**\r\n * Event Type Constants and Helpers\r\n *\r\n * Provides well-typed event names for CRUD operations and lifecycle events.\r\n * Use these instead of hand-typing event strings to prevent typos.\r\n *\r\n * @example\r\n * ```typescript\r\n * import { crudEventType, ARC_LIFECYCLE_EVENTS } from '@classytic/arc/events';\r\n *\r\n * // Subscribe to product creation events\r\n * events.subscribe(crudEventType('product', 'created'), handler);\r\n *\r\n * // Subscribe to Arc lifecycle events\r\n * events.subscribe(ARC_LIFECYCLE_EVENTS.READY, handler);\r\n * ```\r\n */\r\n\r\n/** Suffixes for auto-emitted CRUD events */\r\nexport const CRUD_EVENT_SUFFIXES = Object.freeze(['created', 'updated', 'deleted'] as const);\r\n\r\n/** Type for CRUD event suffixes */\r\nexport type CrudEventSuffix = typeof CRUD_EVENT_SUFFIXES[number];\r\n\r\n/**\r\n * Build a CRUD event type string.\r\n *\r\n * @example\r\n * ```typescript\r\n * crudEventType('product', 'created') // 'product.created'\r\n * crudEventType('order', 'deleted') // 'order.deleted'\r\n * ```\r\n */\r\nexport function crudEventType(resource: string, suffix: CrudEventSuffix): string {\r\n return `${resource}.${suffix}`;\r\n}\r\n\r\n/** Arc framework lifecycle events — emitted automatically by the framework */\r\nexport const ARC_LIFECYCLE_EVENTS = Object.freeze({\r\n /** Emitted when a resource plugin is registered */\r\n RESOURCE_REGISTERED: 'arc.resource.registered',\r\n /** Emitted when Arc is fully ready (all resources registered, onReady fired) */\r\n READY: 'arc.ready',\r\n} as const);\r\n\r\n/** Type for Arc lifecycle event names */\r\nexport type ArcLifecycleEvent = typeof ARC_LIFECYCLE_EVENTS[keyof typeof ARC_LIFECYCLE_EVENTS];\r\n\r\n/** Cache-specific event types for observability and external triggers */\r\nexport const CACHE_EVENTS = Object.freeze({\r\n /** Emitted when a resource's cache version is bumped */\r\n VERSION_BUMPED: 'arc.cache.version.bumped',\r\n /** Emitted when a tag version is bumped */\r\n TAG_VERSION_BUMPED: 'arc.cache.tag.bumped',\r\n} as const);\r\n\r\n/** Type for cache event names */\r\nexport type CacheEvent = typeof CACHE_EVENTS[keyof typeof CACHE_EVENTS];\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmBA,MAAa,sBAAsB,OAAO,OAAO;CAAC;CAAW;CAAW;CAAU,CAAU;;;;;;;;;;AAc5F,SAAgB,cAAc,UAAkB,QAAiC;AAC/E,QAAO,GAAG,SAAS,GAAG;;;AAIxB,MAAa,uBAAuB,OAAO,OAAO;CAEhD,qBAAqB;CAErB,OAAO;CACR,CAAU;;AAMX,MAAa,eAAe,OAAO,OAAO;CAExC,gBAAgB;CAEhB,oBAAoB;CACrB,CAAU"}
@@ -0,0 +1,2 @@
1
+ import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-Bdh_vUU8.mjs";
2
+ export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
@@ -0,0 +1,178 @@
1
+ //#region src/events/transports/redis-stream.ts
2
+ var RedisStreamTransport = class {
3
+ name = "redis-stream";
4
+ redis;
5
+ stream;
6
+ group;
7
+ consumer;
8
+ blockTimeMs;
9
+ batchSize;
10
+ maxRetries;
11
+ claimTimeoutMs;
12
+ deadLetterStream;
13
+ maxLen;
14
+ logger;
15
+ handlers = /* @__PURE__ */ new Map();
16
+ running = false;
17
+ pollPromise = null;
18
+ groupCreated = false;
19
+ constructor(redis, options = {}) {
20
+ this.redis = redis;
21
+ this.stream = options.stream ?? "arc:events";
22
+ this.group = options.group ?? "default";
23
+ this.consumer = options.consumer ?? `consumer-${crypto.randomUUID().slice(0, 8)}`;
24
+ this.blockTimeMs = options.blockTimeMs ?? 5e3;
25
+ this.batchSize = options.batchSize ?? 10;
26
+ this.maxRetries = options.maxRetries ?? 5;
27
+ this.claimTimeoutMs = options.claimTimeoutMs ?? 3e4;
28
+ this.deadLetterStream = options.deadLetterStream ?? "arc:events:dlq";
29
+ this.maxLen = options.maxLen ?? 1e4;
30
+ this.logger = options.logger ?? console;
31
+ }
32
+ async publish(event) {
33
+ const args = [
34
+ this.stream,
35
+ ...this.maxLen > 0 ? [
36
+ "MAXLEN",
37
+ "~",
38
+ String(this.maxLen)
39
+ ] : [],
40
+ "*",
41
+ "type",
42
+ event.type,
43
+ "data",
44
+ JSON.stringify(event)
45
+ ];
46
+ await this.redis.xadd(...args);
47
+ }
48
+ async subscribe(pattern, handler) {
49
+ if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
50
+ this.handlers.get(pattern).add(handler);
51
+ if (!this.running) {
52
+ await this.ensureGroup();
53
+ this.running = true;
54
+ this.pollPromise = this.pollLoop();
55
+ }
56
+ return () => {
57
+ const set = this.handlers.get(pattern);
58
+ if (set) {
59
+ set.delete(handler);
60
+ if (set.size === 0) this.handlers.delete(pattern);
61
+ }
62
+ if (this.handlers.size === 0 && this.running) this.running = false;
63
+ };
64
+ }
65
+ async close() {
66
+ this.running = false;
67
+ this.handlers.clear();
68
+ if (this.pollPromise) {
69
+ await this.pollPromise;
70
+ this.pollPromise = null;
71
+ }
72
+ }
73
+ async ensureGroup() {
74
+ if (this.groupCreated) return;
75
+ try {
76
+ await this.redis.xgroup("CREATE", this.stream, this.group, "$", "MKSTREAM");
77
+ } catch (err) {
78
+ if (err instanceof Error && !err.message.includes("BUSYGROUP")) throw err;
79
+ }
80
+ this.groupCreated = true;
81
+ }
82
+ async pollLoop() {
83
+ while (this.running) try {
84
+ await this.claimPending();
85
+ await this.readNewMessages();
86
+ } catch (err) {
87
+ if (this.running) {
88
+ this.logger.error("[RedisStreamTransport] Poll error:", err);
89
+ await this.sleep(1e3);
90
+ }
91
+ }
92
+ }
93
+ async readNewMessages() {
94
+ const result = await this.redis.xreadgroup("GROUP", this.group, this.consumer, "COUNT", this.batchSize, "BLOCK", this.blockTimeMs, "STREAMS", this.stream, ">");
95
+ if (!result) return;
96
+ for (const [, entries] of result) for (const [messageId, fields] of entries) await this.processEntry(messageId, fields);
97
+ }
98
+ async claimPending() {
99
+ try {
100
+ const pending = await this.redis.xpending(this.stream, this.group, "-", "+", 10);
101
+ if (!pending || pending.length === 0) return;
102
+ const staleIds = [];
103
+ const overRetryIds = [];
104
+ for (const entry of pending) {
105
+ const [id, , idleTime, deliveryCount] = entry;
106
+ if (idleTime > this.claimTimeoutMs) if (deliveryCount >= this.maxRetries) overRetryIds.push(id);
107
+ else staleIds.push(id);
108
+ }
109
+ if (overRetryIds.length > 0) await this.moveToDlq(overRetryIds);
110
+ if (staleIds.length > 0) {
111
+ const claimed = await this.redis.xclaim(this.stream, this.group, this.consumer, this.claimTimeoutMs, ...staleIds);
112
+ for (const [messageId, fields] of claimed) await this.processEntry(messageId, fields);
113
+ }
114
+ } catch {}
115
+ }
116
+ async processEntry(messageId, fields) {
117
+ const fieldMap = /* @__PURE__ */ new Map();
118
+ for (let i = 0; i < fields.length; i += 2) fieldMap.set(fields[i], fields[i + 1]);
119
+ const eventType = fieldMap.get("type");
120
+ const rawData = fieldMap.get("data");
121
+ if (!eventType || !rawData) {
122
+ await this.redis.xack(this.stream, this.group, messageId);
123
+ return;
124
+ }
125
+ let event;
126
+ try {
127
+ event = JSON.parse(rawData, (key, value) => {
128
+ if (key === "timestamp" && typeof value === "string") return new Date(value);
129
+ return value;
130
+ });
131
+ } catch {
132
+ await this.redis.xack(this.stream, this.group, messageId);
133
+ return;
134
+ }
135
+ const matchingHandlers = this.getMatchingHandlers(event.type);
136
+ let allSucceeded = true;
137
+ for (const handler of matchingHandlers) try {
138
+ await handler(event);
139
+ } catch (err) {
140
+ allSucceeded = false;
141
+ this.logger.error(`[RedisStreamTransport] Handler error for ${event.type}:`, err);
142
+ }
143
+ if (allSucceeded) await this.redis.xack(this.stream, this.group, messageId);
144
+ }
145
+ getMatchingHandlers(eventType) {
146
+ const matched = [];
147
+ for (const [pattern, handlers] of this.handlers) if (this.matchesPattern(pattern, eventType)) for (const h of handlers) matched.push(h);
148
+ return matched;
149
+ }
150
+ matchesPattern(pattern, eventType) {
151
+ if (pattern === "*") return true;
152
+ if (pattern === eventType) return true;
153
+ if (pattern.endsWith(".*")) {
154
+ const prefix = pattern.slice(0, -2);
155
+ return eventType.startsWith(prefix + ".");
156
+ }
157
+ return false;
158
+ }
159
+ async moveToDlq(ids) {
160
+ if (this.deadLetterStream === false) {
161
+ for (const id of ids) await this.redis.xack(this.stream, this.group, id);
162
+ return;
163
+ }
164
+ for (const id of ids) try {
165
+ await this.redis.xadd(this.deadLetterStream, "*", "originalStream", this.stream, "originalId", id, "group", this.group, "failedAt", (/* @__PURE__ */ new Date()).toISOString());
166
+ await this.redis.xack(this.stream, this.group, id);
167
+ } catch (err) {
168
+ this.logger.error(`[RedisStreamTransport] DLQ write failed for ${id}:`, err);
169
+ }
170
+ }
171
+ sleep(ms) {
172
+ return new Promise((resolve) => setTimeout(resolve, ms));
173
+ }
174
+ };
175
+
176
+ //#endregion
177
+ export { RedisStreamTransport };
178
+ //# sourceMappingURL=redis-stream-entry.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-stream-entry.mjs","names":[],"sources":["../../../src/events/transports/redis-stream.ts"],"sourcesContent":["/**\n * Redis Stream Event Transport — Durable Event Delivery\n *\n * Uses Redis Streams (`XADD`/`XREADGROUP`) for persistent, exactly-once event\n * delivery across multiple service instances. Unlike Pub/Sub, events are stored\n * in Redis and survive crashes/restarts.\n *\n * Key features:\n * - Consumer groups: each event processed by exactly one consumer per group\n * - Crash recovery: pending entries are auto-claimed after `claimTimeoutMs`\n * - Dead letter stream: events exceeding `maxRetries` are moved to a DLQ\n * - Backpressure: configurable block time and batch size\n *\n * @example\n * ```typescript\n * import { RedisStreamTransport } from '@classytic/arc/events';\n * import Redis from 'ioredis';\n *\n * const transport = new RedisStreamTransport(new Redis(), {\n * stream: 'arc-events',\n * group: 'api-service',\n * consumer: 'worker-1',\n * });\n *\n * await app.register(eventPlugin, { transport });\n * ```\n */\n\nimport type { EventTransport, DomainEvent, EventHandler, EventLogger } from '../EventTransport.js';\n\n// ---------------------------------------------------------------------------\n// Minimal Redis-like interface for Streams support\n// ---------------------------------------------------------------------------\n\nexport interface RedisStreamLike {\n xadd(key: string, id: string, ...fieldValues: string[]): Promise<string | null>;\n xreadgroup(\n command: 'GROUP',\n group: string,\n consumer: string,\n ...args: (string | number)[]\n ): Promise<Array<[string, Array<[string, string[]]>]> | null>;\n xack(key: string, group: string, ...ids: string[]): Promise<number>;\n xgroup(command: string, key: string, group: string, ...args: string[]): Promise<unknown>;\n xpending(\n key: string,\n group: string,\n ...args: (string | number)[]\n ): Promise<Array<[string, string, number, number]>>;\n xclaim(\n key: string,\n group: string,\n consumer: string,\n minIdleTime: number,\n ...ids: string[]\n ): Promise<Array<[string, string[]]>>;\n xlen(key: string): Promise<number>;\n quit(): Promise<unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface RedisStreamTransportOptions {\n /**\n * Redis stream key name.\n * @default 'arc:events'\n */\n stream?: string;\n\n /**\n * Consumer group name. Each group receives every event independently.\n * Multiple instances of the same service should share a group name.\n * @default 'default'\n */\n group?: string;\n\n /**\n * Consumer name within the group. Must be unique per instance.\n * @default 'consumer-<random>'\n */\n consumer?: string;\n\n /**\n * Block time in ms when waiting for new events.\n * @default 5000\n */\n blockTimeMs?: number;\n\n /**\n * Max events to read per batch.\n * @default 10\n */\n batchSize?: number;\n\n /**\n * Max delivery attempts before moving to dead letter stream.\n * @default 5\n */\n maxRetries?: number;\n\n /**\n * Idle time in ms before pending entries are claimed by this consumer.\n * Handles crash recovery — if a consumer dies mid-processing, another\n * consumer will claim its pending entries after this timeout.\n * @default 30000\n */\n claimTimeoutMs?: number;\n\n /**\n * Dead letter stream name. Failed events are moved here after maxRetries.\n * Set to `false` to disable DLQ (failed events are acked and dropped).\n * @default 'arc:events:dlq'\n */\n deadLetterStream?: string | false;\n\n /**\n * Max stream length (approximate). Uses XADD MAXLEN ~ to trim old entries.\n * Set to 0 to disable trimming.\n * @default 10000\n */\n maxLen?: number;\n\n /**\n * Logger for error messages (default: console).\n * Pass `fastify.log` to integrate with your application logger.\n */\n logger?: EventLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Transport\n// ---------------------------------------------------------------------------\n\nexport class RedisStreamTransport implements EventTransport {\n readonly name = 'redis-stream';\n\n private redis: RedisStreamLike;\n private stream: string;\n private group: string;\n private consumer: string;\n private blockTimeMs: number;\n private batchSize: number;\n private maxRetries: number;\n private claimTimeoutMs: number;\n private deadLetterStream: string | false;\n private maxLen: number;\n\n private logger: EventLogger;\n\n private handlers = new Map<string, Set<EventHandler>>();\n private running = false;\n private pollPromise: Promise<void> | null = null;\n private groupCreated = false;\n\n constructor(redis: RedisStreamLike, options: RedisStreamTransportOptions = {}) {\n this.redis = redis;\n this.stream = options.stream ?? 'arc:events';\n this.group = options.group ?? 'default';\n this.consumer = options.consumer ?? `consumer-${crypto.randomUUID().slice(0, 8)}`;\n this.blockTimeMs = options.blockTimeMs ?? 5000;\n this.batchSize = options.batchSize ?? 10;\n this.maxRetries = options.maxRetries ?? 5;\n this.claimTimeoutMs = options.claimTimeoutMs ?? 30_000;\n this.deadLetterStream = options.deadLetterStream ?? 'arc:events:dlq';\n this.maxLen = options.maxLen ?? 10_000;\n this.logger = options.logger ?? console;\n }\n\n // -----------------------------------------------------------------------\n // EventTransport.publish\n // -----------------------------------------------------------------------\n\n async publish(event: DomainEvent): Promise<void> {\n const args: string[] = [\n this.stream,\n ...(this.maxLen > 0 ? ['MAXLEN', '~', String(this.maxLen)] : []),\n '*',\n 'type', event.type,\n 'data', JSON.stringify(event),\n ];\n\n // Use spread to call xadd with dynamic args\n await (this.redis as any).xadd(...args);\n }\n\n // -----------------------------------------------------------------------\n // EventTransport.subscribe\n // -----------------------------------------------------------------------\n\n async subscribe(pattern: string, handler: EventHandler): Promise<() => void> {\n if (!this.handlers.has(pattern)) {\n this.handlers.set(pattern, new Set());\n }\n this.handlers.get(pattern)!.add(handler);\n\n // Start the consumer loop if not already running\n if (!this.running) {\n await this.ensureGroup();\n this.running = true;\n this.pollPromise = this.pollLoop();\n }\n\n return () => {\n const set = this.handlers.get(pattern);\n if (set) {\n set.delete(handler);\n if (set.size === 0) this.handlers.delete(pattern);\n }\n // Stop polling when no handlers remain — prevents CPU/network waste\n if (this.handlers.size === 0 && this.running) {\n this.running = false;\n }\n };\n }\n\n // -----------------------------------------------------------------------\n // EventTransport.close\n // -----------------------------------------------------------------------\n\n async close(): Promise<void> {\n this.running = false;\n this.handlers.clear();\n\n // Wait for the poll loop to finish its current iteration\n if (this.pollPromise) {\n await this.pollPromise;\n this.pollPromise = null;\n }\n }\n\n // -----------------------------------------------------------------------\n // Consumer group management\n // -----------------------------------------------------------------------\n\n private async ensureGroup(): Promise<void> {\n if (this.groupCreated) return;\n\n try {\n // Create the consumer group, starting from new messages ('$')\n // Use MKSTREAM to auto-create the stream if it doesn't exist\n await this.redis.xgroup('CREATE', this.stream, this.group, '$', 'MKSTREAM');\n } catch (err: unknown) {\n // BUSYGROUP = group already exists, which is fine\n if (err instanceof Error && !err.message.includes('BUSYGROUP')) {\n throw err;\n }\n }\n\n this.groupCreated = true;\n }\n\n // -----------------------------------------------------------------------\n // Poll loop — reads new messages + claims pending (crash recovery)\n // -----------------------------------------------------------------------\n\n private async pollLoop(): Promise<void> {\n while (this.running) {\n try {\n // Phase 1: Claim pending entries from dead consumers (crash recovery)\n await this.claimPending();\n\n // Phase 2: Read new messages\n await this.readNewMessages();\n } catch (err) {\n if (this.running) {\n this.logger.error('[RedisStreamTransport] Poll error:', err);\n // Brief pause before retrying on error\n await this.sleep(1000);\n }\n }\n }\n }\n\n private async readNewMessages(): Promise<void> {\n // XREADGROUP GROUP <group> <consumer> COUNT <n> BLOCK <ms> STREAMS <key> >\n const result = await this.redis.xreadgroup(\n 'GROUP', this.group, this.consumer,\n 'COUNT', this.batchSize,\n 'BLOCK', this.blockTimeMs,\n 'STREAMS', this.stream, '>',\n );\n\n if (!result) return; // Timeout, no new messages\n\n for (const [, entries] of result) {\n for (const [messageId, fields] of entries) {\n await this.processEntry(messageId, fields);\n }\n }\n }\n\n private async claimPending(): Promise<void> {\n try {\n // Check for pending entries across all consumers that have been idle\n const pending = await this.redis.xpending(\n this.stream, this.group,\n '-', '+', 10, // Check up to 10 pending entries\n );\n\n if (!pending || pending.length === 0) return;\n\n const staleIds: string[] = [];\n const overRetryIds: string[] = [];\n\n for (const entry of pending) {\n const [id, , idleTime, deliveryCount] = entry;\n if (idleTime > this.claimTimeoutMs) {\n if (deliveryCount >= this.maxRetries) {\n overRetryIds.push(id);\n } else {\n staleIds.push(id);\n }\n }\n }\n\n // Move over-retry entries to DLQ\n if (overRetryIds.length > 0) {\n await this.moveToDlq(overRetryIds);\n }\n\n // Claim stale entries\n if (staleIds.length > 0) {\n const claimed = await this.redis.xclaim(\n this.stream, this.group, this.consumer,\n this.claimTimeoutMs, ...staleIds,\n );\n\n for (const [messageId, fields] of claimed) {\n await this.processEntry(messageId, fields);\n }\n }\n } catch {\n // Pending check failures are non-fatal — will retry next iteration\n }\n }\n\n // -----------------------------------------------------------------------\n // Message processing\n // -----------------------------------------------------------------------\n\n private async processEntry(messageId: string, fields: string[]): Promise<void> {\n // Parse fields array into key-value pairs\n const fieldMap = new Map<string, string>();\n for (let i = 0; i < fields.length; i += 2) {\n fieldMap.set(fields[i]!, fields[i + 1]!);\n }\n\n const eventType = fieldMap.get('type');\n const rawData = fieldMap.get('data');\n if (!eventType || !rawData) {\n // Malformed entry — ack and skip\n await this.redis.xack(this.stream, this.group, messageId);\n return;\n }\n\n let event: DomainEvent;\n try {\n event = JSON.parse(rawData, (key, value) => {\n if (key === 'timestamp' && typeof value === 'string') return new Date(value);\n return value;\n }) as DomainEvent;\n } catch {\n // Unparseable — ack and skip\n await this.redis.xack(this.stream, this.group, messageId);\n return;\n }\n\n // Dispatch to matching handlers\n const matchingHandlers = this.getMatchingHandlers(event.type);\n let allSucceeded = true;\n\n for (const handler of matchingHandlers) {\n try {\n await handler(event);\n } catch (err) {\n allSucceeded = false;\n this.logger.error(`[RedisStreamTransport] Handler error for ${event.type}:`, err);\n }\n }\n\n if (allSucceeded) {\n await this.redis.xack(this.stream, this.group, messageId);\n }\n // If not all succeeded, leave unacked — will be retried via pending claim\n }\n\n private getMatchingHandlers(eventType: string): EventHandler[] {\n const matched: EventHandler[] = [];\n\n for (const [pattern, handlers] of this.handlers) {\n if (this.matchesPattern(pattern, eventType)) {\n for (const h of handlers) {\n matched.push(h);\n }\n }\n }\n\n return matched;\n }\n\n private matchesPattern(pattern: string, eventType: string): boolean {\n if (pattern === '*') return true;\n if (pattern === eventType) return true;\n if (pattern.endsWith('.*')) {\n const prefix = pattern.slice(0, -2);\n return eventType.startsWith(prefix + '.');\n }\n return false;\n }\n\n // -----------------------------------------------------------------------\n // Dead letter queue\n // -----------------------------------------------------------------------\n\n private async moveToDlq(ids: string[]): Promise<void> {\n if (this.deadLetterStream === false) {\n // DLQ disabled — just ack and drop\n for (const id of ids) {\n await this.redis.xack(this.stream, this.group, id);\n }\n return;\n }\n\n // Read the entries, write to DLQ stream, then ack\n for (const id of ids) {\n try {\n await (this.redis as any).xadd(\n this.deadLetterStream,\n '*',\n 'originalStream', this.stream,\n 'originalId', id,\n 'group', this.group,\n 'failedAt', new Date().toISOString(),\n );\n await this.redis.xack(this.stream, this.group, id);\n } catch (err) {\n this.logger.error(`[RedisStreamTransport] DLQ write failed for ${id}:`, err);\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // Utility\n // -----------------------------------------------------------------------\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\nexport default RedisStreamTransport;\n"],"mappings":"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"}