@zdavison/matador 2.0.1
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.
- package/cli.ts +453 -0
- package/dist/checkpoint/context.d.ts +59 -0
- package/dist/checkpoint/context.d.ts.map +1 -0
- package/dist/checkpoint/context.js +140 -0
- package/dist/checkpoint/context.test.d.ts +2 -0
- package/dist/checkpoint/context.test.d.ts.map +1 -0
- package/dist/checkpoint/context.test.js +424 -0
- package/dist/checkpoint/index.d.ts +7 -0
- package/dist/checkpoint/index.d.ts.map +1 -0
- package/dist/checkpoint/index.js +6 -0
- package/dist/checkpoint/stores/memory.d.ts +29 -0
- package/dist/checkpoint/stores/memory.d.ts.map +1 -0
- package/dist/checkpoint/stores/memory.js +39 -0
- package/dist/checkpoint/stores/noop.d.ts +14 -0
- package/dist/checkpoint/stores/noop.d.ts.map +1 -0
- package/dist/checkpoint/stores/noop.js +18 -0
- package/dist/checkpoint/stores/stores.test.d.ts +2 -0
- package/dist/checkpoint/stores/stores.test.d.ts.map +1 -0
- package/dist/checkpoint/stores/stores.test.js +146 -0
- package/dist/checkpoint/types.d.ts +119 -0
- package/dist/checkpoint/types.d.ts.map +1 -0
- package/dist/checkpoint/types.js +1 -0
- package/dist/codec/codec.d.ts +29 -0
- package/dist/codec/codec.d.ts.map +1 -0
- package/dist/codec/codec.js +15 -0
- package/dist/codec/header-aware-codec.d.ts +36 -0
- package/dist/codec/header-aware-codec.d.ts.map +1 -0
- package/dist/codec/header-aware-codec.js +1 -0
- package/dist/codec/index.d.ts +6 -0
- package/dist/codec/index.d.ts.map +1 -0
- package/dist/codec/index.js +3 -0
- package/dist/codec/json-codec.d.ts +13 -0
- package/dist/codec/json-codec.d.ts.map +1 -0
- package/dist/codec/json-codec.js +64 -0
- package/dist/codec/rabbitmq-codec.d.ts +28 -0
- package/dist/codec/rabbitmq-codec.d.ts.map +1 -0
- package/dist/codec/rabbitmq-codec.js +242 -0
- package/dist/codec/rabbitmq-codec.test.d.ts +2 -0
- package/dist/codec/rabbitmq-codec.test.d.ts.map +1 -0
- package/dist/codec/rabbitmq-codec.test.js +433 -0
- package/dist/core/fanout.d.ts +59 -0
- package/dist/core/fanout.d.ts.map +1 -0
- package/dist/core/fanout.js +121 -0
- package/dist/core/fanout.test.d.ts +2 -0
- package/dist/core/fanout.test.d.ts.map +1 -0
- package/dist/core/fanout.test.js +1055 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/matador.d.ts +133 -0
- package/dist/core/matador.d.ts.map +1 -0
- package/dist/core/matador.js +222 -0
- package/dist/core/matador.test.d.ts +2 -0
- package/dist/core/matador.test.d.ts.map +1 -0
- package/dist/core/matador.test.js +446 -0
- package/dist/core/shutdown.d.ts +78 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +111 -0
- package/dist/core/shutdown.test.d.ts +2 -0
- package/dist/core/shutdown.test.d.ts.map +1 -0
- package/dist/core/shutdown.test.js +524 -0
- package/dist/errors/checkpoint-errors.d.ts +30 -0
- package/dist/errors/checkpoint-errors.d.ts.map +1 -0
- package/dist/errors/checkpoint-errors.js +49 -0
- package/dist/errors/has-description.d.ts +18 -0
- package/dist/errors/has-description.d.ts.map +1 -0
- package/dist/errors/has-description.js +9 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +22 -0
- package/dist/errors/matador-errors.d.ts +183 -0
- package/dist/errors/matador-errors.d.ts.map +1 -0
- package/dist/errors/matador-errors.js +376 -0
- package/dist/errors/retry-errors.d.ts +67 -0
- package/dist/errors/retry-errors.d.ts.map +1 -0
- package/dist/errors/retry-errors.js +108 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/safe-hooks.d.ts +34 -0
- package/dist/hooks/safe-hooks.d.ts.map +1 -0
- package/dist/hooks/safe-hooks.js +135 -0
- package/dist/hooks/types.d.ts +189 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +9 -0
- package/dist/index.cjs +3156 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/index.d.ts +3 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/pipeline.d.ts +60 -0
- package/dist/pipeline/pipeline.d.ts.map +1 -0
- package/dist/pipeline/pipeline.js +261 -0
- package/dist/pipeline/pipeline.test.d.ts +2 -0
- package/dist/pipeline/pipeline.test.d.ts.map +1 -0
- package/dist/pipeline/pipeline.test.js +1065 -0
- package/dist/retry/index.d.ts +4 -0
- package/dist/retry/index.d.ts.map +1 -0
- package/dist/retry/index.js +1 -0
- package/dist/retry/policy.d.ts +43 -0
- package/dist/retry/policy.d.ts.map +1 -0
- package/dist/retry/policy.js +1 -0
- package/dist/retry/standard-policy.d.ts +44 -0
- package/dist/retry/standard-policy.d.ts.map +1 -0
- package/dist/retry/standard-policy.js +102 -0
- package/dist/retry/standard-policy.test.d.ts +2 -0
- package/dist/retry/standard-policy.test.d.ts.map +1 -0
- package/dist/retry/standard-policy.test.js +190 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/registry.d.ts +63 -0
- package/dist/schema/registry.d.ts.map +1 -0
- package/dist/schema/registry.js +171 -0
- package/dist/schema/registry.test.d.ts +2 -0
- package/dist/schema/registry.test.d.ts.map +1 -0
- package/dist/schema/registry.test.js +278 -0
- package/dist/schema/types.d.ts +158 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +74 -0
- package/dist/schema/types.test.d.ts +2 -0
- package/dist/schema/types.test.d.ts.map +1 -0
- package/dist/schema/types.test.js +243 -0
- package/dist/topology/builder.d.ts +66 -0
- package/dist/topology/builder.d.ts.map +1 -0
- package/dist/topology/builder.js +156 -0
- package/dist/topology/builder.test.d.ts +2 -0
- package/dist/topology/builder.test.d.ts.map +1 -0
- package/dist/topology/builder.test.js +222 -0
- package/dist/topology/index.d.ts +5 -0
- package/dist/topology/index.d.ts.map +1 -0
- package/dist/topology/index.js +2 -0
- package/dist/topology/types.d.ts +76 -0
- package/dist/topology/types.d.ts.map +1 -0
- package/dist/topology/types.js +18 -0
- package/dist/transport/capabilities.d.ts +66 -0
- package/dist/transport/capabilities.d.ts.map +1 -0
- package/dist/transport/capabilities.js +18 -0
- package/dist/transport/connection-manager.d.ts +95 -0
- package/dist/transport/connection-manager.d.ts.map +1 -0
- package/dist/transport/connection-manager.js +144 -0
- package/dist/transport/index.d.ts +11 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +5 -0
- package/dist/transport/local/local-transport.d.ts +62 -0
- package/dist/transport/local/local-transport.d.ts.map +1 -0
- package/dist/transport/local/local-transport.js +241 -0
- package/dist/transport/local/local-transport.test.d.ts +2 -0
- package/dist/transport/local/local-transport.test.d.ts.map +1 -0
- package/dist/transport/local/local-transport.test.js +192 -0
- package/dist/transport/multi/multi-transport.d.ts +94 -0
- package/dist/transport/multi/multi-transport.d.ts.map +1 -0
- package/dist/transport/multi/multi-transport.js +184 -0
- package/dist/transport/multi/multi-transport.test.d.ts +2 -0
- package/dist/transport/multi/multi-transport.test.d.ts.map +1 -0
- package/dist/transport/multi/multi-transport.test.js +236 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +73 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.js +518 -0
- package/dist/transport/transport.d.ts +152 -0
- package/dist/transport/transport.d.ts.map +1 -0
- package/dist/transport/transport.js +1 -0
- package/dist/types/common.d.ts +41 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +12 -0
- package/dist/types/envelope.d.ts +82 -0
- package/dist/types/envelope.d.ts.map +1 -0
- package/dist/types/envelope.js +36 -0
- package/dist/types/event.d.ts +96 -0
- package/dist/types/event.d.ts.map +1 -0
- package/dist/types/event.js +26 -0
- package/dist/types/event.test.d.ts +2 -0
- package/dist/types/event.test.d.ts.map +1 -0
- package/dist/types/event.test.js +130 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/subscriber.d.ts +207 -0
- package/dist/types/subscriber.d.ts.map +1 -0
- package/dist/types/subscriber.js +99 -0
- package/examples/config.ts +126 -0
- package/examples/event.ts +26 -0
- package/examples/order-event.json +19 -0
- package/package.json +66 -0
- package/src/checkpoint/context.test.ts +510 -0
- package/src/checkpoint/context.ts +213 -0
- package/src/checkpoint/index.ts +30 -0
- package/src/checkpoint/stores/memory.ts +47 -0
- package/src/checkpoint/stores/noop.ts +22 -0
- package/src/checkpoint/stores/stores.test.ts +177 -0
- package/src/checkpoint/types.ts +147 -0
- package/src/codec/codec.ts +42 -0
- package/src/codec/header-aware-codec.ts +41 -0
- package/src/codec/index.ts +11 -0
- package/src/codec/json-codec.ts +69 -0
- package/src/codec/rabbitmq-codec.test.ts +516 -0
- package/src/codec/rabbitmq-codec.ts +336 -0
- package/src/core/fanout.test.ts +1351 -0
- package/src/core/fanout.ts +184 -0
- package/src/core/index.ts +12 -0
- package/src/core/matador.test.ts +575 -0
- package/src/core/matador.ts +344 -0
- package/src/core/shutdown.test.ts +853 -0
- package/src/core/shutdown.ts +165 -0
- package/src/errors/checkpoint-errors.ts +62 -0
- package/src/errors/has-description.ts +25 -0
- package/src/errors/index.ts +57 -0
- package/src/errors/matador-errors.ts +477 -0
- package/src/errors/retry-errors.ts +134 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/safe-hooks.ts +223 -0
- package/src/hooks/types.ts +248 -0
- package/src/index.ts +231 -0
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/pipeline.test.ts +1317 -0
- package/src/pipeline/pipeline.ts +371 -0
- package/src/retry/index.ts +4 -0
- package/src/retry/policy.ts +46 -0
- package/src/retry/standard-policy.test.ts +282 -0
- package/src/retry/standard-policy.ts +156 -0
- package/src/schema/index.ts +16 -0
- package/src/schema/registry.test.ts +339 -0
- package/src/schema/registry.ts +226 -0
- package/src/schema/types.test.ts +281 -0
- package/src/schema/types.ts +217 -0
- package/src/topology/builder.test.ts +275 -0
- package/src/topology/builder.ts +199 -0
- package/src/topology/index.ts +15 -0
- package/src/topology/types.ts +109 -0
- package/src/transport/capabilities.ts +88 -0
- package/src/transport/connection-manager.ts +218 -0
- package/src/transport/index.ts +42 -0
- package/src/transport/local/local-transport.test.ts +262 -0
- package/src/transport/local/local-transport.ts +327 -0
- package/src/transport/multi/multi-transport.test.ts +320 -0
- package/src/transport/multi/multi-transport.ts +294 -0
- package/src/transport/rabbitmq/rabbitmq-transport.ts +753 -0
- package/src/transport/transport.ts +200 -0
- package/src/types/common.ts +53 -0
- package/src/types/envelope.ts +152 -0
- package/src/types/event.test.ts +157 -0
- package/src/types/event.ts +118 -0
- package/src/types/index.ts +52 -0
- package/src/types/subscriber.ts +310 -0
- package/test/e2e/multi-transport.e2e.test.ts +236 -0
- package/test/e2e/rabbitmq-transport.e2e.test.ts +327 -0
- package/test/e2e/transport-compliance.e2e.test.ts +505 -0
- package/test/integration/matador.integration.test.ts +634 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import type { Topology } from '../../topology/types.js';
|
|
3
|
+
import { createEnvelope } from '../../types/index.js';
|
|
4
|
+
import { LocalTransport } from './local-transport.js';
|
|
5
|
+
|
|
6
|
+
describe('LocalTransport', () => {
|
|
7
|
+
let transport: LocalTransport;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
transport = new LocalTransport();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('connection', () => {
|
|
14
|
+
it('should start disconnected', () => {
|
|
15
|
+
expect(transport.isConnected()).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should connect successfully', async () => {
|
|
19
|
+
await transport.connect();
|
|
20
|
+
expect(transport.isConnected()).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should disconnect successfully', async () => {
|
|
24
|
+
await transport.connect();
|
|
25
|
+
await transport.disconnect();
|
|
26
|
+
expect(transport.isConnected()).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('capabilities', () => {
|
|
31
|
+
it('should report correct capabilities', () => {
|
|
32
|
+
expect(transport.name).toBe('local');
|
|
33
|
+
expect(transport.capabilities.deliveryModes).toContain('at-least-once');
|
|
34
|
+
expect(transport.capabilities.delayedMessages).toBe(true);
|
|
35
|
+
expect(transport.capabilities.deadLetterRouting).toBe('manual');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('send and receive', () => {
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
await transport.connect();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should throw when sending while disconnected', async () => {
|
|
45
|
+
await transport.disconnect();
|
|
46
|
+
const envelope = createTestEnvelope();
|
|
47
|
+
|
|
48
|
+
await expect(transport.send('test-queue', envelope)).rejects.toThrow(
|
|
49
|
+
'is not connected',
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should send and receive a message', async () => {
|
|
54
|
+
const envelope = createTestEnvelope();
|
|
55
|
+
await transport.send('test-queue', envelope);
|
|
56
|
+
|
|
57
|
+
const received = await transport.receiveOne('test-queue');
|
|
58
|
+
expect(received).not.toBeNull();
|
|
59
|
+
expect(received?.envelope.id).toBe(envelope.id);
|
|
60
|
+
expect(received?.envelope.data).toEqual({ test: 'data' });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should track queue size correctly', async () => {
|
|
64
|
+
expect(transport.getQueueSize('test-queue')).toBe(0);
|
|
65
|
+
|
|
66
|
+
await transport.send('test-queue', createTestEnvelope());
|
|
67
|
+
expect(transport.getQueueSize('test-queue')).toBe(1);
|
|
68
|
+
|
|
69
|
+
await transport.send('test-queue', createTestEnvelope());
|
|
70
|
+
expect(transport.getQueueSize('test-queue')).toBe(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should complete messages and remove from queue', async () => {
|
|
74
|
+
const envelope = createTestEnvelope();
|
|
75
|
+
await transport.send('test-queue', envelope);
|
|
76
|
+
|
|
77
|
+
const received = await transport.receiveOne('test-queue');
|
|
78
|
+
expect(received).not.toBeNull();
|
|
79
|
+
|
|
80
|
+
await transport.complete(received!.receipt);
|
|
81
|
+
|
|
82
|
+
expect(transport.getQueueSize('test-queue')).toBe(0);
|
|
83
|
+
expect(transport.getCompleted()).toHaveLength(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return null when queue is empty', async () => {
|
|
87
|
+
const received = await transport.receiveOne('empty-queue');
|
|
88
|
+
expect(received).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('subscriptions', () => {
|
|
93
|
+
beforeEach(async () => {
|
|
94
|
+
await transport.connect();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should throw when subscribing while disconnected', async () => {
|
|
98
|
+
await transport.disconnect();
|
|
99
|
+
|
|
100
|
+
await expect(
|
|
101
|
+
transport.subscribe('test-queue', async () => {}),
|
|
102
|
+
).rejects.toThrow('is not connected');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should deliver messages to subscribers', async () => {
|
|
106
|
+
const receivedMessages: unknown[] = [];
|
|
107
|
+
|
|
108
|
+
await transport.subscribe('test-queue', async (envelope) => {
|
|
109
|
+
receivedMessages.push(envelope);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const envelope = createTestEnvelope();
|
|
113
|
+
await transport.send('test-queue', envelope);
|
|
114
|
+
|
|
115
|
+
expect(receivedMessages).toHaveLength(1);
|
|
116
|
+
expect((receivedMessages[0] as { id: string }).id).toBe(envelope.id);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should deliver pending messages when subscribing', async () => {
|
|
120
|
+
const envelope = createTestEnvelope();
|
|
121
|
+
await transport.send('test-queue', envelope);
|
|
122
|
+
|
|
123
|
+
const receivedMessages: unknown[] = [];
|
|
124
|
+
await transport.subscribe('test-queue', async (env) => {
|
|
125
|
+
receivedMessages.push(env);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(receivedMessages).toHaveLength(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should stop delivering after unsubscribe', async () => {
|
|
132
|
+
const receivedMessages: unknown[] = [];
|
|
133
|
+
|
|
134
|
+
const subscription = await transport.subscribe(
|
|
135
|
+
'test-queue',
|
|
136
|
+
async (envelope) => {
|
|
137
|
+
receivedMessages.push(envelope);
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
await transport.send('test-queue', createTestEnvelope());
|
|
142
|
+
expect(receivedMessages).toHaveLength(1);
|
|
143
|
+
|
|
144
|
+
await subscription.unsubscribe();
|
|
145
|
+
expect(subscription.isActive).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('dead letter queue', () => {
|
|
150
|
+
beforeEach(async () => {
|
|
151
|
+
await transport.connect();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should send messages to dead letter queue', async () => {
|
|
155
|
+
const envelope = createTestEnvelope();
|
|
156
|
+
await transport.send('test-queue', envelope);
|
|
157
|
+
|
|
158
|
+
const received = await transport.receiveOne('test-queue');
|
|
159
|
+
expect(received).not.toBeNull();
|
|
160
|
+
|
|
161
|
+
await transport.sendToDeadLetter(
|
|
162
|
+
received!.receipt,
|
|
163
|
+
'dlq',
|
|
164
|
+
envelope,
|
|
165
|
+
'test error',
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(transport.getQueueSize('test-queue')).toBe(0);
|
|
169
|
+
expect(transport.getQueueSize('test-queue.dlq')).toBe(1);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('topology', () => {
|
|
174
|
+
it('should apply topology and create queues', async () => {
|
|
175
|
+
const topology: Topology = {
|
|
176
|
+
namespace: 'test',
|
|
177
|
+
queues: [{ name: 'events' }, { name: 'notifications' }],
|
|
178
|
+
deadLetter: {
|
|
179
|
+
unhandled: { enabled: true },
|
|
180
|
+
undeliverable: { enabled: true },
|
|
181
|
+
},
|
|
182
|
+
retry: {
|
|
183
|
+
enabled: true,
|
|
184
|
+
defaultDelayMs: 1000,
|
|
185
|
+
maxDelayMs: 30000,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
await transport.connect();
|
|
190
|
+
await transport.applyTopology(topology);
|
|
191
|
+
|
|
192
|
+
// Queues should be created
|
|
193
|
+
expect(transport.getQueueSize('test.events')).toBe(0);
|
|
194
|
+
expect(transport.getQueueSize('test.notifications')).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('clear', () => {
|
|
199
|
+
it('should reset all state', async () => {
|
|
200
|
+
await transport.connect();
|
|
201
|
+
await transport.send('test-queue', createTestEnvelope());
|
|
202
|
+
|
|
203
|
+
const received = await transport.receiveOne('test-queue');
|
|
204
|
+
await transport.complete(received!.receipt);
|
|
205
|
+
|
|
206
|
+
transport.clear();
|
|
207
|
+
|
|
208
|
+
expect(transport.getQueueSize('test-queue')).toBe(0);
|
|
209
|
+
expect(transport.getCompleted()).toHaveLength(0);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('delayed messages', () => {
|
|
214
|
+
beforeEach(async () => {
|
|
215
|
+
await transport.connect();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should return immediately and delay message delivery', async () => {
|
|
219
|
+
const envelope = createTestEnvelope();
|
|
220
|
+
|
|
221
|
+
// Send returns immediately (non-blocking)
|
|
222
|
+
await transport.send('test-queue', envelope, {
|
|
223
|
+
delay: 100,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Message should not be in queue immediately
|
|
227
|
+
expect(transport.getQueueSize('test-queue')).toBe(0);
|
|
228
|
+
|
|
229
|
+
// Wait for the delay to pass
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
231
|
+
|
|
232
|
+
// Message should now be in queue
|
|
233
|
+
expect(transport.getQueueSize('test-queue')).toBe(1);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should cancel delayed messages on disconnect', async () => {
|
|
237
|
+
const envelope = createTestEnvelope();
|
|
238
|
+
|
|
239
|
+
await transport.send('test-queue', envelope, {
|
|
240
|
+
delay: 100,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Disconnect before delay expires
|
|
244
|
+
await transport.disconnect();
|
|
245
|
+
|
|
246
|
+
// Wait past the original delay
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
248
|
+
|
|
249
|
+
// Message should never have been delivered
|
|
250
|
+
expect(transport.getQueueSize('test-queue')).toBe(0);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
function createTestEnvelope() {
|
|
256
|
+
return createEnvelope({
|
|
257
|
+
eventKey: 'test.event',
|
|
258
|
+
targetSubscriber: 'test-subscriber',
|
|
259
|
+
data: { test: 'data' },
|
|
260
|
+
importance: 'should-investigate',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { TransportNotConnectedError } from '../../errors/index.js';
|
|
2
|
+
import { type Logger, consoleLogger } from '../../hooks/index.js';
|
|
3
|
+
import type { Topology } from '../../topology/types.js';
|
|
4
|
+
import type { Envelope } from '../../types/index.js';
|
|
5
|
+
import type { TransportCapabilities } from '../capabilities.js';
|
|
6
|
+
import type {
|
|
7
|
+
MessageHandler,
|
|
8
|
+
MessageReceipt,
|
|
9
|
+
SendOptions,
|
|
10
|
+
SubscribeOptions,
|
|
11
|
+
Subscription,
|
|
12
|
+
Transport,
|
|
13
|
+
} from '../transport.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Capabilities of the LocalTransport.
|
|
17
|
+
*/
|
|
18
|
+
const localCapabilities: TransportCapabilities = {
|
|
19
|
+
deliveryModes: ['at-least-once', 'at-most-once'],
|
|
20
|
+
delayedMessages: true, // Implemented with setTimeout
|
|
21
|
+
deadLetterRouting: 'manual',
|
|
22
|
+
attemptTracking: false,
|
|
23
|
+
concurrencyModel: 'none',
|
|
24
|
+
ordering: 'queue',
|
|
25
|
+
priorities: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Internal message structure for the local queue.
|
|
30
|
+
*/
|
|
31
|
+
interface QueuedMessage {
|
|
32
|
+
readonly envelope: Envelope;
|
|
33
|
+
readonly id: string;
|
|
34
|
+
completed: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Internal subscription structure.
|
|
39
|
+
*/
|
|
40
|
+
interface ActiveSubscription {
|
|
41
|
+
readonly handler: MessageHandler;
|
|
42
|
+
readonly options: SubscribeOptions;
|
|
43
|
+
active: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Local in-memory transport for testing and fallback.
|
|
48
|
+
* Messages are stored in memory and delivered synchronously.
|
|
49
|
+
*/
|
|
50
|
+
export class LocalTransport implements Transport {
|
|
51
|
+
readonly name = 'local';
|
|
52
|
+
readonly capabilities = localCapabilities;
|
|
53
|
+
|
|
54
|
+
private connected = false;
|
|
55
|
+
private readonly queues = new Map<string, QueuedMessage[]>();
|
|
56
|
+
private readonly subscriptions = new Map<string, ActiveSubscription[]>();
|
|
57
|
+
private readonly completedMessages: MessageReceipt[] = [];
|
|
58
|
+
private readonly delayedTimers = new Set<ReturnType<typeof setTimeout>>();
|
|
59
|
+
private messageIdCounter = 0;
|
|
60
|
+
private readonly logger: Logger;
|
|
61
|
+
|
|
62
|
+
constructor(logger?: Logger) {
|
|
63
|
+
this.logger = logger ?? consoleLogger;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async connect(): Promise<void> {
|
|
67
|
+
this.connected = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async disconnect(): Promise<void> {
|
|
71
|
+
// Cancel all delayed message timers
|
|
72
|
+
for (const timer of this.delayedTimers) {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
this.delayedTimers.clear();
|
|
76
|
+
|
|
77
|
+
// Deactivate all subscriptions
|
|
78
|
+
for (const subs of this.subscriptions.values()) {
|
|
79
|
+
for (const sub of subs) {
|
|
80
|
+
sub.active = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.connected = false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isConnected(): boolean {
|
|
88
|
+
return this.connected;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async applyTopology(topology: Topology): Promise<void> {
|
|
92
|
+
if (!this.connected) {
|
|
93
|
+
throw new TransportNotConnectedError(this.name, 'applyTopology');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Initialize queues for the topology.
|
|
97
|
+
// Note: Retry queues are not pre-created here because LocalTransport handles
|
|
98
|
+
// retries by re-enqueueing messages with a delay to the original queue, matching
|
|
99
|
+
// how the pipeline schedules retries via transport.send() with a delay option.
|
|
100
|
+
for (const queueDef of topology.queues) {
|
|
101
|
+
const queueName = `${topology.namespace}.${queueDef.name}`;
|
|
102
|
+
if (!this.queues.has(queueName)) {
|
|
103
|
+
this.queues.set(queueName, []);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async send(
|
|
109
|
+
queue: string,
|
|
110
|
+
envelope: Envelope,
|
|
111
|
+
options?: SendOptions,
|
|
112
|
+
): Promise<Transport['name']> {
|
|
113
|
+
if (!this.connected) {
|
|
114
|
+
throw new TransportNotConnectedError(this.name, 'send');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle delayed messages (non-blocking, like real transports)
|
|
118
|
+
if (options?.delay !== undefined && options.delay > 0) {
|
|
119
|
+
this.scheduleDelayedMessage(queue, envelope, options.delay);
|
|
120
|
+
return this.name;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await this.enqueue(queue, envelope);
|
|
124
|
+
return this.name;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Schedules a message for delayed delivery.
|
|
129
|
+
* Returns immediately (non-blocking) to match real transport behavior.
|
|
130
|
+
*/
|
|
131
|
+
private scheduleDelayedMessage(
|
|
132
|
+
queue: string,
|
|
133
|
+
envelope: Envelope,
|
|
134
|
+
delayMs: number,
|
|
135
|
+
): void {
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
this.delayedTimers.delete(timer);
|
|
138
|
+
// Fire and forget - errors are logged in enqueue/deliverToSubscribers
|
|
139
|
+
this.enqueue(queue, envelope).catch((error) => {
|
|
140
|
+
this.logger.error(
|
|
141
|
+
'[Matador] 🔴 Failed to enqueue delayed message',
|
|
142
|
+
error,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
}, delayMs);
|
|
146
|
+
this.delayedTimers.add(timer);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async enqueue(queue: string, envelope: Envelope): Promise<void> {
|
|
150
|
+
const messages = this.getOrCreateQueue(queue);
|
|
151
|
+
const messageId = `${++this.messageIdCounter}`;
|
|
152
|
+
|
|
153
|
+
const queuedMessage: QueuedMessage = {
|
|
154
|
+
envelope,
|
|
155
|
+
id: messageId,
|
|
156
|
+
completed: false,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
messages.push(queuedMessage);
|
|
160
|
+
|
|
161
|
+
// Deliver to any active subscriptions
|
|
162
|
+
await this.deliverToSubscribers(queue, queuedMessage);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private async deliverToSubscribers(
|
|
166
|
+
queue: string,
|
|
167
|
+
message: QueuedMessage,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
const subs = this.subscriptions.get(queue);
|
|
170
|
+
if (!subs) return;
|
|
171
|
+
|
|
172
|
+
for (const sub of subs) {
|
|
173
|
+
if (!sub.active || message.completed) continue;
|
|
174
|
+
|
|
175
|
+
const receipt: MessageReceipt = {
|
|
176
|
+
handle: message,
|
|
177
|
+
redelivered: false,
|
|
178
|
+
attemptNumber: message.envelope.docket.attempts,
|
|
179
|
+
deliveryCount: message.envelope.docket.attempts,
|
|
180
|
+
sourceQueue: queue,
|
|
181
|
+
sourceTransport: this.name,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await sub.handler(message.envelope, receipt);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// Handler errors should be caught in the pipeline
|
|
188
|
+
this.logger.error('🔴 Handler error in message processing', error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async subscribe(
|
|
194
|
+
queue: string,
|
|
195
|
+
handler: MessageHandler,
|
|
196
|
+
options: SubscribeOptions = {},
|
|
197
|
+
): Promise<Subscription> {
|
|
198
|
+
if (!this.connected) {
|
|
199
|
+
throw new TransportNotConnectedError(this.name, 'subscribe');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const subscription: ActiveSubscription = {
|
|
203
|
+
handler,
|
|
204
|
+
options,
|
|
205
|
+
active: true,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const subs = this.subscriptions.get(queue) ?? [];
|
|
209
|
+
subs.push(subscription);
|
|
210
|
+
this.subscriptions.set(queue, subs);
|
|
211
|
+
|
|
212
|
+
// Deliver any pending messages
|
|
213
|
+
const messages = this.queues.get(queue) ?? [];
|
|
214
|
+
for (const message of messages) {
|
|
215
|
+
if (message.completed) continue;
|
|
216
|
+
await this.deliverToSubscribers(queue, message);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
unsubscribe: async () => {
|
|
221
|
+
subscription.active = false;
|
|
222
|
+
const remaining = subs.filter((s) => s !== subscription);
|
|
223
|
+
if (remaining.length > 0) {
|
|
224
|
+
this.subscriptions.set(queue, remaining);
|
|
225
|
+
} else {
|
|
226
|
+
this.subscriptions.delete(queue);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
get isActive() {
|
|
230
|
+
return subscription.active;
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async complete(receipt: MessageReceipt): Promise<void> {
|
|
236
|
+
const message = receipt.handle as QueuedMessage;
|
|
237
|
+
message.completed = true;
|
|
238
|
+
this.completedMessages.push(receipt);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async sendToDeadLetter(
|
|
242
|
+
receipt: MessageReceipt,
|
|
243
|
+
dlqName: string,
|
|
244
|
+
envelope: Envelope,
|
|
245
|
+
_reason: string,
|
|
246
|
+
): Promise<void> {
|
|
247
|
+
const dlqQueueName = `${receipt.sourceQueue}.${dlqName}`;
|
|
248
|
+
await this.enqueue(dlqQueueName, envelope);
|
|
249
|
+
await this.complete(receipt);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Test helpers
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Gets the current size of a queue.
|
|
256
|
+
*/
|
|
257
|
+
getQueueSize(queue: string): number {
|
|
258
|
+
const messages = this.queues.get(queue);
|
|
259
|
+
if (!messages) return 0;
|
|
260
|
+
return messages.filter((m) => !m.completed).length;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Gets all completed message receipts.
|
|
265
|
+
*/
|
|
266
|
+
getCompleted(): readonly MessageReceipt[] {
|
|
267
|
+
return this.completedMessages;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Gets pending (uncompleted) messages from a queue.
|
|
272
|
+
*/
|
|
273
|
+
getPendingMessages(queue: string): readonly Envelope[] {
|
|
274
|
+
const messages = this.queues.get(queue);
|
|
275
|
+
if (!messages) return [];
|
|
276
|
+
return messages.filter((m) => !m.completed).map((m) => m.envelope);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Clears all state (for test isolation).
|
|
281
|
+
*/
|
|
282
|
+
clear(): void {
|
|
283
|
+
this.queues.clear();
|
|
284
|
+
this.subscriptions.clear();
|
|
285
|
+
this.completedMessages.length = 0;
|
|
286
|
+
this.messageIdCounter = 0;
|
|
287
|
+
|
|
288
|
+
for (const timer of this.delayedTimers) {
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
}
|
|
291
|
+
this.delayedTimers.clear();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Receives one message from the queue without a subscription.
|
|
296
|
+
* Useful for testing.
|
|
297
|
+
*/
|
|
298
|
+
async receiveOne(
|
|
299
|
+
queue: string,
|
|
300
|
+
): Promise<{ envelope: Envelope; receipt: MessageReceipt } | null> {
|
|
301
|
+
const messages = this.queues.get(queue);
|
|
302
|
+
if (!messages) return null;
|
|
303
|
+
|
|
304
|
+
const pending = messages.find((m) => !m.completed);
|
|
305
|
+
if (!pending) return null;
|
|
306
|
+
|
|
307
|
+
const receipt: MessageReceipt = {
|
|
308
|
+
handle: pending,
|
|
309
|
+
redelivered: false,
|
|
310
|
+
attemptNumber: pending.envelope.docket.attempts,
|
|
311
|
+
deliveryCount: pending.envelope.docket.attempts,
|
|
312
|
+
sourceQueue: queue,
|
|
313
|
+
sourceTransport: this.name,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
return { envelope: pending.envelope, receipt };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private getOrCreateQueue(queue: string): QueuedMessage[] {
|
|
320
|
+
let messages = this.queues.get(queue);
|
|
321
|
+
if (!messages) {
|
|
322
|
+
messages = [];
|
|
323
|
+
this.queues.set(queue, messages);
|
|
324
|
+
}
|
|
325
|
+
return messages;
|
|
326
|
+
}
|
|
327
|
+
}
|