sab-message-port 1.0.0 → 1.0.2
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 +201 -1
- package/dist/SABMessagePort.min.js +1 -1
- package/package.json +1 -1
- package/src/SABMessagePort.js +213 -0
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm install sab-message-port
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
```javascript
|
|
20
|
-
import { SABMessagePort, SABPipe } from 'sab-message-port';
|
|
20
|
+
import { SABMessagePort, SABPipe, MWChannel } from 'sab-message-port';
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## Quick Start
|
|
@@ -285,6 +285,10 @@ const msg = port.tryRead(); // single message or null
|
|
|
285
285
|
const msgs = port.tryRead(10); // up to 10 messages (array, newest first) or []
|
|
286
286
|
```
|
|
287
287
|
|
|
288
|
+
### `port.tryPeek()` → message | null
|
|
289
|
+
|
|
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
|
+
|
|
288
292
|
### `port.close()`
|
|
289
293
|
|
|
290
294
|
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).
|
|
@@ -362,6 +366,15 @@ Async read using `Atomics.waitAsync`. **Safe on the main thread.**
|
|
|
362
366
|
|
|
363
367
|
Non-blocking read. Equivalent to `reader.read(0, false, maxMessages)`. Returns immediately.
|
|
364
368
|
|
|
369
|
+
#### `reader.tryPeek()` → message | null
|
|
370
|
+
|
|
371
|
+
Non-blocking peek. Returns the next message that would be returned by `read()` or `tryRead()`, **without removing it from the queue**. If the internal queue is empty, attempts a non-blocking read from the shared buffer first. Returns `null` if no data is available.
|
|
372
|
+
|
|
373
|
+
```javascript
|
|
374
|
+
const msg = reader.tryPeek(); // peek at next message, or null
|
|
375
|
+
const same = reader.tryRead(); // consumes the same message
|
|
376
|
+
```
|
|
377
|
+
|
|
365
378
|
#### `reader.onmessage`
|
|
366
379
|
|
|
367
380
|
Event-driven handler. Setting a function starts a continuous async read loop; setting `null` stops it. Handler receives `{ data: message }`. See [`SABMessagePort.onmessage`](#portonmessage) for full behavior details (mutual exclusion, error resilience, re-assignment).
|
|
@@ -378,6 +391,163 @@ Returns `true` if the pipe has been disposed (by either side).
|
|
|
378
391
|
|
|
379
392
|
---
|
|
380
393
|
|
|
394
|
+
## MWChannel
|
|
395
|
+
|
|
396
|
+
A hybrid channel that uses the best transport for each direction: **native `MessagePort`** for worker→main (faster, no SharedArrayBuffer overhead) and **SABPipe** for main→worker (enables blocking reads). The worker can also switch to receiving via `MessagePort` when blocking reads aren't needed.
|
|
397
|
+
|
|
398
|
+
`SABMessagePort` uses SABPipe in both directions — which means worker→main messages pay the SABPipe serialization cost even though the worker never needs to block on outgoing messages. `MWChannel` avoids this by using native `postMessage` for the worker→main direction, where it's typically faster, while keeping SABPipe for the main→worker direction where blocking reads are the whole point.
|
|
399
|
+
|
|
400
|
+
The worker can also switch between blocking (SABPipe) and non-blocking (MessagePort) receive modes at runtime for the main→worker direction.
|
|
401
|
+
|
|
402
|
+
**Key design:**
|
|
403
|
+
- **Worker→main:** Always uses native `MessagePort` (faster, no SABPipe overhead).
|
|
404
|
+
- **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
|
+
- The main thread **always receives via native `MessagePort`** and never blocks.
|
|
406
|
+
|
|
407
|
+
### `new MWChannel(side, sabSizeKB = 128)`
|
|
408
|
+
|
|
409
|
+
Creates a new channel.
|
|
410
|
+
|
|
411
|
+
| Parameter | Default | Description |
|
|
412
|
+
|-----------|---------|-------------|
|
|
413
|
+
| `side` | (required) | `'m'` (main thread) or `'w'` (worker). Throws if invalid. |
|
|
414
|
+
| `sabSizeKB` | `128` | SABPipe buffer size in KB. Main side only. |
|
|
415
|
+
|
|
416
|
+
```javascript
|
|
417
|
+
const port = new MWChannel('m'); // 128 KB SABPipe buffer
|
|
418
|
+
const port = new MWChannel('m', 256); // 256 KB SABPipe buffer
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### `MWChannel.from(initMsg)`
|
|
422
|
+
|
|
423
|
+
Creates the worker side from a received init message. The init message must have `type: 'MWChannel'`.
|
|
424
|
+
|
|
425
|
+
The worker starts in **blocking mode** by default.
|
|
426
|
+
|
|
427
|
+
```javascript
|
|
428
|
+
self.onmessage = (e) => {
|
|
429
|
+
if (e.data.type === 'MWChannel') {
|
|
430
|
+
const port = MWChannel.from(e.data);
|
|
431
|
+
// ready — defaults to blocking mode
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### `port.postInit(target = null, extraProps = {})`
|
|
437
|
+
|
|
438
|
+
Sends the SAB and a `MessagePort` to the other side. The init message contains `{ type: 'MWChannel', buffer, port, ...extraProps }`. The `MessagePort` is included in the transfer list.
|
|
439
|
+
|
|
440
|
+
If `target` is `null`, returns `[msg, transferList]` for manual sending.
|
|
441
|
+
|
|
442
|
+
```javascript
|
|
443
|
+
// Auto-send to worker
|
|
444
|
+
port.postInit(worker, { channel: 'events' });
|
|
445
|
+
|
|
446
|
+
// Manual
|
|
447
|
+
const [msg, transfer] = port.postInit(null);
|
|
448
|
+
worker.postMessage(msg, transfer);
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### `port.postMessage(msg)`
|
|
452
|
+
|
|
453
|
+
**Main side:** Sends via SABPipe (blocking mode) or native `MessagePort` (nonblocking mode), depending on the current mode. Returns a `Promise` in blocking mode, `undefined` in nonblocking mode.
|
|
454
|
+
|
|
455
|
+
**Worker side:** Always sends via native `MessagePort`. Returns `undefined`.
|
|
456
|
+
|
|
457
|
+
### `port.onmessage`
|
|
458
|
+
|
|
459
|
+
**Main side:** Event handler on the native `MessagePort`. Always active — the main thread never blocks. Handler receives `{ data: message }`.
|
|
460
|
+
|
|
461
|
+
**Worker side:** Only available in **nonblocking mode**. Delegates to `MessagePort.onmessage`. Setting `onmessage` in blocking mode throws — use `read()`/`tryRead()` instead.
|
|
462
|
+
|
|
463
|
+
### `port.read(timeout = Infinity, blocking = true, maxMessages = 1)`
|
|
464
|
+
|
|
465
|
+
**Worker side, blocking mode only.** Synchronous read from the SABPipe. Same return-value convention as [`SABPipe.read()`](#readerreadtimeout--infinity-blocking--true-maxmessages--1). Throws if called on main side or in nonblocking mode.
|
|
466
|
+
|
|
467
|
+
### `port.tryRead(maxMessages = 1)`
|
|
468
|
+
|
|
469
|
+
**Worker side, blocking mode only.** Non-blocking read from the SABPipe. Throws if called on main side or in nonblocking mode.
|
|
470
|
+
|
|
471
|
+
### `port.tryPeek()`
|
|
472
|
+
|
|
473
|
+
**Worker side, blocking mode only.** Non-blocking peek from the SABPipe. Returns the next message without removing it, or `null`. Throws if called on main side or in nonblocking mode.
|
|
474
|
+
|
|
475
|
+
### `port.asyncRead(timeout = Infinity, maxMessages = 1)`
|
|
476
|
+
|
|
477
|
+
**Worker side, blocking mode only.** Async read from the SABPipe. Throws if called on main side or in nonblocking mode.
|
|
478
|
+
|
|
479
|
+
### `port.setMode(mode)`
|
|
480
|
+
|
|
481
|
+
Switches the transport mode. `mode` must be `'blocking'` or `'nonblocking'`. No-op if already in the requested mode.
|
|
482
|
+
|
|
483
|
+
**Main side:** Switches the **send** transport. `'blocking'` sends via SABPipe, `'nonblocking'` sends via native `MessagePort`.
|
|
484
|
+
|
|
485
|
+
**Worker side:** Switches the **receive** transport.
|
|
486
|
+
|
|
487
|
+
Switching to `'blocking'`:
|
|
488
|
+
1. Detaches the `onmessage` handler from the native `MessagePort`.
|
|
489
|
+
2. Sets mode to `'blocking'` — `read()`/`tryRead()`/`asyncRead()` become available.
|
|
490
|
+
|
|
491
|
+
Switching to `'nonblocking'`:
|
|
492
|
+
1. Drains any pending messages from the SABPipe via `tryRead()`.
|
|
493
|
+
2. Sets mode to `'nonblocking'` — `onmessage` becomes available.
|
|
494
|
+
3. Drained messages are delivered to the `onmessage` handler when it is set.
|
|
495
|
+
|
|
496
|
+
**Mode switching is not automatic** — the programmer is responsible for coordinating both sides. The typical pattern is:
|
|
497
|
+
|
|
498
|
+
1. Worker sends an RPC to tell main which mode to use for sending.
|
|
499
|
+
2. Main calls `port.setMode(newMode)` to switch its send transport.
|
|
500
|
+
3. Worker calls `port.setMode(newMode)` to switch its receive transport.
|
|
501
|
+
|
|
502
|
+
### `port.close()`
|
|
503
|
+
|
|
504
|
+
Destroys the SABPipe and closes the native `MessagePort`. All subsequent operations throw.
|
|
505
|
+
|
|
506
|
+
### `port.buffer` → `SharedArrayBuffer`
|
|
507
|
+
|
|
508
|
+
The underlying SABPipe buffer.
|
|
509
|
+
|
|
510
|
+
### MWChannel Usage Example
|
|
511
|
+
|
|
512
|
+
```javascript
|
|
513
|
+
// === Main thread ===
|
|
514
|
+
import { MWChannel } from 'sab-message-port';
|
|
515
|
+
|
|
516
|
+
const worker = new Worker('./worker.js', { type: 'module' });
|
|
517
|
+
const port = new MWChannel('m');
|
|
518
|
+
port.postInit(worker, { channel: 'events' });
|
|
519
|
+
|
|
520
|
+
port.onmessage = (e) => { /* worker→main always arrives here */ };
|
|
521
|
+
|
|
522
|
+
// Send in blocking mode (default — worker reads via SABPipe)
|
|
523
|
+
port.postMessage(events);
|
|
524
|
+
|
|
525
|
+
// Worker requests nonblocking mode:
|
|
526
|
+
port.setMode('nonblocking');
|
|
527
|
+
port.postMessage(events); // now sent via native MessagePort
|
|
528
|
+
|
|
529
|
+
// === Worker ===
|
|
530
|
+
import { MWChannel } from 'sab-message-port';
|
|
531
|
+
|
|
532
|
+
self.onmessage = (e) => {
|
|
533
|
+
if (e.data.type !== 'MWChannel') return;
|
|
534
|
+
const port = MWChannel.from(e.data);
|
|
535
|
+
|
|
536
|
+
// Start in blocking mode (default)
|
|
537
|
+
while (running) {
|
|
538
|
+
const events = port.read(); // blocks via SABPipe
|
|
539
|
+
for (const evt of events) handle(evt);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Switch to nonblocking
|
|
543
|
+
port.postMessage({ cmd: 'set_mode', mode: 'nonblocking' }); // tell main
|
|
544
|
+
port.setMode('nonblocking');
|
|
545
|
+
port.onmessage = (e) => handle(e.data);
|
|
546
|
+
};
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
381
551
|
## Message Batching
|
|
382
552
|
|
|
383
553
|
When the writer side calls `postMessage()` multiple times in quick succession (without awaiting), messages are **batched** into a single payload and sent together over the shared buffer. On the reader side, these batched messages are unpacked into an internal queue and delivered one at a time.
|
|
@@ -430,6 +600,36 @@ Sustained throughput (3 second run, ~500 byte messages):
|
|
|
430
600
|
|
|
431
601
|
Blocking reads are faster because `Atomics.wait` wakes with lower latency than the async event loop. Use blocking reads in worker threads for maximum performance; use async reads on the main thread or when you need to interleave with other async work.
|
|
432
602
|
|
|
603
|
+
### Round-Trip Comparison
|
|
604
|
+
|
|
605
|
+
Bidirectional echo test — main sends a message, worker echoes it back, repeat. 1000 round-trips, 118-byte messages. Node.js worker threads.
|
|
606
|
+
|
|
607
|
+
| Configuration | Avg Latency | Messages/sec | Throughput | Relative |
|
|
608
|
+
|---------------|-------------|-------------|------------|----------|
|
|
609
|
+
| MWChannel (blocking worker) | ~16 µs/rt | ~63,000 msg/s | ~14 MB/s | 1.00x |
|
|
610
|
+
| Native MessagePort | ~18 µs/rt | ~56,000 msg/s | ~13 MB/s | 1.15x |
|
|
611
|
+
| SABMessagePort (blocking worker) | ~23 µs/rt | ~43,000 msg/s | ~10 MB/s | 1.49x |
|
|
612
|
+
| SABMessagePort (async both sides) | ~26 µs/rt | ~38,000 msg/s | ~9 MB/s | 1.66x |
|
|
613
|
+
|
|
614
|
+
MWChannel wins because it combines blocking `Atomics.wait` reads (low wake-up latency) with native `MessagePort` writes (zero SABPipe overhead for the worker→main direction). SABMessagePort pays the SABPipe cost in both directions.
|
|
615
|
+
|
|
616
|
+
*Measured with Node.js.*
|
|
617
|
+
|
|
618
|
+
### Round-Trip Comparison (Chrome)
|
|
619
|
+
|
|
620
|
+
Same test, Chrome 137 with cross-origin isolation headers.
|
|
621
|
+
|
|
622
|
+
| Configuration | Avg Latency | Messages/sec | Throughput | Relative |
|
|
623
|
+
|---------------|-------------|-------------|------------|----------|
|
|
624
|
+
| Native MessagePort | ~33 µs/rt | ~30,000 msg/s | ~6.8 MB/s | 1.00x |
|
|
625
|
+
| MWChannel (blocking worker) | ~46 µs/rt | ~21,500 msg/s | ~4.9 MB/s | 1.40x |
|
|
626
|
+
| SABMessagePort (blocking worker) | ~47 µs/rt | ~21,300 msg/s | ~4.8 MB/s | 1.41x |
|
|
627
|
+
| SABMessagePort (async both sides) | ~66 µs/rt | ~15,100 msg/s | ~3.4 MB/s | 1.99x |
|
|
628
|
+
|
|
629
|
+
In Chrome, native `MessagePort` is fastest for round-trips. MWChannel and SABMessagePort blocking are nearly identical — Chrome's `Atomics.wait` wake-up latency is higher than Node.js, reducing the advantage of blocking reads. Async SABMessagePort remains the slowest at ~2x.
|
|
630
|
+
|
|
631
|
+
*Measured with Chrome.*
|
|
632
|
+
|
|
433
633
|
## Requirements
|
|
434
634
|
|
|
435
635
|
- Node.js >= 16 or any browser with `SharedArrayBuffer` support
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var p=new TextEncoder,g=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(t,e=131072,s=0,i=null){this._sab=typeof e=="number"?new SharedArrayBuffer(e):e;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-u.DATA_OFFSET,t==="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(t==="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-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 t=0;t<this.c.CONTROL_TOP;t++)Atomics.store(this.i32,t,this.c.STATUS_DISPOSED);for(let t=0;t<this.c.CONTROL_TOP;t++)Atomics.notify(this.i32,t,1/0);this.i32=null,this.u8=null,this._sab=null}}close(){this.destroy()}_create_write_queue(){let t,e,s=new Promise((i,r)=>{t=i,e=r});return{queue:[],finishWritePromise:s,finishWriteResolveFunc:t,finishWriteRejectFunc:e}}_json_to_chunks(t){let e=p.encode(JSON.stringify(t)),s=[],i=Math.ceil(e.length/this._max_chunk)||1;for(let r=0;r<i;r++){let h=e.subarray(r*this._max_chunk,(r+1)*this._max_chunk);s.push(h)}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 t=this._payload_in_progress.chunks.length;for(let s=0;s<t;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]=t,this.i32[this.c.PART_INDEX]=s,this.i32[this.c.W_DATA_LEN]=i.length,n(`_write: signaling RW_CAN_READ, part ${s}/${t}`),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 e=this._payload_in_progress;this._payload_in_progress=null,e.finishWriteResolveFunc()}finally{this._writing=!1}}async _waitForCanWrite(){for(;;){let t=Atomics.load(this.i32,this.c.RW_SIGNAL);if(t===this.c.RW_CAN_WRITE)return;t===this.c.STATUS_DISPOSED&&this._checkDisposed();let e=Atomics.waitAsync(this.i32,this.c.RW_SIGNAL,t);e.async&&await e.value}}_read(t=!0,e=1/0){if(!this.isReader)throw new Error("Only reader can read");this._checkDisposed(),n(`_read: starting, blocking=${t}, timeout=${e}`);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(!t)return n("_read: non-blocking, no data"),!1;let A=r?e:1/0;n(`_read: entering Atomics.wait, signal=${a}, timeout=${A}`);let w=Atomics.wait(this.i32,this.c.RW_SIGNAL,a,A);if(n(`_read: Atomics.wait returned ${w}`),w==="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 o=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+o);if(s.push(d),n(`_read: got part ${_}/${i}, 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),r=!1,_>=i-1){n("_read: last part received");break}}let h;if(s.length===1)h=s[0];else{let a=s.reduce((_,d)=>_+d.length,0);h=new Uint8Array(a);let o=0;for(let _ of s)h.set(_,o),o+=_.length}let c=JSON.parse(g.decode(h));return c.reverse(),this._read_queue=c.concat(this._read_queue),!0}async _waitForCanRead(t=1/0){let e=t===1/0?1/0:Date.now()+t;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=e===1/0?1/0:Math.max(0,e-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(c=>setTimeout(()=>c("timed-out"),i))])==="timed-out")return!1}}}async _asyncRead(t=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=${t}`);let e=[],s=!0;for(;;){let h=s?t: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 a=this.i32[this.c.W_DATA_LEN],o=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(e.push(d),n(`_asyncRead: got part ${_}/${o}, 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,_>=o-1){n("_asyncRead: last part received");break}}let i;if(e.length===1)i=e[0];else{let h=e.reduce((a,o)=>a+o.length,0);i=new Uint8Array(h);let c=0;for(let a of e)i.set(a,c),c+=a.length}let r=JSON.parse(g.decode(i));return r.reverse(),this._read_queue=r.concat(this._read_queue),!0}finally{this._reading=!1}}postMessage(t){if(!this.isWriter)throw new Error("Only writer can write");this._checkDisposed(),this._write_queue.queue.push(t);let e=this._write_queue.finishWritePromise;return this._asyncWrite().catch(()=>{}),e}read(t=1/0,e=!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(e,t),this._popMessages(s))}tryRead(t=1){return this.read(0,!1,t)}async asyncRead(t=1/0,e=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(e):(await this._asyncRead(t),this._popMessages(e))}set onmessage(t){if(!this.isReader)throw new Error("Only reader can set onmessage");this._onmessage=t??null,t!==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 t=this._read_queue.pop();try{this._onmessage({data:t})}catch{}}if(this._onmessage===null)break;await this._asyncRead()}}catch{}finally{this._messageLoopActive=!1}}}_popMessages(t){if(t===1)return this._read_queue.length>0?this._read_queue.pop():null;{let e=Math.min(t,this._read_queue.length);return e===0?[]:this._read_queue.splice(-e)}}},y=class u{constructor(t="a",e=256){if(t!=="a"&&t!=="b")throw new Error("side must be 'a' or 'b'");this._sab=typeof e=="number"?new SharedArrayBuffer(e*1024):e;let s=this._sab.byteLength/2;t==="a"?(this._writer=new l("w",this._sab,0,s),this._reader=new l("r",this._sab,s,s)):(this._reader=new l("r",this._sab,0,s),this._writer=new l("w",this._sab,s,s))}static from(t){if(t?.type!=="SABMessagePort")throw new Error("Not a SABMessagePort init message");return new u("b",t.buffer)}postInit(t=null,e={}){let s=[{type:"SABMessagePort",buffer:this._sab,...e},[this._sab]];if(t===null)return s;t.postMessage(...s)}postMessage(t){return this._writer.postMessage(t)}set onmessage(t){this._reader.onmessage=t}get onmessage(){return this._reader.onmessage}asyncRead(t,e){return this._reader.asyncRead(t,e)}read(t,e,s){return this._reader.read(t,e,s)}tryRead(t){return this._reader.tryRead(t)}close(){this._writer.destroy(),this._reader.destroy()}get buffer(){return this._sab}};export{y as SABMessagePort,l as SABPipe};
|
|
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};
|
package/package.json
CHANGED
package/src/SABMessagePort.js
CHANGED
|
@@ -637,6 +637,21 @@ export class SABPipe {
|
|
|
637
637
|
return this.read(0, false, max_num_messages);
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
+
/**
|
|
641
|
+
* Non-blocking peek - returns the next message without removing it from the queue.
|
|
642
|
+
* If the queue is empty, attempts a non-blocking read from the SAB first.
|
|
643
|
+
* @returns {*|null} - The next message, or null if no data available
|
|
644
|
+
*/
|
|
645
|
+
tryPeek() {
|
|
646
|
+
if (!this.isReader) throw new Error('Only reader can peek');
|
|
647
|
+
if (this._onmessage !== null) throw new Error('Cannot call tryPeek while onmessage is active');
|
|
648
|
+
this._checkDisposed();
|
|
649
|
+
if (this._read_queue.length === 0) {
|
|
650
|
+
this._read(false, 0);
|
|
651
|
+
}
|
|
652
|
+
return this._read_queue.length > 0 ? this._read_queue[this._read_queue.length - 1] : null;
|
|
653
|
+
}
|
|
654
|
+
|
|
640
655
|
/**
|
|
641
656
|
* Async read message(s) from the channel. Main-thread safe.
|
|
642
657
|
* @param {number} timeout - Timeout in ms
|
|
@@ -791,6 +806,10 @@ export class SABMessagePort {
|
|
|
791
806
|
return this._reader.tryRead(max_num_messages);
|
|
792
807
|
}
|
|
793
808
|
|
|
809
|
+
tryPeek() {
|
|
810
|
+
return this._reader.tryPeek();
|
|
811
|
+
}
|
|
812
|
+
|
|
794
813
|
close() {
|
|
795
814
|
this._writer.destroy();
|
|
796
815
|
this._reader.destroy();
|
|
@@ -800,3 +819,197 @@ export class SABMessagePort {
|
|
|
800
819
|
return this._sab;
|
|
801
820
|
}
|
|
802
821
|
}
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
// ════════════════════════════════════════════════════════════════
|
|
825
|
+
// MWChannel — MessagePort + SABPipe Wrapper
|
|
826
|
+
//
|
|
827
|
+
// Combines a native MessagePort with a SABPipe pair, allowing the
|
|
828
|
+
// worker side to switch between non-blocking (MessagePort) and
|
|
829
|
+
// blocking (SABPipe) receive modes at runtime.
|
|
830
|
+
// Main thread always receives via MessagePort (never blocks).
|
|
831
|
+
// Worker always sends via MessagePort (worker→main is always non-blocking).
|
|
832
|
+
// The SABPipe is one-directional: main→worker only.
|
|
833
|
+
// ════════════════════════════════════════════════════════════════
|
|
834
|
+
|
|
835
|
+
export class MWChannel {
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* @param {'m'|'w'} side - 'm' (main thread) or 'w' (worker thread)
|
|
839
|
+
* @param {number} sabSizeKB - SABPipe buffer size in KB (default 128). Main side only.
|
|
840
|
+
*/
|
|
841
|
+
constructor(side, sabSizeKB = 128) {
|
|
842
|
+
if (side !== 'm' && side !== 'w') throw new Error("side must be 'm' or 'w'");
|
|
843
|
+
this._side = side;
|
|
844
|
+
this._mode = 'blocking'; // default: worker starts in blocking mode
|
|
845
|
+
this._onmessage = null;
|
|
846
|
+
this._pendingDrain = [];
|
|
847
|
+
|
|
848
|
+
if (side === 'm') {
|
|
849
|
+
this._sab = new SharedArrayBuffer(sabSizeKB * 1024);
|
|
850
|
+
this._channel = new MessageChannel();
|
|
851
|
+
this._nativePort = this._channel.port1;
|
|
852
|
+
this._sabWriter = new SABPipe('w', this._sab);
|
|
853
|
+
}
|
|
854
|
+
// Worker side: _sab, _nativePort, _sabReader initialized by from()
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Creates the worker side from a received init message.
|
|
859
|
+
* @param {object} initMsg - Must have type='MWChannel', buffer, port
|
|
860
|
+
*/
|
|
861
|
+
static from(initMsg) {
|
|
862
|
+
if (initMsg?.type !== 'MWChannel') {
|
|
863
|
+
throw new Error('Not a MWChannel init message');
|
|
864
|
+
}
|
|
865
|
+
const mw = new MWChannel('w');
|
|
866
|
+
mw._sab = initMsg.buffer;
|
|
867
|
+
mw._nativePort = initMsg.port;
|
|
868
|
+
mw._sabReader = new SABPipe('r', mw._sab);
|
|
869
|
+
mw._nativePort.start();
|
|
870
|
+
return mw;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Sends init message containing the SAB and a MessagePort to the other side.
|
|
875
|
+
* If target is null, returns [msg, transferList] for manual sending.
|
|
876
|
+
*/
|
|
877
|
+
postInit(target = null, extraProps = {}) {
|
|
878
|
+
if (this._side !== 'm') throw new Error('postInit is only for main side');
|
|
879
|
+
const port2 = this._channel.port2;
|
|
880
|
+
const msg = { type: 'MWChannel', buffer: this._sab, port: port2, ...extraProps };
|
|
881
|
+
const transfer = [port2];
|
|
882
|
+
if (target === null) return [msg, transfer];
|
|
883
|
+
target.postMessage(msg, transfer);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Send a message.
|
|
888
|
+
* Main: sends via SABPipe (blocking mode) or native MessagePort (nonblocking mode).
|
|
889
|
+
* Worker: always sends via native MessagePort.
|
|
890
|
+
*/
|
|
891
|
+
postMessage(msg) {
|
|
892
|
+
if (this._side === 'm') {
|
|
893
|
+
if (this._mode === 'blocking') {
|
|
894
|
+
return this._sabWriter.postMessage(msg);
|
|
895
|
+
} else {
|
|
896
|
+
this._nativePort.postMessage(msg);
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
this._nativePort.postMessage(msg);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Event handler for incoming messages.
|
|
905
|
+
* Main: always delegates to native MessagePort.onmessage.
|
|
906
|
+
* Worker: only available in nonblocking mode (throws in blocking mode).
|
|
907
|
+
*/
|
|
908
|
+
set onmessage(handler) {
|
|
909
|
+
if (this._side === 'm') {
|
|
910
|
+
this._onmessage = handler;
|
|
911
|
+
this._nativePort.onmessage = handler;
|
|
912
|
+
} else {
|
|
913
|
+
if (this._mode === 'blocking') {
|
|
914
|
+
throw new Error('Cannot set onmessage in blocking mode — use read()/tryRead()');
|
|
915
|
+
}
|
|
916
|
+
this._onmessage = handler;
|
|
917
|
+
// Deliver any pending drained messages from mode switch
|
|
918
|
+
if (handler && this._pendingDrain.length > 0) {
|
|
919
|
+
const pending = this._pendingDrain;
|
|
920
|
+
this._pendingDrain = [];
|
|
921
|
+
for (const msg of pending) {
|
|
922
|
+
try { handler({ data: msg }); } catch (e) { /* handler errors don't break setup */ }
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
this._nativePort.onmessage = handler;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
get onmessage() {
|
|
930
|
+
return this._onmessage;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Blocking synchronous read from SABPipe. Worker side, blocking mode only.
|
|
935
|
+
*/
|
|
936
|
+
read(timeout, blocking, max_num_messages) {
|
|
937
|
+
if (this._side !== 'w') throw new Error('read() is only for worker side');
|
|
938
|
+
if (this._mode !== 'blocking') throw new Error('read() only available in blocking mode');
|
|
939
|
+
return this._sabReader.read(timeout, blocking, max_num_messages);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Non-blocking read from SABPipe. Worker side, blocking mode only.
|
|
944
|
+
*/
|
|
945
|
+
tryRead(max_num_messages) {
|
|
946
|
+
if (this._side !== 'w') throw new Error('tryRead() is only for worker side');
|
|
947
|
+
if (this._mode !== 'blocking') throw new Error('tryRead() only available in blocking mode');
|
|
948
|
+
return this._sabReader.tryRead(max_num_messages);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Non-blocking peek from SABPipe. Worker side, blocking mode only.
|
|
953
|
+
*/
|
|
954
|
+
tryPeek() {
|
|
955
|
+
if (this._side !== 'w') throw new Error('tryPeek() is only for worker side');
|
|
956
|
+
if (this._mode !== 'blocking') throw new Error('tryPeek() only available in blocking mode');
|
|
957
|
+
return this._sabReader.tryPeek();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Async read from SABPipe. Worker side, blocking mode only.
|
|
962
|
+
*/
|
|
963
|
+
asyncRead(timeout, max_num_messages) {
|
|
964
|
+
if (this._side !== 'w') throw new Error('asyncRead() is only for worker side');
|
|
965
|
+
if (this._mode !== 'blocking') throw new Error('asyncRead() only available in blocking mode');
|
|
966
|
+
return this._sabReader.asyncRead(timeout, max_num_messages);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Switch mode.
|
|
971
|
+
* Main: switches send transport ('blocking' = SABPipe, 'nonblocking' = native MessagePort).
|
|
972
|
+
* Worker: switches receive transport ('blocking' = SABPipe reads, 'nonblocking' = MessagePort onmessage).
|
|
973
|
+
*/
|
|
974
|
+
setMode(mode) {
|
|
975
|
+
if (mode !== 'blocking' && mode !== 'nonblocking') {
|
|
976
|
+
throw new Error("mode must be 'blocking' or 'nonblocking'");
|
|
977
|
+
}
|
|
978
|
+
if (this._mode === mode) return;
|
|
979
|
+
|
|
980
|
+
if (this._side === 'm') {
|
|
981
|
+
this._mode = mode;
|
|
982
|
+
} else {
|
|
983
|
+
// Worker side
|
|
984
|
+
if (mode === 'blocking') {
|
|
985
|
+
// Switching to blocking: detach native port handler
|
|
986
|
+
this._nativePort.onmessage = null;
|
|
987
|
+
this._onmessage = null;
|
|
988
|
+
this._mode = 'blocking';
|
|
989
|
+
} else {
|
|
990
|
+
// Switching to nonblocking: drain SABPipe
|
|
991
|
+
let msg;
|
|
992
|
+
while ((msg = this._sabReader.tryRead()) !== null) {
|
|
993
|
+
this._pendingDrain.push(msg);
|
|
994
|
+
}
|
|
995
|
+
this._mode = 'nonblocking';
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Close the channel. Destroys SABPipe and closes native MessagePort.
|
|
1002
|
+
*/
|
|
1003
|
+
close() {
|
|
1004
|
+
if (this._side === 'm') {
|
|
1005
|
+
this._sabWriter.destroy();
|
|
1006
|
+
} else {
|
|
1007
|
+
this._sabReader.destroy();
|
|
1008
|
+
}
|
|
1009
|
+
this._nativePort.close();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
get buffer() {
|
|
1013
|
+
return this._sab;
|
|
1014
|
+
}
|
|
1015
|
+
}
|