@zdavison/matador 2.0.8 → 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 (77) hide show
  1. package/dist/index.d.cts +1 -1
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/package.json +6 -2
  5. package/examples/config.ts +0 -126
  6. package/examples/event.ts +0 -26
  7. package/examples/order-event.json +0 -19
  8. package/src/checkpoint/context.test.ts +0 -510
  9. package/src/checkpoint/context.ts +0 -213
  10. package/src/checkpoint/index.ts +0 -30
  11. package/src/checkpoint/stores/memory.ts +0 -47
  12. package/src/checkpoint/stores/noop.ts +0 -22
  13. package/src/checkpoint/stores/stores.test.ts +0 -177
  14. package/src/checkpoint/types.ts +0 -147
  15. package/src/codec/codec.ts +0 -42
  16. package/src/codec/header-aware-codec.ts +0 -41
  17. package/src/codec/index.ts +0 -11
  18. package/src/codec/json-codec.ts +0 -69
  19. package/src/codec/rabbitmq-codec.test.ts +0 -516
  20. package/src/codec/rabbitmq-codec.ts +0 -336
  21. package/src/core/fanout.test.ts +0 -1350
  22. package/src/core/fanout.ts +0 -184
  23. package/src/core/index.ts +0 -12
  24. package/src/core/matador.test.ts +0 -575
  25. package/src/core/matador.ts +0 -357
  26. package/src/core/shutdown.test.ts +0 -853
  27. package/src/core/shutdown.ts +0 -165
  28. package/src/errors/checkpoint-errors.ts +0 -62
  29. package/src/errors/has-description.ts +0 -25
  30. package/src/errors/index.ts +0 -58
  31. package/src/errors/matador-errors.ts +0 -477
  32. package/src/errors/retry-errors.test.ts +0 -175
  33. package/src/errors/retry-errors.ts +0 -188
  34. package/src/hooks/index.ts +0 -14
  35. package/src/hooks/safe-hooks.ts +0 -200
  36. package/src/hooks/types.ts +0 -226
  37. package/src/index.ts +0 -241
  38. package/src/pipeline/index.ts +0 -2
  39. package/src/pipeline/pipeline.test.ts +0 -1377
  40. package/src/pipeline/pipeline.ts +0 -393
  41. package/src/retry/index.ts +0 -4
  42. package/src/retry/policy.ts +0 -46
  43. package/src/retry/standard-policy.test.ts +0 -290
  44. package/src/retry/standard-policy.ts +0 -156
  45. package/src/schema/index.ts +0 -16
  46. package/src/schema/registry.test.ts +0 -339
  47. package/src/schema/registry.ts +0 -229
  48. package/src/schema/types.test.ts +0 -280
  49. package/src/schema/types.ts +0 -217
  50. package/src/topology/builder.test.ts +0 -451
  51. package/src/topology/builder.ts +0 -238
  52. package/src/topology/index.ts +0 -19
  53. package/src/topology/types.ts +0 -183
  54. package/src/transport/capabilities.ts +0 -88
  55. package/src/transport/connection-manager.ts +0 -218
  56. package/src/transport/index.ts +0 -42
  57. package/src/transport/local/local-transport.test.ts +0 -262
  58. package/src/transport/local/local-transport.ts +0 -330
  59. package/src/transport/multi/multi-transport.test.ts +0 -320
  60. package/src/transport/multi/multi-transport.ts +0 -294
  61. package/src/transport/rabbitmq/rabbitmq-transport.test.ts +0 -120
  62. package/src/transport/rabbitmq/rabbitmq-transport.ts +0 -782
  63. package/src/transport/transport.ts +0 -200
  64. package/src/types/common.ts +0 -53
  65. package/src/types/dispatcher.ts +0 -18
  66. package/src/types/envelope.ts +0 -244
  67. package/src/types/event.test.ts +0 -157
  68. package/src/types/event.ts +0 -112
  69. package/src/types/index.ts +0 -62
  70. package/src/types/subscriber.ts +0 -333
  71. package/test/e2e/multi-transport.e2e.test.ts +0 -237
  72. package/test/e2e/rabbitmq-transport.e2e.test.ts +0 -618
  73. package/test/e2e/transport-compliance.e2e.test.ts +0 -506
  74. package/test/integration/matador.integration.test.ts +0 -634
  75. package/tsconfig.json +0 -29
  76. package/tsconfig.tsbuildinfo +0 -1
  77. package/tsup.config.ts +0 -13
@@ -1,290 +0,0 @@
1
- import { describe, expect, it } from 'bun:test';
2
- import {
3
- DoRetry,
4
- DontRetry,
5
- EventAssertionError,
6
- } from '../errors/retry-errors.js';
7
- import type { MessageReceipt } from '../transport/transport.js';
8
- import { createEnvelope } from '../types/envelope.js';
9
- import type { SubscriberDefinition } from '../types/subscriber.js';
10
- import type { RetryContext, RetryDecision } from './policy.js';
11
- import { StandardRetryPolicy } from './standard-policy.js';
12
-
13
- describe('StandardRetryPolicy', () => {
14
- describe('shouldRetry', () => {
15
- it('should dead-letter on EventAssertionError', () => {
16
- const policy = new StandardRetryPolicy();
17
- const envelope = createEnvelope({
18
- eventKey: 'test.event',
19
- targetSubscriber: 'test-subscriber',
20
- data: { test: 'data' },
21
- importance: 'should-investigate',
22
- });
23
- const context = createContext(
24
- new EventAssertionError(envelope, 'Invalid event'),
25
- );
26
-
27
- const decision = policy.shouldRetry(context);
28
-
29
- expect(decision.action).toBe('dead-letter');
30
- assertDeadLetter(decision);
31
- expect(decision.queue).toBe('undeliverable');
32
- expect(decision.reason).toContain('assertion error');
33
- });
34
-
35
- it('should dead-letter on DontRetry error', () => {
36
- const policy = new StandardRetryPolicy();
37
- const context = createContext(new DontRetry('Business rule violation'));
38
-
39
- const decision = policy.shouldRetry(context);
40
-
41
- expect(decision.action).toBe('dead-letter');
42
- assertDeadLetter(decision);
43
- expect(decision.queue).toBe('undeliverable');
44
- expect(decision.reason).toContain('Business rule violation');
45
- });
46
-
47
- it('should retry on DoRetry error if under max attempts', () => {
48
- const policy = new StandardRetryPolicy({ maxAttempts: 3 });
49
- const context = createContext(new DoRetry('Temporary failure'), {
50
- attemptNumber: 1,
51
- });
52
-
53
- const decision = policy.shouldRetry(context);
54
-
55
- expect(decision.action).toBe('retry');
56
- assertRetry(decision);
57
- expect(decision.delay).toBeGreaterThan(0);
58
- });
59
-
60
- it('should dead-letter on DoRetry error if max attempts exceeded', () => {
61
- const policy = new StandardRetryPolicy({ maxAttempts: 3 });
62
- const context = createContext(new DoRetry('Temporary failure'), {
63
- attemptNumber: 3,
64
- });
65
-
66
- const decision = policy.shouldRetry(context);
67
-
68
- expect(decision.action).toBe('dead-letter');
69
- assertDeadLetter(decision);
70
- expect(decision.reason).toContain('max attempts exceeded');
71
- });
72
-
73
- it('should dead-letter when max attempts exceeded', () => {
74
- const policy = new StandardRetryPolicy({ maxAttempts: 3 });
75
- const context = createContext(new Error('Generic error'), {
76
- attemptNumber: 3,
77
- });
78
-
79
- const decision = policy.shouldRetry(context);
80
-
81
- expect(decision.action).toBe('dead-letter');
82
- assertDeadLetter(decision);
83
- expect(decision.queue).toBe('undeliverable');
84
- expect(decision.reason).toContain('max attempts exceeded (3)');
85
- });
86
-
87
- it('should dead-letter non-idempotent subscriber on redelivery', () => {
88
- const policy = new StandardRetryPolicy();
89
- const context = createContext(
90
- new Error('Some error'),
91
- { attemptNumber: 1, redelivered: true },
92
- { idempotent: 'no' },
93
- );
94
-
95
- const decision = policy.shouldRetry(context);
96
-
97
- expect(decision.action).toBe('dead-letter');
98
- assertDeadLetter(decision);
99
- expect(decision.reason).toContain('Non-idempotent subscriber');
100
- });
101
-
102
- it('should retry idempotent subscriber on redelivery', () => {
103
- const policy = new StandardRetryPolicy();
104
- const context = createContext(
105
- new Error('Some error'),
106
- { attemptNumber: 1, redelivered: true },
107
- { idempotent: 'yes' },
108
- );
109
-
110
- const decision = policy.shouldRetry(context);
111
-
112
- expect(decision.action).toBe('retry');
113
- });
114
-
115
- it('should dead-letter unknown idempotency subscriber on redelivery (unknown == no)', () => {
116
- const policy = new StandardRetryPolicy();
117
- const context = createContext(
118
- new Error('Some error'),
119
- { attemptNumber: 1, redelivered: true },
120
- { idempotent: 'unknown' },
121
- );
122
-
123
- const decision = policy.shouldRetry(context);
124
-
125
- expect(decision.action).toBe('dead-letter');
126
- assertDeadLetter(decision);
127
- expect(decision.reason).toContain('Non-idempotent subscriber');
128
- });
129
-
130
- it('should retry resumable subscriber on redelivery', () => {
131
- const policy = new StandardRetryPolicy();
132
- const context = createContext(
133
- new Error('Some error'),
134
- { attemptNumber: 1, redelivered: true },
135
- { idempotent: 'resumable' },
136
- );
137
-
138
- const decision = policy.shouldRetry(context);
139
-
140
- expect(decision.action).toBe('retry');
141
- });
142
-
143
- it('should retry generic errors with backoff', () => {
144
- const policy = new StandardRetryPolicy({ maxAttempts: 5 });
145
- const context = createContext(new Error('Generic error'), {
146
- attemptNumber: 1,
147
- });
148
-
149
- const decision = policy.shouldRetry(context);
150
-
151
- expect(decision.action).toBe('retry');
152
- assertRetry(decision);
153
- expect(decision.delay).toBeDefined();
154
- });
155
- });
156
-
157
- describe('getDelay', () => {
158
- it('should calculate exponential backoff', () => {
159
- const policy = new StandardRetryPolicy({
160
- baseDelay: 1000,
161
- backoffMultiplier: 2,
162
- });
163
-
164
- const delay1 = policy.getDelay(
165
- createContext(new Error(), { attemptNumber: 1 }),
166
- );
167
- const delay2 = policy.getDelay(
168
- createContext(new Error(), { attemptNumber: 2 }),
169
- );
170
- const delay3 = policy.getDelay(
171
- createContext(new Error(), { attemptNumber: 3 }),
172
- );
173
-
174
- expect(delay1).toBe(1000); // 1000 * 2^0
175
- expect(delay2).toBe(2000); // 1000 * 2^1
176
- expect(delay3).toBe(4000); // 1000 * 2^2
177
- });
178
-
179
- it('should cap delay at maxDelay', () => {
180
- const policy = new StandardRetryPolicy({
181
- baseDelay: 1000,
182
- backoffMultiplier: 10,
183
- maxDelay: 5000,
184
- });
185
-
186
- const delay = policy.getDelay(
187
- createContext(new Error(), { attemptNumber: 5 }),
188
- );
189
-
190
- expect(delay).toBe(5000);
191
- });
192
- });
193
-
194
- describe('configuration', () => {
195
- it('should use default configuration', () => {
196
- const policy = new StandardRetryPolicy();
197
- const context = createContext(new Error(), { attemptNumber: 1 });
198
-
199
- const decision = policy.shouldRetry(context);
200
-
201
- // Default maxAttempts is 3, so should retry on attempt 1
202
- expect(decision.action).toBe('retry');
203
- });
204
-
205
- it('should accept custom configuration', () => {
206
- const policy = new StandardRetryPolicy({
207
- maxAttempts: 1,
208
- baseDelay: 500,
209
- maxDelay: 1000,
210
- backoffMultiplier: 1.5,
211
- });
212
-
213
- // With maxAttempts: 1, first attempt should dead-letter
214
- const context = createContext(new Error(), { attemptNumber: 1 });
215
- const decision = policy.shouldRetry(context);
216
-
217
- expect(decision.action).toBe('dead-letter');
218
- });
219
-
220
- it('should merge partial configuration with defaults', () => {
221
- const policy = new StandardRetryPolicy({ maxAttempts: 10 });
222
- // Use attemptNumber: 5 but deliveryCount: 3 to avoid poison detection
223
- const context = createContext(new Error(), {
224
- attemptNumber: 5,
225
- deliveryCount: 3,
226
- });
227
-
228
- // Should still retry because we increased maxAttempts
229
- const decision = policy.shouldRetry(context);
230
- expect(decision.action).toBe('retry');
231
- });
232
- });
233
- });
234
-
235
- function assertDeadLetter(decision: RetryDecision): asserts decision is {
236
- action: 'dead-letter';
237
- queue: string;
238
- reason: string;
239
- } {
240
- if (decision.action !== 'dead-letter') {
241
- throw new Error(`Expected dead-letter action, got ${decision.action}`);
242
- }
243
- }
244
-
245
- function assertRetry(
246
- decision: RetryDecision,
247
- ): asserts decision is { action: 'retry'; delay: number } {
248
- if (decision.action !== 'retry') {
249
- throw new Error(`Expected retry action, got ${decision.action}`);
250
- }
251
- }
252
-
253
- function createContext(
254
- error: Error,
255
- receiptOverrides: Partial<MessageReceipt> = {},
256
- subscriberOverrides: Partial<SubscriberDefinition> = {},
257
- ): RetryContext {
258
- const receipt: MessageReceipt = {
259
- handle: {},
260
- redelivered: false,
261
- attemptNumber: 1,
262
- deliveryCount:
263
- receiptOverrides.deliveryCount ?? receiptOverrides.attemptNumber ?? 1,
264
- sourceQueue: 'test-queue',
265
- sourceTransport: 'mock',
266
- ...receiptOverrides,
267
- };
268
-
269
- const subscriber: SubscriberDefinition = {
270
- name: 'test-subscriber',
271
- description: 'Test subscriber',
272
- idempotent: 'yes',
273
- importance: 'should-investigate',
274
- ...subscriberOverrides,
275
- };
276
-
277
- const envelope = createEnvelope({
278
- eventKey: 'test.event',
279
- targetSubscriber: 'test-subscriber',
280
- data: { test: 'data' },
281
- importance: 'should-investigate',
282
- });
283
-
284
- return {
285
- envelope,
286
- error,
287
- receipt,
288
- subscriber,
289
- };
290
- }
@@ -1,156 +0,0 @@
1
- import {
2
- IdempotentMessageCannotRetryError,
3
- MessageMaybePoisonedError,
4
- isAssertionError,
5
- isDoRetry,
6
- isDontRetry,
7
- } from '../errors/index.js';
8
- import type { RetryContext, RetryDecision, RetryPolicy } from './policy.js';
9
-
10
- /**
11
- * Configuration for the standard retry policy.
12
- */
13
- export interface StandardRetryPolicyConfig {
14
- /** Maximum number of attempts before dead-lettering */
15
- readonly maxAttempts: number;
16
-
17
- /** Base delay between retries in milliseconds */
18
- readonly baseDelay: number;
19
-
20
- /** Maximum delay between retries in milliseconds */
21
- readonly maxDelay: number;
22
-
23
- /** Multiplier for exponential backoff */
24
- readonly backoffMultiplier: number;
25
-
26
- /**
27
- * Maximum native delivery count before considering message poisoned.
28
- * This prevents crash loops from messages that crash the worker.
29
- * Poison messages are sent directly to the dead-letter queue.
30
- * Default: 5
31
- */
32
- readonly maxDeliveries: number;
33
- }
34
-
35
- /**
36
- * Default configuration values.
37
- */
38
- export const defaultRetryConfig: StandardRetryPolicyConfig = {
39
- maxAttempts: 3,
40
- baseDelay: 1000,
41
- maxDelay: 300000, // 5 minutes
42
- backoffMultiplier: 2,
43
- maxDeliveries: 5,
44
- };
45
-
46
- /**
47
- * Standard retry policy implementing Matador v1 behavior.
48
- *
49
- * Decision logic (in priority order):
50
- * 1. Poison message → dead-letter (prevent crash loops)
51
- * 2. EventAssertionError → dead-letter (never retry)
52
- * 3. DontRetry → dead-letter (explicit no-retry)
53
- * 4. DoRetry → retry if under max attempts
54
- * 5. Max attempts exceeded → dead-letter
55
- * 6. Non-idempotent subscriber on redelivery → dead-letter
56
- * 7. Default → retry with exponential backoff
57
- */
58
- export class StandardRetryPolicy implements RetryPolicy {
59
- private readonly config: StandardRetryPolicyConfig;
60
-
61
- constructor(config: Partial<StandardRetryPolicyConfig> = {}) {
62
- this.config = { ...defaultRetryConfig, ...config };
63
- }
64
-
65
- shouldRetry(context: RetryContext): RetryDecision {
66
- const { envelope, error, subscriber, receipt } = context;
67
- const errorMessage = error.message;
68
-
69
- // 1. Poison message detection - prevent crash loops
70
- if (receipt.deliveryCount >= this.config.maxDeliveries) {
71
- const poisonError = new MessageMaybePoisonedError(
72
- envelope.id,
73
- receipt.deliveryCount,
74
- this.config.maxDeliveries,
75
- );
76
- return {
77
- action: 'dead-letter',
78
- queue: 'undeliverable',
79
- reason: poisonError.message,
80
- };
81
- }
82
-
83
- // 2. Assertion errors never retry
84
- if (isAssertionError(error)) {
85
- return {
86
- action: 'dead-letter',
87
- queue: 'undeliverable',
88
- reason: `assertion error: ${errorMessage}`,
89
- };
90
- }
91
-
92
- // 3. Explicit no-retry
93
- if (isDontRetry(error)) {
94
- return {
95
- action: 'dead-letter',
96
- queue: 'undeliverable',
97
- reason: errorMessage,
98
- };
99
- }
100
-
101
- // 4. Explicit retry request
102
- if (isDoRetry(error)) {
103
- if (receipt.attemptNumber >= this.config.maxAttempts) {
104
- return {
105
- action: 'dead-letter',
106
- queue: 'undeliverable',
107
- reason: `max attempts exceeded (${this.config.maxAttempts}) with forced retry`,
108
- };
109
- }
110
- return {
111
- action: 'retry',
112
- delay: this.getDelay(context),
113
- };
114
- }
115
-
116
- // 5. Max attempts exceeded
117
- if (receipt.attemptNumber >= this.config.maxAttempts) {
118
- return {
119
- action: 'dead-letter',
120
- queue: 'undeliverable',
121
- reason: `max attempts exceeded (${this.config.maxAttempts})`,
122
- };
123
- }
124
-
125
- // 6. Non-idempotent subscriber on redelivery
126
- // 'no' and 'unknown' are treated the same (safer default)
127
- if (
128
- receipt.redelivered &&
129
- (subscriber.idempotent === 'no' || subscriber.idempotent === 'unknown')
130
- ) {
131
- const idempotentError = new IdempotentMessageCannotRetryError(
132
- envelope.id,
133
- subscriber.name,
134
- );
135
- return {
136
- action: 'dead-letter',
137
- queue: 'undeliverable',
138
- reason: idempotentError.message,
139
- };
140
- }
141
-
142
- // 7. Default: retry with backoff
143
- return {
144
- action: 'retry',
145
- delay: this.getDelay(context),
146
- };
147
- }
148
-
149
- getDelay(context: RetryContext): number {
150
- const attempt = context.receipt.attemptNumber;
151
- const delay =
152
- this.config.baseDelay *
153
- Math.pow(this.config.backoffMultiplier, attempt - 1);
154
- return Math.min(delay, this.config.maxDelay);
155
- }
156
- }
@@ -1,16 +0,0 @@
1
- export type {
2
- AnyEventClass,
3
- MatadorSchema,
4
- RegisterOptions,
5
- RuntimeSchemaEntry,
6
- RuntimeSchemaEntryTuple,
7
- SchemaEntry,
8
- SchemaEntryTuple,
9
- SchemaIssue,
10
- SchemaPlugin,
11
- SchemaValidationResult,
12
- } from './types.js';
13
-
14
- export { bind, installPlugins, isSchemaEntryTuple } from './types.js';
15
-
16
- export { SchemaError, SchemaRegistry } from './registry.js';