@gjsify/worker_threads 0.21.0 → 0.23.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.
- package/README.md +42 -3
- package/lib/esm/browser.js +1 -1
- package/lib/esm/message-port.js +1 -1
- package/lib/esm/subprocess-port-transport.js +1 -1
- package/lib/esm/worker-bootstrap.js +47 -6
- package/lib/types/worker-bootstrap.d.ts +1 -1
- package/lib/types/worker-orphan.gjs.spec.d.ts +2 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @gjsify/worker_threads
|
|
2
2
|
|
|
3
|
-
GJS
|
|
3
|
+
GJS implementation of the Node.js `worker_threads` module.
|
|
4
|
+
|
|
5
|
+
GJS has no worker *threads* — one SpiderMonkey runtime per process — so a `Worker` here is a `Gio.Subprocess` running the same GJS binary. Everything else is built to match Node's surface on top of that: `MessageChannel` / `MessagePort` / `BroadcastChannel` with structured clone, `Worker` with file-based resolution and `workerData`, and `transferList` support for `ArrayBuffer` (in-process, zero-copy), `MessagePort` (in-process hand-off plus cross-process subprocess IPC) and `SharedBuffer`.
|
|
4
6
|
|
|
5
7
|
Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
|
|
6
8
|
|
|
@@ -17,10 +19,47 @@ yarn add @gjsify/worker_threads
|
|
|
17
19
|
## Usage
|
|
18
20
|
|
|
19
21
|
```typescript
|
|
20
|
-
import { isMainThread } from '@gjsify/worker_threads';
|
|
22
|
+
import { isMainThread, Worker, MessageChannel } from '@gjsify/worker_threads';
|
|
21
23
|
|
|
22
24
|
if (isMainThread) {
|
|
23
|
-
|
|
25
|
+
const worker = new Worker(new URL('./worker.mjs', import.meta.url));
|
|
26
|
+
worker.on('message', (msg) => console.log('from worker:', msg));
|
|
27
|
+
worker.postMessage({ hello: 'world' });
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Shared memory — `SharedBuffer` and its platform scope
|
|
32
|
+
|
|
33
|
+
Stock GJS has no usable `SharedArrayBuffer`: SpiderMonkey gates the constructor behind a COOP/COEP-equivalent opt-in that GJS does not flip, and even if it did, SAB backing stores live in the per-runtime heap — which a subprocess `Worker` does not share. The substitute is **`SharedBuffer`** from the optional [`@gjsify/sab-native`](../sab-native/README.md) bridge, transferred to a worker by passing it through `workerData` or `postMessage`.
|
|
34
|
+
|
|
35
|
+
**Cross-process `SharedBuffer` is Linux-only today.** See [ADR 0013](../../../docs/adr/0013-sab-native-platform-scope.md) for the reasoning.
|
|
36
|
+
|
|
37
|
+
| Platform | Cross-process `SharedBuffer` |
|
|
38
|
+
|---|---|
|
|
39
|
+
| Linux (`x86_64`, `aarch64`, `ppc64`, `s390x`, `riscv64`) | ✅ Supported — `memfd_create(2)` + `mmap(MAP_SHARED)`, `SYS_futex` for `wait`/`notify`, `SCM_RIGHTS` to hand the descriptor to the child |
|
|
40
|
+
| macOS | ❌ Not yet — planned via `shm_open` + `os_sync_wait_on_address` (macOS 14.4+), blocked on a macOS prebuild |
|
|
41
|
+
| Windows | ❌ Not supported — GJS does not run on Windows, and Windows has no cross-process address-keyed wait primitive |
|
|
42
|
+
|
|
43
|
+
### What happens without it
|
|
44
|
+
|
|
45
|
+
Nothing crashes, and **the rest of `worker_threads` is unaffected**. When `@gjsify/sab-native` is not installed or has no prebuild for the current platform:
|
|
46
|
+
|
|
47
|
+
- `Worker`, `postMessage`, `workerData`, `MessageChannel`, `MessagePort`, `BroadcastChannel` and `transferList` for `ArrayBuffer`/`MessagePort` all work exactly as before — the `SCM_RIGHTS` side-channel is simply never created.
|
|
48
|
+
- Only `SharedBuffer` is unavailable. You cannot construct one (`SharedBuffer.create()` throws an error naming the platform scope), so no `SharedBuffer` can reach `postMessage` in the first place.
|
|
49
|
+
- If a `SharedBuffer` placeholder somehow reaches a worker whose typelib is missing, the worker bootstrap throws `SharedBuffer placeholder arrived but @gjsify/sab-native typelib not loaded` rather than failing obscurely.
|
|
50
|
+
|
|
51
|
+
Always gate `SharedBuffer` use on the availability predicate:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { hasNativeSab, SharedBuffer } from '@gjsify/sab-native';
|
|
55
|
+
import { Worker } from '@gjsify/worker_threads';
|
|
56
|
+
|
|
57
|
+
if (hasNativeSab()) {
|
|
58
|
+
const shared = SharedBuffer.create(4096);
|
|
59
|
+
const worker = new Worker(new URL('./worker.mjs', import.meta.url), { workerData: { shared } });
|
|
60
|
+
// …parent and worker now map the same backing store
|
|
61
|
+
} else {
|
|
62
|
+
// Fall back to message passing — correct everywhere, just copies.
|
|
24
63
|
}
|
|
25
64
|
```
|
|
26
65
|
|
package/lib/esm/browser.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";const e=globalThis.DedicatedWorkerGlobalScope!==void 0&&typeof globalThis.importScripts==`function`,t=!e,n=e?Math.floor(Math.random()*2147483647)+1:0,r={},i=Symbol(`worker_threads.SHARE_ENV`),a=`__gjsifyWorkerData`,o=`__gjsifyWorkerExit`;function isWorkerDataEnvelope(e){return typeof e==`object`&&!!e&&a in e}let s=null,c=!1;const l=[];function _setWorkerData(e){for(s=e,c=!0;l.length>0;)l.shift()(e)}let u=s;function onWorkerData(){return!e||c?Promise.resolve(s):new Promise(e=>l.push(e))}const d=globalThis.MessageChannel,f=globalThis.MessagePort,p=globalThis.BroadcastChannel;function makeParentPort(){let e=globalThis,t=new Map,dataInterceptor=t=>{let n=t.data;isWorkerDataEnvelope(n)&&(e.removeEventListener(`message`,dataInterceptor),_setWorkerData(n[a]))};e.addEventListener(`message`,dataInterceptor);let makeForwarder=e=>t=>{let n=t.data;isWorkerDataEnvelope(n)||e(n)};return{postMessage(t,n){n&&n.length>0?e.postMessage(t,n):e.postMessage(t)},on(t,n){let r=t===`close`?`close`:t===`messageerror`?`messageerror`:`message`,i=r===`message`?makeForwarder(n):e=>n(e.data);return n.__wrapped=i,e.addEventListener(r,i),this},off(t,n){let r=t===`close`?`close`:t===`messageerror`?`messageerror`:`message`,i=n.__wrapped;return i&&e.removeEventListener(r,i),this},once(n,r){let i=n===`close`?`close`:n===`messageerror`?`messageerror`:`message`,a=i===`message`?makeForwarder(r):e=>r(e.data),wrapped=n=>{i===`message`&&isWorkerDataEnvelope(n.data)||(e.removeEventListener(i,wrapped),t.delete(r),a(n))};return t.set(r,wrapped),e.addEventListener(i,wrapped),this},addListener(e,t){return this.on(e,t)},removeListener(e,t){return this.off(e,t)},close(){e.close?.()},start(){},ref(){},unref(){}}}const m=e?makeParentPort():null;var Worker=class{_worker;_listeners=new Map;_exited=!1;threadId;resourceLimits;constructor(e,t){let n=e instanceof URL?e:String(e);this._worker=new globalThis.Worker(n,{type:t?.type??`module`,name:t?.name}),this.threadId=Math.floor(Math.random()*2147483647)+1,this.resourceLimits=t?.resourceLimits??{},this._worker.addEventListener(`message`,e=>{let t=e.data;if(typeof t==`object`&&t&&o in t){let e=t[o];this._worker.terminate(),this._emitExit(typeof e==`number`?e:0);return}this._emit(`message`,t)}),this._worker.addEventListener(`messageerror`,e=>this._emit(`messageerror`,e.data)),this._worker.addEventListener(`error`,e=>this._emit(`error`,e.error??e));let r={[a]:t?.workerData},i=t?.transferList;i&&i.length>0?this._worker.postMessage(r,i):this._worker.postMessage(r),queueMicrotask(()=>this._emit(`online`))}_emit(e,...t){let n=this._listeners.get(e);if(n)for(let e of[...n])e(...t)}_emitExit(e){this._exited||(this._exited=!0,this._emit(`exit`,e))}on(e,t){return this._listeners.has(e)||this._listeners.set(e,new Set),this._listeners.get(e).add(t),this}off(e,t){return this._listeners.get(e)?.delete(t),this}once(e,t){let wrapped=(...n)=>{this.off(e,wrapped),t(...n)};return this.on(e,wrapped)}addListener(e,t){return this.on(e,t)}removeListener(e,t){return this.off(e,t)}postMessage(e,t){t&&t.length>0?this._worker.postMessage(e,t):this._worker.postMessage(e)}async terminate(){return this._worker.terminate(),this._emitExit(1),1}ref(){}unref(){}};const h=new Map;function setEnvironmentData(e,t){t===void 0?h.delete(e):h.set(e,t)}function getEnvironmentData(e){return h.get(e)}function receiveMessageOnPort(e){}function markAsUntransferable(e){}function markAsUncloneable(e){}function moveMessagePortToContext(e,t){return e}var g={isMainThread:t,parentPort:m,workerData:u,onWorkerData,threadId:n,resourceLimits:r,SHARE_ENV:i,Worker,MessageChannel:d,MessagePort:f,BroadcastChannel:p,setEnvironmentData,getEnvironmentData,receiveMessageOnPort,markAsUntransferable,markAsUncloneable,moveMessagePortToContext};export{p as BroadcastChannel,d as MessageChannel,f as MessagePort,i as SHARE_ENV,Worker,g as default,getEnvironmentData,t as isMainThread,markAsUncloneable,markAsUntransferable,moveMessagePortToContext,onWorkerData,m as parentPort,receiveMessageOnPort,r as resourceLimits,setEnvironmentData,n as threadId,u as workerData};
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";const e=globalThis.DedicatedWorkerGlobalScope!==void 0&&typeof globalThis.importScripts==`function`,t=!e,n=e?Math.floor(Math.random()*2147483647)+1:0,r={},i=Symbol(`worker_threads.SHARE_ENV`),a=`__gjsifyWorkerData`,o=`__gjsifyWorkerExit`;function isWorkerDataEnvelope(e){return typeof e==`object`&&!!e&&a in e}let s=null,c=!1;const l=[];function _setWorkerData(e){for(s=e,u=e,c=!0;l.length>0;)l.shift()(e)}let u=s;function onWorkerData(){return!e||c?Promise.resolve(s):new Promise(e=>l.push(e))}const d=globalThis.MessageChannel,f=globalThis.MessagePort,p=globalThis.BroadcastChannel;function makeParentPort(){let e=globalThis,t=new Map,dataInterceptor=t=>{let n=t.data;isWorkerDataEnvelope(n)&&(e.removeEventListener(`message`,dataInterceptor),_setWorkerData(n[a]))};e.addEventListener(`message`,dataInterceptor);let makeForwarder=e=>t=>{let n=t.data;isWorkerDataEnvelope(n)||e(n)};return{postMessage(t,n){n&&n.length>0?e.postMessage(t,n):e.postMessage(t)},on(t,n){let r=t===`close`?`close`:t===`messageerror`?`messageerror`:`message`,i=r===`message`?makeForwarder(n):e=>n(e.data);return n.__wrapped=i,e.addEventListener(r,i),this},off(t,n){let r=t===`close`?`close`:t===`messageerror`?`messageerror`:`message`,i=n.__wrapped;return i&&e.removeEventListener(r,i),this},once(n,r){let i=n===`close`?`close`:n===`messageerror`?`messageerror`:`message`,a=i===`message`?makeForwarder(r):e=>r(e.data),wrapped=n=>{i===`message`&&isWorkerDataEnvelope(n.data)||(e.removeEventListener(i,wrapped),t.delete(r),a(n))};return t.set(r,wrapped),e.addEventListener(i,wrapped),this},addListener(e,t){return this.on(e,t)},removeListener(e,t){return this.off(e,t)},close(){e.close?.()},start(){},ref(){},unref(){}}}const m=e?makeParentPort():null;var Worker=class{_worker;_listeners=new Map;_exited=!1;threadId;resourceLimits;constructor(e,t){let n=e instanceof URL?e:String(e);this._worker=new globalThis.Worker(n,{type:t?.type??`module`,name:t?.name}),this.threadId=Math.floor(Math.random()*2147483647)+1,this.resourceLimits=t?.resourceLimits??{},this._worker.addEventListener(`message`,e=>{let t=e.data;if(typeof t==`object`&&t&&o in t){let e=t[o];this._worker.terminate(),this._emitExit(typeof e==`number`?e:0);return}this._emit(`message`,t)}),this._worker.addEventListener(`messageerror`,e=>this._emit(`messageerror`,e.data)),this._worker.addEventListener(`error`,e=>this._emit(`error`,e.error??e));let r={[a]:t?.workerData},i=t?.transferList;i&&i.length>0?this._worker.postMessage(r,i):this._worker.postMessage(r),queueMicrotask(()=>this._emit(`online`))}_emit(e,...t){let n=this._listeners.get(e);if(n)for(let e of[...n])e(...t)}_emitExit(e){this._exited||(this._exited=!0,this._emit(`exit`,e))}on(e,t){return this._listeners.has(e)||this._listeners.set(e,new Set),this._listeners.get(e).add(t),this}off(e,t){return this._listeners.get(e)?.delete(t),this}once(e,t){let wrapped=(...n)=>{this.off(e,wrapped),t(...n)};return this.on(e,wrapped)}addListener(e,t){return this.on(e,t)}removeListener(e,t){return this.off(e,t)}postMessage(e,t){t&&t.length>0?this._worker.postMessage(e,t):this._worker.postMessage(e)}async terminate(){return this._worker.terminate(),this._emitExit(1),1}ref(){}unref(){}};const h=new Map;function setEnvironmentData(e,t){t===void 0?h.delete(e):h.set(e,t)}function getEnvironmentData(e){return h.get(e)}function receiveMessageOnPort(e){}function markAsUntransferable(e){}function markAsUncloneable(e){}function moveMessagePortToContext(e,t){return e}var g={isMainThread:t,parentPort:m,workerData:u,onWorkerData,threadId:n,resourceLimits:r,SHARE_ENV:i,Worker,MessageChannel:d,MessagePort:f,BroadcastChannel:p,setEnvironmentData,getEnvironmentData,receiveMessageOnPort,markAsUntransferable,markAsUncloneable,moveMessagePortToContext};export{p as BroadcastChannel,d as MessageChannel,f as MessagePort,i as SHARE_ENV,Worker,g as default,getEnvironmentData,t as isMainThread,markAsUncloneable,markAsUntransferable,moveMessagePortToContext,onWorkerData,m as parentPort,receiveMessageOnPort,r as resourceLimits,setEnvironmentData,n as threadId,u as workerData};
|
package/lib/esm/message-port.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{isSharedBuffer as e}from"./sab-transfer.js";import{EventEmitter as t}from"node:events";import{MessagePort as n}from"@gjsify/message-channel";function isTransferredPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0}var r=class MessagePort extends t{_closed=!1;_detached=!1;_otherPort=null;_inner=new n;start(){if(this._closed||this._inner._started)return;let e=this._inner._queue.slice();this._inner.start();for(let t of e)this._dispatchEmit(t)}close(){if(this._closed)return;this._closed=!0;let e=this._otherPort;this._otherPort=null,e&&(e._otherPort=null),this._inner.close(),this.emit(`close`),this.removeAllListeners()}postMessage(t,n){if(this._closed)return;let r=this._otherPort;if(!r&&this._inner._transport!==null){if(n&&n.length>0)throw createDataCloneError(`transferList is not supported on cross-process MessagePort yet`);this._inner.postMessage(t);return}if(!r)return;let i=[],a=[];if(n&&n.length>0){let t=new Set;for(let r of n){if(t.has(r))throw createDataCloneError(`Transfer list contains duplicate entries`);if(t.add(r),r instanceof MessagePort){if(r===this)throw createDataCloneError(`Cannot transfer source port`);if(r._closed||r._detached)throw createDataCloneError(`MessagePort in transfer list is already detached`);a.push(r);continue}let o=Object.prototype.toString.call(r).slice(8,-1);if(o===`ArrayBuffer`){let e=r;if(e.detached===!0)throw createDataCloneError(`ArrayBuffer in transfer list is detached`);i.push(e);continue}if(o===`SharedArrayBuffer`)throw createDataCloneError(`SharedArrayBuffer cannot appear in transfer list (it is shared, not transferred)`);if(!e(r))throw createDataCloneError(`Value at index ${n.indexOf(r)} of transfer list is not transferable`)}}let o=a.slice(),s=t;a.length>0&&(s=substitutePortsWithPlaceholders(t,a));let c;try{c=structuredClone(s,{transfer:i.length>0?i:void 0})}catch(e){this.emit(`messageerror`,e instanceof Error?e:Error(`Could not clone message`));return}let l=c;if(o.length>0){let e=o.map(e=>{let t=e._otherPort;e._otherPort=null,e._detached=!0,e._closed=!0;let n=new MessagePort;return n._otherPort=t,t&&(t._otherPort=n),n});l=replacePlaceholdersWithPorts(c,e)}r._receiveMessage(l)}ref(){return this}unref(){return this}_receiveMessage(e){this._closed||(this._inner._receive(e),this._inner._started&&this._dispatchEmit(e))}get _hasQueuedMessages(){return this._inner._queue.length>0}_dequeueMessage(){return this._inner._queue.shift()}get _wasTransferred(){return this._detached}_dispatchEmit(e){Promise.resolve().then(()=>{this._closed||this.emit(`message`,e)})}addEventListener(e,t,n){if(t){if(e===`message`||e===`messageerror`){Object.getPrototypeOf(Object.getPrototypeOf(this._inner)).addEventListener.call(this._inner,e,t,n);return}super.on(e,t)}}removeEventListener(e,t,n){if(t){if(e===`message`||e===`messageerror`){Object.getPrototypeOf(Object.getPrototypeOf(this._inner)).removeEventListener.call(this._inner,e,t,n);return}super.off(e,t)}}get onmessage(){return this._inner.onmessage}set onmessage(e){if(e!==null&&!this._inner._started){let t=this._inner._queue.slice();this._inner.onmessage=e;for(let e of t)this._dispatchEmit(e)}else this._inner.onmessage=e}get onmessageerror(){return this._inner.onmessageerror}set onmessageerror(e){this._inner.onmessageerror=e}on(e,t){return super.on(e,t),e===`message`&&this.start(),this}addListener(e,t){return this.on(e,t)}once(e,t){return super.once(e,t),e===`message`&&this.start(),this}};function createDataCloneError(e){let t=globalThis.DOMException;if(typeof t==`function`){let n=new t(e,`DataCloneError`);if(n.code===void 0)try{Object.defineProperty(n
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{isSharedBuffer as e}from"./sab-transfer.js";import{EventEmitter as t}from"node:events";import{MessagePort as n}from"@gjsify/message-channel";function isTransferredPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0}var r=class MessagePort extends t{_closed=!1;_detached=!1;_otherPort=null;_inner=new n;start(){if(this._closed||this._inner._started)return;let e=this._inner._queue.slice();this._inner.start();for(let t of e)this._dispatchEmit(t)}close(){if(this._closed)return;this._closed=!0;let e=this._otherPort;this._otherPort=null,e&&(e._otherPort=null),this._inner.close(),this.emit(`close`),this.removeAllListeners()}postMessage(t,n){if(this._closed)return;let r=this._otherPort;if(!r&&this._inner._transport!==null){if(n&&n.length>0)throw createDataCloneError(`transferList is not supported on cross-process MessagePort yet`);this._inner.postMessage(t);return}if(!r)return;let i=[],a=[];if(n&&n.length>0){let t=new Set;for(let r of n){if(t.has(r))throw createDataCloneError(`Transfer list contains duplicate entries`);if(t.add(r),r instanceof MessagePort){if(r===this)throw createDataCloneError(`Cannot transfer source port`);if(r._closed||r._detached)throw createDataCloneError(`MessagePort in transfer list is already detached`);a.push(r);continue}let o=Object.prototype.toString.call(r).slice(8,-1);if(o===`ArrayBuffer`){let e=r;if(e.detached===!0)throw createDataCloneError(`ArrayBuffer in transfer list is detached`);i.push(e);continue}if(o===`SharedArrayBuffer`)throw createDataCloneError(`SharedArrayBuffer cannot appear in transfer list (it is shared, not transferred)`);if(!e(r))throw createDataCloneError(`Value at index ${n.indexOf(r)} of transfer list is not transferable`)}}let o=a.slice(),s=t;a.length>0&&(s=substitutePortsWithPlaceholders(t,a));let c;try{c=structuredClone(s,{transfer:i.length>0?i:void 0})}catch(e){this.emit(`messageerror`,e instanceof Error?e:Error(`Could not clone message`));return}let l=c;if(o.length>0){let e=o.map(e=>{let t=e._otherPort;e._otherPort=null,e._detached=!0,e._closed=!0;let n=new MessagePort;return n._otherPort=t,t&&(t._otherPort=n),n});l=replacePlaceholdersWithPorts(c,e)}r._receiveMessage(l)}ref(){return this}unref(){return this}_receiveMessage(e){this._closed||(this._inner._receive(e),this._inner._started&&this._dispatchEmit(e))}get _hasQueuedMessages(){return this._inner._queue.length>0}_dequeueMessage(){return this._inner._queue.shift()}get _wasTransferred(){return this._detached}_dispatchEmit(e){Promise.resolve().then(()=>{this._closed||this.emit(`message`,e)})}addEventListener(e,t,n){if(t){if(e===`message`||e===`messageerror`){Object.getPrototypeOf(Object.getPrototypeOf(this._inner)).addEventListener.call(this._inner,e,t,n);return}super.on(e,t)}}removeEventListener(e,t,n){if(t){if(e===`message`||e===`messageerror`){Object.getPrototypeOf(Object.getPrototypeOf(this._inner)).removeEventListener.call(this._inner,e,t,n);return}super.off(e,t)}}get onmessage(){return this._inner.onmessage}set onmessage(e){if(e!==null&&!this._inner._started){let t=this._inner._queue.slice();this._inner.onmessage=e;for(let e of t)this._dispatchEmit(e)}else this._inner.onmessage=e}get onmessageerror(){return this._inner.onmessageerror}set onmessageerror(e){this._inner.onmessageerror=e}on(e,t){return super.on(e,t),e===`message`&&this.start(),this}addListener(e,t){return this.on(e,t)}once(e,t){return super.once(e,t),e===`message`&&this.start(),this}};function createDataCloneError(e){let t=globalThis.DOMException;if(typeof t==`function`){let n=new t(e,`DataCloneError`);if(n.code===void 0)try{Object.defineProperty(n,"code",{value:25,configurable:!0})}catch{}return n}let n=Error(e);return n.name=`DataCloneError`,n.code=25,n}function substitutePortsWithPlaceholders(e,t){let n=new Map;for(let e=0;e<t.length;e++)n.set(t[e],e);function walk(e,t){if(typeof e!=`object`||!e)return e;if(e instanceof r){let t=n.get(e);return t===void 0?e:{__gjsifyTransferredPort:!0,index:t}}if(t.has(e))return t.get(e);if(Array.isArray(e)){let n=[];t.set(e,n);for(let r=0;r<e.length;r++)r in e&&(n[r]=walk(e[r],t));return n}if(Object.prototype.toString.call(e).slice(8,-1)===`Object`){let n={};t.set(e,n);for(let r of Object.keys(e))n[r]=walk(e[r],t);return n}return e}return walk(e,new Map)}function replacePlaceholdersWithPorts(e,t){function walk(e,n){if(typeof e!=`object`||!e)return e;if(isTransferredPortPlaceholder(e))return t[e.index];if(n.has(e))return n.get(e);if(Array.isArray(e)){n.set(e,e);for(let t=0;t<e.length;t++)t in e&&(e[t]=walk(e[t],n));return e}if(Object.prototype.toString.call(e).slice(8,-1)===`Object`){n.set(e,e);for(let t of Object.keys(e))e[t]=walk(e[t],n);return e}return e}return walk(e,new Map)}export{r as MessagePort};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";var SubprocessPortTransport=class{constructor(e,t){this._sendLine=e,this._registry=t}send(e,t){let n=JSON.stringify({__msgport:e,op:`send`,data:t})+`
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";var SubprocessPortTransport=class{_sendLine;_registry;constructor(e,t){this._sendLine=e,this._registry=t}send(e,t){let n=JSON.stringify({__msgport:e,op:`send`,data:t})+`
|
|
2
2
|
`;this._sendLine(n)}close(e){let t=JSON.stringify({__msgport:e,op:`close`})+`
|
|
3
3
|
`;try{this._sendLine(t)}catch{}this._registry.delete(e)}};let e=1,t=2;function nextParentPortId(){let t=e;return e+=2,t}function nextChildPortId(){let e=t;return t+=2,e}function isCrossProcessPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0&&typeof e.portId==`number`}export{SubprocessPortTransport,isCrossProcessPortPlaceholder,nextChildPortId,nextParentPortId};
|
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
const e=`import GLib from 'gi://GLib';
|
|
2
2
|
import Gio from 'gi://Gio';
|
|
3
|
+
import System from 'system';
|
|
3
4
|
|
|
4
5
|
const loop = new GLib.MainLoop(null, false);
|
|
6
|
+
|
|
7
|
+
// Shutdown bookkeeping.
|
|
8
|
+
//
|
|
9
|
+
// \`loop.run()\` is the LAST statement of this module — it only starts after
|
|
10
|
+
// the worker's own script has finished. \`loop.quit()\` on a loop that is not
|
|
11
|
+
// running yet is a silent NO-OP, so every shutdown request that arrives while
|
|
12
|
+
// the worker script is still executing used to be lost: the module went on to
|
|
13
|
+
// call \`loop.run()\` afterwards and the child ran forever.
|
|
14
|
+
//
|
|
15
|
+
// The parent's death is exactly that case. When the spawner dies its end of
|
|
16
|
+
// our stdin pipe closes, the reader below sees EOF and asked the loop to quit
|
|
17
|
+
// — too early to matter. The child then sat in \`loop.run()\` with no armed
|
|
18
|
+
// source, or (when its script had a still-pending top-level await, which GJS
|
|
19
|
+
// services by spinning the job queue) burned a full core, forever. Node's
|
|
20
|
+
// workers are threads and die with the process that owns them; an orphan that
|
|
21
|
+
// outlives — and out-CPUs — its parent is never right.
|
|
22
|
+
//
|
|
23
|
+
// So: record the request (\`shuttingDown\`) and honour it at both ends —
|
|
24
|
+
// \`loop.run()\` is skipped when shutdown was already requested, and the EOF
|
|
25
|
+
// path exits the process outright so a worker script that never settles
|
|
26
|
+
// cannot keep the orphan alive either. \`terminate\`/\`parentPort.close()\`
|
|
27
|
+
// deliberately keep the softer flag-only path: their parent is alive and
|
|
28
|
+
// waiting on the exit event, and \`Worker.terminate()\` already has a
|
|
29
|
+
// \`force_exit()\` backstop for a wedged script.
|
|
30
|
+
let shuttingDown = false;
|
|
31
|
+
function requestShutdown() {
|
|
32
|
+
shuttingDown = true;
|
|
33
|
+
loop.quit();
|
|
34
|
+
}
|
|
5
35
|
const stdinStream = Gio.UnixInputStream.new(0, false);
|
|
6
36
|
const dataIn = Gio.DataInputStream.new(stdinStream);
|
|
7
37
|
const stdoutStream = Gio.UnixOutputStream.new(1, false);
|
|
@@ -233,7 +263,7 @@ const parentPort = {
|
|
|
233
263
|
},
|
|
234
264
|
emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },
|
|
235
265
|
postMessage(data) { send({ type: 'message', data }); },
|
|
236
|
-
close() { send({ type: 'exit', code: 0 });
|
|
266
|
+
close() { send({ type: 'exit', code: 0 }); requestShutdown(); },
|
|
237
267
|
removeAllListeners(ev) {
|
|
238
268
|
if (ev) _listeners.delete(ev); else _listeners.clear();
|
|
239
269
|
return this;
|
|
@@ -255,7 +285,14 @@ function readNext() {
|
|
|
255
285
|
dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {
|
|
256
286
|
try {
|
|
257
287
|
const [line] = source.read_line_finish_utf8(result);
|
|
258
|
-
if (line === null) {
|
|
288
|
+
if (line === null) {
|
|
289
|
+
// EOF on stdin = the spawner is gone. Nothing can reach us again and
|
|
290
|
+
// nobody is left to observe an exit code, so leave immediately —
|
|
291
|
+
// even from inside the worker script's own pending top-level await.
|
|
292
|
+
requestShutdown();
|
|
293
|
+
System.exit(0);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
259
296
|
const msg = JSON.parse(line);
|
|
260
297
|
if (typeof msg.__msgport === 'number') {
|
|
261
298
|
// Cross-process MessagePort traffic — route to the local child-
|
|
@@ -279,11 +316,13 @@ function readNext() {
|
|
|
279
316
|
if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));
|
|
280
317
|
parentPort.emit('message', materialise(msg.data));
|
|
281
318
|
}
|
|
282
|
-
else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 });
|
|
319
|
+
else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); requestShutdown(); return; }
|
|
283
320
|
readNext();
|
|
284
321
|
} catch (err) {
|
|
285
|
-
|
|
286
|
-
|
|
322
|
+
try {
|
|
323
|
+
send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });
|
|
324
|
+
} catch (_) { /* parent's end of stdout is gone too — nothing to report to */ }
|
|
325
|
+
requestShutdown();
|
|
287
326
|
}
|
|
288
327
|
});
|
|
289
328
|
}
|
|
@@ -303,5 +342,7 @@ try {
|
|
|
303
342
|
send({ type: 'error', message: error.message, stack: error.stack || '' });
|
|
304
343
|
}
|
|
305
344
|
|
|
306
|
-
loop
|
|
345
|
+
// Skip the loop entirely when shutdown was requested while the script above
|
|
346
|
+
// was still running — otherwise that lost quit() strands the child forever.
|
|
347
|
+
if (!shuttingDown) loop.run();
|
|
307
348
|
`;export{e as BOOTSTRAP_CODE};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const BOOTSTRAP_CODE = "import GLib from 'gi://GLib';\nimport Gio from 'gi://Gio';\n\nconst loop = new GLib.MainLoop(null, false);\nconst stdinStream = Gio.UnixInputStream.new(0, false);\nconst dataIn = Gio.DataInputStream.new(stdinStream);\nconst stdoutStream = Gio.UnixOutputStream.new(1, false);\n\nconst _encoder = new TextEncoder();\n\nfunction send(obj) {\n const line = JSON.stringify(obj) + '\\n';\n stdoutStream.write_all(_encoder.encode(line), null);\n}\n\n// Try to load the sab-native typelib synchronously. Absent prebuild\n// is fine \u2014 only fails the receive path when a SharedBuffer actually\n// crosses the wire.\nlet SabNative = null;\ntry { SabNative = imports.gi.GjsifySabNative; } catch (_) { /* prebuild missing */ }\n\n// Read init data (first line, blocking)\nconst [initLine] = dataIn.read_line_utf8(null);\nconst init = JSON.parse(initLine);\n\n// fd \u2192 SharedBuffer cache keyed by tag. Filled by drainSabFds() right\n// before materialise() walks the parsed JSON.\nconst sabFds = new Map();\n\n// Count how many SharedBuffer placeholders sit in a parsed JSON value.\n// The exact count is what we pass to drainSabFds().\nfunction countSabPlaceholders(value) {\n if (value === null || typeof value !== 'object') return 0;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') return 1;\n if (Array.isArray(value)) {\n let n = 0;\n for (const v of value) n += countSabPlaceholders(v);\n return n;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n let n = 0;\n for (const v of Object.values(value)) n += countSabPlaceholders(v);\n return n;\n }\n return 0;\n}\n\n// Synchronously drain exactly @count SCM_RIGHTS messages from fd 3,\n// indexing each received fd by its sender-encoded tag. Called once per\n// incoming JSON message that contains __sab placeholders. recv_fd\n// blocks at most until the kernel has the message \u2014 and the sender\n// always sends fds before writing to stdin, so by the time we get\n// here every fd is already buffered.\nfunction drainSabFds(count) {\n if (!SabNative || count <= 0) return;\n for (let i = 0; i < count; i++) {\n const [fd, tag] = SabNative.FdChannel.recv_fd(3);\n if (fd <= 0) throw new Error('FdChannel.recv_fd returned ' + fd + ' while draining ' + count + ' fds');\n sabFds.set(tag, fd);\n }\n}\n\nfunction materialise(value) {\n if (value === null || typeof value !== 'object') return value;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') {\n const fd = sabFds.get(value.__sab);\n if (fd === undefined) throw new Error('SharedBuffer placeholder \\'' + value.__sab + '\\' arrived before its fd');\n if (!SabNative) throw new Error('SharedBuffer placeholder arrived but @gjsify/sab-native typelib not loaded');\n const native = SabNative.SharedBuffer.from_fd(fd, value.size);\n sabFds.delete(value.__sab);\n return makeSharedBuffer(native);\n }\n if (isPortPlaceholder(value)) {\n return makeChildSidePort(value.portId);\n }\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) value[i] = materialise(value[i]);\n return value;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n for (const k of Object.keys(value)) value[k] = materialise(value[k]);\n return value;\n }\n return value;\n}\n\nfunction makeSharedBuffer(native) {\n return {\n get byteLength() { return native.byte_length; },\n get fd() { return native.fd; },\n getUint8(off) { return native.get_u8(off); },\n setUint8(off, v) { native.set_u8(off, v); },\n getInt32LE(off) { return native.get_i32_le(off); },\n setInt32LE(off, v) { native.set_i32_le(off, v); },\n getUint32LE(off) { return native.get_u32_le(off); },\n setUint32LE(off, v) { native.set_u32_le(off, v); },\n getUint64LE(off) { return native.get_u64_le(off); },\n setUint64LE(off, v) { native.set_u64_le(off, v); },\n readBytes(off, len) {\n const bytes = native.read_bytes(off, len);\n const data = bytes.get_data();\n return data ? new Uint8Array(data) : new Uint8Array(0);\n },\n writeBytes(off, data) { native.write_bytes(off, new GLib.Bytes(data)); },\n get _nativeHandle() { return native; },\n get constructor() { return { name: 'SharedBuffer' }; },\n };\n}\n\n// Cross-process MessagePort support (child side).\n//\n// When a parent's Worker.postMessage(value, [port2]) transfers a\n// MessagePort, the parent encodes the port as a placeholder\n// { __gjsifyTransferredPort: true, portId: <id> } and ships the rest of\n// the value as JSON. On this side, materialise() walks the parsed JSON\n// and substitutes each placeholder for a fresh \"child-side port\" \u2014 an\n// inline EventEmitter-shaped object that mirrors the worker_threads\n// MessagePort surface enough for user code (postMessage / on('message') /\n// on('close') / close()). Outbound traffic via this port travels over\n// stdout as { __msgport, op: 'send', data } JSON lines, which the parent's\n// stdout-reader routes back to the kept end via the parent-side registry.\n//\n// transferList chaining (port-in-port) is intentionally not supported\n// on the cross-process path in v1 \u2014 see STATUS.md Open TODOs.\nconst childPortRegistry = new Map();\n\nfunction makeChildSidePort(portId) {\n const listeners = new Map();\n let closed = false;\n\n function emit(ev /* , ...args */) {\n const args = Array.prototype.slice.call(arguments, 1);\n const fns = (listeners.get(ev) || []).slice();\n for (const fn of fns) {\n try { fn.apply(null, args); } catch (_) { /* swallow */ }\n }\n }\n\n const port = {\n _portId: portId,\n /** @internal Wire-side delivery \u2014 called by the bootstrap stdin\n * dispatcher when it receives a {__msgport: portId, op: 'send'}\n * line. Schedules dispatch on a microtask so listeners fire after\n * the current call stack unwinds \u2014 matches in-process MessagePort\n * semantics. */\n _receive(data) {\n if (closed) return;\n Promise.resolve().then(() => { if (!closed) emit('message', data); });\n },\n /** @internal Called by the dispatcher when the parent sends\n * {__msgport: portId, op: 'close'}. Fires the local 'close' event\n * and stops accepting further messages. */\n _internalClose() {\n if (closed) return;\n closed = true;\n childPortRegistry.delete(portId);\n Promise.resolve().then(() => emit('close'));\n },\n on(ev, fn) {\n if (!listeners.has(ev)) listeners.set(ev, []);\n listeners.get(ev).push(fn);\n return port;\n },\n once(ev, fn) {\n const w = function () { port.off(ev, w); fn.apply(null, arguments); };\n return port.on(ev, w);\n },\n off(ev, fn) {\n const arr = listeners.get(ev);\n if (arr) listeners.set(ev, arr.filter(function (f) { return f !== fn; }));\n return port;\n },\n addListener(ev, fn) { return port.on(ev, fn); },\n removeListener(ev, fn) { return port.off(ev, fn); },\n emit(ev /* , ...args */) {\n emit.apply(null, Array.prototype.slice.call(arguments));\n return port;\n },\n postMessage(data) {\n if (closed) return;\n send({ __msgport: portId, op: 'send', data });\n },\n close() {\n if (closed) return;\n closed = true;\n try { send({ __msgport: portId, op: 'close' }); } catch (_) {}\n childPortRegistry.delete(portId);\n Promise.resolve().then(() => emit('close'));\n },\n start() { /* no-op \u2014 child-side port dispatches on receive */ },\n ref() { return port; },\n unref() { return port; },\n get _isCrossProcess() { return true; },\n get constructor() { return { name: 'MessagePort' }; },\n };\n\n childPortRegistry.set(portId, port);\n return port;\n}\n\nfunction isPortPlaceholder(value) {\n return value !== null && typeof value === 'object'\n && value.__gjsifyTransferredPort === true\n && typeof value.portId === 'number';\n}\n\n// Drain SharedBuffer fds attached to the init line (workerData may carry\n// SharedBuffer instances) before materialise() runs against it.\nif (init.sabSocketFd === 3) {\n try {\n drainSabFds(countSabPlaceholders(init.workerData));\n } catch (err) {\n send({ type: 'error', message: 'init fd drain failed: ' + (err && err.message ? err.message : err), stack: '' });\n }\n}\n\n// Simplified EventEmitter for parentPort\nconst _listeners = new Map();\nconst parentPort = {\n on(ev, fn) {\n if (!_listeners.has(ev)) _listeners.set(ev, []);\n _listeners.get(ev).push(fn);\n return this;\n },\n once(ev, fn) {\n const w = (...a) => { parentPort.off(ev, w); fn(...a); };\n return parentPort.on(ev, w);\n },\n off(ev, fn) {\n const a = _listeners.get(ev);\n if (a) _listeners.set(ev, a.filter(f => f !== fn));\n return this;\n },\n emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },\n postMessage(data) { send({ type: 'message', data }); },\n close() { send({ type: 'exit', code: 0 }); loop.quit(); },\n removeAllListeners(ev) {\n if (ev) _listeners.delete(ev); else _listeners.clear();\n return this;\n },\n};\n\n// Set worker context globals (read by @gjsify/worker_threads when imported by user script)\nglobalThis.__gjsify_worker_context = {\n isMainThread: false,\n parentPort,\n workerData: materialise(init.workerData ?? null),\n threadId: init.threadId ?? 0,\n};\n\nsend({ type: 'online' });\n\n// Async stdin reader for messages from parent\nfunction readNext() {\n dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {\n try {\n const [line] = source.read_line_finish_utf8(result);\n if (line === null) { loop.quit(); return; }\n const msg = JSON.parse(line);\n if (typeof msg.__msgport === 'number') {\n // Cross-process MessagePort traffic \u2014 route to the local child-\n // side port from childPortRegistry by portId. Both 'send' and\n // 'close' dispatch on a microtask inside the port's helpers, so\n // user listeners fire after the current stdin-callback unwinds.\n const cpPort = childPortRegistry.get(msg.__msgport);\n if (cpPort) {\n if (msg.op === 'send') cpPort._receive(msg.data);\n else if (msg.op === 'close') cpPort._internalClose();\n }\n // Unknown portId or op silently dropped \u2014 could happen mid-close\n // race where the parent sends a final message after the child has\n // already torn down the local port.\n }\n else if (msg.type === 'message') {\n // Drain fds for any SharedBuffer placeholders BEFORE materialise.\n // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS\n // messages before writing this stdin line, so they're already\n // buffered.\n if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));\n parentPort.emit('message', materialise(msg.data));\n }\n else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); loop.quit(); return; }\n readNext();\n } catch (err) {\n send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });\n loop.quit();\n }\n });\n}\nreadNext();\n\n// Execute worker code\ntry {\n if (init.eval) {\n const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;\n await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(\n parentPort, globalThis.__gjsify_worker_context.workerData, init.threadId\n );\n } else {\n await import(init.filename);\n }\n} catch (error) {\n send({ type: 'error', message: error.message, stack: error.stack || '' });\n}\n\nloop.run();\n";
|
|
1
|
+
export declare const BOOTSTRAP_CODE = "import GLib from 'gi://GLib';\nimport Gio from 'gi://Gio';\nimport System from 'system';\n\nconst loop = new GLib.MainLoop(null, false);\n\n// Shutdown bookkeeping.\n//\n// `loop.run()` is the LAST statement of this module \u2014 it only starts after\n// the worker's own script has finished. `loop.quit()` on a loop that is not\n// running yet is a silent NO-OP, so every shutdown request that arrives while\n// the worker script is still executing used to be lost: the module went on to\n// call `loop.run()` afterwards and the child ran forever.\n//\n// The parent's death is exactly that case. When the spawner dies its end of\n// our stdin pipe closes, the reader below sees EOF and asked the loop to quit\n// \u2014 too early to matter. The child then sat in `loop.run()` with no armed\n// source, or (when its script had a still-pending top-level await, which GJS\n// services by spinning the job queue) burned a full core, forever. Node's\n// workers are threads and die with the process that owns them; an orphan that\n// outlives \u2014 and out-CPUs \u2014 its parent is never right.\n//\n// So: record the request (`shuttingDown`) and honour it at both ends \u2014\n// `loop.run()` is skipped when shutdown was already requested, and the EOF\n// path exits the process outright so a worker script that never settles\n// cannot keep the orphan alive either. `terminate`/`parentPort.close()`\n// deliberately keep the softer flag-only path: their parent is alive and\n// waiting on the exit event, and `Worker.terminate()` already has a\n// `force_exit()` backstop for a wedged script.\nlet shuttingDown = false;\nfunction requestShutdown() {\n shuttingDown = true;\n loop.quit();\n}\nconst stdinStream = Gio.UnixInputStream.new(0, false);\nconst dataIn = Gio.DataInputStream.new(stdinStream);\nconst stdoutStream = Gio.UnixOutputStream.new(1, false);\n\nconst _encoder = new TextEncoder();\n\nfunction send(obj) {\n const line = JSON.stringify(obj) + '\\n';\n stdoutStream.write_all(_encoder.encode(line), null);\n}\n\n// Try to load the sab-native typelib synchronously. Absent prebuild\n// is fine \u2014 only fails the receive path when a SharedBuffer actually\n// crosses the wire.\nlet SabNative = null;\ntry { SabNative = imports.gi.GjsifySabNative; } catch (_) { /* prebuild missing */ }\n\n// Read init data (first line, blocking)\nconst [initLine] = dataIn.read_line_utf8(null);\nconst init = JSON.parse(initLine);\n\n// fd \u2192 SharedBuffer cache keyed by tag. Filled by drainSabFds() right\n// before materialise() walks the parsed JSON.\nconst sabFds = new Map();\n\n// Count how many SharedBuffer placeholders sit in a parsed JSON value.\n// The exact count is what we pass to drainSabFds().\nfunction countSabPlaceholders(value) {\n if (value === null || typeof value !== 'object') return 0;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') return 1;\n if (Array.isArray(value)) {\n let n = 0;\n for (const v of value) n += countSabPlaceholders(v);\n return n;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n let n = 0;\n for (const v of Object.values(value)) n += countSabPlaceholders(v);\n return n;\n }\n return 0;\n}\n\n// Synchronously drain exactly @count SCM_RIGHTS messages from fd 3,\n// indexing each received fd by its sender-encoded tag. Called once per\n// incoming JSON message that contains __sab placeholders. recv_fd\n// blocks at most until the kernel has the message \u2014 and the sender\n// always sends fds before writing to stdin, so by the time we get\n// here every fd is already buffered.\nfunction drainSabFds(count) {\n if (!SabNative || count <= 0) return;\n for (let i = 0; i < count; i++) {\n const [fd, tag] = SabNative.FdChannel.recv_fd(3);\n if (fd <= 0) throw new Error('FdChannel.recv_fd returned ' + fd + ' while draining ' + count + ' fds');\n sabFds.set(tag, fd);\n }\n}\n\nfunction materialise(value) {\n if (value === null || typeof value !== 'object') return value;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') {\n const fd = sabFds.get(value.__sab);\n if (fd === undefined) throw new Error('SharedBuffer placeholder \\'' + value.__sab + '\\' arrived before its fd');\n if (!SabNative) throw new Error('SharedBuffer placeholder arrived but @gjsify/sab-native typelib not loaded');\n const native = SabNative.SharedBuffer.from_fd(fd, value.size);\n sabFds.delete(value.__sab);\n return makeSharedBuffer(native);\n }\n if (isPortPlaceholder(value)) {\n return makeChildSidePort(value.portId);\n }\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) value[i] = materialise(value[i]);\n return value;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n for (const k of Object.keys(value)) value[k] = materialise(value[k]);\n return value;\n }\n return value;\n}\n\nfunction makeSharedBuffer(native) {\n return {\n get byteLength() { return native.byte_length; },\n get fd() { return native.fd; },\n getUint8(off) { return native.get_u8(off); },\n setUint8(off, v) { native.set_u8(off, v); },\n getInt32LE(off) { return native.get_i32_le(off); },\n setInt32LE(off, v) { native.set_i32_le(off, v); },\n getUint32LE(off) { return native.get_u32_le(off); },\n setUint32LE(off, v) { native.set_u32_le(off, v); },\n getUint64LE(off) { return native.get_u64_le(off); },\n setUint64LE(off, v) { native.set_u64_le(off, v); },\n readBytes(off, len) {\n const bytes = native.read_bytes(off, len);\n const data = bytes.get_data();\n return data ? new Uint8Array(data) : new Uint8Array(0);\n },\n writeBytes(off, data) { native.write_bytes(off, new GLib.Bytes(data)); },\n get _nativeHandle() { return native; },\n get constructor() { return { name: 'SharedBuffer' }; },\n };\n}\n\n// Cross-process MessagePort support (child side).\n//\n// When a parent's Worker.postMessage(value, [port2]) transfers a\n// MessagePort, the parent encodes the port as a placeholder\n// { __gjsifyTransferredPort: true, portId: <id> } and ships the rest of\n// the value as JSON. On this side, materialise() walks the parsed JSON\n// and substitutes each placeholder for a fresh \"child-side port\" \u2014 an\n// inline EventEmitter-shaped object that mirrors the worker_threads\n// MessagePort surface enough for user code (postMessage / on('message') /\n// on('close') / close()). Outbound traffic via this port travels over\n// stdout as { __msgport, op: 'send', data } JSON lines, which the parent's\n// stdout-reader routes back to the kept end via the parent-side registry.\n//\n// transferList chaining (port-in-port) is intentionally not supported\n// on the cross-process path in v1 \u2014 see STATUS.md Open TODOs.\nconst childPortRegistry = new Map();\n\nfunction makeChildSidePort(portId) {\n const listeners = new Map();\n let closed = false;\n\n function emit(ev /* , ...args */) {\n const args = Array.prototype.slice.call(arguments, 1);\n const fns = (listeners.get(ev) || []).slice();\n for (const fn of fns) {\n try { fn.apply(null, args); } catch (_) { /* swallow */ }\n }\n }\n\n const port = {\n _portId: portId,\n /** @internal Wire-side delivery \u2014 called by the bootstrap stdin\n * dispatcher when it receives a {__msgport: portId, op: 'send'}\n * line. Schedules dispatch on a microtask so listeners fire after\n * the current call stack unwinds \u2014 matches in-process MessagePort\n * semantics. */\n _receive(data) {\n if (closed) return;\n Promise.resolve().then(() => { if (!closed) emit('message', data); });\n },\n /** @internal Called by the dispatcher when the parent sends\n * {__msgport: portId, op: 'close'}. Fires the local 'close' event\n * and stops accepting further messages. */\n _internalClose() {\n if (closed) return;\n closed = true;\n childPortRegistry.delete(portId);\n Promise.resolve().then(() => emit('close'));\n },\n on(ev, fn) {\n if (!listeners.has(ev)) listeners.set(ev, []);\n listeners.get(ev).push(fn);\n return port;\n },\n once(ev, fn) {\n const w = function () { port.off(ev, w); fn.apply(null, arguments); };\n return port.on(ev, w);\n },\n off(ev, fn) {\n const arr = listeners.get(ev);\n if (arr) listeners.set(ev, arr.filter(function (f) { return f !== fn; }));\n return port;\n },\n addListener(ev, fn) { return port.on(ev, fn); },\n removeListener(ev, fn) { return port.off(ev, fn); },\n emit(ev /* , ...args */) {\n emit.apply(null, Array.prototype.slice.call(arguments));\n return port;\n },\n postMessage(data) {\n if (closed) return;\n send({ __msgport: portId, op: 'send', data });\n },\n close() {\n if (closed) return;\n closed = true;\n try { send({ __msgport: portId, op: 'close' }); } catch (_) {}\n childPortRegistry.delete(portId);\n Promise.resolve().then(() => emit('close'));\n },\n start() { /* no-op \u2014 child-side port dispatches on receive */ },\n ref() { return port; },\n unref() { return port; },\n get _isCrossProcess() { return true; },\n get constructor() { return { name: 'MessagePort' }; },\n };\n\n childPortRegistry.set(portId, port);\n return port;\n}\n\nfunction isPortPlaceholder(value) {\n return value !== null && typeof value === 'object'\n && value.__gjsifyTransferredPort === true\n && typeof value.portId === 'number';\n}\n\n// Drain SharedBuffer fds attached to the init line (workerData may carry\n// SharedBuffer instances) before materialise() runs against it.\nif (init.sabSocketFd === 3) {\n try {\n drainSabFds(countSabPlaceholders(init.workerData));\n } catch (err) {\n send({ type: 'error', message: 'init fd drain failed: ' + (err && err.message ? err.message : err), stack: '' });\n }\n}\n\n// Simplified EventEmitter for parentPort\nconst _listeners = new Map();\nconst parentPort = {\n on(ev, fn) {\n if (!_listeners.has(ev)) _listeners.set(ev, []);\n _listeners.get(ev).push(fn);\n return this;\n },\n once(ev, fn) {\n const w = (...a) => { parentPort.off(ev, w); fn(...a); };\n return parentPort.on(ev, w);\n },\n off(ev, fn) {\n const a = _listeners.get(ev);\n if (a) _listeners.set(ev, a.filter(f => f !== fn));\n return this;\n },\n emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },\n postMessage(data) { send({ type: 'message', data }); },\n close() { send({ type: 'exit', code: 0 }); requestShutdown(); },\n removeAllListeners(ev) {\n if (ev) _listeners.delete(ev); else _listeners.clear();\n return this;\n },\n};\n\n// Set worker context globals (read by @gjsify/worker_threads when imported by user script)\nglobalThis.__gjsify_worker_context = {\n isMainThread: false,\n parentPort,\n workerData: materialise(init.workerData ?? null),\n threadId: init.threadId ?? 0,\n};\n\nsend({ type: 'online' });\n\n// Async stdin reader for messages from parent\nfunction readNext() {\n dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {\n try {\n const [line] = source.read_line_finish_utf8(result);\n if (line === null) {\n // EOF on stdin = the spawner is gone. Nothing can reach us again and\n // nobody is left to observe an exit code, so leave immediately \u2014\n // even from inside the worker script's own pending top-level await.\n requestShutdown();\n System.exit(0);\n return;\n }\n const msg = JSON.parse(line);\n if (typeof msg.__msgport === 'number') {\n // Cross-process MessagePort traffic \u2014 route to the local child-\n // side port from childPortRegistry by portId. Both 'send' and\n // 'close' dispatch on a microtask inside the port's helpers, so\n // user listeners fire after the current stdin-callback unwinds.\n const cpPort = childPortRegistry.get(msg.__msgport);\n if (cpPort) {\n if (msg.op === 'send') cpPort._receive(msg.data);\n else if (msg.op === 'close') cpPort._internalClose();\n }\n // Unknown portId or op silently dropped \u2014 could happen mid-close\n // race where the parent sends a final message after the child has\n // already torn down the local port.\n }\n else if (msg.type === 'message') {\n // Drain fds for any SharedBuffer placeholders BEFORE materialise.\n // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS\n // messages before writing this stdin line, so they're already\n // buffered.\n if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));\n parentPort.emit('message', materialise(msg.data));\n }\n else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); requestShutdown(); return; }\n readNext();\n } catch (err) {\n try {\n send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });\n } catch (_) { /* parent's end of stdout is gone too \u2014 nothing to report to */ }\n requestShutdown();\n }\n });\n}\nreadNext();\n\n// Execute worker code\ntry {\n if (init.eval) {\n const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;\n await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(\n parentPort, globalThis.__gjsify_worker_context.workerData, init.threadId\n );\n } else {\n await import(init.filename);\n }\n} catch (error) {\n send({ type: 'error', message: error.message, stack: error.stack || '' });\n}\n\n// Skip the loop entirely when shutdown was requested while the script above\n// was still running \u2014 otherwise that lost quit() strands the child forever.\nif (!shuttingDown) loop.run();\n";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/worker_threads",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Node.js worker_threads module for Gjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "lib/esm/index.js",
|
|
@@ -39,18 +39,18 @@
|
|
|
39
39
|
"worker_threads"
|
|
40
40
|
],
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@gjsify/cli": "^0.
|
|
43
|
-
"@gjsify/node-globals": "^0.
|
|
44
|
-
"@gjsify/unit": "^0.
|
|
42
|
+
"@gjsify/cli": "^0.23.0",
|
|
43
|
+
"@gjsify/node-globals": "^0.23.0",
|
|
44
|
+
"@gjsify/unit": "^0.23.0",
|
|
45
45
|
"@types/node": "^25.9.2",
|
|
46
46
|
"typescript": "^6.0.3"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@girs/gio-2.0": "^4.1.0",
|
|
50
50
|
"@girs/glib-2.0": "^4.1.0",
|
|
51
|
-
"@gjsify/events": "^0.
|
|
52
|
-
"@gjsify/message-channel": "^0.
|
|
53
|
-
"@gjsify/sab-native": "^0.
|
|
51
|
+
"@gjsify/events": "^0.23.0",
|
|
52
|
+
"@gjsify/message-channel": "^0.23.0",
|
|
53
|
+
"@gjsify/sab-native": "^0.23.0"
|
|
54
54
|
},
|
|
55
55
|
"gjsify": {
|
|
56
56
|
"runtimes": {
|