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,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
+ });