@zdavison/matador 2.0.9 → 2.0.10

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 (259) hide show
  1. package/dist/checkpoint/context.d.ts +59 -0
  2. package/dist/checkpoint/context.d.ts.map +1 -0
  3. package/dist/checkpoint/context.js +140 -0
  4. package/dist/checkpoint/context.test.d.ts +2 -0
  5. package/dist/checkpoint/context.test.d.ts.map +1 -0
  6. package/dist/checkpoint/context.test.js +424 -0
  7. package/dist/checkpoint/index.d.ts +7 -0
  8. package/dist/checkpoint/index.d.ts.map +1 -0
  9. package/dist/checkpoint/index.js +6 -0
  10. package/dist/checkpoint/stores/memory.d.ts +29 -0
  11. package/dist/checkpoint/stores/memory.d.ts.map +1 -0
  12. package/dist/checkpoint/stores/memory.js +39 -0
  13. package/{src/checkpoint/stores/noop.ts → dist/checkpoint/stores/noop.d.ts} +5 -13
  14. package/dist/checkpoint/stores/noop.d.ts.map +1 -0
  15. package/dist/checkpoint/stores/noop.js +18 -0
  16. package/dist/checkpoint/stores/stores.test.d.ts +2 -0
  17. package/dist/checkpoint/stores/stores.test.d.ts.map +1 -0
  18. package/dist/checkpoint/stores/stores.test.js +146 -0
  19. package/dist/checkpoint/types.d.ts +119 -0
  20. package/dist/checkpoint/types.d.ts.map +1 -0
  21. package/dist/checkpoint/types.js +1 -0
  22. package/dist/codec/codec.d.ts +29 -0
  23. package/dist/codec/codec.d.ts.map +1 -0
  24. package/dist/codec/codec.js +15 -0
  25. package/dist/codec/header-aware-codec.d.ts +36 -0
  26. package/dist/codec/header-aware-codec.d.ts.map +1 -0
  27. package/dist/codec/header-aware-codec.js +1 -0
  28. package/{src/codec/index.ts → dist/codec/index.d.ts} +2 -7
  29. package/dist/codec/index.d.ts.map +1 -0
  30. package/dist/codec/index.js +3 -0
  31. package/dist/codec/json-codec.d.ts +13 -0
  32. package/dist/codec/json-codec.d.ts.map +1 -0
  33. package/dist/codec/json-codec.js +64 -0
  34. package/dist/codec/rabbitmq-codec.d.ts +28 -0
  35. package/dist/codec/rabbitmq-codec.d.ts.map +1 -0
  36. package/dist/codec/rabbitmq-codec.js +242 -0
  37. package/dist/codec/rabbitmq-codec.test.d.ts +2 -0
  38. package/dist/codec/rabbitmq-codec.test.d.ts.map +1 -0
  39. package/dist/codec/rabbitmq-codec.test.js +433 -0
  40. package/dist/core/fanout.d.ts +59 -0
  41. package/dist/core/fanout.d.ts.map +1 -0
  42. package/dist/core/fanout.js +121 -0
  43. package/dist/core/fanout.test.d.ts +2 -0
  44. package/dist/core/fanout.test.d.ts.map +1 -0
  45. package/dist/core/fanout.test.js +1054 -0
  46. package/{src/core/index.ts → dist/core/index.d.ts} +2 -7
  47. package/dist/core/index.d.ts.map +1 -0
  48. package/dist/core/index.js +3 -0
  49. package/dist/core/matador.d.ts +133 -0
  50. package/dist/core/matador.d.ts.map +1 -0
  51. package/dist/core/matador.js +228 -0
  52. package/dist/core/matador.test.d.ts +2 -0
  53. package/dist/core/matador.test.d.ts.map +1 -0
  54. package/dist/core/matador.test.js +446 -0
  55. package/dist/core/shutdown.d.ts +78 -0
  56. package/dist/core/shutdown.d.ts.map +1 -0
  57. package/dist/core/shutdown.js +111 -0
  58. package/dist/core/shutdown.test.d.ts +2 -0
  59. package/dist/core/shutdown.test.d.ts.map +1 -0
  60. package/dist/core/shutdown.test.js +524 -0
  61. package/dist/errors/checkpoint-errors.d.ts +30 -0
  62. package/dist/errors/checkpoint-errors.d.ts.map +1 -0
  63. package/dist/errors/checkpoint-errors.js +49 -0
  64. package/dist/errors/has-description.d.ts +18 -0
  65. package/dist/errors/has-description.d.ts.map +1 -0
  66. package/dist/errors/has-description.js +9 -0
  67. package/dist/errors/index.d.ts +6 -0
  68. package/dist/errors/index.d.ts.map +1 -0
  69. package/dist/errors/index.js +22 -0
  70. package/dist/errors/matador-errors.d.ts +183 -0
  71. package/dist/errors/matador-errors.d.ts.map +1 -0
  72. package/dist/errors/matador-errors.js +376 -0
  73. package/dist/errors/retry-errors.d.ts +102 -0
  74. package/dist/errors/retry-errors.d.ts.map +1 -0
  75. package/dist/errors/retry-errors.js +155 -0
  76. package/dist/errors/retry-errors.test.d.ts +2 -0
  77. package/dist/errors/retry-errors.test.d.ts.map +1 -0
  78. package/dist/errors/retry-errors.test.js +136 -0
  79. package/dist/hooks/index.d.ts +4 -0
  80. package/dist/hooks/index.d.ts.map +1 -0
  81. package/dist/hooks/index.js +2 -0
  82. package/dist/hooks/safe-hooks.d.ts +32 -0
  83. package/dist/hooks/safe-hooks.d.ts.map +1 -0
  84. package/dist/hooks/safe-hooks.js +120 -0
  85. package/dist/hooks/types.d.ts +172 -0
  86. package/dist/hooks/types.d.ts.map +1 -0
  87. package/dist/hooks/types.js +9 -0
  88. package/dist/index.cjs +3228 -0
  89. package/dist/index.cjs.map +1 -0
  90. package/dist/index.d.cts +23 -0
  91. package/dist/index.d.ts +23 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +31 -0
  94. package/dist/index.js.map +1 -0
  95. package/{src/pipeline/index.ts → dist/pipeline/index.d.ts} +1 -0
  96. package/dist/pipeline/index.d.ts.map +1 -0
  97. package/dist/pipeline/index.js +1 -0
  98. package/dist/pipeline/pipeline.d.ts +63 -0
  99. package/dist/pipeline/pipeline.d.ts.map +1 -0
  100. package/dist/pipeline/pipeline.js +273 -0
  101. package/dist/pipeline/pipeline.test.d.ts +2 -0
  102. package/dist/pipeline/pipeline.test.d.ts.map +1 -0
  103. package/dist/pipeline/pipeline.test.js +1112 -0
  104. package/{src/retry/index.ts → dist/retry/index.d.ts} +1 -1
  105. package/dist/retry/index.d.ts.map +1 -0
  106. package/dist/retry/index.js +1 -0
  107. package/dist/retry/policy.d.ts +43 -0
  108. package/dist/retry/policy.d.ts.map +1 -0
  109. package/dist/retry/policy.js +1 -0
  110. package/dist/retry/standard-policy.d.ts +44 -0
  111. package/dist/retry/standard-policy.d.ts.map +1 -0
  112. package/dist/retry/standard-policy.js +102 -0
  113. package/dist/retry/standard-policy.test.d.ts +2 -0
  114. package/dist/retry/standard-policy.test.d.ts.map +1 -0
  115. package/dist/retry/standard-policy.test.js +196 -0
  116. package/dist/schema/index.d.ts +4 -0
  117. package/dist/schema/index.d.ts.map +1 -0
  118. package/dist/schema/index.js +2 -0
  119. package/dist/schema/registry.d.ts +63 -0
  120. package/dist/schema/registry.d.ts.map +1 -0
  121. package/dist/schema/registry.js +174 -0
  122. package/dist/schema/registry.test.d.ts +2 -0
  123. package/dist/schema/registry.test.d.ts.map +1 -0
  124. package/dist/schema/registry.test.js +278 -0
  125. package/{src/schema/types.ts → dist/schema/types.d.ts} +29 -88
  126. package/dist/schema/types.d.ts.map +1 -0
  127. package/dist/schema/types.js +74 -0
  128. package/dist/schema/types.test.d.ts +2 -0
  129. package/dist/schema/types.test.d.ts.map +1 -0
  130. package/dist/schema/types.test.js +242 -0
  131. package/dist/topology/builder.d.ts +80 -0
  132. package/dist/topology/builder.d.ts.map +1 -0
  133. package/dist/topology/builder.js +165 -0
  134. package/dist/topology/builder.test.d.ts +2 -0
  135. package/dist/topology/builder.test.d.ts.map +1 -0
  136. package/dist/topology/builder.test.js +360 -0
  137. package/dist/topology/index.d.ts +5 -0
  138. package/dist/topology/index.d.ts.map +1 -0
  139. package/dist/topology/index.js +2 -0
  140. package/dist/topology/types.d.ts +128 -0
  141. package/dist/topology/types.d.ts.map +1 -0
  142. package/dist/topology/types.js +28 -0
  143. package/dist/transport/capabilities.d.ts +66 -0
  144. package/dist/transport/capabilities.d.ts.map +1 -0
  145. package/dist/transport/capabilities.js +18 -0
  146. package/dist/transport/connection-manager.d.ts +95 -0
  147. package/dist/transport/connection-manager.d.ts.map +1 -0
  148. package/dist/transport/connection-manager.js +144 -0
  149. package/dist/transport/index.d.ts +11 -0
  150. package/dist/transport/index.d.ts.map +1 -0
  151. package/dist/transport/index.js +5 -0
  152. package/dist/transport/local/local-transport.d.ts +62 -0
  153. package/dist/transport/local/local-transport.d.ts.map +1 -0
  154. package/dist/transport/local/local-transport.js +241 -0
  155. package/dist/transport/local/local-transport.test.d.ts +2 -0
  156. package/dist/transport/local/local-transport.test.d.ts.map +1 -0
  157. package/dist/transport/local/local-transport.test.js +192 -0
  158. package/dist/transport/multi/multi-transport.d.ts +94 -0
  159. package/dist/transport/multi/multi-transport.d.ts.map +1 -0
  160. package/dist/transport/multi/multi-transport.js +184 -0
  161. package/dist/transport/multi/multi-transport.test.d.ts +2 -0
  162. package/dist/transport/multi/multi-transport.test.d.ts.map +1 -0
  163. package/dist/transport/multi/multi-transport.test.js +236 -0
  164. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +83 -0
  165. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -0
  166. package/dist/transport/rabbitmq/rabbitmq-transport.js +541 -0
  167. package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts +2 -0
  168. package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts.map +1 -0
  169. package/dist/transport/rabbitmq/rabbitmq-transport.test.js +100 -0
  170. package/dist/transport/transport.d.ts +152 -0
  171. package/dist/transport/transport.d.ts.map +1 -0
  172. package/dist/transport/transport.js +1 -0
  173. package/{src/types/common.ts → dist/types/common.d.ts} +8 -20
  174. package/dist/types/common.d.ts.map +1 -0
  175. package/dist/types/common.js +12 -0
  176. package/dist/types/dispatcher.d.ts +14 -0
  177. package/dist/types/dispatcher.d.ts.map +1 -0
  178. package/dist/types/dispatcher.js +1 -0
  179. package/dist/types/envelope.d.ts +131 -0
  180. package/dist/types/envelope.d.ts.map +1 -0
  181. package/dist/types/envelope.js +99 -0
  182. package/dist/types/event.d.ts +92 -0
  183. package/dist/types/event.d.ts.map +1 -0
  184. package/dist/types/event.js +26 -0
  185. package/dist/types/event.test.d.ts +2 -0
  186. package/dist/types/event.test.d.ts.map +1 -0
  187. package/dist/types/event.test.js +130 -0
  188. package/dist/types/index.d.ts +11 -0
  189. package/dist/types/index.d.ts.map +1 -0
  190. package/dist/types/index.js +4 -0
  191. package/dist/types/subscriber.d.ts +227 -0
  192. package/dist/types/subscriber.d.ts.map +1 -0
  193. package/dist/types/subscriber.js +97 -0
  194. package/package.json +6 -2
  195. package/examples/config.ts +0 -126
  196. package/examples/event.ts +0 -26
  197. package/examples/order-event.json +0 -19
  198. package/src/checkpoint/context.test.ts +0 -510
  199. package/src/checkpoint/context.ts +0 -213
  200. package/src/checkpoint/index.ts +0 -30
  201. package/src/checkpoint/stores/memory.ts +0 -47
  202. package/src/checkpoint/stores/stores.test.ts +0 -177
  203. package/src/checkpoint/types.ts +0 -147
  204. package/src/codec/codec.ts +0 -42
  205. package/src/codec/header-aware-codec.ts +0 -41
  206. package/src/codec/json-codec.ts +0 -69
  207. package/src/codec/rabbitmq-codec.test.ts +0 -516
  208. package/src/codec/rabbitmq-codec.ts +0 -336
  209. package/src/core/fanout.test.ts +0 -1350
  210. package/src/core/fanout.ts +0 -184
  211. package/src/core/matador.test.ts +0 -575
  212. package/src/core/matador.ts +0 -357
  213. package/src/core/shutdown.test.ts +0 -853
  214. package/src/core/shutdown.ts +0 -165
  215. package/src/errors/checkpoint-errors.ts +0 -62
  216. package/src/errors/has-description.ts +0 -25
  217. package/src/errors/index.ts +0 -58
  218. package/src/errors/matador-errors.ts +0 -477
  219. package/src/errors/retry-errors.test.ts +0 -175
  220. package/src/errors/retry-errors.ts +0 -188
  221. package/src/hooks/index.ts +0 -14
  222. package/src/hooks/safe-hooks.ts +0 -200
  223. package/src/hooks/types.ts +0 -226
  224. package/src/index.ts +0 -242
  225. package/src/pipeline/pipeline.test.ts +0 -1377
  226. package/src/pipeline/pipeline.ts +0 -393
  227. package/src/retry/policy.ts +0 -46
  228. package/src/retry/standard-policy.test.ts +0 -290
  229. package/src/retry/standard-policy.ts +0 -156
  230. package/src/schema/index.ts +0 -16
  231. package/src/schema/registry.test.ts +0 -339
  232. package/src/schema/registry.ts +0 -229
  233. package/src/schema/types.test.ts +0 -280
  234. package/src/topology/builder.test.ts +0 -451
  235. package/src/topology/builder.ts +0 -238
  236. package/src/topology/index.ts +0 -19
  237. package/src/topology/types.ts +0 -183
  238. package/src/transport/capabilities.ts +0 -88
  239. package/src/transport/connection-manager.ts +0 -218
  240. package/src/transport/index.ts +0 -42
  241. package/src/transport/local/local-transport.test.ts +0 -262
  242. package/src/transport/local/local-transport.ts +0 -330
  243. package/src/transport/multi/multi-transport.test.ts +0 -320
  244. package/src/transport/multi/multi-transport.ts +0 -294
  245. package/src/transport/rabbitmq/rabbitmq-transport.test.ts +0 -120
  246. package/src/transport/rabbitmq/rabbitmq-transport.ts +0 -782
  247. package/src/transport/transport.ts +0 -200
  248. package/src/types/dispatcher.ts +0 -18
  249. package/src/types/envelope.ts +0 -244
  250. package/src/types/event.test.ts +0 -157
  251. package/src/types/event.ts +0 -112
  252. package/src/types/index.ts +0 -62
  253. package/src/types/subscriber.ts +0 -333
  254. package/test/e2e/multi-transport.e2e.test.ts +0 -237
  255. package/test/e2e/rabbitmq-transport.e2e.test.ts +0 -618
  256. package/test/e2e/transport-compliance.e2e.test.ts +0 -506
  257. package/test/integration/matador.integration.test.ts +0 -634
  258. package/tsconfig.json +0 -29
  259. package/tsup.config.ts +0 -13
@@ -0,0 +1,59 @@
1
+ import type { Envelope, SubscriberDefinition } from '../types/index.js';
2
+ import type { Checkpoint, CheckpointHitContext, CheckpointMissContext, CheckpointStore, JsonSerializable, SubscriberContext } from './types.js';
3
+ /**
4
+ * Hooks for observability during context operations.
5
+ */
6
+ export interface ResumableContextHooks {
7
+ onCheckpointHit?(context: CheckpointHitContext): void | Promise<void>;
8
+ onCheckpointMiss?(context: CheckpointMissContext): void | Promise<void>;
9
+ }
10
+ /**
11
+ * Configuration for creating a ResumableContext.
12
+ */
13
+ export interface ResumableContextConfig {
14
+ readonly store: CheckpointStore;
15
+ readonly envelope: Envelope;
16
+ readonly subscriber: SubscriberDefinition;
17
+ readonly existingCheckpoint?: Checkpoint | undefined;
18
+ readonly hooks?: ResumableContextHooks | undefined;
19
+ }
20
+ /**
21
+ * Implementation of SubscriberContext that provides io() caching.
22
+ *
23
+ * On first execution, io() calls execute their lambdas and cache results.
24
+ * On retry (when existingCheckpoint is provided), cached results are returned
25
+ * without re-executing the lambda.
26
+ */
27
+ export declare class ResumableContext implements SubscriberContext {
28
+ private checkpoint;
29
+ private readonly usedKeys;
30
+ private readonly store;
31
+ private readonly envelope;
32
+ private readonly subscriber;
33
+ private readonly hooks;
34
+ constructor(config: ResumableContextConfig);
35
+ get attempt(): number;
36
+ get isRetry(): boolean;
37
+ io<T extends JsonSerializable>(key: string, fn: () => Promise<T> | T): Promise<T>;
38
+ all<T extends readonly [
39
+ string,
40
+ () => Promise<JsonSerializable> | JsonSerializable
41
+ ][]>(ops: T): Promise<{
42
+ [K in keyof T]: T[K] extends [string, () => Promise<infer R> | infer R] ? R : never;
43
+ }>;
44
+ /**
45
+ * Clears the checkpoint from storage.
46
+ * Called after successful completion or dead-letter.
47
+ */
48
+ clear(): Promise<void>;
49
+ /**
50
+ * Gets the current checkpoint state.
51
+ * Useful for debugging and testing.
52
+ */
53
+ getCheckpoint(): Checkpoint;
54
+ /**
55
+ * Gets the number of cached steps.
56
+ */
57
+ get cachedStepCount(): number;
58
+ }
59
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/checkpoint/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,KAAK,EACV,UAAU,EACV,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,eAAe,CAAC,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,gBAAgB,CAAC,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAC;IAC1C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,KAAK,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC;CACpD;AAED;;;;;;GAMG;AACH,qBAAa,gBAAiB,YAAW,iBAAiB;IACxD,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;gBAE9C,MAAM,EAAE,sBAAsB;IAa1C,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAEK,EAAE,CAAC,CAAC,SAAS,gBAAgB,EACjC,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GACvB,OAAO,CAAC,CAAC,CAAC;IA0CP,GAAG,CACP,CAAC,SAAS,SAAS;QACjB,MAAM;QACN,MAAM,OAAO,CAAC,gBAAgB,CAAC,GAAG,gBAAgB;KACnD,EAAE,EAEH,GAAG,EAAE,CAAC,GACL,OAAO,CAAC;SACR,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GACnE,CAAC,GACD,KAAK;KACV,CAAC;IAoEF;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;;OAGG;IACH,aAAa,IAAI,UAAU;IAI3B;;OAEG;IACH,IAAI,eAAe,IAAI,MAAM,CAE5B;CACF"}
@@ -0,0 +1,140 @@
1
+ import { DuplicateIoKeyError } from '../errors/index.js';
2
+ /**
3
+ * Implementation of SubscriberContext that provides io() caching.
4
+ *
5
+ * On first execution, io() calls execute their lambdas and cache results.
6
+ * On retry (when existingCheckpoint is provided), cached results are returned
7
+ * without re-executing the lambda.
8
+ */
9
+ export class ResumableContext {
10
+ checkpoint;
11
+ usedKeys = new Set();
12
+ store;
13
+ envelope;
14
+ subscriber;
15
+ hooks;
16
+ constructor(config) {
17
+ this.store = config.store;
18
+ this.envelope = config.envelope;
19
+ this.subscriber = config.subscriber;
20
+ this.hooks = config.hooks;
21
+ this.checkpoint = config.existingCheckpoint ?? {
22
+ envelopeId: config.envelope.id,
23
+ subscriberName: config.subscriber.name,
24
+ completedSteps: {},
25
+ };
26
+ }
27
+ get attempt() {
28
+ return this.envelope.docket.attempts;
29
+ }
30
+ get isRetry() {
31
+ return this.attempt > 1;
32
+ }
33
+ async io(key, fn) {
34
+ // Validate key uniqueness within this execution
35
+ if (this.usedKeys.has(key)) {
36
+ throw new DuplicateIoKeyError(key, this.subscriber.name);
37
+ }
38
+ this.usedKeys.add(key);
39
+ // Check cache first
40
+ if (key in this.checkpoint.completedSteps) {
41
+ await this.hooks?.onCheckpointHit?.({
42
+ envelope: this.envelope,
43
+ subscriber: this.subscriber,
44
+ stepKey: key,
45
+ });
46
+ return this.checkpoint.completedSteps[key];
47
+ }
48
+ // Notify cache miss
49
+ await this.hooks?.onCheckpointMiss?.({
50
+ envelope: this.envelope,
51
+ subscriber: this.subscriber,
52
+ stepKey: key,
53
+ });
54
+ // Execute the function - errors propagate, no caching on failure
55
+ const result = await fn();
56
+ // Cache the result
57
+ this.checkpoint = {
58
+ ...this.checkpoint,
59
+ completedSteps: {
60
+ ...this.checkpoint.completedSteps,
61
+ [key]: result,
62
+ },
63
+ };
64
+ // Persist checkpoint immediately (incremental persistence)
65
+ await this.store.set(this.envelope.id, this.checkpoint);
66
+ return result;
67
+ }
68
+ async all(ops) {
69
+ // First, check for duplicates within this all() call
70
+ const keysInThisCall = new Set();
71
+ for (const [key] of ops) {
72
+ if (keysInThisCall.has(key)) {
73
+ throw new DuplicateIoKeyError(key, this.subscriber.name);
74
+ }
75
+ keysInThisCall.add(key);
76
+ }
77
+ // Then validate against previously used keys
78
+ for (const [key] of ops) {
79
+ if (this.usedKeys.has(key)) {
80
+ throw new DuplicateIoKeyError(key, this.subscriber.name);
81
+ }
82
+ }
83
+ // Mark all keys as used
84
+ for (const [key] of ops) {
85
+ this.usedKeys.add(key);
86
+ }
87
+ // Execute all operations in parallel
88
+ const results = await Promise.all(ops.map(async ([key, fn]) => {
89
+ // Check cache first
90
+ if (key in this.checkpoint.completedSteps) {
91
+ await this.hooks?.onCheckpointHit?.({
92
+ envelope: this.envelope,
93
+ subscriber: this.subscriber,
94
+ stepKey: key,
95
+ });
96
+ return this.checkpoint.completedSteps[key];
97
+ }
98
+ // Notify cache miss
99
+ await this.hooks?.onCheckpointMiss?.({
100
+ envelope: this.envelope,
101
+ subscriber: this.subscriber,
102
+ stepKey: key,
103
+ });
104
+ // Execute - errors propagate
105
+ const result = await fn();
106
+ // Cache individually (in local state, will persist after Promise.all)
107
+ this.checkpoint = {
108
+ ...this.checkpoint,
109
+ completedSteps: {
110
+ ...this.checkpoint.completedSteps,
111
+ [key]: result,
112
+ },
113
+ };
114
+ return result;
115
+ }));
116
+ // Persist after all parallel operations complete
117
+ await this.store.set(this.envelope.id, this.checkpoint);
118
+ return results;
119
+ }
120
+ /**
121
+ * Clears the checkpoint from storage.
122
+ * Called after successful completion or dead-letter.
123
+ */
124
+ async clear() {
125
+ await this.store.delete(this.envelope.id);
126
+ }
127
+ /**
128
+ * Gets the current checkpoint state.
129
+ * Useful for debugging and testing.
130
+ */
131
+ getCheckpoint() {
132
+ return this.checkpoint;
133
+ }
134
+ /**
135
+ * Gets the number of cached steps.
136
+ */
137
+ get cachedStepCount() {
138
+ return Object.keys(this.checkpoint.completedSteps).length;
139
+ }
140
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=context.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.test.d.ts","sourceRoot":"","sources":["../../src/checkpoint/context.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,424 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'bun:test';
2
+ import { DuplicateIoKeyError } from '../errors/index.js';
3
+ import { ResumableContext } from './context.js';
4
+ import { MemoryCheckpointStore } from './stores/memory.js';
5
+ function createTestEnvelope(id = 'test-envelope-id', attempts = 1) {
6
+ return {
7
+ id,
8
+ data: { userId: '123' },
9
+ docket: {
10
+ eventKey: 'test.event',
11
+ targetSubscriber: 'test-subscriber',
12
+ importance: 'should-investigate',
13
+ attempts,
14
+ createdAt: new Date().toISOString(),
15
+ },
16
+ };
17
+ }
18
+ function createTestSubscriber(name = 'test-subscriber') {
19
+ return {
20
+ name,
21
+ description: 'Test subscriber',
22
+ idempotent: 'resumable',
23
+ importance: 'should-investigate',
24
+ };
25
+ }
26
+ describe('ResumableContext', () => {
27
+ let store;
28
+ let envelope;
29
+ let subscriber;
30
+ beforeEach(() => {
31
+ store = new MemoryCheckpointStore();
32
+ envelope = createTestEnvelope();
33
+ subscriber = createTestSubscriber();
34
+ });
35
+ describe('io()', () => {
36
+ it('should execute function and cache result on first call', async () => {
37
+ const context = new ResumableContext({
38
+ store,
39
+ envelope,
40
+ subscriber,
41
+ });
42
+ let executeCount = 0;
43
+ const result = await context.io('step-1', () => {
44
+ executeCount++;
45
+ return 'result-1';
46
+ });
47
+ expect(result).toBe('result-1');
48
+ expect(executeCount).toBe(1);
49
+ expect(store.size).toBe(1);
50
+ });
51
+ it('should return cached result on retry without re-executing', async () => {
52
+ // First execution - cache the result
53
+ const firstContext = new ResumableContext({
54
+ store,
55
+ envelope,
56
+ subscriber,
57
+ });
58
+ let executeCount = 0;
59
+ await firstContext.io('step-1', () => {
60
+ executeCount++;
61
+ return 'result-1';
62
+ });
63
+ expect(executeCount).toBe(1);
64
+ // Simulate retry - load existing checkpoint
65
+ const checkpoint = await store.get(envelope.id);
66
+ const retryContext = new ResumableContext({
67
+ store,
68
+ envelope: createTestEnvelope(envelope.id, 2),
69
+ subscriber,
70
+ existingCheckpoint: checkpoint,
71
+ });
72
+ const result = await retryContext.io('step-1', () => {
73
+ executeCount++;
74
+ return 'should-not-be-used';
75
+ });
76
+ expect(result).toBe('result-1');
77
+ expect(executeCount).toBe(1); // Still 1, not re-executed
78
+ });
79
+ it('should throw DuplicateIoKeyError on duplicate key within same execution', async () => {
80
+ const context = new ResumableContext({
81
+ store,
82
+ envelope,
83
+ subscriber,
84
+ });
85
+ await context.io('same-key', () => 'first');
86
+ await expect(context.io('same-key', () => 'second')).rejects.toThrow(DuplicateIoKeyError);
87
+ });
88
+ it('should handle async functions', async () => {
89
+ const context = new ResumableContext({
90
+ store,
91
+ envelope,
92
+ subscriber,
93
+ });
94
+ const result = await context.io('async-step', async () => {
95
+ await new Promise((resolve) => setTimeout(resolve, 10));
96
+ return { value: 42 };
97
+ });
98
+ expect(result).toEqual({ value: 42 });
99
+ });
100
+ it('should not cache failed operations', async () => {
101
+ const context = new ResumableContext({
102
+ store,
103
+ envelope,
104
+ subscriber,
105
+ });
106
+ // First attempt - fails
107
+ await expect(context.io('failing-step', () => {
108
+ throw new Error('Operation failed');
109
+ })).rejects.toThrow('Operation failed');
110
+ // Checkpoint should not contain the failed step
111
+ const checkpoint = await store.get(envelope.id);
112
+ expect(checkpoint?.completedSteps['failing-step']).toBeUndefined();
113
+ });
114
+ it('should persist checkpoint after each io() call', async () => {
115
+ const context = new ResumableContext({
116
+ store,
117
+ envelope,
118
+ subscriber,
119
+ });
120
+ await context.io('step-1', () => 'result-1');
121
+ let checkpoint = await store.get(envelope.id);
122
+ expect(Object.keys(checkpoint.completedSteps)).toHaveLength(1);
123
+ await context.io('step-2', () => 'result-2');
124
+ checkpoint = await store.get(envelope.id);
125
+ expect(Object.keys(checkpoint.completedSteps)).toHaveLength(2);
126
+ });
127
+ it('should support various JSON-serializable return types', async () => {
128
+ const context = new ResumableContext({
129
+ store,
130
+ envelope,
131
+ subscriber,
132
+ });
133
+ expect(await context.io('string', () => 'hello')).toBe('hello');
134
+ expect(await context.io('number', () => 42)).toBe(42);
135
+ expect(await context.io('boolean', () => true)).toBe(true);
136
+ expect(await context.io('null', () => null)).toBe(null);
137
+ expect(await context.io('array', () => [1, 2, 3])).toEqual([1, 2, 3]);
138
+ expect(await context.io('object', () => ({ foo: 'bar' }))).toEqual({
139
+ foo: 'bar',
140
+ });
141
+ });
142
+ });
143
+ describe('all()', () => {
144
+ it('should execute multiple operations in parallel', async () => {
145
+ const context = new ResumableContext({
146
+ store,
147
+ envelope,
148
+ subscriber,
149
+ });
150
+ const executionOrder = [];
151
+ const [a, b, c] = await context.all([
152
+ [
153
+ 'fetch-a',
154
+ () => {
155
+ executionOrder.push('a');
156
+ return 'result-a';
157
+ },
158
+ ],
159
+ [
160
+ 'fetch-b',
161
+ () => {
162
+ executionOrder.push('b');
163
+ return 'result-b';
164
+ },
165
+ ],
166
+ [
167
+ 'fetch-c',
168
+ () => {
169
+ executionOrder.push('c');
170
+ return 'result-c';
171
+ },
172
+ ],
173
+ ]);
174
+ expect(a).toBe('result-a');
175
+ expect(b).toBe('result-b');
176
+ expect(c).toBe('result-c');
177
+ expect(executionOrder).toHaveLength(3);
178
+ });
179
+ it('should cache all results on retry', async () => {
180
+ // First execution
181
+ const firstContext = new ResumableContext({
182
+ store,
183
+ envelope,
184
+ subscriber,
185
+ });
186
+ let executeCount = 0;
187
+ await firstContext.all([
188
+ [
189
+ 'fetch-a',
190
+ () => {
191
+ executeCount++;
192
+ return 'result-a';
193
+ },
194
+ ],
195
+ [
196
+ 'fetch-b',
197
+ () => {
198
+ executeCount++;
199
+ return 'result-b';
200
+ },
201
+ ],
202
+ ]);
203
+ expect(executeCount).toBe(2);
204
+ // Retry with cached checkpoint
205
+ const checkpoint = await store.get(envelope.id);
206
+ const retryContext = new ResumableContext({
207
+ store,
208
+ envelope: createTestEnvelope(envelope.id, 2),
209
+ subscriber,
210
+ existingCheckpoint: checkpoint,
211
+ });
212
+ const [a, b] = await retryContext.all([
213
+ [
214
+ 'fetch-a',
215
+ () => {
216
+ executeCount++;
217
+ return 'new-a';
218
+ },
219
+ ],
220
+ [
221
+ 'fetch-b',
222
+ () => {
223
+ executeCount++;
224
+ return 'new-b';
225
+ },
226
+ ],
227
+ ]);
228
+ expect(a).toBe('result-a'); // Cached
229
+ expect(b).toBe('result-b'); // Cached
230
+ expect(executeCount).toBe(2); // Still 2, not re-executed
231
+ });
232
+ it('should throw on duplicate key in all()', async () => {
233
+ const context = new ResumableContext({
234
+ store,
235
+ envelope,
236
+ subscriber,
237
+ });
238
+ await expect(context.all([
239
+ ['same-key', () => 'first'],
240
+ ['same-key', () => 'second'],
241
+ ])).rejects.toThrow(DuplicateIoKeyError);
242
+ });
243
+ it('should throw if all() key conflicts with previous io() key', async () => {
244
+ const context = new ResumableContext({
245
+ store,
246
+ envelope,
247
+ subscriber,
248
+ });
249
+ await context.io('used-key', () => 'first');
250
+ await expect(context.all([
251
+ ['used-key', () => 'second'],
252
+ ['new-key', () => 'third'],
253
+ ])).rejects.toThrow(DuplicateIoKeyError);
254
+ });
255
+ });
256
+ describe('attempt and isRetry', () => {
257
+ it('should report attempt number from envelope', async () => {
258
+ const context = new ResumableContext({
259
+ store,
260
+ envelope: createTestEnvelope('test', 3),
261
+ subscriber,
262
+ });
263
+ expect(context.attempt).toBe(3);
264
+ });
265
+ it('should report isRetry as false for first attempt', async () => {
266
+ const context = new ResumableContext({
267
+ store,
268
+ envelope: createTestEnvelope('test', 1),
269
+ subscriber,
270
+ });
271
+ expect(context.isRetry).toBe(false);
272
+ });
273
+ it('should report isRetry as true for subsequent attempts', async () => {
274
+ const context = new ResumableContext({
275
+ store,
276
+ envelope: createTestEnvelope('test', 2),
277
+ subscriber,
278
+ });
279
+ expect(context.isRetry).toBe(true);
280
+ });
281
+ });
282
+ describe('clear()', () => {
283
+ it('should delete checkpoint from store', async () => {
284
+ const context = new ResumableContext({
285
+ store,
286
+ envelope,
287
+ subscriber,
288
+ });
289
+ await context.io('step-1', () => 'result');
290
+ expect(store.size).toBe(1);
291
+ await context.clear();
292
+ expect(store.size).toBe(0);
293
+ });
294
+ });
295
+ describe('hooks', () => {
296
+ it('should call onCheckpointHit when using cached value', async () => {
297
+ const onCheckpointHit = vi.fn();
298
+ // First execution - populate cache
299
+ const firstContext = new ResumableContext({
300
+ store,
301
+ envelope,
302
+ subscriber,
303
+ });
304
+ await firstContext.io('cached-step', () => 'cached-result');
305
+ // Retry with hooks
306
+ const checkpoint = await store.get(envelope.id);
307
+ const retryContext = new ResumableContext({
308
+ store,
309
+ envelope: createTestEnvelope(envelope.id, 2),
310
+ subscriber,
311
+ existingCheckpoint: checkpoint,
312
+ hooks: { onCheckpointHit },
313
+ });
314
+ await retryContext.io('cached-step', () => 'new-result');
315
+ expect(onCheckpointHit).toHaveBeenCalledWith({
316
+ envelope: expect.anything(),
317
+ subscriber,
318
+ stepKey: 'cached-step',
319
+ });
320
+ });
321
+ it('should call onCheckpointMiss when executing fresh', async () => {
322
+ const onCheckpointMiss = vi.fn();
323
+ const context = new ResumableContext({
324
+ store,
325
+ envelope,
326
+ subscriber,
327
+ hooks: { onCheckpointMiss },
328
+ });
329
+ await context.io('fresh-step', () => 'result');
330
+ expect(onCheckpointMiss).toHaveBeenCalledWith({
331
+ envelope,
332
+ subscriber,
333
+ stepKey: 'fresh-step',
334
+ });
335
+ });
336
+ });
337
+ describe('real-world scenarios', () => {
338
+ it('should replay cached steps and execute new ones on retry', async () => {
339
+ const executionLog = [];
340
+ let shouldFail = true;
341
+ // First attempt - fails after step-1
342
+ const firstContext = new ResumableContext({
343
+ store,
344
+ envelope,
345
+ subscriber,
346
+ });
347
+ try {
348
+ await firstContext.io('step-0', () => {
349
+ executionLog.push('step-0');
350
+ return 'result-0';
351
+ });
352
+ await firstContext.io('step-1', () => {
353
+ executionLog.push('step-1');
354
+ return 'result-1';
355
+ });
356
+ if (shouldFail) {
357
+ shouldFail = false;
358
+ throw new Error('Simulated failure');
359
+ }
360
+ await firstContext.io('step-2', () => {
361
+ executionLog.push('step-2');
362
+ return 'result-2';
363
+ });
364
+ }
365
+ catch {
366
+ // Expected failure
367
+ }
368
+ expect(executionLog).toEqual(['step-0', 'step-1']);
369
+ // Retry - step-0 and step-1 use cache, step-2 executes
370
+ executionLog.length = 0;
371
+ const checkpoint = await store.get(envelope.id);
372
+ const retryContext = new ResumableContext({
373
+ store,
374
+ envelope: createTestEnvelope(envelope.id, 2),
375
+ subscriber,
376
+ existingCheckpoint: checkpoint,
377
+ });
378
+ await retryContext.io('step-0', () => {
379
+ executionLog.push('step-0');
380
+ return 'should-not-use';
381
+ });
382
+ await retryContext.io('step-1', () => {
383
+ executionLog.push('step-1');
384
+ return 'should-not-use';
385
+ });
386
+ await retryContext.io('step-2', () => {
387
+ executionLog.push('step-2');
388
+ return 'result-2';
389
+ });
390
+ expect(executionLog).toEqual(['step-2']); // Only step-2 executed!
391
+ });
392
+ it('should handle conditional io() calls correctly', async () => {
393
+ const context = new ResumableContext({
394
+ store,
395
+ envelope: { ...envelope, data: { sendEmail: true } },
396
+ subscriber,
397
+ });
398
+ const result = await context.io('send-email', () => ({
399
+ messageId: 'msg-123',
400
+ }));
401
+ expect(result).toEqual({ messageId: 'msg-123' });
402
+ });
403
+ it('should handle dynamic loop with unique keys', async () => {
404
+ const context = new ResumableContext({
405
+ store,
406
+ envelope,
407
+ subscriber,
408
+ });
409
+ const items = [
410
+ { id: 'item-1', value: 10 },
411
+ { id: 'item-2', value: 20 },
412
+ { id: 'item-3', value: 30 },
413
+ ];
414
+ const results = [];
415
+ for (const item of items) {
416
+ const processed = await context.io(`process-${item.id}`, () => {
417
+ return item.value * 2;
418
+ });
419
+ results.push(processed);
420
+ }
421
+ expect(results).toEqual([20, 40, 60]);
422
+ });
423
+ });
424
+ });
@@ -0,0 +1,7 @@
1
+ export type { Checkpoint, CheckpointClearedContext, CheckpointHitContext, CheckpointLoadedContext, CheckpointMissContext, CheckpointStore, JsonSerializable, SubscriberContext, } from './types.js';
2
+ export type { ResumableContextConfig, ResumableContextHooks, } from './context.js';
3
+ export { ResumableContext } from './context.js';
4
+ export { MemoryCheckpointStore } from './stores/memory.js';
5
+ export { NoOpCheckpointStore } from './stores/noop.js';
6
+ export { CheckpointStoreError, DuplicateIoKeyError, isCheckpointStoreError, isDuplicateIoKeyError, } from '../errors/index.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/checkpoint/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,UAAU,EACV,wBAAwB,EACxB,oBAAoB,EACpB,uBAAuB,EACvB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { ResumableContext } from './context.js';
2
+ // Stores
3
+ export { MemoryCheckpointStore } from './stores/memory.js';
4
+ export { NoOpCheckpointStore } from './stores/noop.js';
5
+ // Errors (re-exported from ../errors for convenience)
6
+ export { CheckpointStoreError, DuplicateIoKeyError, isCheckpointStoreError, isDuplicateIoKeyError, } from '../errors/index.js';
@@ -0,0 +1,29 @@
1
+ import type { Checkpoint, CheckpointStore } from '../types.js';
2
+ /**
3
+ * In-memory checkpoint store for testing and development.
4
+ * Checkpoints are lost when the process restarts.
5
+ *
6
+ * For production use, implement a persistent store (e.g., RedisCheckpointStore).
7
+ */
8
+ export declare class MemoryCheckpointStore implements CheckpointStore {
9
+ private readonly checkpoints;
10
+ get(envelopeId: string): Promise<Checkpoint | undefined>;
11
+ set(envelopeId: string, checkpoint: Checkpoint): Promise<void>;
12
+ delete(envelopeId: string): Promise<void>;
13
+ /**
14
+ * Gets the number of stored checkpoints.
15
+ * Useful for testing.
16
+ */
17
+ get size(): number;
18
+ /**
19
+ * Clears all stored checkpoints.
20
+ * Useful for testing.
21
+ */
22
+ clear(): void;
23
+ /**
24
+ * Gets all stored checkpoint IDs.
25
+ * Useful for testing.
26
+ */
27
+ keys(): string[];
28
+ }
29
+ //# sourceMappingURL=memory.d.ts.map