@gjsify/worker_threads 0.64.13 → 0.64.15-next.287

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +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,"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
+ import"./_virtual/_rolldown/runtime.js";import{isSharedBuffer as e}from"./sab-transfer.js";import{createDataCloneError as t}from"./structured-clone-wire.js";import{EventEmitter as n}from"node:events";import{MessagePort as r}from"@gjsify/message-channel";function isTransferredPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0}var i=class MessagePort extends n{_closed=!1;_detached=!1;_otherPort=null;_inner=new r;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(n,r){if(this._closed)return;let i=this._otherPort;if(!i&&this._inner._transport!==null){if(r&&r.length>0)throw t(`Transferring objects through a MessagePort that crosses a worker boundary is not supported; send the port with Worker.postMessage or parentPort.postMessage instead`);this._inner.postMessage(n);return}if(!i)return;let a=[],o=[];if(r&&r.length>0){let n=new Set;for(let i of r){if(n.has(i))throw t(`Transfer list contains duplicate entries`);if(n.add(i),i instanceof MessagePort){if(i===this)throw t(`Cannot transfer source port`);if(i._closed||i._detached)throw t(`MessagePort in transfer list is already detached`);o.push(i);continue}let s=Object.prototype.toString.call(i).slice(8,-1);if(s===`ArrayBuffer`){let e=i;if(e.detached===!0)throw t(`ArrayBuffer in transfer list is detached`);a.push(e);continue}if(s===`SharedArrayBuffer`)throw t(`SharedArrayBuffer cannot appear in transfer list (it is shared, not transferred)`);if(!e(i))throw t(`Value at index ${r.indexOf(i)} of transfer list is not transferable`)}}let s=o.slice(),c=n;o.length>0&&(c=substitutePortsWithPlaceholders(n,o));let l;try{l=structuredClone(c,{transfer:a.length>0?a:void 0})}catch(e){throw e instanceof Error?e:t(`Could not clone message`)}let u=l;if(s.length>0){let e=s.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});u=replacePlaceholdersWithPorts(l,e)}i._receiveMessage(u)}ref(){return this}unref(){return this}_receiveMessage(e){this._closed||(this._inner._receive(e),this._inner._started&&this._dispatchEmit(e))}_receiveMessageError(e){if(this._closed)return;let t=e instanceof Error?e:Error(String(e));Promise.resolve().then(()=>{this._closed||this.emit(`messageerror`,t)})}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 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 i){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{i as MessagePort};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";const e=[`Int8Array`,`Uint8Array`,`Uint8ClampedArray`,`Int16Array`,`Uint16Array`,`Int32Array`,`Uint32Array`,`Float32Array`,`Float64Array`,`BigInt64Array`,`BigUint64Array`];function toBase64(e){let t=new Uint8Array(e),n=``;for(let e=0;e<t.length;e++)n+=String.fromCharCode(t[e]);return btoa(n)}function fromBase64(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return n}function encode(t,n=new Map){if(t==null)return t;if(typeof t==`bigint`)return{__gjsify:`BigInt`,value:t.toString()};if(t===1/0)return{__gjsify:`Infinity`};if(t===-1/0)return{__gjsify:`NegInfinity`};if(typeof t==`number`&&Number.isNaN(t))return{__gjsify:`NaN`};if(typeof t!=`object`)return t;if(n.has(t))throw Error(`DataCloneError: circular reference`);if(t instanceof Date)return{__gjsify:`Date`,value:t.getTime()};if(t instanceof RegExp){let e=t;return{__gjsify:`RegExp`,source:e.source,flags:e.flags}}if(t instanceof Error){let e=t;return{__gjsify:`Error`,name:e.name,message:e.message,stack:e.stack}}if(t instanceof Map){n.set(t,null);let e=[];for(let[r,i]of t)e.push([encode(r,n),encode(i,n)]);return{__gjsify:`Map`,entries:e}}if(t instanceof Set){n.set(t,null);let e=[];for(let r of t)e.push(encode(r,n));return{__gjsify:`Set`,values:e}}let r=Object.prototype.toString.call(t).slice(8,-1);if(r===`ArrayBuffer`)return{__gjsify:`ArrayBuffer`,data:toBase64(t)};if(e.includes(r)){let e=t;return{__gjsify:r,data:toBase64(e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength))}}if(Array.isArray(t))return n.set(t,null),t.map(e=>encode(e,n));if(r===`Object`){n.set(t,null);let e={};for(let r of Object.keys(t))e[r]=encode(t[r],n);return e}throw Error(`DataCloneError: value could not be cloned`)}function decode(t){if(typeof t!=`object`||!t)return t;if(Array.isArray(t))return t.map(e=>decode(e));let n=t;if(typeof n.__gjsify==`string`)switch(n.__gjsify){case`BigInt`:return BigInt(n.value);case`Infinity`:return 1/0;case`NegInfinity`:return-1/0;case`NaN`:return NaN;case`Date`:return new Date(n.value);case`RegExp`:return new RegExp(n.source,n.flags);case`Error`:{let e=Error(n.message);return e.name=n.name,n.stack&&(e.stack=n.stack),e}case`Map`:{let e=new Map;for(let[t,r]of n.entries)e.set(decode(t),decode(r));return e}case`Set`:{let e=new Set;for(let t of n.values)e.add(decode(t));return e}case`ArrayBuffer`:return fromBase64(n.data).buffer;default:if(e.includes(n.__gjsify)){let e=fromBase64(n.data),t=globalThis[n.__gjsify];return new t(e.buffer)}return t}if(Object.prototype.toString.call(t).slice(8,-1)===`Object`){let e={};for(let t of Object.keys(n))e[t]=decode(n[t]);return e}return t}function createDataCloneError(e){let t=globalThis.DOMException;if(typeof t==`function`)return new t(e,`DataCloneError`);let n=Error(e);return n.name=`DataCloneError`,n.code=25,n}export{createDataCloneError,decode,encode};
1
+ import"./_virtual/_rolldown/runtime.js";const e=[`Int8Array`,`Uint8Array`,`Uint8ClampedArray`,`Int16Array`,`Uint16Array`,`Int32Array`,`Uint32Array`,`Float32Array`,`Float64Array`,`BigInt64Array`,`BigUint64Array`],t={Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError},n=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`,r=new Uint8Array(128);for(let e=0;e<64;e++)r[n.charCodeAt(e)]=e;function toBase64(e){let t=new Uint8Array(e),r=``;for(let e=0;e<t.length;e+=3){let i=t[e],a=e+1<t.length,o=e+2<t.length,s=i<<16|(a?t[e+1]:0)<<8|(o?t[e+2]:0);r+=n[s>>18&63]+n[s>>12&63]+(a?n[s>>6&63]:`=`)+(o?n[s&63]:`=`)}return r}function fromBase64(e){let t=e.length;for(;t>0&&e.charCodeAt(t-1)===61;)t--;let n=new Uint8Array(t*3>>2),i=0,a=0,o=0;for(let s=0;s<t;s++)i=i<<6|r[e.charCodeAt(s)],a+=6,a>=8&&(a-=8,n[o++]=i>>a&255);return n}function isHoleTag(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&e.__gjsify===`Hole`}function encode(n,r=new Map){if(n===null)return null;if(n===void 0)return{__gjsify:`Undefined`};switch(typeof n){case`bigint`:return{__gjsify:`BigInt`,value:n.toString()};case`number`:return n===1/0?{__gjsify:`Infinity`}:n===-1/0?{__gjsify:`NegInfinity`}:Number.isNaN(n)?{__gjsify:`NaN`}:Object.is(n,-0)?{__gjsify:`NegZero`}:n;case`string`:case`boolean`:return n;case`function`:case`symbol`:throw createDataCloneError(`${String(n)} could not be cloned.`);default:break}let i=r.get(n);if(i!==void 0)return{__gjsify:`Ref`,id:i};if(r.set(n,r.size),n instanceof Date)return{__gjsify:`Date`,value:n.getTime()};if(n instanceof RegExp){let e=n;return{__gjsify:`RegExp`,source:e.source,flags:e.flags}}if(n instanceof Error){let e=n;return{__gjsify:`Error`,name:t[e.name]===void 0?`Error`:e.name,message:e.message,stack:e.stack}}if(n instanceof Map){let e=[];for(let[t,i]of n)e.push([encode(t,r),encode(i,r)]);return{__gjsify:`Map`,entries:e}}if(n instanceof Set){let e=[];for(let t of n)e.push(encode(t,r));return{__gjsify:`Set`,values:e}}let a=Object.prototype.toString.call(n).slice(8,-1);if(a===`ArrayBuffer`)return{__gjsify:`ArrayBuffer`,data:toBase64(n)};if(a===`DataView`||e.includes(a)){let e=n;return{__gjsify:a,data:toBase64(e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength))}}if(Array.isArray(n)){let e=[];for(let t=0;t<n.length;t++)e.push(t in n?encode(n[t],r):{__gjsify:`Hole`});return e}if(a===`Object`){let e=n,t={};for(let n of Object.keys(e))t[n]=encode(e[n],r);return typeof e.__gjsify==`string`?{__gjsify:`Object`,value:t}:t}throw createDataCloneError(`value could not be cloned.`)}function decode(n,r=[]){if(typeof n!=`object`||!n)return n;if(Array.isArray(n)){let e=[];e.length=n.length,r.push(e);for(let t=0;t<n.length;t++)isHoleTag(n[t])||(e[t]=decode(n[t],r));return e}let i=n;if(typeof i.__gjsify==`string`)switch(i.__gjsify){case`Ref`:return r[i.id];case`Undefined`:return;case`Hole`:return;case`BigInt`:return BigInt(i.value);case`Infinity`:return 1/0;case`NegInfinity`:return-1/0;case`NaN`:return NaN;case`NegZero`:return-0;case`Date`:{let e=new Date(i.value);return r.push(e),e}case`RegExp`:{let e=new RegExp(i.source,i.flags);return r.push(e),e}case`Error`:{let e=new(t[i.name]??Error)(i.message);return i.stack&&(e.stack=i.stack),r.push(e),e}case`Map`:{let e=new Map;r.push(e);for(let[t,n]of i.entries)e.set(decode(t,r),decode(n,r));return e}case`Set`:{let e=new Set;r.push(e);for(let t of i.values)e.add(decode(t,r));return e}case`ArrayBuffer`:{let e=fromBase64(i.data).buffer;return r.push(e),e}case`DataView`:{let e=new DataView(fromBase64(i.data).buffer);return r.push(e),e}case`Object`:{let e=i.value,t={};r.push(t);for(let n of Object.keys(e))t[n]=decode(e[n],r);return t}default:if(e.includes(i.__gjsify)){let e=fromBase64(i.data),t=globalThis[i.__gjsify],n=new t(e.buffer);return r.push(n),n}return r.push(n),n}if(Object.prototype.toString.call(n).slice(8,-1)===`Object`){let e={};r.push(e);for(let t of Object.keys(i))e[t]=decode(i[t],r);return e}return r.push(n),n}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}export{createDataCloneError,decode,encode};
@@ -90,62 +90,107 @@ function drainSabFds(count) {
90
90
  }
91
91
  }
92
92
 
93
- // Tagged JSON codec for structured-clone types (mirrors structured-clone-wire.ts).
93
+ // --- GJSIFY_WIRE_CODEC_BEGIN ---
94
+ // Tagged JSON codec for structured-clone types — the child-side MIRROR of
95
+ // structured-clone-wire.ts. Everything between the BEGIN/END markers is
96
+ // sliced out of this string by structured-clone-wire.spec.ts and run through
97
+ // the same case matrix as the TS copy, so the two cannot drift unnoticed.
98
+ // The spec citations live in the TS file; keep the behaviour identical.
94
99
  const __TYPED_ARRAYS = ['Int8Array','Uint8Array','Uint8ClampedArray','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array','BigInt64Array','BigUint64Array'];
95
- function __toB64(buf){const b=new Uint8Array(buf);let s='';for(let i=0;i<b.length;i++)s+=String.fromCharCode(b[i]);return btoa(s);}
96
- function __fromB64(s){const bin=atob(s);const out=new Uint8Array(bin.length);for(let i=0;i<bin.length;i++)out[i]=bin.charCodeAt(i);return out;}
97
- function encodeWire(value, seen){
98
- if(value===null||value===undefined) return value;
99
- if(typeof value==='bigint') return {__gjsify:'BigInt', value:value.toString()};
100
- if(value===Infinity) return {__gjsify:'Infinity'};
101
- if(value===-Infinity) return {__gjsify:'NegInfinity'};
102
- if(typeof value==='number' && isNaN(value)) return {__gjsify:'NaN'};
103
- if(typeof value!=='object') return value;
100
+ const __ERROR_CTORS = {Error:Error,EvalError:EvalError,RangeError:RangeError,ReferenceError:ReferenceError,SyntaxError:SyntaxError,TypeError:TypeError,URIError:URIError};
101
+ // No btoa/atob here: this code is evaluated by a bare gjs. See the comment on
102
+ // toBase64 in structured-clone-wire.ts for what depending on them cost.
103
+ const __B64C='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
104
+ const __B64D=new Uint8Array(128);for(let i=0;i<64;i++)__B64D[__B64C.charCodeAt(i)]=i;
105
+ function __toB64(buf){const b=new Uint8Array(buf);let s='';for(let i=0;i<b.length;i+=3){const h1=i+1<b.length,h2=i+2<b.length;const n=(b[i]<<16)|((h1?b[i+1]:0)<<8)|(h2?b[i+2]:0);s+=__B64C[(n>>18)&63]+__B64C[(n>>12)&63]+(h1?__B64C[(n>>6)&63]:'=')+(h2?__B64C[n&63]:'=');}return s;}
106
+ function __fromB64(s){let len=s.length;while(len>0&&s.charCodeAt(len-1)===61)len--;const out=new Uint8Array((len*3)>>2);let acc=0,bits=0,o=0;for(let i=0;i<len;i++){acc=(acc<<6)|__B64D[s.charCodeAt(i)];bits+=6;if(bits>=8){bits-=8;out[o++]=(acc>>bits)&255;}}return out;}
107
+ function __dataCloneError(message){
108
+ if(typeof DOMException==='function'){const e=new DOMException(message,'DataCloneError');if(e.code===undefined){try{Object.defineProperty(e,'code',{value:25,configurable:true});}catch(_){}}return e;}
109
+ const e=new Error(message);e.name='DataCloneError';e.code=25;return e;
110
+ }
111
+ function __isHole(v){return typeof v==='object'&&v!==null&&!Array.isArray(v)&&v.__gjsify==='Hole';}
112
+ function encodeWire(value, refs){
113
+ if(refs===undefined) refs=new Map();
114
+ if(value===null) return null;
115
+ if(value===undefined) return {__gjsify:'Undefined'};
116
+ const t=typeof value;
117
+ if(t==='bigint') return {__gjsify:'BigInt', value:value.toString()};
118
+ if(t==='number'){
119
+ if(value===Infinity) return {__gjsify:'Infinity'};
120
+ if(value===-Infinity) return {__gjsify:'NegInfinity'};
121
+ if(isNaN(value)) return {__gjsify:'NaN'};
122
+ if(Object.is(value,-0)) return {__gjsify:'NegZero'};
123
+ return value;
124
+ }
125
+ if(t==='string'||t==='boolean') return value;
126
+ if(t==='function'||t==='symbol') throw __dataCloneError(String(value)+' could not be cloned.');
127
+ const seen=refs.get(value);
128
+ if(seen!==undefined) return {__gjsify:'Ref', id:seen};
129
+ refs.set(value, refs.size);
104
130
  if(value instanceof Date) return {__gjsify:'Date', value:value.getTime()};
105
131
  if(value instanceof RegExp) return {__gjsify:'RegExp', source:value.source, flags:value.flags};
106
- if(value instanceof Error) return {__gjsify:'Error', name:value.name, message:value.message, stack:value.stack};
107
- if(value instanceof Map){const e=[];for(const [k,v] of value) e.push([encodeWire(k,seen),encodeWire(v,seen)]);return {__gjsify:'Map', entries:e};}
108
- if(value instanceof Set){const v=[];for(const x of value) v.push(encodeWire(x,seen));return {__gjsify:'Set', values:v};}
132
+ if(value instanceof Error){const n=__ERROR_CTORS[value.name]===undefined?'Error':value.name;return {__gjsify:'Error', name:n, message:value.message, stack:value.stack};}
133
+ if(value instanceof Map){const e=[];for(const [k,v] of value) e.push([encodeWire(k,refs),encodeWire(v,refs)]);return {__gjsify:'Map', entries:e};}
134
+ if(value instanceof Set){const v=[];for(const x of value) v.push(encodeWire(x,refs));return {__gjsify:'Set', values:v};}
109
135
  const tag=Object.prototype.toString.call(value).slice(8,-1);
110
136
  if(tag==='ArrayBuffer') return {__gjsify:'ArrayBuffer', data:__toB64(value)};
111
- if(__TYPED_ARRAYS.indexOf(tag)!==-1){const ta=value;return {__gjsify:tag, data:__toB64(ta.buffer.slice(ta.byteOffset, ta.byteOffset+ta.byteLength))};}
112
- if(Array.isArray(value)) return value.map(function(x){return encodeWire(x,seen);});
113
- if(tag==='Object'){const out={};for(const k of Object.keys(value)) out[k]=encodeWire(value[k],seen);return out;}
114
- throw new Error('DataCloneError: value could not be cloned');
137
+ if(tag==='DataView'||__TYPED_ARRAYS.indexOf(tag)!==-1){const ta=value;return {__gjsify:tag, data:__toB64(ta.buffer.slice(ta.byteOffset, ta.byteOffset+ta.byteLength))};}
138
+ if(Array.isArray(value)){const out=[];for(let i=0;i<value.length;i++) out.push(i in value?encodeWire(value[i],refs):{__gjsify:'Hole'});return out;}
139
+ if(tag==='Object'){const out={};for(const k of Object.keys(value)) out[k]=encodeWire(value[k],refs);if(typeof value.__gjsify==='string') return {__gjsify:'Object', value:out};return out;}
140
+ throw __dataCloneError('value could not be cloned.');
115
141
  }
116
- function decodeWire(value){
142
+ function decodeWire(value, refs){
143
+ if(refs===undefined) refs=[];
117
144
  if(value===null||value===undefined) return value;
118
145
  if(typeof value!=='object') return value;
119
- if(Array.isArray(value)) return value.map(decodeWire);
146
+ if(Array.isArray(value)){const out=[];out.length=value.length;refs.push(out);for(let i=0;i<value.length;i++){if(__isHole(value[i])) continue;out[i]=decodeWire(value[i],refs);}return out;}
120
147
  const obj=value;
121
148
  if(typeof obj.__gjsify==='string'){
122
149
  switch(obj.__gjsify){
150
+ case 'Ref': return refs[obj.id];
151
+ case 'Undefined': return undefined;
152
+ case 'Hole': return undefined;
123
153
  case 'BigInt': return BigInt(obj.value);
124
154
  case 'Infinity': return Infinity;
125
155
  case 'NegInfinity': return -Infinity;
126
156
  case 'NaN': return NaN;
127
- case 'Date': return new Date(obj.value);
128
- case 'RegExp': return new RegExp(obj.source, obj.flags);
129
- case 'Error': {const e=new Error(obj.message); e.name=obj.name; if(obj.stack) e.stack=obj.stack; return e;}
130
- case 'Map': {const m=new Map(); for(const [k,v] of obj.entries) m.set(decodeWire(k),decodeWire(v)); return m;}
131
- case 'Set': {const s=new Set(); for(const v of obj.values) s.add(decodeWire(v)); return s;}
132
- case 'ArrayBuffer': return __fromB64(obj.data).buffer;
157
+ case 'NegZero': return -0;
158
+ case 'Date': {const d=new Date(obj.value);refs.push(d);return d;}
159
+ case 'RegExp': {const r=new RegExp(obj.source, obj.flags);refs.push(r);return r;}
160
+ case 'Error': {const C=__ERROR_CTORS[obj.name]||Error;const e=new C(obj.message);if(obj.stack) e.stack=obj.stack;refs.push(e);return e;}
161
+ case 'Map': {const m=new Map();refs.push(m);for(const [k,v] of obj.entries) m.set(decodeWire(k,refs),decodeWire(v,refs));return m;}
162
+ case 'Set': {const s=new Set();refs.push(s);for(const v of obj.values) s.add(decodeWire(v,refs));return s;}
163
+ case 'ArrayBuffer': {const b=__fromB64(obj.data).buffer;refs.push(b);return b;}
164
+ case 'DataView': {const d=new DataView(__fromB64(obj.data).buffer);refs.push(d);return d;}
165
+ case 'Object': {const out={};refs.push(out);for(const k of Object.keys(obj.value)) out[k]=decodeWire(obj.value[k],refs);return out;}
133
166
  default: {
134
167
  if(__TYPED_ARRAYS.indexOf(obj.__gjsify)!==-1){
135
168
  const bytes=__fromB64(obj.data);
136
169
  const Ctor=globalThis[obj.__gjsify];
137
- return new Ctor(bytes.buffer);
170
+ const ta=new Ctor(bytes.buffer);
171
+ refs.push(ta);
172
+ return ta;
138
173
  }
174
+ refs.push(value);
139
175
  return value;
140
176
  }
141
177
  }
142
178
  }
143
179
  const tag=Object.prototype.toString.call(value).slice(8,-1);
144
- if(tag==='Object'){const out={};for(const k of Object.keys(obj)) out[k]=decodeWire(obj[k]);return out;}
180
+ if(tag==='Object'){const out={};refs.push(out);for(const k of Object.keys(obj)) out[k]=decodeWire(obj[k],refs);return out;}
181
+ refs.push(value);
145
182
  return value;
146
183
  }
184
+ // --- GJSIFY_WIRE_CODEC_END ---
147
185
 
148
- function materialise(value) {
186
+ // Walks the DECODED tree, so it has to cope with what the wire codec now
187
+ // reconstructs: shared and cyclic references (hence \`seen\`, or a self-
188
+ // referencing workerData recurses until the child dies) and sparse arrays
189
+ // (hence \`i in value\`, or every hole becomes an own \`undefined\` property and
190
+ // the array stops being sparse). The parent-side equivalents in
191
+ // sab-transfer.ts guard both already.
192
+ function materialise(value, seen) {
193
+ if (seen === undefined) seen = new Set();
149
194
  if (value === null || typeof value !== 'object') return value;
150
195
  if (typeof value.__sab === 'number' && typeof value.size === 'number') {
151
196
  const fd = sabFds.get(value.__sab);
@@ -158,12 +203,14 @@ function materialise(value) {
158
203
  if (isPortPlaceholder(value)) {
159
204
  return makeChildSidePort(value.portId);
160
205
  }
206
+ if (seen.has(value)) return value;
207
+ seen.add(value);
161
208
  if (Array.isArray(value)) {
162
- for (let i = 0; i < value.length; i++) value[i] = materialise(value[i]);
209
+ for (let i = 0; i < value.length; i++) if (i in value) value[i] = materialise(value[i], seen);
163
210
  return value;
164
211
  }
165
212
  if (Object.prototype.toString.call(value) === '[object Object]') {
166
- for (const k of Object.keys(value)) value[k] = materialise(value[k]);
213
+ for (const k of Object.keys(value)) value[k] = materialise(value[k], seen);
167
214
  return value;
168
215
  }
169
216
  return value;
@@ -192,97 +239,291 @@ function makeSharedBuffer(native) {
192
239
  };
193
240
  }
194
241
 
195
- // Cross-process MessagePort support (child side).
242
+ // Child-side MessagePort — ONE class for every port this child owns.
243
+ //
244
+ // A port is either linked to a peer in this process (made by the inline
245
+ // MessageChannel below) or routed over the wire (_wireId set): a port the
246
+ // parent transferred in, or the kept end of a channel this child sent to the
247
+ // parent. It offers both surfaces Node's worker_threads MessagePort has — the
248
+ // EventEmitter one (on/once/off/emit) and the DOM one (addEventListener,
249
+ // onmessage, start/close, MessageEvent-shaped {data}). Like Node, attaching a
250
+ // 'message' listener through either surface (or assigning onmessage) starts
251
+ // the port; messages that arrive earlier are queued, not dropped.
196
252
  //
197
- // When a parent's Worker.postMessage(value, [port2]) transfers a
198
- // MessagePort, the parent encodes the port as a placeholder
199
- // { __gjsifyTransferredPort: true, portId: <id> } and ships the rest of
200
- // the value as JSON. On this side, materialise() walks the parsed JSON
201
- // and substitutes each placeholder for a fresh "child-side port" — an
202
- // inline EventEmitter-shaped object that mirrors the worker_threads
203
- // MessagePort surface enough for user code (postMessage / on('message') /
204
- // on('close') / close()). Outbound traffic via this port travels over
205
- // stdout as { __msgport, op: 'send', data } JSON lines, which the parent's
206
- // stdout-reader routes back to the kept end via the parent-side registry.
253
+ // Outbound wire traffic travels over stdout as { __msgport, op, data } lines,
254
+ // which the parent's stdout reader routes back by the same id. Ids are even
255
+ // here (2, 4, …) and odd on the parent, so the two namespaces never collide.
207
256
  //
208
- // transferList chaining (port-in-port) is intentionally not supported
209
- // on the cross-process path in v1 — see status/open-todos/README.md.
257
+ // Not supported: transferring a port THROUGH a port (port-in-port). It would
258
+ // need a per-message port table on both ends of the wire; throwing a
259
+ // DataCloneError says so loudly instead. See the step 4 notes in
260
+ // docs/reports/2026-10-30-web-worker-and-comlink-on-gjs.md.
210
261
  const childPortRegistry = new Map();
262
+ let nextChildPortId = 2;
263
+ const PORT_TOKEN = {};
211
264
 
212
- function makeChildSidePort(portId) {
213
- const listeners = new Map();
214
- let closed = false;
265
+ class MessagePort {
266
+ constructor(token) {
267
+ if (token !== PORT_TOKEN) throw new TypeError('Illegal constructor');
268
+ this._ee = new Map();
269
+ this._dom = new Map();
270
+ this._onmessage = null;
271
+ this._started = false;
272
+ this._closed = false;
273
+ this._detached = false;
274
+ this._queue = [];
275
+ this._partner = null;
276
+ this._wireId = 0;
277
+ }
278
+
279
+ // --- delivery -------------------------------------------------------------
215
280
 
216
- function emit(ev /* , ...args */) {
217
- const args = Array.prototype.slice.call(arguments, 1);
218
- const fns = (listeners.get(ev) || []).slice();
281
+ /** @internal An already-decoded value arrives; queued until start(). */
282
+ _deliver(value) {
283
+ if (this._closed) return;
284
+ if (!this._started) { this._queue.push(value); return; }
285
+ this._schedule(value);
286
+ }
287
+
288
+ _schedule(value) {
289
+ Promise.resolve().then(() => {
290
+ if (this._closed) return;
291
+ this._emitEE('message', [value]);
292
+ this._emitDom('message', value);
293
+ });
294
+ }
295
+
296
+ /** @internal Wire delivery: the stdin dispatcher hands over the encoded payload. */
297
+ _receive(data) {
298
+ if (this._closed) return;
299
+ // A payload this end cannot decode becomes 'messageerror' on the port.
300
+ // Letting decodeWire throw here would reject a microtask, and GJS has no
301
+ // unhandled-rejection surface — the message would vanish.
302
+ let decoded;
303
+ try { decoded = decodeWire(data); }
304
+ catch (err) {
305
+ Promise.resolve().then(() => {
306
+ if (!this._closed) this._emitEE('messageerror', [err instanceof Error ? err : new Error(String(err))]);
307
+ });
308
+ return;
309
+ }
310
+ this._deliver(decoded);
311
+ }
312
+
313
+ /** @internal The other end closed — no echo back. */
314
+ _internalClose() { this._shutdown(false); }
315
+
316
+ _emitEE(ev, args) {
317
+ const fns = (this._ee.get(ev) || []).slice();
219
318
  for (const fn of fns) {
220
- try { fn.apply(null, args); } catch (_) { /* swallow */ }
319
+ try { fn.apply(this, args); } catch (_) { /* swallow */ }
221
320
  }
222
321
  }
223
322
 
224
- const port = {
225
- _portId: portId,
226
- /** @internal Wire-side delivery — called by the bootstrap stdin
227
- * dispatcher when it receives a {__msgport: portId, op: 'send'}
228
- * line. Schedules dispatch on a microtask so listeners fire after
229
- * the current call stack unwinds — matches in-process MessagePort
230
- * semantics. */
231
- _receive(data) {
232
- if (closed) return;
233
- Promise.resolve().then(() => { if (!closed) emit('message', decodeWire(data)); });
234
- },
235
- /** @internal Called by the dispatcher when the parent sends
236
- * {__msgport: portId, op: 'close'}. Fires the local 'close' event
237
- * and stops accepting further messages. */
238
- _internalClose() {
239
- if (closed) return;
240
- closed = true;
241
- childPortRegistry.delete(portId);
242
- Promise.resolve().then(() => emit('close'));
243
- },
244
- on(ev, fn) {
245
- if (!listeners.has(ev)) listeners.set(ev, []);
246
- listeners.get(ev).push(fn);
247
- return port;
248
- },
249
- once(ev, fn) {
250
- const w = function () { port.off(ev, w); fn.apply(null, arguments); };
251
- return port.on(ev, w);
252
- },
253
- off(ev, fn) {
254
- const arr = listeners.get(ev);
255
- if (arr) listeners.set(ev, arr.filter(function (f) { return f !== fn; }));
256
- return port;
257
- },
258
- addListener(ev, fn) { return port.on(ev, fn); },
259
- removeListener(ev, fn) { return port.off(ev, fn); },
260
- emit(ev /* , ...args */) {
261
- emit.apply(null, Array.prototype.slice.call(arguments));
262
- return port;
263
- },
264
- postMessage(data) {
265
- if (closed) return;
266
- send({ __msgport: portId, op: 'send', data: encodeWire(data) });
267
- },
268
- close() {
269
- if (closed) return;
270
- closed = true;
271
- try { send({ __msgport: portId, op: 'close' }); } catch (_) {}
272
- childPortRegistry.delete(portId);
273
- Promise.resolve().then(() => emit('close'));
274
- },
275
- start() { /* no-op — child-side port dispatches on receive */ },
276
- ref() { return port; },
277
- unref() { return port; },
278
- get _isCrossProcess() { return true; },
279
- get constructor() { return { name: 'MessagePort' }; },
280
- };
323
+ _emitDom(type, data) {
324
+ const event = { type: type, data: data, target: this, currentTarget: this, ports: [], origin: '', lastEventId: '', source: null };
325
+ const fns = (this._dom.get(type) || []).slice();
326
+ for (const fn of fns) {
327
+ try { if (typeof fn === 'function') fn.call(this, event); else fn.handleEvent(event); } catch (_) { /* swallow */ }
328
+ }
329
+ if (type === 'message' && typeof this._onmessage === 'function') {
330
+ try { this._onmessage.call(this, event); } catch (_) { /* swallow */ }
331
+ }
332
+ }
333
+
334
+ _shutdown(notifyPeer) {
335
+ if (this._closed) return;
336
+ this._closed = true;
337
+ const peer = this._partner;
338
+ this._partner = null;
339
+ if (this._wireId) {
340
+ if (notifyPeer) {
341
+ try { send({ __msgport: this._wireId, op: 'close' }); } catch (_) { /* parent's stdout is gone */ }
342
+ }
343
+ childPortRegistry.delete(this._wireId);
344
+ }
345
+ // A local pair closes together, each end firing its own 'close'.
346
+ if (peer) { peer._partner = null; peer._shutdown(false); }
347
+ Promise.resolve().then(() => {
348
+ this._emitEE('close', []);
349
+ this._emitDom('close', undefined);
350
+ this._ee.clear();
351
+ this._dom.clear();
352
+ maybeExit();
353
+ });
354
+ }
355
+
356
+ // --- the port surface -----------------------------------------------------
357
+
358
+ postMessage(data, transferList) {
359
+ if (this._closed) return;
360
+ if (transferList) {
361
+ for (const item of transferList) {
362
+ if (item instanceof MessagePort) {
363
+ throw __dataCloneError('Transferring a MessagePort through a MessagePort is not supported in a worker child; send it with parentPort.postMessage(value, [port]).');
364
+ }
365
+ }
366
+ }
367
+ const encoded = encodeWire(data);
368
+ if (this._wireId) {
369
+ send({ __msgport: this._wireId, op: 'send', data: encoded });
370
+ return;
371
+ }
372
+ const peer = this._partner;
373
+ if (peer) peer._deliver(decodeWire(encoded));
374
+ }
375
+
376
+ start() {
377
+ if (this._started || this._closed) return;
378
+ this._started = true;
379
+ for (const value of this._queue.splice(0)) this._schedule(value);
380
+ }
381
+
382
+ close() { this._shutdown(true); }
383
+ ref() { return this; }
384
+ unref() { return this; }
385
+
386
+ // --- EventEmitter surface -------------------------------------------------
281
387
 
388
+ on(ev, fn) {
389
+ if (!this._ee.has(ev)) this._ee.set(ev, []);
390
+ this._ee.get(ev).push(fn);
391
+ if (ev === 'message') this.start();
392
+ return this;
393
+ }
394
+ addListener(ev, fn) { return this.on(ev, fn); }
395
+ once(ev, fn) {
396
+ const self = this;
397
+ const wrapper = function () { self.off(ev, wrapper); return fn.apply(self, arguments); };
398
+ wrapper.listener = fn;
399
+ return this.on(ev, wrapper);
400
+ }
401
+ off(ev, fn) {
402
+ const arr = this._ee.get(ev);
403
+ if (arr) this._ee.set(ev, arr.filter(function (f) { return f !== fn && f.listener !== fn; }));
404
+ return this;
405
+ }
406
+ removeListener(ev, fn) { return this.off(ev, fn); }
407
+ removeAllListeners(ev) {
408
+ if (ev === undefined) this._ee.clear(); else this._ee.delete(ev);
409
+ return this;
410
+ }
411
+ listenerCount(ev) { return (this._ee.get(ev) || []).length; }
412
+ emit(ev /* , ...args */) {
413
+ const had = (this._ee.get(ev) || []).length > 0;
414
+ this._emitEE(ev, Array.prototype.slice.call(arguments, 1));
415
+ return had;
416
+ }
417
+
418
+ // --- DOM surface ----------------------------------------------------------
419
+
420
+ addEventListener(type, fn) {
421
+ if (!fn) return;
422
+ if (!this._dom.has(type)) this._dom.set(type, []);
423
+ const arr = this._dom.get(type);
424
+ if (arr.indexOf(fn) === -1) arr.push(fn);
425
+ if (type === 'message') this.start();
426
+ }
427
+ removeEventListener(type, fn) {
428
+ const arr = this._dom.get(type);
429
+ if (arr) this._dom.set(type, arr.filter(function (f) { return f !== fn; }));
430
+ }
431
+ get onmessage() { return this._onmessage; }
432
+ set onmessage(fn) {
433
+ this._onmessage = typeof fn === 'function' ? fn : null;
434
+ if (this._onmessage) this.start();
435
+ }
436
+ }
437
+
438
+ /** A port whose peer lives in the parent, routed by portId. */
439
+ function makeChildSidePort(portId) {
440
+ const port = new MessagePort(PORT_TOKEN);
441
+ port._wireId = portId;
282
442
  childPortRegistry.set(portId, port);
283
443
  return port;
284
444
  }
285
445
 
446
+ class MessageChannel {
447
+ constructor() {
448
+ const a = new MessagePort(PORT_TOKEN);
449
+ const b = new MessagePort(PORT_TOKEN);
450
+ a._partner = b;
451
+ b._partner = a;
452
+ this.port1 = a;
453
+ this.port2 = b;
454
+ }
455
+ }
456
+
457
+ // Bare gjs has neither; a bundled child's globals register only fills what is
458
+ // still undefined, so these win there too and every child port is one class.
459
+ if (typeof globalThis.MessageChannel === 'undefined') globalThis.MessageChannel = MessageChannel;
460
+ if (typeof globalThis.MessagePort === 'undefined') globalThis.MessagePort = MessagePort;
461
+
462
+ // parentPort.postMessage(value, [port]): turn each listed child port into a
463
+ // wire port. The kept end (the peer) is rebound to a fresh even id and
464
+ // registered; the transferred end is detached. Returns the value with each
465
+ // port replaced by a placeholder plus a commit() that performs the side
466
+ // effects — run it only after the value encoded, so a DataCloneError leaves
467
+ // every port untouched.
468
+ function planPortTransfer(value, transferList) {
469
+ const ports = [];
470
+ for (const item of transferList) {
471
+ if (!(item instanceof MessagePort)) continue;
472
+ if (ports.indexOf(item) !== -1) throw __dataCloneError('Transfer list contains duplicate entries');
473
+ if (item._closed || item._detached) throw __dataCloneError('MessagePort in transfer list is already detached');
474
+ if (item._wireId) throw __dataCloneError('A MessagePort that already crosses the worker boundary cannot be transferred again.');
475
+ ports.push(item);
476
+ }
477
+ if (ports.length === 0) return { value: value, commit: function () { return []; } };
478
+ const placeholders = new Map();
479
+ for (const port of ports) {
480
+ placeholders.set(port, { __gjsifyTransferredPort: true, portId: nextChildPortId });
481
+ nextChildPortId += 2;
482
+ }
483
+ function walk(v, seen) {
484
+ if (v === null || typeof v !== 'object') return v;
485
+ if (v instanceof MessagePort) return placeholders.has(v) ? placeholders.get(v) : v;
486
+ if (seen.has(v)) return seen.get(v);
487
+ if (Array.isArray(v)) {
488
+ const out = [];
489
+ seen.set(v, out);
490
+ for (let i = 0; i < v.length; i++) if (i in v) out[i] = walk(v[i], seen);
491
+ return out;
492
+ }
493
+ if (Object.prototype.toString.call(v) === '[object Object]') {
494
+ const out = {};
495
+ seen.set(v, out);
496
+ for (const k of Object.keys(v)) out[k] = walk(v[k], seen);
497
+ return out;
498
+ }
499
+ return v;
500
+ }
501
+ return {
502
+ value: walk(value, new Map()),
503
+ commit() {
504
+ const lines = [];
505
+ for (const port of ports) {
506
+ const portId = placeholders.get(port).portId;
507
+ const peer = port._partner;
508
+ port._partner = null;
509
+ port._detached = true;
510
+ port._closed = true;
511
+ if (peer) {
512
+ peer._partner = null;
513
+ peer._wireId = portId;
514
+ childPortRegistry.set(portId, peer);
515
+ // What the peer posted before the transfer waits in the detached
516
+ // port's queue; it travels right behind the carrying message.
517
+ for (const queued of port._queue.splice(0)) {
518
+ lines.push({ __msgport: portId, op: 'send', data: encodeWire(queued) });
519
+ }
520
+ }
521
+ }
522
+ return lines;
523
+ },
524
+ };
525
+ }
526
+
286
527
  function isPortPlaceholder(value) {
287
528
  return value !== null && typeof value === 'object'
288
529
  && value.__gjsifyTransferredPort === true
@@ -319,7 +560,15 @@ const parentPort = {
319
560
  return this;
320
561
  },
321
562
  emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },
322
- postMessage(data) { send({ type: 'message', data: encodeWire(data) }); },
563
+ postMessage(data, transferList) {
564
+ let plan = null;
565
+ if (transferList && transferList.length > 0) {
566
+ plan = planPortTransfer(data, transferList);
567
+ data = plan.value;
568
+ }
569
+ send({ type: 'message', data: encodeWire(data) });
570
+ if (plan) for (const line of plan.commit()) send(line);
571
+ },
323
572
  close() { send({ type: 'exit', code: 0 }); requestShutdown(); },
324
573
  removeAllListeners(ev) {
325
574
  if (ev) _listeners.delete(ev); else _listeners.clear();
@@ -376,6 +625,13 @@ globalThis.__gjsify_worker_context = {
376
625
  // Only ever referenced inside a worker child — the main thread has no
377
626
  // worker context, so its stderr-only reporting stays untouched.
378
627
  crash,
628
+ // A deliberate exit code, the way \`process.exit(n)\` ends a Node worker:
629
+ // no \`error\`, just \`exit\` n. \`System.exit\` alone is deferred while the
630
+ // main loop runs, so the loop has to be asked to quit first.
631
+ exit(code) {
632
+ requestShutdown();
633
+ System.exit(code);
634
+ },
379
635
  };
380
636
 
381
637
  // Child-local timers for every spawned child.
@@ -462,8 +718,22 @@ function dispatchLine(line) {
462
718
  // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS
463
719
  // messages before writing this stdin line, so they're already
464
720
  // buffered.
465
- if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));
466
- parentPort.emit('message', materialise(decodeWire(msg.data)));
721
+ //
722
+ // Deserialising is guarded, the emit is not: a message we cannot decode is
723
+ // reported as 'messageerror' on parentPort and the worker stays alive,
724
+ // whereas an exception from a user's 'message' listener still belongs in
725
+ // readNext's catch (= the crash contract). Before this, BOTH went there,
726
+ // so one undecodable message killed the worker with a 'bootstrap message
727
+ // error'.
728
+ let value;
729
+ try {
730
+ if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));
731
+ value = materialise(decodeWire(msg.data));
732
+ } catch (err) {
733
+ parentPort.emit('messageerror', err instanceof Error ? err : new Error(String(err)));
734
+ return true;
735
+ }
736
+ parentPort.emit('message', value);
467
737
  }
468
738
  else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); requestShutdown(); return false; }
469
739
  return true;
package/lib/esm/worker.js CHANGED
@@ -1,5 +1,5 @@
1
- import"./_virtual/_rolldown/runtime.js";import{extractSharedBuffers as e}from"./sab-transfer.js";import{MessagePort as t}from"./message-port.js";import{decode as n,encode as r}from"./structured-clone-wire.js";import{SubprocessPortTransport as i,nextParentPortId as a}from"./subprocess-port-transport.js";import{BOOTSTRAP_CODE as o}from"./worker-bootstrap.js";import{EventEmitter as s}from"node:events";import c from"@girs/gio-2.0";import l from"@girs/glib-2.0";import{fdChannel as u}from"@gjsify/sab-native";import{acquireMainLoop as d,releaseMainLoop as f}from"@gjsify/utils/main-loop";let p=1;const m=new TextEncoder,h=m.encode(o);var g=class Worker extends s{threadId;resourceLimits;_subprocess=null;_stdinPipe=null;_exited=!1;_bootstrapFile=null;_sabSocketFd=-1;_holdsProcess=!1;_sabNextTag=0;_portRegistry=new Map;constructor(e,t){super(),this.threadId=p++,this.resourceLimits=t?.resourceLimits||{};let n=t?.eval===!0,i=Worker._resolveFilename(e,n),a=`${l.get_tmp_dir()}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;this._bootstrapFile=c.File.new_for_path(a);try{this._bootstrapFile.replace_contents(h,null,!1,c.FileCreateFlags.REPLACE_DESTINATION,null)}catch(e){throw Error(`Failed to create worker bootstrap: ${e instanceof Error?e.message:e}`)}let o=new c.SubprocessLauncher({flags:c.SubprocessFlags.STDIN_PIPE|c.SubprocessFlags.STDOUT_PIPE|c.SubprocessFlags.STDERR_PIPE});if(t?.env&&typeof t.env==`object`)for(let[e,n]of Object.entries(t.env))o.setenv(e,String(n),!0);let s=-1;if(u){let e=u.makePair();e&&(this._sabSocketFd=e.parentFd,s=e.childFd,o.take_fd(s,3))}let f=`gjs`;try{let e=l.file_read_link(`/proc/self/exe`);e&&(f=e)}catch{}try{this._subprocess=o.spawnv([f,`-m`,a])}catch(e){throw this._cleanup(),Error(`Failed to spawn worker: ${e instanceof Error?e.message:e}`)}this._stdinPipe=this._subprocess.get_stdin_pipe();let g=this._subprocess.get_stdout_pipe();if(!n){let e=i.startsWith(`file://`)?i.slice(7):i;if(!c.File.new_for_path(e).query_exists(null)){this._cleanup();let t=Error(`Cannot find module '${e}'`);t.code=`ERR_MODULE_NOT_FOUND`,Promise.resolve().then(()=>{this.emit(`error`,t),this._exited=!0,this.emit(`exit`,1)});return}}this._holdsProcess=!0,d();let _=this._serializeWithSabTransfer(t?.workerData??null),v=JSON.stringify({threadId:this.threadId,workerData:r(_),eval:n,filename:n?void 0:i,code:n?i:void 0,sabSocketFd:this._sabSocketFd===-1?-1:3})+`
2
- `;try{this._stdinPipe.write_all(m.encode(v),null)}catch(e){throw this._cleanup(),Error(`Failed to send init data: ${e instanceof Error?e.message:e}`)}if(g){let e=c.DataInputStream.new(g);this._readMessages(e)}let y=this._subprocess.get_stderr_pipe();y&&this._readStderr(c.DataInputStream.new(y)),this._subprocess.wait_async(null,()=>{this._onExit()})}postMessage(e,t){if(!(this._exited||!this._stdinPipe))try{let n=e;t&&t.length>0&&(n=this._extractCrossProcessPorts(e,t));let i=r(this._serializeWithSabTransfer(n)),a=JSON.stringify({type:`message`,data:i})+`
3
- `;this._stdinPipe.write_all(m.encode(a),null)}catch{}}_writeStdinLine=e=>{if(!(this._exited||!this._stdinPipe))try{this._stdinPipe.write_all(m.encode(e),null)}catch{}};_extractCrossProcessPorts(e,n){let r=new Map;for(let e of n){if(!(e instanceof t))continue;let n=e,o=a(),s=n._otherPort;s&&(s._otherPort=null,n._otherPort=null,s._inner._portId=o,s._inner._partner=null,s._inner._transport=new i(this._writeStdinLine,this._portRegistry),this._portRegistry.set(o,s)),n._detached=!0,n._closed=!0,n._inner.close(),r.set(n,{__gjsifyTransferredPort:!0,portId:o})}if(r.size===0)return e;function walk(e,n){if(typeof e!=`object`||!e)return e;if(e instanceof t)return r.get(e)??e;if(n.has(e))return n.get(e);if(Array.isArray(e)){let t=[];n.set(e,t);for(let r=0;r<e.length;r++)r in e&&(t[r]=walk(e[r],n));return t}if(Object.prototype.toString.call(e).slice(8,-1)===`Object`){let t={};n.set(e,t);for(let r of Object.keys(e))t[r]=walk(e[r],n);return t}return e}return walk(e,new Map)}_serializeWithSabTransfer(t){if(!u||this._sabSocketFd===-1)return t;let{value:n,table:r,nextTag:i}=e(t,this._sabNextTag);this._sabNextTag=i;for(let{tag:e,buffer:t}of r)if(!u.sendFd(this._sabSocketFd,t.fd,e))throw Error(`Failed to send SharedBuffer fd over worker side-channel (tag ${e})`);return n}terminate(){if(this._exited)return Promise.resolve(0);let e=new Promise(e=>{this.once(`exit`,t=>e(t))});try{if(this._stdinPipe){let e=JSON.stringify({type:`terminate`})+`
4
- `;this._stdinPipe.write_all(m.encode(e),null)}}catch{}return setTimeout(()=>{!this._exited&&this._subprocess&&this._subprocess.force_exit()},500),e}ref(){return!this._exited&&!this._holdsProcess&&(this._holdsProcess=!0,d()),this}unref(){return this._releaseProcessHold(),this}static _resolveFilename(e,t){if(t)return String(e);if(e instanceof URL)return e.href;let n=String(e);if(n.startsWith(`file://`)||n.startsWith(`http://`)||n.startsWith(`https://`))return n;if(n.startsWith(`/`))return`file://`+n;if(n.startsWith(`./`)||n.startsWith(`../`)||!n.includes(`/`)){let e=l.get_current_dir(),t=l.build_filenamev([e,n]);return`file://`+(c.File.new_for_path(t).get_path()||t)}return`file://`+n}_readMessages(e){e.read_line_async(l.PRIORITY_DEFAULT,null,(t,r)=>{try{let[t]=e.read_line_finish_utf8(r);if(t===null)return;let i=JSON.parse(t);if(typeof i.__msgport==`number`){let t=this._portRegistry.get(i.__msgport);t&&(i.op===`send`?t._receiveMessage(n(i.data)):i.op===`close`&&(this._portRegistry.delete(i.__msgport),t.close())),this._readMessages(e);return}switch(i.type){case`online`:this.emit(`online`);break;case`message`:this.emit(`message`,n(i.data));break;case`error`:{let e=Error(i.message);i.stack&&(e.stack=i.stack),this.emit(`error`,e);break}}this._readMessages(e)}catch{}})}_stderrChunks=[];_readStderr(e){e.read_line_async(l.PRIORITY_DEFAULT,null,(t,n)=>{try{let[t]=e.read_line_finish_utf8(n);if(t===null){if(this._stderrChunks.length>0){let e=this._stderrChunks.join(`
5
- `);this.listenerCount(`error`)===0&&this.emit(`error`,Error(e))}return}this._stderrChunks.push(t),this._readStderr(e)}catch{}})}_onExit(){if(this._exited)return;this._exited=!0;let e=this._subprocess?.get_if_exited()?this._subprocess.get_exit_status():1;this._releaseProcessHold(),this._cleanup(),this.emit(`exit`,e)}_releaseProcessHold(){this._holdsProcess&&(this._holdsProcess=!1,f())}_cleanup(){if(this._bootstrapFile){try{this._bootstrapFile.delete(null)}catch{}this._bootstrapFile=null}if(this._stdinPipe){try{this._stdinPipe.close(null)}catch{}this._stdinPipe=null}this._sabSocketFd!==-1&&u?.closeFd&&(u.closeFd(this._sabSocketFd),this._sabSocketFd=-1);for(let e of this._portRegistry.values())e._inner._closed=!0,e._inner._transport=null;this._portRegistry.clear(),this._subprocess=null}};export{g as Worker};
1
+ import"./_virtual/_rolldown/runtime.js";import{extractSharedBuffers as e}from"./sab-transfer.js";import{decode as t,encode as n}from"./structured-clone-wire.js";import{MessagePort as r}from"./message-port.js";import{SubprocessPortTransport as i,isCrossProcessPortPlaceholder as a,nextParentPortId as o}from"./subprocess-port-transport.js";import{BOOTSTRAP_CODE as s}from"./worker-bootstrap.js";import{EventEmitter as c}from"node:events";import{MessagePort as l}from"@gjsify/message-channel";import u from"@girs/gio-2.0";import d from"@girs/glib-2.0";import{fdChannel as f}from"@gjsify/sab-native";import{acquireMainLoop as p,releaseMainLoop as m}from"@gjsify/utils/main-loop";let h=1;const g=new TextEncoder,_=g.encode(s);var v=class Worker extends c{threadId;resourceLimits;_subprocess=null;_stdinPipe=null;_exited=!1;_bootstrapFile=null;_sabSocketFd=-1;_holdsProcess=!1;_sabNextTag=0;_portRegistry=new Map;constructor(e,t){super(),this.threadId=h++,this.resourceLimits=t?.resourceLimits||{};let r=t?.eval===!0,i=Worker._resolveFilename(e,r),a=`${d.get_tmp_dir()}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;this._bootstrapFile=u.File.new_for_path(a);try{this._bootstrapFile.replace_contents(_,null,!1,u.FileCreateFlags.REPLACE_DESTINATION,null)}catch(e){throw Error(`Failed to create worker bootstrap: ${e instanceof Error?e.message:e}`)}let o=new u.SubprocessLauncher({flags:u.SubprocessFlags.STDIN_PIPE|u.SubprocessFlags.STDOUT_PIPE|u.SubprocessFlags.STDERR_PIPE});if(t?.env&&typeof t.env==`object`)for(let[e,n]of Object.entries(t.env))o.setenv(e,String(n),!0);let s=-1;if(f){let e=f.makePair();e&&(this._sabSocketFd=e.parentFd,s=e.childFd,o.take_fd(s,3))}let c=`gjs`;try{let e=d.file_read_link(`/proc/self/exe`);e&&(c=e)}catch{}try{this._subprocess=o.spawnv([c,`-m`,a])}catch(e){throw this._cleanup(),Error(`Failed to spawn worker: ${e instanceof Error?e.message:e}`)}this._stdinPipe=this._subprocess.get_stdin_pipe();let l=this._subprocess.get_stdout_pipe();if(!r){let e=i.startsWith(`file://`)?i.slice(7):i;if(!u.File.new_for_path(e).query_exists(null)){this._cleanup();let t=Error(`Cannot find module '${e}'`);t.code=`ERR_MODULE_NOT_FOUND`,Promise.resolve().then(()=>{this.emit(`error`,t),this._exited=!0,this.emit(`exit`,1)});return}}this._holdsProcess=!0,p();let m=this._serializeWithSabTransfer(t?.workerData??null),v=JSON.stringify({threadId:this.threadId,workerData:n(m),eval:r,filename:r?void 0:i,code:r?i:void 0,sabSocketFd:this._sabSocketFd===-1?-1:3})+`
2
+ `;try{this._stdinPipe.write_all(g.encode(v),null)}catch(e){throw this._cleanup(),Error(`Failed to send init data: ${e instanceof Error?e.message:e}`)}if(l){let e=u.DataInputStream.new(l);this._readMessages(e)}let y=this._subprocess.get_stderr_pipe();y&&this._readStderr(u.DataInputStream.new(y)),this._subprocess.wait_async(null,()=>{this._onExit()})}postMessage(e,t){if(this._exited||!this._stdinPipe)return;let r=e;t&&t.length>0&&(r=this._extractCrossProcessPorts(e,t));let i=n(this._serializeWithSabTransfer(r)),a=JSON.stringify({type:`message`,data:i})+`
3
+ `;try{this._stdinPipe.write_all(g.encode(a),null)}catch{}}_writeStdinLine=e=>{if(!(this._exited||!this._stdinPipe))try{this._stdinPipe.write_all(g.encode(e),null)}catch{}};_extractCrossProcessPorts(e,t){let n=new Map;for(let e of t){let t=e instanceof r;if(!t&&!(e instanceof l))continue;let a=e,s=o(),c=t?a._otherPort:a._partner;if(c){let e=c instanceof r?c._inner:c;c instanceof r&&(c._otherPort=null),t&&(a._otherPort=null),e._partner=null,(a instanceof r?a._inner:a)._partner=null,e._portId=s,e._transport=new i(this._writeStdinLine,this._portRegistry),this._portRegistry.set(s,c)}a instanceof r?(a._detached=!0,a._closed=!0,a._inner.close()):a.close(),n.set(a,{__gjsifyTransferredPort:!0,portId:s})}if(n.size===0)return e;function walk(e,t){if(typeof e!=`object`||!e)return e;if(e instanceof r||e instanceof l)return n.get(e)??e;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)}_adoptChildPorts(e,t){if(typeof e!=`object`||!e)return e;if(a(e)){let t=this._portRegistry.get(e.portId);if(t)return t;let n=new r;return n._inner._portId=e.portId,n._inner._transport=new i(this._writeStdinLine,this._portRegistry),this._portRegistry.set(e.portId,n),n}if(t.has(e))return e;if(t.add(e),Array.isArray(e))for(let n=0;n<e.length;n++)n in e&&(e[n]=this._adoptChildPorts(e[n],t));else if(Object.prototype.toString.call(e)===`[object Object]`){let n=e;for(let e of Object.keys(n))n[e]=this._adoptChildPorts(n[e],t)}return e}_serializeWithSabTransfer(t){if(!f||this._sabSocketFd===-1)return t;let{value:n,table:r,nextTag:i}=e(t,this._sabNextTag);this._sabNextTag=i;for(let{tag:e,buffer:t}of r)if(!f.sendFd(this._sabSocketFd,t.fd,e))throw Error(`Failed to send SharedBuffer fd over worker side-channel (tag ${e})`);return n}terminate(){if(this._exited)return Promise.resolve(0);let e=new Promise(e=>{this.once(`exit`,t=>e(t))});try{if(this._stdinPipe){let e=JSON.stringify({type:`terminate`})+`
4
+ `;this._stdinPipe.write_all(g.encode(e),null)}}catch{}return setTimeout(()=>{!this._exited&&this._subprocess&&this._subprocess.force_exit()},500),e}ref(){return!this._exited&&!this._holdsProcess&&(this._holdsProcess=!0,p()),this}unref(){return this._releaseProcessHold(),this}static _resolveFilename(e,t){if(t)return String(e);if(e instanceof URL)return e.href;let n=String(e);if(n.startsWith(`file://`)||n.startsWith(`http://`)||n.startsWith(`https://`))return n;if(n.startsWith(`/`))return`file://`+n;if(n.startsWith(`./`)||n.startsWith(`../`)||!n.includes(`/`)){let e=d.get_current_dir(),t=d.build_filenamev([e,n]);return`file://`+(u.File.new_for_path(t).get_path()||t)}return`file://`+n}_readMessages(e){e.read_line_async(d.PRIORITY_DEFAULT,null,(n,r)=>{let i;try{let[t]=e.read_line_finish_utf8(r);if(t===null)return;i=JSON.parse(t)}catch{return}try{if(typeof i.__msgport==`number`){let e=this._portRegistry.get(i.__msgport);if(e)if(i.op===`send`){let n;try{n=t(i.data)}catch(t){e._receiveMessageError?.(t);return}e instanceof l?e._receive(n):e._receiveMessage(n)}else i.op===`close`&&(this._portRegistry.delete(i.__msgport),e.close());return}switch(i.type){case`online`:this.emit(`online`);break;case`message`:{let e;try{e=t(i.data)}catch(e){this.emit(`messageerror`,e instanceof Error?e:Error(String(e)));break}this.emit(`message`,this._adoptChildPorts(e,new Set));break}case`error`:{let e=Error(i.message);i.stack&&(e.stack=i.stack),this.emit(`error`,e);break}}}catch{}finally{this._readMessages(e)}})}_stderrChunks=[];_readStderr(e){e.read_line_async(d.PRIORITY_DEFAULT,null,(t,n)=>{try{let[t]=e.read_line_finish_utf8(n);if(t===null){if(this._stderrChunks.length>0){let e=this._stderrChunks.join(`
5
+ `);this.listenerCount(`error`)===0&&this.emit(`error`,Error(e))}return}this._stderrChunks.push(t),this._readStderr(e)}catch{}})}_onExit(){if(this._exited)return;this._exited=!0;let e=this._subprocess?.get_if_exited()?this._subprocess.get_exit_status():1;this._releaseProcessHold(),this._cleanup(),this.emit(`exit`,e)}_releaseProcessHold(){this._holdsProcess&&(this._holdsProcess=!1,m())}_cleanup(){if(this._bootstrapFile){try{this._bootstrapFile.delete(null)}catch{}this._bootstrapFile=null}if(this._stdinPipe){try{this._stdinPipe.close(null)}catch{}this._stdinPipe=null}this._sabSocketFd!==-1&&f?.closeFd&&(f.closeFd(this._sabSocketFd),this._sabSocketFd=-1);for(let e of this._portRegistry.values())e._inner._closed=!0,e._inner._transport=null;this._portRegistry.clear(),this._subprocess=null}};export{v as Worker};
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -15,6 +15,17 @@ export declare class MessagePort extends EventEmitter {
15
15
  ref(): this;
16
16
  unref(): this;
17
17
  _receiveMessage(message: unknown): void;
18
+ /**
19
+ * @internal An incoming message arrived but could not be deserialised, so
20
+ * there is no `message` to deliver. Node surfaces exactly this as
21
+ * `'messageerror'` on the receiving port and stays usable afterwards, which
22
+ * is the alternative to the message vanishing without a trace.
23
+ *
24
+ * EventEmitter surface only: the W3C side wants a `MessageEvent` on the
25
+ * inner port so that `onmessageerror` fires too, and that mapping belongs
26
+ * with the rest of the web surface in `@gjsify/web-worker`.
27
+ */
28
+ _receiveMessageError(err: unknown): void;
18
29
  get _hasQueuedMessages(): boolean;
19
30
  _dequeueMessage(): unknown | undefined;
20
31
  /** @internal Has this port been transferred elsewhere? */
@@ -1,3 +1,18 @@
1
- export declare function encode(value: unknown, seen?: Map<object, unknown>): unknown;
2
- export declare function decode(value: unknown): unknown;
1
+ /**
2
+ * Encode `value` into a JSON-safe tree.
3
+ *
4
+ * `refs` is the serialisation memory of §2.7.3: every object gets an id the
5
+ * first time it is reached, and a second sighting becomes a `Ref` to that id.
6
+ * The ids are implicit — pre-order position, assigned by `refs.size` here and
7
+ * by push order in `decode` — so sharing and cycles cost no extra bytes on the
8
+ * wire and need no second pass over the tree.
9
+ */
10
+ export declare function encode(value: unknown, refs?: Map<object, number>): unknown;
11
+ /**
12
+ * Decode a tree produced by `encode`. `refs` is the deserialisation memory:
13
+ * each object is pushed in the same pre-order the encoder numbered it in, so
14
+ * `Ref` ids resolve by index. Containers are pushed BEFORE their children so
15
+ * that a cycle can point back at a half-built parent.
16
+ */
17
+ export declare function decode(value: unknown, refs?: unknown[]): unknown;
3
18
  export declare function createDataCloneError(message: string): Error;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -1 +1 @@
1
- export declare const BOOTSTRAP_CODE = "import GLib from 'gi://GLib?version=2.0';\nimport Gio from 'gi://Gio?version=2.0';\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\n// Tagged JSON codec for structured-clone types (mirrors structured-clone-wire.ts).\nconst __TYPED_ARRAYS = ['Int8Array','Uint8Array','Uint8ClampedArray','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array','BigInt64Array','BigUint64Array'];\nfunction __toB64(buf){const b=new Uint8Array(buf);let s='';for(let i=0;i<b.length;i++)s+=String.fromCharCode(b[i]);return btoa(s);}\nfunction __fromB64(s){const bin=atob(s);const out=new Uint8Array(bin.length);for(let i=0;i<bin.length;i++)out[i]=bin.charCodeAt(i);return out;}\nfunction encodeWire(value, seen){\n if(value===null||value===undefined) return value;\n if(typeof value==='bigint') return {__gjsify:'BigInt', value:value.toString()};\n if(value===Infinity) return {__gjsify:'Infinity'};\n if(value===-Infinity) return {__gjsify:'NegInfinity'};\n if(typeof value==='number' && isNaN(value)) return {__gjsify:'NaN'};\n if(typeof value!=='object') return value;\n if(value instanceof Date) return {__gjsify:'Date', value:value.getTime()};\n if(value instanceof RegExp) return {__gjsify:'RegExp', source:value.source, flags:value.flags};\n if(value instanceof Error) return {__gjsify:'Error', name:value.name, message:value.message, stack:value.stack};\n if(value instanceof Map){const e=[];for(const [k,v] of value) e.push([encodeWire(k,seen),encodeWire(v,seen)]);return {__gjsify:'Map', entries:e};}\n if(value instanceof Set){const v=[];for(const x of value) v.push(encodeWire(x,seen));return {__gjsify:'Set', values:v};}\n const tag=Object.prototype.toString.call(value).slice(8,-1);\n if(tag==='ArrayBuffer') return {__gjsify:'ArrayBuffer', data:__toB64(value)};\n if(__TYPED_ARRAYS.indexOf(tag)!==-1){const ta=value;return {__gjsify:tag, data:__toB64(ta.buffer.slice(ta.byteOffset, ta.byteOffset+ta.byteLength))};}\n if(Array.isArray(value)) return value.map(function(x){return encodeWire(x,seen);});\n if(tag==='Object'){const out={};for(const k of Object.keys(value)) out[k]=encodeWire(value[k],seen);return out;}\n throw new Error('DataCloneError: value could not be cloned');\n}\nfunction decodeWire(value){\n if(value===null||value===undefined) return value;\n if(typeof value!=='object') return value;\n if(Array.isArray(value)) return value.map(decodeWire);\n const obj=value;\n if(typeof obj.__gjsify==='string'){\n switch(obj.__gjsify){\n case 'BigInt': return BigInt(obj.value);\n case 'Infinity': return Infinity;\n case 'NegInfinity': return -Infinity;\n case 'NaN': return NaN;\n case 'Date': return new Date(obj.value);\n case 'RegExp': return new RegExp(obj.source, obj.flags);\n case 'Error': {const e=new Error(obj.message); e.name=obj.name; if(obj.stack) e.stack=obj.stack; return e;}\n case 'Map': {const m=new Map(); for(const [k,v] of obj.entries) m.set(decodeWire(k),decodeWire(v)); return m;}\n case 'Set': {const s=new Set(); for(const v of obj.values) s.add(decodeWire(v)); return s;}\n case 'ArrayBuffer': return __fromB64(obj.data).buffer;\n default: {\n if(__TYPED_ARRAYS.indexOf(obj.__gjsify)!==-1){\n const bytes=__fromB64(obj.data);\n const Ctor=globalThis[obj.__gjsify];\n return new Ctor(bytes.buffer);\n }\n return value;\n }\n }\n }\n const tag=Object.prototype.toString.call(value).slice(8,-1);\n if(tag==='Object'){const out={};for(const k of Object.keys(obj)) out[k]=decodeWire(obj[k]);return out;}\n return value;\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/open-todos/README.md.\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', decodeWire(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: encodeWire(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();\nfunction _listenersChanged() { maybeExit(); }\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 _listenersChanged();\n return this;\n },\n emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },\n postMessage(data) { send({ type: 'message', data: encodeWire(data) }); },\n close() { send({ type: 'exit', code: 0 }); requestShutdown(); },\n removeAllListeners(ev) {\n if (ev) _listeners.delete(ev); else _listeners.clear();\n _listenersChanged();\n return this;\n },\n};\n\n// Worker-crash contract and the fallback timers behind it.\n//\n// GJS exposes no global error surface a bootstrap could catch into: measured\n// on gjs 1.88.1, `globalThis.addEventListener` does not exist, an uncaught\n// throw inside a GLib callback only prints a Gjs-CRITICAL while the loop keeps\n// running, and `System` has no uncaughtException/unhandledRejection hook. So a\n// worker that crashes outside a message handler is a silent orphan \u2014 the\n// parent waits on `error`/`exit` and never gets either (issue #2336). Every\n// crash path below funnels through `crash()`: one `error` line to the parent,\n// then exit 1, which is Node's `error` then `exit` 1.\n// Node ends a worker once its script is done and nothing keeps the event loop\n// alive: no parentPort 'message' listener, no pending timer, no open\n// MessagePort. The child is a subprocess that would otherwise park in\n// `loop.run()` on its stdin forever, so its parent would never see `exit`.\n// Only this bootstrap's own timers can be counted: a child whose timers were\n// replaced by a richer provider (the globals register) or that has other\n// handles we cannot see keeps the old parked behaviour.\nlet scriptDone = false;\nconst shimTimers = { nextId: 0, byId: new Map() };\nlet shimSetTimeout = null;\nfunction maybeExit() {\n if (!scriptDone || shuttingDown) return;\n if (globalThis.setTimeout !== shimSetTimeout) return;\n if (shimTimers.byId.size > 0 || childPortRegistry.size > 0) return;\n for (const fns of _listeners.values()) if (fns.length > 0) return;\n requestShutdown();\n System.exit(0);\n}\n\nfunction crash(error) {\n try {\n send({ type: 'error', message: (error && error.message) ? error.message : String(error), stack: (error && error.stack) || '' });\n } catch (_) { /* parent's stdout is gone \u2014 the exit below is what it still sees */ }\n requestShutdown();\n System.exit(1);\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(decodeWire(init.workerData ?? null)),\n threadId: init.threadId ?? 0,\n // The crash contract. Called by the fallback shim below for a raw .mjs\n // child and by the timers provider in a bundled one (globals register).\n // Only ever referenced inside a worker child \u2014 the main thread has no\n // worker context, so its stderr-only reporting stays untouched.\n crash,\n};\n\n// Child-local timers for every spawned child.\n//\n// Bare gjs ships its own setTimeout from its ESM modules\n// (`resource:///org/gnome/gjs/modules/esm/_timers.js`, measured on\n// gjs 1.88.1), but a throw inside one of its\n// callbacks only prints a Gjs-CRITICAL while the loop keeps running, so a\n// crashing worker sits invisible. The shim OVERWRITES the built-in: its\n// callbacks report through the crash contract and then the child dies the\n// Node way. A richer provider (the timers register of a bundled child) still\n// wins \u2014 it replaces these globals on import, and its dispatch carries the\n// same contract (reportTimerThrow + context.crash).\n{\n const timers = shimTimers;\n function clearById(id) {\n const gid = timers.byId.get(id);\n if (gid === undefined) return;\n timers.byId.delete(id);\n maybeExit();\n // g_source_remove has no throw path: a stale id logs a GLib CRITICAL and\n // returns false, it never raises (same contract the globals register\n // states for its own _cancel()).\n GLib.source_remove(gid);\n }\n globalThis.setTimeout = function (fn, delay) {\n if (typeof fn !== 'function') throw new TypeError('Callback must be a function. Received ' + typeof fn);\n const id = ++timers.nextId;\n const args = Array.prototype.slice.call(arguments, 2);\n const gid = GLib.timeout_add(GLib.PRIORITY_DEFAULT, Math.max(0, Number(delay) || 0), () => {\n timers.byId.delete(id);\n try { fn.apply(null, args); } catch (error) { crash(error); }\n maybeExit();\n return GLib.SOURCE_REMOVE;\n });\n timers.byId.set(id, gid);\n return id;\n };\n globalThis.setInterval = function (fn, delay) {\n if (typeof fn !== 'function') throw new TypeError('Callback must be a function. Received ' + typeof fn);\n const id = ++timers.nextId;\n const args = Array.prototype.slice.call(arguments, 2);\n const gid = GLib.timeout_add(GLib.PRIORITY_DEFAULT, Math.max(0, Number(delay) || 0), () => {\n try { fn.apply(null, args); } catch (error) { crash(error); return GLib.SOURCE_REMOVE; }\n return GLib.SOURCE_CONTINUE;\n });\n timers.byId.set(id, gid);\n return id;\n };\n shimSetTimeout = globalThis.setTimeout;\n globalThis.clearTimeout = clearById;\n globalThis.clearInterval = clearById;\n}\n\nsend({ type: 'online' });\n\n// Lines arriving on stdin before the worker script settles. The dynamic\n// import below needs a quiet main loop to resolve (GJS quirk), so a line\n// read while it is still pending is queued instead of dispatched into\n// parentPort's message handler \u2014 drained once the script is done.\nconst pendingLines = [];\n\n// Dispatch one parsed-as-JSON-eligible stdin line. Returns false for\n// 'terminate', which requests shutdown and must not be followed by\n// another read \u2014 the process is on its way out.\nfunction dispatchLine(line) {\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(decodeWire(msg.data)));\n }\n else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); requestShutdown(); return false; }\n return true;\n}\n\n// Async stdin reader for messages from parent. Runs from the start rather\n// than only once the worker script settles: EOF on stdin means the spawner\n// is gone, and that can arrive while the script's own top-level await never\n// settles on its own, so EOF has to be caught independently of dispatch.\n//\n// macOS's poll() never reports POLLHUP for a pipe whose write end closed \u2014\n// only POLLIN, backed by a zero-length read (measured on darwin; Linux sets\n// both), so a HUP-only watch armed before the script settles never fires\n// there and the orphan outlives its spawner. Reading the stream the normal\n// way sees that same zero-length read as EOF on either OS, which is why\n// this one reader replaces the separate HUP watch this bootstrap used to\n// run alongside it.\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 if (!scriptDone) {\n pendingLines.push(line);\n readNext();\n return;\n }\n if (dispatchLine(line)) 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}\n// Start the main loop so await import() has a running loop to resolve\n// against, and run the worker code from an idle callback: idle_add fires\n// as soon as the loop is running (even with pending I/O in the stdin\n// buffer), whereas timeout_add(0) can be starved by a pending async read\n// that never completes because import() hung.\nif (!shuttingDown) {\n readNext();\n GLib.idle_add(GLib.PRIORITY_DEFAULT, function() {\n (async function() {\n try {\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 scriptDone = true;\n // Drain whatever arrived on stdin while the import above was still\n // settling \u2014 queued instead of dispatched, see pendingLines above.\n for (const queued of pendingLines) {\n if (!dispatchLine(queued)) break;\n }\n pendingLines.length = 0;\n maybeExit();\n } catch (error) {\n // A crashed script limps no further: reporting the error and then\n // keeping the loop alive strands the parent on `exit` the same way the\n // silent orphan does. Node emits `error` and then `exit` 1 (the exit\n // reaches the parent through wait_async, not through a line).\n try { send({ type: 'error', message: error.message, stack: error.stack || '' }); } catch (_) {}\n requestShutdown();\n System.exit(1);\n }\n })();\n return GLib.SOURCE_REMOVE;\n });\n loop.run();\n}\n";
1
+ export declare const BOOTSTRAP_CODE = "import GLib from 'gi://GLib?version=2.0';\nimport Gio from 'gi://Gio?version=2.0';\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\n// --- GJSIFY_WIRE_CODEC_BEGIN ---\n// Tagged JSON codec for structured-clone types \u2014 the child-side MIRROR of\n// structured-clone-wire.ts. Everything between the BEGIN/END markers is\n// sliced out of this string by structured-clone-wire.spec.ts and run through\n// the same case matrix as the TS copy, so the two cannot drift unnoticed.\n// The spec citations live in the TS file; keep the behaviour identical.\nconst __TYPED_ARRAYS = ['Int8Array','Uint8Array','Uint8ClampedArray','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array','BigInt64Array','BigUint64Array'];\nconst __ERROR_CTORS = {Error:Error,EvalError:EvalError,RangeError:RangeError,ReferenceError:ReferenceError,SyntaxError:SyntaxError,TypeError:TypeError,URIError:URIError};\n// No btoa/atob here: this code is evaluated by a bare gjs. See the comment on\n// toBase64 in structured-clone-wire.ts for what depending on them cost.\nconst __B64C='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst __B64D=new Uint8Array(128);for(let i=0;i<64;i++)__B64D[__B64C.charCodeAt(i)]=i;\nfunction __toB64(buf){const b=new Uint8Array(buf);let s='';for(let i=0;i<b.length;i+=3){const h1=i+1<b.length,h2=i+2<b.length;const n=(b[i]<<16)|((h1?b[i+1]:0)<<8)|(h2?b[i+2]:0);s+=__B64C[(n>>18)&63]+__B64C[(n>>12)&63]+(h1?__B64C[(n>>6)&63]:'=')+(h2?__B64C[n&63]:'=');}return s;}\nfunction __fromB64(s){let len=s.length;while(len>0&&s.charCodeAt(len-1)===61)len--;const out=new Uint8Array((len*3)>>2);let acc=0,bits=0,o=0;for(let i=0;i<len;i++){acc=(acc<<6)|__B64D[s.charCodeAt(i)];bits+=6;if(bits>=8){bits-=8;out[o++]=(acc>>bits)&255;}}return out;}\nfunction __dataCloneError(message){\n if(typeof DOMException==='function'){const e=new DOMException(message,'DataCloneError');if(e.code===undefined){try{Object.defineProperty(e,'code',{value:25,configurable:true});}catch(_){}}return e;}\n const e=new Error(message);e.name='DataCloneError';e.code=25;return e;\n}\nfunction __isHole(v){return typeof v==='object'&&v!==null&&!Array.isArray(v)&&v.__gjsify==='Hole';}\nfunction encodeWire(value, refs){\n if(refs===undefined) refs=new Map();\n if(value===null) return null;\n if(value===undefined) return {__gjsify:'Undefined'};\n const t=typeof value;\n if(t==='bigint') return {__gjsify:'BigInt', value:value.toString()};\n if(t==='number'){\n if(value===Infinity) return {__gjsify:'Infinity'};\n if(value===-Infinity) return {__gjsify:'NegInfinity'};\n if(isNaN(value)) return {__gjsify:'NaN'};\n if(Object.is(value,-0)) return {__gjsify:'NegZero'};\n return value;\n }\n if(t==='string'||t==='boolean') return value;\n if(t==='function'||t==='symbol') throw __dataCloneError(String(value)+' could not be cloned.');\n const seen=refs.get(value);\n if(seen!==undefined) return {__gjsify:'Ref', id:seen};\n refs.set(value, refs.size);\n if(value instanceof Date) return {__gjsify:'Date', value:value.getTime()};\n if(value instanceof RegExp) return {__gjsify:'RegExp', source:value.source, flags:value.flags};\n if(value instanceof Error){const n=__ERROR_CTORS[value.name]===undefined?'Error':value.name;return {__gjsify:'Error', name:n, message:value.message, stack:value.stack};}\n if(value instanceof Map){const e=[];for(const [k,v] of value) e.push([encodeWire(k,refs),encodeWire(v,refs)]);return {__gjsify:'Map', entries:e};}\n if(value instanceof Set){const v=[];for(const x of value) v.push(encodeWire(x,refs));return {__gjsify:'Set', values:v};}\n const tag=Object.prototype.toString.call(value).slice(8,-1);\n if(tag==='ArrayBuffer') return {__gjsify:'ArrayBuffer', data:__toB64(value)};\n if(tag==='DataView'||__TYPED_ARRAYS.indexOf(tag)!==-1){const ta=value;return {__gjsify:tag, data:__toB64(ta.buffer.slice(ta.byteOffset, ta.byteOffset+ta.byteLength))};}\n if(Array.isArray(value)){const out=[];for(let i=0;i<value.length;i++) out.push(i in value?encodeWire(value[i],refs):{__gjsify:'Hole'});return out;}\n if(tag==='Object'){const out={};for(const k of Object.keys(value)) out[k]=encodeWire(value[k],refs);if(typeof value.__gjsify==='string') return {__gjsify:'Object', value:out};return out;}\n throw __dataCloneError('value could not be cloned.');\n}\nfunction decodeWire(value, refs){\n if(refs===undefined) refs=[];\n if(value===null||value===undefined) return value;\n if(typeof value!=='object') return value;\n if(Array.isArray(value)){const out=[];out.length=value.length;refs.push(out);for(let i=0;i<value.length;i++){if(__isHole(value[i])) continue;out[i]=decodeWire(value[i],refs);}return out;}\n const obj=value;\n if(typeof obj.__gjsify==='string'){\n switch(obj.__gjsify){\n case 'Ref': return refs[obj.id];\n case 'Undefined': return undefined;\n case 'Hole': return undefined;\n case 'BigInt': return BigInt(obj.value);\n case 'Infinity': return Infinity;\n case 'NegInfinity': return -Infinity;\n case 'NaN': return NaN;\n case 'NegZero': return -0;\n case 'Date': {const d=new Date(obj.value);refs.push(d);return d;}\n case 'RegExp': {const r=new RegExp(obj.source, obj.flags);refs.push(r);return r;}\n case 'Error': {const C=__ERROR_CTORS[obj.name]||Error;const e=new C(obj.message);if(obj.stack) e.stack=obj.stack;refs.push(e);return e;}\n case 'Map': {const m=new Map();refs.push(m);for(const [k,v] of obj.entries) m.set(decodeWire(k,refs),decodeWire(v,refs));return m;}\n case 'Set': {const s=new Set();refs.push(s);for(const v of obj.values) s.add(decodeWire(v,refs));return s;}\n case 'ArrayBuffer': {const b=__fromB64(obj.data).buffer;refs.push(b);return b;}\n case 'DataView': {const d=new DataView(__fromB64(obj.data).buffer);refs.push(d);return d;}\n case 'Object': {const out={};refs.push(out);for(const k of Object.keys(obj.value)) out[k]=decodeWire(obj.value[k],refs);return out;}\n default: {\n if(__TYPED_ARRAYS.indexOf(obj.__gjsify)!==-1){\n const bytes=__fromB64(obj.data);\n const Ctor=globalThis[obj.__gjsify];\n const ta=new Ctor(bytes.buffer);\n refs.push(ta);\n return ta;\n }\n refs.push(value);\n return value;\n }\n }\n }\n const tag=Object.prototype.toString.call(value).slice(8,-1);\n if(tag==='Object'){const out={};refs.push(out);for(const k of Object.keys(obj)) out[k]=decodeWire(obj[k],refs);return out;}\n refs.push(value);\n return value;\n}\n// --- GJSIFY_WIRE_CODEC_END ---\n\n// Walks the DECODED tree, so it has to cope with what the wire codec now\n// reconstructs: shared and cyclic references (hence `seen`, or a self-\n// referencing workerData recurses until the child dies) and sparse arrays\n// (hence `i in value`, or every hole becomes an own `undefined` property and\n// the array stops being sparse). The parent-side equivalents in\n// sab-transfer.ts guard both already.\nfunction materialise(value, seen) {\n if (seen === undefined) seen = new Set();\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 (seen.has(value)) return value;\n seen.add(value);\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) if (i in value) value[i] = materialise(value[i], seen);\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], seen);\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// Child-side MessagePort \u2014 ONE class for every port this child owns.\n//\n// A port is either linked to a peer in this process (made by the inline\n// MessageChannel below) or routed over the wire (_wireId set): a port the\n// parent transferred in, or the kept end of a channel this child sent to the\n// parent. It offers both surfaces Node's worker_threads MessagePort has \u2014 the\n// EventEmitter one (on/once/off/emit) and the DOM one (addEventListener,\n// onmessage, start/close, MessageEvent-shaped {data}). Like Node, attaching a\n// 'message' listener through either surface (or assigning onmessage) starts\n// the port; messages that arrive earlier are queued, not dropped.\n//\n// Outbound wire traffic travels over stdout as { __msgport, op, data } lines,\n// which the parent's stdout reader routes back by the same id. Ids are even\n// here (2, 4, \u2026) and odd on the parent, so the two namespaces never collide.\n//\n// Not supported: transferring a port THROUGH a port (port-in-port). It would\n// need a per-message port table on both ends of the wire; throwing a\n// DataCloneError says so loudly instead. See the step 4 notes in\n// docs/reports/2026-10-30-web-worker-and-comlink-on-gjs.md.\nconst childPortRegistry = new Map();\nlet nextChildPortId = 2;\nconst PORT_TOKEN = {};\n\nclass MessagePort {\n constructor(token) {\n if (token !== PORT_TOKEN) throw new TypeError('Illegal constructor');\n this._ee = new Map();\n this._dom = new Map();\n this._onmessage = null;\n this._started = false;\n this._closed = false;\n this._detached = false;\n this._queue = [];\n this._partner = null;\n this._wireId = 0;\n }\n\n // --- delivery -------------------------------------------------------------\n\n /** @internal An already-decoded value arrives; queued until start(). */\n _deliver(value) {\n if (this._closed) return;\n if (!this._started) { this._queue.push(value); return; }\n this._schedule(value);\n }\n\n _schedule(value) {\n Promise.resolve().then(() => {\n if (this._closed) return;\n this._emitEE('message', [value]);\n this._emitDom('message', value);\n });\n }\n\n /** @internal Wire delivery: the stdin dispatcher hands over the encoded payload. */\n _receive(data) {\n if (this._closed) return;\n // A payload this end cannot decode becomes 'messageerror' on the port.\n // Letting decodeWire throw here would reject a microtask, and GJS has no\n // unhandled-rejection surface \u2014 the message would vanish.\n let decoded;\n try { decoded = decodeWire(data); }\n catch (err) {\n Promise.resolve().then(() => {\n if (!this._closed) this._emitEE('messageerror', [err instanceof Error ? err : new Error(String(err))]);\n });\n return;\n }\n this._deliver(decoded);\n }\n\n /** @internal The other end closed \u2014 no echo back. */\n _internalClose() { this._shutdown(false); }\n\n _emitEE(ev, args) {\n const fns = (this._ee.get(ev) || []).slice();\n for (const fn of fns) {\n try { fn.apply(this, args); } catch (_) { /* swallow */ }\n }\n }\n\n _emitDom(type, data) {\n const event = { type: type, data: data, target: this, currentTarget: this, ports: [], origin: '', lastEventId: '', source: null };\n const fns = (this._dom.get(type) || []).slice();\n for (const fn of fns) {\n try { if (typeof fn === 'function') fn.call(this, event); else fn.handleEvent(event); } catch (_) { /* swallow */ }\n }\n if (type === 'message' && typeof this._onmessage === 'function') {\n try { this._onmessage.call(this, event); } catch (_) { /* swallow */ }\n }\n }\n\n _shutdown(notifyPeer) {\n if (this._closed) return;\n this._closed = true;\n const peer = this._partner;\n this._partner = null;\n if (this._wireId) {\n if (notifyPeer) {\n try { send({ __msgport: this._wireId, op: 'close' }); } catch (_) { /* parent's stdout is gone */ }\n }\n childPortRegistry.delete(this._wireId);\n }\n // A local pair closes together, each end firing its own 'close'.\n if (peer) { peer._partner = null; peer._shutdown(false); }\n Promise.resolve().then(() => {\n this._emitEE('close', []);\n this._emitDom('close', undefined);\n this._ee.clear();\n this._dom.clear();\n maybeExit();\n });\n }\n\n // --- the port surface -----------------------------------------------------\n\n postMessage(data, transferList) {\n if (this._closed) return;\n if (transferList) {\n for (const item of transferList) {\n if (item instanceof MessagePort) {\n throw __dataCloneError('Transferring a MessagePort through a MessagePort is not supported in a worker child; send it with parentPort.postMessage(value, [port]).');\n }\n }\n }\n const encoded = encodeWire(data);\n if (this._wireId) {\n send({ __msgport: this._wireId, op: 'send', data: encoded });\n return;\n }\n const peer = this._partner;\n if (peer) peer._deliver(decodeWire(encoded));\n }\n\n start() {\n if (this._started || this._closed) return;\n this._started = true;\n for (const value of this._queue.splice(0)) this._schedule(value);\n }\n\n close() { this._shutdown(true); }\n ref() { return this; }\n unref() { return this; }\n\n // --- EventEmitter surface -------------------------------------------------\n\n on(ev, fn) {\n if (!this._ee.has(ev)) this._ee.set(ev, []);\n this._ee.get(ev).push(fn);\n if (ev === 'message') this.start();\n return this;\n }\n addListener(ev, fn) { return this.on(ev, fn); }\n once(ev, fn) {\n const self = this;\n const wrapper = function () { self.off(ev, wrapper); return fn.apply(self, arguments); };\n wrapper.listener = fn;\n return this.on(ev, wrapper);\n }\n off(ev, fn) {\n const arr = this._ee.get(ev);\n if (arr) this._ee.set(ev, arr.filter(function (f) { return f !== fn && f.listener !== fn; }));\n return this;\n }\n removeListener(ev, fn) { return this.off(ev, fn); }\n removeAllListeners(ev) {\n if (ev === undefined) this._ee.clear(); else this._ee.delete(ev);\n return this;\n }\n listenerCount(ev) { return (this._ee.get(ev) || []).length; }\n emit(ev /* , ...args */) {\n const had = (this._ee.get(ev) || []).length > 0;\n this._emitEE(ev, Array.prototype.slice.call(arguments, 1));\n return had;\n }\n\n // --- DOM surface ----------------------------------------------------------\n\n addEventListener(type, fn) {\n if (!fn) return;\n if (!this._dom.has(type)) this._dom.set(type, []);\n const arr = this._dom.get(type);\n if (arr.indexOf(fn) === -1) arr.push(fn);\n if (type === 'message') this.start();\n }\n removeEventListener(type, fn) {\n const arr = this._dom.get(type);\n if (arr) this._dom.set(type, arr.filter(function (f) { return f !== fn; }));\n }\n get onmessage() { return this._onmessage; }\n set onmessage(fn) {\n this._onmessage = typeof fn === 'function' ? fn : null;\n if (this._onmessage) this.start();\n }\n}\n\n/** A port whose peer lives in the parent, routed by portId. */\nfunction makeChildSidePort(portId) {\n const port = new MessagePort(PORT_TOKEN);\n port._wireId = portId;\n childPortRegistry.set(portId, port);\n return port;\n}\n\nclass MessageChannel {\n constructor() {\n const a = new MessagePort(PORT_TOKEN);\n const b = new MessagePort(PORT_TOKEN);\n a._partner = b;\n b._partner = a;\n this.port1 = a;\n this.port2 = b;\n }\n}\n\n// Bare gjs has neither; a bundled child's globals register only fills what is\n// still undefined, so these win there too and every child port is one class.\nif (typeof globalThis.MessageChannel === 'undefined') globalThis.MessageChannel = MessageChannel;\nif (typeof globalThis.MessagePort === 'undefined') globalThis.MessagePort = MessagePort;\n\n// parentPort.postMessage(value, [port]): turn each listed child port into a\n// wire port. The kept end (the peer) is rebound to a fresh even id and\n// registered; the transferred end is detached. Returns the value with each\n// port replaced by a placeholder plus a commit() that performs the side\n// effects \u2014 run it only after the value encoded, so a DataCloneError leaves\n// every port untouched.\nfunction planPortTransfer(value, transferList) {\n const ports = [];\n for (const item of transferList) {\n if (!(item instanceof MessagePort)) continue;\n if (ports.indexOf(item) !== -1) throw __dataCloneError('Transfer list contains duplicate entries');\n if (item._closed || item._detached) throw __dataCloneError('MessagePort in transfer list is already detached');\n if (item._wireId) throw __dataCloneError('A MessagePort that already crosses the worker boundary cannot be transferred again.');\n ports.push(item);\n }\n if (ports.length === 0) return { value: value, commit: function () { return []; } };\n const placeholders = new Map();\n for (const port of ports) {\n placeholders.set(port, { __gjsifyTransferredPort: true, portId: nextChildPortId });\n nextChildPortId += 2;\n }\n function walk(v, seen) {\n if (v === null || typeof v !== 'object') return v;\n if (v instanceof MessagePort) return placeholders.has(v) ? placeholders.get(v) : v;\n if (seen.has(v)) return seen.get(v);\n if (Array.isArray(v)) {\n const out = [];\n seen.set(v, out);\n for (let i = 0; i < v.length; i++) if (i in v) out[i] = walk(v[i], seen);\n return out;\n }\n if (Object.prototype.toString.call(v) === '[object Object]') {\n const out = {};\n seen.set(v, out);\n for (const k of Object.keys(v)) out[k] = walk(v[k], seen);\n return out;\n }\n return v;\n }\n return {\n value: walk(value, new Map()),\n commit() {\n const lines = [];\n for (const port of ports) {\n const portId = placeholders.get(port).portId;\n const peer = port._partner;\n port._partner = null;\n port._detached = true;\n port._closed = true;\n if (peer) {\n peer._partner = null;\n peer._wireId = portId;\n childPortRegistry.set(portId, peer);\n // What the peer posted before the transfer waits in the detached\n // port's queue; it travels right behind the carrying message.\n for (const queued of port._queue.splice(0)) {\n lines.push({ __msgport: portId, op: 'send', data: encodeWire(queued) });\n }\n }\n }\n return lines;\n },\n };\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();\nfunction _listenersChanged() { maybeExit(); }\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 _listenersChanged();\n return this;\n },\n emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },\n postMessage(data, transferList) {\n let plan = null;\n if (transferList && transferList.length > 0) {\n plan = planPortTransfer(data, transferList);\n data = plan.value;\n }\n send({ type: 'message', data: encodeWire(data) });\n if (plan) for (const line of plan.commit()) send(line);\n },\n close() { send({ type: 'exit', code: 0 }); requestShutdown(); },\n removeAllListeners(ev) {\n if (ev) _listeners.delete(ev); else _listeners.clear();\n _listenersChanged();\n return this;\n },\n};\n\n// Worker-crash contract and the fallback timers behind it.\n//\n// GJS exposes no global error surface a bootstrap could catch into: measured\n// on gjs 1.88.1, `globalThis.addEventListener` does not exist, an uncaught\n// throw inside a GLib callback only prints a Gjs-CRITICAL while the loop keeps\n// running, and `System` has no uncaughtException/unhandledRejection hook. So a\n// worker that crashes outside a message handler is a silent orphan \u2014 the\n// parent waits on `error`/`exit` and never gets either (issue #2336). Every\n// crash path below funnels through `crash()`: one `error` line to the parent,\n// then exit 1, which is Node's `error` then `exit` 1.\n// Node ends a worker once its script is done and nothing keeps the event loop\n// alive: no parentPort 'message' listener, no pending timer, no open\n// MessagePort. The child is a subprocess that would otherwise park in\n// `loop.run()` on its stdin forever, so its parent would never see `exit`.\n// Only this bootstrap's own timers can be counted: a child whose timers were\n// replaced by a richer provider (the globals register) or that has other\n// handles we cannot see keeps the old parked behaviour.\nlet scriptDone = false;\nconst shimTimers = { nextId: 0, byId: new Map() };\nlet shimSetTimeout = null;\nfunction maybeExit() {\n if (!scriptDone || shuttingDown) return;\n if (globalThis.setTimeout !== shimSetTimeout) return;\n if (shimTimers.byId.size > 0 || childPortRegistry.size > 0) return;\n for (const fns of _listeners.values()) if (fns.length > 0) return;\n requestShutdown();\n System.exit(0);\n}\n\nfunction crash(error) {\n try {\n send({ type: 'error', message: (error && error.message) ? error.message : String(error), stack: (error && error.stack) || '' });\n } catch (_) { /* parent's stdout is gone \u2014 the exit below is what it still sees */ }\n requestShutdown();\n System.exit(1);\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(decodeWire(init.workerData ?? null)),\n threadId: init.threadId ?? 0,\n // The crash contract. Called by the fallback shim below for a raw .mjs\n // child and by the timers provider in a bundled one (globals register).\n // Only ever referenced inside a worker child \u2014 the main thread has no\n // worker context, so its stderr-only reporting stays untouched.\n crash,\n // A deliberate exit code, the way `process.exit(n)` ends a Node worker:\n // no `error`, just `exit` n. `System.exit` alone is deferred while the\n // main loop runs, so the loop has to be asked to quit first.\n exit(code) {\n requestShutdown();\n System.exit(code);\n },\n};\n\n// Child-local timers for every spawned child.\n//\n// Bare gjs ships its own setTimeout from its ESM modules\n// (`resource:///org/gnome/gjs/modules/esm/_timers.js`, measured on\n// gjs 1.88.1), but a throw inside one of its\n// callbacks only prints a Gjs-CRITICAL while the loop keeps running, so a\n// crashing worker sits invisible. The shim OVERWRITES the built-in: its\n// callbacks report through the crash contract and then the child dies the\n// Node way. A richer provider (the timers register of a bundled child) still\n// wins \u2014 it replaces these globals on import, and its dispatch carries the\n// same contract (reportTimerThrow + context.crash).\n{\n const timers = shimTimers;\n function clearById(id) {\n const gid = timers.byId.get(id);\n if (gid === undefined) return;\n timers.byId.delete(id);\n maybeExit();\n // g_source_remove has no throw path: a stale id logs a GLib CRITICAL and\n // returns false, it never raises (same contract the globals register\n // states for its own _cancel()).\n GLib.source_remove(gid);\n }\n globalThis.setTimeout = function (fn, delay) {\n if (typeof fn !== 'function') throw new TypeError('Callback must be a function. Received ' + typeof fn);\n const id = ++timers.nextId;\n const args = Array.prototype.slice.call(arguments, 2);\n const gid = GLib.timeout_add(GLib.PRIORITY_DEFAULT, Math.max(0, Number(delay) || 0), () => {\n timers.byId.delete(id);\n try { fn.apply(null, args); } catch (error) { crash(error); }\n maybeExit();\n return GLib.SOURCE_REMOVE;\n });\n timers.byId.set(id, gid);\n return id;\n };\n globalThis.setInterval = function (fn, delay) {\n if (typeof fn !== 'function') throw new TypeError('Callback must be a function. Received ' + typeof fn);\n const id = ++timers.nextId;\n const args = Array.prototype.slice.call(arguments, 2);\n const gid = GLib.timeout_add(GLib.PRIORITY_DEFAULT, Math.max(0, Number(delay) || 0), () => {\n try { fn.apply(null, args); } catch (error) { crash(error); return GLib.SOURCE_REMOVE; }\n return GLib.SOURCE_CONTINUE;\n });\n timers.byId.set(id, gid);\n return id;\n };\n shimSetTimeout = globalThis.setTimeout;\n globalThis.clearTimeout = clearById;\n globalThis.clearInterval = clearById;\n}\n\nsend({ type: 'online' });\n\n// Lines arriving on stdin before the worker script settles. The dynamic\n// import below needs a quiet main loop to resolve (GJS quirk), so a line\n// read while it is still pending is queued instead of dispatched into\n// parentPort's message handler \u2014 drained once the script is done.\nconst pendingLines = [];\n\n// Dispatch one parsed-as-JSON-eligible stdin line. Returns false for\n// 'terminate', which requests shutdown and must not be followed by\n// another read \u2014 the process is on its way out.\nfunction dispatchLine(line) {\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 //\n // Deserialising is guarded, the emit is not: a message we cannot decode is\n // reported as 'messageerror' on parentPort and the worker stays alive,\n // whereas an exception from a user's 'message' listener still belongs in\n // readNext's catch (= the crash contract). Before this, BOTH went there,\n // so one undecodable message killed the worker with a 'bootstrap message\n // error'.\n let value;\n try {\n if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));\n value = materialise(decodeWire(msg.data));\n } catch (err) {\n parentPort.emit('messageerror', err instanceof Error ? err : new Error(String(err)));\n return true;\n }\n parentPort.emit('message', value);\n }\n else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); requestShutdown(); return false; }\n return true;\n}\n\n// Async stdin reader for messages from parent. Runs from the start rather\n// than only once the worker script settles: EOF on stdin means the spawner\n// is gone, and that can arrive while the script's own top-level await never\n// settles on its own, so EOF has to be caught independently of dispatch.\n//\n// macOS's poll() never reports POLLHUP for a pipe whose write end closed \u2014\n// only POLLIN, backed by a zero-length read (measured on darwin; Linux sets\n// both), so a HUP-only watch armed before the script settles never fires\n// there and the orphan outlives its spawner. Reading the stream the normal\n// way sees that same zero-length read as EOF on either OS, which is why\n// this one reader replaces the separate HUP watch this bootstrap used to\n// run alongside it.\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 if (!scriptDone) {\n pendingLines.push(line);\n readNext();\n return;\n }\n if (dispatchLine(line)) 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}\n// Start the main loop so await import() has a running loop to resolve\n// against, and run the worker code from an idle callback: idle_add fires\n// as soon as the loop is running (even with pending I/O in the stdin\n// buffer), whereas timeout_add(0) can be starved by a pending async read\n// that never completes because import() hung.\nif (!shuttingDown) {\n readNext();\n GLib.idle_add(GLib.PRIORITY_DEFAULT, function() {\n (async function() {\n try {\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 scriptDone = true;\n // Drain whatever arrived on stdin while the import above was still\n // settling \u2014 queued instead of dispatched, see pendingLines above.\n for (const queued of pendingLines) {\n if (!dispatchLine(queued)) break;\n }\n pendingLines.length = 0;\n maybeExit();\n } catch (error) {\n // A crashed script limps no further: reporting the error and then\n // keeping the loop alive strands the parent on `exit` the same way the\n // silent orphan does. Node emits `error` and then `exit` 1 (the exit\n // reaches the parent through wait_async, not through a line).\n try { send({ type: 'error', message: error.message, stack: error.stack || '' }); } catch (_) {}\n requestShutdown();\n System.exit(1);\n }\n })();\n return GLib.SOURCE_REMOVE;\n });\n loop.run();\n}\n";
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -64,6 +64,15 @@ export declare class Worker extends EventEmitter {
64
64
  * up the same validation in a follow-up if a real bug shakes loose.
65
65
  */
66
66
  private _extractCrossProcessPorts;
67
+ /**
68
+ * Turn the `{ __gjsifyTransferredPort, portId }` placeholders a child put
69
+ * into a message (`parentPort.postMessage(value, [port])`) into parent-side
70
+ * ports. Each new port has no in-process partner; its transport writes
71
+ * `{ __msgport, op }` lines to the child's stdin and the registry routes the
72
+ * child's replies back to it by the same (even) id. Walks in place — the
73
+ * tree was just decoded, nothing else holds it.
74
+ */
75
+ private _adoptChildPorts;
67
76
  /**
68
77
  * Walk `value` for SharedBuffer instances, ship each fd over the
69
78
  * parent-side end of the FdChannel socketpair, return the placeholder-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/worker_threads",
3
- "version": "0.64.13",
3
+ "version": "0.64.15-next.287",
4
4
  "description": "Node.js worker_threads module for Gjs",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -39,19 +39,19 @@
39
39
  "worker_threads"
40
40
  ],
41
41
  "devDependencies": {
42
- "@gjsify/cli": "^0.64.13",
43
- "@gjsify/node-globals": "^0.64.13",
44
- "@gjsify/unit": "^0.64.13",
42
+ "@gjsify/cli": "0.64.15-next.287",
43
+ "@gjsify/node-globals": "0.64.15-next.287",
44
+ "@gjsify/unit": "0.64.15-next.287",
45
45
  "@types/node": "^25.9.2",
46
46
  "typescript": "^6.0.3"
47
47
  },
48
48
  "dependencies": {
49
49
  "@girs/gio-2.0": "5.4.0",
50
50
  "@girs/glib-2.0": "5.4.0",
51
- "@gjsify/events": "^0.64.13",
52
- "@gjsify/message-channel": "^0.64.13",
53
- "@gjsify/sab-native": "^0.64.13",
54
- "@gjsify/utils": "^0.64.13"
51
+ "@gjsify/events": "0.64.15-next.287",
52
+ "@gjsify/message-channel": "0.64.15-next.287",
53
+ "@gjsify/sab-native": "0.64.15-next.287",
54
+ "@gjsify/utils": "0.64.15-next.287"
55
55
  },
56
56
  "gjsify": {
57
57
  "runtimes": {
@@ -71,5 +71,6 @@
71
71
  "bugs": {
72
72
  "url": "https://github.com/gjsify/gjsify/issues"
73
73
  },
74
- "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/node/worker_threads#readme"
74
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/node/worker_threads#readme",
75
+ "gitHead": "5742b282a8a2bc8633dd8384ef436768447f6a83"
75
76
  }