@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,327 @@
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeAll,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ it,
9
+ } from 'bun:test';
10
+ import {
11
+ RabbitMQContainer,
12
+ type StartedRabbitMQContainer,
13
+ } from '@testcontainers/rabbitmq';
14
+ import type { Subscription } from '../../src/transport/index.js';
15
+ import { RabbitMQTransport } from '../../src/transport/rabbitmq/rabbitmq-transport.js';
16
+ import {
17
+ createTestEnvelope,
18
+ createTestTopology,
19
+ } from './transport-compliance.e2e.test.js';
20
+
21
+ // Skip tests if docker is not available
22
+ const SKIP_E2E = process.env.SKIP_E2E_TESTS === 'true';
23
+
24
+ describe.skipIf(SKIP_E2E)('RabbitMQ Transport E2E', () => {
25
+ let container: StartedRabbitMQContainer;
26
+ let connectionUrl: string;
27
+
28
+ beforeAll(async () => {
29
+ // Start RabbitMQ container
30
+ container = await new RabbitMQContainer('rabbitmq:3.13-management')
31
+ .withExposedPorts(5672, 15672)
32
+ .start();
33
+
34
+ connectionUrl = container.getAmqpUrl();
35
+ console.log(`RabbitMQ container started at ${connectionUrl}`);
36
+ }, 120_000);
37
+
38
+ afterAll(async () => {
39
+ if (container) {
40
+ await container.stop();
41
+ }
42
+ });
43
+
44
+ describe('RabbitMQ-specific features', () => {
45
+ let transport: RabbitMQTransport;
46
+ let subscriptions: Subscription[] = [];
47
+
48
+ beforeEach(async () => {
49
+ transport = new RabbitMQTransport({
50
+ url: connectionUrl,
51
+ quorumQueues: false, // Use classic queues for faster tests
52
+ defaultPrefetch: 5,
53
+ });
54
+ await transport.connect();
55
+ await transport.applyTopology(createTestTopology(`test-${Date.now()}`));
56
+ subscriptions = [];
57
+ });
58
+
59
+ afterEach(async () => {
60
+ for (const sub of subscriptions) {
61
+ if (sub.isActive) {
62
+ await sub.unsubscribe();
63
+ }
64
+ }
65
+ if (transport.isConnected()) {
66
+ await transport.disconnect();
67
+ }
68
+ });
69
+
70
+ it('should report correct capabilities', () => {
71
+ expect(transport.name).toBe('rabbitmq');
72
+ expect(transport.capabilities.deliveryModes).toContain('at-least-once');
73
+ expect(transport.capabilities.deadLetterRouting).toBe('native');
74
+ expect(transport.capabilities.attemptTracking).toBe(true);
75
+ expect(transport.capabilities.concurrencyModel).toBe('prefetch');
76
+ expect(transport.capabilities.priorities).toBe(true);
77
+ // delayedMessages depends on plugin availability
78
+ });
79
+
80
+ it('should handle message priority', async () => {
81
+ const topology = createTestTopology(`priority-${Date.now()}`);
82
+ topology.queues[0] = { ...topology.queues[0], priorities: true };
83
+
84
+ await transport.applyTopology(topology);
85
+ const queueName = `${topology.namespace}.events`;
86
+
87
+ const receivedOrder: number[] = [];
88
+
89
+ const subscription = await transport.subscribe(
90
+ queueName,
91
+ async (env, receipt) => {
92
+ receivedOrder.push((env.data as { priority: number }).priority);
93
+ await transport.complete(receipt);
94
+ },
95
+ );
96
+ subscriptions.push(subscription);
97
+
98
+ // Send messages with different priorities
99
+ await transport.send(
100
+ queueName,
101
+ createTestEnvelope({ eventKey: 'priority.1' }),
102
+ { priority: 1 },
103
+ );
104
+ await transport.send(
105
+ queueName,
106
+ createTestEnvelope({ eventKey: 'priority.5' }),
107
+ { priority: 5 },
108
+ );
109
+ await transport.send(
110
+ queueName,
111
+ createTestEnvelope({ eventKey: 'priority.10' }),
112
+ { priority: 10 },
113
+ );
114
+
115
+ // Wait for all messages
116
+ await waitFor(() => receivedOrder.length >= 3, 5000);
117
+
118
+ // Priority is not strictly guaranteed in RabbitMQ,
119
+ // but higher priority messages should generally come first
120
+ expect(receivedOrder.length).toBe(3);
121
+ });
122
+
123
+ it('should track attempt number in headers', async () => {
124
+ const topology = createTestTopology(`attempts-${Date.now()}`);
125
+ await transport.applyTopology(topology);
126
+ const queueName = `${topology.namespace}.events`;
127
+
128
+ let attemptNumber = 0;
129
+
130
+ const subscription = await transport.subscribe(
131
+ queueName,
132
+ async (_env, receipt) => {
133
+ attemptNumber = receipt.attemptNumber;
134
+ await transport.complete(receipt);
135
+ },
136
+ );
137
+ subscriptions.push(subscription);
138
+
139
+ await transport.send(queueName, createTestEnvelope());
140
+
141
+ await waitFor(() => attemptNumber > 0, 5000);
142
+
143
+ expect(attemptNumber).toBe(1);
144
+ });
145
+
146
+ it('should handle prefetch/concurrency per queue', async () => {
147
+ const topology = createTestTopology(`prefetch-${Date.now()}`);
148
+ await transport.applyTopology(topology);
149
+ const queueName = `${topology.namespace}.events`;
150
+
151
+ const processing = new Set<string>();
152
+ let maxConcurrent = 0;
153
+
154
+ const subscription = await transport.subscribe(
155
+ queueName,
156
+ async (env, receipt) => {
157
+ processing.add(env.id);
158
+ maxConcurrent = Math.max(maxConcurrent, processing.size);
159
+
160
+ // Simulate processing time
161
+ await new Promise((resolve) => setTimeout(resolve, 100));
162
+
163
+ processing.delete(env.id);
164
+ await transport.complete(receipt);
165
+ },
166
+ { concurrency: 3 },
167
+ );
168
+ subscriptions.push(subscription);
169
+
170
+ // Send more messages than prefetch allows
171
+ for (let i = 0; i < 10; i++) {
172
+ await transport.send(queueName, createTestEnvelope({ id: `msg-${i}` }));
173
+ }
174
+
175
+ // Wait for all messages to be processed
176
+ await waitFor(() => processing.size === 0, 10000);
177
+
178
+ // Should never exceed prefetch
179
+ expect(maxConcurrent).toBeLessThanOrEqual(3);
180
+ });
181
+
182
+ it('should throw when sending delayed message without plugin', async () => {
183
+ // Standard RabbitMQ container doesn't have delayed message plugin
184
+ const topology = createTestTopology(`delay-${Date.now()}`);
185
+ await transport.applyTopology(topology);
186
+ const queueName = `${topology.namespace}.events`;
187
+
188
+ // Should throw because plugin is not available
189
+ await expect(
190
+ transport.send(queueName, createTestEnvelope(), { delay: 1000 }),
191
+ ).rejects.toThrow('delayed message exchange plugin');
192
+ });
193
+
194
+ it('should report delayedMessages capability based on plugin', () => {
195
+ // Standard RabbitMQ without plugin should report false
196
+ expect(transport.capabilities.delayedMessages).toBe(false);
197
+ });
198
+ });
199
+
200
+ describe('reconnection behavior', () => {
201
+ it('should handle disconnect and reconnect', async () => {
202
+ const transport = new RabbitMQTransport({
203
+ url: connectionUrl,
204
+ connection: {
205
+ maxReconnectAttempts: 3,
206
+ initialReconnectDelay: 100,
207
+ },
208
+ });
209
+
210
+ await transport.connect();
211
+ expect(transport.isConnected()).toBe(true);
212
+
213
+ await transport.disconnect();
214
+ expect(transport.isConnected()).toBe(false);
215
+
216
+ // Reconnect
217
+ await transport.connect();
218
+ expect(transport.isConnected()).toBe(true);
219
+
220
+ await transport.disconnect();
221
+ });
222
+ });
223
+
224
+ describe('channel per queue isolation', () => {
225
+ let transport: RabbitMQTransport;
226
+ const subscriptions: Subscription[] = [];
227
+
228
+ beforeEach(async () => {
229
+ transport = new RabbitMQTransport({
230
+ url: connectionUrl,
231
+ quorumQueues: false,
232
+ defaultPrefetch: 2,
233
+ });
234
+ await transport.connect();
235
+ });
236
+
237
+ afterEach(async () => {
238
+ for (const sub of subscriptions) {
239
+ if (sub.isActive) {
240
+ await sub.unsubscribe();
241
+ }
242
+ }
243
+ if (transport.isConnected()) {
244
+ await transport.disconnect();
245
+ }
246
+ });
247
+
248
+ it('should have independent concurrency per queue', async () => {
249
+ const topology = createTestTopology(`isolation-${Date.now()}`);
250
+ await transport.applyTopology(topology);
251
+
252
+ const queue1 = `${topology.namespace}.events`;
253
+ const queue2 = `${topology.namespace}.notifications`;
254
+
255
+ const queue1Processing = new Set<string>();
256
+ const queue2Processing = new Set<string>();
257
+ let queue1MaxConcurrent = 0;
258
+ let queue2MaxConcurrent = 0;
259
+
260
+ // Subscribe to both queues with different concurrency
261
+ const sub1 = await transport.subscribe(
262
+ queue1,
263
+ async (env, receipt) => {
264
+ queue1Processing.add(env.id);
265
+ queue1MaxConcurrent = Math.max(
266
+ queue1MaxConcurrent,
267
+ queue1Processing.size,
268
+ );
269
+ await new Promise((resolve) => setTimeout(resolve, 50));
270
+ queue1Processing.delete(env.id);
271
+ await transport.complete(receipt);
272
+ },
273
+ { concurrency: 2 },
274
+ );
275
+ subscriptions.push(sub1);
276
+
277
+ const sub2 = await transport.subscribe(
278
+ queue2,
279
+ async (env, receipt) => {
280
+ queue2Processing.add(env.id);
281
+ queue2MaxConcurrent = Math.max(
282
+ queue2MaxConcurrent,
283
+ queue2Processing.size,
284
+ );
285
+ await new Promise((resolve) => setTimeout(resolve, 50));
286
+ queue2Processing.delete(env.id);
287
+ await transport.complete(receipt);
288
+ },
289
+ { concurrency: 3 },
290
+ );
291
+ subscriptions.push(sub2);
292
+
293
+ // Send messages to both queues
294
+ for (let i = 0; i < 6; i++) {
295
+ await transport.send(queue1, createTestEnvelope({ id: `q1-${i}` }));
296
+ await transport.send(queue2, createTestEnvelope({ id: `q2-${i}` }));
297
+ }
298
+
299
+ // Wait for all to complete
300
+ await waitFor(
301
+ () => queue1Processing.size === 0 && queue2Processing.size === 0,
302
+ 10000,
303
+ );
304
+
305
+ // Each queue should respect its own prefetch
306
+ expect(queue1MaxConcurrent).toBeLessThanOrEqual(2);
307
+ expect(queue2MaxConcurrent).toBeLessThanOrEqual(3);
308
+ });
309
+ });
310
+ });
311
+
312
+ /**
313
+ * Waits for a condition to be true, with timeout.
314
+ */
315
+ async function waitFor(
316
+ condition: () => boolean,
317
+ timeoutMs: number,
318
+ intervalMs = 50,
319
+ ): Promise<void> {
320
+ const startTime = Date.now();
321
+ while (!condition()) {
322
+ if (Date.now() - startTime > timeoutMs) {
323
+ throw new Error(`Timeout waiting for condition after ${timeoutMs}ms`);
324
+ }
325
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
326
+ }
327
+ }