@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,393 +0,0 @@
1
- import type { Checkpoint, CheckpointStore } from '../checkpoint/index.js';
2
- import { NoOpCheckpointStore, ResumableContext } from '../checkpoint/index.js';
3
- import type { Codec } from '../codec/index.js';
4
- import { CodecDecodeError } from '../codec/index.js';
5
- import {
6
- SubscriberIsStubError,
7
- SubscriberNotRegisteredError,
8
- } from '../errors/index.js';
9
- import type { SafeHooks } from '../hooks/index.js';
10
- import type { RetryDecision, RetryPolicy } from '../retry/index.js';
11
- import type { SchemaRegistry } from '../schema/index.js';
12
- import type { MessageReceipt, Transport } from '../transport/index.js';
13
- import type {
14
- CallbackContext,
15
- Dispatcher,
16
- Envelope,
17
- ResumableCallbackContext,
18
- SubscriberDefinition,
19
- } from '../types/index.js';
20
- import { isResumableSubscriber } from '../types/index.js';
21
-
22
- /**
23
- * Configuration for the processing pipeline.
24
- */
25
- export interface PipelineConfig {
26
- readonly transport: Transport;
27
- readonly schema: SchemaRegistry;
28
- readonly codec: Codec;
29
- readonly retryPolicy: RetryPolicy;
30
- readonly hooks: SafeHooks;
31
- /** Optional checkpoint store for resumable subscribers */
32
- readonly checkpointStore?: CheckpointStore | undefined;
33
- /** Dispatcher (matador) for sending events from subscriber callbacks */
34
- readonly dispatcher: Dispatcher;
35
- }
36
-
37
- /**
38
- * Result of pipeline processing.
39
- */
40
- export interface ProcessResult {
41
- readonly success: boolean;
42
- readonly envelope?: Envelope | undefined;
43
- readonly subscriber?: SubscriberDefinition | undefined;
44
- readonly error?: Error | undefined;
45
- readonly decision?: RetryDecision | undefined;
46
- readonly durationMs: number;
47
- }
48
-
49
- /**
50
- * Processing pipeline for incoming messages.
51
- *
52
- * Handles the complete message lifecycle:
53
- * 1. Decode envelope from raw bytes
54
- * 2. Lookup subscriber from schema
55
- * 3. Execute subscriber callback with hooks
56
- * 4. Handle success/failure with retry policy
57
- */
58
- export class ProcessingPipeline {
59
- private readonly transport: Transport;
60
- private readonly schema: SchemaRegistry;
61
- private readonly codec: Codec;
62
- private readonly retryPolicy: RetryPolicy;
63
- private readonly hooks: SafeHooks;
64
- private readonly checkpointStore: CheckpointStore;
65
- private readonly dispatcher: Dispatcher;
66
-
67
- constructor(config: PipelineConfig) {
68
- this.transport = config.transport;
69
- this.schema = config.schema;
70
- this.codec = config.codec;
71
- this.retryPolicy = config.retryPolicy;
72
- this.hooks = config.hooks;
73
- this.checkpointStore = config.checkpointStore ?? new NoOpCheckpointStore();
74
- this.dispatcher = config.dispatcher;
75
- }
76
-
77
- /**
78
- * Processes a raw message from the transport.
79
- */
80
- async process(
81
- rawMessage: Uint8Array,
82
- receipt: MessageReceipt,
83
- ): Promise<ProcessResult> {
84
- const startTime = performance.now();
85
-
86
- // 1. Decode envelope
87
- let envelope: Envelope;
88
- try {
89
- envelope = this.codec.decode(rawMessage);
90
- } catch (error) {
91
- const decodeError =
92
- error instanceof CodecDecodeError
93
- ? error
94
- : new CodecDecodeError('Unknown decode error', error);
95
-
96
- await this.transport.complete(receipt);
97
- await this.hooks.onDecodeError({
98
- error: decodeError,
99
- rawMessage,
100
- sourceQueue: receipt.sourceQueue,
101
- transport: receipt.sourceTransport,
102
- });
103
-
104
- return {
105
- success: false,
106
- error: decodeError,
107
- durationMs: performance.now() - startTime,
108
- };
109
- }
110
-
111
- // 2. Lookup subscriber from schema
112
- const subscriberDef = this.schema.getSubscriberDefinition(
113
- envelope.docket.eventKey,
114
- envelope.docket.targetSubscriber,
115
- );
116
-
117
- if (!subscriberDef) {
118
- const error = new SubscriberNotRegisteredError(
119
- envelope.docket.targetSubscriber,
120
- envelope.docket.eventKey,
121
- );
122
-
123
- // Retry unhandled messages (likely deployment timing issue)
124
- const decision = this.getUnhandledRetryDecision(envelope, receipt, error);
125
- await this.handleRetryDecision(receipt, envelope, decision);
126
-
127
- return {
128
- success: false,
129
- envelope,
130
- error,
131
- decision,
132
- durationMs: performance.now() - startTime,
133
- };
134
- }
135
-
136
- // Get executable subscriber
137
- const subscriber = this.schema.getExecutableSubscriber(
138
- envelope.docket.eventKey,
139
- envelope.docket.targetSubscriber,
140
- );
141
-
142
- if (!subscriber) {
143
- // Subscriber is a stub (remote implementation)
144
- const error = new SubscriberIsStubError(envelope.docket.targetSubscriber);
145
-
146
- // Retry stub messages (likely deployment timing issue)
147
- const decision = this.getUnhandledRetryDecision(envelope, receipt, error);
148
- await this.handleRetryDecision(receipt, envelope, decision);
149
-
150
- return {
151
- success: false,
152
- envelope,
153
- subscriber: subscriberDef,
154
- error,
155
- decision,
156
- durationMs: performance.now() - startTime,
157
- };
158
- }
159
-
160
- // 3. Execute subscriber callback with hooks
161
- let result: unknown;
162
- let error: Error | undefined;
163
- let context: ResumableContext | undefined;
164
-
165
- // For resumable subscribers, load existing checkpoint and create context
166
- const isResumable = isResumableSubscriber(subscriber);
167
- let existingCheckpoint: Checkpoint | undefined;
168
-
169
- if (isResumable) {
170
- existingCheckpoint = await this.checkpointStore.get(envelope.id);
171
-
172
- if (existingCheckpoint) {
173
- await this.hooks.onCheckpointLoaded?.({
174
- envelope,
175
- subscriber: subscriberDef,
176
- checkpoint: existingCheckpoint,
177
- cachedSteps: Object.keys(existingCheckpoint.completedSteps).length,
178
- });
179
- }
180
-
181
- context = new ResumableContext({
182
- store: this.checkpointStore,
183
- envelope,
184
- subscriber: subscriberDef,
185
- existingCheckpoint,
186
- hooks: {
187
- onCheckpointHit: (ctx) => this.hooks.onCheckpointHit?.(ctx),
188
- onCheckpointMiss: (ctx) => this.hooks.onCheckpointMiss?.(ctx),
189
- },
190
- });
191
- }
192
-
193
- // Create base callback context with matador dispatcher
194
- const callbackContext: CallbackContext = { matador: this.dispatcher };
195
-
196
- await this.hooks.onWorkerWrap(envelope, subscriberDef, async () => {
197
- await this.hooks.onWorkerBeforeProcess(envelope, subscriberDef);
198
-
199
- try {
200
- if (isResumable && context) {
201
- // Resumable subscriber: pass combined context (checkpoint + matador)
202
- // Cast needed because TypeScript can't narrow the union type based on isResumable
203
- const resumableCallback = subscriber.callback as (
204
- envelope: Envelope,
205
- context: ResumableCallbackContext,
206
- ) => Promise<void> | void;
207
- // Create combined context by binding class methods and adding matador
208
- const fullContext: ResumableCallbackContext = {
209
- io: context.io.bind(context),
210
- all: context.all.bind(context),
211
- attempt: context.attempt,
212
- isRetry: context.isRetry,
213
- matador: this.dispatcher,
214
- };
215
- result = await resumableCallback(envelope, fullContext);
216
- } else {
217
- // Standard subscriber: pass envelope and callback context
218
- const standardCallback = subscriber.callback as (
219
- envelope: Envelope,
220
- context: CallbackContext,
221
- ) => Promise<void> | void;
222
- result = await standardCallback(envelope, callbackContext);
223
- }
224
- } catch (e) {
225
- error = e instanceof Error ? e : new Error(String(e));
226
- }
227
- });
228
-
229
- const durationMs = performance.now() - startTime;
230
-
231
- // 4. Handle success
232
- if (!error) {
233
- // Clear checkpoint on success for resumable subscribers
234
- if (context) {
235
- await context.clear();
236
- await this.hooks.onCheckpointCleared?.({
237
- envelope,
238
- subscriber: subscriberDef,
239
- reason: 'success',
240
- });
241
- }
242
-
243
- await this.transport.complete(receipt);
244
- await this.hooks.onWorkerSuccess({
245
- envelope,
246
- subscriber: subscriberDef,
247
- result,
248
- durationMs,
249
- transport: receipt.sourceTransport,
250
- });
251
-
252
- return {
253
- success: true,
254
- envelope,
255
- subscriber: subscriberDef,
256
- durationMs,
257
- };
258
- }
259
-
260
- // 5. Handle failure - consult retry policy
261
- const decision = this.retryPolicy.shouldRetry({
262
- envelope,
263
- error,
264
- subscriber: subscriberDef,
265
- receipt,
266
- });
267
-
268
- // Update envelope with error info
269
- envelope.docket.lastError = error.message;
270
- envelope.docket.firstError ??= error.message;
271
-
272
- // Clear checkpoint on dead-letter (terminal state)
273
- if (decision.action === 'dead-letter' && context) {
274
- await context.clear();
275
- await this.hooks.onCheckpointCleared?.({
276
- envelope,
277
- subscriber: subscriberDef,
278
- reason: 'dead-letter',
279
- });
280
- }
281
-
282
- await this.handleRetryDecision(receipt, envelope, decision);
283
-
284
- await this.hooks.onWorkerError({
285
- envelope,
286
- subscriber: subscriberDef,
287
- error,
288
- durationMs,
289
- decision,
290
- transport: receipt.sourceTransport,
291
- });
292
-
293
- return {
294
- success: false,
295
- envelope,
296
- subscriber: subscriberDef,
297
- error,
298
- decision,
299
- durationMs,
300
- };
301
- }
302
-
303
- private async handleRetryDecision(
304
- receipt: MessageReceipt,
305
- envelope: Envelope,
306
- decision: RetryDecision,
307
- ): Promise<void> {
308
- switch (decision.action) {
309
- case 'retry': {
310
- // Increment attempts and schedule retry
311
- envelope.docket.attempts++;
312
- envelope.docket.scheduledFor = new Date(
313
- Date.now() + decision.delay,
314
- ).toISOString();
315
-
316
- await this.transport.send(receipt.sourceQueue, envelope, {
317
- delay: decision.delay,
318
- });
319
- await this.transport.complete(receipt);
320
- break;
321
- }
322
-
323
- case 'dead-letter': {
324
- await this.sendToDeadLetter(
325
- receipt,
326
- envelope,
327
- decision.queue,
328
- decision.reason,
329
- );
330
- break;
331
- }
332
-
333
- case 'discard': {
334
- await this.transport.complete(receipt);
335
- break;
336
- }
337
- }
338
- }
339
-
340
- private async sendToDeadLetter(
341
- receipt: MessageReceipt,
342
- envelope: Envelope,
343
- dlqName: string,
344
- reason: string,
345
- ): Promise<void> {
346
- envelope.docket.originalQueue ??= receipt.sourceQueue;
347
-
348
- if (this.transport.sendToDeadLetter) {
349
- await this.transport.sendToDeadLetter(receipt, dlqName, envelope, reason);
350
- } else {
351
- // Manual: send to DLQ then complete original
352
- const fullDlqName = `${receipt.sourceQueue}.${dlqName}`;
353
- await this.transport.send(fullDlqName, envelope);
354
- await this.transport.complete(receipt);
355
- }
356
- }
357
-
358
- /**
359
- * Gets retry decision for unhandled messages (subscriber not found).
360
- * Uses same retry policy logic but sends to 'unhandled' DLQ after max attempts.
361
- */
362
- private getUnhandledRetryDecision(
363
- envelope: Envelope,
364
- receipt: MessageReceipt,
365
- error: Error,
366
- ): RetryDecision {
367
- // Create a synthetic subscriber definition for retry policy
368
- const syntheticSubscriber: SubscriberDefinition = {
369
- name: envelope.docket.targetSubscriber,
370
- description: 'Synthetic subscriber for unhandled message retry',
371
- idempotent: 'yes', // Safe to retry unhandled messages
372
- importance: envelope.docket.importance ?? 'should-investigate',
373
- };
374
-
375
- const decision = this.retryPolicy.shouldRetry({
376
- envelope,
377
- error,
378
- subscriber: syntheticSubscriber,
379
- receipt,
380
- });
381
-
382
- // Override dead-letter queue to 'unhandled' instead of 'undeliverable'
383
- if (decision.action === 'dead-letter') {
384
- return {
385
- action: 'dead-letter',
386
- queue: 'unhandled',
387
- reason: decision.reason,
388
- };
389
- }
390
-
391
- return decision;
392
- }
393
- }
@@ -1,4 +0,0 @@
1
- export type { RetryContext, RetryDecision, RetryPolicy } from './policy.js';
2
-
3
- export type { StandardRetryPolicyConfig } from './standard-policy.js';
4
- export { defaultRetryConfig, StandardRetryPolicy } from './standard-policy.js';
@@ -1,46 +0,0 @@
1
- import type { MessageReceipt } from '../transport/index.js';
2
- import type { Envelope, SubscriberDefinition } from '../types/index.js';
3
-
4
- /**
5
- * Context provided to retry policy for decision making.
6
- */
7
- export interface RetryContext {
8
- /** The message envelope */
9
- readonly envelope: Envelope;
10
-
11
- /** The error that caused the failure */
12
- readonly error: Error;
13
-
14
- /** The subscriber definition */
15
- readonly subscriber: SubscriberDefinition;
16
-
17
- /** Message receipt with delivery information */
18
- readonly receipt: MessageReceipt;
19
- }
20
-
21
- /**
22
- * Decision returned by retry policy.
23
- */
24
- export type RetryDecision =
25
- | { readonly action: 'retry'; readonly delay: number }
26
- | {
27
- readonly action: 'dead-letter';
28
- readonly queue: string;
29
- readonly reason: string;
30
- }
31
- | { readonly action: 'discard'; readonly reason: string };
32
-
33
- /**
34
- * Interface for retry policies.
35
- */
36
- export interface RetryPolicy {
37
- /**
38
- * Determines what to do with a failed message.
39
- */
40
- shouldRetry(context: RetryContext): RetryDecision;
41
-
42
- /**
43
- * Calculates the delay for a retry attempt.
44
- */
45
- getDelay(context: RetryContext): number;
46
- }