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.
@@ -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})}});})();