@gjsify/worker_threads 0.64.19 → 0.64.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/esm/message-port.js +1 -1
- package/lib/esm/worker-bootstrap.js +3 -3
- package/lib/esm/worker.js +4 -5
- package/lib/types/worker-bootstrap.d.ts +1 -1
- package/lib/types/worker.d.ts +8 -2
- package/package.json +9 -8
- package/lib/esm/sab-transfer.js +0 -1
- package/lib/esm/structured-clone-wire.js +0 -1
- package/lib/esm/subprocess-port-transport.js +0 -3
- package/lib/types/sab-transfer.d.ts +0 -57
- package/lib/types/structured-clone-wire.d.ts +0 -21
- package/lib/types/subprocess-port-transport.d.ts +0 -43
package/lib/esm/message-port.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{EventEmitter as e}from"node:events";import{MessagePort as t}from"@gjsify/message-channel";import{createDataCloneError as n,isSharedBuffer as r}from"@gjsify/ipc-channel/wire";function isTransferredPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0}var i=class MessagePort extends e{_closed=!1;_detached=!1;_otherPort=null;_inner=new t;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(e,t){if(this._closed)return;let i=this._otherPort;if(!i&&this._inner._transport!==null){if(t&&t.length>0)throw n(`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(e);return}if(!i)return;let a=[],o=[];if(t&&t.length>0){let e=new Set;for(let i of t){if(e.has(i))throw n(`Transfer list contains duplicate entries`);if(e.add(i),i instanceof MessagePort){if(i===this)throw n(`Cannot transfer source port`);if(i._closed||i._detached)throw n(`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 n(`ArrayBuffer in transfer list is detached`);a.push(e);continue}if(s===`SharedArrayBuffer`)throw n(`SharedArrayBuffer cannot appear in transfer list (it is shared, not transferred)`);if(!r(i))throw n(`Value at index ${t.indexOf(i)} of transfer list is not transferable`)}}let s=o.slice(),c=e;o.length>0&&(c=substitutePortsWithPlaceholders(e,o));let l;try{l=structuredClone(c,{transfer:a.length>0?a:void 0})}catch(e){throw e instanceof Error?e:n(`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),this._inner._receiveMessageError())})}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};
|
|
@@ -92,14 +92,14 @@ function drainSabFds(count) {
|
|
|
92
92
|
|
|
93
93
|
// --- GJSIFY_WIRE_CODEC_BEGIN ---
|
|
94
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
|
|
95
|
+
// @gjsify/ipc-channel's structured-clone-wire.ts. Everything between the BEGIN/END markers is
|
|
96
96
|
// sliced out of this string by structured-clone-wire.spec.ts and run through
|
|
97
97
|
// the same case matrix as the TS copy, so the two cannot drift unnoticed.
|
|
98
98
|
// The spec citations live in the TS file; keep the behaviour identical.
|
|
99
99
|
const __TYPED_ARRAYS = ['Int8Array','Uint8Array','Uint8ClampedArray','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array','BigInt64Array','BigUint64Array'];
|
|
100
100
|
const __ERROR_CTORS = {Error:Error,EvalError:EvalError,RangeError:RangeError,ReferenceError:ReferenceError,SyntaxError:SyntaxError,TypeError:TypeError,URIError:URIError};
|
|
101
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.
|
|
102
|
+
// toBase64 in @gjsify/ipc-channel's structured-clone-wire.ts for what depending on them cost.
|
|
103
103
|
const __B64C='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
104
104
|
const __B64D=new Uint8Array(128);for(let i=0;i<64;i++)__B64D[__B64C.charCodeAt(i)]=i;
|
|
105
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;}
|
|
@@ -188,7 +188,7 @@ function decodeWire(value, refs){
|
|
|
188
188
|
// referencing workerData recurses until the child dies) and sparse arrays
|
|
189
189
|
// (hence \`i in value\`, or every hole becomes an own \`undefined\` property and
|
|
190
190
|
// the array stops being sparse). The parent-side equivalents in
|
|
191
|
-
// sab-transfer.ts guard both already.
|
|
191
|
+
// @gjsify/ipc-channel's sab-transfer.ts guard both already.
|
|
192
192
|
function materialise(value, seen) {
|
|
193
193
|
if (seen === undefined) seen = new Set();
|
|
194
194
|
if (value === null || typeof value !== 'object') return value;
|
package/lib/esm/worker.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{
|
|
2
|
-
`;try{this.
|
|
3
|
-
`;try{this.
|
|
4
|
-
|
|
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,g())}_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&&p?.closeFd&&(p.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{b as Worker};
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{MessagePort as e}from"./message-port.js";import{BOOTSTRAP_CODE as t}from"./worker-bootstrap.js";import{EventEmitter as n}from"node:events";import{MessagePort as r}from"@gjsify/message-channel";import i from"@girs/gio-2.0";import a from"@girs/glib-2.0";import{fdChannel as o}from"@gjsify/sab-native";import{hostExecPath as s}from"@gjsify/utils/core";import{acquireMainLoop as c,releaseMainLoop as l}from"@gjsify/utils/main-loop";import{LineChannel as u,SubprocessPortTransport as d,createErrorByName as f,decode as p,encode as m,extractSharedBuffers as h,isCrossProcessPortPlaceholder as g,nextParentPortId as _}from"@gjsify/ipc-channel";let v=1;const y=new TextEncoder().encode(t);var b=class Worker extends n{threadId;resourceLimits;_subprocess=null;_channel=null;_exited=!1;_bootstrapFile=null;_sabSocketFd=-1;_holdsProcess=!1;_sabNextTag=0;_portRegistry=new Map;constructor(e,t){super(),this.threadId=v++,this.resourceLimits=t?.resourceLimits||{};let n=t?.eval===!0,r=Worker._resolveFilename(e,n),l=`${a.get_tmp_dir()}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;this._bootstrapFile=i.File.new_for_path(l);try{this._bootstrapFile.replace_contents(y,null,!1,i.FileCreateFlags.REPLACE_DESTINATION,null)}catch(e){throw Error(`Failed to create worker bootstrap: ${e instanceof Error?e.message:e}`)}let d=new i.SubprocessLauncher({flags:i.SubprocessFlags.STDIN_PIPE|i.SubprocessFlags.STDOUT_PIPE|i.SubprocessFlags.STDERR_PIPE});if(t?.env&&typeof t.env==`object`)for(let[e,n]of Object.entries(t.env))d.setenv(e,String(n),!0);let f=-1;if(o){let e=o.makePair();e&&(this._sabSocketFd=e.parentFd,f=e.childFd,d.take_fd(f,3))}let p=s()??`gjs`;try{this._subprocess=d.spawnv([p,`-m`,l])}catch(e){throw this._cleanup(),Error(`Failed to spawn worker: ${e instanceof Error?e.message:e}`)}let h=this._subprocess.get_stdout_pipe();if(this._channel=new u(this._subprocess.get_stdin_pipe(),h),!n){let e=r.startsWith(`file://`)?r.slice(7):r;if(!i.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,c();let g=this._serializeWithSabTransfer(t?.workerData??null),_=JSON.stringify({threadId:this.threadId,workerData:m(g),eval:n,filename:n?void 0:r,code:n?r:void 0,sabSocketFd:this._sabSocketFd===-1?-1:3})+`
|
|
2
|
+
`;try{this._channel.writeLine(_)}catch(e){throw this._cleanup(),Error(`Failed to send init data: ${e instanceof Error?e.message:e}`)}h&&this._channel.startReading(e=>this._onChannelMessage(e));let b=this._subprocess.get_stderr_pipe();b&&this._readStderr(i.DataInputStream.new(b)),this._subprocess.wait_async(null,()=>{this._onExit()})}postMessage(e,t){if(this._exited||!this._channel?.writable)return;let n=e;t&&t.length>0&&(n=this._extractCrossProcessPorts(e,t));let r=m(this._serializeWithSabTransfer(n)),i=JSON.stringify({type:`message`,data:r})+`
|
|
3
|
+
`;try{this._channel.writeLine(i)}catch{}}_writeStdinLine=e=>{if(!(this._exited||!this._channel?.writable))try{this._channel.writeLine(e)}catch{}};_extractCrossProcessPorts(t,n){let i=new Map;for(let t of n){let n=t instanceof e;if(!n&&!(t instanceof r))continue;let a=t,o=_(),s=n?a._otherPort:a._partner;if(s){let t=s instanceof e?s._inner:s;s instanceof e&&(s._otherPort=null),n&&(a._otherPort=null),t._partner=null,(a instanceof e?a._inner:a)._partner=null,t._portId=o,t._transport=new d(this._writeStdinLine,this._portRegistry),this._portRegistry.set(o,s)}a instanceof e?(a._detached=!0,a._closed=!0,a._inner.close()):a.close(),i.set(a,{__gjsifyTransferredPort:!0,portId:o})}if(i.size===0)return t;function walk(t,n){if(typeof t!=`object`||!t)return t;if(t instanceof e||t instanceof r)return i.get(t)??t;if(n.has(t))return n.get(t);if(Array.isArray(t)){let e=[];n.set(t,e);for(let r=0;r<t.length;r++)r in t&&(e[r]=walk(t[r],n));return e}if(Object.prototype.toString.call(t).slice(8,-1)===`Object`){let e={};n.set(t,e);for(let r of Object.keys(t))e[r]=walk(t[r],n);return e}return t}return walk(t,new Map)}_adoptChildPorts(t,n){if(typeof t!=`object`||!t)return t;if(g(t)){let n=this._portRegistry.get(t.portId);if(n)return n;let r=new e;return r._inner._portId=t.portId,r._inner._transport=new d(this._writeStdinLine,this._portRegistry),this._portRegistry.set(t.portId,r),r}if(n.has(t))return t;if(n.add(t),Array.isArray(t))for(let e=0;e<t.length;e++)e in t&&(t[e]=this._adoptChildPorts(t[e],n));else if(Object.prototype.toString.call(t)===`[object Object]`){let e=t;for(let t of Object.keys(e))e[t]=this._adoptChildPorts(e[t],n)}return t}_serializeWithSabTransfer(e){if(!o||this._sabSocketFd===-1)return e;let{value:t,table:n,nextTag:r}=h(e,this._sabNextTag);this._sabNextTag=r;for(let{tag:e,buffer:t}of n)if(!o.sendFd(this._sabSocketFd,t.fd,e))throw Error(`Failed to send SharedBuffer fd over worker side-channel (tag ${e})`);return t}terminate(){if(this._exited)return Promise.resolve(0);let e=new Promise(e=>{this.once(`exit`,t=>e(t))});try{this._channel?.writable&&this._channel.send({type:`terminate`})}catch{}return setTimeout(()=>{!this._exited&&this._subprocess&&this._subprocess.force_exit()},500),e}ref(){return!this._exited&&!this._holdsProcess&&(this._holdsProcess=!0,c()),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=a.get_current_dir(),t=a.build_filenamev([e,n]);return`file://`+(i.File.new_for_path(t).get_path()||t)}return`file://`+n}_onChannelMessage(e){if(typeof e.__msgport==`number`){let t=this._portRegistry.get(e.__msgport);if(t)if(e.op===`send`){let n;try{n=p(e.data)}catch(e){t instanceof r?t._receiveMessageError():t._receiveMessageError(e);return}t instanceof r?t._receive(n):t._receiveMessage(n)}else e.op===`close`&&(this._portRegistry.delete(e.__msgport),t.close());return}switch(e.type){case`online`:this.emit(`online`);break;case`message`:{let t;try{t=p(e.data)}catch(e){this.emit(`messageerror`,e instanceof Error?e:Error(String(e)));break}this.emit(`message`,this._adoptChildPorts(t,new Set));break}case`error`:{let t=f(e.name,e.message??``);e.stack&&(t.stack=e.stack),this.emit(`error`,t);break}}}_stderrChunks=[];_readStderr(e){e.read_line_async(a.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(`
|
|
4
|
+
`);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,l())}_cleanup(){if(this._bootstrapFile){try{this._bootstrapFile.delete(null)}catch{}this._bootstrapFile=null}this._channel&&=(this._channel.closeOutput(),null),this._sabSocketFd!==-1&&o?.closeFd&&(o.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{b as Worker};
|
|
@@ -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// --- 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._onmessageerror = 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) return;\n this._emitEE('messageerror', [err instanceof Error ? err : new Error(String(err))]);\n this._emitDom('messageerror', null);\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 if (type === 'messageerror' && typeof this._onmessageerror === 'function') {\n try { this._onmessageerror.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 get onmessageerror() { return this._onmessageerror; }\n set onmessageerror(fn) { this._onmessageerror = typeof fn === 'function' ? fn : null; }\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', name: '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', name: (error && error.name) || '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', name: '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', name: (error && error.name) || '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// @gjsify/ipc-channel's 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 @gjsify/ipc-channel's 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// @gjsify/ipc-channel's 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._onmessageerror = 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) return;\n this._emitEE('messageerror', [err instanceof Error ? err : new Error(String(err))]);\n this._emitDom('messageerror', null);\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 if (type === 'messageerror' && typeof this._onmessageerror === 'function') {\n try { this._onmessageerror.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 get onmessageerror() { return this._onmessageerror; }\n set onmessageerror(fn) { this._onmessageerror = typeof fn === 'function' ? fn : null; }\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', name: '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', name: (error && error.name) || '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', name: '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', name: (error && error.name) || '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";
|
package/lib/types/worker.d.ts
CHANGED
|
@@ -16,7 +16,8 @@ export declare class Worker extends EventEmitter {
|
|
|
16
16
|
readonly threadId: number;
|
|
17
17
|
readonly resourceLimits: Record<string, unknown>;
|
|
18
18
|
private _subprocess;
|
|
19
|
-
|
|
19
|
+
/** Child stdin (writes) and stdout (reads), one JSON message per line. */
|
|
20
|
+
private _channel;
|
|
20
21
|
private _exited;
|
|
21
22
|
private _bootstrapFile;
|
|
22
23
|
/** Parent-side end of the SCM_RIGHTS side-channel for SharedBuffer fds.
|
|
@@ -94,7 +95,12 @@ export declare class Worker extends EventEmitter {
|
|
|
94
95
|
* - eval mode → returned as-is (it's code, not a path)
|
|
95
96
|
*/
|
|
96
97
|
private static _resolveFilename;
|
|
97
|
-
|
|
98
|
+
/**
|
|
99
|
+
* One message from the child's stdout. A throw from here (a listener of
|
|
100
|
+
* ours, or 'error' emitted with nothing listening) does not end the read
|
|
101
|
+
* loop — `LineChannel` arms the next read either way.
|
|
102
|
+
*/
|
|
103
|
+
private _onChannelMessage;
|
|
98
104
|
private _stderrChunks;
|
|
99
105
|
private _readStderr;
|
|
100
106
|
private _onExit;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/worker_threads",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.21",
|
|
4
4
|
"description": "Node.js worker_threads module for Gjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "lib/esm/index.js",
|
|
@@ -39,19 +39,20 @@
|
|
|
39
39
|
"worker_threads"
|
|
40
40
|
],
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@gjsify/cli": "^0.64.
|
|
43
|
-
"@gjsify/node-globals": "^0.64.
|
|
44
|
-
"@gjsify/unit": "^0.64.
|
|
42
|
+
"@gjsify/cli": "^0.64.21",
|
|
43
|
+
"@gjsify/node-globals": "^0.64.21",
|
|
44
|
+
"@gjsify/unit": "^0.64.21",
|
|
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.
|
|
52
|
-
"@gjsify/
|
|
53
|
-
"@gjsify/
|
|
54
|
-
"@gjsify/
|
|
51
|
+
"@gjsify/events": "^0.64.21",
|
|
52
|
+
"@gjsify/ipc-channel": "^0.64.21",
|
|
53
|
+
"@gjsify/message-channel": "^0.64.21",
|
|
54
|
+
"@gjsify/sab-native": "^0.64.21",
|
|
55
|
+
"@gjsify/utils": "^0.64.21"
|
|
55
56
|
},
|
|
56
57
|
"gjsify": {
|
|
57
58
|
"runtimes": {
|
package/lib/esm/sab-transfer.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";function isSharedBufferPlaceholder(e){return typeof e==`object`&&!!e&&typeof e.__sab==`number`&&typeof e.size==`number`}function isSharedBuffer(e){return typeof e==`object`&&!!e&&e.constructor?.name===`SharedBuffer`}function extractSharedBuffers(e,t){let n=[],r=new Map,i=t;function walk(e,t){if(typeof e!=`object`||!e)return e;if(isSharedBuffer(e)){let t=r.get(e);return t===void 0&&(t=i++,r.set(e,t),n.push({tag:t,buffer:e})),{__sab:t,size:e.byteLength}}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{value:walk(e,new Map),table:n,nextTag:i}}function materializeSharedBuffers(e,t){function walk(e,n){if(typeof e!=`object`||!e)return e;if(isSharedBufferPlaceholder(e))return t(e.__sab,e.size);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{extractSharedBuffers,isSharedBuffer,isSharedBufferPlaceholder,materializeSharedBuffers};
|
|
@@ -1 +0,0 @@
|
|
|
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};function createErrorByName(e,n){return new((e===void 0?void 0:t[e])??Error)(n)}const 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,createErrorByName,decode,encode};
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{encode as e}from"./structured-clone-wire.js";var SubprocessPortTransport=class{_sendLine;_registry;constructor(e,t){this._sendLine=e,this._registry=t}send(t,n){let r=JSON.stringify({__msgport:t,op:`send`,data:e(n)})+`
|
|
2
|
-
`;this._sendLine(r)}close(e){let t=JSON.stringify({__msgport:e,op:`close`})+`
|
|
3
|
-
`;try{this._sendLine(t)}catch{}this._registry.delete(e)}};let t=1,n=2;function nextParentPortId(){let e=t;return t+=2,e}function nextChildPortId(){let e=n;return n+=2,e}function isCrossProcessPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0&&typeof e.portId==`number`}export{SubprocessPortTransport,isCrossProcessPortPlaceholder,nextChildPortId,nextParentPortId};
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { SharedBuffer } from '@gjsify/sab-native';
|
|
2
|
-
/**
|
|
3
|
-
* Placeholder for a SharedBuffer instance that has been transferred
|
|
4
|
-
* cross-process. The receiver reconstructs a SharedBuffer from the
|
|
5
|
-
* incoming fd indexed by `__sab` (the tag the sender attached to the
|
|
6
|
-
* SCM_RIGHTS message), with the original byteLength.
|
|
7
|
-
*/
|
|
8
|
-
export interface SharedBufferPlaceholder {
|
|
9
|
-
readonly __sab: number;
|
|
10
|
-
readonly size: number;
|
|
11
|
-
}
|
|
12
|
-
export declare function isSharedBufferPlaceholder(value: unknown): value is SharedBufferPlaceholder;
|
|
13
|
-
/**
|
|
14
|
-
* Detect a SharedBuffer instance without holding a hard runtime reference
|
|
15
|
-
* to the constructor — keeps the check resilient when sab-native's
|
|
16
|
-
* prebuild is unavailable (the import resolves but the class never gets
|
|
17
|
-
* instantiated) and avoids paying the typelib load just to typecheck a
|
|
18
|
-
* message payload.
|
|
19
|
-
*/
|
|
20
|
-
export declare function isSharedBuffer(value: unknown): value is SharedBuffer;
|
|
21
|
-
/**
|
|
22
|
-
* Walk `value`, replace every SharedBuffer instance with a placeholder
|
|
23
|
-
* carrying a freshly-allocated tag, and return the substituted tree
|
|
24
|
-
* alongside the table of (tag, SharedBuffer) pairs the caller must send
|
|
25
|
-
* over the SCM_RIGHTS side-channel before the JSON message itself.
|
|
26
|
-
*
|
|
27
|
-
* The walker handles plain objects + arrays. Other tagged types
|
|
28
|
-
* (Map, Set, …) are passed through unchanged — same constraint Node
|
|
29
|
-
* applies to its in-built clone walker.
|
|
30
|
-
*
|
|
31
|
-
* Tags start at `startTag` and increment per discovery. Callers should
|
|
32
|
-
* thread a per-Worker sequence counter so tags stay unique within a
|
|
33
|
-
* single FdChannel pair's lifetime (4 G tags = 2³² = plenty).
|
|
34
|
-
*/
|
|
35
|
-
export declare function extractSharedBuffers(value: unknown, startTag: number): {
|
|
36
|
-
value: unknown;
|
|
37
|
-
table: {
|
|
38
|
-
tag: number;
|
|
39
|
-
buffer: SharedBuffer;
|
|
40
|
-
}[];
|
|
41
|
-
nextTag: number;
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* Receiver-side counterpart: walk a tree that may contain
|
|
45
|
-
* SharedBufferPlaceholder leaves and replace each with the corresponding
|
|
46
|
-
* SharedBuffer reconstructed from the fdMap. Mutates the input in place
|
|
47
|
-
* (callers always work on a freshly-parsed JSON tree, so this avoids an
|
|
48
|
-
* extra copy).
|
|
49
|
-
*
|
|
50
|
-
* If a placeholder references a tag that is not yet in `fdMap`, the
|
|
51
|
-
* caller should buffer the message and retry once the recv-loop fills
|
|
52
|
-
* the missing entry. In the current protocol the sender always
|
|
53
|
-
* sends fds before the JSON line, so the map is populated by the time
|
|
54
|
-
* the JSON arrives — but the bootstrap layer is responsible for that
|
|
55
|
-
* ordering.
|
|
56
|
-
*/
|
|
57
|
-
export declare function materializeSharedBuffers(value: unknown, resolveTag: (tag: number, size: number) => SharedBuffer): unknown;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/** An error rebuilt from a name and message sent as plain fields (the child's
|
|
2
|
-
* `error` line), with the same interned names as the codec's `Error` tag. */
|
|
3
|
-
export declare function createErrorByName(name: string | undefined, message: string): Error;
|
|
4
|
-
/**
|
|
5
|
-
* Encode `value` into a JSON-safe tree.
|
|
6
|
-
*
|
|
7
|
-
* `refs` is the serialisation memory of §2.7.3: every object gets an id the
|
|
8
|
-
* first time it is reached, and a second sighting becomes a `Ref` to that id.
|
|
9
|
-
* The ids are implicit — pre-order position, assigned by `refs.size` here and
|
|
10
|
-
* by push order in `decode` — so sharing and cycles cost no extra bytes on the
|
|
11
|
-
* wire and need no second pass over the tree.
|
|
12
|
-
*/
|
|
13
|
-
export declare function encode(value: unknown, refs?: Map<object, number>): unknown;
|
|
14
|
-
/**
|
|
15
|
-
* Decode a tree produced by `encode`. `refs` is the deserialisation memory:
|
|
16
|
-
* each object is pushed in the same pre-order the encoder numbered it in, so
|
|
17
|
-
* `Ref` ids resolve by index. Containers are pushed BEFORE their children so
|
|
18
|
-
* that a cycle can point back at a half-built parent.
|
|
19
|
-
*/
|
|
20
|
-
export declare function decode(value: unknown, refs?: unknown[]): unknown;
|
|
21
|
-
export declare function createDataCloneError(message: string): Error;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { MessagePortTransport } from '@gjsify/message-channel';
|
|
2
|
-
/** Function that writes one already-encoded line to the wire (e.g. parent's
|
|
3
|
-
* `child.stdin.write(line)` or child bootstrap's `process.stdout.write(line)`). */
|
|
4
|
-
export type WireWriter = (line: string) => void;
|
|
5
|
-
/**
|
|
6
|
-
* Per-Worker registry of locally-resident cross-process MessagePort
|
|
7
|
-
* instances, keyed by portId. The wire-side dispatcher (stdout reader on
|
|
8
|
-
* parent, stdin reader on child) looks up the local port by portId and
|
|
9
|
-
* routes incoming `{ __msgport, op: 'send', data }` to
|
|
10
|
-
* `port._inner._receive(data)`. `op: 'close'` calls `port._inner.close()`.
|
|
11
|
-
*
|
|
12
|
-
* Parent-side: keyed by the same portId attached to the SubprocessPortTransport
|
|
13
|
-
* that the OTHER side's port writes from. Child-side bootstrap maintains its
|
|
14
|
-
* own registry with the same shape.
|
|
15
|
-
*/
|
|
16
|
-
export interface CrossProcessPortRegistry extends Map<number, any> {
|
|
17
|
-
}
|
|
18
|
-
export declare class SubprocessPortTransport implements MessagePortTransport {
|
|
19
|
-
private readonly _sendLine;
|
|
20
|
-
private readonly _registry;
|
|
21
|
-
/**
|
|
22
|
-
* @param sendLine writes a JSON-line to the wire (parent: child.stdin;
|
|
23
|
-
* child: process.stdout).
|
|
24
|
-
* @param registry the local registry the port is registered in; on
|
|
25
|
-
* `close()` we drop the entry to keep the registry from
|
|
26
|
-
* growing unbounded across spawn/transfer/terminate
|
|
27
|
-
* cycles.
|
|
28
|
-
*/
|
|
29
|
-
constructor(_sendLine: WireWriter, _registry: CrossProcessPortRegistry);
|
|
30
|
-
send(portId: number, data: unknown): void;
|
|
31
|
-
close(portId: number): void;
|
|
32
|
-
}
|
|
33
|
-
export declare function nextParentPortId(): number;
|
|
34
|
-
export declare function nextChildPortId(): number;
|
|
35
|
-
/** Cross-process MessagePort placeholder. Distinct from the in-process
|
|
36
|
-
* `{ index }` form so the receiver-side materialiser can dispatch on shape. */
|
|
37
|
-
export interface CrossProcessPortPlaceholder {
|
|
38
|
-
readonly __gjsifyTransferredPort: true;
|
|
39
|
-
/** Stable id used by both sides' registries to route subsequent
|
|
40
|
-
* `{ __msgport, op }` lines back to the right local port. */
|
|
41
|
-
readonly portId: number;
|
|
42
|
-
}
|
|
43
|
-
export declare function isCrossProcessPortPlaceholder(value: unknown): value is CrossProcessPortPlaceholder;
|