@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,618 +0,0 @@
1
- import {
2
- afterAll,
3
- afterEach,
4
- beforeAll,
5
- beforeEach,
6
- describe,
7
- expect,
8
- it,
9
- } from 'bun:test';
10
- import {
11
- RabbitMQContainer,
12
- type StartedRabbitMQContainer,
13
- } from '@testcontainers/rabbitmq';
14
- import type { Topology } from '../../src/topology/types.js';
15
- import type { Subscription } from '../../src/transport/index.js';
16
- import { RabbitMQTransport } from '../../src/transport/rabbitmq/rabbitmq-transport.js';
17
- import {
18
- createTestEnvelope,
19
- createTestTopology,
20
- } from './transport-compliance.e2e.test.js';
21
-
22
- // Skip tests if docker is not available
23
- const SKIP_E2E = process.env.SKIP_E2E_TESTS === 'true';
24
-
25
- describe.skipIf(SKIP_E2E)('RabbitMQ Transport E2E', () => {
26
- let container: StartedRabbitMQContainer;
27
- let connectionUrl: string;
28
-
29
- beforeAll(async () => {
30
- // Start RabbitMQ container
31
- container = await new RabbitMQContainer('rabbitmq:3.13-management')
32
- .withExposedPorts(5672, 15672)
33
- .start();
34
-
35
- connectionUrl = container.getAmqpUrl();
36
- console.log(`RabbitMQ container started at ${connectionUrl}`);
37
- }, 120_000);
38
-
39
- afterAll(async () => {
40
- if (container) {
41
- await container.stop();
42
- }
43
- });
44
-
45
- describe('RabbitMQ-specific features', () => {
46
- let transport: RabbitMQTransport;
47
- let subscriptions: Subscription[] = [];
48
-
49
- beforeEach(async () => {
50
- transport = new RabbitMQTransport({
51
- url: connectionUrl,
52
- connectionName: 'matador-e2e-test',
53
- quorumQueues: false, // Use classic queues for faster tests
54
- defaultPrefetch: 5,
55
- });
56
- await transport.connect();
57
- await transport.applyTopology(createTestTopology(`test-${Date.now()}`));
58
- subscriptions = [];
59
- });
60
-
61
- afterEach(async () => {
62
- for (const sub of subscriptions) {
63
- if (sub.isActive) {
64
- await sub.unsubscribe();
65
- }
66
- }
67
- if (transport.isConnected()) {
68
- await transport.disconnect();
69
- }
70
- });
71
-
72
- it('should report correct capabilities', () => {
73
- expect(transport.name).toBe('rabbitmq');
74
- expect(transport.capabilities.deliveryModes).toContain('at-least-once');
75
- expect(transport.capabilities.deadLetterRouting).toBe('native');
76
- expect(transport.capabilities.attemptTracking).toBe(true);
77
- expect(transport.capabilities.concurrencyModel).toBe('prefetch');
78
- expect(transport.capabilities.priorities).toBe(true);
79
- // delayedMessages depends on plugin availability
80
- });
81
-
82
- it('should handle message priority', async () => {
83
- const topology = createTestTopology(`priority-${Date.now()}`);
84
- topology.queues[0] = { ...topology.queues[0], priorities: true };
85
-
86
- await transport.applyTopology(topology);
87
- const queueName = `${topology.namespace}.events`;
88
-
89
- const receivedOrder: number[] = [];
90
-
91
- const subscription = await transport.subscribe(
92
- queueName,
93
- async (env, receipt) => {
94
- receivedOrder.push((env.data as { priority: number }).priority);
95
- await transport.complete(receipt);
96
- },
97
- );
98
- subscriptions.push(subscription);
99
-
100
- // Send messages with different priorities
101
- await transport.send(
102
- queueName,
103
- createTestEnvelope({ eventKey: 'priority.1' }),
104
- { priority: 1 },
105
- );
106
- await transport.send(
107
- queueName,
108
- createTestEnvelope({ eventKey: 'priority.5' }),
109
- { priority: 5 },
110
- );
111
- await transport.send(
112
- queueName,
113
- createTestEnvelope({ eventKey: 'priority.10' }),
114
- { priority: 10 },
115
- );
116
-
117
- // Wait for all messages
118
- await waitFor(() => receivedOrder.length >= 3, 5000);
119
-
120
- // Priority is not strictly guaranteed in RabbitMQ,
121
- // but higher priority messages should generally come first
122
- expect(receivedOrder.length).toBe(3);
123
- });
124
-
125
- it('should track attempt number in headers', async () => {
126
- const topology = createTestTopology(`attempts-${Date.now()}`);
127
- await transport.applyTopology(topology);
128
- const queueName = `${topology.namespace}.events`;
129
-
130
- let attemptNumber = 0;
131
-
132
- const subscription = await transport.subscribe(
133
- queueName,
134
- async (_env, receipt) => {
135
- attemptNumber = receipt.attemptNumber;
136
- await transport.complete(receipt);
137
- },
138
- );
139
- subscriptions.push(subscription);
140
-
141
- await transport.send(queueName, createTestEnvelope());
142
-
143
- await waitFor(() => attemptNumber > 0, 5000);
144
-
145
- expect(attemptNumber).toBe(1);
146
- });
147
-
148
- it('should handle prefetch/concurrency per queue', async () => {
149
- const topology = createTestTopology(`prefetch-${Date.now()}`);
150
- await transport.applyTopology(topology);
151
- const queueName = `${topology.namespace}.events`;
152
-
153
- const processing = new Set<string>();
154
- let maxConcurrent = 0;
155
-
156
- const subscription = await transport.subscribe(
157
- queueName,
158
- async (env, receipt) => {
159
- processing.add(env.id);
160
- maxConcurrent = Math.max(maxConcurrent, processing.size);
161
-
162
- // Simulate processing time
163
- await new Promise((resolve) => setTimeout(resolve, 100));
164
-
165
- processing.delete(env.id);
166
- await transport.complete(receipt);
167
- },
168
- { concurrency: 3 },
169
- );
170
- subscriptions.push(subscription);
171
-
172
- // Send more messages than prefetch allows
173
- for (let i = 0; i < 10; i++) {
174
- await transport.send(queueName, createTestEnvelope({ id: `msg-${i}` }));
175
- }
176
-
177
- // Wait for all messages to be processed
178
- await waitFor(() => processing.size === 0, 10000);
179
-
180
- // Should never exceed prefetch
181
- expect(maxConcurrent).toBeLessThanOrEqual(3);
182
- });
183
-
184
- it('should throw when sending delayed message without plugin', async () => {
185
- // Standard RabbitMQ container doesn't have delayed message plugin
186
- const topology = createTestTopology(`delay-${Date.now()}`);
187
- await transport.applyTopology(topology);
188
- const queueName = `${topology.namespace}.events`;
189
-
190
- // Should throw because plugin is not available
191
- await expect(
192
- transport.send(queueName, createTestEnvelope(), { delay: 1000 }),
193
- ).rejects.toThrow('delayed message exchange plugin');
194
- });
195
-
196
- it('should report delayedMessages capability based on plugin', () => {
197
- // Standard RabbitMQ without plugin should report false
198
- expect(transport.capabilities.delayedMessages).toBe(false);
199
- });
200
- });
201
-
202
- describe('reconnection behavior', () => {
203
- it('should handle disconnect and reconnect', async () => {
204
- const transport = new RabbitMQTransport({
205
- url: connectionUrl,
206
- connectionName: 'matador-reconnect-test',
207
- connection: {
208
- maxReconnectAttempts: 3,
209
- initialReconnectDelay: 100,
210
- },
211
- });
212
-
213
- await transport.connect();
214
- expect(transport.isConnected()).toBe(true);
215
-
216
- await transport.disconnect();
217
- expect(transport.isConnected()).toBe(false);
218
-
219
- // Reconnect
220
- await transport.connect();
221
- expect(transport.isConnected()).toBe(true);
222
-
223
- await transport.disconnect();
224
- });
225
- });
226
-
227
- describe('exact queue definitions with transport options', () => {
228
- let transport: RabbitMQTransport;
229
- const subscriptions: Subscription[] = [];
230
-
231
- beforeEach(async () => {
232
- transport = new RabbitMQTransport({
233
- url: connectionUrl,
234
- connectionName: 'matador-exact-queue-test',
235
- quorumQueues: false,
236
- defaultPrefetch: 5,
237
- });
238
- await transport.connect();
239
- });
240
-
241
- afterEach(async () => {
242
- for (const sub of subscriptions) {
243
- if (sub.isActive) {
244
- await sub.unsubscribe();
245
- }
246
- }
247
- if (transport.isConnected()) {
248
- await transport.disconnect();
249
- }
250
- });
251
-
252
- it('should apply exact RabbitMQ options when asserting queue', async () => {
253
- const namespace = `exact-opts-${Date.now()}`;
254
- const exactQueueName = `${namespace}.shared.custom-queue`;
255
-
256
- const topology: Topology = {
257
- namespace,
258
- queues: [
259
- {
260
- name: exactQueueName,
261
- exact: true,
262
- transport: {
263
- rabbitmq: {
264
- options: {
265
- durable: true,
266
- arguments: {
267
- 'x-max-length': 1000,
268
- 'x-message-ttl': 60000,
269
- },
270
- },
271
- },
272
- },
273
- },
274
- ],
275
- deadLetter: {
276
- unhandled: { enabled: false },
277
- undeliverable: { enabled: false },
278
- },
279
- retry: {
280
- enabled: true,
281
- defaultDelayMs: 1000,
282
- maxDelayMs: 30000,
283
- },
284
- };
285
-
286
- // Should not throw - queue options should be applied correctly
287
- await transport.applyTopology(topology);
288
-
289
- // Verify queue works by sending and receiving a message
290
- let receivedMessage = false;
291
- const subscription = await transport.subscribe(
292
- exactQueueName,
293
- async (_env, receipt) => {
294
- receivedMessage = true;
295
- await transport.complete(receipt);
296
- },
297
- );
298
- subscriptions.push(subscription);
299
-
300
- await transport.send(exactQueueName, createTestEnvelope());
301
- await waitFor(() => receivedMessage, 5000);
302
-
303
- expect(receivedMessage).toBe(true);
304
- });
305
-
306
- it('should create retry queue for exact queue when retry is enabled', async () => {
307
- const namespace = `exact-retry-${Date.now()}`;
308
- const exactQueueName = `${namespace}.shared.retry-queue`;
309
-
310
- const topology: Topology = {
311
- namespace,
312
- queues: [
313
- {
314
- name: exactQueueName,
315
- exact: true,
316
- transport: {
317
- rabbitmq: {
318
- options: {
319
- durable: true,
320
- },
321
- },
322
- },
323
- },
324
- ],
325
- deadLetter: {
326
- unhandled: { enabled: false },
327
- undeliverable: { enabled: false },
328
- },
329
- retry: {
330
- enabled: true,
331
- defaultDelayMs: 1000,
332
- maxDelayMs: 30000,
333
- },
334
- };
335
-
336
- // Apply topology - should create both work queue and retry queue
337
- await transport.applyTopology(topology);
338
-
339
- // Verify the main queue works
340
- let receivedCount = 0;
341
- const subscription = await transport.subscribe(
342
- exactQueueName,
343
- async (_env, receipt) => {
344
- receivedCount++;
345
- await transport.complete(receipt);
346
- },
347
- );
348
- subscriptions.push(subscription);
349
-
350
- await transport.send(exactQueueName, createTestEnvelope());
351
- await waitFor(() => receivedCount === 1, 5000);
352
-
353
- expect(receivedCount).toBe(1);
354
- });
355
-
356
- it('should use custom dead letter exchange from exact options', async () => {
357
- const namespace = `exact-dlx-${Date.now()}`;
358
- const exactQueueName = `${namespace}.shared.dlx-queue`;
359
- const customDlxExchange = `${namespace}.custom-dlx`;
360
-
361
- // First create the custom DLX exchange manually
362
- // (In real usage, this would be managed externally)
363
-
364
- const topology: Topology = {
365
- namespace,
366
- queues: [
367
- {
368
- name: exactQueueName,
369
- exact: true,
370
- transport: {
371
- rabbitmq: {
372
- options: {
373
- durable: true,
374
- deadLetterExchange: customDlxExchange,
375
- },
376
- },
377
- },
378
- },
379
- ],
380
- deadLetter: {
381
- unhandled: { enabled: false },
382
- undeliverable: { enabled: false },
383
- },
384
- retry: {
385
- enabled: false,
386
- defaultDelayMs: 1000,
387
- maxDelayMs: 30000,
388
- },
389
- };
390
-
391
- // Should not throw - custom DLX should be set
392
- await transport.applyTopology(topology);
393
-
394
- // Verify queue works
395
- let receivedMessage = false;
396
- const subscription = await transport.subscribe(
397
- exactQueueName,
398
- async (_env, receipt) => {
399
- receivedMessage = true;
400
- await transport.complete(receipt);
401
- },
402
- );
403
- subscriptions.push(subscription);
404
-
405
- await transport.send(exactQueueName, createTestEnvelope());
406
- await waitFor(() => receivedMessage, 5000);
407
-
408
- expect(receivedMessage).toBe(true);
409
- });
410
-
411
- it('should apply RabbitMQ options without exact mode', async () => {
412
- const namespace = `opts-no-exact-${Date.now()}`;
413
-
414
- const topology: Topology = {
415
- namespace,
416
- queues: [
417
- {
418
- name: 'custom-options-queue',
419
- // exact: false (default) - namespace prefix will be added
420
- transport: {
421
- rabbitmq: {
422
- options: {
423
- durable: true,
424
- arguments: {
425
- 'x-max-length': 500,
426
- },
427
- },
428
- },
429
- },
430
- },
431
- ],
432
- deadLetter: {
433
- unhandled: { enabled: false },
434
- undeliverable: { enabled: false },
435
- },
436
- retry: {
437
- enabled: true,
438
- defaultDelayMs: 1000,
439
- maxDelayMs: 30000,
440
- },
441
- };
442
-
443
- await transport.applyTopology(topology);
444
-
445
- // Queue name should have namespace prefix since exact: false
446
- const queueName = `${namespace}.custom-options-queue`;
447
-
448
- let receivedMessage = false;
449
- const subscription = await transport.subscribe(
450
- queueName,
451
- async (_env, receipt) => {
452
- receivedMessage = true;
453
- await transport.complete(receipt);
454
- },
455
- );
456
- subscriptions.push(subscription);
457
-
458
- await transport.send(queueName, createTestEnvelope());
459
- await waitFor(() => receivedMessage, 5000);
460
-
461
- expect(receivedMessage).toBe(true);
462
- });
463
-
464
- it('should allow queue name with dots when exact: true', async () => {
465
- const namespace = `exact-dots-${Date.now()}`;
466
- const exactQueueName = 'matador.shared.id-platform.events';
467
-
468
- const topology: Topology = {
469
- namespace,
470
- queues: [
471
- {
472
- name: exactQueueName,
473
- exact: true,
474
- transport: {
475
- rabbitmq: {
476
- options: {
477
- durable: true,
478
- },
479
- },
480
- },
481
- },
482
- ],
483
- deadLetter: {
484
- unhandled: { enabled: false },
485
- undeliverable: { enabled: false },
486
- },
487
- retry: {
488
- enabled: true,
489
- defaultDelayMs: 1000,
490
- maxDelayMs: 30000,
491
- },
492
- };
493
-
494
- await transport.applyTopology(topology);
495
-
496
- // Verify queue with dots in name works
497
- let receivedMessage = false;
498
- const subscription = await transport.subscribe(
499
- exactQueueName,
500
- async (_env, receipt) => {
501
- receivedMessage = true;
502
- await transport.complete(receipt);
503
- },
504
- );
505
- subscriptions.push(subscription);
506
-
507
- await transport.send(exactQueueName, createTestEnvelope());
508
- await waitFor(() => receivedMessage, 5000);
509
-
510
- expect(receivedMessage).toBe(true);
511
- });
512
- });
513
-
514
- describe('channel per queue isolation', () => {
515
- let transport: RabbitMQTransport;
516
- const subscriptions: Subscription[] = [];
517
-
518
- beforeEach(async () => {
519
- transport = new RabbitMQTransport({
520
- url: connectionUrl,
521
- connectionName: 'matador-isolation-test',
522
- quorumQueues: false,
523
- defaultPrefetch: 2,
524
- });
525
- await transport.connect();
526
- });
527
-
528
- afterEach(async () => {
529
- for (const sub of subscriptions) {
530
- if (sub.isActive) {
531
- await sub.unsubscribe();
532
- }
533
- }
534
- if (transport.isConnected()) {
535
- await transport.disconnect();
536
- }
537
- });
538
-
539
- it('should have independent concurrency per queue', async () => {
540
- const topology = createTestTopology(`isolation-${Date.now()}`);
541
- await transport.applyTopology(topology);
542
-
543
- const queue1 = `${topology.namespace}.events`;
544
- const queue2 = `${topology.namespace}.notifications`;
545
-
546
- const queue1Processing = new Set<string>();
547
- const queue2Processing = new Set<string>();
548
- let queue1MaxConcurrent = 0;
549
- let queue2MaxConcurrent = 0;
550
-
551
- // Subscribe to both queues with different concurrency
552
- const sub1 = await transport.subscribe(
553
- queue1,
554
- async (env, receipt) => {
555
- queue1Processing.add(env.id);
556
- queue1MaxConcurrent = Math.max(
557
- queue1MaxConcurrent,
558
- queue1Processing.size,
559
- );
560
- await new Promise((resolve) => setTimeout(resolve, 50));
561
- queue1Processing.delete(env.id);
562
- await transport.complete(receipt);
563
- },
564
- { concurrency: 2 },
565
- );
566
- subscriptions.push(sub1);
567
-
568
- const sub2 = await transport.subscribe(
569
- queue2,
570
- async (env, receipt) => {
571
- queue2Processing.add(env.id);
572
- queue2MaxConcurrent = Math.max(
573
- queue2MaxConcurrent,
574
- queue2Processing.size,
575
- );
576
- await new Promise((resolve) => setTimeout(resolve, 50));
577
- queue2Processing.delete(env.id);
578
- await transport.complete(receipt);
579
- },
580
- { concurrency: 3 },
581
- );
582
- subscriptions.push(sub2);
583
-
584
- // Send messages to both queues
585
- for (let i = 0; i < 6; i++) {
586
- await transport.send(queue1, createTestEnvelope({ id: `q1-${i}` }));
587
- await transport.send(queue2, createTestEnvelope({ id: `q2-${i}` }));
588
- }
589
-
590
- // Wait for all to complete
591
- await waitFor(
592
- () => queue1Processing.size === 0 && queue2Processing.size === 0,
593
- 10000,
594
- );
595
-
596
- // Each queue should respect its own prefetch
597
- expect(queue1MaxConcurrent).toBeLessThanOrEqual(2);
598
- expect(queue2MaxConcurrent).toBeLessThanOrEqual(3);
599
- });
600
- });
601
- });
602
-
603
- /**
604
- * Waits for a condition to be true, with timeout.
605
- */
606
- async function waitFor(
607
- condition: () => boolean,
608
- timeoutMs: number,
609
- intervalMs = 50,
610
- ): Promise<void> {
611
- const startTime = Date.now();
612
- while (!condition()) {
613
- if (Date.now() - startTime > timeoutMs) {
614
- throw new Error(`Timeout waiting for condition after ${timeoutMs}ms`);
615
- }
616
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
617
- }
618
- }