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,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for authentication error handling and edge cases
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import assert from "node:assert";
|
|
7
|
+
import {
|
|
8
|
+
Logger,
|
|
9
|
+
TextFormatter,
|
|
10
|
+
JSONFormatter,
|
|
11
|
+
createMockHandler,
|
|
12
|
+
} from "../backends.js";
|
|
13
|
+
import type { LogEntry } from "../types.js";
|
|
14
|
+
|
|
15
|
+
describe("Authentication Error Handling", () => {
|
|
16
|
+
it("should provide clear error messages for authentication failures", async () => {
|
|
17
|
+
const errors: string[] = [];
|
|
18
|
+
const mockHandler = async (log: LogEntry) => {
|
|
19
|
+
// Simulate 401 auth error
|
|
20
|
+
const error = new Error("HTTP 401 - Unauthorized");
|
|
21
|
+
errors.push(error.message);
|
|
22
|
+
throw error;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const logger = new Logger([mockHandler]);
|
|
26
|
+
|
|
27
|
+
// Should catch and log error without crashing
|
|
28
|
+
await logger.info("test", "auth.test");
|
|
29
|
+
|
|
30
|
+
assert.strictEqual(errors.length, 1);
|
|
31
|
+
assert.ok(errors[0].includes("401"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should warn when handlers are created without credentials", async () => {
|
|
35
|
+
const warnings: string[] = [];
|
|
36
|
+
const originalWarn = console.warn;
|
|
37
|
+
console.warn = (msg: string) => warnings.push(msg);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Import and create handlers without config
|
|
41
|
+
const { createPagerDutyHandler, createDatadogHandler } = await import(
|
|
42
|
+
"../backends.js"
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
createPagerDutyHandler({});
|
|
46
|
+
createDatadogHandler({});
|
|
47
|
+
|
|
48
|
+
assert.ok(warnings.length > 0);
|
|
49
|
+
assert.ok(warnings.some((w) => w.includes("not configured")));
|
|
50
|
+
} finally {
|
|
51
|
+
console.warn = originalWarn;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("Circular Reference Handling", () => {
|
|
57
|
+
it("should handle circular references in TextFormatter", () => {
|
|
58
|
+
const formatter = new TextFormatter();
|
|
59
|
+
|
|
60
|
+
// Create circular reference
|
|
61
|
+
const circular: any = { name: "test" };
|
|
62
|
+
circular.self = circular;
|
|
63
|
+
|
|
64
|
+
const log: LogEntry = {
|
|
65
|
+
level: "info",
|
|
66
|
+
message: "Test message",
|
|
67
|
+
topic: "circular.test",
|
|
68
|
+
context: circular,
|
|
69
|
+
timestamp: Date.now(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const result = formatter.format(log);
|
|
73
|
+
|
|
74
|
+
assert.ok(result.includes("Test message"));
|
|
75
|
+
assert.ok(result.includes("[Unable to stringify:"));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should handle deeply nested objects in TextFormatter", () => {
|
|
79
|
+
const formatter = new TextFormatter();
|
|
80
|
+
|
|
81
|
+
const deepNested: any = { level1: {} };
|
|
82
|
+
let current = deepNested.level1;
|
|
83
|
+
|
|
84
|
+
// Create 100 levels of nesting
|
|
85
|
+
for (let i = 2; i <= 100; i++) {
|
|
86
|
+
current[`level${i}`] = {};
|
|
87
|
+
current = current[`level${i}`];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const log: LogEntry = {
|
|
91
|
+
level: "info",
|
|
92
|
+
message: "Deep nested test",
|
|
93
|
+
topic: "nested.test",
|
|
94
|
+
context: deepNested,
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Should not throw
|
|
99
|
+
const result = formatter.format(log);
|
|
100
|
+
assert.ok(result.includes("Deep nested test"));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should handle objects with non-serializable values", () => {
|
|
104
|
+
const formatter = new TextFormatter();
|
|
105
|
+
|
|
106
|
+
const log: LogEntry = {
|
|
107
|
+
level: "info",
|
|
108
|
+
message: "Non-serializable test",
|
|
109
|
+
topic: "serialize.test",
|
|
110
|
+
context: {
|
|
111
|
+
func: () => {},
|
|
112
|
+
symbol: Symbol("test"),
|
|
113
|
+
undef: undefined,
|
|
114
|
+
},
|
|
115
|
+
timestamp: Date.now(),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Should not throw
|
|
119
|
+
const result = formatter.format(log);
|
|
120
|
+
assert.ok(result.includes("Non-serializable test"));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should handle empty context gracefully", () => {
|
|
124
|
+
const formatter = new TextFormatter();
|
|
125
|
+
|
|
126
|
+
const log: LogEntry = {
|
|
127
|
+
level: "info",
|
|
128
|
+
message: "Empty context",
|
|
129
|
+
topic: "empty.test",
|
|
130
|
+
context: {},
|
|
131
|
+
timestamp: Date.now(),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const result = formatter.format(log);
|
|
135
|
+
assert.ok(result.includes("Empty context"));
|
|
136
|
+
assert.ok(!result.includes("Context:"));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should handle missing context gracefully", () => {
|
|
140
|
+
const formatter = new TextFormatter();
|
|
141
|
+
|
|
142
|
+
const log: LogEntry = {
|
|
143
|
+
level: "info",
|
|
144
|
+
message: "No context",
|
|
145
|
+
topic: "nocontext.test",
|
|
146
|
+
timestamp: Date.now(),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const result = formatter.format(log);
|
|
150
|
+
assert.ok(result.includes("No context"));
|
|
151
|
+
assert.ok(!result.includes("Context:"));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("JSONFormatter Edge Cases", () => {
|
|
156
|
+
it("should pass through log entry unchanged", () => {
|
|
157
|
+
const formatter = new JSONFormatter();
|
|
158
|
+
|
|
159
|
+
const log: LogEntry = {
|
|
160
|
+
level: "info",
|
|
161
|
+
message: "Test",
|
|
162
|
+
topic: "json.test",
|
|
163
|
+
context: { key: "value" },
|
|
164
|
+
timestamp: Date.now(),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const result = formatter.format(log);
|
|
168
|
+
assert.deepStrictEqual(result, log);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should handle log entry with circular reference (passed to handler)", () => {
|
|
172
|
+
const formatter = new JSONFormatter();
|
|
173
|
+
const mock = createMockHandler();
|
|
174
|
+
|
|
175
|
+
// Create circular reference
|
|
176
|
+
const circular: any = { name: "test" };
|
|
177
|
+
circular.self = circular;
|
|
178
|
+
|
|
179
|
+
const log: LogEntry = {
|
|
180
|
+
level: "info",
|
|
181
|
+
message: "Circular",
|
|
182
|
+
topic: "circular.json",
|
|
183
|
+
context: circular,
|
|
184
|
+
timestamp: Date.now(),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// JSONFormatter passes through, handler should catch serialization error
|
|
188
|
+
const result = formatter.format(log);
|
|
189
|
+
assert.strictEqual(result.context, circular);
|
|
190
|
+
|
|
191
|
+
// Note: Actual JSON.stringify will happen in handler (e.g., postJSON)
|
|
192
|
+
// which should have its own error handling for circular refs
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("Handler Configuration Warnings", () => {
|
|
197
|
+
it("should warn about missing CloudWatch SDK recommendation", async () => {
|
|
198
|
+
const warnings: string[] = [];
|
|
199
|
+
const originalWarn = console.warn;
|
|
200
|
+
console.warn = (msg: string) => warnings.push(msg);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const { createCloudWatchHandler } = await import("../backends.js");
|
|
204
|
+
createCloudWatchHandler({});
|
|
205
|
+
|
|
206
|
+
assert.ok(warnings.some((w) => w.includes("@aws-sdk")));
|
|
207
|
+
} finally {
|
|
208
|
+
console.warn = originalWarn;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("should warn about Loki authentication options", async () => {
|
|
213
|
+
const warnings: string[] = [];
|
|
214
|
+
const originalWarn = console.warn;
|
|
215
|
+
console.warn = (msg: string) => warnings.push(msg);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const { createLokiHandler } = await import("../backends.js");
|
|
219
|
+
createLokiHandler({});
|
|
220
|
+
|
|
221
|
+
assert.ok(warnings.some((w) => w.includes("endpoint not configured")));
|
|
222
|
+
} finally {
|
|
223
|
+
console.warn = originalWarn;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should warn about Prometheus authentication requirements", async () => {
|
|
228
|
+
const warnings: string[] = [];
|
|
229
|
+
const originalWarn = console.warn;
|
|
230
|
+
console.warn = (msg: string) => warnings.push(msg);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const { createPrometheusHandler } = await import("../backends.js");
|
|
234
|
+
createPrometheusHandler({});
|
|
235
|
+
|
|
236
|
+
assert.ok(warnings.some((w) => w.includes("endpoint not configured")));
|
|
237
|
+
} finally {
|
|
238
|
+
console.warn = originalWarn;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { describe, test, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createDspPipeline, DspProcessor } from "../bindings.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_OPTIONS = { channels: 1, sampleRate: 44100 };
|
|
6
|
+
|
|
7
|
+
function assertCloseTo(actual: number, expected: number, precision = 4) {
|
|
8
|
+
const tolerance = Math.pow(10, -precision);
|
|
9
|
+
assert.ok(
|
|
10
|
+
Math.abs(actual - expected) < tolerance,
|
|
11
|
+
`Expected ${actual} to be close to ${expected} (tolerance: ${tolerance})`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("DSP Pipeline Chaining", () => {
|
|
16
|
+
let processor: DspProcessor;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
processor = createDspPipeline();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("Two-Stage Chains", () => {
|
|
23
|
+
test("should chain MovingAverage → Rectify", async () => {
|
|
24
|
+
processor.MovingAverage({ mode: "moving", windowSize: 2 }).Rectify({ mode: "full" });
|
|
25
|
+
|
|
26
|
+
// Input with negative values
|
|
27
|
+
const input = new Float32Array([1, -3, 2, -4]);
|
|
28
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
29
|
+
|
|
30
|
+
// Stage 1 (MovingAverage):
|
|
31
|
+
// [1] → 1
|
|
32
|
+
// [1, -3] → -1
|
|
33
|
+
// [-3, 2] → -0.5
|
|
34
|
+
// [2, -4] → -1
|
|
35
|
+
//
|
|
36
|
+
// Stage 2 (Rectify full):
|
|
37
|
+
// 1 → 1
|
|
38
|
+
// -1 → 1
|
|
39
|
+
// -0.5 → 0.5
|
|
40
|
+
// -1 → 1
|
|
41
|
+
assertCloseTo(output[0], 1);
|
|
42
|
+
assertCloseTo(output[1], 1);
|
|
43
|
+
assertCloseTo(output[2], 0.5);
|
|
44
|
+
assertCloseTo(output[3], 1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("should chain Rectify → MovingAverage", async () => {
|
|
48
|
+
processor.Rectify({ mode: "full" }).MovingAverage({ mode: "moving", windowSize: 2 });
|
|
49
|
+
|
|
50
|
+
const input = new Float32Array([1, -3, 2, -4]);
|
|
51
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
52
|
+
|
|
53
|
+
// Stage 1 (Rectify): [1, 3, 2, 4]
|
|
54
|
+
// Stage 2 (MovingAverage):
|
|
55
|
+
// [1] → 1
|
|
56
|
+
// [1, 3] → 2
|
|
57
|
+
// [3, 2] → 2.5
|
|
58
|
+
// [2, 4] → 3
|
|
59
|
+
assertCloseTo(output[0], 1);
|
|
60
|
+
assertCloseTo(output[1], 2);
|
|
61
|
+
assertCloseTo(output[2], 2.5);
|
|
62
|
+
assertCloseTo(output[3], 3);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("should chain MovingAverage → RMS", async () => {
|
|
66
|
+
processor.MovingAverage({ mode: "moving", windowSize: 2 }).Rms({ mode: "moving", windowSize: 2 });
|
|
67
|
+
|
|
68
|
+
const input = new Float32Array([4, 0, 8, 0]);
|
|
69
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
70
|
+
|
|
71
|
+
// Stage 1 (MovingAverage):
|
|
72
|
+
// [4] → 4
|
|
73
|
+
// [4, 0] → 2
|
|
74
|
+
// [0, 8] → 4
|
|
75
|
+
// [8, 0] → 4
|
|
76
|
+
//
|
|
77
|
+
// Stage 2 (RMS):
|
|
78
|
+
// [4] → 4
|
|
79
|
+
// [4, 2] → sqrt(20/2) = 3.1622...
|
|
80
|
+
// [2, 4] → sqrt(20/2) = 3.1622...
|
|
81
|
+
// [4, 4] → 4
|
|
82
|
+
assertCloseTo(output[0], 4);
|
|
83
|
+
assertCloseTo(output[1], 3.1622);
|
|
84
|
+
assertCloseTo(output[2], 3.1622);
|
|
85
|
+
assertCloseTo(output[3], 4);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("should chain Rectify → RMS", async () => {
|
|
89
|
+
processor.Rectify({ mode: "half" }).Rms({ mode: "moving", windowSize: 2 });
|
|
90
|
+
|
|
91
|
+
const input = new Float32Array([3, -3, 4, -4]);
|
|
92
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
93
|
+
|
|
94
|
+
// Stage 1 (Rectify half): [3, 0, 4, 0]
|
|
95
|
+
// Stage 2 (RMS):
|
|
96
|
+
// [3] → 3
|
|
97
|
+
// [3, 0] → sqrt(9/2) = 2.1213...
|
|
98
|
+
// [0, 4] → sqrt(16/2) = 2.8284...
|
|
99
|
+
// [4, 0] → sqrt(16/2) = 2.8284...
|
|
100
|
+
assertCloseTo(output[0], 3);
|
|
101
|
+
assertCloseTo(output[1], 2.1213);
|
|
102
|
+
assertCloseTo(output[2], 2.8284);
|
|
103
|
+
assertCloseTo(output[3], 2.8284);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("Three-Stage Chains", () => {
|
|
108
|
+
test("should chain MovingAverage → RMS → Rectify", async () => {
|
|
109
|
+
processor
|
|
110
|
+
.MovingAverage({ mode: "moving", windowSize: 2 })
|
|
111
|
+
.Rms({ mode: "moving", windowSize: 2 })
|
|
112
|
+
.Rectify({ mode: "full" });
|
|
113
|
+
|
|
114
|
+
const input = new Float32Array([2, -2, 2, -2]);
|
|
115
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
116
|
+
|
|
117
|
+
// Stage 1 (MovingAverage):
|
|
118
|
+
// [2] → 2
|
|
119
|
+
// [2, -2] → 0
|
|
120
|
+
// [-2, 2] → 0
|
|
121
|
+
// [2, -2] → 0
|
|
122
|
+
//
|
|
123
|
+
// Stage 2 (RMS):
|
|
124
|
+
// [2] → 2
|
|
125
|
+
// [2, 0] → sqrt(4/2) = 1.4142...
|
|
126
|
+
// [0, 0] → 0
|
|
127
|
+
// [0, 0] → 0
|
|
128
|
+
//
|
|
129
|
+
// Stage 3 (Rectify): All already positive or zero
|
|
130
|
+
assertCloseTo(output[0], 2);
|
|
131
|
+
assertCloseTo(output[1], 1.4142);
|
|
132
|
+
assertCloseTo(output[2], 0);
|
|
133
|
+
assertCloseTo(output[3], 0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("should chain Rectify → MovingAverage → RMS", async () => {
|
|
137
|
+
processor
|
|
138
|
+
.Rectify({ mode: "full" })
|
|
139
|
+
.MovingAverage({ mode: "moving", windowSize: 2 })
|
|
140
|
+
.Rms({ mode: "moving", windowSize: 2 });
|
|
141
|
+
|
|
142
|
+
const input = new Float32Array([1, -1, 1, -1]);
|
|
143
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
144
|
+
|
|
145
|
+
// Stage 1 (Rectify): [1, 1, 1, 1]
|
|
146
|
+
// Stage 2 (MovingAverage): [1, 1, 1, 1]
|
|
147
|
+
// Stage 3 (RMS): [1, 1, 1, 1]
|
|
148
|
+
output.forEach((val) => assertCloseTo(val, 1));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("should chain MovingAverage → Rectify → RMS", async () => {
|
|
152
|
+
processor
|
|
153
|
+
.MovingAverage({ mode: "moving", windowSize: 3 })
|
|
154
|
+
.Rectify({ mode: "full" })
|
|
155
|
+
.Rms({ mode: "moving", windowSize: 2 });
|
|
156
|
+
|
|
157
|
+
const input = new Float32Array([3, -6, 3, 0, -3]);
|
|
158
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
159
|
+
|
|
160
|
+
assert.equal(output.length, 5);
|
|
161
|
+
// All outputs should be positive (after rectify) and meaningful
|
|
162
|
+
output.forEach((val) => assert.ok(val >= 0));
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("State Management with Chains", () => {
|
|
167
|
+
test("should save and restore state for two-stage chain", async () => {
|
|
168
|
+
processor.MovingAverage({ mode: "moving", windowSize: 2 }).Rectify({ mode: "full" });
|
|
169
|
+
|
|
170
|
+
// Build state
|
|
171
|
+
await processor.process(new Float32Array([1, -2, 3]), DEFAULT_OPTIONS);
|
|
172
|
+
|
|
173
|
+
const stateJson = await processor.saveState();
|
|
174
|
+
const state = JSON.parse(stateJson);
|
|
175
|
+
|
|
176
|
+
// Verify state structure
|
|
177
|
+
assert.equal(state.stages.length, 2);
|
|
178
|
+
assert.equal(state.stages[0].type, "movingAverage");
|
|
179
|
+
assert.equal(state.stages[1].type, "rectify");
|
|
180
|
+
|
|
181
|
+
// Load into new processor
|
|
182
|
+
const processor2 = createDspPipeline();
|
|
183
|
+
processor2.MovingAverage({ mode: "moving", windowSize: 2 }).Rectify({ mode: "full" });
|
|
184
|
+
await processor2.loadState(stateJson);
|
|
185
|
+
|
|
186
|
+
// Both should produce same output
|
|
187
|
+
const output1 = await processor.process(
|
|
188
|
+
new Float32Array([4]),
|
|
189
|
+
DEFAULT_OPTIONS
|
|
190
|
+
);
|
|
191
|
+
const output2 = await processor2.process(
|
|
192
|
+
new Float32Array([4]),
|
|
193
|
+
DEFAULT_OPTIONS
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
assertCloseTo(output2[0], output1[0]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("should save and restore state for three-stage chain", async () => {
|
|
200
|
+
processor
|
|
201
|
+
.MovingAverage({ mode: "moving", windowSize: 2 })
|
|
202
|
+
.Rms({ mode: "moving", windowSize: 2 })
|
|
203
|
+
.Rectify({ mode: "half" });
|
|
204
|
+
|
|
205
|
+
// Build state
|
|
206
|
+
await processor.process(new Float32Array([1, 2, 3, 4]), DEFAULT_OPTIONS);
|
|
207
|
+
|
|
208
|
+
const stateJson = await processor.saveState();
|
|
209
|
+
const state = JSON.parse(stateJson);
|
|
210
|
+
|
|
211
|
+
// Verify all three stages
|
|
212
|
+
assert.equal(state.stages.length, 3);
|
|
213
|
+
assert.equal(state.stages[0].type, "movingAverage");
|
|
214
|
+
assert.equal(state.stages[1].type, "rms");
|
|
215
|
+
assert.equal(state.stages[2].type, "rectify");
|
|
216
|
+
|
|
217
|
+
// Load and verify continuity
|
|
218
|
+
const processor2 = createDspPipeline();
|
|
219
|
+
processor2
|
|
220
|
+
.MovingAverage({ mode: "moving", windowSize: 2 })
|
|
221
|
+
.Rms({ mode: "moving", windowSize: 2 })
|
|
222
|
+
.Rectify({ mode: "half" });
|
|
223
|
+
await processor2.loadState(stateJson);
|
|
224
|
+
|
|
225
|
+
const output1 = await processor.process(
|
|
226
|
+
new Float32Array([5]),
|
|
227
|
+
DEFAULT_OPTIONS
|
|
228
|
+
);
|
|
229
|
+
const output2 = await processor2.process(
|
|
230
|
+
new Float32Array([5]),
|
|
231
|
+
DEFAULT_OPTIONS
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
assertCloseTo(output2[0], output1[0]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("should maintain state continuity across batches in chain", async () => {
|
|
238
|
+
processor.MovingAverage({ mode: "moving", windowSize: 3 }).Rms({ mode: "moving", windowSize: 2 });
|
|
239
|
+
|
|
240
|
+
// Process multiple batches
|
|
241
|
+
const output1 = await processor.process(
|
|
242
|
+
new Float32Array([1, 2]),
|
|
243
|
+
DEFAULT_OPTIONS
|
|
244
|
+
);
|
|
245
|
+
const output2 = await processor.process(
|
|
246
|
+
new Float32Array([3, 4]),
|
|
247
|
+
DEFAULT_OPTIONS
|
|
248
|
+
);
|
|
249
|
+
const output3 = await processor.process(
|
|
250
|
+
new Float32Array([5]),
|
|
251
|
+
DEFAULT_OPTIONS
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// All outputs should be valid
|
|
255
|
+
assert.ok(output1.every((v) => !isNaN(v) && v >= 0));
|
|
256
|
+
assert.ok(output2.every((v) => !isNaN(v) && v >= 0));
|
|
257
|
+
assert.ok(output3.every((v) => !isNaN(v) && v >= 0));
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("should reset entire chain correctly", async () => {
|
|
261
|
+
processor
|
|
262
|
+
.MovingAverage({ mode: "moving", windowSize: 2 })
|
|
263
|
+
.Rms({ mode: "moving", windowSize: 2 })
|
|
264
|
+
.Rectify();
|
|
265
|
+
|
|
266
|
+
// Build state
|
|
267
|
+
await processor.process(new Float32Array([1, 2, 3, 4]), DEFAULT_OPTIONS);
|
|
268
|
+
|
|
269
|
+
// Reset
|
|
270
|
+
processor.clearState();
|
|
271
|
+
|
|
272
|
+
// Process fresh data
|
|
273
|
+
const output = await processor.process(
|
|
274
|
+
new Float32Array([10]),
|
|
275
|
+
DEFAULT_OPTIONS
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// Should be close to 10 (single value through the pipeline)
|
|
279
|
+
assertCloseTo(output[0], 10);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("Order Dependency", () => {
|
|
284
|
+
test("should produce different results with different chain order", async () => {
|
|
285
|
+
const processor1 = createDspPipeline();
|
|
286
|
+
processor1.Rectify({ mode: "half" }).MovingAverage({ mode: "moving", windowSize: 2 });
|
|
287
|
+
|
|
288
|
+
const processor2 = createDspPipeline();
|
|
289
|
+
processor2.MovingAverage({ mode: "moving", windowSize: 2 }).Rectify({ mode: "half" });
|
|
290
|
+
|
|
291
|
+
const input = new Float32Array([1, -3, 2]);
|
|
292
|
+
|
|
293
|
+
const output1 = await processor1.process(
|
|
294
|
+
new Float32Array(input),
|
|
295
|
+
DEFAULT_OPTIONS
|
|
296
|
+
);
|
|
297
|
+
const output2 = await processor2.process(
|
|
298
|
+
new Float32Array(input),
|
|
299
|
+
DEFAULT_OPTIONS
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Processor1: Rectify first → [1, 0, 2] → MovingAvg → [1, 0.5, 1]
|
|
303
|
+
// Processor2: MovingAvg first → [1, -1, 0.5] → Rectify → [1, 0, 0.5]
|
|
304
|
+
assert.notDeepEqual(Array.from(output1), Array.from(output2));
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("Complex Scenarios", () => {
|
|
309
|
+
test("should handle empty input in chain", async () => {
|
|
310
|
+
processor.MovingAverage({ mode: "moving", windowSize: 2 }).Rms({ mode: "moving", windowSize: 2 });
|
|
311
|
+
|
|
312
|
+
const output = await processor.process(
|
|
313
|
+
new Float32Array([]),
|
|
314
|
+
DEFAULT_OPTIONS
|
|
315
|
+
);
|
|
316
|
+
assert.equal(output.length, 0);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("should handle single sample through entire chain", async () => {
|
|
320
|
+
processor
|
|
321
|
+
.MovingAverage({ mode: "moving", windowSize: 3 })
|
|
322
|
+
.Rectify({ mode: "full" })
|
|
323
|
+
.Rms({ mode: "moving", windowSize: 2 });
|
|
324
|
+
|
|
325
|
+
const output = await processor.process(
|
|
326
|
+
new Float32Array([5]),
|
|
327
|
+
DEFAULT_OPTIONS
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
assert.equal(output.length, 1);
|
|
331
|
+
assertCloseTo(output[0], 5);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("should process large chain efficiently", async () => {
|
|
335
|
+
processor
|
|
336
|
+
.MovingAverage({ mode: "moving", windowSize: 5 })
|
|
337
|
+
.Rectify({ mode: "full" })
|
|
338
|
+
.Rms({ mode: "moving", windowSize: 5 })
|
|
339
|
+
.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
340
|
+
|
|
341
|
+
const input = new Float32Array(100).map((_, i) => Math.sin(i * 0.1));
|
|
342
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
343
|
+
|
|
344
|
+
assert.equal(output.length, 100);
|
|
345
|
+
output.forEach((val) => {
|
|
346
|
+
assert.ok(!isNaN(val));
|
|
347
|
+
assert.ok(val >= 0); // After rectify, all should be positive
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("should handle repeated same-stage chains", async () => {
|
|
352
|
+
processor
|
|
353
|
+
.MovingAverage({ mode: "moving", windowSize: 2 })
|
|
354
|
+
.MovingAverage({ mode: "moving", windowSize: 2 })
|
|
355
|
+
.MovingAverage({ mode: "moving", windowSize: 2 });
|
|
356
|
+
|
|
357
|
+
const input = new Float32Array([1, 2, 3, 4, 5]);
|
|
358
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
359
|
+
|
|
360
|
+
assert.equal(output.length, 5);
|
|
361
|
+
// Triple smoothing should produce smoother output
|
|
362
|
+
output.forEach((val) => assert.ok(!isNaN(val) && val > 0));
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("Edge Cases in Chains", () => {
|
|
367
|
+
test("should handle all-zero signal through chain", async () => {
|
|
368
|
+
processor.MovingAverage({ mode: "moving", windowSize: 3 }).Rms({ mode: "moving", windowSize: 2 });
|
|
369
|
+
|
|
370
|
+
const input = new Float32Array([0, 0, 0, 0, 0]);
|
|
371
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
372
|
+
|
|
373
|
+
output.forEach((val) => assertCloseTo(val, 0));
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("should handle extreme values through chain", async () => {
|
|
377
|
+
processor.Rectify({ mode: "full" }).MovingAverage({ mode: "moving", windowSize: 2 });
|
|
378
|
+
|
|
379
|
+
const input = new Float32Array([1e6, -1e6, 1e-6, -1e-6]);
|
|
380
|
+
const output = await processor.process(input, DEFAULT_OPTIONS);
|
|
381
|
+
|
|
382
|
+
assert.ok(output.every((v) => !isNaN(v)));
|
|
383
|
+
assert.ok(output.some((v) => v > 1e5)); // Large values present
|
|
384
|
+
assert.ok(output.some((v) => v < 1e-5)); // Small values present
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
});
|