@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,853 @@
1
+ import { beforeEach, describe, expect, it, mock } from 'bun:test';
2
+ import type { Logger } from '../hooks/index.js';
3
+ import { ShutdownManager, type ShutdownState } from './shutdown.js';
4
+
5
+ describe('ShutdownManager', () => {
6
+ let getEnqueueCount: () => number;
7
+ let stopReceiving: () => Promise<void>;
8
+ let disconnectTransport: () => Promise<void>;
9
+ let manager: ShutdownManager;
10
+ let enqueuedCount: number;
11
+
12
+ beforeEach(() => {
13
+ enqueuedCount = 0;
14
+ getEnqueueCount = mock(() => enqueuedCount);
15
+ stopReceiving = mock(async () => {});
16
+ disconnectTransport = mock(async () => {});
17
+ });
18
+
19
+ describe('static factory', () => {
20
+ it('should create instance via static create method', () => {
21
+ manager = new ShutdownManager(
22
+ getEnqueueCount,
23
+ stopReceiving,
24
+ disconnectTransport,
25
+ );
26
+
27
+ expect(manager).toBeInstanceOf(ShutdownManager);
28
+ expect(manager.state).toBe('running');
29
+ });
30
+
31
+ it('should accept partial config overrides', () => {
32
+ manager = new ShutdownManager(
33
+ getEnqueueCount,
34
+ stopReceiving,
35
+ disconnectTransport,
36
+ { gracefulShutdownTimeout: 5000 },
37
+ );
38
+
39
+ expect(manager).toBeInstanceOf(ShutdownManager);
40
+ });
41
+ });
42
+
43
+ describe('initial state', () => {
44
+ beforeEach(() => {
45
+ manager = new ShutdownManager(
46
+ getEnqueueCount,
47
+ stopReceiving,
48
+ disconnectTransport,
49
+ );
50
+ });
51
+
52
+ it('should start in running state', () => {
53
+ expect(manager.state).toBe('running');
54
+ });
55
+
56
+ it('should allow enqueue initially', () => {
57
+ expect(manager.isEnqueueAllowed).toBe(true);
58
+ });
59
+
60
+ it('should report idle when no events processing', () => {
61
+ const state = manager.getHandlersState();
62
+ expect(state.isIdle).toBe(true);
63
+ expect(state.eventsBeingProcessed).toBe(0);
64
+ expect(state.eventsBeingEnqueued).toBe(0);
65
+ });
66
+ });
67
+
68
+ describe('getHandlersState', () => {
69
+ beforeEach(() => {
70
+ manager = new ShutdownManager(
71
+ getEnqueueCount,
72
+ stopReceiving,
73
+ disconnectTransport,
74
+ );
75
+ });
76
+
77
+ it('should return correct handler state with no activity', () => {
78
+ const state = manager.getHandlersState();
79
+
80
+ expect(state.eventsBeingProcessed).toBe(0);
81
+ expect(state.eventsBeingEnqueued).toBe(0);
82
+ expect(state.isIdle).toBe(true);
83
+ });
84
+
85
+ it('should track events being processed', () => {
86
+ manager.incrementProcessing();
87
+ manager.incrementProcessing();
88
+
89
+ const state = manager.getHandlersState();
90
+
91
+ expect(state.eventsBeingProcessed).toBe(2);
92
+ expect(state.isIdle).toBe(false);
93
+ });
94
+
95
+ it('should track events being enqueued via callback', () => {
96
+ enqueuedCount = 3;
97
+
98
+ const state = manager.getHandlersState();
99
+
100
+ expect(state.eventsBeingEnqueued).toBe(3);
101
+ expect(state.isIdle).toBe(false);
102
+ });
103
+
104
+ it('should report not idle when processing exists', () => {
105
+ manager.incrementProcessing();
106
+ enqueuedCount = 0;
107
+
108
+ const state = manager.getHandlersState();
109
+
110
+ expect(state.isIdle).toBe(false);
111
+ });
112
+
113
+ it('should report not idle when enqueuing exists', () => {
114
+ enqueuedCount = 2;
115
+
116
+ const state = manager.getHandlersState();
117
+
118
+ expect(state.isIdle).toBe(false);
119
+ });
120
+
121
+ it('should report idle only when both counters are zero', () => {
122
+ manager.incrementProcessing();
123
+ manager.incrementProcessing();
124
+ enqueuedCount = 1;
125
+
126
+ let state = manager.getHandlersState();
127
+ expect(state.isIdle).toBe(false);
128
+
129
+ manager.decrementProcessing();
130
+ state = manager.getHandlersState();
131
+ expect(state.isIdle).toBe(false);
132
+
133
+ manager.decrementProcessing();
134
+ state = manager.getHandlersState();
135
+ expect(state.isIdle).toBe(false);
136
+
137
+ enqueuedCount = 0;
138
+ state = manager.getHandlersState();
139
+ expect(state.isIdle).toBe(true);
140
+ });
141
+ });
142
+
143
+ describe('processing counter', () => {
144
+ beforeEach(() => {
145
+ manager = new ShutdownManager(
146
+ getEnqueueCount,
147
+ stopReceiving,
148
+ disconnectTransport,
149
+ );
150
+ });
151
+
152
+ it('should increment processing counter', () => {
153
+ manager.incrementProcessing();
154
+ expect(manager.getHandlersState().eventsBeingProcessed).toBe(1);
155
+
156
+ manager.incrementProcessing();
157
+ expect(manager.getHandlersState().eventsBeingProcessed).toBe(2);
158
+ });
159
+
160
+ it('should decrement processing counter', () => {
161
+ manager.incrementProcessing();
162
+ manager.incrementProcessing();
163
+ manager.incrementProcessing();
164
+
165
+ manager.decrementProcessing();
166
+ expect(manager.getHandlersState().eventsBeingProcessed).toBe(2);
167
+
168
+ manager.decrementProcessing();
169
+ expect(manager.getHandlersState().eventsBeingProcessed).toBe(1);
170
+ });
171
+ });
172
+
173
+ describe('graceful shutdown', () => {
174
+ beforeEach(() => {
175
+ manager = new ShutdownManager(
176
+ getEnqueueCount,
177
+ stopReceiving,
178
+ disconnectTransport,
179
+ );
180
+ });
181
+
182
+ it('should transition through all shutdown states', async () => {
183
+ const states: ShutdownState[] = [];
184
+
185
+ const checkState = async () => {
186
+ states.push(manager.state);
187
+ };
188
+
189
+ stopReceiving = mock(async () => {
190
+ await checkState();
191
+ });
192
+
193
+ disconnectTransport = mock(async () => {
194
+ await checkState();
195
+ });
196
+
197
+ manager = new ShutdownManager(
198
+ getEnqueueCount,
199
+ stopReceiving,
200
+ disconnectTransport,
201
+ );
202
+
203
+ const initialState = manager.state;
204
+ await manager.shutdown();
205
+ const finalState = manager.state;
206
+
207
+ expect(initialState).toBe('running');
208
+ expect(states[0]).toBe('stopping-receive');
209
+ expect(states[1]).toBe('disconnecting');
210
+ expect(finalState).toBe('stopped');
211
+ });
212
+
213
+ it('should call stopReceiving during shutdown', async () => {
214
+ await manager.shutdown();
215
+
216
+ expect(stopReceiving).toHaveBeenCalledTimes(1);
217
+ });
218
+
219
+ it('should call disconnectTransport during shutdown', async () => {
220
+ await manager.shutdown();
221
+
222
+ expect(disconnectTransport).toHaveBeenCalledTimes(1);
223
+ });
224
+
225
+ it('should disable enqueue during shutdown', async () => {
226
+ expect(manager.isEnqueueAllowed).toBe(true);
227
+
228
+ await manager.shutdown();
229
+
230
+ expect(manager.isEnqueueAllowed).toBe(false);
231
+ });
232
+
233
+ it('should wait for in-flight messages to complete', async () => {
234
+ manager.incrementProcessing();
235
+ manager.incrementProcessing();
236
+
237
+ const shutdownPromise = manager.shutdown();
238
+
239
+ expect(manager.state).toBe('stopping-receive');
240
+
241
+ await new Promise((resolve) => setTimeout(resolve, 50));
242
+
243
+ expect(manager.state).toBe('waiting-handlers');
244
+
245
+ manager.decrementProcessing();
246
+
247
+ await new Promise((resolve) => setTimeout(resolve, 50));
248
+
249
+ expect(manager.state).toBe('waiting-handlers');
250
+
251
+ manager.decrementProcessing();
252
+
253
+ await shutdownPromise;
254
+
255
+ expect(manager.state).toBe('stopped');
256
+ });
257
+
258
+ it('should wait for enqueued messages to complete', async () => {
259
+ enqueuedCount = 2;
260
+
261
+ const shutdownPromise = manager.shutdown();
262
+
263
+ await new Promise((resolve) => setTimeout(resolve, 50));
264
+
265
+ expect(manager.state).toBe('waiting-handlers');
266
+
267
+ enqueuedCount = 0;
268
+
269
+ await shutdownPromise;
270
+
271
+ expect(manager.state).toBe('stopped');
272
+ });
273
+
274
+ it('should wait for both processing and enqueued to complete', async () => {
275
+ manager.incrementProcessing();
276
+ enqueuedCount = 1;
277
+
278
+ const shutdownPromise = manager.shutdown();
279
+
280
+ await new Promise((resolve) => setTimeout(resolve, 50));
281
+
282
+ expect(manager.state).toBe('waiting-handlers');
283
+
284
+ manager.decrementProcessing();
285
+
286
+ await new Promise((resolve) => setTimeout(resolve, 50));
287
+
288
+ expect(manager.state).toBe('waiting-handlers');
289
+
290
+ enqueuedCount = 0;
291
+
292
+ await shutdownPromise;
293
+
294
+ expect(manager.state).toBe('stopped');
295
+ });
296
+ });
297
+
298
+ describe('shutdown timeout', () => {
299
+ it('should force shutdown after timeout', async () => {
300
+ const logs: string[] = [];
301
+ const mockLogger: Logger = {
302
+ debug: (msg: string) => logs.push(`debug: ${msg}`),
303
+ info: (msg: string) => logs.push(`info: ${msg}`),
304
+ warn: (msg: string) => logs.push(`warn: ${msg}`),
305
+ error: (msg: string) => logs.push(`error: ${msg}`),
306
+ };
307
+
308
+ manager = new ShutdownManager(
309
+ getEnqueueCount,
310
+ stopReceiving,
311
+ disconnectTransport,
312
+ {
313
+ gracefulShutdownTimeout: 100,
314
+ idlePollingInterval: 20,
315
+ logger: mockLogger,
316
+ },
317
+ );
318
+
319
+ manager.incrementProcessing();
320
+ manager.incrementProcessing();
321
+
322
+ await manager.shutdown();
323
+
324
+ expect(manager.state).toBe('stopped');
325
+ expect(manager.getHandlersState().eventsBeingProcessed).toBe(2);
326
+ expect(logs.some((log) => log.includes('Shutdown timeout reached'))).toBe(
327
+ true,
328
+ );
329
+ expect(
330
+ logs.some((log) => log.includes('2 events still processing')),
331
+ ).toBe(true);
332
+ });
333
+
334
+ it('should use custom timeout configuration', async () => {
335
+ const logs: string[] = [];
336
+ const mockLogger: Logger = {
337
+ debug: () => {},
338
+ info: () => {},
339
+ warn: (msg: string) => logs.push(msg),
340
+ error: () => {},
341
+ };
342
+
343
+ manager = new ShutdownManager(
344
+ getEnqueueCount,
345
+ stopReceiving,
346
+ disconnectTransport,
347
+ {
348
+ gracefulShutdownTimeout: 50,
349
+ idlePollingInterval: 10,
350
+ logger: mockLogger,
351
+ },
352
+ );
353
+
354
+ manager.incrementProcessing();
355
+
356
+ const startTime = Date.now();
357
+ await manager.shutdown();
358
+ const duration = Date.now() - startTime;
359
+
360
+ expect(duration).toBeGreaterThanOrEqual(50);
361
+ expect(duration).toBeLessThan(200);
362
+ expect(logs.length).toBeGreaterThan(0);
363
+ });
364
+
365
+ it('should poll at configured interval', async () => {
366
+ let pollCount = 0;
367
+ const countingGetEnqueueCount = mock(() => {
368
+ pollCount++;
369
+ return 1;
370
+ });
371
+
372
+ manager = new ShutdownManager(
373
+ countingGetEnqueueCount,
374
+ stopReceiving,
375
+ disconnectTransport,
376
+ {
377
+ gracefulShutdownTimeout: 100,
378
+ idlePollingInterval: 20,
379
+ },
380
+ );
381
+
382
+ await manager.shutdown();
383
+
384
+ expect(pollCount).toBeGreaterThan(3);
385
+ });
386
+ });
387
+
388
+ describe('idempotent shutdown', () => {
389
+ beforeEach(() => {
390
+ manager = new ShutdownManager(
391
+ getEnqueueCount,
392
+ stopReceiving,
393
+ disconnectTransport,
394
+ );
395
+ });
396
+
397
+ it('should be idempotent when called multiple times', async () => {
398
+ await manager.shutdown();
399
+ expect(manager.state).toBe('stopped');
400
+
401
+ await manager.shutdown();
402
+ expect(manager.state).toBe('stopped');
403
+
404
+ await manager.shutdown();
405
+ expect(manager.state).toBe('stopped');
406
+
407
+ expect(stopReceiving).toHaveBeenCalledTimes(1);
408
+ expect(disconnectTransport).toHaveBeenCalledTimes(1);
409
+ });
410
+
411
+ it('should handle concurrent shutdown calls', async () => {
412
+ const results = await Promise.all([
413
+ manager.shutdown(),
414
+ manager.shutdown(),
415
+ manager.shutdown(),
416
+ ]);
417
+
418
+ expect(manager.state).toBe('stopped');
419
+ expect(results).toHaveLength(3);
420
+ expect(stopReceiving).toHaveBeenCalledTimes(1);
421
+ expect(disconnectTransport).toHaveBeenCalledTimes(1);
422
+ });
423
+
424
+ it('should not restart shutdown sequence if already shutting down', async () => {
425
+ manager.incrementProcessing();
426
+
427
+ const shutdown1 = manager.shutdown();
428
+
429
+ expect(manager.state).toBe('stopping-receive');
430
+
431
+ const shutdown2 = manager.shutdown();
432
+
433
+ manager.decrementProcessing();
434
+
435
+ await Promise.all([shutdown1, shutdown2]);
436
+
437
+ expect(manager.state).toBe('stopped');
438
+ expect(stopReceiving).toHaveBeenCalledTimes(1);
439
+ });
440
+ });
441
+
442
+ describe('forceStop', () => {
443
+ beforeEach(() => {
444
+ manager = new ShutdownManager(
445
+ getEnqueueCount,
446
+ stopReceiving,
447
+ disconnectTransport,
448
+ );
449
+ });
450
+
451
+ it('should stop immediately without waiting', async () => {
452
+ manager.incrementProcessing();
453
+ manager.incrementProcessing();
454
+ enqueuedCount = 5;
455
+
456
+ await manager.forceStop();
457
+
458
+ expect(manager.state).toBe('stopped');
459
+ expect(manager.isEnqueueAllowed).toBe(false);
460
+ expect(manager.getHandlersState().eventsBeingProcessed).toBe(2);
461
+ expect(manager.getHandlersState().eventsBeingEnqueued).toBe(5);
462
+ });
463
+
464
+ it('should call stopReceiving', async () => {
465
+ await manager.forceStop();
466
+
467
+ expect(stopReceiving).toHaveBeenCalledTimes(1);
468
+ });
469
+
470
+ it('should call disconnectTransport', async () => {
471
+ await manager.forceStop();
472
+
473
+ expect(disconnectTransport).toHaveBeenCalledTimes(1);
474
+ });
475
+
476
+ it('should complete much faster than graceful shutdown', async () => {
477
+ manager = new ShutdownManager(
478
+ getEnqueueCount,
479
+ stopReceiving,
480
+ disconnectTransport,
481
+ {
482
+ gracefulShutdownTimeout: 1000,
483
+ },
484
+ );
485
+
486
+ manager.incrementProcessing();
487
+
488
+ const startTime = Date.now();
489
+ await manager.forceStop();
490
+ const duration = Date.now() - startTime;
491
+
492
+ expect(duration).toBeLessThan(100);
493
+ });
494
+
495
+ it('should disable enqueue', async () => {
496
+ expect(manager.isEnqueueAllowed).toBe(true);
497
+
498
+ await manager.forceStop();
499
+
500
+ expect(manager.isEnqueueAllowed).toBe(false);
501
+ });
502
+ });
503
+
504
+ describe('state transitions', () => {
505
+ beforeEach(() => {
506
+ manager = new ShutdownManager(
507
+ getEnqueueCount,
508
+ stopReceiving,
509
+ disconnectTransport,
510
+ );
511
+ });
512
+
513
+ it('should follow correct sequence: running -> stopping-receive -> waiting-handlers -> stopping-enqueue -> disconnecting -> stopped', async () => {
514
+ const states: ShutdownState[] = [];
515
+
516
+ states.push(manager.state);
517
+
518
+ const originalStopReceiving = stopReceiving;
519
+ const originalDisconnect = disconnectTransport;
520
+
521
+ stopReceiving = mock(async () => {
522
+ await originalStopReceiving();
523
+ states.push(manager.state);
524
+ });
525
+
526
+ disconnectTransport = mock(async () => {
527
+ await originalDisconnect();
528
+ states.push(manager.state);
529
+ });
530
+
531
+ manager = new ShutdownManager(
532
+ getEnqueueCount,
533
+ stopReceiving,
534
+ disconnectTransport,
535
+ );
536
+
537
+ manager.incrementProcessing();
538
+
539
+ const shutdownPromise = manager.shutdown();
540
+
541
+ await new Promise((resolve) => setTimeout(resolve, 50));
542
+ states.push(manager.state);
543
+
544
+ manager.decrementProcessing();
545
+
546
+ await shutdownPromise;
547
+ states.push(manager.state);
548
+
549
+ expect(states[0]).toBe('running');
550
+ expect(states[1]).toBe('stopping-receive');
551
+ expect(states[2]).toBe('waiting-handlers');
552
+ expect(states[3]).toBe('disconnecting');
553
+ expect(states[4]).toBe('stopped');
554
+ });
555
+
556
+ it('should remain in waiting-handlers while events are processing', async () => {
557
+ manager.incrementProcessing();
558
+
559
+ const shutdownPromise = manager.shutdown();
560
+
561
+ await new Promise((resolve) => setTimeout(resolve, 50));
562
+ expect(manager.state).toBe('waiting-handlers');
563
+
564
+ await new Promise((resolve) => setTimeout(resolve, 50));
565
+ expect(manager.state).toBe('waiting-handlers');
566
+
567
+ manager.decrementProcessing();
568
+
569
+ await shutdownPromise;
570
+ expect(manager.state).toBe('stopped');
571
+ });
572
+ });
573
+
574
+ describe('event handler deregistration during shutdown', () => {
575
+ beforeEach(() => {
576
+ manager = new ShutdownManager(
577
+ getEnqueueCount,
578
+ stopReceiving,
579
+ disconnectTransport,
580
+ );
581
+ });
582
+
583
+ it('should block new enqueue operations after stopping-enqueue state', async () => {
584
+ manager = new ShutdownManager(
585
+ getEnqueueCount,
586
+ stopReceiving,
587
+ disconnectTransport,
588
+ {
589
+ idlePollingInterval: 20,
590
+ },
591
+ );
592
+
593
+ manager.incrementProcessing();
594
+
595
+ expect(manager.isEnqueueAllowed).toBe(true);
596
+
597
+ const shutdownPromise = manager.shutdown();
598
+
599
+ await new Promise((resolve) => setTimeout(resolve, 50));
600
+
601
+ expect(manager.state).toBe('waiting-handlers');
602
+ expect(manager.isEnqueueAllowed).toBe(true);
603
+
604
+ manager.decrementProcessing();
605
+
606
+ await shutdownPromise;
607
+
608
+ expect(manager.state).toBe('stopped');
609
+ expect(manager.isEnqueueAllowed).toBe(false);
610
+ });
611
+
612
+ it('should allow completion of in-flight events during shutdown', async () => {
613
+ manager.incrementProcessing();
614
+ manager.incrementProcessing();
615
+ const initialCount = manager.getHandlersState().eventsBeingProcessed;
616
+
617
+ const shutdownPromise = manager.shutdown();
618
+
619
+ await new Promise((resolve) => setTimeout(resolve, 50));
620
+
621
+ manager.decrementProcessing();
622
+
623
+ expect(manager.getHandlersState().eventsBeingProcessed).toBe(
624
+ initialCount - 1,
625
+ );
626
+
627
+ manager.decrementProcessing();
628
+
629
+ await shutdownPromise;
630
+
631
+ expect(manager.getHandlersState().eventsBeingProcessed).toBe(0);
632
+ expect(manager.state).toBe('stopped');
633
+ });
634
+ });
635
+
636
+ describe('configuration', () => {
637
+ it('should use default configuration when none provided', () => {
638
+ manager = new ShutdownManager(
639
+ getEnqueueCount,
640
+ stopReceiving,
641
+ disconnectTransport,
642
+ );
643
+
644
+ expect(manager).toBeInstanceOf(ShutdownManager);
645
+ });
646
+
647
+ it('should accept custom gracefulShutdownTimeout', async () => {
648
+ const logs: string[] = [];
649
+ const mockLogger: Logger = {
650
+ debug: () => {},
651
+ info: () => {},
652
+ warn: (msg: string) => logs.push(msg),
653
+ error: () => {},
654
+ };
655
+
656
+ manager = new ShutdownManager(
657
+ getEnqueueCount,
658
+ stopReceiving,
659
+ disconnectTransport,
660
+ {
661
+ gracefulShutdownTimeout: 10,
662
+ idlePollingInterval: 5,
663
+ logger: mockLogger,
664
+ },
665
+ );
666
+
667
+ manager.incrementProcessing();
668
+
669
+ const startTime = Date.now();
670
+ await manager.shutdown();
671
+ const duration = Date.now() - startTime;
672
+
673
+ expect(duration).toBeGreaterThanOrEqual(10);
674
+ expect(duration).toBeLessThan(100);
675
+ });
676
+
677
+ it('should accept custom logger', async () => {
678
+ const logs: string[] = [];
679
+ const mockLogger: Logger = {
680
+ debug: (msg: string) => logs.push(msg),
681
+ info: (msg: string) => logs.push(msg),
682
+ warn: (msg: string) => logs.push(msg),
683
+ error: (msg: string) => logs.push(msg),
684
+ };
685
+
686
+ manager = new ShutdownManager(
687
+ getEnqueueCount,
688
+ stopReceiving,
689
+ disconnectTransport,
690
+ {
691
+ gracefulShutdownTimeout: 50,
692
+ logger: mockLogger,
693
+ },
694
+ );
695
+
696
+ manager.incrementProcessing();
697
+
698
+ await manager.shutdown();
699
+
700
+ expect(logs.some((log) => log.includes('Shutdown timeout reached'))).toBe(
701
+ true,
702
+ );
703
+ });
704
+
705
+ it('should accept custom idlePollingInterval', async () => {
706
+ let pollCount = 0;
707
+ const countingGetEnqueueCount = mock(() => {
708
+ pollCount++;
709
+ if (pollCount > 2) {
710
+ return 0;
711
+ }
712
+ return 1;
713
+ });
714
+
715
+ manager = new ShutdownManager(
716
+ countingGetEnqueueCount,
717
+ stopReceiving,
718
+ disconnectTransport,
719
+ {
720
+ idlePollingInterval: 10,
721
+ gracefulShutdownTimeout: 1000,
722
+ },
723
+ );
724
+
725
+ const startTime = Date.now();
726
+ await manager.shutdown();
727
+ const duration = Date.now() - startTime;
728
+
729
+ expect(pollCount).toBeGreaterThanOrEqual(2);
730
+ expect(duration).toBeGreaterThanOrEqual(20);
731
+ expect(duration).toBeLessThan(200);
732
+ });
733
+ });
734
+
735
+ describe('error handling', () => {
736
+ it('should handle errors in stopReceiving', async () => {
737
+ const error = new Error('Stop receiving failed');
738
+ stopReceiving = mock(async () => {
739
+ throw error;
740
+ });
741
+
742
+ manager = new ShutdownManager(
743
+ getEnqueueCount,
744
+ stopReceiving,
745
+ disconnectTransport,
746
+ );
747
+
748
+ await expect(manager.shutdown()).rejects.toThrow('Stop receiving failed');
749
+ });
750
+
751
+ it('should handle errors in disconnectTransport', async () => {
752
+ const error = new Error('Disconnect failed');
753
+ disconnectTransport = mock(async () => {
754
+ throw error;
755
+ });
756
+
757
+ manager = new ShutdownManager(
758
+ getEnqueueCount,
759
+ stopReceiving,
760
+ disconnectTransport,
761
+ );
762
+
763
+ await expect(manager.shutdown()).rejects.toThrow('Disconnect failed');
764
+ });
765
+
766
+ it('should handle errors in getEnqueueCount callback', async () => {
767
+ getEnqueueCount = mock(() => {
768
+ throw new Error('Count failed');
769
+ });
770
+
771
+ manager = new ShutdownManager(
772
+ getEnqueueCount,
773
+ stopReceiving,
774
+ disconnectTransport,
775
+ );
776
+
777
+ expect(() => manager.getHandlersState()).toThrow('Count failed');
778
+ });
779
+ });
780
+
781
+ describe('edge cases', () => {
782
+ beforeEach(() => {
783
+ manager = new ShutdownManager(
784
+ getEnqueueCount,
785
+ stopReceiving,
786
+ disconnectTransport,
787
+ );
788
+ });
789
+
790
+ it('should handle shutdown with no active events', async () => {
791
+ await manager.shutdown();
792
+
793
+ expect(manager.state).toBe('stopped');
794
+ expect(manager.getHandlersState().isIdle).toBe(true);
795
+ });
796
+
797
+ it('should handle rapid increment/decrement cycles', () => {
798
+ for (let i = 0; i < 100; i++) {
799
+ manager.incrementProcessing();
800
+ }
801
+
802
+ expect(manager.getHandlersState().eventsBeingProcessed).toBe(100);
803
+
804
+ for (let i = 0; i < 100; i++) {
805
+ manager.decrementProcessing();
806
+ }
807
+
808
+ expect(manager.getHandlersState().eventsBeingProcessed).toBe(0);
809
+ expect(manager.getHandlersState().isIdle).toBe(true);
810
+ });
811
+
812
+ it('should handle events completing exactly at timeout', async () => {
813
+ manager = new ShutdownManager(
814
+ getEnqueueCount,
815
+ stopReceiving,
816
+ disconnectTransport,
817
+ {
818
+ gracefulShutdownTimeout: 100,
819
+ idlePollingInterval: 50,
820
+ },
821
+ );
822
+
823
+ manager.incrementProcessing();
824
+
825
+ const shutdownPromise = manager.shutdown();
826
+
827
+ setTimeout(() => {
828
+ manager.decrementProcessing();
829
+ }, 90);
830
+
831
+ await shutdownPromise;
832
+
833
+ expect(manager.state).toBe('stopped');
834
+ });
835
+
836
+ it('should handle zero timeout gracefully', async () => {
837
+ manager = new ShutdownManager(
838
+ getEnqueueCount,
839
+ stopReceiving,
840
+ disconnectTransport,
841
+ {
842
+ gracefulShutdownTimeout: 0,
843
+ },
844
+ );
845
+
846
+ manager.incrementProcessing();
847
+
848
+ await manager.shutdown();
849
+
850
+ expect(manager.state).toBe('stopped');
851
+ });
852
+ });
853
+ });