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