supersonic-scsynth 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/supersonic.js +20 -3448
- package/dist/wasm/manifest.json +3 -3
- package/dist/wasm/scsynth-nrt.wasm +0 -0
- package/dist/workers/debug_worker.js +2 -281
- package/dist/workers/osc_in_worker.js +1 -279
- package/dist/workers/osc_out_prescheduler_worker.js +1 -705
- package/dist/workers/scsynth_audio_worklet.js +2 -543
- package/package.json +1 -1
|
@@ -1,705 +1 @@
|
|
|
1
|
-
|
|
2
|
-
SuperSonic - OSC Pre-Scheduler Worker
|
|
3
|
-
Ports the Bleep pre-scheduler design:
|
|
4
|
-
- Single priority queue of future bundles/events
|
|
5
|
-
- One timer driving dispatch (no per-event setTimeout storm)
|
|
6
|
-
- Tag-based cancellation to drop pending runs before they hit WASM
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Shared memory for ring buffer writing
|
|
10
|
-
var sharedBuffer = null;
|
|
11
|
-
var ringBufferBase = null;
|
|
12
|
-
var bufferConstants = null;
|
|
13
|
-
var atomicView = null;
|
|
14
|
-
var dataView = null;
|
|
15
|
-
var uint8View = null;
|
|
16
|
-
|
|
17
|
-
// Ring buffer control indices
|
|
18
|
-
var CONTROL_INDICES = {};
|
|
19
|
-
|
|
20
|
-
// Metrics view (for writing stats to SAB)
|
|
21
|
-
var metricsView = null;
|
|
22
|
-
var METRICS_INDICES = {};
|
|
23
|
-
|
|
24
|
-
// Priority queue implemented as binary min-heap
|
|
25
|
-
// Entries: { ntpTime, seq, editorId, runTag, oscData }
|
|
26
|
-
var eventHeap = [];
|
|
27
|
-
var periodicTimer = null; // Single periodic timer (25ms interval)
|
|
28
|
-
var sequenceCounter = 0;
|
|
29
|
-
var isDispatching = false; // Prevent reentrancy into dispatch loop
|
|
30
|
-
|
|
31
|
-
// Message sequence counter for ring buffer writes (NOT a metric - used for message headers)
|
|
32
|
-
var outgoingMessageSeq = 0;
|
|
33
|
-
|
|
34
|
-
// Retry queue for failed writes
|
|
35
|
-
var retryQueue = [];
|
|
36
|
-
var MAX_RETRY_QUEUE_SIZE = 100;
|
|
37
|
-
var MAX_RETRIES_PER_MESSAGE = 5;
|
|
38
|
-
|
|
39
|
-
// Timing constants
|
|
40
|
-
var NTP_EPOCH_OFFSET = 2208988800; // Seconds from 1900-01-01 to 1970-01-01
|
|
41
|
-
var POLL_INTERVAL_MS = 25; // Check every 25ms
|
|
42
|
-
var LOOKAHEAD_S = 0.100; // 100ms lookahead window
|
|
43
|
-
|
|
44
|
-
function schedulerLog() {
|
|
45
|
-
// Toggle to true for verbose diagnostics
|
|
46
|
-
var DEBUG = true; // Enable for debugging
|
|
47
|
-
if (DEBUG) {
|
|
48
|
-
console.log.apply(console, arguments);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ============================================================================
|
|
53
|
-
// NTP TIME HELPERS
|
|
54
|
-
// ============================================================================
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get current NTP time from system clock
|
|
58
|
-
*
|
|
59
|
-
* Bundles contain full NTP timestamps. We just need to compare them against
|
|
60
|
-
* current NTP time (system clock) to know when to dispatch.
|
|
61
|
-
*
|
|
62
|
-
* AudioContext timing, drift correction, etc. are handled by the C++ side.
|
|
63
|
-
* The prescheduler only needs to know "what time is it now in NTP?"
|
|
64
|
-
*/
|
|
65
|
-
function getCurrentNTP() {
|
|
66
|
-
// Convert current system time to NTP
|
|
67
|
-
var perfTimeMs = performance.timeOrigin + performance.now();
|
|
68
|
-
return (perfTimeMs / 1000) + NTP_EPOCH_OFFSET;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Extract NTP timestamp from OSC bundle
|
|
73
|
-
* Returns NTP time in seconds (double), or null if not a bundle
|
|
74
|
-
*/
|
|
75
|
-
function extractNTPFromBundle(oscData) {
|
|
76
|
-
if (oscData.length >= 16 && oscData[0] === 0x23) { // '#bundle'
|
|
77
|
-
var view = new DataView(oscData.buffer, oscData.byteOffset);
|
|
78
|
-
var ntpSeconds = view.getUint32(8, false);
|
|
79
|
-
var ntpFraction = view.getUint32(12, false);
|
|
80
|
-
return ntpSeconds + ntpFraction / 0x100000000;
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Legacy wrapper for backwards compatibility
|
|
87
|
-
*/
|
|
88
|
-
function getBundleTimestamp(oscMessage) {
|
|
89
|
-
return extractNTPFromBundle(oscMessage);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ============================================================================
|
|
93
|
-
// SHARED ARRAY BUFFER ACCESS
|
|
94
|
-
// ============================================================================
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Initialize ring buffer access for writing directly to SharedArrayBuffer
|
|
98
|
-
*/
|
|
99
|
-
function initSharedBuffer() {
|
|
100
|
-
if (!sharedBuffer || !bufferConstants) {
|
|
101
|
-
console.error('[PreScheduler] Cannot init - missing buffer or constants');
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
atomicView = new Int32Array(sharedBuffer);
|
|
106
|
-
dataView = new DataView(sharedBuffer);
|
|
107
|
-
uint8View = new Uint8Array(sharedBuffer);
|
|
108
|
-
|
|
109
|
-
// Calculate control indices for ring buffer
|
|
110
|
-
CONTROL_INDICES = {
|
|
111
|
-
IN_HEAD: (ringBufferBase + bufferConstants.CONTROL_START + 0) / 4,
|
|
112
|
-
IN_TAIL: (ringBufferBase + bufferConstants.CONTROL_START + 4) / 4
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Initialize metrics view (OSC Out metrics are at offsets 7-12 in the metrics array)
|
|
116
|
-
var metricsBase = ringBufferBase + bufferConstants.METRICS_START;
|
|
117
|
-
metricsView = new Uint32Array(sharedBuffer, metricsBase, bufferConstants.METRICS_SIZE / 4);
|
|
118
|
-
|
|
119
|
-
METRICS_INDICES = {
|
|
120
|
-
EVENTS_PENDING: 7,
|
|
121
|
-
MAX_EVENTS_PENDING: 8,
|
|
122
|
-
BUNDLES_WRITTEN: 9,
|
|
123
|
-
BUNDLES_DROPPED: 10,
|
|
124
|
-
RETRIES_SUCCEEDED: 11,
|
|
125
|
-
RETRIES_FAILED: 12,
|
|
126
|
-
BUNDLES_SCHEDULED: 13,
|
|
127
|
-
EVENTS_CANCELLED: 14,
|
|
128
|
-
TOTAL_DISPATCHES: 15,
|
|
129
|
-
MESSAGES_RETRIED: 16,
|
|
130
|
-
RETRY_QUEUE_SIZE: 17,
|
|
131
|
-
RETRY_QUEUE_MAX: 18
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
console.log('[PreScheduler] SharedArrayBuffer initialized with direct ring buffer writing and metrics');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Write metrics to SharedArrayBuffer
|
|
139
|
-
* Increments use Atomics.add() for thread safety, stores use Atomics.store()
|
|
140
|
-
*/
|
|
141
|
-
function updateMetrics() {
|
|
142
|
-
if (!metricsView) return;
|
|
143
|
-
|
|
144
|
-
// Update current values (use Atomics.store for absolute values)
|
|
145
|
-
Atomics.store(metricsView, METRICS_INDICES.EVENTS_PENDING, eventHeap.length);
|
|
146
|
-
|
|
147
|
-
// Update max if current exceeds it
|
|
148
|
-
var currentPending = eventHeap.length;
|
|
149
|
-
var currentMax = Atomics.load(metricsView, METRICS_INDICES.MAX_EVENTS_PENDING);
|
|
150
|
-
if (currentPending > currentMax) {
|
|
151
|
-
Atomics.store(metricsView, METRICS_INDICES.MAX_EVENTS_PENDING, currentPending);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Write OSC message directly to ring buffer (replaces MessagePort to writer worker)
|
|
157
|
-
* This is now the ONLY place that writes to the ring buffer
|
|
158
|
-
* Returns true if successful, false if failed (caller should queue for retry)
|
|
159
|
-
*/
|
|
160
|
-
function writeToRingBuffer(oscMessage, isRetry) {
|
|
161
|
-
if (!sharedBuffer || !atomicView) {
|
|
162
|
-
console.error('[PreScheduler] Not initialized for ring buffer writing');
|
|
163
|
-
if (metricsView) Atomics.add(metricsView, METRICS_INDICES.BUNDLES_DROPPED, 1);
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
var payloadSize = oscMessage.length;
|
|
168
|
-
var totalSize = bufferConstants.MESSAGE_HEADER_SIZE + payloadSize;
|
|
169
|
-
|
|
170
|
-
// Check if message fits in buffer at all
|
|
171
|
-
if (totalSize > bufferConstants.IN_BUFFER_SIZE - bufferConstants.MESSAGE_HEADER_SIZE) {
|
|
172
|
-
console.error('[PreScheduler] Message too large:', totalSize);
|
|
173
|
-
if (metricsView) Atomics.add(metricsView, METRICS_INDICES.BUNDLES_DROPPED, 1);
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Try to write (non-blocking, single attempt)
|
|
178
|
-
var head = Atomics.load(atomicView, CONTROL_INDICES.IN_HEAD);
|
|
179
|
-
var tail = Atomics.load(atomicView, CONTROL_INDICES.IN_TAIL);
|
|
180
|
-
|
|
181
|
-
// Calculate available space
|
|
182
|
-
var available = (bufferConstants.IN_BUFFER_SIZE - 1 - head + tail) % bufferConstants.IN_BUFFER_SIZE;
|
|
183
|
-
|
|
184
|
-
if (available < totalSize) {
|
|
185
|
-
// Buffer full - return false so caller can queue for retry
|
|
186
|
-
if (!isRetry) {
|
|
187
|
-
// Only increment bundlesDropped on initial attempt
|
|
188
|
-
// Retries increment different counters
|
|
189
|
-
if (metricsView) Atomics.add(metricsView, METRICS_INDICES.BUNDLES_DROPPED, 1);
|
|
190
|
-
console.warn('[PreScheduler] Ring buffer full, message will be queued for retry');
|
|
191
|
-
}
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ringbuf.js approach: split writes across wrap boundary
|
|
196
|
-
// No padding markers - just split the write into two parts if it wraps
|
|
197
|
-
|
|
198
|
-
var spaceToEnd = bufferConstants.IN_BUFFER_SIZE - head;
|
|
199
|
-
|
|
200
|
-
if (totalSize > spaceToEnd) {
|
|
201
|
-
// Message will wrap - write in two parts
|
|
202
|
-
// Create header as byte array to simplify split writes
|
|
203
|
-
var headerBytes = new Uint8Array(bufferConstants.MESSAGE_HEADER_SIZE);
|
|
204
|
-
var headerView = new DataView(headerBytes.buffer);
|
|
205
|
-
headerView.setUint32(0, bufferConstants.MESSAGE_MAGIC, true);
|
|
206
|
-
headerView.setUint32(4, totalSize, true);
|
|
207
|
-
headerView.setUint32(8, outgoingMessageSeq, true);
|
|
208
|
-
headerView.setUint32(12, 0, true);
|
|
209
|
-
|
|
210
|
-
var writePos1 = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
|
|
211
|
-
var writePos2 = ringBufferBase + bufferConstants.IN_BUFFER_START;
|
|
212
|
-
|
|
213
|
-
// Write header (may be split)
|
|
214
|
-
if (spaceToEnd >= bufferConstants.MESSAGE_HEADER_SIZE) {
|
|
215
|
-
// Header fits contiguously
|
|
216
|
-
uint8View.set(headerBytes, writePos1);
|
|
217
|
-
|
|
218
|
-
// Write payload (split across boundary)
|
|
219
|
-
var payloadBytesInFirstPart = spaceToEnd - bufferConstants.MESSAGE_HEADER_SIZE;
|
|
220
|
-
uint8View.set(oscMessage.subarray(0, payloadBytesInFirstPart), writePos1 + bufferConstants.MESSAGE_HEADER_SIZE);
|
|
221
|
-
uint8View.set(oscMessage.subarray(payloadBytesInFirstPart), writePos2);
|
|
222
|
-
} else {
|
|
223
|
-
// Header is split across boundary
|
|
224
|
-
uint8View.set(headerBytes.subarray(0, spaceToEnd), writePos1);
|
|
225
|
-
uint8View.set(headerBytes.subarray(spaceToEnd), writePos2);
|
|
226
|
-
|
|
227
|
-
// All payload goes at beginning
|
|
228
|
-
var payloadOffset = bufferConstants.MESSAGE_HEADER_SIZE - spaceToEnd;
|
|
229
|
-
uint8View.set(oscMessage, writePos2 + payloadOffset);
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
// Message fits contiguously - write normally
|
|
233
|
-
var writePos = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
|
|
234
|
-
|
|
235
|
-
// Write header
|
|
236
|
-
dataView.setUint32(writePos, bufferConstants.MESSAGE_MAGIC, true);
|
|
237
|
-
dataView.setUint32(writePos + 4, totalSize, true);
|
|
238
|
-
dataView.setUint32(writePos + 8, outgoingMessageSeq, true);
|
|
239
|
-
dataView.setUint32(writePos + 12, 0, true);
|
|
240
|
-
|
|
241
|
-
// Write payload
|
|
242
|
-
uint8View.set(oscMessage, writePos + bufferConstants.MESSAGE_HEADER_SIZE);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Diagnostic: Log first few writes
|
|
246
|
-
if (outgoingMessageSeq < 5) {
|
|
247
|
-
schedulerLog('[PreScheduler] Write:', 'seq=' + outgoingMessageSeq,
|
|
248
|
-
'pos=' + head, 'size=' + totalSize, 'newHead=' + ((head + totalSize) % bufferConstants.IN_BUFFER_SIZE));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// CRITICAL: Ensure memory barrier before publishing head pointer
|
|
252
|
-
// All previous writes (header + payload) must be visible to C++ reader
|
|
253
|
-
// Atomics.load provides necessary memory fence/barrier
|
|
254
|
-
Atomics.load(atomicView, CONTROL_INDICES.IN_HEAD);
|
|
255
|
-
|
|
256
|
-
// Update head pointer (publish message)
|
|
257
|
-
var newHead = (head + totalSize) % bufferConstants.IN_BUFFER_SIZE;
|
|
258
|
-
Atomics.store(atomicView, CONTROL_INDICES.IN_HEAD, newHead);
|
|
259
|
-
|
|
260
|
-
// Increment sequence counter for next message
|
|
261
|
-
outgoingMessageSeq = (outgoingMessageSeq + 1) & 0xFFFFFFFF;
|
|
262
|
-
|
|
263
|
-
// Update SAB metrics
|
|
264
|
-
if (metricsView) Atomics.add(metricsView, METRICS_INDICES.BUNDLES_WRITTEN, 1);
|
|
265
|
-
return true;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Add a message to the retry queue
|
|
270
|
-
*/
|
|
271
|
-
function queueForRetry(oscData, context) {
|
|
272
|
-
if (retryQueue.length >= MAX_RETRY_QUEUE_SIZE) {
|
|
273
|
-
console.error('[PreScheduler] Retry queue full, dropping message permanently');
|
|
274
|
-
if (metricsView) Atomics.add(metricsView, METRICS_INDICES.RETRIES_FAILED, 1);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
retryQueue.push({
|
|
279
|
-
oscData: oscData,
|
|
280
|
-
retryCount: 0,
|
|
281
|
-
context: context || 'unknown',
|
|
282
|
-
queuedAt: performance.now()
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// Update SAB metrics
|
|
286
|
-
if (metricsView) {
|
|
287
|
-
Atomics.store(metricsView, METRICS_INDICES.RETRY_QUEUE_SIZE, retryQueue.length);
|
|
288
|
-
var currentMax = Atomics.load(metricsView, METRICS_INDICES.RETRY_QUEUE_MAX);
|
|
289
|
-
if (retryQueue.length > currentMax) {
|
|
290
|
-
Atomics.store(metricsView, METRICS_INDICES.RETRY_QUEUE_MAX, retryQueue.length);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
schedulerLog('[PreScheduler] Queued message for retry:', context, 'queue size:', retryQueue.length);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Attempt to retry queued messages
|
|
299
|
-
* Called periodically from checkAndDispatch
|
|
300
|
-
*/
|
|
301
|
-
function processRetryQueue() {
|
|
302
|
-
if (retryQueue.length === 0) {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
var i = 0;
|
|
307
|
-
while (i < retryQueue.length) {
|
|
308
|
-
var item = retryQueue[i];
|
|
309
|
-
|
|
310
|
-
// Try to write
|
|
311
|
-
var success = writeToRingBuffer(item.oscData, true);
|
|
312
|
-
|
|
313
|
-
if (success) {
|
|
314
|
-
// Success - remove from queue
|
|
315
|
-
retryQueue.splice(i, 1);
|
|
316
|
-
if (metricsView) {
|
|
317
|
-
Atomics.add(metricsView, METRICS_INDICES.RETRIES_SUCCEEDED, 1);
|
|
318
|
-
Atomics.add(metricsView, METRICS_INDICES.MESSAGES_RETRIED, 1);
|
|
319
|
-
Atomics.store(metricsView, METRICS_INDICES.RETRY_QUEUE_SIZE, retryQueue.length);
|
|
320
|
-
}
|
|
321
|
-
schedulerLog('[PreScheduler] Retry succeeded for:', item.context,
|
|
322
|
-
'after', item.retryCount + 1, 'attempts');
|
|
323
|
-
// Don't increment i - we removed an item
|
|
324
|
-
} else {
|
|
325
|
-
// Failed - increment retry count
|
|
326
|
-
item.retryCount++;
|
|
327
|
-
if (metricsView) Atomics.add(metricsView, METRICS_INDICES.MESSAGES_RETRIED, 1);
|
|
328
|
-
|
|
329
|
-
if (item.retryCount >= MAX_RETRIES_PER_MESSAGE) {
|
|
330
|
-
// Give up on this message
|
|
331
|
-
console.error('[PreScheduler] Giving up on message after',
|
|
332
|
-
MAX_RETRIES_PER_MESSAGE, 'retries:', item.context);
|
|
333
|
-
retryQueue.splice(i, 1);
|
|
334
|
-
if (metricsView) {
|
|
335
|
-
Atomics.add(metricsView, METRICS_INDICES.RETRIES_FAILED, 1);
|
|
336
|
-
Atomics.store(metricsView, METRICS_INDICES.RETRY_QUEUE_SIZE, retryQueue.length);
|
|
337
|
-
}
|
|
338
|
-
// Don't increment i - we removed an item
|
|
339
|
-
} else {
|
|
340
|
-
// Keep in queue, try again next cycle
|
|
341
|
-
i++;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Schedule an OSC bundle by its NTP timestamp
|
|
349
|
-
* Non-bundles or bundles without timestamps are dispatched immediately
|
|
350
|
-
*/
|
|
351
|
-
function scheduleEvent(oscData, editorId, runTag) {
|
|
352
|
-
var ntpTime = extractNTPFromBundle(oscData);
|
|
353
|
-
|
|
354
|
-
if (ntpTime === null) {
|
|
355
|
-
// Not a bundle - dispatch immediately to ring buffer
|
|
356
|
-
schedulerLog('[PreScheduler] Non-bundle message, dispatching immediately');
|
|
357
|
-
var success = writeToRingBuffer(oscData, false);
|
|
358
|
-
if (!success) {
|
|
359
|
-
// Queue for retry
|
|
360
|
-
queueForRetry(oscData, 'immediate message');
|
|
361
|
-
}
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
var currentNTP = getCurrentNTP();
|
|
366
|
-
var timeUntilExec = ntpTime - currentNTP;
|
|
367
|
-
|
|
368
|
-
// Create event with NTP timestamp
|
|
369
|
-
var event = {
|
|
370
|
-
ntpTime: ntpTime,
|
|
371
|
-
seq: sequenceCounter++,
|
|
372
|
-
editorId: editorId || 0,
|
|
373
|
-
runTag: runTag || '',
|
|
374
|
-
oscData: oscData
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
heapPush(event);
|
|
378
|
-
|
|
379
|
-
if (metricsView) Atomics.add(metricsView, METRICS_INDICES.BUNDLES_SCHEDULED, 1);
|
|
380
|
-
updateMetrics(); // Update SAB with current queue depth and peak
|
|
381
|
-
|
|
382
|
-
schedulerLog('[PreScheduler] Scheduled bundle:',
|
|
383
|
-
'NTP=' + ntpTime.toFixed(3),
|
|
384
|
-
'current=' + currentNTP.toFixed(3),
|
|
385
|
-
'wait=' + (timeUntilExec * 1000).toFixed(1) + 'ms',
|
|
386
|
-
'pending=' + eventHeap.length);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function heapPush(event) {
|
|
390
|
-
eventHeap.push(event);
|
|
391
|
-
siftUp(eventHeap.length - 1);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function heapPeek() {
|
|
395
|
-
return eventHeap.length > 0 ? eventHeap[0] : null;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function heapPop() {
|
|
399
|
-
if (eventHeap.length === 0) {
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
var top = eventHeap[0];
|
|
403
|
-
var last = eventHeap.pop();
|
|
404
|
-
if (eventHeap.length > 0) {
|
|
405
|
-
eventHeap[0] = last;
|
|
406
|
-
siftDown(0);
|
|
407
|
-
}
|
|
408
|
-
return top;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function siftUp(index) {
|
|
412
|
-
while (index > 0) {
|
|
413
|
-
var parent = Math.floor((index - 1) / 2);
|
|
414
|
-
if (compareEvents(eventHeap[index], eventHeap[parent]) >= 0) {
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
swap(index, parent);
|
|
418
|
-
index = parent;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function siftDown(index) {
|
|
423
|
-
var length = eventHeap.length;
|
|
424
|
-
while (true) {
|
|
425
|
-
var left = 2 * index + 1;
|
|
426
|
-
var right = 2 * index + 2;
|
|
427
|
-
var smallest = index;
|
|
428
|
-
|
|
429
|
-
if (left < length && compareEvents(eventHeap[left], eventHeap[smallest]) < 0) {
|
|
430
|
-
smallest = left;
|
|
431
|
-
}
|
|
432
|
-
if (right < length && compareEvents(eventHeap[right], eventHeap[smallest]) < 0) {
|
|
433
|
-
smallest = right;
|
|
434
|
-
}
|
|
435
|
-
if (smallest === index) {
|
|
436
|
-
break;
|
|
437
|
-
}
|
|
438
|
-
swap(index, smallest);
|
|
439
|
-
index = smallest;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function compareEvents(a, b) {
|
|
444
|
-
if (a.ntpTime === b.ntpTime) {
|
|
445
|
-
return a.seq - b.seq;
|
|
446
|
-
}
|
|
447
|
-
return a.ntpTime - b.ntpTime;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function swap(i, j) {
|
|
451
|
-
var tmp = eventHeap[i];
|
|
452
|
-
eventHeap[i] = eventHeap[j];
|
|
453
|
-
eventHeap[j] = tmp;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Start periodic polling (called once on init)
|
|
458
|
-
*/
|
|
459
|
-
function startPeriodicPolling() {
|
|
460
|
-
if (periodicTimer !== null) {
|
|
461
|
-
console.warn('[PreScheduler] Polling already started');
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
console.log('[PreScheduler] Starting periodic polling (every ' + POLL_INTERVAL_MS + 'ms)');
|
|
466
|
-
checkAndDispatch(); // Start immediately
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Stop periodic polling
|
|
471
|
-
*/
|
|
472
|
-
function stopPeriodicPolling() {
|
|
473
|
-
if (periodicTimer !== null) {
|
|
474
|
-
clearTimeout(periodicTimer);
|
|
475
|
-
periodicTimer = null;
|
|
476
|
-
console.log('[PreScheduler] Stopped periodic polling');
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Periodic check and dispatch function
|
|
482
|
-
* Uses NTP timestamps and global offset for drift-free timing
|
|
483
|
-
*/
|
|
484
|
-
function checkAndDispatch() {
|
|
485
|
-
isDispatching = true;
|
|
486
|
-
|
|
487
|
-
// First, try to process any queued retries
|
|
488
|
-
processRetryQueue();
|
|
489
|
-
|
|
490
|
-
var currentNTP = getCurrentNTP();
|
|
491
|
-
var lookaheadTime = currentNTP + LOOKAHEAD_S;
|
|
492
|
-
var dispatchCount = 0;
|
|
493
|
-
var dispatchStart = performance.now();
|
|
494
|
-
|
|
495
|
-
// Dispatch all bundles that are ready
|
|
496
|
-
while (eventHeap.length > 0) {
|
|
497
|
-
var nextEvent = heapPeek();
|
|
498
|
-
|
|
499
|
-
if (nextEvent.ntpTime <= lookaheadTime) {
|
|
500
|
-
// Ready to dispatch
|
|
501
|
-
heapPop();
|
|
502
|
-
updateMetrics(); // Update SAB with current queue depth
|
|
503
|
-
|
|
504
|
-
var timeUntilExec = nextEvent.ntpTime - currentNTP;
|
|
505
|
-
if (metricsView) Atomics.add(metricsView, METRICS_INDICES.TOTAL_DISPATCHES, 1);
|
|
506
|
-
|
|
507
|
-
schedulerLog('[PreScheduler] Dispatching bundle:',
|
|
508
|
-
'NTP=' + nextEvent.ntpTime.toFixed(3),
|
|
509
|
-
'current=' + currentNTP.toFixed(3),
|
|
510
|
-
'early=' + (timeUntilExec * 1000).toFixed(1) + 'ms',
|
|
511
|
-
'remaining=' + eventHeap.length);
|
|
512
|
-
|
|
513
|
-
var success = writeToRingBuffer(nextEvent.oscData, false);
|
|
514
|
-
if (!success) {
|
|
515
|
-
// Queue for retry
|
|
516
|
-
queueForRetry(nextEvent.oscData, 'scheduled bundle NTP=' + nextEvent.ntpTime.toFixed(3));
|
|
517
|
-
}
|
|
518
|
-
dispatchCount++;
|
|
519
|
-
} else {
|
|
520
|
-
// Rest aren't ready yet (heap is sorted)
|
|
521
|
-
break;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
if (dispatchCount > 0 || eventHeap.length > 0 || retryQueue.length > 0) {
|
|
526
|
-
schedulerLog('[PreScheduler] Dispatch cycle complete:',
|
|
527
|
-
'dispatched=' + dispatchCount,
|
|
528
|
-
'pending=' + eventHeap.length,
|
|
529
|
-
'retrying=' + retryQueue.length);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
isDispatching = false;
|
|
533
|
-
|
|
534
|
-
// Reschedule for next check (fixed interval)
|
|
535
|
-
periodicTimer = setTimeout(checkAndDispatch, POLL_INTERVAL_MS);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function cancelBy(predicate) {
|
|
539
|
-
if (eventHeap.length === 0) {
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
var before = eventHeap.length;
|
|
544
|
-
var remaining = [];
|
|
545
|
-
|
|
546
|
-
for (var i = 0; i < eventHeap.length; i++) {
|
|
547
|
-
var event = eventHeap[i];
|
|
548
|
-
if (!predicate(event)) {
|
|
549
|
-
remaining.push(event);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
var removed = before - remaining.length;
|
|
554
|
-
if (removed > 0) {
|
|
555
|
-
eventHeap = remaining;
|
|
556
|
-
heapify();
|
|
557
|
-
if (metricsView) Atomics.add(metricsView, METRICS_INDICES.EVENTS_CANCELLED, removed);
|
|
558
|
-
updateMetrics(); // Update SAB with current queue depth
|
|
559
|
-
console.log('[PreScheduler] Cancelled ' + removed + ' events, ' + eventHeap.length + ' remaining');
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function heapify() {
|
|
564
|
-
for (var i = Math.floor(eventHeap.length / 2) - 1; i >= 0; i--) {
|
|
565
|
-
siftDown(i);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function cancelEditorTag(editorId, runTag) {
|
|
570
|
-
cancelBy(function(event) {
|
|
571
|
-
return event.editorId === editorId && event.runTag === runTag;
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function cancelEditor(editorId) {
|
|
576
|
-
cancelBy(function(event) {
|
|
577
|
-
return event.editorId === editorId;
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function cancelAllTags() {
|
|
582
|
-
if (eventHeap.length === 0) {
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
var cancelled = eventHeap.length;
|
|
586
|
-
if (metricsView) Atomics.add(metricsView, METRICS_INDICES.EVENTS_CANCELLED, cancelled);
|
|
587
|
-
eventHeap = [];
|
|
588
|
-
updateMetrics(); // Update SAB (sets eventsPending to 0)
|
|
589
|
-
console.log('[PreScheduler] Cancelled all ' + cancelled + ' events');
|
|
590
|
-
// Note: Periodic timer continues running (it will just find empty queue)
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Helpers reused from legacy worker for immediate send
|
|
594
|
-
function isBundle(data) {
|
|
595
|
-
if (!data || data.length < 8) {
|
|
596
|
-
return false;
|
|
597
|
-
}
|
|
598
|
-
return data[0] === 0x23 && data[1] === 0x62 && data[2] === 0x75 && data[3] === 0x6e &&
|
|
599
|
-
data[4] === 0x64 && data[5] === 0x6c && data[6] === 0x65 && data[7] === 0x00;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function extractMessagesFromBundle(data) {
|
|
603
|
-
var messages = [];
|
|
604
|
-
var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
605
|
-
var offset = 16; // skip "#bundle\0" + timetag
|
|
606
|
-
|
|
607
|
-
while (offset < data.length) {
|
|
608
|
-
var messageSize = view.getInt32(offset, false);
|
|
609
|
-
offset += 4;
|
|
610
|
-
|
|
611
|
-
if (messageSize <= 0 || offset + messageSize > data.length) {
|
|
612
|
-
break;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
var messageData = data.slice(offset, offset + messageSize);
|
|
616
|
-
messages.push(messageData);
|
|
617
|
-
offset += messageSize;
|
|
618
|
-
|
|
619
|
-
while (offset % 4 !== 0 && offset < data.length) {
|
|
620
|
-
offset++;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
return messages;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function processImmediate(oscData) {
|
|
628
|
-
if (isBundle(oscData)) {
|
|
629
|
-
var messages = extractMessagesFromBundle(oscData);
|
|
630
|
-
for (var i = 0; i < messages.length; i++) {
|
|
631
|
-
var success = writeToRingBuffer(messages[i], false);
|
|
632
|
-
if (!success) {
|
|
633
|
-
queueForRetry(messages[i], 'immediate bundle message ' + i);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
} else {
|
|
637
|
-
var success = writeToRingBuffer(oscData, false);
|
|
638
|
-
if (!success) {
|
|
639
|
-
queueForRetry(oscData, 'immediate message');
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Message handling
|
|
645
|
-
self.addEventListener('message', function(event) {
|
|
646
|
-
var data = event.data;
|
|
647
|
-
|
|
648
|
-
try {
|
|
649
|
-
switch (data.type) {
|
|
650
|
-
case 'init':
|
|
651
|
-
sharedBuffer = data.sharedBuffer;
|
|
652
|
-
ringBufferBase = data.ringBufferBase;
|
|
653
|
-
bufferConstants = data.bufferConstants;
|
|
654
|
-
|
|
655
|
-
// Initialize SharedArrayBuffer views (including offset)
|
|
656
|
-
initSharedBuffer();
|
|
657
|
-
|
|
658
|
-
// Start periodic polling
|
|
659
|
-
startPeriodicPolling();
|
|
660
|
-
|
|
661
|
-
schedulerLog('[OSCPreSchedulerWorker] Initialized with NTP-based scheduling and direct ring buffer writing');
|
|
662
|
-
self.postMessage({ type: 'initialized' });
|
|
663
|
-
break;
|
|
664
|
-
|
|
665
|
-
case 'send':
|
|
666
|
-
// NTP-based scheduling: extract NTP from bundle
|
|
667
|
-
// scheduleEvent() will dispatch immediately if not a bundle
|
|
668
|
-
scheduleEvent(
|
|
669
|
-
data.oscData,
|
|
670
|
-
data.editorId || 0,
|
|
671
|
-
data.runTag || ''
|
|
672
|
-
);
|
|
673
|
-
break;
|
|
674
|
-
|
|
675
|
-
case 'sendImmediate':
|
|
676
|
-
processImmediate(data.oscData);
|
|
677
|
-
break;
|
|
678
|
-
|
|
679
|
-
case 'cancelEditorTag':
|
|
680
|
-
if (data.runTag !== undefined && data.runTag !== null && data.runTag !== '') {
|
|
681
|
-
cancelEditorTag(data.editorId || 0, data.runTag);
|
|
682
|
-
}
|
|
683
|
-
break;
|
|
684
|
-
|
|
685
|
-
case 'cancelEditor':
|
|
686
|
-
cancelEditor(data.editorId || 0);
|
|
687
|
-
break;
|
|
688
|
-
|
|
689
|
-
case 'cancelAll':
|
|
690
|
-
cancelAllTags();
|
|
691
|
-
break;
|
|
692
|
-
|
|
693
|
-
default:
|
|
694
|
-
console.warn('[OSCPreSchedulerWorker] Unknown message type:', data.type);
|
|
695
|
-
}
|
|
696
|
-
} catch (error) {
|
|
697
|
-
console.error('[OSCPreSchedulerWorker] Error:', error);
|
|
698
|
-
self.postMessage({
|
|
699
|
-
type: 'error',
|
|
700
|
-
error: error.message
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
schedulerLog('[OSCPreSchedulerWorker] Script loaded');
|
|
1
|
+
(()=>{var S=null,d=null,u=null,g=null,A=null,s=null,v={},a=null,E={},i=[],b=null,k=0,F=!1,_=0,o=[],V=100,y=5,q=2208988800,M=25,Q=.1;function L(){var e=performance.timeOrigin+performance.now();return e/1e3+q}function x(e){if(e.length>=16&&e[0]===35){var r=new DataView(e.buffer,e.byteOffset),t=r.getUint32(8,!1),n=r.getUint32(12,!1);return t+n/4294967296}return null}function X(){if(!S||!u){console.error("[PreScheduler] Cannot init - missing buffer or constants");return}g=new Int32Array(S),A=new DataView(S),s=new Uint8Array(S),v={IN_HEAD:(d+u.CONTROL_START+0)/4,IN_TAIL:(d+u.CONTROL_START+4)/4};var e=d+u.METRICS_START;a=new Uint32Array(S,e,u.METRICS_SIZE/4),E={EVENTS_PENDING:7,MAX_EVENTS_PENDING:8,BUNDLES_WRITTEN:9,BUNDLES_DROPPED:10,RETRIES_SUCCEEDED:11,RETRIES_FAILED:12,BUNDLES_SCHEDULED:13,EVENTS_CANCELLED:14,TOTAL_DISPATCHES:15,MESSAGES_RETRIED:16,RETRY_QUEUE_SIZE:17,RETRY_QUEUE_MAX:18}}function D(){if(a){Atomics.store(a,E.EVENTS_PENDING,i.length);var e=i.length,r=Atomics.load(a,E.MAX_EVENTS_PENDING);e>r&&Atomics.store(a,E.MAX_EVENTS_PENDING,e)}}function I(e,r){if(!S||!g)return console.error("[PreScheduler] Not initialized for ring buffer writing"),a&&Atomics.add(a,E.BUNDLES_DROPPED,1),!1;var t=e.length,n=u.MESSAGE_HEADER_SIZE+t;if(n>u.IN_BUFFER_SIZE-u.MESSAGE_HEADER_SIZE)return console.error("[PreScheduler] Message too large:",n),a&&Atomics.add(a,E.BUNDLES_DROPPED,1),!1;var l=Atomics.load(g,v.IN_HEAD),c=Atomics.load(g,v.IN_TAIL),h=(u.IN_BUFFER_SIZE-1-l+c)%u.IN_BUFFER_SIZE;if(h<n)return r||(a&&Atomics.add(a,E.BUNDLES_DROPPED,1),console.warn("[PreScheduler] Ring buffer full, message will be queued for retry")),!1;var f=u.IN_BUFFER_SIZE-l;if(n>f){var R=new Uint8Array(u.MESSAGE_HEADER_SIZE),m=new DataView(R.buffer);m.setUint32(0,u.MESSAGE_MAGIC,!0),m.setUint32(4,n,!0),m.setUint32(8,_,!0),m.setUint32(12,0,!0);var N=d+u.IN_BUFFER_START+l,U=d+u.IN_BUFFER_START;if(f>=u.MESSAGE_HEADER_SIZE){s.set(R,N);var C=f-u.MESSAGE_HEADER_SIZE;s.set(e.subarray(0,C),N+u.MESSAGE_HEADER_SIZE),s.set(e.subarray(C),U)}else{s.set(R.subarray(0,f),N),s.set(R.subarray(f),U);var H=u.MESSAGE_HEADER_SIZE-f;s.set(e,U+H)}}else{var T=d+u.IN_BUFFER_START+l;A.setUint32(T,u.MESSAGE_MAGIC,!0),A.setUint32(T+4,n,!0),A.setUint32(T+8,_,!0),A.setUint32(T+12,0,!0),s.set(e,T+u.MESSAGE_HEADER_SIZE)}_<5&&(""+_,""+l,""+n,""+(l+n)%u.IN_BUFFER_SIZE,void 0),Atomics.load(g,v.IN_HEAD);var Z=(l+n)%u.IN_BUFFER_SIZE;return Atomics.store(g,v.IN_HEAD,Z),_=_+1&4294967295,a&&Atomics.add(a,E.BUNDLES_WRITTEN,1),!0}function p(e,r){if(o.length>=V){console.error("[PreScheduler] Retry queue full, dropping message permanently"),a&&Atomics.add(a,E.RETRIES_FAILED,1);return}if(o.push({oscData:e,retryCount:0,context:r||"unknown",queuedAt:performance.now()}),a){Atomics.store(a,E.RETRY_QUEUE_SIZE,o.length);var t=Atomics.load(a,E.RETRY_QUEUE_MAX);o.length>t&&Atomics.store(a,E.RETRY_QUEUE_MAX,o.length)}o.length}function Y(){if(o.length!==0)for(var e=0;e<o.length;){var r=o[e],t=I(r.oscData,!0);t?(o.splice(e,1),a&&(Atomics.add(a,E.RETRIES_SUCCEEDED,1),Atomics.add(a,E.MESSAGES_RETRIED,1),Atomics.store(a,E.RETRY_QUEUE_SIZE,o.length)),r.context,r.retryCount+1,void 0):(r.retryCount++,a&&Atomics.add(a,E.MESSAGES_RETRIED,1),r.retryCount>=y?(console.error("[PreScheduler] Giving up on message after",y,"retries:",r.context),o.splice(e,1),a&&(Atomics.add(a,E.RETRIES_FAILED,1),Atomics.store(a,E.RETRY_QUEUE_SIZE,o.length))):e++)}}function W(e,r,t){var n=x(e);if(n===null){var l=I(e,!1);l||p(e,"immediate message");return}var c=L(),h=n-c,f={ntpTime:n,seq:k++,editorId:r||0,runTag:t||"",oscData:e};z(f),a&&Atomics.add(a,E.BUNDLES_SCHEDULED,1),D(),""+n.toFixed(3),""+c.toFixed(3),""+(h*1e3).toFixed(1),""+i.length}function z(e){i.push(e),$(i.length-1)}function K(){return i.length>0?i[0]:null}function J(){if(i.length===0)return null;var e=i[0],r=i.pop();return i.length>0&&(i[0]=r,w(0)),e}function $(e){for(;e>0;){var r=Math.floor((e-1)/2);if(P(i[e],i[r])>=0)break;B(e,r),e=r}}function w(e){for(var r=i.length;;){var t=2*e+1,n=2*e+2,l=e;if(t<r&&P(i[t],i[l])<0&&(l=t),n<r&&P(i[n],i[l])<0&&(l=n),l===e)break;B(e,l),e=l}}function P(e,r){return e.ntpTime===r.ntpTime?e.seq-r.seq:e.ntpTime-r.ntpTime}function B(e,r){var t=i[e];i[e]=i[r],i[r]=t}function j(){if(b!==null){console.warn("[PreScheduler] Polling already started");return}""+M,O()}function O(){F=!0,Y();for(var e=L(),r=e+Q,t=0,n=performance.now();i.length>0;){var l=K();if(l.ntpTime<=r){J(),D();var c=l.ntpTime-e;a&&Atomics.add(a,E.TOTAL_DISPATCHES,1),""+l.ntpTime.toFixed(3),""+e.toFixed(3),""+(c*1e3).toFixed(1),""+i.length;var h=I(l.oscData,!1);h||p(l.oscData,"scheduled bundle NTP="+l.ntpTime.toFixed(3)),t++}else break}(t>0||i.length>0||o.length>0)&&(""+t,""+i.length,""+o.length,void 0),F=!1,b=setTimeout(O,M)}function G(e){if(i.length!==0){for(var r=i.length,t=[],n=0;n<i.length;n++){var l=i[n];e(l)||t.push(l)}var c=r-t.length;c>0&&(i=t,ee(),a&&Atomics.add(a,E.EVENTS_CANCELLED,c),D(),""+c+i.length,void 0)}}function ee(){for(var e=Math.floor(i.length/2)-1;e>=0;e--)w(e)}function re(e,r){G(function(t){return t.editorId===e&&t.runTag===r})}function ne(e){G(function(r){return r.editorId===e})}function te(){if(i.length!==0){var e=i.length;a&&Atomics.add(a,E.EVENTS_CANCELLED,e),i=[],D(),""+e}}function ie(e){return!e||e.length<8?!1:e[0]===35&&e[1]===98&&e[2]===117&&e[3]===110&&e[4]===100&&e[5]===108&&e[6]===101&&e[7]===0}function ae(e){for(var r=[],t=new DataView(e.buffer,e.byteOffset,e.byteLength),n=16;n<e.length;){var l=t.getInt32(n,!1);if(n+=4,l<=0||n+l>e.length)break;var c=e.slice(n,n+l);for(r.push(c),n+=l;n%4!==0&&n<e.length;)n++}return r}function le(e){if(ie(e))for(var r=ae(e),t=0;t<r.length;t++){var n=I(r[t],!1);n||p(r[t],"immediate bundle message "+t)}else{var n=I(e,!1);n||p(e,"immediate message")}}self.addEventListener("message",function(e){var r=e.data;try{switch(r.type){case"init":S=r.sharedBuffer,d=r.ringBufferBase,u=r.bufferConstants,X(),j(),self.postMessage({type:"initialized"});break;case"send":W(r.oscData,r.editorId||0,r.runTag||"");break;case"sendImmediate":le(r.oscData);break;case"cancelEditorTag":r.runTag!==void 0&&r.runTag!==null&&r.runTag!==""&&re(r.editorId||0,r.runTag);break;case"cancelEditor":ne(r.editorId||0);break;case"cancelAll":te();break;default:console.warn("[OSCPreSchedulerWorker] Unknown message type:",r.type)}}catch(t){console.error("[OSCPreSchedulerWorker] Error:",t),self.postMessage({type:"error",error:t.message})}});})();
|