@zdavison/matador 4.0.3 → 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.
- package/dist/core/matador.d.ts.map +1 -1
- package/dist/core/matador.js +4 -1
- package/dist/index.cjs +156 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/pipeline.d.ts +30 -2
- package/dist/pipeline/pipeline.d.ts.map +1 -1
- package/dist/pipeline/pipeline.js +65 -9
- package/dist/retry/index.d.ts +1 -1
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/retry/policy.d.ts +27 -0
- package/dist/retry/policy.d.ts.map +1 -1
- package/dist/retry/standard-policy.d.ts +33 -2
- package/dist/retry/standard-policy.d.ts.map +1 -1
- package/dist/retry/standard-policy.js +73 -9
- package/dist/topology/types.d.ts +14 -0
- package/dist/topology/types.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.js +3 -0
- package/package.json +1 -1
- package/dist/checkpoint/context.test.d.ts +0 -2
- package/dist/checkpoint/context.test.d.ts.map +0 -1
- package/dist/checkpoint/context.test.js +0 -428
- package/dist/checkpoint/stores/stores.test.d.ts +0 -2
- package/dist/checkpoint/stores/stores.test.d.ts.map +0 -1
- package/dist/checkpoint/stores/stores.test.js +0 -146
- package/dist/codec/rabbitmq-codec.test.d.ts +0 -2
- package/dist/codec/rabbitmq-codec.test.d.ts.map +0 -1
- package/dist/codec/rabbitmq-codec.test.js +0 -428
- package/dist/core/fanout.test.d.ts +0 -2
- package/dist/core/fanout.test.d.ts.map +0 -1
- package/dist/core/fanout.test.js +0 -1970
- package/dist/core/matador.test.d.ts +0 -2
- package/dist/core/matador.test.d.ts.map +0 -1
- package/dist/core/matador.test.js +0 -813
- package/dist/core/shutdown.test.d.ts +0 -2
- package/dist/core/shutdown.test.d.ts.map +0 -1
- package/dist/core/shutdown.test.js +0 -599
- package/dist/errors/retry-errors.test.d.ts +0 -2
- package/dist/errors/retry-errors.test.d.ts.map +0 -1
- package/dist/errors/retry-errors.test.js +0 -136
- package/dist/pipeline/pipeline.test.d.ts +0 -2
- package/dist/pipeline/pipeline.test.d.ts.map +0 -1
- package/dist/pipeline/pipeline.test.js +0 -1124
- package/dist/retry/standard-policy.test.d.ts +0 -2
- package/dist/retry/standard-policy.test.d.ts.map +0 -1
- package/dist/retry/standard-policy.test.js +0 -196
- package/dist/schema/registry.test.d.ts +0 -2
- package/dist/schema/registry.test.d.ts.map +0 -1
- package/dist/schema/registry.test.js +0 -278
- package/dist/schema/types.test.d.ts +0 -2
- package/dist/schema/types.test.d.ts.map +0 -1
- package/dist/schema/types.test.js +0 -246
- package/dist/topology/builder.test.d.ts +0 -2
- package/dist/topology/builder.test.d.ts.map +0 -1
- package/dist/topology/builder.test.js +0 -547
- package/dist/transport/local/local-transport.test.d.ts +0 -2
- package/dist/transport/local/local-transport.test.d.ts.map +0 -1
- package/dist/transport/local/local-transport.test.js +0 -232
- package/dist/transport/multi/multi-transport.test.d.ts +0 -2
- package/dist/transport/multi/multi-transport.test.d.ts.map +0 -1
- package/dist/transport/multi/multi-transport.test.js +0 -322
- package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.d.ts +0 -2
- package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.d.ts.map +0 -1
- package/dist/transport/rabbitmq/rabbitmq-transport-reconnection.test.js +0 -149
- package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts +0 -2
- package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts.map +0 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.test.js +0 -230
- package/dist/types/event.test.d.ts +0 -2
- package/dist/types/event.test.d.ts.map +0 -1
- package/dist/types/event.test.js +0 -130
|
@@ -1,813 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
2
|
-
import { SomeSendError, TransportSendError } from '../errors/index.js';
|
|
3
|
-
import { TopologyBuilder } from '../topology/builder.js';
|
|
4
|
-
import { LocalTransport, MultiTransport } from '../transport/index.js';
|
|
5
|
-
import { MatadorEvent, createSubscriber } from '../types/index.js';
|
|
6
|
-
import { Matador } from './matador.js';
|
|
7
|
-
class UserCreatedEvent extends MatadorEvent {
|
|
8
|
-
data;
|
|
9
|
-
static key = 'user.created';
|
|
10
|
-
static description = 'Fired when a new user is created';
|
|
11
|
-
constructor(data) {
|
|
12
|
-
super();
|
|
13
|
-
this.data = data;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
class OrderPlacedEvent extends MatadorEvent {
|
|
17
|
-
data;
|
|
18
|
-
static key = 'order.placed';
|
|
19
|
-
static description = 'Fired when an order is placed';
|
|
20
|
-
constructor(data) {
|
|
21
|
-
super();
|
|
22
|
-
this.data = data;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
class StepOneEvent extends MatadorEvent {
|
|
26
|
-
data;
|
|
27
|
-
static key = 'step.one';
|
|
28
|
-
static description = 'First step of a chain';
|
|
29
|
-
constructor(data) {
|
|
30
|
-
super();
|
|
31
|
-
this.data = data;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
class StepTwoEvent extends MatadorEvent {
|
|
35
|
-
data;
|
|
36
|
-
static key = 'step.two';
|
|
37
|
-
static description = 'Follow-up step dispatched from within step one';
|
|
38
|
-
constructor(data) {
|
|
39
|
-
super();
|
|
40
|
-
this.data = data;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
describe('Matador', () => {
|
|
44
|
-
let transport;
|
|
45
|
-
let matador;
|
|
46
|
-
beforeEach(() => {
|
|
47
|
-
transport = new LocalTransport();
|
|
48
|
-
});
|
|
49
|
-
afterEach(async () => {
|
|
50
|
-
if (matador) {
|
|
51
|
-
await matador.shutdown();
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
describe('configuration', () => {
|
|
55
|
-
it('should create with schema in config using constructor', () => {
|
|
56
|
-
const topology = TopologyBuilder.create()
|
|
57
|
-
.withNamespace('test')
|
|
58
|
-
.addQueue('events')
|
|
59
|
-
.build();
|
|
60
|
-
const subscriber = createSubscriber({
|
|
61
|
-
name: 'handle-user',
|
|
62
|
-
description: 'Handles user events',
|
|
63
|
-
callback: async () => { },
|
|
64
|
-
});
|
|
65
|
-
const schema = {
|
|
66
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
67
|
-
};
|
|
68
|
-
matador = new Matador({ transport, topology, schema });
|
|
69
|
-
expect(matador).toBeInstanceOf(Matador);
|
|
70
|
-
expect(matador.isConnected()).toBe(false);
|
|
71
|
-
});
|
|
72
|
-
it('should support hooks as second argument for dependency injection', () => {
|
|
73
|
-
const topology = TopologyBuilder.create()
|
|
74
|
-
.withNamespace('test')
|
|
75
|
-
.addQueue('events')
|
|
76
|
-
.build();
|
|
77
|
-
const subscriber = createSubscriber({
|
|
78
|
-
name: 'handle-user',
|
|
79
|
-
description: 'Handles user events',
|
|
80
|
-
callback: async () => { },
|
|
81
|
-
});
|
|
82
|
-
const schema = {
|
|
83
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
84
|
-
};
|
|
85
|
-
const logs = [];
|
|
86
|
-
const mockLogger = {
|
|
87
|
-
debug: (msg) => logs.push(`debug: ${msg}`),
|
|
88
|
-
info: (msg) => logs.push(`info: ${msg}`),
|
|
89
|
-
warn: (msg) => logs.push(`warn: ${msg}`),
|
|
90
|
-
error: (msg) => logs.push(`error: ${msg}`),
|
|
91
|
-
};
|
|
92
|
-
matador = new Matador({ transport, topology, schema }, { logger: mockLogger });
|
|
93
|
-
expect(matador).toBeInstanceOf(Matador);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
describe('registration', () => {
|
|
97
|
-
it('should register events via schema in constructor', () => {
|
|
98
|
-
const topology = TopologyBuilder.create()
|
|
99
|
-
.withNamespace('test')
|
|
100
|
-
.addQueue('events')
|
|
101
|
-
.build();
|
|
102
|
-
const subscriber = createSubscriber({
|
|
103
|
-
name: 'send-welcome-email',
|
|
104
|
-
description: 'Sends welcome email',
|
|
105
|
-
callback: async () => { },
|
|
106
|
-
});
|
|
107
|
-
const schema = {
|
|
108
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
109
|
-
};
|
|
110
|
-
matador = new Matador({ transport, topology, schema });
|
|
111
|
-
expect(matador).toBeInstanceOf(Matador);
|
|
112
|
-
});
|
|
113
|
-
it('should support multiple events in schema', () => {
|
|
114
|
-
const topology = TopologyBuilder.create()
|
|
115
|
-
.withNamespace('test')
|
|
116
|
-
.addQueue('events')
|
|
117
|
-
.build();
|
|
118
|
-
const userSub = createSubscriber({
|
|
119
|
-
name: 'handle-user',
|
|
120
|
-
description: 'Handles user events',
|
|
121
|
-
callback: async () => { },
|
|
122
|
-
});
|
|
123
|
-
const orderSub = createSubscriber({
|
|
124
|
-
name: 'handle-order',
|
|
125
|
-
description: 'Handles order events',
|
|
126
|
-
callback: async () => { },
|
|
127
|
-
});
|
|
128
|
-
const schema = {
|
|
129
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [userSub]],
|
|
130
|
-
[OrderPlacedEvent.key]: [OrderPlacedEvent, [orderSub]],
|
|
131
|
-
};
|
|
132
|
-
matador = new Matador({ transport, topology, schema });
|
|
133
|
-
expect(matador).toBeInstanceOf(Matador);
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
describe('start', () => {
|
|
137
|
-
it('should connect transport and be ready', async () => {
|
|
138
|
-
const topology = TopologyBuilder.create()
|
|
139
|
-
.withNamespace('test')
|
|
140
|
-
.addQueue('events')
|
|
141
|
-
.build();
|
|
142
|
-
const subscriber = createSubscriber({
|
|
143
|
-
name: 'handle-user',
|
|
144
|
-
description: 'Handles user events',
|
|
145
|
-
callback: async () => { },
|
|
146
|
-
});
|
|
147
|
-
const schema = {
|
|
148
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
149
|
-
};
|
|
150
|
-
matador = new Matador({ transport, topology, schema });
|
|
151
|
-
await matador.start();
|
|
152
|
-
expect(matador.isConnected()).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
it('should throw if started twice', async () => {
|
|
155
|
-
const topology = TopologyBuilder.create()
|
|
156
|
-
.withNamespace('test')
|
|
157
|
-
.addQueue('events')
|
|
158
|
-
.build();
|
|
159
|
-
const subscriber = createSubscriber({
|
|
160
|
-
name: 'handle-user',
|
|
161
|
-
description: 'Handles user events',
|
|
162
|
-
callback: async () => { },
|
|
163
|
-
});
|
|
164
|
-
const schema = {
|
|
165
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
166
|
-
};
|
|
167
|
-
matador = new Matador({ transport, topology, schema });
|
|
168
|
-
await matador.start();
|
|
169
|
-
// start() is idempotent - calling it again should not throw
|
|
170
|
-
await expect(matador.start()).resolves.toBeUndefined();
|
|
171
|
-
});
|
|
172
|
-
it('should throw on invalid schema', async () => {
|
|
173
|
-
const topology = TopologyBuilder.create()
|
|
174
|
-
.withNamespace('test')
|
|
175
|
-
.addQueue('events')
|
|
176
|
-
.build();
|
|
177
|
-
// Register duplicate subscriber names
|
|
178
|
-
const sub1 = createSubscriber({
|
|
179
|
-
name: 'same-name',
|
|
180
|
-
description: 'Test subscriber',
|
|
181
|
-
callback: async () => { },
|
|
182
|
-
});
|
|
183
|
-
const sub2 = createSubscriber({
|
|
184
|
-
name: 'same-name',
|
|
185
|
-
description: 'Test subscriber',
|
|
186
|
-
callback: async () => { },
|
|
187
|
-
});
|
|
188
|
-
const schema = {
|
|
189
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [sub1, sub2]],
|
|
190
|
-
};
|
|
191
|
-
matador = new Matador({ transport, topology, schema });
|
|
192
|
-
expect(matador.start()).rejects.toThrow('Schema validation failed');
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
describe('send', () => {
|
|
196
|
-
it('should throw if not started', async () => {
|
|
197
|
-
const topology = TopologyBuilder.create()
|
|
198
|
-
.withNamespace('test')
|
|
199
|
-
.addQueue('events')
|
|
200
|
-
.build();
|
|
201
|
-
const subscriber = createSubscriber({
|
|
202
|
-
name: 'handle-user',
|
|
203
|
-
description: 'Handles user events',
|
|
204
|
-
callback: async () => { },
|
|
205
|
-
});
|
|
206
|
-
const schema = {
|
|
207
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
208
|
-
};
|
|
209
|
-
matador = new Matador({ transport, topology, schema });
|
|
210
|
-
const event = new UserCreatedEvent({
|
|
211
|
-
userId: '123',
|
|
212
|
-
email: 'test@example.com',
|
|
213
|
-
});
|
|
214
|
-
await expect(matador.send(event)).rejects.toThrow('Matador has not been started');
|
|
215
|
-
});
|
|
216
|
-
it('should send events to transport', async () => {
|
|
217
|
-
const topology = TopologyBuilder.create()
|
|
218
|
-
.withNamespace('test')
|
|
219
|
-
.addQueue('events')
|
|
220
|
-
.build();
|
|
221
|
-
const subscriber = createSubscriber({
|
|
222
|
-
name: 'handle-user',
|
|
223
|
-
description: 'Handles user events',
|
|
224
|
-
callback: async () => { },
|
|
225
|
-
});
|
|
226
|
-
const schema = {
|
|
227
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
228
|
-
};
|
|
229
|
-
matador = new Matador({
|
|
230
|
-
transport,
|
|
231
|
-
topology,
|
|
232
|
-
schema,
|
|
233
|
-
consumeFrom: ['events'],
|
|
234
|
-
});
|
|
235
|
-
await matador.start();
|
|
236
|
-
const event = new UserCreatedEvent({
|
|
237
|
-
userId: '123',
|
|
238
|
-
email: 'test@example.com',
|
|
239
|
-
});
|
|
240
|
-
const result = await matador.send(event);
|
|
241
|
-
expect(result.subscribersSent).toBe(1);
|
|
242
|
-
expect(result.errors).toHaveLength(0);
|
|
243
|
-
});
|
|
244
|
-
it('should send events using class + data shorthand', async () => {
|
|
245
|
-
const topology = TopologyBuilder.create()
|
|
246
|
-
.withNamespace('test')
|
|
247
|
-
.addQueue('events')
|
|
248
|
-
.build();
|
|
249
|
-
const subscriber = createSubscriber({
|
|
250
|
-
name: 'handle-user',
|
|
251
|
-
description: 'Handles user events',
|
|
252
|
-
callback: async () => { },
|
|
253
|
-
});
|
|
254
|
-
const schema = {
|
|
255
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
256
|
-
};
|
|
257
|
-
matador = new Matador({
|
|
258
|
-
transport,
|
|
259
|
-
topology,
|
|
260
|
-
schema,
|
|
261
|
-
consumeFrom: ['events'],
|
|
262
|
-
});
|
|
263
|
-
await matador.start();
|
|
264
|
-
// Use the shorthand: send(EventClass, data)
|
|
265
|
-
const result = await matador.send(UserCreatedEvent, {
|
|
266
|
-
userId: '123',
|
|
267
|
-
email: 'test@example.com',
|
|
268
|
-
});
|
|
269
|
-
expect(result.subscribersSent).toBe(1);
|
|
270
|
-
expect(result.errors).toHaveLength(0);
|
|
271
|
-
});
|
|
272
|
-
it('should create one envelope per subscriber', async () => {
|
|
273
|
-
const topology = TopologyBuilder.create()
|
|
274
|
-
.withNamespace('test')
|
|
275
|
-
.addQueue('events')
|
|
276
|
-
.build();
|
|
277
|
-
const sub1 = createSubscriber({
|
|
278
|
-
name: 'sub-1',
|
|
279
|
-
description: 'Subscriber 1',
|
|
280
|
-
callback: async () => { },
|
|
281
|
-
});
|
|
282
|
-
const sub2 = createSubscriber({
|
|
283
|
-
name: 'sub-2',
|
|
284
|
-
description: 'Subscriber 2',
|
|
285
|
-
callback: async () => { },
|
|
286
|
-
});
|
|
287
|
-
const sub3 = createSubscriber({
|
|
288
|
-
name: 'sub-3',
|
|
289
|
-
description: 'Subscriber 3',
|
|
290
|
-
callback: async () => { },
|
|
291
|
-
});
|
|
292
|
-
const schema = {
|
|
293
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [sub1, sub2, sub3]],
|
|
294
|
-
};
|
|
295
|
-
matador = new Matador({
|
|
296
|
-
transport,
|
|
297
|
-
topology,
|
|
298
|
-
schema,
|
|
299
|
-
consumeFrom: ['events'],
|
|
300
|
-
});
|
|
301
|
-
await matador.start();
|
|
302
|
-
const event = new UserCreatedEvent({
|
|
303
|
-
userId: '123',
|
|
304
|
-
email: 'test@example.com',
|
|
305
|
-
});
|
|
306
|
-
const result = await matador.send(event);
|
|
307
|
-
expect(result.subscribersSent).toBe(3);
|
|
308
|
-
});
|
|
309
|
-
it('should include correlation ID in send', async () => {
|
|
310
|
-
const topology = TopologyBuilder.create()
|
|
311
|
-
.withNamespace('test')
|
|
312
|
-
.addQueue('events')
|
|
313
|
-
.build();
|
|
314
|
-
const subscriber = createSubscriber({
|
|
315
|
-
name: 'handle-user',
|
|
316
|
-
description: 'Handles user events',
|
|
317
|
-
callback: async () => { },
|
|
318
|
-
});
|
|
319
|
-
const schema = {
|
|
320
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
321
|
-
};
|
|
322
|
-
matador = new Matador({
|
|
323
|
-
transport,
|
|
324
|
-
topology,
|
|
325
|
-
schema,
|
|
326
|
-
consumeFrom: ['events'],
|
|
327
|
-
});
|
|
328
|
-
await matador.start();
|
|
329
|
-
const event = new UserCreatedEvent({
|
|
330
|
-
userId: '123',
|
|
331
|
-
email: 'test@example.com',
|
|
332
|
-
});
|
|
333
|
-
const result = await matador.send(event, {
|
|
334
|
-
correlationId: 'request-456',
|
|
335
|
-
});
|
|
336
|
-
expect(result.subscribersSent).toBe(1);
|
|
337
|
-
});
|
|
338
|
-
it('should throw SomeSendError when transport send fails', async () => {
|
|
339
|
-
const topology = TopologyBuilder.create()
|
|
340
|
-
.withNamespace('test')
|
|
341
|
-
.addQueue('events')
|
|
342
|
-
.build();
|
|
343
|
-
const subscriber = createSubscriber({
|
|
344
|
-
name: 'handle-user',
|
|
345
|
-
description: 'Handles user events',
|
|
346
|
-
callback: async () => { },
|
|
347
|
-
});
|
|
348
|
-
const schema = {
|
|
349
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
350
|
-
};
|
|
351
|
-
matador = new Matador({ transport, topology, schema });
|
|
352
|
-
await matador.start();
|
|
353
|
-
const sendError = new Error('Network timeout');
|
|
354
|
-
transport.send = mock(async () => {
|
|
355
|
-
throw sendError;
|
|
356
|
-
});
|
|
357
|
-
const thrown = await matador
|
|
358
|
-
.send(new UserCreatedEvent({ userId: '123', email: 'test@example.com' }), { buffer: false })
|
|
359
|
-
.catch((e) => e);
|
|
360
|
-
expect(thrown).toBeInstanceOf(SomeSendError);
|
|
361
|
-
expect(thrown.eventKey).toBe(UserCreatedEvent.key);
|
|
362
|
-
expect(thrown.errors).toHaveLength(1);
|
|
363
|
-
expect(thrown.errors[0]?.subscriberName).toBe('handle-user');
|
|
364
|
-
expect(thrown.errors[0]?.queue).toBe('matador.test.events');
|
|
365
|
-
expect(thrown.errors[0]?.error).toBeInstanceOf(TransportSendError);
|
|
366
|
-
expect(thrown.errors[0]?.error.cause).toBe(sendError);
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
|
-
describe('shutdown', () => {
|
|
370
|
-
it('should gracefully shutdown', async () => {
|
|
371
|
-
const topology = TopologyBuilder.create()
|
|
372
|
-
.withNamespace('test')
|
|
373
|
-
.addQueue('events')
|
|
374
|
-
.build();
|
|
375
|
-
const subscriber = createSubscriber({
|
|
376
|
-
name: 'handle-user',
|
|
377
|
-
description: 'Handles user events',
|
|
378
|
-
callback: async () => { },
|
|
379
|
-
});
|
|
380
|
-
const schema = {
|
|
381
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
382
|
-
};
|
|
383
|
-
matador = new Matador({ transport, topology, schema });
|
|
384
|
-
await matador.start();
|
|
385
|
-
await matador.shutdown();
|
|
386
|
-
expect(matador.isConnected()).toBe(false);
|
|
387
|
-
});
|
|
388
|
-
it('should be idempotent', async () => {
|
|
389
|
-
const topology = TopologyBuilder.create()
|
|
390
|
-
.withNamespace('test')
|
|
391
|
-
.addQueue('events')
|
|
392
|
-
.build();
|
|
393
|
-
const subscriber = createSubscriber({
|
|
394
|
-
name: 'handle-user',
|
|
395
|
-
description: 'Handles user events',
|
|
396
|
-
callback: async () => { },
|
|
397
|
-
});
|
|
398
|
-
const schema = {
|
|
399
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
400
|
-
};
|
|
401
|
-
matador = new Matador({ transport, topology, schema });
|
|
402
|
-
await matador.start();
|
|
403
|
-
await matador.shutdown();
|
|
404
|
-
await matador.shutdown(); // Should not throw
|
|
405
|
-
expect(matador.isConnected()).toBe(false);
|
|
406
|
-
});
|
|
407
|
-
it('should reject send after shutdown initiated', async () => {
|
|
408
|
-
const topology = TopologyBuilder.create()
|
|
409
|
-
.withNamespace('test')
|
|
410
|
-
.addQueue('events')
|
|
411
|
-
.build();
|
|
412
|
-
const subscriber = createSubscriber({
|
|
413
|
-
name: 'handle-user',
|
|
414
|
-
description: 'Handles user events',
|
|
415
|
-
callback: async () => { },
|
|
416
|
-
});
|
|
417
|
-
const schema = {
|
|
418
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
419
|
-
};
|
|
420
|
-
matador = new Matador({ transport, topology, schema });
|
|
421
|
-
await matador.start();
|
|
422
|
-
// Initiate shutdown but don't await
|
|
423
|
-
const shutdownPromise = matador.shutdown();
|
|
424
|
-
const event = new UserCreatedEvent({
|
|
425
|
-
userId: '123',
|
|
426
|
-
email: 'test@example.com',
|
|
427
|
-
});
|
|
428
|
-
// Send should fail during/after shutdown
|
|
429
|
-
await shutdownPromise;
|
|
430
|
-
expect(matador.send(event)).rejects.toThrow();
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
describe('stopReceiving', () => {
|
|
434
|
-
it('should return false when not started', async () => {
|
|
435
|
-
const topology = TopologyBuilder.create()
|
|
436
|
-
.withNamespace('test')
|
|
437
|
-
.addQueue('events')
|
|
438
|
-
.build();
|
|
439
|
-
const subscriber = createSubscriber({
|
|
440
|
-
name: 'handle-user',
|
|
441
|
-
description: 'Handles user events',
|
|
442
|
-
callback: async () => { },
|
|
443
|
-
});
|
|
444
|
-
const schema = {
|
|
445
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
446
|
-
};
|
|
447
|
-
matador = new Matador({ transport, topology, schema });
|
|
448
|
-
const result = await matador.stopReceiving();
|
|
449
|
-
expect(result).toBe(false);
|
|
450
|
-
});
|
|
451
|
-
it('should return true when started and stop receiving messages', async () => {
|
|
452
|
-
const topology = TopologyBuilder.create()
|
|
453
|
-
.withNamespace('test')
|
|
454
|
-
.addQueue('events')
|
|
455
|
-
.build();
|
|
456
|
-
const subscriber = createSubscriber({
|
|
457
|
-
name: 'handle-user',
|
|
458
|
-
description: 'Handles user events',
|
|
459
|
-
callback: async () => { },
|
|
460
|
-
});
|
|
461
|
-
const schema = {
|
|
462
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
463
|
-
};
|
|
464
|
-
matador = new Matador({ transport, topology, schema });
|
|
465
|
-
await matador.start();
|
|
466
|
-
const result = await matador.stopReceiving();
|
|
467
|
-
expect(result).toBe(true);
|
|
468
|
-
expect(matador.isConnected()).toBe(true);
|
|
469
|
-
});
|
|
470
|
-
it('should return false when called twice', async () => {
|
|
471
|
-
const topology = TopologyBuilder.create()
|
|
472
|
-
.withNamespace('test')
|
|
473
|
-
.addQueue('events')
|
|
474
|
-
.build();
|
|
475
|
-
const subscriber = createSubscriber({
|
|
476
|
-
name: 'handle-user',
|
|
477
|
-
description: 'Handles user events',
|
|
478
|
-
callback: async () => { },
|
|
479
|
-
});
|
|
480
|
-
const schema = {
|
|
481
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
482
|
-
};
|
|
483
|
-
matador = new Matador({ transport, topology, schema });
|
|
484
|
-
await matador.start();
|
|
485
|
-
const result1 = await matador.stopReceiving();
|
|
486
|
-
const result2 = await matador.stopReceiving();
|
|
487
|
-
expect(result1).toBe(true);
|
|
488
|
-
expect(result2).toBe(false);
|
|
489
|
-
});
|
|
490
|
-
it('should allow shutdown after stopReceiving', async () => {
|
|
491
|
-
const topology = TopologyBuilder.create()
|
|
492
|
-
.withNamespace('test')
|
|
493
|
-
.addQueue('events')
|
|
494
|
-
.build();
|
|
495
|
-
const subscriber = createSubscriber({
|
|
496
|
-
name: 'handle-user',
|
|
497
|
-
description: 'Handles user events',
|
|
498
|
-
callback: async () => { },
|
|
499
|
-
});
|
|
500
|
-
const schema = {
|
|
501
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
502
|
-
};
|
|
503
|
-
matador = new Matador({ transport, topology, schema });
|
|
504
|
-
await matador.start();
|
|
505
|
-
await matador.stopReceiving();
|
|
506
|
-
await matador.shutdown();
|
|
507
|
-
expect(matador.isConnected()).toBe(false);
|
|
508
|
-
});
|
|
509
|
-
it('should allow sending events after stopReceiving but before shutdown', async () => {
|
|
510
|
-
const topology = TopologyBuilder.create()
|
|
511
|
-
.withNamespace('test')
|
|
512
|
-
.addQueue('events')
|
|
513
|
-
.build();
|
|
514
|
-
const subscriber = createSubscriber({
|
|
515
|
-
name: 'handle-user',
|
|
516
|
-
description: 'Handles user events',
|
|
517
|
-
callback: async () => { },
|
|
518
|
-
});
|
|
519
|
-
const schema = {
|
|
520
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
521
|
-
};
|
|
522
|
-
matador = new Matador({
|
|
523
|
-
transport,
|
|
524
|
-
topology,
|
|
525
|
-
schema,
|
|
526
|
-
consumeFrom: ['events'],
|
|
527
|
-
});
|
|
528
|
-
await matador.start();
|
|
529
|
-
// stopReceiving() unsubscribes this instance's own handler. With a real
|
|
530
|
-
// broker, the queue would still exist independently (another replica,
|
|
531
|
-
// or this instance after restart, could pick the message up) — simulate
|
|
532
|
-
// that here with a subscription outside Matador's own lifecycle so
|
|
533
|
-
// LocalTransport still has somewhere to deliver to.
|
|
534
|
-
await transport.subscribe('matador.test.events', async () => { });
|
|
535
|
-
await matador.stopReceiving();
|
|
536
|
-
const event = new UserCreatedEvent({
|
|
537
|
-
userId: '123',
|
|
538
|
-
email: 'test@example.com',
|
|
539
|
-
});
|
|
540
|
-
const result = await matador.send(event);
|
|
541
|
-
expect(result.subscribersSent).toBe(1);
|
|
542
|
-
});
|
|
543
|
-
});
|
|
544
|
-
describe('idle state', () => {
|
|
545
|
-
it('should report idle when no processing', async () => {
|
|
546
|
-
const topology = TopologyBuilder.create()
|
|
547
|
-
.withNamespace('test')
|
|
548
|
-
.addQueue('events')
|
|
549
|
-
.build();
|
|
550
|
-
const subscriber = createSubscriber({
|
|
551
|
-
name: 'handle-user',
|
|
552
|
-
description: 'Handles user events',
|
|
553
|
-
callback: async () => { },
|
|
554
|
-
});
|
|
555
|
-
const schema = {
|
|
556
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
557
|
-
};
|
|
558
|
-
matador = new Matador({ transport, topology, schema });
|
|
559
|
-
await matador.start();
|
|
560
|
-
expect(matador.isIdle()).toBe(true);
|
|
561
|
-
});
|
|
562
|
-
it('should return handlers state', async () => {
|
|
563
|
-
const topology = TopologyBuilder.create()
|
|
564
|
-
.withNamespace('test')
|
|
565
|
-
.addQueue('events')
|
|
566
|
-
.build();
|
|
567
|
-
const subscriber = createSubscriber({
|
|
568
|
-
name: 'handle-user',
|
|
569
|
-
description: 'Handles user events',
|
|
570
|
-
callback: async () => { },
|
|
571
|
-
});
|
|
572
|
-
const schema = {
|
|
573
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
574
|
-
};
|
|
575
|
-
matador = new Matador({ transport, topology, schema });
|
|
576
|
-
await matador.start();
|
|
577
|
-
const state = matador.getHandlersState();
|
|
578
|
-
expect(state.isIdle).toBe(true);
|
|
579
|
-
expect(state.eventsBeingProcessed).toBe(0);
|
|
580
|
-
expect(state.eventsBeingEnqueued).toBe(0);
|
|
581
|
-
});
|
|
582
|
-
it('should wait for idle', async () => {
|
|
583
|
-
const topology = TopologyBuilder.create()
|
|
584
|
-
.withNamespace('test')
|
|
585
|
-
.addQueue('events')
|
|
586
|
-
.build();
|
|
587
|
-
const subscriber = createSubscriber({
|
|
588
|
-
name: 'handle-user',
|
|
589
|
-
description: 'Handles user events',
|
|
590
|
-
callback: async () => { },
|
|
591
|
-
});
|
|
592
|
-
const schema = {
|
|
593
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
594
|
-
};
|
|
595
|
-
matador = new Matador({ transport, topology, schema });
|
|
596
|
-
await matador.start();
|
|
597
|
-
const isIdle = await matador.waitForIdle(1000);
|
|
598
|
-
expect(isIdle).toBe(true);
|
|
599
|
-
});
|
|
600
|
-
});
|
|
601
|
-
describe('consuming from queues', () => {
|
|
602
|
-
it('should subscribe to specified queues', async () => {
|
|
603
|
-
const topology = TopologyBuilder.create()
|
|
604
|
-
.withNamespace('test')
|
|
605
|
-
.addQueue('events')
|
|
606
|
-
.addQueue('notifications')
|
|
607
|
-
.build();
|
|
608
|
-
const subscriber = createSubscriber({
|
|
609
|
-
name: 'handle-user',
|
|
610
|
-
description: 'Handles user events',
|
|
611
|
-
callback: async () => { },
|
|
612
|
-
});
|
|
613
|
-
const schema = {
|
|
614
|
-
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
615
|
-
};
|
|
616
|
-
matador = new Matador({
|
|
617
|
-
transport,
|
|
618
|
-
topology,
|
|
619
|
-
schema,
|
|
620
|
-
consumeFrom: ['events'],
|
|
621
|
-
});
|
|
622
|
-
await matador.start();
|
|
623
|
-
expect(matador.isConnected()).toBe(true);
|
|
624
|
-
});
|
|
625
|
-
});
|
|
626
|
-
describe('self-dispatch during graceful shutdown (stopReceiving)', () => {
|
|
627
|
-
it('LOCAL transport only: a self-dispatched follow-up should still be delivered after stopReceiving()', async () => {
|
|
628
|
-
const topology = TopologyBuilder.create()
|
|
629
|
-
.withNamespace('test')
|
|
630
|
-
.addQueue('events')
|
|
631
|
-
.build();
|
|
632
|
-
const received = [];
|
|
633
|
-
let releaseGate;
|
|
634
|
-
const gate = new Promise((resolve) => {
|
|
635
|
-
releaseGate = resolve;
|
|
636
|
-
});
|
|
637
|
-
let markStarted;
|
|
638
|
-
const started = new Promise((resolve) => {
|
|
639
|
-
markStarted = resolve;
|
|
640
|
-
});
|
|
641
|
-
const stepOneSub = createSubscriber({
|
|
642
|
-
name: 'step-one-handler',
|
|
643
|
-
description: 'Handles step one',
|
|
644
|
-
callback: async (envelope, context) => {
|
|
645
|
-
markStarted?.();
|
|
646
|
-
await gate;
|
|
647
|
-
await context.matador.send(StepTwoEvent, { id: envelope.data.id });
|
|
648
|
-
},
|
|
649
|
-
});
|
|
650
|
-
const stepTwoSub = createSubscriber({
|
|
651
|
-
name: 'step-two-handler',
|
|
652
|
-
description: 'Handles step two',
|
|
653
|
-
callback: async (envelope) => {
|
|
654
|
-
received.push(envelope.data.id);
|
|
655
|
-
},
|
|
656
|
-
});
|
|
657
|
-
const schema = {
|
|
658
|
-
[StepOneEvent.key]: [StepOneEvent, [stepOneSub]],
|
|
659
|
-
[StepTwoEvent.key]: [StepTwoEvent, [stepTwoSub]],
|
|
660
|
-
};
|
|
661
|
-
matador = new Matador({
|
|
662
|
-
transport,
|
|
663
|
-
topology,
|
|
664
|
-
schema,
|
|
665
|
-
consumeFrom: ['events'],
|
|
666
|
-
});
|
|
667
|
-
await matador.start();
|
|
668
|
-
void matador.send(StepOneEvent, { id: 'abc' });
|
|
669
|
-
await started;
|
|
670
|
-
// Graceful shutdown begins while step-one is still in flight.
|
|
671
|
-
await matador.stopReceiving();
|
|
672
|
-
// The in-flight handler now self-dispatches its follow-up event.
|
|
673
|
-
releaseGate?.();
|
|
674
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
675
|
-
// LocalTransport is process-local — no other consumer could ever pick
|
|
676
|
-
// this up, so it should still be delivered here.
|
|
677
|
-
expect(received).toEqual(['abc']);
|
|
678
|
-
});
|
|
679
|
-
it('MULTI transport (external primary + local fallback): a dispatched event that falls back to local should still be delivered after stopReceiving()', async () => {
|
|
680
|
-
const topology = TopologyBuilder.create()
|
|
681
|
-
.withNamespace('test')
|
|
682
|
-
.addQueue('events')
|
|
683
|
-
.build();
|
|
684
|
-
const received = [];
|
|
685
|
-
let releaseGate;
|
|
686
|
-
const gate = new Promise((resolve) => {
|
|
687
|
-
releaseGate = resolve;
|
|
688
|
-
});
|
|
689
|
-
let markStarted;
|
|
690
|
-
const started = new Promise((resolve) => {
|
|
691
|
-
markStarted = resolve;
|
|
692
|
-
});
|
|
693
|
-
// Always-failing "external" transport, forcing every send to fall
|
|
694
|
-
// back to the local transport below.
|
|
695
|
-
const externalMock = {
|
|
696
|
-
name: 'rabbitmq',
|
|
697
|
-
capabilities: {
|
|
698
|
-
deliveryModes: ['at-least-once'],
|
|
699
|
-
delayedMessages: false,
|
|
700
|
-
deadLetterRouting: 'native',
|
|
701
|
-
attemptTracking: true,
|
|
702
|
-
concurrencyModel: 'prefetch',
|
|
703
|
-
ordering: 'none',
|
|
704
|
-
priorities: false,
|
|
705
|
-
},
|
|
706
|
-
isConnected: () => true,
|
|
707
|
-
connect: mock(async () => { }),
|
|
708
|
-
disconnect: mock(async () => { }),
|
|
709
|
-
send: mock(async () => {
|
|
710
|
-
throw new Error('RabbitMQ down');
|
|
711
|
-
}),
|
|
712
|
-
subscribe: mock(async () => ({
|
|
713
|
-
unsubscribe: mock(async () => { }),
|
|
714
|
-
isActive: true,
|
|
715
|
-
})),
|
|
716
|
-
applyTopology: mock(async () => { }),
|
|
717
|
-
complete: mock(async () => { }),
|
|
718
|
-
};
|
|
719
|
-
const localTransport = new LocalTransport();
|
|
720
|
-
const multiTransport = new MultiTransport({
|
|
721
|
-
transports: [externalMock, localTransport],
|
|
722
|
-
});
|
|
723
|
-
const stepOneSub = createSubscriber({
|
|
724
|
-
name: 'step-one-handler',
|
|
725
|
-
description: 'Handles step one',
|
|
726
|
-
callback: async (envelope, context) => {
|
|
727
|
-
markStarted?.();
|
|
728
|
-
await gate;
|
|
729
|
-
await context.matador.send(StepTwoEvent, { id: envelope.data.id });
|
|
730
|
-
},
|
|
731
|
-
});
|
|
732
|
-
const stepTwoSub = createSubscriber({
|
|
733
|
-
name: 'step-two-handler',
|
|
734
|
-
description: 'Handles step two',
|
|
735
|
-
callback: async (envelope) => {
|
|
736
|
-
received.push(envelope.data.id);
|
|
737
|
-
},
|
|
738
|
-
});
|
|
739
|
-
const schema = {
|
|
740
|
-
[StepOneEvent.key]: [StepOneEvent, [stepOneSub]],
|
|
741
|
-
[StepTwoEvent.key]: [StepTwoEvent, [stepTwoSub]],
|
|
742
|
-
};
|
|
743
|
-
matador = new Matador({
|
|
744
|
-
transport: multiTransport,
|
|
745
|
-
topology,
|
|
746
|
-
schema,
|
|
747
|
-
consumeFrom: ['events'],
|
|
748
|
-
});
|
|
749
|
-
await matador.start();
|
|
750
|
-
void matador.send(StepOneEvent, { id: 'abc' });
|
|
751
|
-
await started;
|
|
752
|
-
await matador.stopReceiving();
|
|
753
|
-
releaseGate?.();
|
|
754
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
755
|
-
// The follow-up falls back to the local transport. Local is
|
|
756
|
-
// process-local — no other consumer could ever pick it up — so it
|
|
757
|
-
// should still be delivered here, even though the external transport's
|
|
758
|
-
// own subscription was (correctly) cut off immediately.
|
|
759
|
-
expect(received).toEqual(['abc']);
|
|
760
|
-
});
|
|
761
|
-
it('RABBIT-like (external) transport only: send() after stopReceiving() should still succeed regardless of local subscription state', async () => {
|
|
762
|
-
const topology = TopologyBuilder.create()
|
|
763
|
-
.withNamespace('test')
|
|
764
|
-
.addQueue('events')
|
|
765
|
-
.build();
|
|
766
|
-
const subscriber = createSubscriber({
|
|
767
|
-
name: 'step-one-handler',
|
|
768
|
-
description: 'Handles step one',
|
|
769
|
-
callback: async () => { },
|
|
770
|
-
});
|
|
771
|
-
const schema = {
|
|
772
|
-
[StepOneEvent.key]: [StepOneEvent, [subscriber]],
|
|
773
|
-
};
|
|
774
|
-
const externalTransport = {
|
|
775
|
-
name: 'rabbitmq',
|
|
776
|
-
capabilities: {
|
|
777
|
-
deliveryModes: ['at-least-once'],
|
|
778
|
-
delayedMessages: false,
|
|
779
|
-
deadLetterRouting: 'native',
|
|
780
|
-
attemptTracking: true,
|
|
781
|
-
concurrencyModel: 'prefetch',
|
|
782
|
-
ordering: 'none',
|
|
783
|
-
priorities: false,
|
|
784
|
-
},
|
|
785
|
-
isConnected: () => true,
|
|
786
|
-
connect: mock(async () => { }),
|
|
787
|
-
disconnect: mock(async () => { }),
|
|
788
|
-
send: mock(async () => 'rabbitmq'),
|
|
789
|
-
subscribe: mock(async () => ({
|
|
790
|
-
unsubscribe: mock(async () => { }),
|
|
791
|
-
isActive: true,
|
|
792
|
-
})),
|
|
793
|
-
applyTopology: mock(async () => { }),
|
|
794
|
-
complete: mock(async () => { }),
|
|
795
|
-
};
|
|
796
|
-
matador = new Matador({
|
|
797
|
-
transport: externalTransport,
|
|
798
|
-
topology,
|
|
799
|
-
schema,
|
|
800
|
-
consumeFrom: ['events'],
|
|
801
|
-
});
|
|
802
|
-
await matador.start();
|
|
803
|
-
await matador.stopReceiving();
|
|
804
|
-
const result = await matador.send(StepOneEvent, { id: 'xyz' });
|
|
805
|
-
// A real broker doesn't need an active local consumer to accept a
|
|
806
|
-
// publish — another replica (or this instance, once it stops
|
|
807
|
-
// pretending to be shut down) could still consume it later. This case
|
|
808
|
-
// is not part of the bug — included as a contrast baseline.
|
|
809
|
-
expect(result.subscribersSent).toBe(1);
|
|
810
|
-
expect(result.errors).toHaveLength(0);
|
|
811
|
-
});
|
|
812
|
-
});
|
|
813
|
-
});
|