supersonic-scsynth 0.1.8 → 0.2.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.
@@ -7,24 +7,14 @@
7
7
  */
8
8
 
9
9
  /**
10
- * OSC OUT Worker - Scheduler for sending OSC bundles to scsynth
11
- * Handles timed bundles with priority queue scheduling
12
- * Writes directly to SharedArrayBuffer ring buffer
10
+ * OSC OUT Worker - Scheduler for OSC bundles
11
+ * Handles timed bundles and forwards them to the writer worker
12
+ * NO LONGER writes to ring buffer directly - all writes go through osc_writer_worker.js
13
13
  * ES5-compatible for Qt WebEngine
14
14
  */
15
15
 
16
- // Ring buffer configuration
17
- var sharedBuffer = null;
18
- var ringBufferBase = null;
19
- var atomicView = null;
20
- var dataView = null;
21
- var uint8View = null;
22
-
23
- // Ring buffer layout constants (loaded from WASM at initialization)
24
- var bufferConstants = null;
25
-
26
- // Control indices (calculated after init)
27
- var CONTROL_INDICES = {};
16
+ // Reference to the writer worker
17
+ var writerWorker = null;
28
18
 
29
19
  // Scheduling state
30
20
  var scheduledEvents = [];
@@ -33,181 +23,48 @@ var cachedTimeDelta = null;
33
23
  var minimumScheduleRequirementS = 0.002; // 2ms for audio precision
34
24
  var latencyS = 0.05; // 50ms latency compensation for scsynth
35
25
 
36
- // Message queue for handling backpressure
37
- var immediateQueue = []; // Queue of messages waiting to be written
38
- var isWriting = false; // Flag to prevent concurrent write attempts
39
- var writeRetryTimer = null;
26
+ var DEBUG_SCHED_LOGS = false;
27
+ function schedulerLog() {
28
+ if (DEBUG_SCHED_LOGS) {
29
+ console.log.apply(console, arguments);
30
+ }
31
+ }
32
+ function schedulerWarn() {
33
+ if (DEBUG_SCHED_LOGS) {
34
+ console.warn.apply(console, arguments);
35
+ }
36
+ }
40
37
 
41
38
  // Statistics
42
39
  var stats = {
43
40
  bundlesScheduled: 0,
44
- bundlesWritten: 0,
45
- bundlesDropped: 0,
46
- bufferOverruns: 0,
47
- retries: 0,
48
- queueDepth: 0,
49
- maxQueueDepth: 0
41
+ bundlesSentToWriter: 0
50
42
  };
51
43
 
52
44
  /**
53
- * Initialize ring buffer access
45
+ * Initialize scheduler
54
46
  */
55
- function initRingBuffer(buffer, base, constants) {
56
- sharedBuffer = buffer;
57
- ringBufferBase = base;
58
- bufferConstants = constants;
59
- atomicView = new Int32Array(sharedBuffer);
60
- dataView = new DataView(sharedBuffer);
61
- uint8View = new Uint8Array(sharedBuffer);
62
-
63
- // Calculate control indices using constants from WASM
64
- CONTROL_INDICES = {
65
- IN_HEAD: (ringBufferBase + bufferConstants.CONTROL_START + 0) / 4,
66
- IN_TAIL: (ringBufferBase + bufferConstants.CONTROL_START + 4) / 4
67
- };
47
+ function init(buffer, base, constants) {
48
+ // We don't need the ring buffer anymore, but keep the params for compatibility
49
+ schedulerLog('[OSCSchedulerWorker] Initialized (scheduler only, no ring buffer access)');
68
50
  }
69
51
 
70
52
  /**
71
- * Queue a message for writing (handles backpressure)
53
+ * Send message to writer worker
72
54
  */
73
- function queueMessage(oscMessage) {
74
- immediateQueue.push(oscMessage);
75
- stats.queueDepth = immediateQueue.length;
76
-
77
- if (stats.queueDepth > stats.maxQueueDepth) {
78
- stats.maxQueueDepth = stats.queueDepth;
79
- }
80
-
81
- // Start processing if not already running
82
- if (!isWriting) {
83
- processQueue();
84
- }
85
- }
86
-
87
- /**
88
- * Process the message queue - blocks until space is available
89
- */
90
- function processQueue() {
91
- if (isWriting || immediateQueue.length === 0) {
55
+ function sendToWriter(oscMessage) {
56
+ if (!writerWorker) {
57
+ console.error('[OSCSchedulerWorker] Writer worker not set');
92
58
  return;
93
59
  }
94
60
 
95
- isWriting = true;
96
-
97
- function processNext() {
98
- if (immediateQueue.length === 0) {
99
- isWriting = false;
100
- stats.queueDepth = 0;
101
- return;
102
- }
103
-
104
- var message = immediateQueue[0]; // Peek at first message
105
-
106
- // Block until there's space, then write
107
- var success = writeToRingBufferBlocking(message);
108
-
109
- if (success) {
110
- // Success! Remove from queue
111
- immediateQueue.shift();
112
- stats.queueDepth = immediateQueue.length;
113
-
114
- // Process next message
115
- if (immediateQueue.length > 0) {
116
- setTimeout(processNext, 0);
117
- } else {
118
- isWriting = false;
119
- }
120
- } else {
121
- // Fatal error (message too large or not initialized)
122
- console.error('[OSCOutWorker] Fatal error, dropping message');
123
- immediateQueue.shift(); // Remove bad message
124
- stats.bundlesDropped++;
125
- stats.queueDepth = immediateQueue.length;
126
-
127
- // Continue with next message
128
- setTimeout(processNext, 0);
129
- }
130
- }
131
-
132
- processNext();
133
- }
134
-
135
- /**
136
- * Write OSC message to ring buffer - blocks until space available
137
- * Returns true on success, false on fatal error (message too large)
138
- */
139
- function writeToRingBufferBlocking(oscMessage) {
140
- if (!sharedBuffer) {
141
- console.error('[OSCOutWorker] Not initialized');
142
- return false;
143
- }
144
-
145
- var payloadSize = oscMessage.length;
146
- var totalSize = bufferConstants.MESSAGE_HEADER_SIZE + payloadSize;
147
-
148
- // Check if message fits in buffer at all (account for padding at wrap)
149
- if (totalSize > bufferConstants.IN_BUFFER_SIZE - bufferConstants.MESSAGE_HEADER_SIZE) {
150
- console.error('[OSCOutWorker] Message too large:', totalSize, 'max:', bufferConstants.IN_BUFFER_SIZE - bufferConstants.MESSAGE_HEADER_SIZE);
151
- return false;
152
- }
153
-
154
- // Keep trying until we have space
155
- while (true) {
156
- var head = Atomics.load(atomicView, CONTROL_INDICES.IN_HEAD);
157
- var tail = Atomics.load(atomicView, CONTROL_INDICES.IN_TAIL);
158
-
159
- // Check available space
160
- var available = (bufferConstants.IN_BUFFER_SIZE - 1 - head + tail) % bufferConstants.IN_BUFFER_SIZE;
161
-
162
- if (available >= totalSize) {
163
- // Check if message fits contiguously, otherwise write padding and wrap
164
- var spaceToEnd = bufferConstants.IN_BUFFER_SIZE - head;
165
-
166
- if (totalSize > spaceToEnd) {
167
- // Message won't fit at end - write padding marker and wrap to beginning
168
- var paddingPos = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
169
- dataView.setUint32(paddingPos, bufferConstants.PADDING_MAGIC, true);
170
- dataView.setUint32(paddingPos + 4, 0, true);
171
- dataView.setUint32(paddingPos + 8, 0, true);
172
- dataView.setUint32(paddingPos + 12, 0, true);
173
-
174
- // Wrap head to beginning
175
- head = 0;
176
- }
177
-
178
- // We have space! Write the message (now guaranteed contiguous)
179
- var writePos = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
180
-
181
- // Write message header
182
- dataView.setUint32(writePos, bufferConstants.MESSAGE_MAGIC, true);
183
- dataView.setUint32(writePos + 4, totalSize, true);
184
- dataView.setUint32(writePos + 8, stats.bundlesWritten, true); // sequence
185
- dataView.setUint32(writePos + 12, 0, true); // padding
186
-
187
- // Write payload
188
- uint8View.set(oscMessage, writePos + bufferConstants.MESSAGE_HEADER_SIZE);
189
-
190
- // Update head pointer (publish message)
191
- var newHead = (head + totalSize) % bufferConstants.IN_BUFFER_SIZE;
192
- Atomics.store(atomicView, CONTROL_INDICES.IN_HEAD, newHead);
193
-
194
- stats.bundlesWritten++;
195
- return true;
196
- }
197
-
198
- // Buffer is full - wait for tail to move (scsynth to consume)
199
- stats.bufferOverruns++;
200
-
201
- // Wait on the tail pointer - will wake when scsynth consumes data
202
- // Timeout after 100ms to check if worker should stop
203
- var result = Atomics.wait(atomicView, CONTROL_INDICES.IN_TAIL, tail, 100);
61
+ // Send to writer worker via postMessage
62
+ writerWorker.postMessage({
63
+ type: 'write',
64
+ oscData: oscMessage
65
+ });
204
66
 
205
- if (result === 'ok' || result === 'not-equal') {
206
- // Tail moved! Loop will retry
207
- stats.retries++;
208
- }
209
- // On timeout, loop continues to retry (allows checking for stop signal)
210
- }
67
+ stats.bundlesSentToWriter++;
211
68
  }
212
69
 
213
70
  /**
@@ -308,7 +165,7 @@ function extractMessagesFromBundle(data) {
308
165
 
309
166
  /**
310
167
  * Process incoming OSC data (message or bundle)
311
- * Pre-scheduler: waits for calculated time then sends to ring buffer
168
+ * Pre-scheduler: waits for calculated time then sends to writer
312
169
  * waitTimeMs is calculated by SuperSonic based on AudioContext time
313
170
  */
314
171
  function processOSC(oscData, editorId, runTag, waitTimeMs) {
@@ -316,13 +173,13 @@ function processOSC(oscData, editorId, runTag, waitTimeMs) {
316
173
 
317
174
  // If no wait time provided, or wait time is 0 or negative, send immediately
318
175
  if (waitTimeMs === null || waitTimeMs === undefined || waitTimeMs <= 0) {
319
- queueMessage(oscData);
176
+ sendToWriter(oscData);
320
177
  return;
321
178
  }
322
179
 
323
180
  // Schedule to send after waitTimeMs
324
181
  setTimeout(function() {
325
- queueMessage(oscData);
182
+ sendToWriter(oscData);
326
183
  }, waitTimeMs);
327
184
  }
328
185
 
@@ -338,11 +195,11 @@ function processImmediate(oscData) {
338
195
  // Send each message individually for immediate execution
339
196
  var messages = extractMessagesFromBundle(oscData);
340
197
  for (var i = 0; i < messages.length; i++) {
341
- queueMessage(messages[i]);
198
+ sendToWriter(messages[i]);
342
199
  }
343
200
  } else {
344
201
  // Regular message - send as-is
345
- queueMessage(oscData);
202
+ sendToWriter(oscData);
346
203
  }
347
204
  }
348
205
 
@@ -420,7 +277,7 @@ function runNextEvent() {
420
277
  var data = event[2];
421
278
 
422
279
  // Send the complete bundle unchanged (with original timestamp)
423
- queueMessage(data);
280
+ sendToWriter(data);
424
281
 
425
282
  scheduleNextEvent();
426
283
  }
@@ -469,10 +326,16 @@ self.onmessage = function(event) {
469
326
  try {
470
327
  switch (data.type) {
471
328
  case 'init':
472
- initRingBuffer(data.sharedBuffer, data.ringBufferBase, data.bufferConstants);
329
+ init(data.sharedBuffer, data.ringBufferBase, data.bufferConstants);
473
330
  self.postMessage({ type: 'initialized' });
474
331
  break;
475
332
 
333
+ case 'setWriterWorker':
334
+ // Set reference to writer worker (passed as MessagePort)
335
+ writerWorker = data.port;
336
+ schedulerLog('[OSCSchedulerWorker] Writer worker connected');
337
+ break;
338
+
476
339
  case 'send':
477
340
  // Single send method for both messages and bundles
478
341
  // waitTimeMs is calculated by SuperSonic based on AudioContext time
@@ -505,7 +368,7 @@ self.onmessage = function(event) {
505
368
  break;
506
369
 
507
370
  default:
508
- console.warn('[OSCOutWorker] Unknown message type:', data.type);
371
+ schedulerWarn('[OSCOutWorker] Unknown message type:', data.type);
509
372
  }
510
373
  } catch (error) {
511
374
  console.error('[OSCOutWorker] Error:', error);
@@ -516,4 +379,4 @@ self.onmessage = function(event) {
516
379
  }
517
380
  };
518
381
 
519
- console.log('[OSCOutWorker] Script loaded');
382
+ schedulerLog('[OSCSchedulerWorker] Script loaded - scheduler only, delegates to writer worker');
@@ -0,0 +1,305 @@
1
+ /*
2
+ SuperSonic - SuperCollider AudioWorklet WebAssembly port
3
+ Copyright (c) 2025 Sam Aaron
4
+
5
+ Based on SuperCollider by James McCartney and community
6
+ GPL v3 or later
7
+ */
8
+
9
+ /**
10
+ * Base class for ring buffer workers
11
+ * Provides common functionality for polling ring buffers with Atomics.wait()
12
+ * ES5-compatible for Qt WebEngine
13
+ */
14
+
15
+ // Ring buffer configuration
16
+ var RingBufferWorkerBase = function(bufferType) {
17
+ this.bufferType = bufferType; // 'OUT', 'SYSTEM', etc.
18
+ this.sharedBuffer = null;
19
+ this.ringBufferBase = null;
20
+ this.atomicView = null;
21
+ this.dataView = null;
22
+ this.uint8View = null;
23
+ this.bufferConstants = null;
24
+ this.CONTROL_INDICES = {};
25
+ this.running = false;
26
+ this.loggedCorruptionState = false;
27
+
28
+ // Statistics
29
+ this.stats = {
30
+ messagesReceived: 0,
31
+ lastSequenceReceived: -1,
32
+ droppedMessages: 0,
33
+ wakeups: 0,
34
+ timeouts: 0
35
+ };
36
+ };
37
+
38
+ /**
39
+ * Initialize ring buffer access
40
+ */
41
+ RingBufferWorkerBase.prototype.initRingBuffer = function(buffer, base, constants) {
42
+ this.sharedBuffer = buffer;
43
+ this.ringBufferBase = base;
44
+ this.bufferConstants = constants;
45
+ this.atomicView = new Int32Array(this.sharedBuffer);
46
+ this.dataView = new DataView(this.sharedBuffer);
47
+ this.uint8View = new Uint8Array(this.sharedBuffer);
48
+
49
+ // Calculate control indices based on buffer type
50
+ var headOffset, tailOffset;
51
+ if (this.bufferType === 'OUT') {
52
+ headOffset = 8;
53
+ tailOffset = 12;
54
+ } else if (this.bufferType === 'SYSTEM') {
55
+ headOffset = 24; // system_head offset
56
+ tailOffset = 28; // system_tail offset
57
+ }
58
+
59
+ // Debug logging for SYSTEM worker
60
+ if (this.bufferType === 'SYSTEM') {
61
+ console.log('[' + this.bufferType + 'Worker] Init params:', {
62
+ ringBufferBase: base,
63
+ CONTROL_START: constants.CONTROL_START,
64
+ headOffset: headOffset,
65
+ tailOffset: tailOffset,
66
+ calculatedHeadByteOffset: base + constants.CONTROL_START + headOffset,
67
+ calculatedHeadIndex: (base + constants.CONTROL_START + headOffset) / 4
68
+ });
69
+ console.log('[' + this.bufferType + 'Worker] Buffer constants:', {
70
+ SYSTEM_BUFFER_START: constants.SYSTEM_BUFFER_START,
71
+ SYSTEM_BUFFER_SIZE: constants.SYSTEM_BUFFER_SIZE,
72
+ MESSAGE_MAGIC: constants.MESSAGE_MAGIC,
73
+ PADDING_MAGIC: constants.PADDING_MAGIC
74
+ });
75
+ }
76
+
77
+ this.CONTROL_INDICES = {
78
+ HEAD: (this.ringBufferBase + this.bufferConstants.CONTROL_START + headOffset) / 4,
79
+ TAIL: (this.ringBufferBase + this.bufferConstants.CONTROL_START + tailOffset) / 4
80
+ };
81
+
82
+ // Debug: log buffer info for SYSTEM
83
+ if (this.bufferType === 'SYSTEM') {
84
+ var bufferInfo = this.getBufferInfo();
85
+ console.log('[' + this.bufferType + 'Worker] Buffer info:', bufferInfo);
86
+ console.log('[' + this.bufferType + 'Worker] Control indices:', this.CONTROL_INDICES);
87
+
88
+ // Check initial buffer state - read first 64 bytes
89
+ var startPos = this.ringBufferBase + bufferInfo.start;
90
+ console.log('[' + this.bufferType + 'Worker] First 64 bytes of buffer at position', startPos + ':');
91
+ for (var i = 0; i < 64; i += 4) {
92
+ var value = this.dataView.getUint32(startPos + i, true);
93
+ console.log(' Offset', i + ':', '0x' + value.toString(16).padStart(8, '0'));
94
+ }
95
+
96
+ // Check current head/tail values
97
+ var currentHead = Atomics.load(this.atomicView, this.CONTROL_INDICES.HEAD);
98
+ var currentTail = Atomics.load(this.atomicView, this.CONTROL_INDICES.TAIL);
99
+ console.log('[' + this.bufferType + 'Worker] Initial head:', currentHead, 'tail:', currentTail);
100
+ }
101
+ };
102
+
103
+ /**
104
+ * Get buffer start and size based on type
105
+ */
106
+ RingBufferWorkerBase.prototype.getBufferInfo = function() {
107
+ if (this.bufferType === 'OUT') {
108
+ return {
109
+ start: this.bufferConstants.OUT_BUFFER_START,
110
+ size: this.bufferConstants.OUT_BUFFER_SIZE
111
+ };
112
+ } else if (this.bufferType === 'SYSTEM') {
113
+ return {
114
+ start: this.bufferConstants.SYSTEM_BUFFER_START,
115
+ size: this.bufferConstants.SYSTEM_BUFFER_SIZE
116
+ };
117
+ }
118
+ return null;
119
+ };
120
+
121
+ /**
122
+ * Read all available messages from buffer
123
+ */
124
+ RingBufferWorkerBase.prototype.readMessages = function() {
125
+ var head = Atomics.load(this.atomicView, this.CONTROL_INDICES.HEAD);
126
+ var tail = Atomics.load(this.atomicView, this.CONTROL_INDICES.TAIL);
127
+
128
+ var messages = [];
129
+
130
+ if (head === tail) {
131
+ return messages; // No messages
132
+ }
133
+
134
+ var bufferInfo = this.getBufferInfo();
135
+ var currentTail = tail;
136
+ var messagesRead = 0;
137
+ var maxMessages = 100;
138
+
139
+ while (currentTail !== head && messagesRead < maxMessages) {
140
+ var readPos = this.ringBufferBase + bufferInfo.start + currentTail;
141
+
142
+ // Read message header (now always contiguous due to padding)
143
+ var magic = this.dataView.getUint32(readPos, true);
144
+
145
+ // Check for padding marker - skip to beginning
146
+ if (magic === this.bufferConstants.PADDING_MAGIC) {
147
+ currentTail = 0;
148
+ continue;
149
+ }
150
+
151
+ if (magic !== this.bufferConstants.MESSAGE_MAGIC) {
152
+ // Only log first few corrupted messages to avoid spamming
153
+ if (this.stats.droppedMessages < 5) {
154
+ console.error('[' + this.bufferType + 'Worker] Corrupted message at position', currentTail, 'magic:', magic, 'expected:', this.bufferConstants.MESSAGE_MAGIC);
155
+ }
156
+ this.stats.droppedMessages++;
157
+ // Skip this byte and continue
158
+ currentTail = (currentTail + 1) % bufferInfo.size;
159
+ continue;
160
+ }
161
+
162
+ var length = this.dataView.getUint32(readPos + 4, true);
163
+ var sequence = this.dataView.getUint32(readPos + 8, true);
164
+ var padding = this.dataView.getUint32(readPos + 12, true); // unused padding field
165
+
166
+ // Validate message length
167
+ if (length < this.bufferConstants.MESSAGE_HEADER_SIZE || length > bufferInfo.size) {
168
+ // Only log first few invalid lengths to avoid spamming
169
+ if (this.stats.droppedMessages < 5) {
170
+ console.error('[' + this.bufferType + 'Worker] Invalid message length:', length);
171
+ }
172
+ this.stats.droppedMessages++;
173
+ currentTail = (currentTail + 1) % bufferInfo.size;
174
+ continue;
175
+ }
176
+
177
+ // Check for dropped messages via sequence
178
+ if (this.stats.lastSequenceReceived >= 0) {
179
+ var expectedSeq = (this.stats.lastSequenceReceived + 1) & 0xFFFFFFFF;
180
+ if (sequence !== expectedSeq) {
181
+ var dropped = (sequence - expectedSeq + 0x100000000) & 0xFFFFFFFF;
182
+ if (dropped < 1000) { // Sanity check
183
+ console.warn('[' + this.bufferType + 'Worker] Detected', dropped, 'dropped messages');
184
+ this.stats.droppedMessages += dropped;
185
+ }
186
+ }
187
+ }
188
+ this.stats.lastSequenceReceived = sequence;
189
+
190
+ // Read payload (OSC binary data) - now contiguous due to padding
191
+ var payloadLength = length - this.bufferConstants.MESSAGE_HEADER_SIZE;
192
+ var payloadStart = readPos + this.bufferConstants.MESSAGE_HEADER_SIZE;
193
+
194
+ // Create a proper copy (not a view into SharedArrayBuffer)
195
+ var payload = new Uint8Array(payloadLength);
196
+ for (var i = 0; i < payloadLength; i++) {
197
+ payload[i] = this.uint8View[payloadStart + i];
198
+ }
199
+
200
+ messages.push({
201
+ oscData: payload,
202
+ sequence: sequence
203
+ });
204
+
205
+ // Move to next message
206
+ currentTail = (currentTail + length) % bufferInfo.size;
207
+ messagesRead++;
208
+ this.stats.messagesReceived++;
209
+ }
210
+
211
+ // Update tail pointer (consume messages)
212
+ if (messagesRead > 0) {
213
+ Atomics.store(this.atomicView, this.CONTROL_INDICES.TAIL, currentTail);
214
+ }
215
+
216
+ return messages;
217
+ };
218
+
219
+ /**
220
+ * Main wait loop using Atomics.wait for instant wake
221
+ */
222
+ RingBufferWorkerBase.prototype.waitLoop = function() {
223
+ var self = this;
224
+ while (this.running) {
225
+ try {
226
+ // Get current HEAD value
227
+ var currentHead = Atomics.load(this.atomicView, this.CONTROL_INDICES.HEAD);
228
+ var currentTail = Atomics.load(this.atomicView, this.CONTROL_INDICES.TAIL);
229
+
230
+ // If buffer is empty, wait for AudioWorklet to notify us
231
+ if (currentHead === currentTail) {
232
+ // Wait for up to 100ms (allows checking stop signal)
233
+ var result = Atomics.wait(this.atomicView, this.CONTROL_INDICES.HEAD, currentHead, 100);
234
+
235
+ if (result === 'ok' || result === 'not-equal') {
236
+ // We were notified or value changed!
237
+ this.stats.wakeups++;
238
+ } else if (result === 'timed-out') {
239
+ this.stats.timeouts++;
240
+ continue; // Check running flag
241
+ }
242
+ }
243
+
244
+ // Read all available messages
245
+ var messages = this.readMessages();
246
+
247
+ // Debug: log ONCE if we tried to read but got no valid messages due to corruption
248
+ if (messages.length === 0 && currentHead !== currentTail && this.stats.droppedMessages > 0) {
249
+ if (this.bufferType === 'SYSTEM' && !this.loggedCorruptionState) {
250
+ console.log('[' + this.bufferType + 'Worker] Head != tail (head=' + currentHead + ', tail=' + currentTail + ') but all messages corrupted. Dropped:', this.stats.droppedMessages);
251
+ this.loggedCorruptionState = true;
252
+ }
253
+ }
254
+
255
+ if (messages.length > 0) {
256
+ // Send to main thread
257
+ self.postMessage({
258
+ type: 'messages',
259
+ messages: messages,
260
+ stats: {
261
+ wakeups: this.stats.wakeups,
262
+ timeouts: this.stats.timeouts,
263
+ messagesReceived: this.stats.messagesReceived,
264
+ droppedMessages: this.stats.droppedMessages
265
+ }
266
+ });
267
+ }
268
+
269
+ } catch (error) {
270
+ console.error('[' + this.bufferType + 'Worker] Error in wait loop:', error);
271
+ self.postMessage({
272
+ type: 'error',
273
+ error: error.message
274
+ });
275
+
276
+ // Brief pause on error before retrying
277
+ Atomics.wait(this.atomicView, 0, this.atomicView[0], 10);
278
+ }
279
+ }
280
+ };
281
+
282
+ /**
283
+ * Start the wait loop
284
+ */
285
+ RingBufferWorkerBase.prototype.start = function() {
286
+ if (!this.sharedBuffer) {
287
+ console.error('[' + this.bufferType + 'Worker] Cannot start - not initialized');
288
+ return;
289
+ }
290
+
291
+ if (this.running) {
292
+ console.warn('[' + this.bufferType + 'Worker] Already running');
293
+ return;
294
+ }
295
+
296
+ this.running = true;
297
+ this.waitLoop();
298
+ };
299
+
300
+ /**
301
+ * Stop the wait loop
302
+ */
303
+ RingBufferWorkerBase.prototype.stop = function() {
304
+ this.running = false;
305
+ };