waveform-analysis 0.1.48 → 0.1.50

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 (117) hide show
  1. package/README.md +120 -5
  2. package/dist/assets/workerRuntime-Dt94jbqq.js +1 -0
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.js +2683 -1153
  5. package/dist/style.css +1 -1
  6. package/dist/types/components/WaveformChart.vue.d.ts +14 -0
  7. package/dist/types/components/annotation/WaveformAnnotationEditor.vue.d.ts +0 -1
  8. package/dist/types/components/annotation/markup.d.ts +5 -6
  9. package/dist/types/components/annotation/types.d.ts +2 -0
  10. package/dist/types/components/core/latestTaskScheduler.d.ts +19 -0
  11. package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
  12. package/dist/types/components/core/types.d.ts +2 -0
  13. package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
  14. package/dist/types/components/core/useWaveformData.d.ts +2 -0
  15. package/dist/types/components/core/useWaveformLayout.d.ts +2 -0
  16. package/dist/types/components/core/useWaveformRenderSampling.d.ts +19 -0
  17. package/dist/types/components/core/waveformChartTypes.d.ts +8 -1
  18. package/dist/types/components/core/workerSamplingClient.d.ts +8 -0
  19. package/dist/types/components/data/types.d.ts +1 -1
  20. package/dist/types/components/interaction/zoomConstraints.d.ts +2 -0
  21. package/dist/types/components/waveform.d.ts +1 -1
  22. package/dist/types/core/compactSamplePointSource.d.ts +3 -0
  23. package/dist/types/core/data.d.ts +7 -1
  24. package/dist/types/core/index.d.ts +2 -1
  25. package/dist/types/core/rendering.d.ts +3 -1
  26. package/dist/types/core/renderingOptions.d.ts +11 -1
  27. package/dist/types/core/samplingReference.d.ts +36 -0
  28. package/dist/types/core/samplingReferenceValues.d.ts +4 -0
  29. package/dist/types/core/samplingReferenceVectors.d.ts +17 -0
  30. package/dist/types/core/wasmSampling.d.ts +48 -0
  31. package/dist/types/core/waveformPointSource.d.ts +53 -0
  32. package/dist/types/core/workerSampling/index.d.ts +5 -0
  33. package/dist/types/core/workerSampling/javascriptBackend.d.ts +3 -0
  34. package/dist/types/core/workerSampling/multiresolution.d.ts +29 -0
  35. package/dist/types/core/workerSampling/protocol.d.ts +153 -0
  36. package/dist/types/core/workerSampling/repository.d.ts +36 -0
  37. package/dist/types/core/workerSampling/repositoryDataset.d.ts +29 -0
  38. package/dist/types/core/workerSampling/repositorySupport.d.ts +27 -0
  39. package/dist/types/core/workerSampling/samplingCache.d.ts +24 -0
  40. package/dist/types/core/workerSampling/workerRuntime.d.ts +1 -0
  41. package/dist/types/index.d.ts +2 -2
  42. package/dist/types/types/chart.d.ts +56 -0
  43. package/dist/types/types/data.d.ts +20 -2
  44. package/dist/types/types/index.d.ts +2 -2
  45. package/dist/types/views/WasmSamplingDemo.vue.d.ts +3 -0
  46. package/package.json +5 -4
  47. package/src/App.test.ts +11 -7
  48. package/src/App.vue +2 -2
  49. package/src/DemoRouterApp.vue +1 -0
  50. package/src/components/WaveformChart.css +2 -0
  51. package/src/components/annotation/WaveformAnnotationEditor.css +2 -2
  52. package/src/components/annotation/WaveformAnnotationEditor.vue +8 -4
  53. package/src/components/annotation/markup.ts +45 -26
  54. package/src/components/annotation/types.ts +2 -0
  55. package/src/components/annotation/useWaveformChartAnnotations.ts +12 -15
  56. package/src/components/core/latestTaskScheduler.test.ts +51 -0
  57. package/src/components/core/latestTaskScheduler.ts +75 -0
  58. package/src/components/core/trackLayoutBuilder.ts +3 -0
  59. package/src/components/core/types.ts +2 -0
  60. package/src/components/core/useWaveformChartController.ts +14 -0
  61. package/src/components/core/useWaveformData.test.ts +95 -0
  62. package/src/components/core/useWaveformData.ts +23 -32
  63. package/src/components/core/useWaveformLayout.ts +4 -4
  64. package/src/components/core/useWaveformRenderSampling.test.ts +17 -0
  65. package/src/components/core/useWaveformRenderSampling.ts +394 -0
  66. package/src/components/core/waveformChartTypes.ts +13 -0
  67. package/src/components/core/workerSamplingClient.ts +148 -0
  68. package/src/components/data/types.ts +9 -0
  69. package/src/components/interaction/WaveformTooltip.test.ts +1 -3
  70. package/src/components/interaction/WaveformTooltip.vue +13 -12
  71. package/src/components/interaction/useWaveformHover.ts +7 -7
  72. package/src/components/interaction/zoomConstraints.ts +5 -1
  73. package/src/components/waveform.ts +9 -0
  74. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +15 -4
  75. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +11 -6
  76. package/src/components/waveformChartCases/dataLifecycle.test.ts +1 -3
  77. package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +117 -0
  78. package/src/components/waveformChartCases/xDomainStrategy.test.ts +14 -2
  79. package/src/core/compactSamplePointSource.ts +76 -0
  80. package/src/core/data.test.ts +114 -0
  81. package/src/core/data.ts +177 -75
  82. package/src/core/index.ts +11 -0
  83. package/src/core/rendering.test.ts +87 -12
  84. package/src/core/rendering.ts +35 -1
  85. package/src/core/renderingOptions.ts +124 -15
  86. package/src/core/samplingReference.test.ts +124 -0
  87. package/src/core/samplingReference.ts +243 -0
  88. package/src/core/samplingReferenceValues.ts +169 -0
  89. package/src/core/samplingReferenceVectors.ts +46 -0
  90. package/src/core/wasmSampling.test.ts +205 -0
  91. package/src/core/wasmSampling.ts +230 -0
  92. package/src/core/waveformPointSource.ts +395 -0
  93. package/src/core/workerSampling/index.ts +30 -0
  94. package/src/core/workerSampling/javascriptBackend.ts +15 -0
  95. package/src/core/workerSampling/multiresolution.test.ts +69 -0
  96. package/src/core/workerSampling/multiresolution.ts +300 -0
  97. package/src/core/workerSampling/protocol.test.ts +34 -0
  98. package/src/core/workerSampling/protocol.ts +207 -0
  99. package/src/core/workerSampling/repository.test.ts +400 -0
  100. package/src/core/workerSampling/repository.ts +400 -0
  101. package/src/core/workerSampling/repositoryCache.test.ts +163 -0
  102. package/src/core/workerSampling/repositoryDataset.ts +194 -0
  103. package/src/core/workerSampling/repositorySupport.ts +225 -0
  104. package/src/core/workerSampling/repositoryTypedDataset.test.ts +89 -0
  105. package/src/core/workerSampling/repositoryWasmDataset.test.ts +70 -0
  106. package/src/core/workerSampling/samplingCache.ts +94 -0
  107. package/src/core/workerSampling/workerRuntime.ts +40 -0
  108. package/src/data/simulatedWaveforms.test.ts +19 -0
  109. package/src/data/simulatedWaveforms.ts +11 -8
  110. package/src/demoRoutes.css +177 -0
  111. package/src/index.ts +18 -0
  112. package/src/router.ts +5 -0
  113. package/src/types/chart.ts +63 -0
  114. package/src/types/data.ts +24 -1
  115. package/src/types/index.ts +9 -0
  116. package/src/views/WasmSamplingDemo.test.ts +13 -0
  117. package/src/views/WasmSamplingDemo.vue +180 -0
@@ -0,0 +1,300 @@
1
+ import type { WaveformPoint } from '@/types'
2
+
3
+ import type { WorkerSamplingOutput } from './protocol'
4
+
5
+ export type IndexedSamplingStrategy =
6
+ 'none' | 'peak' | 'lttb' | 'average' | 'min' | 'max' | 'minmax' | 'sum'
7
+
8
+ interface ExtremaLayer {
9
+ minimumIndexes: Uint32Array
10
+ maximumIndexes: Uint32Array
11
+ }
12
+
13
+ interface SumLayer {
14
+ x: Float64Array
15
+ y: Float64Array
16
+ count: Uint32Array
17
+ }
18
+
19
+ export interface MultiResolutionSamplingIndexOptions {
20
+ /** Bounds one accessed dataset's lazily-built indexes; source points are not included. */
21
+ maxBytes?: number
22
+ }
23
+
24
+ const DEFAULT_MAX_BYTES = 8 * 1024 * 1024
25
+
26
+ function targetCount(value: number, pointCount: number) {
27
+ if (pointCount === 0) return 0
28
+ if (!Number.isFinite(value)) return pointCount
29
+ return Math.min(pointCount, Math.max(1, Math.floor(value)))
30
+ }
31
+
32
+ function bucketBounds(bucket: number, bucketCount: number, start: number, end: number) {
33
+ const span = end - start
34
+ return {
35
+ start: start + Math.floor((bucket * span) / bucketCount),
36
+ end: start + Math.floor(((bucket + 1) * span) / bucketCount),
37
+ }
38
+ }
39
+
40
+ function addUnique(indexes: number[], index: number) {
41
+ if (indexes[indexes.length - 1] !== index) indexes.push(index)
42
+ }
43
+
44
+ function byteLength(layer: ExtremaLayer | SumLayer) {
45
+ return 'minimumIndexes' in layer
46
+ ? layer.minimumIndexes.byteLength + layer.maximumIndexes.byteLength
47
+ : layer.x.byteLength + layer.y.byteLength + layer.count.byteLength
48
+ }
49
+
50
+ function largestAlignedBlock(start: number, remaining: number) {
51
+ let size = 1
52
+ while (size * 2 <= remaining && start % (size * 2) === 0) size *= 2
53
+ return size
54
+ }
55
+
56
+ export class MultiResolutionSamplingIndex {
57
+ private readonly extremaLayers: Array<ExtremaLayer | undefined> = []
58
+ private readonly sumLayers: Array<SumLayer | undefined> = []
59
+ private indexBytes = 0
60
+ private readonly maxBytes: number
61
+
62
+ constructor(
63
+ private readonly points: readonly WaveformPoint[],
64
+ options: MultiResolutionSamplingIndexOptions = {},
65
+ ) {
66
+ this.maxBytes = Number.isFinite(options.maxBytes)
67
+ ? Math.max(0, Math.floor(options.maxBytes ?? 0))
68
+ : DEFAULT_MAX_BYTES
69
+ }
70
+
71
+ get byteLength() {
72
+ return this.indexBytes
73
+ }
74
+
75
+ dispose() {
76
+ this.extremaLayers.length = 0
77
+ this.sumLayers.length = 0
78
+ this.indexBytes = 0
79
+ }
80
+
81
+ sample(
82
+ start: number,
83
+ end: number,
84
+ strategy: IndexedSamplingStrategy,
85
+ requestedTarget: number,
86
+ ): WorkerSamplingOutput | undefined {
87
+ const rangeStart = Math.max(0, Math.min(this.points.length, Math.floor(start)))
88
+ const rangeEnd = Math.max(rangeStart, Math.min(this.points.length, Math.floor(end)))
89
+ const count = rangeEnd - rangeStart
90
+ const target = targetCount(requestedTarget, count)
91
+ if (strategy === 'lttb') return undefined
92
+ if (strategy === 'average' || strategy === 'sum') {
93
+ return this.aggregate(rangeStart, rangeEnd, target, strategy)
94
+ }
95
+ if (strategy === 'none' || count <= target) return this.allIndexes(rangeStart, rangeEnd)
96
+ if (target === 1) return this.indexes([rangeStart])
97
+ if (target === 2) return this.indexes([rangeStart, rangeEnd - 1])
98
+ return this.extrema(rangeStart, rangeEnd, target, strategy)
99
+ }
100
+
101
+ private allIndexes(start: number, end: number): WorkerSamplingOutput {
102
+ return {
103
+ kind: 'source-indexes',
104
+ sourceIndexes: Uint32Array.from({ length: end - start }, (_, index) => start + index),
105
+ }
106
+ }
107
+
108
+ private indexes(indexes: number[]): WorkerSamplingOutput {
109
+ return { kind: 'source-indexes', sourceIndexes: Uint32Array.from(indexes) }
110
+ }
111
+
112
+ private extrema(
113
+ start: number,
114
+ end: number,
115
+ target: number,
116
+ strategy: Exclude<IndexedSamplingStrategy, 'none' | 'lttb' | 'average' | 'sum'>,
117
+ ): WorkerSamplingOutput | undefined {
118
+ const bucketCount =
119
+ strategy === 'peak'
120
+ ? Math.floor((target - 2) / 4)
121
+ : strategy === 'minmax'
122
+ ? Math.floor((target - 2) / 2)
123
+ : Math.max(0, target - 2)
124
+ if (bucketCount === 0) return this.indexes([start, end - 1])
125
+ const indexes = [start]
126
+ for (let bucket = 0; bucket < bucketCount; bucket += 1) {
127
+ const bounds = bucketBounds(bucket, bucketCount, start + 1, end - 1)
128
+ const range = this.queryExtrema(bounds.start, bounds.end)
129
+ if (!range) return undefined
130
+ if (strategy === 'min') addUnique(indexes, range.minimum)
131
+ else if (strategy === 'max') addUnique(indexes, range.maximum)
132
+ else if (strategy === 'minmax') {
133
+ if (range.minimum <= range.maximum) {
134
+ addUnique(indexes, range.minimum)
135
+ addUnique(indexes, range.maximum)
136
+ } else {
137
+ addUnique(indexes, range.maximum)
138
+ addUnique(indexes, range.minimum)
139
+ }
140
+ } else {
141
+ for (const index of [bounds.start, range.minimum, range.maximum, bounds.end - 1].sort(
142
+ (left, right) => left - right,
143
+ )) {
144
+ addUnique(indexes, index)
145
+ }
146
+ }
147
+ }
148
+ addUnique(indexes, end - 1)
149
+ return this.indexes(indexes)
150
+ }
151
+
152
+ private aggregate(
153
+ start: number,
154
+ end: number,
155
+ target: number,
156
+ strategy: 'average' | 'sum',
157
+ ): WorkerSamplingOutput | undefined {
158
+ const x = new Float64Array(target)
159
+ const y = new Float64Array(target)
160
+ for (let bucket = 0; bucket < target; bucket += 1) {
161
+ const bounds = bucketBounds(bucket, target, start, end)
162
+ const aggregate = this.querySum(bounds.start, bounds.end)
163
+ if (!aggregate || aggregate.count === 0) return undefined
164
+ x[bucket] = aggregate.x / aggregate.count
165
+ y[bucket] = strategy === 'average' ? aggregate.y / aggregate.count : aggregate.y
166
+ }
167
+ return { kind: 'aggregates', x, y }
168
+ }
169
+
170
+ private queryExtrema(start: number, end: number) {
171
+ const level = Math.floor(Math.log2(Math.max(1, end - start)))
172
+ if (!this.ensureExtrema(level)) return undefined
173
+ let minimum = start
174
+ let maximum = start
175
+ for (let position = start; position < end;) {
176
+ const size = largestAlignedBlock(position, end - position)
177
+ const blockLevel = Math.floor(Math.log2(size))
178
+ const block = position / size
179
+ const layer = this.extremaLayers[blockLevel]
180
+ const nextMinimum = layer ? layer.minimumIndexes[block]! : position
181
+ const nextMaximum = layer ? layer.maximumIndexes[block]! : position
182
+ if (this.isMinimum(nextMinimum, minimum)) minimum = nextMinimum
183
+ if (this.isMaximum(nextMaximum, maximum)) maximum = nextMaximum
184
+ position += size
185
+ }
186
+ return { minimum, maximum }
187
+ }
188
+
189
+ private querySum(start: number, end: number) {
190
+ const level = Math.floor(Math.log2(Math.max(1, end - start)))
191
+ if (!this.ensureSum(level)) return undefined
192
+ let x = 0
193
+ let y = 0
194
+ let count = 0
195
+ for (let position = start; position < end;) {
196
+ const size = largestAlignedBlock(position, end - position)
197
+ const blockLevel = Math.floor(Math.log2(size))
198
+ const block = position / size
199
+ const layer = this.sumLayers[blockLevel]
200
+ if (layer) {
201
+ x += layer.x[block]!
202
+ y += layer.y[block]!
203
+ count += layer.count[block]!
204
+ } else {
205
+ const point = this.points[position]!
206
+ x += point.x
207
+ y += point.y
208
+ count += 1
209
+ }
210
+ position += size
211
+ }
212
+ return { x, y, count }
213
+ }
214
+
215
+ private ensureExtrema(level: number) {
216
+ for (let current = 1; current <= level; current += 1) {
217
+ if (this.extremaLayers[current]) continue
218
+ const length = Math.ceil(this.points.length / 2 ** current)
219
+ const layer: ExtremaLayer = {
220
+ minimumIndexes: new Uint32Array(length),
221
+ maximumIndexes: new Uint32Array(length),
222
+ }
223
+ if (!this.reserve(byteLength(layer))) return false
224
+ const previous = this.extremaLayers[current - 1]
225
+ for (let block = 0; block < length; block += 1) {
226
+ const left = block * 2
227
+ const right = left + 1
228
+ const leftMinimum = previous ? previous.minimumIndexes[left]! : left
229
+ const leftMaximum = previous ? previous.maximumIndexes[left]! : left
230
+ const hasRight = previous
231
+ ? right < previous.minimumIndexes.length
232
+ : right < this.points.length
233
+ const rightMinimum = previous && hasRight ? previous.minimumIndexes[right]! : right
234
+ const rightMaximum = previous && hasRight ? previous.maximumIndexes[right]! : right
235
+ layer.minimumIndexes[block] =
236
+ hasRight && this.isMinimum(rightMinimum, leftMinimum) ? rightMinimum : leftMinimum
237
+ layer.maximumIndexes[block] =
238
+ hasRight && this.isMaximum(rightMaximum, leftMaximum) ? rightMaximum : leftMaximum
239
+ }
240
+ this.extremaLayers[current] = layer
241
+ }
242
+ return true
243
+ }
244
+
245
+ private ensureSum(level: number) {
246
+ for (let current = 1; current <= level; current += 1) {
247
+ if (this.sumLayers[current]) continue
248
+ const length = Math.ceil(this.points.length / 2 ** current)
249
+ const layer: SumLayer = {
250
+ x: new Float64Array(length),
251
+ y: new Float64Array(length),
252
+ count: new Uint32Array(length),
253
+ }
254
+ if (!this.reserve(byteLength(layer))) return false
255
+ const previous = this.sumLayers[current - 1]
256
+ for (let block = 0; block < length; block += 1) {
257
+ const left = block * 2
258
+ const right = left + 1
259
+ if (previous) {
260
+ layer.x[block] = previous.x[left]! + (right < previous.x.length ? previous.x[right]! : 0)
261
+ layer.y[block] = previous.y[left]! + (right < previous.y.length ? previous.y[right]! : 0)
262
+ layer.count[block] =
263
+ previous.count[left]! + (right < previous.count.length ? previous.count[right]! : 0)
264
+ } else {
265
+ const leftPoint = this.points[left]!
266
+ const rightPoint = this.points[right]
267
+ layer.x[block] = leftPoint.x + (rightPoint?.x ?? 0)
268
+ layer.y[block] = leftPoint.y + (rightPoint?.y ?? 0)
269
+ layer.count[block] = rightPoint ? 2 : 1
270
+ }
271
+ }
272
+ this.sumLayers[current] = layer
273
+ }
274
+ return true
275
+ }
276
+
277
+ private reserve(bytes: number) {
278
+ if (this.indexBytes + bytes > this.maxBytes) return false
279
+ this.indexBytes += bytes
280
+ return true
281
+ }
282
+
283
+ private isMinimum(candidate: number, current: number) {
284
+ const candidatePoint = this.points[candidate]!
285
+ const currentPoint = this.points[current]!
286
+ return (
287
+ candidatePoint.y < currentPoint.y ||
288
+ (candidatePoint.y === currentPoint.y && candidate < current)
289
+ )
290
+ }
291
+
292
+ private isMaximum(candidate: number, current: number) {
293
+ const candidatePoint = this.points[candidate]!
294
+ const currentPoint = this.points[current]!
295
+ return (
296
+ candidatePoint.y > currentPoint.y ||
297
+ (candidatePoint.y === currentPoint.y && candidate < current)
298
+ )
299
+ }
300
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { isCurrentWorkerSamplingResponse } from './protocol'
4
+
5
+ describe('worker sampling response validation', () => {
6
+ it('rejects old request ids and outdated per-series revisions before UI state is committed', () => {
7
+ const current = { requestId: 12, revisions: { alpha: 2, beta: 1 } }
8
+ const response = {
9
+ type: 'sample-viewport-response' as const,
10
+ requestId: 12,
11
+ results: [
12
+ { datasetId: 'alpha', revision: 2 },
13
+ { datasetId: 'beta', revision: 0 },
14
+ ],
15
+ }
16
+
17
+ expect(isCurrentWorkerSamplingResponse({ ...response, requestId: 11 } as never, current)).toBe(
18
+ false,
19
+ )
20
+ expect(isCurrentWorkerSamplingResponse(response as never, current)).toBe(false)
21
+ expect(
22
+ isCurrentWorkerSamplingResponse(
23
+ {
24
+ ...response,
25
+ results: [
26
+ { datasetId: 'alpha', revision: 2 },
27
+ { datasetId: 'beta', revision: 1 },
28
+ ],
29
+ } as never,
30
+ current,
31
+ ),
32
+ ).toBe(true)
33
+ })
34
+ })
@@ -0,0 +1,207 @@
1
+ import type {
2
+ WaveformPoint,
3
+ WaveformLineType,
4
+ WaveformPointType,
5
+ WaveformSamplingBackend,
6
+ WaveformSamplingDiagnostics,
7
+ WaveformSamplingMode,
8
+ WaveformSamplingStrategy,
9
+ } from '@/types'
10
+ import type { WorkerSamplingDataset } from '../waveformPointSource'
11
+
12
+ export const DEFAULT_WORKER_SAMPLING_AUTO_THRESHOLD = 1_000
13
+ export const DEFAULT_WORKER_SAMPLING_MAX_POINTS_PER_PIXEL = 4
14
+
15
+ export type WorkerSamplingBackendKind = WaveformSamplingBackend
16
+ export type WorkerSamplingStatus = 'ok' | 'stale-revision' | 'wasm-unavailable' | 'not-found'
17
+
18
+ export interface WorkerSamplingDatasetMetrics {
19
+ inputPointCount: number
20
+ validPointCount: number
21
+ xDomain: [number, number] | null
22
+ yDomain: [number, number] | null
23
+ /** Current lazily-built multiresolution index allocation for this dataset. */
24
+ indexBytes?: number
25
+ }
26
+
27
+ export interface WorkerSamplingSourceIndexes {
28
+ kind: 'source-indexes'
29
+ sourceIndexes: Uint32Array
30
+ }
31
+
32
+ export interface WorkerSamplingAggregates {
33
+ kind: 'aggregates'
34
+ x: Float64Array
35
+ y: Float64Array
36
+ }
37
+
38
+ export type WorkerSamplingOutput = WorkerSamplingSourceIndexes | WorkerSamplingAggregates
39
+
40
+ export interface WorkerSamplingBackendRequest {
41
+ x: Float64Array
42
+ y: Float64Array
43
+ /** Empty for compact datasets; retained for custom legacy backend compatibility. */
44
+ points: readonly WaveformPoint[]
45
+ strategy: Exclude<WaveformSamplingStrategy, 'auto'>
46
+ targetPointCount: number
47
+ }
48
+
49
+ export interface WorkerSamplingBackend {
50
+ readonly kind: Extract<WorkerSamplingBackendKind, 'javascript' | 'wasm'>
51
+ sample(request: WorkerSamplingBackendRequest): WorkerSamplingOutput
52
+ }
53
+
54
+ export type WorkerSamplingDiagnostics = WaveformSamplingDiagnostics
55
+
56
+ export interface RegisterDatasetRequest {
57
+ type: 'register-dataset'
58
+ requestId: number
59
+ datasetId: string
60
+ revision: 0
61
+ /** Legacy object points remain accepted for repository consumers. */
62
+ points?: readonly WaveformPoint[]
63
+ /** Compact numeric columns used by TypedArray chart inputs. */
64
+ dataset?: WorkerSamplingDataset
65
+ }
66
+
67
+ export interface ReplaceDatasetRequest {
68
+ type: 'replace-dataset'
69
+ requestId: number
70
+ datasetId: string
71
+ revision: number
72
+ points?: readonly WaveformPoint[]
73
+ dataset?: WorkerSamplingDataset
74
+ }
75
+
76
+ export interface WorkerSamplingSeriesRequest {
77
+ seriesId: string
78
+ datasetId: string
79
+ revision: number
80
+ xDomain: [number, number]
81
+ /** Optional main-thread hint; the Worker recomputes this from xDomain before sampling. */
82
+ visibleStartIndex?: number
83
+ /** Exclusive visible-range endpoint. */
84
+ visibleEndIndex?: number
85
+ plotWidth: number
86
+ mode: WaveformSamplingMode
87
+ autoThreshold?: number
88
+ /** Interaction-only hysteresis. Omit previousSelectedMode after the interaction for exact bounds. */
89
+ autoHysteresis?: number
90
+ previousSelectedMode?: 'raw' | 'sampled'
91
+ strategy: WaveformSamplingStrategy
92
+ maxPointsPerPixel?: number
93
+ rawPointLimit?: number
94
+ wasmFailureFallback?: 'error' | 'javascript'
95
+ /** Rendering inputs that affect the cache key, even when only line geometry is overridden. */
96
+ lineType?: WaveformLineType
97
+ pointType?: WaveformPointType
98
+ errorBarVisible?: boolean
99
+ pointMinSpacing?: number
100
+ errorBarMinSpacing?: number
101
+ }
102
+
103
+ export interface SampleViewportRequest {
104
+ type: 'sample-viewport'
105
+ requestId: number
106
+ series: readonly WorkerSamplingSeriesRequest[]
107
+ }
108
+
109
+ export interface FindNearestPointRequest {
110
+ type: 'find-nearest-point'
111
+ requestId: number
112
+ datasetId: string
113
+ revision: number
114
+ x: number
115
+ }
116
+
117
+ export interface GetDatasetMetricsRequest {
118
+ type: 'get-dataset-metrics'
119
+ requestId: number
120
+ datasetId: string
121
+ revision: number
122
+ }
123
+
124
+ export interface DisposeDatasetRequest {
125
+ type: 'dispose-dataset'
126
+ requestId: number
127
+ datasetId: string
128
+ revision: number
129
+ }
130
+
131
+ export interface DisposeAllRequest {
132
+ type: 'dispose-all'
133
+ requestId: number
134
+ }
135
+
136
+ export type WorkerSamplingRequest =
137
+ | RegisterDatasetRequest
138
+ | ReplaceDatasetRequest
139
+ | SampleViewportRequest
140
+ | FindNearestPointRequest
141
+ | GetDatasetMetricsRequest
142
+ | DisposeDatasetRequest
143
+ | DisposeAllRequest
144
+
145
+ export interface DatasetResponse {
146
+ type: 'dataset-response'
147
+ requestId: number
148
+ datasetId: string
149
+ revision: number
150
+ status: WorkerSamplingStatus
151
+ metrics?: WorkerSamplingDatasetMetrics
152
+ }
153
+
154
+ export interface WorkerSamplingSeriesResponse {
155
+ seriesId: string
156
+ datasetId: string
157
+ revision: number
158
+ status: WorkerSamplingStatus
159
+ output?: WorkerSamplingOutput
160
+ diagnostics: WorkerSamplingDiagnostics
161
+ }
162
+
163
+ export interface SampleViewportResponse {
164
+ type: 'sample-viewport-response'
165
+ requestId: number
166
+ results: readonly WorkerSamplingSeriesResponse[]
167
+ /** Set by the worker when WASM initialization failed and a fallback was selected. */
168
+ workerError?: string
169
+ }
170
+
171
+ export interface FindNearestPointResponse {
172
+ type: 'find-nearest-point-response'
173
+ requestId: number
174
+ datasetId: string
175
+ revision: number
176
+ status: WorkerSamplingStatus
177
+ point?: WaveformPoint
178
+ }
179
+
180
+ export interface DisposeAllResponse {
181
+ type: 'dispose-all-response'
182
+ requestId: number
183
+ disposedDatasetIds: readonly string[]
184
+ }
185
+
186
+ export type WorkerSamplingResponse =
187
+ DatasetResponse | SampleViewportResponse | FindNearestPointResponse | DisposeAllResponse
188
+
189
+ export interface WorkerSamplingResponseExpectation {
190
+ requestId: number
191
+ revisions: Readonly<Record<string, number>>
192
+ }
193
+
194
+ /** Rejects responses from an earlier request or an earlier data revision before UI state is updated. */
195
+ export function isCurrentWorkerSamplingResponse(
196
+ response: WorkerSamplingResponse,
197
+ expectation: WorkerSamplingResponseExpectation,
198
+ ): boolean {
199
+ if (response.requestId !== expectation.requestId) return false
200
+ if (response.type === 'sample-viewport-response') {
201
+ return response.results.every(
202
+ (result) => expectation.revisions[result.datasetId] === result.revision,
203
+ )
204
+ }
205
+ if (response.type === 'dispose-all-response') return true
206
+ return expectation.revisions[response.datasetId] === response.revision
207
+ }