dspx 0.1.1-alpha.0

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 (172) hide show
  1. package/.github/workflows/ci.yml +185 -0
  2. package/.vscode/c_cpp_properties.json +17 -0
  3. package/.vscode/settings.json +68 -0
  4. package/.vscode/tasks.json +28 -0
  5. package/DISCLAIMER.md +32 -0
  6. package/LICENSE +21 -0
  7. package/README.md +1803 -0
  8. package/ROADMAP.md +192 -0
  9. package/TECHNICAL_DEBT.md +165 -0
  10. package/binding.gyp +65 -0
  11. package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
  12. package/docs/AUTHENTICATION_SECURITY.md +396 -0
  13. package/docs/BACKEND_IMPROVEMENTS.md +399 -0
  14. package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
  15. package/docs/FFT_IMPLEMENTATION.md +490 -0
  16. package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
  17. package/docs/FFT_USER_GUIDE.md +494 -0
  18. package/docs/FILTERS_IMPLEMENTATION.md +260 -0
  19. package/docs/FILTER_API_GUIDE.md +418 -0
  20. package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
  21. package/docs/LOGGER_API_REFERENCE.md +350 -0
  22. package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
  23. package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
  24. package/docs/PHASES_5_7_SUMMARY.md +403 -0
  25. package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
  26. package/docs/SIMD_OPTIMIZATIONS.md +211 -0
  27. package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
  28. package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
  29. package/docs/TIMESERIES_QUICK_REF.md +85 -0
  30. package/docs/advanced.md +559 -0
  31. package/docs/time-series-guide.md +617 -0
  32. package/docs/time-series-migration.md +376 -0
  33. package/jest.config.js +37 -0
  34. package/package.json +42 -0
  35. package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
  36. package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
  37. package/scripts/test.js +24 -0
  38. package/src/build/dsp-ts-redis.node +0 -0
  39. package/src/native/DspPipeline.cc +675 -0
  40. package/src/native/DspPipeline.h +44 -0
  41. package/src/native/FftBindings.cc +817 -0
  42. package/src/native/FilterBindings.cc +1001 -0
  43. package/src/native/IDspStage.h +53 -0
  44. package/src/native/adapters/InterpolatorStage.h +201 -0
  45. package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
  46. package/src/native/adapters/MovingAverageStage.h +306 -0
  47. package/src/native/adapters/RectifyStage.h +88 -0
  48. package/src/native/adapters/ResamplerStage.h +238 -0
  49. package/src/native/adapters/RmsStage.h +299 -0
  50. package/src/native/adapters/SscStage.h +121 -0
  51. package/src/native/adapters/VarianceStage.h +307 -0
  52. package/src/native/adapters/WampStage.h +114 -0
  53. package/src/native/adapters/WaveformLengthStage.h +115 -0
  54. package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
  55. package/src/native/core/FftEngine.cc +441 -0
  56. package/src/native/core/FftEngine.h +224 -0
  57. package/src/native/core/FirFilter.cc +324 -0
  58. package/src/native/core/FirFilter.h +149 -0
  59. package/src/native/core/IirFilter.cc +576 -0
  60. package/src/native/core/IirFilter.h +210 -0
  61. package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
  62. package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
  63. package/src/native/core/MovingAverageFilter.cc +18 -0
  64. package/src/native/core/MovingAverageFilter.h +135 -0
  65. package/src/native/core/MovingFftFilter.cc +291 -0
  66. package/src/native/core/MovingFftFilter.h +203 -0
  67. package/src/native/core/MovingVarianceFilter.cc +194 -0
  68. package/src/native/core/MovingVarianceFilter.h +114 -0
  69. package/src/native/core/MovingZScoreFilter.cc +215 -0
  70. package/src/native/core/MovingZScoreFilter.h +113 -0
  71. package/src/native/core/Policies.h +352 -0
  72. package/src/native/core/RmsFilter.cc +18 -0
  73. package/src/native/core/RmsFilter.h +131 -0
  74. package/src/native/core/SscFilter.cc +16 -0
  75. package/src/native/core/SscFilter.h +137 -0
  76. package/src/native/core/WampFilter.cc +16 -0
  77. package/src/native/core/WampFilter.h +101 -0
  78. package/src/native/core/WaveformLengthFilter.cc +17 -0
  79. package/src/native/core/WaveformLengthFilter.h +98 -0
  80. package/src/native/utils/CircularBufferArray.cc +336 -0
  81. package/src/native/utils/CircularBufferArray.h +62 -0
  82. package/src/native/utils/CircularBufferVector.cc +145 -0
  83. package/src/native/utils/CircularBufferVector.h +45 -0
  84. package/src/native/utils/NapiUtils.cc +53 -0
  85. package/src/native/utils/NapiUtils.h +21 -0
  86. package/src/native/utils/SimdOps.h +870 -0
  87. package/src/native/utils/SlidingWindowFilter.cc +239 -0
  88. package/src/native/utils/SlidingWindowFilter.h +159 -0
  89. package/src/native/utils/TimeSeriesBuffer.cc +205 -0
  90. package/src/native/utils/TimeSeriesBuffer.h +140 -0
  91. package/src/ts/CircularLogBuffer.ts +87 -0
  92. package/src/ts/DriftDetector.ts +331 -0
  93. package/src/ts/TopicRouter.ts +428 -0
  94. package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
  95. package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
  96. package/src/ts/__tests__/Chaining.test.ts +387 -0
  97. package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
  98. package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
  99. package/src/ts/__tests__/DriftDetector.test.ts +389 -0
  100. package/src/ts/__tests__/Fft.test.ts +484 -0
  101. package/src/ts/__tests__/ListState.test.ts +153 -0
  102. package/src/ts/__tests__/Logger.test.ts +208 -0
  103. package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
  104. package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
  105. package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
  106. package/src/ts/__tests__/MovingAverage.test.ts +322 -0
  107. package/src/ts/__tests__/RMS.test.ts +315 -0
  108. package/src/ts/__tests__/Rectify.test.ts +272 -0
  109. package/src/ts/__tests__/Redis.test.ts +456 -0
  110. package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
  111. package/src/ts/__tests__/Tap.test.ts +164 -0
  112. package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
  113. package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
  114. package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
  115. package/src/ts/__tests__/TimeSeries.test.ts +254 -0
  116. package/src/ts/__tests__/TopicRouter.test.ts +332 -0
  117. package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
  118. package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
  119. package/src/ts/__tests__/Variance.test.ts +509 -0
  120. package/src/ts/__tests__/WaveformLength.test.ts +147 -0
  121. package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
  122. package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
  123. package/src/ts/advanced-dsp.ts +566 -0
  124. package/src/ts/backends.ts +1137 -0
  125. package/src/ts/bindings.ts +1225 -0
  126. package/src/ts/easter-egg.ts +42 -0
  127. package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
  128. package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
  129. package/src/ts/examples/MovingAverage/test-state.ts +85 -0
  130. package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
  131. package/src/ts/examples/RMS/test-state.ts +97 -0
  132. package/src/ts/examples/RMS/test-streaming.ts +253 -0
  133. package/src/ts/examples/Rectify/test-state.ts +107 -0
  134. package/src/ts/examples/Rectify/test-streaming.ts +242 -0
  135. package/src/ts/examples/Variance/test-state.ts +195 -0
  136. package/src/ts/examples/Variance/test-streaming.ts +260 -0
  137. package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
  138. package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
  139. package/src/ts/examples/advanced-dsp-examples.ts +397 -0
  140. package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
  141. package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
  142. package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
  143. package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
  144. package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
  145. package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
  146. package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
  147. package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
  148. package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
  149. package/src/ts/examples/chaining/test-chaining.ts +52 -0
  150. package/src/ts/examples/emg-features-example.ts +284 -0
  151. package/src/ts/examples/fft-example.ts +309 -0
  152. package/src/ts/examples/fft-examples.ts +349 -0
  153. package/src/ts/examples/filter-examples.ts +320 -0
  154. package/src/ts/examples/list-state-example.ts +131 -0
  155. package/src/ts/examples/logger-example.ts +91 -0
  156. package/src/ts/examples/notch-filter-examples.ts +243 -0
  157. package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
  158. package/src/ts/examples/phase6-7/production-observability.ts +476 -0
  159. package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
  160. package/src/ts/examples/redis/redis-example.ts +202 -0
  161. package/src/ts/examples/redis-example.ts +202 -0
  162. package/src/ts/examples/simd-benchmark.ts +126 -0
  163. package/src/ts/examples/tap-debugging.ts +230 -0
  164. package/src/ts/examples/timeseries/comparison-example.ts +290 -0
  165. package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
  166. package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
  167. package/src/ts/examples/waveform-length-example.ts +139 -0
  168. package/src/ts/fft.ts +722 -0
  169. package/src/ts/filters.ts +1078 -0
  170. package/src/ts/index.ts +120 -0
  171. package/src/ts/types.ts +589 -0
  172. package/tsconfig.json +15 -0
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Advanced TopicRouter Tests
3
+ *
4
+ * Tests for:
5
+ * - Concurrency control
6
+ * - Metrics tracking
7
+ * - RouteOptions configuration
8
+ */
9
+
10
+ import { describe, it } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { createTopicRouter, createMockHandler } from "../index.js";
13
+ import type { LogEntry } from "../types.js";
14
+
15
+ describe("TopicRouter Concurrency Control", () => {
16
+ it("should limit concurrent handler executions", async () => {
17
+ let activeCount = 0;
18
+ let maxConcurrent = 0;
19
+
20
+ const slowHandler = async () => {
21
+ activeCount++;
22
+ maxConcurrent = Math.max(maxConcurrent, activeCount);
23
+ await new Promise((resolve) => setTimeout(resolve, 10));
24
+ activeCount--;
25
+ };
26
+
27
+ const router = createTopicRouter()
28
+ .errors(slowHandler, { concurrency: 2 })
29
+ .build();
30
+
31
+ const logs: LogEntry[] = Array.from({ length: 10 }, (_, i) => ({
32
+ level: "error",
33
+ message: `Error ${i}`,
34
+ topic: "pipeline.error",
35
+ timestamp: Date.now(),
36
+ }));
37
+
38
+ await router.routeBatch(logs);
39
+
40
+ // Should never exceed concurrency limit of 2
41
+ assert.ok(
42
+ maxConcurrent <= 2,
43
+ `Max concurrent was ${maxConcurrent}, expected <= 2`
44
+ );
45
+ });
46
+
47
+ it("should queue tasks when concurrency limit is reached", async () => {
48
+ const executionOrder: number[] = [];
49
+ const delays = [50, 30, 10]; // First task takes longest
50
+
51
+ const router = createTopicRouter()
52
+ .custom(
53
+ /^pipeline\./,
54
+ async (log: LogEntry) => {
55
+ const taskId = parseInt(log.message.split(" ")[1]);
56
+ await new Promise((resolve) => setTimeout(resolve, delays[taskId]));
57
+ executionOrder.push(taskId);
58
+ },
59
+ { concurrency: 1 }
60
+ ) // Force sequential execution
61
+ .build();
62
+
63
+ const logs: LogEntry[] = [0, 1, 2].map((i) => ({
64
+ level: "info",
65
+ message: `Task ${i}`,
66
+ topic: "pipeline.info",
67
+ timestamp: Date.now(),
68
+ }));
69
+
70
+ await router.routeBatch(logs);
71
+
72
+ // Should execute in order despite varying durations
73
+ assert.deepEqual(executionOrder, [0, 1, 2]);
74
+ });
75
+
76
+ it("should allow unlimited concurrency by default", async () => {
77
+ let maxConcurrent = 0;
78
+ let activeCount = 0;
79
+
80
+ const handler = async () => {
81
+ activeCount++;
82
+ maxConcurrent = Math.max(maxConcurrent, activeCount);
83
+ await new Promise((resolve) => setTimeout(resolve, 5));
84
+ activeCount--;
85
+ };
86
+
87
+ const router = createTopicRouter()
88
+ .errors(handler) // No concurrency limit
89
+ .build();
90
+
91
+ const logs: LogEntry[] = Array.from({ length: 20 }, (_, i) => ({
92
+ level: "error",
93
+ message: `Error ${i}`,
94
+ topic: "pipeline.error",
95
+ timestamp: Date.now(),
96
+ }));
97
+
98
+ await router.routeBatch(logs);
99
+
100
+ // Should run many tasks concurrently
101
+ assert.ok(
102
+ maxConcurrent > 5,
103
+ `Max concurrent was ${maxConcurrent}, expected > 5`
104
+ );
105
+ });
106
+
107
+ it("should handle different concurrency limits per route", async () => {
108
+ const route1Active: number[] = [];
109
+ const route2Active: number[] = [];
110
+
111
+ const handler1 = async () => {
112
+ route1Active.push(Date.now());
113
+ await new Promise((resolve) => setTimeout(resolve, 10));
114
+ };
115
+
116
+ const handler2 = async () => {
117
+ route2Active.push(Date.now());
118
+ await new Promise((resolve) => setTimeout(resolve, 10));
119
+ };
120
+
121
+ const router = createTopicRouter()
122
+ .errors(handler1, { concurrency: 1 })
123
+ .debug(handler2, { concurrency: 5 })
124
+ .build();
125
+
126
+ const errorLogs: LogEntry[] = Array.from({ length: 5 }, (_, i) => ({
127
+ level: "error",
128
+ message: `Error ${i}`,
129
+ topic: "pipeline.error",
130
+ timestamp: Date.now(),
131
+ }));
132
+
133
+ const debugLogs: LogEntry[] = Array.from({ length: 5 }, (_, i) => ({
134
+ level: "debug",
135
+ message: `Debug ${i}`,
136
+ topic: "pipeline.debug",
137
+ timestamp: Date.now(),
138
+ }));
139
+
140
+ await Promise.all([
141
+ router.routeBatch(errorLogs),
142
+ router.routeBatch(debugLogs),
143
+ ]);
144
+
145
+ assert.equal(route1Active.length, 5);
146
+ assert.equal(route2Active.length, 5);
147
+ });
148
+ });
149
+
150
+ describe("TopicRouter Metrics Tracking", () => {
151
+ it("should track execution count", async () => {
152
+ const mock = createMockHandler();
153
+
154
+ const router = createTopicRouter()
155
+ .errors(mock.handler, { trackMetrics: true })
156
+ .build();
157
+
158
+ const logs: LogEntry[] = Array.from({ length: 5 }, (_, i) => ({
159
+ level: "error",
160
+ message: `Error ${i}`,
161
+ topic: "pipeline.error",
162
+ timestamp: Date.now(),
163
+ }));
164
+
165
+ await router.routeBatch(logs);
166
+
167
+ const metrics = router.getMetrics();
168
+ assert.equal(metrics.length, 1);
169
+ assert.equal(metrics[0].executionCount, 5);
170
+ });
171
+
172
+ it("should track duration statistics", async () => {
173
+ const delays = [10, 20, 30];
174
+
175
+ const router = createTopicRouter()
176
+ .custom(
177
+ /^pipeline\./,
178
+ async (log: LogEntry) => {
179
+ const taskId = parseInt(log.message.split(" ")[1]);
180
+ await new Promise((resolve) => setTimeout(resolve, delays[taskId]));
181
+ },
182
+ { trackMetrics: true }
183
+ )
184
+ .build();
185
+
186
+ const logs: LogEntry[] = [0, 1, 2].map((i) => ({
187
+ level: "info",
188
+ message: `Task ${i}`,
189
+ topic: "pipeline.info",
190
+ timestamp: Date.now(),
191
+ }));
192
+
193
+ await router.routeBatch(logs);
194
+
195
+ const metrics = router.getMetrics();
196
+ assert.equal(metrics.length, 1);
197
+
198
+ const m = metrics[0];
199
+ // Use >= 9 instead of >= 10 to account for timing variability across systems
200
+ assert.ok(
201
+ m.minDuration >= 9,
202
+ `Min duration ${m.minDuration} should be >= 9ms`
203
+ );
204
+ // Use >= 28 instead of >= 29 to account for timing variability across systems
205
+ assert.ok(
206
+ m.maxDuration >= 28,
207
+ `Max duration ${m.maxDuration} should be >= 28ms`
208
+ );
209
+ assert.ok(
210
+ m.averageDuration >= 14,
211
+ `Avg duration ${m.averageDuration} should be >= 14ms`
212
+ );
213
+ assert.ok(m.totalDuration > 0);
214
+ });
215
+
216
+ it("should track error count", async () => {
217
+ let callCount = 0;
218
+
219
+ const handler = async () => {
220
+ callCount++;
221
+ if (callCount > 1) {
222
+ throw new Error("Handler error");
223
+ }
224
+ };
225
+
226
+ const router = createTopicRouter()
227
+ .errors(handler, { trackMetrics: true })
228
+ .build();
229
+
230
+ // Process 3 logs: first succeeds, next two fail
231
+ await router.routeBatch([
232
+ {
233
+ level: "error",
234
+ message: "Call 1",
235
+ topic: "pipeline.error",
236
+ timestamp: Date.now(),
237
+ },
238
+ {
239
+ level: "error",
240
+ message: "Call 2",
241
+ topic: "pipeline.error",
242
+ timestamp: Date.now(),
243
+ },
244
+ {
245
+ level: "error",
246
+ message: "Call 3",
247
+ topic: "pipeline.error",
248
+ timestamp: Date.now(),
249
+ },
250
+ ]);
251
+
252
+ const metrics = router.getMetrics();
253
+ assert.equal(metrics.length, 1);
254
+ assert.equal(metrics[0].executionCount, 3);
255
+ assert.equal(metrics[0].errorCount, 2);
256
+ });
257
+
258
+ it("should track last executed timestamp", async () => {
259
+ const mock = createMockHandler();
260
+
261
+ const router = createTopicRouter()
262
+ .errors(mock.handler, { trackMetrics: true })
263
+ .build();
264
+
265
+ const beforeTime = Date.now();
266
+
267
+ await router.route({
268
+ level: "error",
269
+ message: "Test",
270
+ topic: "pipeline.error",
271
+ timestamp: Date.now(),
272
+ });
273
+
274
+ const afterTime = Date.now();
275
+
276
+ const metrics = router.getMetrics();
277
+ assert.equal(metrics.length, 1);
278
+ assert.ok(metrics[0].lastExecuted);
279
+ assert.ok(metrics[0].lastExecuted >= beforeTime);
280
+ assert.ok(metrics[0].lastExecuted <= afterTime);
281
+ });
282
+
283
+ it("should not track metrics when trackMetrics is false", async () => {
284
+ const mock = createMockHandler();
285
+
286
+ const router = createTopicRouter()
287
+ .errors(mock.handler, { trackMetrics: false })
288
+ .build();
289
+
290
+ await router.route({
291
+ level: "error",
292
+ message: "Test",
293
+ topic: "pipeline.error",
294
+ timestamp: Date.now(),
295
+ });
296
+
297
+ // When trackMetrics is false, getMetrics() should not return this route
298
+ const metrics = router.getMetrics();
299
+ assert.equal(metrics.length, 0); // Filtered out because trackMetrics is false
300
+
301
+ // But the handler should still be called
302
+ assert.equal(mock.getLogs().length, 1);
303
+ });
304
+
305
+ it("should reset metrics correctly", async () => {
306
+ const mock = createMockHandler();
307
+
308
+ const router = createTopicRouter()
309
+ .errors(mock.handler, { trackMetrics: true })
310
+ .build();
311
+
312
+ await router.routeBatch([
313
+ {
314
+ level: "error",
315
+ message: "Error 1",
316
+ topic: "pipeline.error",
317
+ timestamp: Date.now(),
318
+ },
319
+ {
320
+ level: "error",
321
+ message: "Error 2",
322
+ topic: "pipeline.error",
323
+ timestamp: Date.now(),
324
+ },
325
+ ]);
326
+
327
+ let metrics = router.getMetrics();
328
+ assert.equal(metrics[0].executionCount, 2);
329
+
330
+ router.resetMetrics();
331
+
332
+ metrics = router.getMetrics();
333
+ assert.equal(metrics[0].executionCount, 0);
334
+ assert.equal(metrics[0].totalDuration, 0);
335
+ assert.equal(metrics[0].errorCount, 0);
336
+ assert.equal(metrics[0].lastExecuted, undefined);
337
+ });
338
+
339
+ it("should track metrics independently per route", async () => {
340
+ const mock1 = createMockHandler();
341
+ const mock2 = createMockHandler();
342
+
343
+ const router = createTopicRouter()
344
+ .errors(mock1.handler, { trackMetrics: true })
345
+ .debug(mock2.handler, { trackMetrics: true })
346
+ .build();
347
+
348
+ await router.routeBatch([
349
+ {
350
+ level: "error",
351
+ message: "Error 1",
352
+ topic: "pipeline.error",
353
+ timestamp: Date.now(),
354
+ },
355
+ {
356
+ level: "error",
357
+ message: "Error 2",
358
+ topic: "pipeline.error",
359
+ timestamp: Date.now(),
360
+ },
361
+ {
362
+ level: "debug",
363
+ message: "Debug 1",
364
+ topic: "pipeline.debug",
365
+ timestamp: Date.now(),
366
+ },
367
+ ]);
368
+
369
+ const metrics = router.getMetrics();
370
+ assert.equal(metrics.length, 2);
371
+
372
+ // Sort by execution count to get consistent ordering
373
+ const sorted = metrics.sort((a, b) => b.executionCount - a.executionCount);
374
+
375
+ // First route should have 2 executions (errors)
376
+ assert.equal(sorted[0].executionCount, 2);
377
+
378
+ // Second route should have 1 execution (debug)
379
+ assert.equal(sorted[1].executionCount, 1);
380
+ });
381
+ });
382
+
383
+ describe("TopicRouter RouteOptions Integration", () => {
384
+ it("should support both concurrency and metrics tracking", async () => {
385
+ let activeCount = 0;
386
+ let maxConcurrent = 0;
387
+
388
+ const handler = async () => {
389
+ activeCount++;
390
+ maxConcurrent = Math.max(maxConcurrent, activeCount);
391
+ await new Promise((resolve) => setTimeout(resolve, 10));
392
+ activeCount--;
393
+ };
394
+
395
+ const router = createTopicRouter()
396
+ .errors(handler, {
397
+ concurrency: 2,
398
+ trackMetrics: true,
399
+ })
400
+ .build();
401
+
402
+ const logs: LogEntry[] = Array.from({ length: 10 }, (_, i) => ({
403
+ level: "error",
404
+ message: `Error ${i}`,
405
+ topic: "pipeline.error",
406
+ timestamp: Date.now(),
407
+ }));
408
+
409
+ await router.routeBatch(logs);
410
+
411
+ // Check concurrency
412
+ assert.ok(maxConcurrent <= 2);
413
+
414
+ // Check metrics
415
+ const metrics = router.getMetrics();
416
+ assert.equal(metrics.length, 1);
417
+ assert.equal(metrics[0].executionCount, 10);
418
+ assert.ok(metrics[0].totalDuration > 0);
419
+ });
420
+
421
+ it("should support partial options (only concurrency)", async () => {
422
+ let activeCount = 0;
423
+ let maxConcurrent = 0;
424
+
425
+ const handler = async () => {
426
+ activeCount++;
427
+ maxConcurrent = Math.max(maxConcurrent, activeCount);
428
+ await new Promise((resolve) => setTimeout(resolve, 5));
429
+ activeCount--;
430
+ };
431
+
432
+ const router = createTopicRouter()
433
+ .errors(handler, { concurrency: 1 })
434
+ .build();
435
+
436
+ const logs: LogEntry[] = Array.from({ length: 5 }, (_, i) => ({
437
+ level: "error",
438
+ message: `Error ${i}`,
439
+ topic: "pipeline.error",
440
+ timestamp: Date.now(),
441
+ }));
442
+
443
+ await router.routeBatch(logs);
444
+
445
+ assert.equal(maxConcurrent, 1);
446
+ });
447
+
448
+ it("should support partial options (only trackMetrics)", async () => {
449
+ const mock = createMockHandler();
450
+
451
+ const router = createTopicRouter()
452
+ .errors(mock.handler, { trackMetrics: true })
453
+ .build();
454
+
455
+ await router.route({
456
+ level: "error",
457
+ message: "Test",
458
+ topic: "pipeline.error",
459
+ timestamp: Date.now(),
460
+ });
461
+
462
+ const metrics = router.getMetrics();
463
+ assert.equal(metrics[0].executionCount, 1);
464
+ });
465
+
466
+ it("should handle no options (default behavior)", async () => {
467
+ const mock = createMockHandler();
468
+
469
+ const router = createTopicRouter()
470
+ .errors(mock.handler) // No options
471
+ .build();
472
+
473
+ await router.route({
474
+ level: "error",
475
+ message: "Test",
476
+ topic: "pipeline.error",
477
+ timestamp: Date.now(),
478
+ });
479
+
480
+ // Should work fine with defaults
481
+ assert.equal(mock.getLogs().length, 1);
482
+ });
483
+ });