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,290 @@
1
+ /**
2
+ * Comparison: Sample-Based vs Time-Based Processing
3
+ *
4
+ * This example demonstrates the difference between legacy sample-based
5
+ * and new time-based processing, especially with irregular data.
6
+ */
7
+
8
+ import { createDspPipeline } from "../../bindings";
9
+
10
+ /**
11
+ * Generate uniform sensor data (ideal case)
12
+ */
13
+ function generateUniformData(count: number, sampleRate: number) {
14
+ const samples = new Float32Array(count);
15
+ const timestamps = new Float32Array(count);
16
+
17
+ for (let i = 0; i < count; i++) {
18
+ samples[i] = Math.sin(i * 0.1) * 10 + (Math.random() - 0.5) * 2;
19
+ timestamps[i] = i * (1000 / sampleRate); // Milliseconds
20
+ }
21
+
22
+ return { samples, timestamps };
23
+ }
24
+
25
+ /**
26
+ * Generate irregular sensor data (realistic case)
27
+ */
28
+ function generateIrregularData(count: number, avgInterval: number) {
29
+ const samples = new Float32Array(count);
30
+ const timestamps = new Float32Array(count);
31
+ let currentTime = 0;
32
+
33
+ for (let i = 0; i < count; i++) {
34
+ samples[i] = Math.sin(i * 0.1) * 10 + (Math.random() - 0.5) * 2;
35
+
36
+ // Random interval: 50% to 150% of average
37
+ const interval = avgInterval * (0.5 + Math.random());
38
+ currentTime += interval;
39
+ timestamps[i] = currentTime;
40
+ }
41
+
42
+ return { samples, timestamps };
43
+ }
44
+
45
+ /**
46
+ * Compare sample-based vs time-based on UNIFORM data
47
+ */
48
+ async function compareUniformData() {
49
+ console.log("=== Comparison on UNIFORM Data (Ideal Case) ===\n");
50
+
51
+ const sampleRate = 100; // 100 Hz
52
+ const { samples, timestamps } = generateUniformData(100, sampleRate);
53
+
54
+ // Sample-based pipeline (legacy)
55
+ const pipelineSample = createDspPipeline();
56
+ pipelineSample.MovingAverage({
57
+ mode: "moving",
58
+ windowSize: 50, // 50 samples
59
+ });
60
+
61
+ // Time-based pipeline (new)
62
+ const pipelineTime = createDspPipeline();
63
+ pipelineTime.MovingAverage({
64
+ mode: "moving",
65
+ windowDuration: 500, // 500ms (equivalent to 50 samples at 100 Hz)
66
+ });
67
+
68
+ // Process with sample-based
69
+ const resultSample = await pipelineSample.process(samples, {
70
+ sampleRate,
71
+ channels: 1,
72
+ });
73
+
74
+ // Process with time-based
75
+ const resultTime = await pipelineTime.process(samples, timestamps, {
76
+ channels: 1,
77
+ });
78
+
79
+ // Compare results
80
+ console.log("Input: Uniform 100 Hz sampling, 100 samples");
81
+ console.log("Window: 50 samples (sample-based) vs 500ms (time-based)");
82
+ console.log("\nComparison (first 5 samples):");
83
+ console.log(" Index | Input | Sample-Based | Time-Based | Difference");
84
+ console.log(" ------|----------|--------------|------------|------------");
85
+
86
+ for (let i = 0; i < 5; i++) {
87
+ const diff = Math.abs(resultSample[i] - resultTime[i]);
88
+ console.log(
89
+ ` ${i.toString().padStart(5)} | ${samples[i]
90
+ .toFixed(3)
91
+ .padStart(8)} | ` +
92
+ `${resultSample[i].toFixed(3).padStart(12)} | ${resultTime[i]
93
+ .toFixed(3)
94
+ .padStart(10)} | ` +
95
+ `${diff.toFixed(6).padStart(10)}`
96
+ );
97
+ }
98
+
99
+ // Calculate max difference
100
+ let maxDiff = 0;
101
+ for (let i = 0; i < samples.length; i++) {
102
+ maxDiff = Math.max(maxDiff, Math.abs(resultSample[i] - resultTime[i]));
103
+ }
104
+
105
+ console.log(
106
+ `\n✅ Max difference: ${maxDiff.toFixed(6)} (essentially identical)`
107
+ );
108
+ console.log(
109
+ " → On uniform data, both methods produce equivalent results\n"
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Compare sample-based vs time-based on IRREGULAR data
115
+ */
116
+ async function compareIrregularData() {
117
+ console.log("=== Comparison on IRREGULAR Data (Realistic Case) ===\n");
118
+
119
+ const avgInterval = 10; // ~100 Hz average, but irregular
120
+ const { samples, timestamps } = generateIrregularData(100, avgInterval);
121
+
122
+ // Calculate actual intervals
123
+ const intervals: number[] = [];
124
+ for (let i = 1; i < timestamps.length; i++) {
125
+ intervals.push(timestamps[i] - timestamps[i - 1]);
126
+ }
127
+ const avgActualInterval =
128
+ intervals.reduce((a, b) => a + b, 0) / intervals.length;
129
+ const minInterval = Math.min(...intervals);
130
+ const maxInterval = Math.max(...intervals);
131
+
132
+ console.log("Input: Irregular sampling");
133
+ console.log(
134
+ ` Average interval: ${avgActualInterval.toFixed(2)}ms (~${(
135
+ 1000 / avgActualInterval
136
+ ).toFixed(0)} Hz)`
137
+ );
138
+ console.log(
139
+ ` Interval range: ${minInterval.toFixed(2)}ms - ${maxInterval.toFixed(
140
+ 2
141
+ )}ms`
142
+ );
143
+ console.log(
144
+ ` Jitter: ${(
145
+ ((maxInterval - minInterval) / avgActualInterval) *
146
+ 100
147
+ ).toFixed(1)}%\n`
148
+ );
149
+
150
+ // Sample-based pipeline (treats irregular data as uniform)
151
+ const pipelineSample = createDspPipeline();
152
+ pipelineSample.MovingAverage({
153
+ mode: "moving",
154
+ windowSize: 50, // 50 samples (ignores timestamps)
155
+ });
156
+
157
+ // Time-based pipeline (respects timestamps)
158
+ const pipelineTime = createDspPipeline();
159
+ pipelineTime.MovingAverage({
160
+ mode: "moving",
161
+ windowDuration: 500, // 500ms (uses actual timestamps)
162
+ });
163
+
164
+ // Process with sample-based (ignores irregularity)
165
+ const resultSample = await pipelineSample.process(samples, {
166
+ channels: 1, // No sampleRate = sequential timestamps [0,1,2,...]
167
+ });
168
+
169
+ // Process with time-based (respects timestamps)
170
+ const resultTime = await pipelineTime.process(samples, timestamps, {
171
+ channels: 1,
172
+ });
173
+
174
+ // Compare results
175
+ console.log("Window: 50 samples (sample-based) vs 500ms (time-based)");
176
+ console.log("\nComparison (samples 20-24, where differences emerge):");
177
+ console.log(
178
+ " Index | Input | Sample-Based | Time-Based | Difference | Δt (ms)"
179
+ );
180
+ console.log(
181
+ " ------|----------|--------------|------------|------------|--------"
182
+ );
183
+
184
+ for (let i = 20; i < 25; i++) {
185
+ const diff = Math.abs(resultSample[i] - resultTime[i]);
186
+ const deltaT = i > 0 ? timestamps[i] - timestamps[i - 1] : 0;
187
+ console.log(
188
+ ` ${i.toString().padStart(5)} | ${samples[i]
189
+ .toFixed(3)
190
+ .padStart(8)} | ` +
191
+ `${resultSample[i].toFixed(3).padStart(12)} | ${resultTime[i]
192
+ .toFixed(3)
193
+ .padStart(10)} | ` +
194
+ `${diff.toFixed(3).padStart(10)} | ${deltaT.toFixed(2).padStart(6)}`
195
+ );
196
+ }
197
+
198
+ // Calculate statistics on differences
199
+ let sumDiff = 0;
200
+ let maxDiff = 0;
201
+ for (let i = 0; i < samples.length; i++) {
202
+ const diff = Math.abs(resultSample[i] - resultTime[i]);
203
+ sumDiff += diff;
204
+ maxDiff = Math.max(maxDiff, diff);
205
+ }
206
+ const avgDiff = sumDiff / samples.length;
207
+
208
+ console.log(`\n⚠️ Average difference: ${avgDiff.toFixed(3)}`);
209
+ console.log(`⚠️ Max difference: ${maxDiff.toFixed(3)}`);
210
+ console.log(" → On irregular data, results diverge significantly!");
211
+ console.log(" → Time-based processing accounts for actual timing\n");
212
+ }
213
+
214
+ /**
215
+ * Demonstrate when to use each approach
216
+ */
217
+ async function showUseCases() {
218
+ console.log("=== When to Use Each Approach ===\n");
219
+
220
+ console.log("✅ USE SAMPLE-BASED (windowSize) when:");
221
+ console.log(" • Data is uniformly sampled (ADC, audio, high-rate sensors)");
222
+ console.log(" • Sample count is more important than time");
223
+ console.log(" • Maximum performance is critical");
224
+ console.log(" • Working with legacy code\n");
225
+
226
+ console.log("✅ USE TIME-BASED (windowDuration) when:");
227
+ console.log(" • Data arrives at irregular intervals (network, IoT)");
228
+ console.log(' • Time-based analysis is required ("last 5 seconds")');
229
+ console.log(" • Dealing with real-world sensor delays/jitter");
230
+ console.log(" • Need intuitive window specification\n");
231
+
232
+ console.log("✅ USE BOTH (windowSize + windowDuration) when:");
233
+ console.log(" • Need to enforce maximum sample count AND time window");
234
+ console.log(" • Want memory limits with time-based semantics");
235
+ console.log(" • Processing variable-rate data with hard limits\n");
236
+ }
237
+
238
+ /**
239
+ * Migration example: converting existing code
240
+ */
241
+ async function showMigration() {
242
+ console.log("=== Migration Example ===\n");
243
+
244
+ console.log("BEFORE (Sample-Based):");
245
+ console.log("```typescript");
246
+ console.log("const pipeline = createDspPipeline();");
247
+ console.log("pipeline.MovingAverage({ mode: 'moving', windowSize: 100 });");
248
+ console.log("");
249
+ console.log("await pipeline.process(samples, {");
250
+ console.log(" sampleRate: 1000, // 1000 Hz");
251
+ console.log(" channels: 1");
252
+ console.log("});");
253
+ console.log("```\n");
254
+
255
+ console.log("AFTER (Time-Based):");
256
+ console.log("```typescript");
257
+ console.log("const pipeline = createDspPipeline();");
258
+ console.log("// 100 samples at 1000 Hz = 100ms");
259
+ console.log(
260
+ "pipeline.MovingAverage({ mode: 'moving', windowDuration: 100 });"
261
+ );
262
+ console.log("");
263
+ console.log("// Generate timestamps from your data source");
264
+ console.log("const timestamps = new Float32Array(samples.length);");
265
+ console.log("for (let i = 0; i < samples.length; i++) {");
266
+ console.log(" timestamps[i] = i; // 1ms per sample for 1000 Hz");
267
+ console.log("}");
268
+ console.log("");
269
+ console.log("await pipeline.process(samples, timestamps, { channels: 1 });");
270
+ console.log("```\n");
271
+
272
+ console.log("Conversion Formula:");
273
+ console.log(" windowDuration (ms) = (windowSize / sampleRate) * 1000\n");
274
+ }
275
+
276
+ // Run all comparisons
277
+ async function main() {
278
+ await compareUniformData();
279
+ await compareIrregularData();
280
+ await showUseCases();
281
+ await showMigration();
282
+
283
+ console.log("=== Comparison Complete ===");
284
+ console.log("\n💡 Key Takeaway:");
285
+ console.log(" • Uniform data: Both methods equivalent");
286
+ console.log(" • Irregular data: Time-based processing essential");
287
+ console.log(" • Choose based on your data characteristics!\n");
288
+ }
289
+
290
+ main().catch(console.error);
@@ -0,0 +1,143 @@
1
+ /**
2
+ * IoT Sensor Processing with Irregular Timestamps
3
+ *
4
+ * This example demonstrates processing sensor data with network jitter,
5
+ * where samples arrive at irregular intervals.
6
+ */
7
+
8
+ import { createDspPipeline } from "../../bindings";
9
+
10
+ interface SensorReading {
11
+ value: number;
12
+ timestamp: number; // Unix timestamp in milliseconds
13
+ sensorId: string;
14
+ }
15
+
16
+ /**
17
+ * Simulates IoT sensor data with realistic network jitter
18
+ */
19
+ function generateIrregularSensorData(
20
+ count: number,
21
+ baseInterval: number = 100
22
+ ): SensorReading[] {
23
+ const readings: SensorReading[] = [];
24
+ let currentTime = Date.now();
25
+
26
+ for (let i = 0; i < count; i++) {
27
+ // Add random jitter: ±50% of base interval
28
+ const jitter = (Math.random() - 0.5) * baseInterval;
29
+ currentTime += baseInterval + jitter;
30
+
31
+ // Simulate temperature sensor with noise
32
+ const baseTemp = 20 + Math.sin(i * 0.05) * 5; // Varying baseline
33
+ const noise = (Math.random() - 0.5) * 2; // ±1 degree noise
34
+
35
+ readings.push({
36
+ value: baseTemp + noise,
37
+ timestamp: currentTime,
38
+ sensorId: "TEMP-001",
39
+ });
40
+ }
41
+
42
+ return readings;
43
+ }
44
+
45
+ /**
46
+ * Process sensor data with time-based moving average
47
+ */
48
+ async function processSensorData() {
49
+ console.log("=== IoT Sensor Processing Example ===\n");
50
+
51
+ // Generate 50 readings with ~100ms intervals (but irregular)
52
+ const readings = generateIrregularSensorData(50, 100);
53
+
54
+ console.log("Raw sensor readings (first 5):");
55
+ readings.slice(0, 5).forEach((r, i) => {
56
+ const interval = i > 0 ? r.timestamp - readings[i - 1].timestamp : 0;
57
+ console.log(
58
+ ` [${i}] ${r.value.toFixed(2)}°C at t=${
59
+ r.timestamp
60
+ } (Δ=${interval.toFixed(0)}ms)`
61
+ );
62
+ });
63
+
64
+ // Create pipeline with time-based window (5 second moving average)
65
+ const pipeline = createDspPipeline();
66
+ pipeline.MovingAverage({
67
+ mode: "moving",
68
+ windowDuration: 5000, // 5 seconds
69
+ });
70
+
71
+ // Convert to Float32Arrays
72
+ const samples = new Float32Array(readings.map((r) => r.value));
73
+ const timestamps = new Float32Array(readings.map((r) => r.timestamp));
74
+
75
+ // Process with timestamps
76
+ const smoothed = await pipeline.process(samples, timestamps, {
77
+ channels: 1,
78
+ });
79
+
80
+ console.log("\nSmoothed data (5-second moving average, first 5):");
81
+ for (let i = 0; i < 5; i++) {
82
+ console.log(
83
+ ` [${i}] Raw: ${samples[i].toFixed(2)}°C → Smoothed: ${smoothed[
84
+ i
85
+ ].toFixed(2)}°C`
86
+ );
87
+ }
88
+
89
+ // Calculate statistics
90
+ const rawStdDev = calculateStdDev(Array.from(samples));
91
+ const smoothedStdDev = calculateStdDev(Array.from(smoothed));
92
+ const noiseReduction = ((rawStdDev - smoothedStdDev) / rawStdDev) * 100;
93
+
94
+ console.log("\nNoise reduction statistics:");
95
+ console.log(` Raw data std dev: ${rawStdDev.toFixed(3)}°C`);
96
+ console.log(` Smoothed std dev: ${smoothedStdDev.toFixed(3)}°C`);
97
+ console.log(` Noise reduction: ${noiseReduction.toFixed(1)}%`);
98
+
99
+ // Demonstrate state persistence
100
+ const state = await pipeline.saveState();
101
+ console.log(`\nSaved pipeline state (${state.length} bytes)`);
102
+
103
+ // Create new pipeline and restore state
104
+ const pipeline2 = createDspPipeline();
105
+ pipeline2.MovingAverage({
106
+ mode: "moving",
107
+ windowDuration: 5000,
108
+ });
109
+ await pipeline2.loadState(state);
110
+ console.log("State restored to new pipeline");
111
+
112
+ // Process new data with restored state
113
+ const newReadings = generateIrregularSensorData(5, 100);
114
+ const newSamples = new Float32Array(newReadings.map((r) => r.value));
115
+ const newTimestamps = new Float32Array(
116
+ newReadings.map(
117
+ (r) => r.timestamp + readings[readings.length - 1].timestamp
118
+ )
119
+ );
120
+ const continued = await pipeline2.process(newSamples, newTimestamps, {
121
+ channels: 1,
122
+ });
123
+
124
+ console.log("\nContinued processing with restored state:");
125
+ for (let i = 0; i < continued.length; i++) {
126
+ console.log(
127
+ ` [${i}] ${newSamples[i].toFixed(2)}°C → ${continued[i].toFixed(2)}°C`
128
+ );
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Calculate standard deviation
134
+ */
135
+ function calculateStdDev(values: number[]): number {
136
+ const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
137
+ const variance =
138
+ values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / values.length;
139
+ return Math.sqrt(variance);
140
+ }
141
+
142
+ // Run example
143
+ processSensorData().catch(console.error);
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Redis-Backed Streaming Data Processing
3
+ *
4
+ * This example shows how to process streaming sensor data with Redis state
5
+ * persistence, handling data chunks with timestamps.
6
+ */
7
+
8
+ import { createDspPipeline } from "../../bindings";
9
+ import { createClient } from "redis";
10
+
11
+ interface DataChunk {
12
+ deviceId: string;
13
+ samples: Float32Array;
14
+ timestamps: Float32Array;
15
+ }
16
+
17
+ /**
18
+ * Streaming processor with Redis state persistence
19
+ */
20
+ class StreamingProcessor {
21
+ private pipeline;
22
+ private redis;
23
+ private stateKey: string;
24
+
25
+ constructor(deviceId: string) {
26
+ this.pipeline = createDspPipeline();
27
+
28
+ // Multi-stage pipeline for sensor data
29
+ this.pipeline
30
+ .MovingAverage({
31
+ mode: "moving",
32
+ windowDuration: 10000, // 10 second smoothing
33
+ })
34
+ .Rms({
35
+ mode: "moving",
36
+ windowDuration: 5000, // 5 second RMS
37
+ })
38
+ .ZScoreNormalize({
39
+ mode: "moving",
40
+ windowDuration: 60000, // 1 minute normalization window
41
+ epsilon: 1e-6,
42
+ });
43
+
44
+ this.redis = createClient();
45
+ this.stateKey = `dsp:streaming:${deviceId}`;
46
+ }
47
+
48
+ async initialize() {
49
+ await this.redis.connect();
50
+ console.log(`Connected to Redis for device state persistence`);
51
+
52
+ // Try to restore previous state
53
+ const savedState = await this.redis.get(this.stateKey);
54
+ if (savedState) {
55
+ await this.pipeline.loadState(savedState);
56
+ console.log(`Restored previous state for ${this.stateKey}`);
57
+ } else {
58
+ console.log(`No previous state found, starting fresh`);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Process a chunk of streaming data
64
+ */
65
+ async processChunk(chunk: DataChunk): Promise<Float32Array> {
66
+ console.log(
67
+ `\nProcessing chunk: ${chunk.samples.length} samples from ${chunk.deviceId}`
68
+ );
69
+ console.log(
70
+ ` Time range: ${chunk.timestamps[0]} - ${
71
+ chunk.timestamps[chunk.timestamps.length - 1]
72
+ }`
73
+ );
74
+
75
+ // Process data
76
+ const result = await this.pipeline.process(
77
+ chunk.samples,
78
+ chunk.timestamps,
79
+ { channels: 1 }
80
+ );
81
+
82
+ // Save state to Redis
83
+ const newState = await this.pipeline.saveState();
84
+ await this.redis.set(this.stateKey, newState, {
85
+ EX: 3600, // 1 hour TTL
86
+ });
87
+ console.log(` Saved state to Redis (${newState.length} bytes)`);
88
+
89
+ return result;
90
+ }
91
+
92
+ async close() {
93
+ await this.redis.quit();
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Simulate streaming sensor data chunks
99
+ */
100
+ function generateDataChunk(
101
+ chunkIndex: number,
102
+ samplesPerChunk: number,
103
+ startTime: number
104
+ ): DataChunk {
105
+ const samples = new Float32Array(samplesPerChunk);
106
+ const timestamps = new Float32Array(samplesPerChunk);
107
+
108
+ for (let i = 0; i < samplesPerChunk; i++) {
109
+ const globalIndex = chunkIndex * samplesPerChunk + i;
110
+
111
+ // Simulate sensor data: sine wave + noise + drift
112
+ const baseSignal = Math.sin(globalIndex * 0.1) * 10;
113
+ const noise = (Math.random() - 0.5) * 2;
114
+ const drift = globalIndex * 0.01;
115
+
116
+ samples[i] = baseSignal + noise + drift;
117
+
118
+ // Irregular timestamps (realistic network jitter)
119
+ const nominalInterval = 100; // ~100ms
120
+ const jitter = (Math.random() - 0.5) * 20; // ±10ms jitter
121
+ timestamps[i] = startTime + i * nominalInterval + jitter;
122
+ }
123
+
124
+ return {
125
+ deviceId: "SENSOR-001",
126
+ samples,
127
+ timestamps,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Main streaming example
133
+ */
134
+ async function runStreamingExample() {
135
+ console.log("=== Redis-Backed Streaming Processing Example ===\n");
136
+
137
+ const processor = new StreamingProcessor("SENSOR-001");
138
+ await processor.initialize();
139
+
140
+ // Simulate 5 chunks of streaming data
141
+ const chunksCount = 5;
142
+ const samplesPerChunk = 20;
143
+ let startTime = Date.now();
144
+
145
+ const allResults: number[] = [];
146
+
147
+ for (let i = 0; i < chunksCount; i++) {
148
+ console.log(`\n--- Chunk ${i + 1}/${chunksCount} ---`);
149
+
150
+ const chunk = generateDataChunk(i, samplesPerChunk, startTime);
151
+ const result = await processor.processChunk(chunk);
152
+
153
+ // Show sample results
154
+ console.log(` Results (first 3):`);
155
+ for (let j = 0; j < Math.min(3, result.length); j++) {
156
+ console.log(
157
+ ` [${j}] Input: ${chunk.samples[j].toFixed(2)} → Output: ${result[
158
+ j
159
+ ].toFixed(3)}`
160
+ );
161
+ }
162
+
163
+ allResults.push(...Array.from(result));
164
+
165
+ // Update start time for next chunk
166
+ startTime = chunk.timestamps[chunk.timestamps.length - 1] + 100;
167
+
168
+ // Simulate real-time delay between chunks
169
+ await new Promise((resolve) => setTimeout(resolve, 100));
170
+ }
171
+
172
+ console.log(
173
+ `\n\nProcessed ${allResults.length} total samples across ${chunksCount} chunks`
174
+ );
175
+ console.log(
176
+ `Final output range: [${Math.min(...allResults).toFixed(3)}, ${Math.max(
177
+ ...allResults
178
+ ).toFixed(3)}]`
179
+ );
180
+
181
+ await processor.close();
182
+ console.log("\nClosed Redis connection");
183
+ }
184
+
185
+ /**
186
+ * Example: Recovery from interruption
187
+ */
188
+ async function demonstrateRecovery() {
189
+ console.log("\n\n=== Demonstrating Recovery After Interruption ===\n");
190
+
191
+ // Process first chunk
192
+ const processor1 = new StreamingProcessor("SENSOR-002");
193
+ await processor1.initialize();
194
+
195
+ const chunk1 = generateDataChunk(0, 20, Date.now());
196
+ await processor1.processChunk(chunk1);
197
+ console.log("Processed chunk 1, state saved");
198
+
199
+ await processor1.close();
200
+ console.log("Simulating process crash/restart...\n");
201
+
202
+ // Simulate restart - new processor instance
203
+ const processor2 = new StreamingProcessor("SENSOR-002");
204
+ await processor2.initialize(); // Automatically restores state
205
+
206
+ // Continue with next chunk
207
+ const chunk2 = generateDataChunk(
208
+ 1,
209
+ 20,
210
+ chunk1.timestamps[chunk1.timestamps.length - 1] + 100
211
+ );
212
+ const result2 = await processor2.processChunk(chunk2);
213
+
214
+ console.log("Successfully continued processing after restart!");
215
+ console.log(` Processed ${result2.length} samples with restored context`);
216
+
217
+ await processor2.close();
218
+ }
219
+
220
+ // Run examples
221
+ async function main() {
222
+ try {
223
+ await runStreamingExample();
224
+ await demonstrateRecovery();
225
+ } catch (error) {
226
+ console.error("Error:", error);
227
+ if (error instanceof Error && error.message.includes("ECONNREFUSED")) {
228
+ console.error("\n⚠️ Make sure Redis is running: redis-server");
229
+ }
230
+ }
231
+ }
232
+
233
+ main().catch(console.error);