sab-message-port 1.0.2 → 1.0.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/README.md +51 -3
- package/dist/SABMessagePort.min.js +1 -1
- package/package.json +1 -1
- package/src/SABMessagePort.js +129 -12
package/README.md
CHANGED
|
@@ -289,6 +289,36 @@ const msgs = port.tryRead(10); // up to 10 messages (array, newest first) or
|
|
|
289
289
|
|
|
290
290
|
Non-blocking peek. Returns the next message without removing it from the queue. If the queue is empty, attempts a non-blocking read from the shared buffer first. Returns `null` if no data is available.
|
|
291
291
|
|
|
292
|
+
### `port.queueLimit` (getter/setter)
|
|
293
|
+
|
|
294
|
+
Optional per-queue message count limit. When the queue exceeds the limit, the **oldest** messages are silently discarded. Applies to both the internal write queue and read queue.
|
|
295
|
+
|
|
296
|
+
- **`null`** (default): No limit — queues grow without bound.
|
|
297
|
+
- **Non-negative integer**: Maximum number of messages allowed in each queue.
|
|
298
|
+
- Setting a lower limit immediately trims the existing queue.
|
|
299
|
+
|
|
300
|
+
```javascript
|
|
301
|
+
const port = new SABMessagePort('a', 256, 5); // queueLimit=5 via constructor
|
|
302
|
+
port.queueLimit = 10; // change at runtime
|
|
303
|
+
port.queueLimit = null; // disable
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The third constructor parameter (`queueLimit`) sets the initial limit. `SABMessagePort.from(initMsg, queueLimit)` also accepts it.
|
|
307
|
+
|
|
308
|
+
### `port.onQueueOverflow` (getter/setter)
|
|
309
|
+
|
|
310
|
+
Callback invoked **before** a queue is truncated due to exceeding its `queueLimit`. Receives the overflowing queue array by reference — the callback can inspect, log, or modify it. After the callback returns, the queue is truncated only if it still exceeds the limit.
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
port.onQueueOverflow = (queue) => {
|
|
314
|
+
console.warn(`Dropping ${queue.length - port.queueLimit} messages`);
|
|
315
|
+
// Or handle manually: queue.splice(0, queue.length - port.queueLimit);
|
|
316
|
+
};
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
- **`null`** (default): No callback — overflow is silently truncated.
|
|
320
|
+
- Propagates to both internal SABPipe instances (writer and reader).
|
|
321
|
+
|
|
292
322
|
### `port.close()`
|
|
293
323
|
|
|
294
324
|
Disposes both directions. Unblocks any waiting readers/writers by signaling disposal. After closing, all `postMessage()`, `read()`, `asyncRead()`, and `tryRead()` calls will throw. Calling `close()` multiple times is safe (subsequent calls are no-ops).
|
|
@@ -305,7 +335,7 @@ Unidirectional channel — one end writes, the other reads. Used internally by `
|
|
|
305
335
|
|
|
306
336
|
All messages must be **JSON-serializable**. Message ordering is **FIFO**.
|
|
307
337
|
|
|
308
|
-
### `new SABPipe(role, sabOrSize = 131072, byteOffset = 0, sectionSize = null)`
|
|
338
|
+
### `new SABPipe(role, sabOrSize = 131072, byteOffset = 0, sectionSize = null, queueLimit = null)`
|
|
309
339
|
|
|
310
340
|
| Parameter | Default | Description |
|
|
311
341
|
|-----------|---------|-------------|
|
|
@@ -313,6 +343,7 @@ All messages must be **JSON-serializable**. Message ordering is **FIFO**.
|
|
|
313
343
|
| `sabOrSize` | `131072` | Byte size for a new buffer (128 KB), or an existing `SharedArrayBuffer`. |
|
|
314
344
|
| `byteOffset` | `0` | Starting byte offset in the SAB. |
|
|
315
345
|
| `sectionSize` | `null` | Section size in bytes. Defaults to the remaining SAB from `byteOffset`. |
|
|
346
|
+
| `queueLimit` | `null` | Max messages in the queue. `null` = unlimited. When exceeded, oldest messages are discarded. |
|
|
316
347
|
|
|
317
348
|
The writer and reader must share the same `SharedArrayBuffer` (and same offset/section) to communicate. Role enforcement is strict: the writer can only call `postMessage()`, and the reader can only call `read()`/`asyncRead()`/`tryRead()`/`onmessage`. Calling the wrong method throws.
|
|
318
349
|
|
|
@@ -381,6 +412,14 @@ Event-driven handler. Setting a function starts a continuous async read loop; se
|
|
|
381
412
|
|
|
382
413
|
### Shared
|
|
383
414
|
|
|
415
|
+
#### `pipe.queueLimit` (getter/setter)
|
|
416
|
+
|
|
417
|
+
Optional per-queue message count limit. See [`SABMessagePort.queueLimit`](#portqueuelimit-gettersetter) for details.
|
|
418
|
+
|
|
419
|
+
#### `pipe.onQueueOverflow` (getter/setter)
|
|
420
|
+
|
|
421
|
+
Callback invoked before queue truncation. See [`SABMessagePort.onQueueOverflow`](#portonqueueoverflow-gettersetter) for details.
|
|
422
|
+
|
|
384
423
|
#### `pipe.close()` / `pipe.destroy()`
|
|
385
424
|
|
|
386
425
|
Disposes the channel and unblocks any waiting readers/writers. After disposal, all read/write operations throw `'SABPipe disposed'`. Safe to call multiple times.
|
|
@@ -404,7 +443,7 @@ The worker can also switch between blocking (SABPipe) and non-blocking (MessageP
|
|
|
404
443
|
- **Main→worker:** Uses SABPipe by default (enables `read()`/`tryRead()` on the worker). Can be switched to native `MessagePort` when blocking reads aren't needed.
|
|
405
444
|
- The main thread **always receives via native `MessagePort`** and never blocks.
|
|
406
445
|
|
|
407
|
-
### `new MWChannel(side, sabSizeKB = 128)`
|
|
446
|
+
### `new MWChannel(side, sabSizeKB = 128, queueLimit = null)`
|
|
408
447
|
|
|
409
448
|
Creates a new channel.
|
|
410
449
|
|
|
@@ -412,13 +451,14 @@ Creates a new channel.
|
|
|
412
451
|
|-----------|---------|-------------|
|
|
413
452
|
| `side` | (required) | `'m'` (main thread) or `'w'` (worker). Throws if invalid. |
|
|
414
453
|
| `sabSizeKB` | `128` | SABPipe buffer size in KB. Main side only. |
|
|
454
|
+
| `queueLimit` | `null` | Max messages in the queue. `null` = unlimited. |
|
|
415
455
|
|
|
416
456
|
```javascript
|
|
417
457
|
const port = new MWChannel('m'); // 128 KB SABPipe buffer
|
|
418
458
|
const port = new MWChannel('m', 256); // 256 KB SABPipe buffer
|
|
419
459
|
```
|
|
420
460
|
|
|
421
|
-
### `MWChannel.from(initMsg)`
|
|
461
|
+
### `MWChannel.from(initMsg, queueLimit = null)`
|
|
422
462
|
|
|
423
463
|
Creates the worker side from a received init message. The init message must have `type: 'MWChannel'`.
|
|
424
464
|
|
|
@@ -499,6 +539,14 @@ Switching to `'nonblocking'`:
|
|
|
499
539
|
2. Main calls `port.setMode(newMode)` to switch its send transport.
|
|
500
540
|
3. Worker calls `port.setMode(newMode)` to switch its receive transport.
|
|
501
541
|
|
|
542
|
+
### `port.queueLimit` (getter/setter)
|
|
543
|
+
|
|
544
|
+
Optional per-queue message count limit. Delegates to the underlying SABPipe (`_sabWriter` on main, `_sabReader` on worker). Also enforces on the `_pendingDrain` buffer during mode switches.
|
|
545
|
+
|
|
546
|
+
### `port.onQueueOverflow` (getter/setter)
|
|
547
|
+
|
|
548
|
+
Callback invoked before queue truncation. Delegates to the underlying SABPipe and also fires for `_pendingDrain` overflow. See [`SABMessagePort.onQueueOverflow`](#portonqueueoverflow-gettersetter) for details.
|
|
549
|
+
|
|
502
550
|
### `port.close()`
|
|
503
551
|
|
|
504
552
|
Destroys the SABPipe and closes the native `MessagePort`. All subsequent operations throw.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var p=new TextEncoder,y=new TextDecoder,R=!1,n=R?(...c)=>console.log("[SABPipe]",...c):()=>{},u=class c{static STATUS=0;static RW_SIGNAL=1;static W_DATA_LEN=2;static NUM_PARTS=3;static PART_INDEX=4;static RESERVED_1=5;static RESERVED_2=6;static RESERVED_3=7;static CONTROL_TOP=8;static DATA_OFFSET=32;static STATUS_ACTIVE=0;static STATUS_DISPOSED=-1;static RW_CAN_WRITE=0;static RW_CAN_READ=1;constructor(e,t=131072,s=0,i=null){this._sab=typeof t=="number"?new SharedArrayBuffer(t):t;let r=i??this._sab.byteLength-s;if(this.i32=new Int32Array(this._sab,s,r>>2),this.u8=new Uint8Array(this._sab,s,r),this.maxChunk=r-c.DATA_OFFSET,e==="w")this.isWriter=!0,this.isReader=!1,this._write_queue=this._create_write_queue(),this._read_queue=null,this._payload_in_progress=null,this._writing=!1;else if(e==="r")this.isWriter=!1,this.isReader=!0,this._read_queue=[],this._write_queue=null,this._reading=!1,this._onmessage=null,this._messageLoopActive=!1;else throw new Error("Invalid role parameter: must be 'r' or 'w'");this._max_chunk=r-c.DATA_OFFSET}c=this.constructor;isDisposed(){return this.i32===null||this.i32[this.c.STATUS]===this.c.STATUS_DISPOSED}_checkDisposed(){if(this.isDisposed())throw this.i32=null,this.u8=null,this._sab=null,new Error("SABPipe disposed")}destroy(){if(this.i32!==null){for(let e=0;e<this.c.CONTROL_TOP;e++)Atomics.store(this.i32,e,this.c.STATUS_DISPOSED);for(let e=0;e<this.c.CONTROL_TOP;e++)Atomics.notify(this.i32,e,1/0);this.i32=null,this.u8=null,this._sab=null}}close(){this.destroy()}_create_write_queue(){let e,t,s=new Promise((i,r)=>{e=i,t=r});return{queue:[],finishWritePromise:s,finishWriteResolveFunc:e,finishWriteRejectFunc:t}}_json_to_chunks(e){let t=p.encode(JSON.stringify(e)),s=[],i=Math.ceil(t.length/this._max_chunk)||1;for(let r=0;r<i;r++){let o=t.subarray(r*this._max_chunk,(r+1)*this._max_chunk);s.push(o)}return s}async _asyncWrite(){if(!this.isWriter)throw new Error("Only writer can write");if(this._checkDisposed(),this._writing){n("_write: already in progress, returning");return}this._writing=!0;try{if(n("_write: waiting for can write"),await this._waitForCanWrite(),n("_write: can write now"),Atomics.load(this.i32,this.c.RW_SIGNAL)===this.c.STATUS_DISPOSED&&this._checkDisposed(),this._write_queue.queue.length===0)return;this._payload_in_progress=this._write_queue,this._payload_in_progress.chunks=this._json_to_chunks(this._payload_in_progress.queue),this._payload_in_progress.currentPart=0,this._write_queue=this._create_write_queue(),this._payload_in_progress.finishWritePromise.then(()=>{this._write_queue.queue.length>0&&this._asyncWrite().catch(()=>{})});let e=this._payload_in_progress.chunks.length;for(let s=0;s<e;s++){s>0&&(await this._waitForCanWrite(),Atomics.load(this.i32,this.c.RW_SIGNAL)===this.c.STATUS_DISPOSED&&this._checkDisposed());let i=this._payload_in_progress.chunks[s];this.u8.set(i,this.c.DATA_OFFSET),this.i32[this.c.NUM_PARTS]=e,this.i32[this.c.PART_INDEX]=s,this.i32[this.c.W_DATA_LEN]=i.length,n(`_write: signaling RW_CAN_READ, part ${s}/${e}`),Atomics.store(this.i32,this.c.RW_SIGNAL,this.c.RW_CAN_READ),Atomics.notify(this.i32,this.c.RW_SIGNAL,1)}n("_write: all parts sent, resolving promise");let t=this._payload_in_progress;this._payload_in_progress=null,t.finishWriteResolveFunc()}finally{this._writing=!1}}async _waitForCanWrite(){for(;;){let e=Atomics.load(this.i32,this.c.RW_SIGNAL);if(e===this.c.RW_CAN_WRITE)return;e===this.c.STATUS_DISPOSED&&this._checkDisposed();let t=Atomics.waitAsync(this.i32,this.c.RW_SIGNAL,e);t.async&&await t.value}}_read(e=!0,t=1/0){if(!this.isReader)throw new Error("Only reader can read");this._checkDisposed(),n(`_read: starting, blocking=${e}, timeout=${t}`);let s=[],i=1,r=!0;for(;;){let a=Atomics.load(this.i32,this.c.RW_SIGNAL);if(n(`_read: signal=${a}, RW_CAN_READ=${this.c.RW_CAN_READ}`),a===this.c.STATUS_DISPOSED&&this._checkDisposed(),a!==this.c.RW_CAN_READ){if(!e)return n("_read: non-blocking, no data"),!1;let w=r?t:1/0;n(`_read: entering Atomics.wait, signal=${a}, timeout=${w}`);let g=Atomics.wait(this.i32,this.c.RW_SIGNAL,a,w);if(n(`_read: Atomics.wait returned ${g}`),g==="timed-out")return n("_read: timed out"),!1;let f=Atomics.load(this.i32,this.c.RW_SIGNAL);if(n(`_read: after wake, newSignal=${f}`),f===this.c.STATUS_DISPOSED&&this._checkDisposed(),f!==this.c.RW_CAN_READ){n("_read: spurious wakeup, retrying");continue}}let h=this.i32[this.c.W_DATA_LEN];i=this.i32[this.c.NUM_PARTS];let _=this.i32[this.c.PART_INDEX],d=this.u8.slice(this.c.DATA_OFFSET,this.c.DATA_OFFSET+h);if(s.push(d),n(`_read: got part ${_}/${i}, len=${h}, signaling RW_CAN_WRITE`),Atomics.store(this.i32,this.c.RW_SIGNAL,this.c.RW_CAN_WRITE),Atomics.notify(this.i32,this.c.RW_SIGNAL,1),r=!1,_>=i-1){n("_read: last part received");break}}let o;if(s.length===1)o=s[0];else{let a=s.reduce((_,d)=>_+d.length,0);o=new Uint8Array(a);let h=0;for(let _ of s)o.set(_,h),h+=_.length}let l=JSON.parse(y.decode(o));return l.reverse(),this._read_queue=l.concat(this._read_queue),!0}async _waitForCanRead(e=1/0){let t=e===1/0?1/0:Date.now()+e;for(;;){let s=Atomics.load(this.i32,this.c.RW_SIGNAL);if(s===this.c.RW_CAN_READ)return!0;s===this.c.STATUS_DISPOSED&&this._checkDisposed();let i=t===1/0?1/0:Math.max(0,t-Date.now());if(i===0)return!1;let r=Atomics.waitAsync(this.i32,this.c.RW_SIGNAL,s);if(r.async){if(i===1/0)await r.value;else if(await Promise.race([r.value,new Promise(l=>setTimeout(()=>l("timed-out"),i))])==="timed-out")return!1}}}async _asyncRead(e=1/0){if(!this.isReader)throw new Error("Only reader can read");if(this._checkDisposed(),this._reading)return n("_asyncRead: already in progress, returning"),!1;this._reading=!0;try{n(`_asyncRead: starting, timeout=${e}`);let t=[],s=!0;for(;;){let o=s?e:1/0;if(n(`_asyncRead: waiting for can read, timeout=${o}`),!await this._waitForCanRead(o))return n("_asyncRead: timed out or no data"),!1;Atomics.load(this.i32,this.c.RW_SIGNAL)===this.c.STATUS_DISPOSED&&this._checkDisposed();let a=this.i32[this.c.W_DATA_LEN],h=this.i32[this.c.NUM_PARTS],_=this.i32[this.c.PART_INDEX],d=this.u8.slice(this.c.DATA_OFFSET,this.c.DATA_OFFSET+a);if(t.push(d),n(`_asyncRead: got part ${_}/${h}, len=${a}, signaling RW_CAN_WRITE`),Atomics.store(this.i32,this.c.RW_SIGNAL,this.c.RW_CAN_WRITE),Atomics.notify(this.i32,this.c.RW_SIGNAL,1),s=!1,_>=h-1){n("_asyncRead: last part received");break}}let i;if(t.length===1)i=t[0];else{let o=t.reduce((a,h)=>a+h.length,0);i=new Uint8Array(o);let l=0;for(let a of t)i.set(a,l),l+=a.length}let r=JSON.parse(y.decode(i));return r.reverse(),this._read_queue=r.concat(this._read_queue),!0}finally{this._reading=!1}}postMessage(e){if(!this.isWriter)throw new Error("Only writer can write");this._checkDisposed(),this._write_queue.queue.push(e);let t=this._write_queue.finishWritePromise;return this._asyncWrite().catch(()=>{}),t}read(e=1/0,t=!0,s=1){if(!this.isReader)throw new Error("Only reader can read");if(this._onmessage!==null)throw new Error("Cannot call read while onmessage is active");return this._checkDisposed(),this._read_queue.length>0?this._popMessages(s):(this._read(t,e),this._popMessages(s))}tryRead(e=1){return this.read(0,!1,e)}tryPeek(){if(!this.isReader)throw new Error("Only reader can peek");if(this._onmessage!==null)throw new Error("Cannot call tryPeek while onmessage is active");return this._checkDisposed(),this._read_queue.length===0&&this._read(!1,0),this._read_queue.length>0?this._read_queue[this._read_queue.length-1]:null}async asyncRead(e=1/0,t=1){if(!this.isReader)throw new Error("Only reader can read");if(this._onmessage!==null)throw new Error("Cannot call asyncRead while onmessage is active");return this._checkDisposed(),this._read_queue.length>0?this._popMessages(t):(await this._asyncRead(e),this._popMessages(t))}set onmessage(e){if(!this.isReader)throw new Error("Only reader can set onmessage");this._onmessage=e??null,e!==null&&!this._messageLoopActive&&this._messageLoop()}get onmessage(){return this._onmessage}async _messageLoop(){if(!this._messageLoopActive){this._messageLoopActive=!0;try{for(;this._onmessage!==null;){for(;this._read_queue.length>0&&this._onmessage!==null;){let e=this._read_queue.pop();try{this._onmessage({data:e})}catch{}}if(this._onmessage===null)break;await this._asyncRead()}}catch{}finally{this._messageLoopActive=!1}}}_popMessages(e){if(e===1)return this._read_queue.length>0?this._read_queue.pop():null;{let t=Math.min(e,this._read_queue.length);return t===0?[]:this._read_queue.splice(-t)}}},m=class c{constructor(e="a",t=256){if(e!=="a"&&e!=="b")throw new Error("side must be 'a' or 'b'");this._sab=typeof t=="number"?new SharedArrayBuffer(t*1024):t;let s=this._sab.byteLength/2;e==="a"?(this._writer=new u("w",this._sab,0,s),this._reader=new u("r",this._sab,s,s)):(this._reader=new u("r",this._sab,0,s),this._writer=new u("w",this._sab,s,s))}static from(e){if(e?.type!=="SABMessagePort")throw new Error("Not a SABMessagePort init message");return new c("b",e.buffer)}postInit(e=null,t={}){let s=[{type:"SABMessagePort",buffer:this._sab,...t},[this._sab]];if(e===null)return s;e.postMessage(...s)}postMessage(e){return this._writer.postMessage(e)}set onmessage(e){this._reader.onmessage=e}get onmessage(){return this._reader.onmessage}asyncRead(e,t){return this._reader.asyncRead(e,t)}read(e,t,s){return this._reader.read(e,t,s)}tryRead(e){return this._reader.tryRead(e)}tryPeek(){return this._reader.tryPeek()}close(){this._writer.destroy(),this._reader.destroy()}get buffer(){return this._sab}},A=class c{constructor(e,t=128){if(e!=="m"&&e!=="w")throw new Error("side must be 'm' or 'w'");this._side=e,this._mode="blocking",this._onmessage=null,this._pendingDrain=[],e==="m"&&(this._sab=new SharedArrayBuffer(t*1024),this._channel=new MessageChannel,this._nativePort=this._channel.port1,this._sabWriter=new u("w",this._sab))}static from(e){if(e?.type!=="MWChannel")throw new Error("Not a MWChannel init message");let t=new c("w");return t._sab=e.buffer,t._nativePort=e.port,t._sabReader=new u("r",t._sab),t._nativePort.start(),t}postInit(e=null,t={}){if(this._side!=="m")throw new Error("postInit is only for main side");let s=this._channel.port2,i={type:"MWChannel",buffer:this._sab,port:s,...t},r=[s];if(e===null)return[i,r];e.postMessage(i,r)}postMessage(e){if(this._side==="m"){if(this._mode==="blocking")return this._sabWriter.postMessage(e);this._nativePort.postMessage(e)}else this._nativePort.postMessage(e)}set onmessage(e){if(this._side==="m")this._onmessage=e,this._nativePort.onmessage=e;else{if(this._mode==="blocking")throw new Error("Cannot set onmessage in blocking mode \u2014 use read()/tryRead()");if(this._onmessage=e,e&&this._pendingDrain.length>0){let t=this._pendingDrain;this._pendingDrain=[];for(let s of t)try{e({data:s})}catch{}}this._nativePort.onmessage=e}}get onmessage(){return this._onmessage}read(e,t,s){if(this._side!=="w")throw new Error("read() is only for worker side");if(this._mode!=="blocking")throw new Error("read() only available in blocking mode");return this._sabReader.read(e,t,s)}tryRead(e){if(this._side!=="w")throw new Error("tryRead() is only for worker side");if(this._mode!=="blocking")throw new Error("tryRead() only available in blocking mode");return this._sabReader.tryRead(e)}tryPeek(){if(this._side!=="w")throw new Error("tryPeek() is only for worker side");if(this._mode!=="blocking")throw new Error("tryPeek() only available in blocking mode");return this._sabReader.tryPeek()}asyncRead(e,t){if(this._side!=="w")throw new Error("asyncRead() is only for worker side");if(this._mode!=="blocking")throw new Error("asyncRead() only available in blocking mode");return this._sabReader.asyncRead(e,t)}setMode(e){if(e!=="blocking"&&e!=="nonblocking")throw new Error("mode must be 'blocking' or 'nonblocking'");if(this._mode!==e)if(this._side==="m")this._mode=e;else if(e==="blocking")this._nativePort.onmessage=null,this._onmessage=null,this._mode="blocking";else{let t;for(;(t=this._sabReader.tryRead())!==null;)this._pendingDrain.push(t);this._mode="nonblocking"}}close(){this._side==="m"?this._sabWriter.destroy():this._sabReader.destroy(),this._nativePort.close()}get buffer(){return this._sab}};export{A as MWChannel,m as SABMessagePort,u as SABPipe};
|
|
1
|
+
var A=new TextEncoder,m=new TextDecoder,R=!1,n=R?(...u)=>console.log("[SABPipe]",...u):()=>{},l=class u{static STATUS=0;static RW_SIGNAL=1;static W_DATA_LEN=2;static NUM_PARTS=3;static PART_INDEX=4;static RESERVED_1=5;static RESERVED_2=6;static RESERVED_3=7;static CONTROL_TOP=8;static DATA_OFFSET=32;static STATUS_ACTIVE=0;static STATUS_DISPOSED=-1;static RW_CAN_WRITE=0;static RW_CAN_READ=1;constructor(e,t=131072,i=0,s=null,r=null){this._sab=typeof t=="number"?new SharedArrayBuffer(t):t;let h=s??this._sab.byteLength-i;if(this.i32=new Int32Array(this._sab,i,h>>2),this.u8=new Uint8Array(this._sab,i,h),this.maxChunk=h-u.DATA_OFFSET,e==="w")this.isWriter=!0,this.isReader=!1,this._write_queue=this._create_write_queue(),this._read_queue=null,this._payload_in_progress=null,this._writing=!1;else if(e==="r")this.isWriter=!1,this.isReader=!0,this._read_queue=[],this._write_queue=null,this._reading=!1,this._onmessage=null,this._messageLoopActive=!1;else throw new Error("Invalid role parameter: must be 'r' or 'w'");this._queueLimit=r,this._onQueueOverflow=null,this._max_chunk=h-u.DATA_OFFSET}c=this.constructor;isDisposed(){return this.i32===null||this.i32[this.c.STATUS]===this.c.STATUS_DISPOSED}_checkDisposed(){if(this.isDisposed())throw this.i32=null,this.u8=null,this._sab=null,new Error("SABPipe disposed")}destroy(){if(this.i32!==null){for(let e=0;e<this.c.CONTROL_TOP;e++)Atomics.store(this.i32,e,this.c.STATUS_DISPOSED);for(let e=0;e<this.c.CONTROL_TOP;e++)Atomics.notify(this.i32,e,1/0);this.i32=null,this.u8=null,this._sab=null}}close(){this.destroy()}get queueLimit(){return this._queueLimit}get onQueueOverflow(){return this._onQueueOverflow}set onQueueOverflow(e){this._onQueueOverflow=e??null}set queueLimit(e){if(e!==null&&(!Number.isInteger(e)||e<0))throw new Error("queueLimit must be null or a non-negative integer");this._queueLimit=e,e!==null&&(this.isWriter&&this._write_queue.queue.length>e&&(this._onQueueOverflow&&this._onQueueOverflow(this._write_queue.queue),this._write_queue.queue.length>e&&this._write_queue.queue.splice(0,this._write_queue.queue.length-e)),this.isReader&&this._read_queue&&this._read_queue.length>e&&(this._onQueueOverflow&&this._onQueueOverflow(this._read_queue),this._read_queue.length>e&&(this._read_queue.length=e)))}_enforceQueueLimit(){this._queueLimit!==null&&this._read_queue.length>this._queueLimit&&(this._onQueueOverflow&&this._onQueueOverflow(this._read_queue),this._read_queue.length>this._queueLimit&&(this._read_queue.length=this._queueLimit))}_create_write_queue(){let e,t,i=new Promise((s,r)=>{e=s,t=r});return{queue:[],finishWritePromise:i,finishWriteResolveFunc:e,finishWriteRejectFunc:t}}_json_to_chunks(e){let t=A.encode(JSON.stringify(e)),i=[],s=Math.ceil(t.length/this._max_chunk)||1;for(let r=0;r<s;r++){let h=t.subarray(r*this._max_chunk,(r+1)*this._max_chunk);i.push(h)}return i}async _asyncWrite(){if(!this.isWriter)throw new Error("Only writer can write");if(this._checkDisposed(),this._writing){n("_write: already in progress, returning");return}this._writing=!0;try{if(n("_write: waiting for can write"),await this._waitForCanWrite(),n("_write: can write now"),Atomics.load(this.i32,this.c.RW_SIGNAL)===this.c.STATUS_DISPOSED&&this._checkDisposed(),this._write_queue.queue.length===0)return;this._payload_in_progress=this._write_queue,this._payload_in_progress.chunks=this._json_to_chunks(this._payload_in_progress.queue),this._payload_in_progress.currentPart=0,this._write_queue=this._create_write_queue(),this._payload_in_progress.finishWritePromise.then(()=>{this._write_queue.queue.length>0&&this._asyncWrite().catch(()=>{})});let e=this._payload_in_progress.chunks.length;for(let i=0;i<e;i++){i>0&&(await this._waitForCanWrite(),Atomics.load(this.i32,this.c.RW_SIGNAL)===this.c.STATUS_DISPOSED&&this._checkDisposed());let s=this._payload_in_progress.chunks[i];this.u8.set(s,this.c.DATA_OFFSET),this.i32[this.c.NUM_PARTS]=e,this.i32[this.c.PART_INDEX]=i,this.i32[this.c.W_DATA_LEN]=s.length,n(`_write: signaling RW_CAN_READ, part ${i}/${e}`),Atomics.store(this.i32,this.c.RW_SIGNAL,this.c.RW_CAN_READ),Atomics.notify(this.i32,this.c.RW_SIGNAL,1)}n("_write: all parts sent, resolving promise");let t=this._payload_in_progress;this._payload_in_progress=null,t.finishWriteResolveFunc()}finally{this._writing=!1}}async _waitForCanWrite(){for(;;){let e=Atomics.load(this.i32,this.c.RW_SIGNAL);if(e===this.c.RW_CAN_WRITE)return;e===this.c.STATUS_DISPOSED&&this._checkDisposed();let t=Atomics.waitAsync(this.i32,this.c.RW_SIGNAL,e);t.async&&await t.value}}_read(e=!0,t=1/0){if(!this.isReader)throw new Error("Only reader can read");this._checkDisposed(),n(`_read: starting, blocking=${e}, timeout=${t}`);let i=[],s=1,r=!0;for(;;){let o=Atomics.load(this.i32,this.c.RW_SIGNAL);if(n(`_read: signal=${o}, RW_CAN_READ=${this.c.RW_CAN_READ}`),o===this.c.STATUS_DISPOSED&&this._checkDisposed(),o!==this.c.RW_CAN_READ){if(!e)return n("_read: non-blocking, no data"),!1;let w=r?t:1/0;n(`_read: entering Atomics.wait, signal=${o}, timeout=${w}`);let g=Atomics.wait(this.i32,this.c.RW_SIGNAL,o,w);if(n(`_read: Atomics.wait returned ${g}`),g==="timed-out")return n("_read: timed out"),!1;let f=Atomics.load(this.i32,this.c.RW_SIGNAL);if(n(`_read: after wake, newSignal=${f}`),f===this.c.STATUS_DISPOSED&&this._checkDisposed(),f!==this.c.RW_CAN_READ){n("_read: spurious wakeup, retrying");continue}}let a=this.i32[this.c.W_DATA_LEN];s=this.i32[this.c.NUM_PARTS];let _=this.i32[this.c.PART_INDEX],d=this.u8.slice(this.c.DATA_OFFSET,this.c.DATA_OFFSET+a);if(i.push(d),n(`_read: got part ${_}/${s}, len=${a}, signaling RW_CAN_WRITE`),Atomics.store(this.i32,this.c.RW_SIGNAL,this.c.RW_CAN_WRITE),Atomics.notify(this.i32,this.c.RW_SIGNAL,1),r=!1,_>=s-1){n("_read: last part received");break}}let h;if(i.length===1)h=i[0];else{let o=i.reduce((_,d)=>_+d.length,0);h=new Uint8Array(o);let a=0;for(let _ of i)h.set(_,a),a+=_.length}let c=JSON.parse(m.decode(h));return c.reverse(),this._read_queue=c.concat(this._read_queue),this._enforceQueueLimit(),!0}async _waitForCanRead(e=1/0){let t=e===1/0?1/0:Date.now()+e;for(;;){let i=Atomics.load(this.i32,this.c.RW_SIGNAL);if(i===this.c.RW_CAN_READ)return!0;i===this.c.STATUS_DISPOSED&&this._checkDisposed();let s=t===1/0?1/0:Math.max(0,t-Date.now());if(s===0)return!1;let r=Atomics.waitAsync(this.i32,this.c.RW_SIGNAL,i);if(r.async){if(s===1/0)await r.value;else if(await Promise.race([r.value,new Promise(c=>setTimeout(()=>c("timed-out"),s))])==="timed-out")return!1}}}async _asyncRead(e=1/0){if(!this.isReader)throw new Error("Only reader can read");if(this._checkDisposed(),this._reading)return n("_asyncRead: already in progress, returning"),!1;this._reading=!0;try{n(`_asyncRead: starting, timeout=${e}`);let t=[],i=!0;for(;;){let h=i?e:1/0;if(n(`_asyncRead: waiting for can read, timeout=${h}`),!await this._waitForCanRead(h))return n("_asyncRead: timed out or no data"),!1;Atomics.load(this.i32,this.c.RW_SIGNAL)===this.c.STATUS_DISPOSED&&this._checkDisposed();let o=this.i32[this.c.W_DATA_LEN],a=this.i32[this.c.NUM_PARTS],_=this.i32[this.c.PART_INDEX],d=this.u8.slice(this.c.DATA_OFFSET,this.c.DATA_OFFSET+o);if(t.push(d),n(`_asyncRead: got part ${_}/${a}, len=${o}, signaling RW_CAN_WRITE`),Atomics.store(this.i32,this.c.RW_SIGNAL,this.c.RW_CAN_WRITE),Atomics.notify(this.i32,this.c.RW_SIGNAL,1),i=!1,_>=a-1){n("_asyncRead: last part received");break}}let s;if(t.length===1)s=t[0];else{let h=t.reduce((o,a)=>o+a.length,0);s=new Uint8Array(h);let c=0;for(let o of t)s.set(o,c),c+=o.length}let r=JSON.parse(m.decode(s));return r.reverse(),this._read_queue=r.concat(this._read_queue),this._enforceQueueLimit(),!0}finally{this._reading=!1}}postMessage(e){if(!this.isWriter)throw new Error("Only writer can write");this._checkDisposed(),this._write_queue.queue.push(e),this._queueLimit!==null&&this._write_queue.queue.length>this._queueLimit&&(this._onQueueOverflow&&this._onQueueOverflow(this._write_queue.queue),this._write_queue.queue.length>this._queueLimit&&this._write_queue.queue.splice(0,this._write_queue.queue.length-this._queueLimit));let t=this._write_queue.finishWritePromise;return this._asyncWrite().catch(()=>{}),t}read(e=1/0,t=!0,i=1){if(!this.isReader)throw new Error("Only reader can read");if(this._onmessage!==null)throw new Error("Cannot call read while onmessage is active");return this._checkDisposed(),this._read_queue.length>0?this._popMessages(i):(this._read(t,e),this._popMessages(i))}tryRead(e=1){return this.read(0,!1,e)}tryPeek(){if(!this.isReader)throw new Error("Only reader can peek");if(this._onmessage!==null)throw new Error("Cannot call tryPeek while onmessage is active");return this._checkDisposed(),this._read_queue.length===0&&this._read(!1,0),this._read_queue.length>0?this._read_queue[this._read_queue.length-1]:null}async asyncRead(e=1/0,t=1){if(!this.isReader)throw new Error("Only reader can read");if(this._onmessage!==null)throw new Error("Cannot call asyncRead while onmessage is active");return this._checkDisposed(),this._read_queue.length>0?this._popMessages(t):(await this._asyncRead(e),this._popMessages(t))}set onmessage(e){if(!this.isReader)throw new Error("Only reader can set onmessage");this._onmessage=e??null,e!==null&&!this._messageLoopActive&&this._messageLoop()}get onmessage(){return this._onmessage}async _messageLoop(){if(!this._messageLoopActive){this._messageLoopActive=!0;try{for(;this._onmessage!==null;){for(;this._read_queue.length>0&&this._onmessage!==null;){let e=this._read_queue.pop();try{this._onmessage({data:e})}catch{}}if(this._onmessage===null)break;await this._asyncRead()}}catch{}finally{this._messageLoopActive=!1}}}_popMessages(e){if(e===1)return this._read_queue.length>0?this._read_queue.pop():null;{let t=Math.min(e,this._read_queue.length);return t===0?[]:this._read_queue.splice(-t)}}},p=class u{constructor(e="a",t=256,i=null){if(e!=="a"&&e!=="b")throw new Error("side must be 'a' or 'b'");this._sab=typeof t=="number"?new SharedArrayBuffer(t*1024):t;let s=this._sab.byteLength/2;e==="a"?(this._writer=new l("w",this._sab,0,s,i),this._reader=new l("r",this._sab,s,s,i)):(this._reader=new l("r",this._sab,0,s,i),this._writer=new l("w",this._sab,s,s,i))}static from(e,t=null){if(e?.type!=="SABMessagePort")throw new Error("Not a SABMessagePort init message");return new u("b",e.buffer,t)}postInit(e=null,t={}){let i=[{type:"SABMessagePort",buffer:this._sab,...t},[this._sab]];if(e===null)return i;e.postMessage(...i)}postMessage(e){return this._writer.postMessage(e)}set onmessage(e){this._reader.onmessage=e}get onmessage(){return this._reader.onmessage}asyncRead(e,t){return this._reader.asyncRead(e,t)}read(e,t,i){return this._reader.read(e,t,i)}tryRead(e){return this._reader.tryRead(e)}tryPeek(){return this._reader.tryPeek()}get queueLimit(){return this._writer.queueLimit}set queueLimit(e){this._writer.queueLimit=e,this._reader.queueLimit=e}get onQueueOverflow(){return this._writer.onQueueOverflow}set onQueueOverflow(e){this._writer.onQueueOverflow=e,this._reader.onQueueOverflow=e}close(){this._writer.destroy(),this._reader.destroy()}get buffer(){return this._sab}},y=class u{constructor(e,t=128,i=null){if(e!=="m"&&e!=="w")throw new Error("side must be 'm' or 'w'");this._side=e,this._mode="blocking",this._onmessage=null,this._pendingDrain=[],this._queueLimit=i,this._onQueueOverflow=null,e==="m"&&(this._sab=new SharedArrayBuffer(t*1024),this._channel=new MessageChannel,this._nativePort=this._channel.port1,this._sabWriter=new l("w",this._sab,void 0,void 0,i))}static from(e,t=null){if(e?.type!=="MWChannel")throw new Error("Not a MWChannel init message");let i=new u("w");return i._queueLimit=t,i._sab=e.buffer,i._nativePort=e.port,i._sabReader=new l("r",i._sab,void 0,void 0,t),i._nativePort.start(),i}postInit(e=null,t={}){if(this._side!=="m")throw new Error("postInit is only for main side");let i=this._channel.port2,s={type:"MWChannel",buffer:this._sab,port:i,...t},r=[i];if(e===null)return[s,r];e.postMessage(s,r)}postMessage(e){if(this._side==="m"){if(this._mode==="blocking")return this._sabWriter.postMessage(e);this._nativePort.postMessage(e)}else this._nativePort.postMessage(e)}set onmessage(e){if(this._side==="m")this._onmessage=e,this._nativePort.onmessage=e;else{if(this._mode==="blocking")throw new Error("Cannot set onmessage in blocking mode \u2014 use read()/tryRead()");if(this._onmessage=e,e&&this._pendingDrain.length>0){let t=this._pendingDrain;this._pendingDrain=[];for(let i of t)try{e({data:i})}catch{}}this._nativePort.onmessage=e}}get onmessage(){return this._onmessage}read(e,t,i){if(this._side!=="w")throw new Error("read() is only for worker side");if(this._mode!=="blocking")throw new Error("read() only available in blocking mode");return this._sabReader.read(e,t,i)}tryRead(e){if(this._side!=="w")throw new Error("tryRead() is only for worker side");if(this._mode!=="blocking")throw new Error("tryRead() only available in blocking mode");return this._sabReader.tryRead(e)}tryPeek(){if(this._side!=="w")throw new Error("tryPeek() is only for worker side");if(this._mode!=="blocking")throw new Error("tryPeek() only available in blocking mode");return this._sabReader.tryPeek()}asyncRead(e,t){if(this._side!=="w")throw new Error("asyncRead() is only for worker side");if(this._mode!=="blocking")throw new Error("asyncRead() only available in blocking mode");return this._sabReader.asyncRead(e,t)}setMode(e){if(e!=="blocking"&&e!=="nonblocking")throw new Error("mode must be 'blocking' or 'nonblocking'");if(this._mode!==e)if(this._side==="m")this._mode=e;else if(e==="blocking")this._nativePort.onmessage=null,this._onmessage=null,this._mode="blocking";else{let t;for(;(t=this._sabReader.tryRead())!==null;)this._pendingDrain.push(t);this._queueLimit!==null&&this._pendingDrain.length>this._queueLimit&&(this._onQueueOverflow&&this._onQueueOverflow(this._pendingDrain),this._pendingDrain.length>this._queueLimit&&this._pendingDrain.splice(0,this._pendingDrain.length-this._queueLimit)),this._mode="nonblocking"}}get queueLimit(){return this._side==="m"?this._sabWriter.queueLimit:this._sabReader.queueLimit}set queueLimit(e){if(e!==null&&(!Number.isInteger(e)||e<0))throw new Error("queueLimit must be null or a non-negative integer");this._queueLimit=e,this._side==="m"?this._sabWriter.queueLimit=e:this._sabReader.queueLimit=e,e!==null&&this._pendingDrain.length>e&&(this._onQueueOverflow&&this._onQueueOverflow(this._pendingDrain),this._pendingDrain.length>e&&this._pendingDrain.splice(0,this._pendingDrain.length-e))}get onQueueOverflow(){return this._onQueueOverflow}set onQueueOverflow(e){this._onQueueOverflow=e??null,this._side==="m"?this._sabWriter.onQueueOverflow=e:this._sabReader&&(this._sabReader.onQueueOverflow=e)}close(){this._side==="m"?this._sabWriter.destroy():this._sabReader.destroy(),this._nativePort.close()}get buffer(){return this._sab}};export{y as MWChannel,p as SABMessagePort,l as SABPipe};
|
package/package.json
CHANGED
package/src/SABMessagePort.js
CHANGED
|
@@ -140,7 +140,7 @@ export class SABPipe {
|
|
|
140
140
|
static RW_CAN_READ = 1;
|
|
141
141
|
|
|
142
142
|
|
|
143
|
-
constructor(role, sabOrSize = 131072, byteOffset = 0, sectionSize = null) { // 128 KB default
|
|
143
|
+
constructor(role, sabOrSize = 131072, byteOffset = 0, sectionSize = null, queueLimit = null) { // 128 KB default
|
|
144
144
|
this._sab = typeof sabOrSize === 'number'
|
|
145
145
|
? new SharedArrayBuffer(sabOrSize)
|
|
146
146
|
: sabOrSize;
|
|
@@ -164,6 +164,8 @@ export class SABPipe {
|
|
|
164
164
|
this._onmessage = null;
|
|
165
165
|
this._messageLoopActive = false;
|
|
166
166
|
} else throw new Error("Invalid role parameter: must be 'r' or 'w'");
|
|
167
|
+
this._queueLimit = queueLimit;
|
|
168
|
+
this._onQueueOverflow = null;
|
|
167
169
|
this._max_chunk = size - SABPipe.DATA_OFFSET;
|
|
168
170
|
}
|
|
169
171
|
|
|
@@ -202,6 +204,48 @@ export class SABPipe {
|
|
|
202
204
|
this.destroy();
|
|
203
205
|
}
|
|
204
206
|
|
|
207
|
+
get queueLimit() {
|
|
208
|
+
return this._queueLimit;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
get onQueueOverflow() {
|
|
212
|
+
return this._onQueueOverflow;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
set onQueueOverflow(handler) {
|
|
216
|
+
this._onQueueOverflow = handler ?? null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
set queueLimit(value) {
|
|
220
|
+
if (value !== null && (!Number.isInteger(value) || value < 0)) {
|
|
221
|
+
throw new Error('queueLimit must be null or a non-negative integer');
|
|
222
|
+
}
|
|
223
|
+
this._queueLimit = value;
|
|
224
|
+
if (value !== null) {
|
|
225
|
+
if (this.isWriter && this._write_queue.queue.length > value) {
|
|
226
|
+
if (this._onQueueOverflow) this._onQueueOverflow(this._write_queue.queue);
|
|
227
|
+
if (this._write_queue.queue.length > value) {
|
|
228
|
+
this._write_queue.queue.splice(0, this._write_queue.queue.length - value);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (this.isReader && this._read_queue && this._read_queue.length > value) {
|
|
232
|
+
if (this._onQueueOverflow) this._onQueueOverflow(this._read_queue);
|
|
233
|
+
if (this._read_queue.length > value) {
|
|
234
|
+
this._read_queue.length = value;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_enforceQueueLimit() {
|
|
241
|
+
if (this._queueLimit !== null && this._read_queue.length > this._queueLimit) {
|
|
242
|
+
if (this._onQueueOverflow) this._onQueueOverflow(this._read_queue);
|
|
243
|
+
if (this._read_queue.length > this._queueLimit) {
|
|
244
|
+
this._read_queue.length = this._queueLimit;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
205
249
|
_create_write_queue() {
|
|
206
250
|
let resolveFunc, rejectFunc;
|
|
207
251
|
const finishWritePromise = new Promise((resolve, reject) => {
|
|
@@ -440,6 +484,7 @@ export class SABPipe {
|
|
|
440
484
|
|
|
441
485
|
// Prepend to read_queue (new messages at front, oldest at end)
|
|
442
486
|
this._read_queue = messages.concat(this._read_queue);
|
|
487
|
+
this._enforceQueueLimit();
|
|
443
488
|
|
|
444
489
|
return true;
|
|
445
490
|
}
|
|
@@ -570,6 +615,7 @@ export class SABPipe {
|
|
|
570
615
|
const messages = JSON.parse(_decoder.decode(payloadBytes));
|
|
571
616
|
messages.reverse();
|
|
572
617
|
this._read_queue = messages.concat(this._read_queue);
|
|
618
|
+
this._enforceQueueLimit();
|
|
573
619
|
|
|
574
620
|
return true;
|
|
575
621
|
} finally {
|
|
@@ -594,6 +640,14 @@ export class SABPipe {
|
|
|
594
640
|
// Push message to queue
|
|
595
641
|
this._write_queue.queue.push(jsonMessage);
|
|
596
642
|
|
|
643
|
+
// Enforce queue limit — discard oldest (front) messages
|
|
644
|
+
if (this._queueLimit !== null && this._write_queue.queue.length > this._queueLimit) {
|
|
645
|
+
if (this._onQueueOverflow) this._onQueueOverflow(this._write_queue.queue);
|
|
646
|
+
if (this._write_queue.queue.length > this._queueLimit) {
|
|
647
|
+
this._write_queue.queue.splice(0, this._write_queue.queue.length - this._queueLimit);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
597
651
|
// Save promise BEFORE _asyncWrite() might replace _write_queue
|
|
598
652
|
const promise = this._write_queue.finishWritePromise;
|
|
599
653
|
|
|
@@ -753,7 +807,7 @@ export class SABPipe {
|
|
|
753
807
|
|
|
754
808
|
export class SABMessagePort {
|
|
755
809
|
|
|
756
|
-
constructor(side = 'a', sabOrSizeKB = 256) {
|
|
810
|
+
constructor(side = 'a', sabOrSizeKB = 256, queueLimit = null) {
|
|
757
811
|
if (side !== 'a' && side !== 'b') throw new Error("side must be 'a' or 'b'");
|
|
758
812
|
|
|
759
813
|
this._sab = (typeof sabOrSizeKB === 'number')
|
|
@@ -763,19 +817,19 @@ export class SABMessagePort {
|
|
|
763
817
|
const sectionSize = this._sab.byteLength / 2;
|
|
764
818
|
|
|
765
819
|
if (side === 'a') {
|
|
766
|
-
this._writer = new SABPipe('w', this._sab, 0, sectionSize);
|
|
767
|
-
this._reader = new SABPipe('r', this._sab, sectionSize, sectionSize);
|
|
820
|
+
this._writer = new SABPipe('w', this._sab, 0, sectionSize, queueLimit);
|
|
821
|
+
this._reader = new SABPipe('r', this._sab, sectionSize, sectionSize, queueLimit);
|
|
768
822
|
} else {
|
|
769
|
-
this._reader = new SABPipe('r', this._sab, 0, sectionSize);
|
|
770
|
-
this._writer = new SABPipe('w', this._sab, sectionSize, sectionSize);
|
|
823
|
+
this._reader = new SABPipe('r', this._sab, 0, sectionSize, queueLimit);
|
|
824
|
+
this._writer = new SABPipe('w', this._sab, sectionSize, sectionSize, queueLimit);
|
|
771
825
|
}
|
|
772
826
|
}
|
|
773
827
|
|
|
774
|
-
static from(initMsg) {
|
|
828
|
+
static from(initMsg, queueLimit = null) {
|
|
775
829
|
if (initMsg?.type !== 'SABMessagePort') {
|
|
776
830
|
throw new Error('Not a SABMessagePort init message');
|
|
777
831
|
}
|
|
778
|
-
return new SABMessagePort('b', initMsg.buffer);
|
|
832
|
+
return new SABMessagePort('b', initMsg.buffer, queueLimit);
|
|
779
833
|
}
|
|
780
834
|
|
|
781
835
|
postInit(target = null, extraProps = {}) {
|
|
@@ -810,6 +864,24 @@ export class SABMessagePort {
|
|
|
810
864
|
return this._reader.tryPeek();
|
|
811
865
|
}
|
|
812
866
|
|
|
867
|
+
get queueLimit() {
|
|
868
|
+
return this._writer.queueLimit;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
set queueLimit(value) {
|
|
872
|
+
this._writer.queueLimit = value;
|
|
873
|
+
this._reader.queueLimit = value;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
get onQueueOverflow() {
|
|
877
|
+
return this._writer.onQueueOverflow;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
set onQueueOverflow(handler) {
|
|
881
|
+
this._writer.onQueueOverflow = handler;
|
|
882
|
+
this._reader.onQueueOverflow = handler;
|
|
883
|
+
}
|
|
884
|
+
|
|
813
885
|
close() {
|
|
814
886
|
this._writer.destroy();
|
|
815
887
|
this._reader.destroy();
|
|
@@ -838,18 +910,20 @@ export class MWChannel {
|
|
|
838
910
|
* @param {'m'|'w'} side - 'm' (main thread) or 'w' (worker thread)
|
|
839
911
|
* @param {number} sabSizeKB - SABPipe buffer size in KB (default 128). Main side only.
|
|
840
912
|
*/
|
|
841
|
-
constructor(side, sabSizeKB = 128) {
|
|
913
|
+
constructor(side, sabSizeKB = 128, queueLimit = null) {
|
|
842
914
|
if (side !== 'm' && side !== 'w') throw new Error("side must be 'm' or 'w'");
|
|
843
915
|
this._side = side;
|
|
844
916
|
this._mode = 'blocking'; // default: worker starts in blocking mode
|
|
845
917
|
this._onmessage = null;
|
|
846
918
|
this._pendingDrain = [];
|
|
919
|
+
this._queueLimit = queueLimit;
|
|
920
|
+
this._onQueueOverflow = null;
|
|
847
921
|
|
|
848
922
|
if (side === 'm') {
|
|
849
923
|
this._sab = new SharedArrayBuffer(sabSizeKB * 1024);
|
|
850
924
|
this._channel = new MessageChannel();
|
|
851
925
|
this._nativePort = this._channel.port1;
|
|
852
|
-
this._sabWriter = new SABPipe('w', this._sab);
|
|
926
|
+
this._sabWriter = new SABPipe('w', this._sab, undefined, undefined, queueLimit);
|
|
853
927
|
}
|
|
854
928
|
// Worker side: _sab, _nativePort, _sabReader initialized by from()
|
|
855
929
|
}
|
|
@@ -858,14 +932,15 @@ export class MWChannel {
|
|
|
858
932
|
* Creates the worker side from a received init message.
|
|
859
933
|
* @param {object} initMsg - Must have type='MWChannel', buffer, port
|
|
860
934
|
*/
|
|
861
|
-
static from(initMsg) {
|
|
935
|
+
static from(initMsg, queueLimit = null) {
|
|
862
936
|
if (initMsg?.type !== 'MWChannel') {
|
|
863
937
|
throw new Error('Not a MWChannel init message');
|
|
864
938
|
}
|
|
865
939
|
const mw = new MWChannel('w');
|
|
940
|
+
mw._queueLimit = queueLimit;
|
|
866
941
|
mw._sab = initMsg.buffer;
|
|
867
942
|
mw._nativePort = initMsg.port;
|
|
868
|
-
mw._sabReader = new SABPipe('r', mw._sab);
|
|
943
|
+
mw._sabReader = new SABPipe('r', mw._sab, undefined, undefined, queueLimit);
|
|
869
944
|
mw._nativePort.start();
|
|
870
945
|
return mw;
|
|
871
946
|
}
|
|
@@ -992,11 +1067,53 @@ export class MWChannel {
|
|
|
992
1067
|
while ((msg = this._sabReader.tryRead()) !== null) {
|
|
993
1068
|
this._pendingDrain.push(msg);
|
|
994
1069
|
}
|
|
1070
|
+
if (this._queueLimit !== null && this._pendingDrain.length > this._queueLimit) {
|
|
1071
|
+
if (this._onQueueOverflow) this._onQueueOverflow(this._pendingDrain);
|
|
1072
|
+
if (this._pendingDrain.length > this._queueLimit) {
|
|
1073
|
+
this._pendingDrain.splice(0, this._pendingDrain.length - this._queueLimit);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
995
1076
|
this._mode = 'nonblocking';
|
|
996
1077
|
}
|
|
997
1078
|
}
|
|
998
1079
|
}
|
|
999
1080
|
|
|
1081
|
+
get queueLimit() {
|
|
1082
|
+
if (this._side === 'm') return this._sabWriter.queueLimit;
|
|
1083
|
+
return this._sabReader.queueLimit;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
set queueLimit(value) {
|
|
1087
|
+
if (value !== null && (!Number.isInteger(value) || value < 0)) {
|
|
1088
|
+
throw new Error('queueLimit must be null or a non-negative integer');
|
|
1089
|
+
}
|
|
1090
|
+
this._queueLimit = value;
|
|
1091
|
+
if (this._side === 'm') {
|
|
1092
|
+
this._sabWriter.queueLimit = value;
|
|
1093
|
+
} else {
|
|
1094
|
+
this._sabReader.queueLimit = value;
|
|
1095
|
+
}
|
|
1096
|
+
if (value !== null && this._pendingDrain.length > value) {
|
|
1097
|
+
if (this._onQueueOverflow) this._onQueueOverflow(this._pendingDrain);
|
|
1098
|
+
if (this._pendingDrain.length > value) {
|
|
1099
|
+
this._pendingDrain.splice(0, this._pendingDrain.length - value);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
get onQueueOverflow() {
|
|
1105
|
+
return this._onQueueOverflow;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
set onQueueOverflow(handler) {
|
|
1109
|
+
this._onQueueOverflow = handler ?? null;
|
|
1110
|
+
if (this._side === 'm') {
|
|
1111
|
+
this._sabWriter.onQueueOverflow = handler;
|
|
1112
|
+
} else if (this._sabReader) {
|
|
1113
|
+
this._sabReader.onQueueOverflow = handler;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1000
1117
|
/**
|
|
1001
1118
|
* Close the channel. Destroys SABPipe and closes native MessagePort.
|
|
1002
1119
|
*/
|