@zdavison/matador 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/index.cjs +48 -9
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/pipeline/pipeline.d.ts.map +1 -1
  5. package/dist/pipeline/pipeline.js +5 -1
  6. package/dist/retry/policy.d.ts +2 -0
  7. package/dist/retry/policy.d.ts.map +1 -1
  8. package/dist/retry/standard-policy.d.ts +16 -3
  9. package/dist/retry/standard-policy.d.ts.map +1 -1
  10. package/dist/retry/standard-policy.js +36 -9
  11. package/package.json +1 -1
  12. package/dist/checkpoint/context.test.d.ts +0 -2
  13. package/dist/checkpoint/context.test.d.ts.map +0 -1
  14. package/dist/checkpoint/context.test.js +0 -428
  15. package/dist/checkpoint/stores/stores.test.d.ts +0 -2
  16. package/dist/checkpoint/stores/stores.test.d.ts.map +0 -1
  17. package/dist/checkpoint/stores/stores.test.js +0 -146
  18. package/dist/codec/rabbitmq-codec.test.d.ts +0 -2
  19. package/dist/codec/rabbitmq-codec.test.d.ts.map +0 -1
  20. package/dist/codec/rabbitmq-codec.test.js +0 -428
  21. package/dist/core/fanout.test.d.ts +0 -2
  22. package/dist/core/fanout.test.d.ts.map +0 -1
  23. package/dist/core/fanout.test.js +0 -1970
  24. package/dist/core/matador.test.d.ts +0 -2
  25. package/dist/core/matador.test.d.ts.map +0 -1
  26. package/dist/core/matador.test.js +0 -906
  27. package/dist/core/shutdown.test.d.ts +0 -2
  28. package/dist/core/shutdown.test.d.ts.map +0 -1
  29. package/dist/core/shutdown.test.js +0 -599
  30. package/dist/errors/retry-errors.test.d.ts +0 -2
  31. package/dist/errors/retry-errors.test.d.ts.map +0 -1
  32. package/dist/errors/retry-errors.test.js +0 -136
  33. package/dist/pipeline/pipeline.test.d.ts +0 -2
  34. package/dist/pipeline/pipeline.test.d.ts.map +0 -1
  35. package/dist/pipeline/pipeline.test.js +0 -1247
  36. package/dist/retry/standard-policy.test.d.ts +0 -2
  37. package/dist/retry/standard-policy.test.d.ts.map +0 -1
  38. package/dist/retry/standard-policy.test.js +0 -261
  39. package/dist/schema/registry.test.d.ts +0 -2
  40. package/dist/schema/registry.test.d.ts.map +0 -1
  41. package/dist/schema/registry.test.js +0 -278
  42. package/dist/schema/types.test.d.ts +0 -2
  43. package/dist/schema/types.test.d.ts.map +0 -1
  44. package/dist/schema/types.test.js +0 -246
  45. package/dist/topology/builder.test.d.ts +0 -2
  46. package/dist/topology/builder.test.d.ts.map +0 -1
  47. package/dist/topology/builder.test.js +0 -554
  48. package/dist/transport/local/local-transport.test.d.ts +0 -2
  49. package/dist/transport/local/local-transport.test.d.ts.map +0 -1
  50. package/dist/transport/local/local-transport.test.js +0 -232
  51. package/dist/transport/multi/multi-transport.test.d.ts +0 -2
  52. package/dist/transport/multi/multi-transport.test.d.ts.map +0 -1
  53. package/dist/transport/multi/multi-transport.test.js +0 -322
  54. package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.d.ts +0 -2
  55. package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.d.ts.map +0 -1
  56. package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.js +0 -149
  57. package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts +0 -2
  58. package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts.map +0 -1
  59. package/dist/transport/rabbitmq/rabbitmq-transport.test.js +0 -258
  60. package/dist/types/event.test.d.ts +0 -2
  61. package/dist/types/event.test.d.ts.map +0 -1
  62. package/dist/types/event.test.js +0 -130
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/pipeline/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE1E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAM/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,KAAK,EAEV,UAAU,EACV,QAAQ,EAER,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAG3B;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IACvD,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,UAAU,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;gBAE5B,MAAM,EAAE,cAAc;IAUlC;;OAEG;IACG,OAAO,CACX,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC;IAoNzB;;;;;;;;;;;OAWG;YACW,mBAAmB;IAiCjC;;;;;;;;;;;OAWG;YACW,aAAa;YA4Cb,mBAAmB;YAqCnB,gBAAgB;IAkB9B;;;OAGG;IACH,OAAO,CAAC,yBAAyB;CA+BlC"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/pipeline/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE1E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAM/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,KAAK,EAEV,UAAU,EACV,QAAQ,EAER,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAG3B;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IACvD,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,UAAU,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;gBAE5B,MAAM,EAAE,cAAc;IAUlC;;OAEG;IACG,OAAO,CACX,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC;IAoNzB;;;;;;;;;;;OAWG;YACW,mBAAmB;IAqCjC;;;;;;;;;;;OAWG;YACW,aAAa;YA4Cb,mBAAmB;YAqCnB,gBAAgB;IAkB9B;;;OAGG;IACH,OAAO,CAAC,yBAAyB;CA+BlC"}
@@ -204,7 +204,11 @@ export class ProcessingPipeline {
204
204
  * @returns A `ProcessResult` indicating if we should continue with processing the message, or fail it preemptively
205
205
  */
206
206
  async handleShouldProcess(envelope, subscriberDef, isResumable, receipt, startTime) {
207
- const decision = this.retryPolicy.shouldProcess({ envelope, receipt });
207
+ const decision = this.retryPolicy.shouldProcess({
208
+ envelope,
209
+ subscriber: subscriberDef,
210
+ receipt,
211
+ });
208
212
  // If shouldProcess passes, we can proceed with normal processing
209
213
  if (decision.action === 'process') {
210
214
  return {
@@ -42,6 +42,8 @@ export type ProcessDecision = {
42
42
  export interface ProcessContext {
43
43
  /** The message envelope */
44
44
  readonly envelope: Envelope;
45
+ /** The subscriber definition */
46
+ readonly subscriber: SubscriberDefinition;
45
47
  /** Message receipt with delivery information */
46
48
  readonly receipt: MessageReceipt;
47
49
  }
@@ -1 +1 @@
1
- {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/retry/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAE5B,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAEtB,gCAAgC;IAChC,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAC;IAE1C,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpD;IACE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB,OAAO,CAAC,aAAa,EAAE;IAAE,MAAM,EAAE,aAAa,GAAG,SAAS,CAAA;CAAE,CAAC,CAAC;AAElE;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAE5B,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,eAAe,CAAC;IAExD;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,aAAa,CAAC;IAElD;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAAC;CACzC"}
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/retry/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAE5B,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAEtB,gCAAgC;IAChC,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAC;IAE1C,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpD;IACE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB,OAAO,CAAC,aAAa,EAAE;IAAE,MAAM,EAAE,aAAa,GAAG,SAAS,CAAA;CAAE,CAAC,CAAC;AAElE;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAE5B,gCAAgC;IAChC,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAC;IAE1C,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,eAAe,CAAC;IAExD;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,aAAa,CAAC;IAElD;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAAC;CACzC"}
@@ -32,20 +32,23 @@ export declare const defaultRetryConfig: StandardRetryPolicyConfig;
32
32
  * 3. DontRetry → dead-letter (explicit no-retry)
33
33
  * 4. DoRetry → retry if under max attempts
34
34
  * 5. Max attempts exceeded → dead-letter
35
- * 6. Non-idempotent subscriber on redelivery → dead-letter
35
+ * 6. Non-idempotent subscriber → dead-letter
36
36
  * 7. Default → retry with exponential backoff
37
37
  */
38
38
  export declare class StandardRetryPolicy implements RetryPolicy {
39
39
  private readonly config;
40
40
  constructor(config?: Partial<StandardRetryPolicyConfig>);
41
41
  /**
42
- * Check if the message should be dead-lettered before the subscriber callback is invoked
42
+ * Check if the message should be dead-lettered before the subscriber callback is invoked.
43
+ * This is needed when a subscriber crashes and the `shouldRetry` policy is never executed.
43
44
  *
44
45
  * Uses the same delivery-count threshold as `shouldRetry` poison check, so an
45
46
  * already-poisoned message is dead-lettered without ever running the callback again.
46
47
  *
48
+ * Checks the subscriber idempotency setting to prevent processing when a previous attempt crashed.
49
+ *
47
50
  * @param context - The shouldProcess context.
48
- * @returns A 'process' decision if the message is not poisoned; a dead-letter decision otherwise.
51
+ * @returns A 'process' decision if subscriber processing should be attempted; a dead-letter decision otherwise.
49
52
  */
50
53
  shouldProcess(context: ProcessContext): ProcessDecision;
51
54
  shouldRetry(context: RetryContext): RetryDecision;
@@ -58,5 +61,15 @@ export declare class StandardRetryPolicy implements RetryPolicy {
58
61
  * @returns A MessageMaybePoisonedError if the message is poisoned; null otherwise
59
62
  */
60
63
  private checkPoisoned;
64
+ /**
65
+ * Checks whether the subscriber allows retries based on its idempotency setting
66
+ *
67
+ * @param envelope - The message envelope.
68
+ * @param receipt - The message receipt.
69
+ * @param subscriber - The subscriber definition
70
+ * @param error - The processing error, if it exists
71
+ * @returns A IdempotentMessageCannotRetryError if the message is poisoned; null otherwise
72
+ */
73
+ private checkIdempotency;
61
74
  }
62
75
  //# sourceMappingURL=standard-policy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"standard-policy.d.ts","sourceRoot":"","sources":["../../src/retry/standard-policy.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACZ,aAAa,EACb,WAAW,EACZ,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,uDAAuD;IACvD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,yCAAyC;IACzC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IAEnC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,yBAMhC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAoB,YAAW,WAAW;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;gBAEvC,MAAM,GAAE,OAAO,CAAC,yBAAyB,CAAM;IAI3D;;;;;;;;OAQG;IACH,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,eAAe;IAiBvD,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,aAAa;IAkFjD,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM;IAOvC;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;CAatB"}
1
+ {"version":3,"file":"standard-policy.d.ts","sourceRoot":"","sources":["../../src/retry/standard-policy.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACZ,aAAa,EACb,WAAW,EACZ,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,uDAAuD;IACvD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,yCAAyC;IACzC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IAEnC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,yBAMhC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAoB,YAAW,WAAW;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;gBAEvC,MAAM,GAAE,OAAO,CAAC,yBAAyB,CAAM;IAI3D;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,eAAe;IAgCvD,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,aAAa;IAiFjD,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM;IAOvC;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAcrB;;;;;;;;OAQG;IACH,OAAO,CAAC,gBAAgB;CAkBzB"}
@@ -18,7 +18,7 @@ export const defaultRetryConfig = {
18
18
  * 3. DontRetry → dead-letter (explicit no-retry)
19
19
  * 4. DoRetry → retry if under max attempts
20
20
  * 5. Max attempts exceeded → dead-letter
21
- * 6. Non-idempotent subscriber on redelivery → dead-letter
21
+ * 6. Non-idempotent subscriber → dead-letter
22
22
  * 7. Default → retry with exponential backoff
23
23
  */
24
24
  export class StandardRetryPolicy {
@@ -27,13 +27,16 @@ export class StandardRetryPolicy {
27
27
  this.config = { ...defaultRetryConfig, ...config };
28
28
  }
29
29
  /**
30
- * Check if the message should be dead-lettered before the subscriber callback is invoked
30
+ * Check if the message should be dead-lettered before the subscriber callback is invoked.
31
+ * This is needed when a subscriber crashes and the `shouldRetry` policy is never executed.
31
32
  *
32
33
  * Uses the same delivery-count threshold as `shouldRetry` poison check, so an
33
34
  * already-poisoned message is dead-lettered without ever running the callback again.
34
35
  *
36
+ * Checks the subscriber idempotency setting to prevent processing when a previous attempt crashed.
37
+ *
35
38
  * @param context - The shouldProcess context.
36
- * @returns A 'process' decision if the message is not poisoned; a dead-letter decision otherwise.
39
+ * @returns A 'process' decision if subscriber processing should be attempted; a dead-letter decision otherwise.
37
40
  */
38
41
  shouldProcess(context) {
39
42
  // Poison message detection - prevent crash loops
@@ -46,6 +49,15 @@ export class StandardRetryPolicy {
46
49
  reason: poisonError.message,
47
50
  };
48
51
  }
52
+ // Non-idempotent subscriber on redelivery
53
+ const idempotencyError = this.checkIdempotency(context.envelope, context.receipt, context.subscriber);
54
+ if (idempotencyError) {
55
+ return {
56
+ action: 'dead-letter',
57
+ queue: 'undeliverable',
58
+ reason: idempotencyError.message,
59
+ };
60
+ }
49
61
  // Allow processing to continue
50
62
  return { action: 'process' };
51
63
  }
@@ -100,15 +112,13 @@ export class StandardRetryPolicy {
100
112
  reason: `max attempts exceeded (${this.config.maxAttempts})`,
101
113
  };
102
114
  }
103
- // 6. Non-idempotent subscriber on redelivery
104
- // 'no' and 'unknown' are treated the same (safer default)
105
- if (receipt.redelivered &&
106
- (subscriber.idempotent === 'no' || subscriber.idempotent === 'unknown')) {
107
- const idempotentError = new IdempotentMessageCannotRetryError(envelope.id, subscriber.name);
115
+ // 6. Non-idempotent subscriber
116
+ const idempotencyError = this.checkIdempotency(envelope, receipt, subscriber, error);
117
+ if (idempotencyError) {
108
118
  return {
109
119
  action: 'dead-letter',
110
120
  queue: 'undeliverable',
111
- reason: idempotentError.message,
121
+ reason: idempotencyError.message,
112
122
  };
113
123
  }
114
124
  // 7. Default: retry with backoff
@@ -135,4 +145,21 @@ export class StandardRetryPolicy {
135
145
  }
136
146
  return null;
137
147
  }
148
+ /**
149
+ * Checks whether the subscriber allows retries based on its idempotency setting
150
+ *
151
+ * @param envelope - The message envelope.
152
+ * @param receipt - The message receipt.
153
+ * @param subscriber - The subscriber definition
154
+ * @param error - The processing error, if it exists
155
+ * @returns A IdempotentMessageCannotRetryError if the message is poisoned; null otherwise
156
+ */
157
+ checkIdempotency(envelope, receipt, subscriber, error) {
158
+ // 'no' and 'unknown' are treated the same (safer default)
159
+ if ((error || receipt.redelivered) &&
160
+ (subscriber.idempotent === 'no' || subscriber.idempotent === 'unknown')) {
161
+ return new IdempotentMessageCannotRetryError(envelope.id, subscriber.name);
162
+ }
163
+ return null;
164
+ }
138
165
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zdavison/matador",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "A batteries-included event system.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=context.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"context.test.d.ts","sourceRoot":"","sources":["../../src/checkpoint/context.test.ts"],"names":[],"mappings":""}
@@ -1,428 +0,0 @@
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
- if (!checkpoint)
123
- throw new Error('expected checkpoint after step-1');
124
- expect(Object.keys(checkpoint.completedSteps)).toHaveLength(1);
125
- await context.io('step-2', () => 'result-2');
126
- checkpoint = await store.get(envelope.id);
127
- if (!checkpoint)
128
- throw new Error('expected checkpoint after step-2');
129
- expect(Object.keys(checkpoint.completedSteps)).toHaveLength(2);
130
- });
131
- it('should support various JSON-serializable return types', async () => {
132
- const context = new ResumableContext({
133
- store,
134
- envelope,
135
- subscriber,
136
- });
137
- expect(await context.io('string', () => 'hello')).toBe('hello');
138
- expect(await context.io('number', () => 42)).toBe(42);
139
- expect(await context.io('boolean', () => true)).toBe(true);
140
- expect(await context.io('null', () => null)).toBe(null);
141
- expect(await context.io('array', () => [1, 2, 3])).toEqual([1, 2, 3]);
142
- expect(await context.io('object', () => ({ foo: 'bar' }))).toEqual({
143
- foo: 'bar',
144
- });
145
- });
146
- });
147
- describe('all()', () => {
148
- it('should execute multiple operations in parallel', async () => {
149
- const context = new ResumableContext({
150
- store,
151
- envelope,
152
- subscriber,
153
- });
154
- const executionOrder = [];
155
- const [a, b, c] = await context.all([
156
- [
157
- 'fetch-a',
158
- () => {
159
- executionOrder.push('a');
160
- return 'result-a';
161
- },
162
- ],
163
- [
164
- 'fetch-b',
165
- () => {
166
- executionOrder.push('b');
167
- return 'result-b';
168
- },
169
- ],
170
- [
171
- 'fetch-c',
172
- () => {
173
- executionOrder.push('c');
174
- return 'result-c';
175
- },
176
- ],
177
- ]);
178
- expect(a).toBe('result-a');
179
- expect(b).toBe('result-b');
180
- expect(c).toBe('result-c');
181
- expect(executionOrder).toHaveLength(3);
182
- });
183
- it('should cache all results on retry', async () => {
184
- // First execution
185
- const firstContext = new ResumableContext({
186
- store,
187
- envelope,
188
- subscriber,
189
- });
190
- let executeCount = 0;
191
- await firstContext.all([
192
- [
193
- 'fetch-a',
194
- () => {
195
- executeCount++;
196
- return 'result-a';
197
- },
198
- ],
199
- [
200
- 'fetch-b',
201
- () => {
202
- executeCount++;
203
- return 'result-b';
204
- },
205
- ],
206
- ]);
207
- expect(executeCount).toBe(2);
208
- // Retry with cached checkpoint
209
- const checkpoint = await store.get(envelope.id);
210
- const retryContext = new ResumableContext({
211
- store,
212
- envelope: createTestEnvelope(envelope.id, 2),
213
- subscriber,
214
- existingCheckpoint: checkpoint,
215
- });
216
- const [a, b] = await retryContext.all([
217
- [
218
- 'fetch-a',
219
- () => {
220
- executeCount++;
221
- return 'new-a';
222
- },
223
- ],
224
- [
225
- 'fetch-b',
226
- () => {
227
- executeCount++;
228
- return 'new-b';
229
- },
230
- ],
231
- ]);
232
- expect(a).toBe('result-a'); // Cached
233
- expect(b).toBe('result-b'); // Cached
234
- expect(executeCount).toBe(2); // Still 2, not re-executed
235
- });
236
- it('should throw on duplicate key in all()', async () => {
237
- const context = new ResumableContext({
238
- store,
239
- envelope,
240
- subscriber,
241
- });
242
- await expect(context.all([
243
- ['same-key', () => 'first'],
244
- ['same-key', () => 'second'],
245
- ])).rejects.toThrow(DuplicateIoKeyError);
246
- });
247
- it('should throw if all() key conflicts with previous io() key', async () => {
248
- const context = new ResumableContext({
249
- store,
250
- envelope,
251
- subscriber,
252
- });
253
- await context.io('used-key', () => 'first');
254
- await expect(context.all([
255
- ['used-key', () => 'second'],
256
- ['new-key', () => 'third'],
257
- ])).rejects.toThrow(DuplicateIoKeyError);
258
- });
259
- });
260
- describe('attempt and isRetry', () => {
261
- it('should report attempt number from envelope', async () => {
262
- const context = new ResumableContext({
263
- store,
264
- envelope: createTestEnvelope('test', 3),
265
- subscriber,
266
- });
267
- expect(context.attempt).toBe(3);
268
- });
269
- it('should report isRetry as false for first attempt', async () => {
270
- const context = new ResumableContext({
271
- store,
272
- envelope: createTestEnvelope('test', 1),
273
- subscriber,
274
- });
275
- expect(context.isRetry).toBe(false);
276
- });
277
- it('should report isRetry as true for subsequent attempts', async () => {
278
- const context = new ResumableContext({
279
- store,
280
- envelope: createTestEnvelope('test', 2),
281
- subscriber,
282
- });
283
- expect(context.isRetry).toBe(true);
284
- });
285
- });
286
- describe('clear()', () => {
287
- it('should delete checkpoint from store', async () => {
288
- const context = new ResumableContext({
289
- store,
290
- envelope,
291
- subscriber,
292
- });
293
- await context.io('step-1', () => 'result');
294
- expect(store.size).toBe(1);
295
- await context.clear();
296
- expect(store.size).toBe(0);
297
- });
298
- });
299
- describe('hooks', () => {
300
- it('should call onCheckpointHit when using cached value', async () => {
301
- const onCheckpointHit = vi.fn();
302
- // First execution - populate cache
303
- const firstContext = new ResumableContext({
304
- store,
305
- envelope,
306
- subscriber,
307
- });
308
- await firstContext.io('cached-step', () => 'cached-result');
309
- // Retry with hooks
310
- const checkpoint = await store.get(envelope.id);
311
- const retryContext = new ResumableContext({
312
- store,
313
- envelope: createTestEnvelope(envelope.id, 2),
314
- subscriber,
315
- existingCheckpoint: checkpoint,
316
- hooks: { onCheckpointHit },
317
- });
318
- await retryContext.io('cached-step', () => 'new-result');
319
- expect(onCheckpointHit).toHaveBeenCalledWith({
320
- envelope: expect.anything(),
321
- subscriber,
322
- stepKey: 'cached-step',
323
- });
324
- });
325
- it('should call onCheckpointMiss when executing fresh', async () => {
326
- const onCheckpointMiss = vi.fn();
327
- const context = new ResumableContext({
328
- store,
329
- envelope,
330
- subscriber,
331
- hooks: { onCheckpointMiss },
332
- });
333
- await context.io('fresh-step', () => 'result');
334
- expect(onCheckpointMiss).toHaveBeenCalledWith({
335
- envelope,
336
- subscriber,
337
- stepKey: 'fresh-step',
338
- });
339
- });
340
- });
341
- describe('real-world scenarios', () => {
342
- it('should replay cached steps and execute new ones on retry', async () => {
343
- const executionLog = [];
344
- let shouldFail = true;
345
- // First attempt - fails after step-1
346
- const firstContext = new ResumableContext({
347
- store,
348
- envelope,
349
- subscriber,
350
- });
351
- try {
352
- await firstContext.io('step-0', () => {
353
- executionLog.push('step-0');
354
- return 'result-0';
355
- });
356
- await firstContext.io('step-1', () => {
357
- executionLog.push('step-1');
358
- return 'result-1';
359
- });
360
- if (shouldFail) {
361
- shouldFail = false;
362
- throw new Error('Simulated failure');
363
- }
364
- await firstContext.io('step-2', () => {
365
- executionLog.push('step-2');
366
- return 'result-2';
367
- });
368
- }
369
- catch {
370
- // Expected failure
371
- }
372
- expect(executionLog).toEqual(['step-0', 'step-1']);
373
- // Retry - step-0 and step-1 use cache, step-2 executes
374
- executionLog.length = 0;
375
- const checkpoint = await store.get(envelope.id);
376
- const retryContext = new ResumableContext({
377
- store,
378
- envelope: createTestEnvelope(envelope.id, 2),
379
- subscriber,
380
- existingCheckpoint: checkpoint,
381
- });
382
- await retryContext.io('step-0', () => {
383
- executionLog.push('step-0');
384
- return 'should-not-use';
385
- });
386
- await retryContext.io('step-1', () => {
387
- executionLog.push('step-1');
388
- return 'should-not-use';
389
- });
390
- await retryContext.io('step-2', () => {
391
- executionLog.push('step-2');
392
- return 'result-2';
393
- });
394
- expect(executionLog).toEqual(['step-2']); // Only step-2 executed!
395
- });
396
- it('should handle conditional io() calls correctly', async () => {
397
- const context = new ResumableContext({
398
- store,
399
- envelope: { ...envelope, data: { sendEmail: true } },
400
- subscriber,
401
- });
402
- const result = await context.io('send-email', () => ({
403
- messageId: 'msg-123',
404
- }));
405
- expect(result).toEqual({ messageId: 'msg-123' });
406
- });
407
- it('should handle dynamic loop with unique keys', async () => {
408
- const context = new ResumableContext({
409
- store,
410
- envelope,
411
- subscriber,
412
- });
413
- const items = [
414
- { id: 'item-1', value: 10 },
415
- { id: 'item-2', value: 20 },
416
- { id: 'item-3', value: 30 },
417
- ];
418
- const results = [];
419
- for (const item of items) {
420
- const processed = await context.io(`process-${item.id}`, () => {
421
- return item.value * 2;
422
- });
423
- results.push(processed);
424
- }
425
- expect(results).toEqual([20, 40, 60]);
426
- });
427
- });
428
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=stores.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"stores.test.d.ts","sourceRoot":"","sources":["../../../src/checkpoint/stores/stores.test.ts"],"names":[],"mappings":""}