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.
- package/.github/workflows/ci.yml +185 -0
- package/.vscode/c_cpp_properties.json +17 -0
- package/.vscode/settings.json +68 -0
- package/.vscode/tasks.json +28 -0
- package/DISCLAIMER.md +32 -0
- package/LICENSE +21 -0
- package/README.md +1803 -0
- package/ROADMAP.md +192 -0
- package/TECHNICAL_DEBT.md +165 -0
- package/binding.gyp +65 -0
- package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
- package/docs/AUTHENTICATION_SECURITY.md +396 -0
- package/docs/BACKEND_IMPROVEMENTS.md +399 -0
- package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
- package/docs/FFT_IMPLEMENTATION.md +490 -0
- package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
- package/docs/FFT_USER_GUIDE.md +494 -0
- package/docs/FILTERS_IMPLEMENTATION.md +260 -0
- package/docs/FILTER_API_GUIDE.md +418 -0
- package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
- package/docs/LOGGER_API_REFERENCE.md +350 -0
- package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
- package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
- package/docs/PHASES_5_7_SUMMARY.md +403 -0
- package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
- package/docs/SIMD_OPTIMIZATIONS.md +211 -0
- package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
- package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
- package/docs/TIMESERIES_QUICK_REF.md +85 -0
- package/docs/advanced.md +559 -0
- package/docs/time-series-guide.md +617 -0
- package/docs/time-series-migration.md +376 -0
- package/jest.config.js +37 -0
- package/package.json +42 -0
- package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
- package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
- package/scripts/test.js +24 -0
- package/src/build/dsp-ts-redis.node +0 -0
- package/src/native/DspPipeline.cc +675 -0
- package/src/native/DspPipeline.h +44 -0
- package/src/native/FftBindings.cc +817 -0
- package/src/native/FilterBindings.cc +1001 -0
- package/src/native/IDspStage.h +53 -0
- package/src/native/adapters/InterpolatorStage.h +201 -0
- package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
- package/src/native/adapters/MovingAverageStage.h +306 -0
- package/src/native/adapters/RectifyStage.h +88 -0
- package/src/native/adapters/ResamplerStage.h +238 -0
- package/src/native/adapters/RmsStage.h +299 -0
- package/src/native/adapters/SscStage.h +121 -0
- package/src/native/adapters/VarianceStage.h +307 -0
- package/src/native/adapters/WampStage.h +114 -0
- package/src/native/adapters/WaveformLengthStage.h +115 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
- package/src/native/core/FftEngine.cc +441 -0
- package/src/native/core/FftEngine.h +224 -0
- package/src/native/core/FirFilter.cc +324 -0
- package/src/native/core/FirFilter.h +149 -0
- package/src/native/core/IirFilter.cc +576 -0
- package/src/native/core/IirFilter.h +210 -0
- package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
- package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
- package/src/native/core/MovingAverageFilter.cc +18 -0
- package/src/native/core/MovingAverageFilter.h +135 -0
- package/src/native/core/MovingFftFilter.cc +291 -0
- package/src/native/core/MovingFftFilter.h +203 -0
- package/src/native/core/MovingVarianceFilter.cc +194 -0
- package/src/native/core/MovingVarianceFilter.h +114 -0
- package/src/native/core/MovingZScoreFilter.cc +215 -0
- package/src/native/core/MovingZScoreFilter.h +113 -0
- package/src/native/core/Policies.h +352 -0
- package/src/native/core/RmsFilter.cc +18 -0
- package/src/native/core/RmsFilter.h +131 -0
- package/src/native/core/SscFilter.cc +16 -0
- package/src/native/core/SscFilter.h +137 -0
- package/src/native/core/WampFilter.cc +16 -0
- package/src/native/core/WampFilter.h +101 -0
- package/src/native/core/WaveformLengthFilter.cc +17 -0
- package/src/native/core/WaveformLengthFilter.h +98 -0
- package/src/native/utils/CircularBufferArray.cc +336 -0
- package/src/native/utils/CircularBufferArray.h +62 -0
- package/src/native/utils/CircularBufferVector.cc +145 -0
- package/src/native/utils/CircularBufferVector.h +45 -0
- package/src/native/utils/NapiUtils.cc +53 -0
- package/src/native/utils/NapiUtils.h +21 -0
- package/src/native/utils/SimdOps.h +870 -0
- package/src/native/utils/SlidingWindowFilter.cc +239 -0
- package/src/native/utils/SlidingWindowFilter.h +159 -0
- package/src/native/utils/TimeSeriesBuffer.cc +205 -0
- package/src/native/utils/TimeSeriesBuffer.h +140 -0
- package/src/ts/CircularLogBuffer.ts +87 -0
- package/src/ts/DriftDetector.ts +331 -0
- package/src/ts/TopicRouter.ts +428 -0
- package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
- package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
- package/src/ts/__tests__/Chaining.test.ts +387 -0
- package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
- package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
- package/src/ts/__tests__/DriftDetector.test.ts +389 -0
- package/src/ts/__tests__/Fft.test.ts +484 -0
- package/src/ts/__tests__/ListState.test.ts +153 -0
- package/src/ts/__tests__/Logger.test.ts +208 -0
- package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
- package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
- package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
- package/src/ts/__tests__/MovingAverage.test.ts +322 -0
- package/src/ts/__tests__/RMS.test.ts +315 -0
- package/src/ts/__tests__/Rectify.test.ts +272 -0
- package/src/ts/__tests__/Redis.test.ts +456 -0
- package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
- package/src/ts/__tests__/Tap.test.ts +164 -0
- package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
- package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
- package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
- package/src/ts/__tests__/TimeSeries.test.ts +254 -0
- package/src/ts/__tests__/TopicRouter.test.ts +332 -0
- package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
- package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
- package/src/ts/__tests__/Variance.test.ts +509 -0
- package/src/ts/__tests__/WaveformLength.test.ts +147 -0
- package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
- package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
- package/src/ts/advanced-dsp.ts +566 -0
- package/src/ts/backends.ts +1137 -0
- package/src/ts/bindings.ts +1225 -0
- package/src/ts/easter-egg.ts +42 -0
- package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
- package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
- package/src/ts/examples/MovingAverage/test-state.ts +85 -0
- package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
- package/src/ts/examples/RMS/test-state.ts +97 -0
- package/src/ts/examples/RMS/test-streaming.ts +253 -0
- package/src/ts/examples/Rectify/test-state.ts +107 -0
- package/src/ts/examples/Rectify/test-streaming.ts +242 -0
- package/src/ts/examples/Variance/test-state.ts +195 -0
- package/src/ts/examples/Variance/test-streaming.ts +260 -0
- package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
- package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
- package/src/ts/examples/advanced-dsp-examples.ts +397 -0
- package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
- package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
- package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
- package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
- package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
- package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
- package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
- package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
- package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
- package/src/ts/examples/chaining/test-chaining.ts +52 -0
- package/src/ts/examples/emg-features-example.ts +284 -0
- package/src/ts/examples/fft-example.ts +309 -0
- package/src/ts/examples/fft-examples.ts +349 -0
- package/src/ts/examples/filter-examples.ts +320 -0
- package/src/ts/examples/list-state-example.ts +131 -0
- package/src/ts/examples/logger-example.ts +91 -0
- package/src/ts/examples/notch-filter-examples.ts +243 -0
- package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
- package/src/ts/examples/phase6-7/production-observability.ts +476 -0
- package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
- package/src/ts/examples/redis/redis-example.ts +202 -0
- package/src/ts/examples/redis-example.ts +202 -0
- package/src/ts/examples/simd-benchmark.ts +126 -0
- package/src/ts/examples/tap-debugging.ts +230 -0
- package/src/ts/examples/timeseries/comparison-example.ts +290 -0
- package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
- package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
- package/src/ts/examples/waveform-length-example.ts +139 -0
- package/src/ts/fft.ts +722 -0
- package/src/ts/filters.ts +1078 -0
- package/src/ts/index.ts +120 -0
- package/src/ts/types.ts +589 -0
- 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
|
+
});
|