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,617 @@
1
+ # Time-Series Processing Guide
2
+
3
+ ## Overview
4
+
5
+ The DSP library supports **time-series processing** with explicit timestamps, enabling:
6
+
7
+ - ✅ Explicit timestamp tracking for sample metadata
8
+ - ✅ Intuitive time-based window specification (via `windowDuration`)
9
+ - ✅ **True time-based sample expiration** - samples expire by actual age, not just count
10
+ - ✅ Proper state persistence with timestamps
11
+ - ✅ Uniform and irregular sampling data processing
12
+ - ✅ 100% backwards compatibility with sample-based API
13
+
14
+ ---
15
+
16
+ ## Quick Start
17
+
18
+ ### Legacy Sample-Based (Still Works!)
19
+
20
+ ```typescript
21
+ import { createDspPipeline } from "dspx";
22
+
23
+ const pipeline = createDspPipeline();
24
+ pipeline.MovingAverage({ mode: "moving", windowSize: 100 });
25
+
26
+ const samples = new Float32Array(1000);
27
+ // ... fill with sensor data ...
28
+
29
+ await pipeline.process(samples, {
30
+ sampleRate: 100, // 100 Hz
31
+ channels: 1,
32
+ });
33
+ ```
34
+
35
+ ### New Time-Based Processing
36
+
37
+ ```typescript
38
+ import { createDspPipeline } from "dspx";
39
+
40
+ const pipeline = createDspPipeline();
41
+ // Use time-based window instead of sample count
42
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 }); // 5 seconds
43
+
44
+ const samples = new Float32Array([1.2, 3.4, 2.1, 4.5, 3.3]);
45
+ const timestamps = new Float32Array([0, 100, 250, 400, 500]); // milliseconds
46
+
47
+ await pipeline.process(samples, timestamps, { channels: 1 });
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Three Processing Modes
53
+
54
+ ### 1. Legacy Mode (Auto-Generated Timestamps from Sample Rate)
55
+
56
+ **Best for:** Uniform sampling, backwards compatibility
57
+
58
+ ```typescript
59
+ const pipeline = createDspPipeline();
60
+ pipeline.MovingAverage({ mode: "moving", windowSize: 50 });
61
+
62
+ await pipeline.process(samples, {
63
+ sampleRate: 100, // 100 Hz = 10ms per sample
64
+ channels: 1,
65
+ });
66
+ // Internally generates timestamps: [0, 10, 20, 30, ...]
67
+ ```
68
+
69
+ ### 2. Time-Based Mode (Explicit Timestamps)
70
+
71
+ **Best for:** Tracking sample timing, time-based window specification
72
+
73
+ ```typescript
74
+ const pipeline = createDspPipeline();
75
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 }); // 5 seconds (converted to sample count)
76
+
77
+ // Real timestamps from your sensors/network
78
+ const timestamps = new Float32Array([
79
+ 1698889200000, // Unix timestamp in ms
80
+ 1698889200150,
81
+ 1698889200320,
82
+ 1698889200500,
83
+ ]);
84
+
85
+ await pipeline.process(samples, timestamps, { channels: 1 });
86
+ ```
87
+
88
+ ### 3. Auto-Sequential Mode (No Sample Rate)
89
+
90
+ **Best for:** Sample-count based processing without time awareness
91
+
92
+ ```typescript
93
+ const pipeline = createDspPipeline();
94
+ pipeline.MovingAverage({ mode: "moving", windowSize: 10 });
95
+
96
+ await pipeline.process(samples, { channels: 1 });
97
+ // Internally generates timestamps: [0, 1, 2, 3, ...]
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Window Parameters: windowSize vs windowDuration
103
+
104
+ ### windowSize (Legacy - Sample Count)
105
+
106
+ ```typescript
107
+ pipeline.MovingAverage({
108
+ mode: "moving",
109
+ windowSize: 100, // Last 100 samples
110
+ });
111
+ ```
112
+
113
+ **Pros:**
114
+
115
+ - Predictable memory usage
116
+ - Consistent across sample rates
117
+ - Faster (no timestamp checks)
118
+
119
+ **Cons:**
120
+
121
+ - Not intuitive for time-based analysis
122
+ - ~~Breaks with irregular sampling~~
123
+ - Requires manual sample rate calculations
124
+
125
+ ### windowDuration (Time-Based Expiration)
126
+
127
+ ```typescript
128
+ pipeline.MovingAverage({
129
+ mode: "moving",
130
+ windowDuration: 5000, // Last 5 seconds of data (true time-based)
131
+ });
132
+ ```
133
+
134
+ **Pros:**
135
+
136
+ - **True time-based expiration** - samples automatically expire when they're older than the window duration
137
+ - Intuitive ("5 seconds of data")
138
+ - Works correctly with irregular sampling (samples expire by age, not count)
139
+ - Independent of sample rate
140
+ - Accurate representation of data within the time window
141
+
142
+ **Cons:**
143
+
144
+ - Requires timestamps to be provided
145
+ - Slightly more memory overhead (stores timestamps alongside samples)
146
+ - Requires buffer size estimation at initialization (uses 3x safety factor)
147
+
148
+ **Implementation:**
149
+
150
+ When `windowDuration` is specified, the library implements **true time-based expiration**:
151
+
152
+ 1. A circular buffer is created with capacity = 3× estimated sample count (safety margin)
153
+ 2. Each sample is stored with its timestamp
154
+ 3. Before adding new samples, old samples are expired using `expireOld(currentTimestamp)`
155
+ 4. Samples are removed when: `sample_timestamp < current_timestamp - windowDuration`
156
+
157
+ **Example - Irregular Sampling:**
158
+
159
+ ```typescript
160
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 1000 }); // 1 second window
161
+
162
+ // Samples arrive at irregular intervals
163
+ const samples = new Float32Array([10, 20, 30, 40]);
164
+ const timestamps = new Float32Array([
165
+ 0, // Sample at 0ms
166
+ 50, // Sample at 50ms
167
+ 600, // Sample at 600ms (550ms gap!)
168
+ 650, // Sample at 650ms
169
+ ]);
170
+
171
+ await pipeline.process(samples, timestamps, { channels: 1 });
172
+
173
+ // At 650ms:
174
+ // - Sample at 0ms is EXPIRED (650 - 0 = 650ms > 1000ms window) ❌
175
+ // - Sample at 50ms is EXPIRED (650 - 50 = 600ms > 1000ms window) ❌
176
+ // - Sample at 600ms is KEPT (650 - 600 = 50ms < 1000ms) ✓
177
+ // - Sample at 650ms is KEPT (current sample) ✓
178
+ // Result: Moving average of [30, 40] = 35
179
+ ```
180
+
181
+ This is fundamentally different from sample-count windows, where exactly N samples would be kept regardless of their timestamps.
182
+
183
+ **Buffer Size Estimation:**
184
+
185
+ The circular buffer capacity is estimated as `3 × (windowDuration_ms / 1000) × estimated_sample_rate`:
186
+
187
+ ```typescript
188
+ // Example: 5 second window at 100 Hz
189
+ // windowDuration = 5000ms
190
+ // sample_rate = 100 Hz
191
+ // capacity = 3 × (5000 / 1000) × 100 = 1500 samples
192
+
193
+ // Why 3x?
194
+ // - Handles bursts (multiple samples arriving together)
195
+ // - Safety margin for sample rate variations
196
+ // - Prevents buffer overflow before expiration
197
+ ```
198
+
199
+ **Important:** True time-based expiration means the number of samples in the window varies dynamically based on sample timing, not a fixed count.
200
+
201
+ ---
202
+
203
+ ## All Filters Support Time-Series
204
+
205
+ All filters support both `windowSize` and `windowDuration`:
206
+
207
+ ### MovingAverage ✅ Time-based expiration
208
+
209
+ ```typescript
210
+ // Sample-based
211
+ pipeline.MovingAverage({ mode: "moving", windowSize: 50 });
212
+
213
+ // Time-based (true time-based expiration)
214
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 10000 }); // 10 seconds
215
+ ```
216
+
217
+ ### RMS (Root Mean Square) ✅ Time-based expiration
218
+
219
+ ```typescript
220
+ // Time-based RMS over last 100ms
221
+ pipeline.Rms({ mode: "moving", windowDuration: 100 });
222
+ ```
223
+
224
+ ### MeanAbsoluteValue ✅ Time-based expiration
225
+
226
+ ```typescript
227
+ // Time-based MAV over last 250ms
228
+ pipeline.MeanAbsoluteValue({ mode: "moving", windowDuration: 250 });
229
+ ```
230
+
231
+ ### Variance ✅ Time-based expiration
232
+
233
+ ```typescript
234
+ // Time-based Variance over last 3 seconds (true time-based expiration)
235
+ pipeline.Variance({ mode: "moving", windowDuration: 3000 });
236
+ ```
237
+
238
+ ### Z-Score Normalization ✅ Time-based expiration
239
+
240
+ ```typescript
241
+ // Time-based Z-Score over last 30 seconds (true time-based expiration)
242
+ pipeline.ZScoreNormalize({
243
+ mode: "moving",
244
+ windowDuration: 30000,
245
+ epsilon: 1e-6,
246
+ });
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Multi-Channel Processing with Timestamps
252
+
253
+ ```typescript
254
+ const pipeline = createDspPipeline();
255
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 });
256
+
257
+ // 2 channels, interleaved: [ch0_sample0, ch1_sample0, ch0_sample1, ch1_sample1, ...]
258
+ const samples = new Float32Array([
259
+ 1.0,
260
+ 10.0, // Sample 0: ch0=1.0, ch1=10.0
261
+ 2.0,
262
+ 20.0, // Sample 1: ch0=2.0, ch1=20.0
263
+ 3.0,
264
+ 30.0, // Sample 2: ch0=3.0, ch1=30.0
265
+ ]);
266
+
267
+ // One timestamp per SAMPLE (not per channel value)
268
+ const timestamps = new Float32Array([0, 100, 200]);
269
+
270
+ await pipeline.process(samples, timestamps, { channels: 2 });
271
+ ```
272
+
273
+ **Important:** The timestamps array length should equal `samples.length / channels`.
274
+
275
+ ---
276
+
277
+ ## Real-World Examples
278
+
279
+ ### IoT Sensor with Network Jitter
280
+
281
+ ```typescript
282
+ import { createDspPipeline } from "dspx";
283
+
284
+ const pipeline = createDspPipeline();
285
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 10000 }); // 10 second window
286
+
287
+ async function processSensorData(sensorReadings) {
288
+ const samples = new Float32Array(sensorReadings.length);
289
+ const timestamps = new Float32Array(sensorReadings.length);
290
+
291
+ sensorReadings.forEach((reading, i) => {
292
+ samples[i] = reading.value;
293
+ timestamps[i] = reading.timestamp; // Unix timestamp in ms
294
+ });
295
+
296
+ const smoothed = await pipeline.process(samples, timestamps, { channels: 1 });
297
+ return smoothed;
298
+ }
299
+
300
+ // Readings might arrive at irregular intervals due to network latency
301
+ const readings = [
302
+ { value: 23.5, timestamp: 1698889200000 },
303
+ { value: 24.1, timestamp: 1698889200150 }, // 150ms later
304
+ { value: 23.8, timestamp: 1698889200380 }, // 230ms later (jitter)
305
+ { value: 24.5, timestamp: 1698889200500 }, // 120ms later
306
+ ];
307
+
308
+ const result = await processSensorData(readings);
309
+ ```
310
+
311
+ ### Financial Data (Irregular Market Hours)
312
+
313
+ ```typescript
314
+ const pipeline = createDspPipeline();
315
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 3600000 }); // 1 hour
316
+
317
+ // Stock ticks at irregular intervals
318
+ const prices = new Float32Array([100.5, 100.7, 100.3, 101.2, 100.9]);
319
+ const tickTimestamps = new Float32Array([
320
+ 1698889200000, // 9:00:00 AM
321
+ 1698889260000, // 9:01:00 AM
322
+ 1698889290000, // 9:01:30 AM (30 sec later)
323
+ 1698889320000, // 9:02:00 AM
324
+ 1698889500000, // 9:05:00 AM (3 min gap)
325
+ ]);
326
+
327
+ const movingAvg = await pipeline.process(prices, tickTimestamps, {
328
+ channels: 1,
329
+ });
330
+ ```
331
+
332
+ ### EMG/ECG Signal Processing
333
+
334
+ ```typescript
335
+ const pipeline = createDspPipeline();
336
+ pipeline
337
+ .Rectify({ mode: "full" })
338
+ .MovingAverage({ mode: "moving", windowDuration: 250 }) // 250ms window
339
+ .Rms({ mode: "moving", windowDuration: 100 }); // 100ms RMS
340
+
341
+ // High-rate biosignal sampling
342
+ const emgSignal = new Float32Array(1000);
343
+ const timestamps = new Float32Array(1000);
344
+
345
+ // Fill with actual sensor data at ~1000 Hz
346
+ for (let i = 0; i < 1000; i++) {
347
+ emgSignal[i] = Math.sin(i * 0.1) + Math.random() * 0.1;
348
+ timestamps[i] = i; // 1ms per sample
349
+ }
350
+
351
+ const processed = await pipeline.process(emgSignal, timestamps, {
352
+ channels: 1,
353
+ });
354
+ ```
355
+
356
+ ---
357
+
358
+ ## State Persistence with Redis
359
+
360
+ Time-series processing works seamlessly with Redis state persistence:
361
+
362
+ ```typescript
363
+ import { createDspPipeline } from "dspx";
364
+ import { createClient } from "redis";
365
+
366
+ const redis = createClient();
367
+ await redis.connect();
368
+
369
+ const pipeline = createDspPipeline();
370
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 });
371
+
372
+ // Process streaming data
373
+ async function processChunk(samples, timestamps) {
374
+ // Restore previous state
375
+ const savedState = await redis.get("dsp:sensor:123");
376
+ if (savedState) {
377
+ await pipeline.loadState(savedState);
378
+ }
379
+
380
+ // Process new chunk
381
+ const result = await pipeline.process(samples, timestamps, { channels: 1 });
382
+
383
+ // Save state for next chunk
384
+ const newState = await pipeline.saveState();
385
+ await redis.set("dsp:sensor:123", newState, { EX: 3600 }); // 1 hour TTL
386
+
387
+ return result;
388
+ }
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Migration Guide
394
+
395
+ ### Migrating from Sample-Based to Time-Based
396
+
397
+ #### Before (Sample-Based)
398
+
399
+ ```typescript
400
+ const pipeline = createDspPipeline();
401
+ pipeline.MovingAverage({ mode: "moving", windowSize: 100 }); // 100 samples
402
+
403
+ await pipeline.process(samples, {
404
+ sampleRate: 1000, // 1000 Hz
405
+ channels: 1,
406
+ });
407
+ ```
408
+
409
+ #### After (Time-Based)
410
+
411
+ ```typescript
412
+ const pipeline = createDspPipeline();
413
+ // 100 samples at 1000 Hz = 100ms window
414
+ pipeline.MovingAverage({ mode: "moving", windowDuration: 100 }); // 100ms
415
+
416
+ // Generate timestamps from your data source
417
+ const timestamps = samples.map((_, i) => i * (1000 / 1000)); // 1ms per sample
418
+
419
+ await pipeline.process(samples, timestamps, { channels: 1 });
420
+ ```
421
+
422
+ ### Converting Window Size to Duration
423
+
424
+ If you know your sample rate, convert `windowSize` to `windowDuration`:
425
+
426
+ ```
427
+ windowDuration (ms) = (windowSize / sampleRate) * 1000
428
+
429
+ Example:
430
+ windowSize = 500 samples
431
+ sampleRate = 100 Hz
432
+ windowDuration = (500 / 100) * 1000 = 5000 ms (5 seconds)
433
+ ```
434
+
435
+ ---
436
+
437
+ ## Performance Considerations
438
+
439
+ ### Memory Usage
440
+
441
+ - **Sample-based windows:** Fixed memory (`windowSize` samples)
442
+ - **Time-based windows:** Fixed memory (converted to `windowSize` samples at initialization)
443
+
444
+ ### Processing Speed
445
+
446
+ - **With timestamps:** Minimal overhead (timestamps are passed but not used for expiration)
447
+ - **Without timestamps:** Fastest (legacy mode)
448
+
449
+ **Recommendation:** Use `windowDuration` when you want to specify windows in time units and have consistent sample rates. Use `windowSize` for maximum control and when sample rate varies significantly.
450
+
451
+ ---
452
+
453
+ ## Error Handling
454
+
455
+ ### Timestamp Validation
456
+
457
+ ```typescript
458
+ // Error: Timestamp array length mismatch
459
+ const samples = new Float32Array(5);
460
+ const timestamps = new Float32Array(3); // Wrong!
461
+
462
+ await pipeline.process(samples, timestamps, { channels: 1 });
463
+ // Throws: "Timestamps length (3) must match samples length (5)"
464
+ ```
465
+
466
+ ### Window Parameter Validation
467
+
468
+ ```typescript
469
+ // Error: Neither windowSize nor windowDuration specified
470
+ pipeline.MovingAverage({ mode: "moving" }); // Throws!
471
+
472
+ // Error: Invalid windowDuration
473
+ pipeline.MovingAverage({ mode: "moving", windowDuration: -100 }); // Throws!
474
+
475
+ // Error: Invalid windowSize
476
+ pipeline.MovingAverage({ mode: "moving", windowSize: 0 }); // Throws!
477
+ ```
478
+
479
+ ---
480
+
481
+ ## API Reference
482
+
483
+ ### Process Methods
484
+
485
+ #### `process(samples, timestamps, options)`
486
+
487
+ Time-based processing with explicit timestamps.
488
+
489
+ ```typescript
490
+ async process(
491
+ samples: Float32Array,
492
+ timestamps: Float32Array,
493
+ options: { channels: number }
494
+ ): Promise<Float32Array>
495
+ ```
496
+
497
+ #### `process(samples, options)`
498
+
499
+ Legacy sample-based processing (auto-generates timestamps).
500
+
501
+ ```typescript
502
+ async process(
503
+ samples: Float32Array,
504
+ options: { sampleRate?: number; channels: number }
505
+ ): Promise<Float32Array>
506
+ ```
507
+
508
+ #### `processCopy(samples, timestamps, options)`
509
+
510
+ Process a copy (preserves original).
511
+
512
+ ```typescript
513
+ async processCopy(
514
+ samples: Float32Array,
515
+ timestamps: Float32Array,
516
+ options: { channels: number }
517
+ ): Promise<Float32Array>
518
+ ```
519
+
520
+ ### Filter Parameters
521
+
522
+ All filters accept either `windowSize` or `windowDuration`:
523
+
524
+ ```typescript
525
+ interface FilterParams {
526
+ mode: "batch" | "moving";
527
+ windowSize?: number; // Number of samples
528
+ windowDuration?: number; // Milliseconds
529
+ }
530
+ ```
531
+
532
+ ---
533
+
534
+ ## Best Practices
535
+
536
+ ### ✅ DO
537
+
538
+ - Use `windowDuration` for true time-based windows that adapt to irregular sampling
539
+ - Pass explicit timestamps to enable time-based expiration
540
+ - Use `windowSize` for fixed sample-count windows (faster, no timestamp overhead)
541
+ - Validate timestamp ordering (should be non-decreasing) when using timestamps
542
+ - Use state persistence for streaming applications
543
+ - Provide sufficient buffer capacity when using both `windowSize` and `windowDuration`
544
+
545
+ ### ❌ DON'T
546
+
547
+ - Forget to provide timestamps when using `windowDuration` (required for time-based expiration)
548
+ - Expect `windowSize` alone to give time-based behavior (it only controls sample count)
549
+ - Assume time-based windows always contain the same number of samples (they adapt dynamically)
550
+ - Forget to handle timezone conversions when using absolute timestamps
551
+ - Process data with backwards-jumping timestamps (non-monotonic time)
552
+
553
+ ---
554
+
555
+ ## Troubleshooting
556
+
557
+ ### Q: My time-based windows aren't working as expected
558
+
559
+ **A:** Time-based windows now implement true time-based expiration! Samples are automatically removed when they're older than `windowDuration` from the current timestamp. Make sure you're providing timestamps with each `process()` call. The number of samples in the window will vary dynamically based on your sampling rate and timing.
560
+
561
+ ### Q: How does time-based expiration handle irregular sampling?
562
+
563
+ **A:** Time-based expiration works correctly with irregular sampling. Samples expire based on their actual age (timestamp difference), not their position in the buffer. For example, with a 1-second window:
564
+
565
+ - A burst of samples arriving within 100ms will all be kept
566
+ - A sample from 1.5 seconds ago will be expired
567
+ - The window always contains samples from the last `windowDuration` milliseconds
568
+
569
+ ### Q: What's the difference between windowSize and windowDuration?
570
+
571
+ **A:**
572
+
573
+ - **windowSize**: Keeps exactly N samples (fixed count, regardless of time span)
574
+ - **windowDuration**: Keeps samples from the last T milliseconds (variable count, based on timing)
575
+
576
+ Use `windowSize` when you want a fixed number of samples. Use `windowDuration` when you want a fixed time span.
577
+
578
+ ### Q: Should I use milliseconds or seconds for timestamps?
579
+
580
+ **A:** Use **milliseconds** for consistency with JavaScript `Date.now()` and Unix timestamps. The library expects milliseconds.
581
+
582
+ ### Q: Can I use both windowSize and windowDuration?
583
+
584
+ **A:** Yes! When both are specified, the filter uses time-based expiration (`windowDuration`) but also allocates a buffer sized for `windowSize` samples. This gives you control over both the time window and memory usage. However, typically you'd use one or the other:
585
+
586
+ - Use `windowSize` alone for sample-count windows
587
+ - Use `windowDuration` alone for true time-based windows (buffer size auto-estimated)
588
+
589
+ ### Q: What happens if I don't provide timestamps?
590
+
591
+ **A:** The library auto-generates sequential timestamps `[0, 1, 2, ...]`. If you provide `sampleRate`, it generates time-based timestamps.
592
+
593
+ ---
594
+
595
+ ## Future Enhancements (Roadmap)
596
+
597
+ - [x] **True time-based filtering** - ✅ FULLY IMPLEMENTED for all moving window filters!
598
+ - MovingAverage, RMS, MeanAbsoluteValue, Variance, and ZScoreNormalize all support true time-based expiration
599
+ - [ ] **Polyphase decimation/interpolation** - High-quality resampling to uniform intervals
600
+ - Anti-aliasing lowpass filter before decimation
601
+ - Polyphase FIR filter banks for efficient computation
602
+ - Support for arbitrary rational resampling ratios (L/M)
603
+ - [ ] **Timestamp interpolation** - Fill gaps in irregular data with linear/spline interpolation
604
+ - [ ] **Timestamp-aware state format** - Include timestamps in serialized state
605
+ - [ ] **Window overlap control** - For advanced signal analysis (STFT, spectrograms)
606
+
607
+ ---
608
+
609
+ ## Need Help?
610
+
611
+ - **GitHub Issues:** [Report bugs or request features](https://github.com/A-KGeorge/dspx/issues)
612
+ - **Documentation:** Check `docs/` folder for additional guides
613
+ - **Examples:** See `src/ts/examples/` for working code samples
614
+
615
+ ---
616
+
617
+ **Happy time-series processing! 📈**