@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.
Files changed (256) hide show
  1. package/cli.ts +453 -0
  2. package/dist/checkpoint/context.d.ts +59 -0
  3. package/dist/checkpoint/context.d.ts.map +1 -0
  4. package/dist/checkpoint/context.js +140 -0
  5. package/dist/checkpoint/context.test.d.ts +2 -0
  6. package/dist/checkpoint/context.test.d.ts.map +1 -0
  7. package/dist/checkpoint/context.test.js +424 -0
  8. package/dist/checkpoint/index.d.ts +7 -0
  9. package/dist/checkpoint/index.d.ts.map +1 -0
  10. package/dist/checkpoint/index.js +6 -0
  11. package/dist/checkpoint/stores/memory.d.ts +29 -0
  12. package/dist/checkpoint/stores/memory.d.ts.map +1 -0
  13. package/dist/checkpoint/stores/memory.js +39 -0
  14. package/dist/checkpoint/stores/noop.d.ts +14 -0
  15. package/dist/checkpoint/stores/noop.d.ts.map +1 -0
  16. package/dist/checkpoint/stores/noop.js +18 -0
  17. package/dist/checkpoint/stores/stores.test.d.ts +2 -0
  18. package/dist/checkpoint/stores/stores.test.d.ts.map +1 -0
  19. package/dist/checkpoint/stores/stores.test.js +146 -0
  20. package/dist/checkpoint/types.d.ts +119 -0
  21. package/dist/checkpoint/types.d.ts.map +1 -0
  22. package/dist/checkpoint/types.js +1 -0
  23. package/dist/codec/codec.d.ts +29 -0
  24. package/dist/codec/codec.d.ts.map +1 -0
  25. package/dist/codec/codec.js +15 -0
  26. package/dist/codec/header-aware-codec.d.ts +36 -0
  27. package/dist/codec/header-aware-codec.d.ts.map +1 -0
  28. package/dist/codec/header-aware-codec.js +1 -0
  29. package/dist/codec/index.d.ts +6 -0
  30. package/dist/codec/index.d.ts.map +1 -0
  31. package/dist/codec/index.js +3 -0
  32. package/dist/codec/json-codec.d.ts +13 -0
  33. package/dist/codec/json-codec.d.ts.map +1 -0
  34. package/dist/codec/json-codec.js +64 -0
  35. package/dist/codec/rabbitmq-codec.d.ts +28 -0
  36. package/dist/codec/rabbitmq-codec.d.ts.map +1 -0
  37. package/dist/codec/rabbitmq-codec.js +242 -0
  38. package/dist/codec/rabbitmq-codec.test.d.ts +2 -0
  39. package/dist/codec/rabbitmq-codec.test.d.ts.map +1 -0
  40. package/dist/codec/rabbitmq-codec.test.js +433 -0
  41. package/dist/core/fanout.d.ts +59 -0
  42. package/dist/core/fanout.d.ts.map +1 -0
  43. package/dist/core/fanout.js +121 -0
  44. package/dist/core/fanout.test.d.ts +2 -0
  45. package/dist/core/fanout.test.d.ts.map +1 -0
  46. package/dist/core/fanout.test.js +1055 -0
  47. package/dist/core/index.d.ts +7 -0
  48. package/dist/core/index.d.ts.map +1 -0
  49. package/dist/core/index.js +3 -0
  50. package/dist/core/matador.d.ts +133 -0
  51. package/dist/core/matador.d.ts.map +1 -0
  52. package/dist/core/matador.js +222 -0
  53. package/dist/core/matador.test.d.ts +2 -0
  54. package/dist/core/matador.test.d.ts.map +1 -0
  55. package/dist/core/matador.test.js +446 -0
  56. package/dist/core/shutdown.d.ts +78 -0
  57. package/dist/core/shutdown.d.ts.map +1 -0
  58. package/dist/core/shutdown.js +111 -0
  59. package/dist/core/shutdown.test.d.ts +2 -0
  60. package/dist/core/shutdown.test.d.ts.map +1 -0
  61. package/dist/core/shutdown.test.js +524 -0
  62. package/dist/errors/checkpoint-errors.d.ts +30 -0
  63. package/dist/errors/checkpoint-errors.d.ts.map +1 -0
  64. package/dist/errors/checkpoint-errors.js +49 -0
  65. package/dist/errors/has-description.d.ts +18 -0
  66. package/dist/errors/has-description.d.ts.map +1 -0
  67. package/dist/errors/has-description.js +9 -0
  68. package/dist/errors/index.d.ts +6 -0
  69. package/dist/errors/index.d.ts.map +1 -0
  70. package/dist/errors/index.js +22 -0
  71. package/dist/errors/matador-errors.d.ts +183 -0
  72. package/dist/errors/matador-errors.d.ts.map +1 -0
  73. package/dist/errors/matador-errors.js +376 -0
  74. package/dist/errors/retry-errors.d.ts +67 -0
  75. package/dist/errors/retry-errors.d.ts.map +1 -0
  76. package/dist/errors/retry-errors.js +108 -0
  77. package/dist/hooks/index.d.ts +4 -0
  78. package/dist/hooks/index.d.ts.map +1 -0
  79. package/dist/hooks/index.js +2 -0
  80. package/dist/hooks/safe-hooks.d.ts +34 -0
  81. package/dist/hooks/safe-hooks.d.ts.map +1 -0
  82. package/dist/hooks/safe-hooks.js +135 -0
  83. package/dist/hooks/types.d.ts +189 -0
  84. package/dist/hooks/types.d.ts.map +1 -0
  85. package/dist/hooks/types.js +9 -0
  86. package/dist/index.cjs +3156 -0
  87. package/dist/index.cjs.map +1 -0
  88. package/dist/index.d.cts +23 -0
  89. package/dist/index.d.ts +23 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +31 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/pipeline/index.d.ts +3 -0
  94. package/dist/pipeline/index.d.ts.map +1 -0
  95. package/dist/pipeline/index.js +1 -0
  96. package/dist/pipeline/pipeline.d.ts +60 -0
  97. package/dist/pipeline/pipeline.d.ts.map +1 -0
  98. package/dist/pipeline/pipeline.js +261 -0
  99. package/dist/pipeline/pipeline.test.d.ts +2 -0
  100. package/dist/pipeline/pipeline.test.d.ts.map +1 -0
  101. package/dist/pipeline/pipeline.test.js +1065 -0
  102. package/dist/retry/index.d.ts +4 -0
  103. package/dist/retry/index.d.ts.map +1 -0
  104. package/dist/retry/index.js +1 -0
  105. package/dist/retry/policy.d.ts +43 -0
  106. package/dist/retry/policy.d.ts.map +1 -0
  107. package/dist/retry/policy.js +1 -0
  108. package/dist/retry/standard-policy.d.ts +44 -0
  109. package/dist/retry/standard-policy.d.ts.map +1 -0
  110. package/dist/retry/standard-policy.js +102 -0
  111. package/dist/retry/standard-policy.test.d.ts +2 -0
  112. package/dist/retry/standard-policy.test.d.ts.map +1 -0
  113. package/dist/retry/standard-policy.test.js +190 -0
  114. package/dist/schema/index.d.ts +4 -0
  115. package/dist/schema/index.d.ts.map +1 -0
  116. package/dist/schema/index.js +2 -0
  117. package/dist/schema/registry.d.ts +63 -0
  118. package/dist/schema/registry.d.ts.map +1 -0
  119. package/dist/schema/registry.js +171 -0
  120. package/dist/schema/registry.test.d.ts +2 -0
  121. package/dist/schema/registry.test.d.ts.map +1 -0
  122. package/dist/schema/registry.test.js +278 -0
  123. package/dist/schema/types.d.ts +158 -0
  124. package/dist/schema/types.d.ts.map +1 -0
  125. package/dist/schema/types.js +74 -0
  126. package/dist/schema/types.test.d.ts +2 -0
  127. package/dist/schema/types.test.d.ts.map +1 -0
  128. package/dist/schema/types.test.js +243 -0
  129. package/dist/topology/builder.d.ts +66 -0
  130. package/dist/topology/builder.d.ts.map +1 -0
  131. package/dist/topology/builder.js +156 -0
  132. package/dist/topology/builder.test.d.ts +2 -0
  133. package/dist/topology/builder.test.d.ts.map +1 -0
  134. package/dist/topology/builder.test.js +222 -0
  135. package/dist/topology/index.d.ts +5 -0
  136. package/dist/topology/index.d.ts.map +1 -0
  137. package/dist/topology/index.js +2 -0
  138. package/dist/topology/types.d.ts +76 -0
  139. package/dist/topology/types.d.ts.map +1 -0
  140. package/dist/topology/types.js +18 -0
  141. package/dist/transport/capabilities.d.ts +66 -0
  142. package/dist/transport/capabilities.d.ts.map +1 -0
  143. package/dist/transport/capabilities.js +18 -0
  144. package/dist/transport/connection-manager.d.ts +95 -0
  145. package/dist/transport/connection-manager.d.ts.map +1 -0
  146. package/dist/transport/connection-manager.js +144 -0
  147. package/dist/transport/index.d.ts +11 -0
  148. package/dist/transport/index.d.ts.map +1 -0
  149. package/dist/transport/index.js +5 -0
  150. package/dist/transport/local/local-transport.d.ts +62 -0
  151. package/dist/transport/local/local-transport.d.ts.map +1 -0
  152. package/dist/transport/local/local-transport.js +241 -0
  153. package/dist/transport/local/local-transport.test.d.ts +2 -0
  154. package/dist/transport/local/local-transport.test.d.ts.map +1 -0
  155. package/dist/transport/local/local-transport.test.js +192 -0
  156. package/dist/transport/multi/multi-transport.d.ts +94 -0
  157. package/dist/transport/multi/multi-transport.d.ts.map +1 -0
  158. package/dist/transport/multi/multi-transport.js +184 -0
  159. package/dist/transport/multi/multi-transport.test.d.ts +2 -0
  160. package/dist/transport/multi/multi-transport.test.d.ts.map +1 -0
  161. package/dist/transport/multi/multi-transport.test.js +236 -0
  162. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +73 -0
  163. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -0
  164. package/dist/transport/rabbitmq/rabbitmq-transport.js +518 -0
  165. package/dist/transport/transport.d.ts +152 -0
  166. package/dist/transport/transport.d.ts.map +1 -0
  167. package/dist/transport/transport.js +1 -0
  168. package/dist/types/common.d.ts +41 -0
  169. package/dist/types/common.d.ts.map +1 -0
  170. package/dist/types/common.js +12 -0
  171. package/dist/types/envelope.d.ts +82 -0
  172. package/dist/types/envelope.d.ts.map +1 -0
  173. package/dist/types/envelope.js +36 -0
  174. package/dist/types/event.d.ts +96 -0
  175. package/dist/types/event.d.ts.map +1 -0
  176. package/dist/types/event.js +26 -0
  177. package/dist/types/event.test.d.ts +2 -0
  178. package/dist/types/event.test.d.ts.map +1 -0
  179. package/dist/types/event.test.js +130 -0
  180. package/dist/types/index.d.ts +9 -0
  181. package/dist/types/index.d.ts.map +1 -0
  182. package/dist/types/index.js +4 -0
  183. package/dist/types/subscriber.d.ts +207 -0
  184. package/dist/types/subscriber.d.ts.map +1 -0
  185. package/dist/types/subscriber.js +99 -0
  186. package/examples/config.ts +126 -0
  187. package/examples/event.ts +26 -0
  188. package/examples/order-event.json +19 -0
  189. package/package.json +66 -0
  190. package/src/checkpoint/context.test.ts +510 -0
  191. package/src/checkpoint/context.ts +213 -0
  192. package/src/checkpoint/index.ts +30 -0
  193. package/src/checkpoint/stores/memory.ts +47 -0
  194. package/src/checkpoint/stores/noop.ts +22 -0
  195. package/src/checkpoint/stores/stores.test.ts +177 -0
  196. package/src/checkpoint/types.ts +147 -0
  197. package/src/codec/codec.ts +42 -0
  198. package/src/codec/header-aware-codec.ts +41 -0
  199. package/src/codec/index.ts +11 -0
  200. package/src/codec/json-codec.ts +69 -0
  201. package/src/codec/rabbitmq-codec.test.ts +516 -0
  202. package/src/codec/rabbitmq-codec.ts +336 -0
  203. package/src/core/fanout.test.ts +1351 -0
  204. package/src/core/fanout.ts +184 -0
  205. package/src/core/index.ts +12 -0
  206. package/src/core/matador.test.ts +575 -0
  207. package/src/core/matador.ts +344 -0
  208. package/src/core/shutdown.test.ts +853 -0
  209. package/src/core/shutdown.ts +165 -0
  210. package/src/errors/checkpoint-errors.ts +62 -0
  211. package/src/errors/has-description.ts +25 -0
  212. package/src/errors/index.ts +57 -0
  213. package/src/errors/matador-errors.ts +477 -0
  214. package/src/errors/retry-errors.ts +134 -0
  215. package/src/hooks/index.ts +15 -0
  216. package/src/hooks/safe-hooks.ts +223 -0
  217. package/src/hooks/types.ts +248 -0
  218. package/src/index.ts +231 -0
  219. package/src/pipeline/index.ts +2 -0
  220. package/src/pipeline/pipeline.test.ts +1317 -0
  221. package/src/pipeline/pipeline.ts +371 -0
  222. package/src/retry/index.ts +4 -0
  223. package/src/retry/policy.ts +46 -0
  224. package/src/retry/standard-policy.test.ts +282 -0
  225. package/src/retry/standard-policy.ts +156 -0
  226. package/src/schema/index.ts +16 -0
  227. package/src/schema/registry.test.ts +339 -0
  228. package/src/schema/registry.ts +226 -0
  229. package/src/schema/types.test.ts +281 -0
  230. package/src/schema/types.ts +217 -0
  231. package/src/topology/builder.test.ts +275 -0
  232. package/src/topology/builder.ts +199 -0
  233. package/src/topology/index.ts +15 -0
  234. package/src/topology/types.ts +109 -0
  235. package/src/transport/capabilities.ts +88 -0
  236. package/src/transport/connection-manager.ts +218 -0
  237. package/src/transport/index.ts +42 -0
  238. package/src/transport/local/local-transport.test.ts +262 -0
  239. package/src/transport/local/local-transport.ts +327 -0
  240. package/src/transport/multi/multi-transport.test.ts +320 -0
  241. package/src/transport/multi/multi-transport.ts +294 -0
  242. package/src/transport/rabbitmq/rabbitmq-transport.ts +753 -0
  243. package/src/transport/transport.ts +200 -0
  244. package/src/types/common.ts +53 -0
  245. package/src/types/envelope.ts +152 -0
  246. package/src/types/event.test.ts +157 -0
  247. package/src/types/event.ts +118 -0
  248. package/src/types/index.ts +52 -0
  249. package/src/types/subscriber.ts +310 -0
  250. package/test/e2e/multi-transport.e2e.test.ts +236 -0
  251. package/test/e2e/rabbitmq-transport.e2e.test.ts +327 -0
  252. package/test/e2e/transport-compliance.e2e.test.ts +505 -0
  253. package/test/integration/matador.integration.test.ts +634 -0
  254. package/tsconfig.json +29 -0
  255. package/tsconfig.tsbuildinfo +1 -0
  256. 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
+ }