@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,634 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
DoRetry,
|
|
4
|
+
DontRetry,
|
|
5
|
+
LocalTransport,
|
|
6
|
+
Matador,
|
|
7
|
+
MatadorEvent,
|
|
8
|
+
type MatadorSchema,
|
|
9
|
+
TopologyBuilder,
|
|
10
|
+
createSubscriber,
|
|
11
|
+
} from '../../src/index.js';
|
|
12
|
+
|
|
13
|
+
class UserCreatedEvent extends MatadorEvent {
|
|
14
|
+
static readonly key = 'user.created';
|
|
15
|
+
static readonly description = 'Fired when a new user is created';
|
|
16
|
+
|
|
17
|
+
constructor(public data: { userId: string; email: string }) {
|
|
18
|
+
super();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class OrderPlacedEvent extends MatadorEvent {
|
|
23
|
+
static readonly key = 'order.placed';
|
|
24
|
+
static readonly description = 'Fired when an order is placed';
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
public data: {
|
|
28
|
+
orderId: string;
|
|
29
|
+
amount: number;
|
|
30
|
+
userId: string;
|
|
31
|
+
},
|
|
32
|
+
) {
|
|
33
|
+
super();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class PaymentProcessedEvent extends MatadorEvent {
|
|
38
|
+
static readonly key = 'payment.processed';
|
|
39
|
+
static readonly description = 'Fired when a payment is processed';
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
public data: {
|
|
43
|
+
paymentId: string;
|
|
44
|
+
orderId: string;
|
|
45
|
+
status: 'success' | 'failed';
|
|
46
|
+
},
|
|
47
|
+
) {
|
|
48
|
+
super();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('Matador Integration Tests', () => {
|
|
53
|
+
let transport: LocalTransport;
|
|
54
|
+
let matador: Matador;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
transport = new LocalTransport();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
if (matador) {
|
|
62
|
+
await matador.shutdown();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('full message flow', () => {
|
|
67
|
+
it('should dispatch event and process successfully', async () => {
|
|
68
|
+
const processedUsers: string[] = [];
|
|
69
|
+
|
|
70
|
+
const topology = TopologyBuilder.create()
|
|
71
|
+
.withNamespace('int-test')
|
|
72
|
+
.addQueue('events')
|
|
73
|
+
.build();
|
|
74
|
+
|
|
75
|
+
const subscriber = createSubscriber({
|
|
76
|
+
name: 'process-user',
|
|
77
|
+
description: 'Processes user events',
|
|
78
|
+
callback: async ({ data }) => {
|
|
79
|
+
processedUsers.push(data.userId);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const schema: MatadorSchema = {
|
|
84
|
+
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
matador = new Matador({
|
|
88
|
+
transport,
|
|
89
|
+
topology,
|
|
90
|
+
schema,
|
|
91
|
+
consumeFrom: ['events'],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await matador.start();
|
|
95
|
+
|
|
96
|
+
const event = new UserCreatedEvent({
|
|
97
|
+
userId: 'user-123',
|
|
98
|
+
email: 'test@example.com',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await matador.send(event);
|
|
102
|
+
|
|
103
|
+
// Wait for processing
|
|
104
|
+
await matador.waitForIdle(5000);
|
|
105
|
+
|
|
106
|
+
expect(result.subscribersSent).toBe(1);
|
|
107
|
+
expect(result.errors).toHaveLength(0);
|
|
108
|
+
expect(processedUsers).toContain('user-123');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should fan out to multiple subscribers', async () => {
|
|
112
|
+
const notifications: string[] = [];
|
|
113
|
+
const analytics: string[] = [];
|
|
114
|
+
const emails: string[] = [];
|
|
115
|
+
|
|
116
|
+
const topology = TopologyBuilder.create()
|
|
117
|
+
.withNamespace('int-fanout')
|
|
118
|
+
.addQueue('events')
|
|
119
|
+
.build();
|
|
120
|
+
|
|
121
|
+
const notifySub = createSubscriber({
|
|
122
|
+
name: 'send-notification',
|
|
123
|
+
description: 'Sends notifications',
|
|
124
|
+
callback: async ({ data }) => {
|
|
125
|
+
notifications.push(data.userId);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const analyticsSub = createSubscriber({
|
|
130
|
+
name: 'track-analytics',
|
|
131
|
+
description: 'Tracks analytics',
|
|
132
|
+
callback: async ({ data }) => {
|
|
133
|
+
analytics.push(data.userId);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const emailSub = createSubscriber({
|
|
138
|
+
name: 'send-welcome-email',
|
|
139
|
+
description: 'Sends welcome email',
|
|
140
|
+
callback: async ({ data }) => {
|
|
141
|
+
emails.push(data.email);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const schema: MatadorSchema = {
|
|
146
|
+
[UserCreatedEvent.key]: [
|
|
147
|
+
UserCreatedEvent,
|
|
148
|
+
[notifySub, analyticsSub, emailSub],
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
matador = new Matador({
|
|
153
|
+
transport,
|
|
154
|
+
topology,
|
|
155
|
+
schema,
|
|
156
|
+
consumeFrom: ['events'],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await matador.start();
|
|
160
|
+
|
|
161
|
+
const event = new UserCreatedEvent({
|
|
162
|
+
userId: 'user-456',
|
|
163
|
+
email: 'fan@out.com',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = await matador.send(event);
|
|
167
|
+
await matador.waitForIdle(5000);
|
|
168
|
+
|
|
169
|
+
expect(result.subscribersSent).toBe(3);
|
|
170
|
+
expect(notifications).toContain('user-456');
|
|
171
|
+
expect(analytics).toContain('user-456');
|
|
172
|
+
expect(emails).toContain('fan@out.com');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should handle multiple event types', async () => {
|
|
176
|
+
const users: string[] = [];
|
|
177
|
+
const orders: string[] = [];
|
|
178
|
+
|
|
179
|
+
const topology = TopologyBuilder.create()
|
|
180
|
+
.withNamespace('int-multi-event')
|
|
181
|
+
.addQueue('events')
|
|
182
|
+
.build();
|
|
183
|
+
|
|
184
|
+
const userSub = createSubscriber({
|
|
185
|
+
name: 'process-user',
|
|
186
|
+
description: 'Processes user events',
|
|
187
|
+
callback: async ({ data }) => {
|
|
188
|
+
users.push(data.userId);
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const orderSub = createSubscriber({
|
|
193
|
+
name: 'process-order',
|
|
194
|
+
description: 'Processes order events',
|
|
195
|
+
callback: async ({ data }) => {
|
|
196
|
+
orders.push(data.orderId);
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const schema: MatadorSchema = {
|
|
201
|
+
[UserCreatedEvent.key]: [UserCreatedEvent, [userSub]],
|
|
202
|
+
[OrderPlacedEvent.key]: [OrderPlacedEvent, [orderSub]],
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
matador = new Matador({
|
|
206
|
+
transport,
|
|
207
|
+
topology,
|
|
208
|
+
schema,
|
|
209
|
+
consumeFrom: ['events'],
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await matador.start();
|
|
213
|
+
|
|
214
|
+
await matador.send(
|
|
215
|
+
new UserCreatedEvent({ userId: 'u1', email: 'u1@test.com' }),
|
|
216
|
+
);
|
|
217
|
+
await matador.send(
|
|
218
|
+
new OrderPlacedEvent({ orderId: 'o1', amount: 100, userId: 'u1' }),
|
|
219
|
+
);
|
|
220
|
+
await matador.send(
|
|
221
|
+
new UserCreatedEvent({ userId: 'u2', email: 'u2@test.com' }),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
await matador.waitForIdle(5000);
|
|
225
|
+
|
|
226
|
+
expect(users).toEqual(['u1', 'u2']);
|
|
227
|
+
expect(orders).toEqual(['o1']);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('retry behavior', () => {
|
|
232
|
+
it('should retry failed subscribers', async () => {
|
|
233
|
+
let attempts = 0;
|
|
234
|
+
const maxAttempts = 3;
|
|
235
|
+
|
|
236
|
+
const topology = TopologyBuilder.create()
|
|
237
|
+
.withNamespace('int-retry')
|
|
238
|
+
.addQueue('events')
|
|
239
|
+
.withRetry({ enabled: true, defaultDelayMs: 10, maxDelayMs: 100 })
|
|
240
|
+
.build();
|
|
241
|
+
|
|
242
|
+
const subscriber = createSubscriber({
|
|
243
|
+
name: 'flaky-subscriber',
|
|
244
|
+
description: 'Flaky subscriber for retry testing',
|
|
245
|
+
callback: async () => {
|
|
246
|
+
attempts++;
|
|
247
|
+
if (attempts < maxAttempts) {
|
|
248
|
+
throw new DoRetry('Temporary failure');
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const schema: MatadorSchema = {
|
|
254
|
+
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
matador = new Matador({
|
|
258
|
+
transport,
|
|
259
|
+
topology,
|
|
260
|
+
schema,
|
|
261
|
+
consumeFrom: ['events'],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await matador.start();
|
|
265
|
+
|
|
266
|
+
await matador.send(
|
|
267
|
+
new UserCreatedEvent({ userId: 'retry-user', email: 'retry@test.com' }),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Wait for retries
|
|
271
|
+
await waitFor(() => attempts >= maxAttempts, 5000);
|
|
272
|
+
|
|
273
|
+
expect(attempts).toBe(maxAttempts);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should not retry when DontRetry is thrown', async () => {
|
|
277
|
+
let attempts = 0;
|
|
278
|
+
|
|
279
|
+
const topology = TopologyBuilder.create()
|
|
280
|
+
.withNamespace('int-dont-retry')
|
|
281
|
+
.addQueue('events')
|
|
282
|
+
.build();
|
|
283
|
+
|
|
284
|
+
const subscriber = createSubscriber({
|
|
285
|
+
name: 'permanent-fail-subscriber',
|
|
286
|
+
description: 'Permanent failure subscriber',
|
|
287
|
+
callback: async () => {
|
|
288
|
+
attempts++;
|
|
289
|
+
throw new DontRetry('Permanent failure');
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const schema: MatadorSchema = {
|
|
294
|
+
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
matador = new Matador({
|
|
298
|
+
transport,
|
|
299
|
+
topology,
|
|
300
|
+
schema,
|
|
301
|
+
consumeFrom: ['events'],
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await matador.start();
|
|
305
|
+
|
|
306
|
+
await matador.send(
|
|
307
|
+
new UserCreatedEvent({
|
|
308
|
+
userId: 'no-retry-user',
|
|
309
|
+
email: 'noretry@test.com',
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
await matador.waitForIdle(2000);
|
|
314
|
+
|
|
315
|
+
// Should only try once
|
|
316
|
+
expect(attempts).toBe(1);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('correlation ID propagation', () => {
|
|
321
|
+
it('should propagate correlation ID through event chain', async () => {
|
|
322
|
+
const correlationIds: string[] = [];
|
|
323
|
+
|
|
324
|
+
const topology = TopologyBuilder.create()
|
|
325
|
+
.withNamespace('int-correlation')
|
|
326
|
+
.addQueue('events')
|
|
327
|
+
.build();
|
|
328
|
+
|
|
329
|
+
const subscriber = createSubscriber({
|
|
330
|
+
name: 'track-correlation',
|
|
331
|
+
description: 'Tracks correlation IDs',
|
|
332
|
+
callback: async ({ docket }) => {
|
|
333
|
+
if (docket.correlationId) {
|
|
334
|
+
correlationIds.push(docket.correlationId);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const schema: MatadorSchema = {
|
|
340
|
+
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
matador = new Matador({
|
|
344
|
+
transport,
|
|
345
|
+
topology,
|
|
346
|
+
schema,
|
|
347
|
+
consumeFrom: ['events'],
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await matador.start();
|
|
351
|
+
|
|
352
|
+
await matador.send(
|
|
353
|
+
new UserCreatedEvent({ userId: 'corr-user', email: 'corr@test.com' }),
|
|
354
|
+
{ correlationId: 'request-abc-123' },
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
await matador.waitForIdle(2000);
|
|
358
|
+
|
|
359
|
+
expect(correlationIds).toContain('request-abc-123');
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('metadata handling', () => {
|
|
364
|
+
it('should include metadata in docket', async () => {
|
|
365
|
+
const receivedMetadata: Record<string, unknown>[] = [];
|
|
366
|
+
|
|
367
|
+
const topology = TopologyBuilder.create()
|
|
368
|
+
.withNamespace('int-metadata')
|
|
369
|
+
.addQueue('events')
|
|
370
|
+
.build();
|
|
371
|
+
|
|
372
|
+
const subscriber = createSubscriber({
|
|
373
|
+
name: 'capture-metadata',
|
|
374
|
+
description: 'Captures metadata',
|
|
375
|
+
callback: async ({ docket }) => {
|
|
376
|
+
if (docket.metadata) {
|
|
377
|
+
receivedMetadata.push(docket.metadata);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const schema: MatadorSchema = {
|
|
383
|
+
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
matador = new Matador({
|
|
387
|
+
transport,
|
|
388
|
+
topology,
|
|
389
|
+
schema,
|
|
390
|
+
consumeFrom: ['events'],
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
await matador.start();
|
|
394
|
+
|
|
395
|
+
await matador.send(
|
|
396
|
+
new UserCreatedEvent({
|
|
397
|
+
userId: 'meta-user',
|
|
398
|
+
email: 'meta@test.com',
|
|
399
|
+
}),
|
|
400
|
+
{
|
|
401
|
+
metadata: {
|
|
402
|
+
source: 'api',
|
|
403
|
+
requestId: 'req-123',
|
|
404
|
+
userAgent: 'test-client',
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
await matador.waitForIdle(2000);
|
|
410
|
+
|
|
411
|
+
expect(receivedMetadata.length).toBe(1);
|
|
412
|
+
expect(receivedMetadata[0]).toMatchObject({
|
|
413
|
+
source: 'api',
|
|
414
|
+
requestId: 'req-123',
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe('graceful shutdown', () => {
|
|
420
|
+
it('should complete in-flight processing during shutdown', async () => {
|
|
421
|
+
const processed: string[] = [];
|
|
422
|
+
let processingStarted = false;
|
|
423
|
+
|
|
424
|
+
const topology = TopologyBuilder.create()
|
|
425
|
+
.withNamespace('int-shutdown')
|
|
426
|
+
.addQueue('events')
|
|
427
|
+
.build();
|
|
428
|
+
|
|
429
|
+
const subscriber = createSubscriber({
|
|
430
|
+
name: 'slow-subscriber',
|
|
431
|
+
description: 'Slow subscriber for shutdown testing',
|
|
432
|
+
callback: async ({ data }) => {
|
|
433
|
+
processingStarted = true;
|
|
434
|
+
// Simulate slow processing
|
|
435
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
436
|
+
processed.push(data.userId);
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const schema: MatadorSchema = {
|
|
441
|
+
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
matador = new Matador({
|
|
445
|
+
transport,
|
|
446
|
+
topology,
|
|
447
|
+
schema,
|
|
448
|
+
consumeFrom: ['events'],
|
|
449
|
+
shutdownConfig: {
|
|
450
|
+
gracefulTimeoutMs: 5000,
|
|
451
|
+
pollIntervalMs: 50,
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
await matador.start();
|
|
456
|
+
|
|
457
|
+
// Dispatch event
|
|
458
|
+
await matador.send(
|
|
459
|
+
new UserCreatedEvent({
|
|
460
|
+
userId: 'shutdown-user',
|
|
461
|
+
email: 'shutdown@test.com',
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
// Wait for processing to start
|
|
466
|
+
await waitFor(() => processingStarted, 1000);
|
|
467
|
+
|
|
468
|
+
// Initiate shutdown while processing
|
|
469
|
+
await matador.shutdown();
|
|
470
|
+
|
|
471
|
+
// Processing should have completed
|
|
472
|
+
expect(processed).toContain('shutdown-user');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('should reject new dispatches during shutdown', async () => {
|
|
476
|
+
const topology = TopologyBuilder.create()
|
|
477
|
+
.withNamespace('int-shutdown-reject')
|
|
478
|
+
.addQueue('events')
|
|
479
|
+
.build();
|
|
480
|
+
|
|
481
|
+
const subscriber = createSubscriber({
|
|
482
|
+
name: 'test-subscriber',
|
|
483
|
+
description: 'Test subscriber',
|
|
484
|
+
callback: async () => {},
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const schema: MatadorSchema = {
|
|
488
|
+
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
matador = new Matador({
|
|
492
|
+
transport,
|
|
493
|
+
topology,
|
|
494
|
+
schema,
|
|
495
|
+
consumeFrom: ['events'],
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
await matador.start();
|
|
499
|
+
|
|
500
|
+
// Start shutdown
|
|
501
|
+
const shutdownPromise = matador.shutdown();
|
|
502
|
+
|
|
503
|
+
// Try to dispatch after shutdown started
|
|
504
|
+
await shutdownPromise;
|
|
505
|
+
|
|
506
|
+
await expect(
|
|
507
|
+
matador.send(
|
|
508
|
+
new UserCreatedEvent({ userId: 'late', email: 'late@test.com' }),
|
|
509
|
+
),
|
|
510
|
+
).rejects.toThrow();
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe('subscriber filtering', () => {
|
|
515
|
+
it('should filter subscribers based on enabled hook', async () => {
|
|
516
|
+
const processed: string[] = [];
|
|
517
|
+
|
|
518
|
+
const topology = TopologyBuilder.create()
|
|
519
|
+
.withNamespace('int-filter')
|
|
520
|
+
.addQueue('events')
|
|
521
|
+
.build();
|
|
522
|
+
|
|
523
|
+
const enabledSub = createSubscriber({
|
|
524
|
+
name: 'enabled-subscriber',
|
|
525
|
+
description: 'Enabled subscriber',
|
|
526
|
+
callback: async ({ data }) => {
|
|
527
|
+
processed.push(`enabled:${data.userId}`);
|
|
528
|
+
},
|
|
529
|
+
enabled: () => true,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const disabledSub = createSubscriber({
|
|
533
|
+
name: 'disabled-subscriber',
|
|
534
|
+
description: 'Disabled subscriber',
|
|
535
|
+
callback: async ({ data }) => {
|
|
536
|
+
processed.push(`disabled:${data.userId}`);
|
|
537
|
+
},
|
|
538
|
+
enabled: () => false,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const schema: MatadorSchema = {
|
|
542
|
+
[UserCreatedEvent.key]: [UserCreatedEvent, [enabledSub, disabledSub]],
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
matador = new Matador({
|
|
546
|
+
transport,
|
|
547
|
+
topology,
|
|
548
|
+
schema,
|
|
549
|
+
consumeFrom: ['events'],
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
await matador.start();
|
|
553
|
+
|
|
554
|
+
const result = await matador.send(
|
|
555
|
+
new UserCreatedEvent({
|
|
556
|
+
userId: 'filter-user',
|
|
557
|
+
email: 'filter@test.com',
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
await matador.waitForIdle(2000);
|
|
562
|
+
|
|
563
|
+
// Only enabled subscriber should be dispatched
|
|
564
|
+
expect(result.subscribersSent).toBe(1);
|
|
565
|
+
expect(processed).toContain('enabled:filter-user');
|
|
566
|
+
expect(processed).not.toContain('disabled:filter-user');
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe('handlers state', () => {
|
|
571
|
+
it('should track handlers state during processing', async () => {
|
|
572
|
+
let wasNotIdle = false;
|
|
573
|
+
|
|
574
|
+
const topology = TopologyBuilder.create()
|
|
575
|
+
.withNamespace('int-state')
|
|
576
|
+
.addQueue('events')
|
|
577
|
+
.build();
|
|
578
|
+
|
|
579
|
+
const subscriber = createSubscriber({
|
|
580
|
+
name: 'state-subscriber',
|
|
581
|
+
description: 'State tracking subscriber',
|
|
582
|
+
callback: async () => {
|
|
583
|
+
// Check state while processing
|
|
584
|
+
const state = matador.getHandlersState();
|
|
585
|
+
if (state.eventsBeingProcessed > 0) {
|
|
586
|
+
wasNotIdle = true;
|
|
587
|
+
}
|
|
588
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const schema: MatadorSchema = {
|
|
593
|
+
[UserCreatedEvent.key]: [UserCreatedEvent, [subscriber]],
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
matador = new Matador({
|
|
597
|
+
transport,
|
|
598
|
+
topology,
|
|
599
|
+
schema,
|
|
600
|
+
consumeFrom: ['events'],
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
await matador.start();
|
|
604
|
+
|
|
605
|
+
expect(matador.isIdle()).toBe(true);
|
|
606
|
+
|
|
607
|
+
await matador.send(
|
|
608
|
+
new UserCreatedEvent({ userId: 'state-user', email: 'state@test.com' }),
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
await matador.waitForIdle(5000);
|
|
612
|
+
|
|
613
|
+
expect(wasNotIdle).toBe(true);
|
|
614
|
+
expect(matador.isIdle()).toBe(true);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Waits for a condition to be true, with timeout.
|
|
621
|
+
*/
|
|
622
|
+
async function waitFor(
|
|
623
|
+
condition: () => boolean,
|
|
624
|
+
timeoutMs: number,
|
|
625
|
+
intervalMs = 50,
|
|
626
|
+
): Promise<void> {
|
|
627
|
+
const startTime = Date.now();
|
|
628
|
+
while (!condition()) {
|
|
629
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
630
|
+
throw new Error(`Timeout waiting for condition after ${timeoutMs}ms`);
|
|
631
|
+
}
|
|
632
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
633
|
+
}
|
|
634
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"composite": true,
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"lib": ["ES2022"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "./src",
|
|
16
|
+
"noEmit": false,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"noImplicitReturns": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"exactOptionalPropertyTypes": true,
|
|
22
|
+
"noUncheckedIndexedAccess": true,
|
|
23
|
+
"resolveJsonModule": true,
|
|
24
|
+
"isolatedModules": true,
|
|
25
|
+
"verbatimModuleSyntax": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src/**/*"],
|
|
28
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
29
|
+
}
|