@gjsify/worker_threads 0.25.1 → 0.26.1
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.
|
@@ -151,7 +151,7 @@ function makeSharedBuffer(native) {
|
|
|
151
151
|
// stdout-reader routes back to the kept end via the parent-side registry.
|
|
152
152
|
//
|
|
153
153
|
// transferList chaining (port-in-port) is intentionally not supported
|
|
154
|
-
// on the cross-process path in v1 — see
|
|
154
|
+
// on the cross-process path in v1 — see status/open-todos.md.
|
|
155
155
|
const childPortRegistry = new Map();
|
|
156
156
|
|
|
157
157
|
function makeChildSidePort(portId) {
|
package/lib/esm/worker.js
CHANGED
|
@@ -2,4 +2,4 @@ import"./_virtual/_rolldown/runtime.js";import{extractSharedBuffers as e}from"./
|
|
|
2
2
|
`;try{this._stdinPipe.write_all(u.encode(h),null)}catch(e){throw this._cleanup(),Error(`Failed to send init data: ${e instanceof Error?e.message:e}`)}if(p){let e=o.DataInputStream.new(p);this._readMessages(e)}let g=this._subprocess.get_stderr_pipe();g&&this._readStderr(o.DataInputStream.new(g)),this._subprocess.wait_async(null,()=>{this._onExit()})}postMessage(e,t){if(!(this._exited||!this._stdinPipe))try{let n=e;t&&t.length>0&&(n=this._extractCrossProcessPorts(e,t));let r=this._serializeWithSabTransfer(n),i=JSON.stringify({type:`message`,data:r})+`
|
|
3
3
|
`;this._stdinPipe.write_all(u.encode(i),null)}catch{}}_writeStdinLine=e=>{if(!(this._exited||!this._stdinPipe))try{this._stdinPipe.write_all(u.encode(e),null)}catch{}};_extractCrossProcessPorts(e,i){let a=new Map;for(let e of i){if(!(e instanceof t))continue;let i=e,o=r(),s=i._otherPort;s&&(s._otherPort=null,i._otherPort=null,s._inner._portId=o,s._inner._partner=null,s._inner._transport=new n(this._writeStdinLine,this._portRegistry),this._portRegistry.set(o,s)),i._detached=!0,i._closed=!0,i._inner.close(),a.set(i,{__gjsifyTransferredPort:!0,portId:o})}if(a.size===0)return e;function walk(e,n){if(typeof e!=`object`||!e)return e;if(e instanceof t)return a.get(e)??e;if(n.has(e))return n.get(e);if(Array.isArray(e)){let t=[];n.set(e,t);for(let r=0;r<e.length;r++)r in e&&(t[r]=walk(e[r],n));return t}if(Object.prototype.toString.call(e).slice(8,-1)===`Object`){let t={};n.set(e,t);for(let r of Object.keys(e))t[r]=walk(e[r],n);return t}return e}return walk(e,new Map)}_serializeWithSabTransfer(t){if(!c||this._sabSocketFd===-1)return t;let{value:n,table:r,nextTag:i}=e(t,this._sabNextTag);this._sabNextTag=i;for(let{tag:e,buffer:t}of r)if(!c.sendFd(this._sabSocketFd,t.fd,e))throw Error(`Failed to send SharedBuffer fd over worker side-channel (tag ${e})`);return n}terminate(){if(this._exited)return Promise.resolve(0);let e=new Promise(e=>{this.once(`exit`,t=>e(t))});try{if(this._stdinPipe){let e=JSON.stringify({type:`terminate`})+`
|
|
4
4
|
`;this._stdinPipe.write_all(u.encode(e),null)}}catch{}return setTimeout(()=>{!this._exited&&this._subprocess&&this._subprocess.force_exit()},500),e}ref(){return this}unref(){return 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=s.get_current_dir(),t=s.build_filenamev([e,n]);return`file://`+(o.File.new_for_path(t).get_path()||t)}return`file://`+n}_readMessages(e){e.read_line_async(s.PRIORITY_DEFAULT,null,(t,n)=>{try{let[t]=e.read_line_finish_utf8(n);if(t===null)return;let r=JSON.parse(t);if(typeof r.__msgport==`number`){let t=this._portRegistry.get(r.__msgport);t&&(r.op===`send`?t._receiveMessage(r.data):r.op===`close`&&(this._portRegistry.delete(r.__msgport),t.close())),this._readMessages(e);return}switch(r.type){case`online`:this.emit(`online`);break;case`message`:this.emit(`message`,r.data);break;case`error`:{let e=Error(r.message);r.stack&&(e.stack=r.stack),this.emit(`error`,e);break}}this._readMessages(e)}catch{}})}_stderrChunks=[];_readStderr(e){e.read_line_async(s.PRIORITY_DEFAULT,null,(t,n)=>{try{let[t]=e.read_line_finish_utf8(n);if(t===null){if(this._stderrChunks.length>0){let e=this._stderrChunks.join(`
|
|
5
|
-
`);this.listenerCount(`error`)===0&&this.emit(`error`,Error(e))}return}this._stderrChunks.push(t),this._readStderr(e)}catch{}})}_onExit(){if(this._exited)return;this._exited=!0;let e=this._subprocess?.get_if_exited()?this._subprocess.get_exit_status():1;this._cleanup(),this.emit(`exit`,e)}_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}
|
|
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._cleanup(),this.emit(`exit`,e)}_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&&c?.closeFd&&(c.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{f as Worker};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const BOOTSTRAP_CODE = "import GLib from 'gi://GLib';\nimport Gio from 'gi://Gio';\nimport System from 'system';\n\nconst loop = new GLib.MainLoop(null, false);\n\n// Shutdown bookkeeping.\n//\n// `loop.run()` is the LAST statement of this module \u2014 it only starts after\n// the worker's own script has finished. `loop.quit()` on a loop that is not\n// running yet is a silent NO-OP, so every shutdown request that arrives while\n// the worker script is still executing used to be lost: the module went on to\n// call `loop.run()` afterwards and the child ran forever.\n//\n// The parent's death is exactly that case. When the spawner dies its end of\n// our stdin pipe closes, the reader below sees EOF and asked the loop to quit\n// \u2014 too early to matter. The child then sat in `loop.run()` with no armed\n// source, or (when its script had a still-pending top-level await, which GJS\n// services by spinning the job queue) burned a full core, forever. Node's\n// workers are threads and die with the process that owns them; an orphan that\n// outlives \u2014 and out-CPUs \u2014 its parent is never right.\n//\n// So: record the request (`shuttingDown`) and honour it at both ends \u2014\n// `loop.run()` is skipped when shutdown was already requested, and the EOF\n// path exits the process outright so a worker script that never settles\n// cannot keep the orphan alive either. `terminate`/`parentPort.close()`\n// deliberately keep the softer flag-only path: their parent is alive and\n// waiting on the exit event, and `Worker.terminate()` already has a\n// `force_exit()` backstop for a wedged script.\nlet shuttingDown = false;\nfunction requestShutdown() {\n shuttingDown = true;\n loop.quit();\n}\nconst stdinStream = Gio.UnixInputStream.new(0, false);\nconst dataIn = Gio.DataInputStream.new(stdinStream);\nconst stdoutStream = Gio.UnixOutputStream.new(1, false);\n\nconst _encoder = new TextEncoder();\n\nfunction send(obj) {\n const line = JSON.stringify(obj) + '\\n';\n stdoutStream.write_all(_encoder.encode(line), null);\n}\n\n// Try to load the sab-native typelib synchronously. Absent prebuild\n// is fine \u2014 only fails the receive path when a SharedBuffer actually\n// crosses the wire.\nlet SabNative = null;\ntry { SabNative = imports.gi.GjsifySabNative; } catch (_) { /* prebuild missing */ }\n\n// Read init data (first line, blocking)\nconst [initLine] = dataIn.read_line_utf8(null);\nconst init = JSON.parse(initLine);\n\n// fd \u2192 SharedBuffer cache keyed by tag. Filled by drainSabFds() right\n// before materialise() walks the parsed JSON.\nconst sabFds = new Map();\n\n// Count how many SharedBuffer placeholders sit in a parsed JSON value.\n// The exact count is what we pass to drainSabFds().\nfunction countSabPlaceholders(value) {\n if (value === null || typeof value !== 'object') return 0;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') return 1;\n if (Array.isArray(value)) {\n let n = 0;\n for (const v of value) n += countSabPlaceholders(v);\n return n;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n let n = 0;\n for (const v of Object.values(value)) n += countSabPlaceholders(v);\n return n;\n }\n return 0;\n}\n\n// Synchronously drain exactly @count SCM_RIGHTS messages from fd 3,\n// indexing each received fd by its sender-encoded tag. Called once per\n// incoming JSON message that contains __sab placeholders. recv_fd\n// blocks at most until the kernel has the message \u2014 and the sender\n// always sends fds before writing to stdin, so by the time we get\n// here every fd is already buffered.\nfunction drainSabFds(count) {\n if (!SabNative || count <= 0) return;\n for (let i = 0; i < count; i++) {\n const [fd, tag] = SabNative.FdChannel.recv_fd(3);\n if (fd <= 0) throw new Error('FdChannel.recv_fd returned ' + fd + ' while draining ' + count + ' fds');\n sabFds.set(tag, fd);\n }\n}\n\nfunction materialise(value) {\n if (value === null || typeof value !== 'object') return value;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') {\n const fd = sabFds.get(value.__sab);\n if (fd === undefined) throw new Error('SharedBuffer placeholder \\'' + value.__sab + '\\' arrived before its fd');\n if (!SabNative) throw new Error('SharedBuffer placeholder arrived but @gjsify/sab-native typelib not loaded');\n const native = SabNative.SharedBuffer.from_fd(fd, value.size);\n sabFds.delete(value.__sab);\n return makeSharedBuffer(native);\n }\n if (isPortPlaceholder(value)) {\n return makeChildSidePort(value.portId);\n }\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) value[i] = materialise(value[i]);\n return value;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n for (const k of Object.keys(value)) value[k] = materialise(value[k]);\n return value;\n }\n return value;\n}\n\nfunction makeSharedBuffer(native) {\n return {\n get byteLength() { return native.byte_length; },\n get fd() { return native.fd; },\n getUint8(off) { return native.get_u8(off); },\n setUint8(off, v) { native.set_u8(off, v); },\n getInt32LE(off) { return native.get_i32_le(off); },\n setInt32LE(off, v) { native.set_i32_le(off, v); },\n getUint32LE(off) { return native.get_u32_le(off); },\n setUint32LE(off, v) { native.set_u32_le(off, v); },\n getUint64LE(off) { return native.get_u64_le(off); },\n setUint64LE(off, v) { native.set_u64_le(off, v); },\n readBytes(off, len) {\n const bytes = native.read_bytes(off, len);\n const data = bytes.get_data();\n return data ? new Uint8Array(data) : new Uint8Array(0);\n },\n writeBytes(off, data) { native.write_bytes(off, new GLib.Bytes(data)); },\n get _nativeHandle() { return native; },\n get constructor() { return { name: 'SharedBuffer' }; },\n };\n}\n\n// Cross-process MessagePort support (child side).\n//\n// When a parent's Worker.postMessage(value, [port2]) transfers a\n// MessagePort, the parent encodes the port as a placeholder\n// { __gjsifyTransferredPort: true, portId: <id> } and ships the rest of\n// the value as JSON. On this side, materialise() walks the parsed JSON\n// and substitutes each placeholder for a fresh \"child-side port\" \u2014 an\n// inline EventEmitter-shaped object that mirrors the worker_threads\n// MessagePort surface enough for user code (postMessage / on('message') /\n// on('close') / close()). Outbound traffic via this port travels over\n// stdout as { __msgport, op: 'send', data } JSON lines, which the parent's\n// stdout-reader routes back to the kept end via the parent-side registry.\n//\n// transferList chaining (port-in-port) is intentionally not supported\n// on the cross-process path in v1 \u2014 see STATUS.md Open TODOs.\nconst childPortRegistry = new Map();\n\nfunction makeChildSidePort(portId) {\n const listeners = new Map();\n let closed = false;\n\n function emit(ev /* , ...args */) {\n const args = Array.prototype.slice.call(arguments, 1);\n const fns = (listeners.get(ev) || []).slice();\n for (const fn of fns) {\n try { fn.apply(null, args); } catch (_) { /* swallow */ }\n }\n }\n\n const port = {\n _portId: portId,\n /** @internal Wire-side delivery \u2014 called by the bootstrap stdin\n * dispatcher when it receives a {__msgport: portId, op: 'send'}\n * line. Schedules dispatch on a microtask so listeners fire after\n * the current call stack unwinds \u2014 matches in-process MessagePort\n * semantics. */\n _receive(data) {\n if (closed) return;\n Promise.resolve().then(() => { if (!closed) emit('message', data); });\n },\n /** @internal Called by the dispatcher when the parent sends\n * {__msgport: portId, op: 'close'}. Fires the local 'close' event\n * and stops accepting further messages. */\n _internalClose() {\n if (closed) return;\n closed = true;\n childPortRegistry.delete(portId);\n Promise.resolve().then(() => emit('close'));\n },\n on(ev, fn) {\n if (!listeners.has(ev)) listeners.set(ev, []);\n listeners.get(ev).push(fn);\n return port;\n },\n once(ev, fn) {\n const w = function () { port.off(ev, w); fn.apply(null, arguments); };\n return port.on(ev, w);\n },\n off(ev, fn) {\n const arr = listeners.get(ev);\n if (arr) listeners.set(ev, arr.filter(function (f) { return f !== fn; }));\n return port;\n },\n addListener(ev, fn) { return port.on(ev, fn); },\n removeListener(ev, fn) { return port.off(ev, fn); },\n emit(ev /* , ...args */) {\n emit.apply(null, Array.prototype.slice.call(arguments));\n return port;\n },\n postMessage(data) {\n if (closed) return;\n send({ __msgport: portId, op: 'send', data });\n },\n close() {\n if (closed) return;\n closed = true;\n try { send({ __msgport: portId, op: 'close' }); } catch (_) {}\n childPortRegistry.delete(portId);\n Promise.resolve().then(() => emit('close'));\n },\n start() { /* no-op \u2014 child-side port dispatches on receive */ },\n ref() { return port; },\n unref() { return port; },\n get _isCrossProcess() { return true; },\n get constructor() { return { name: 'MessagePort' }; },\n };\n\n childPortRegistry.set(portId, port);\n return port;\n}\n\nfunction isPortPlaceholder(value) {\n return value !== null && typeof value === 'object'\n && value.__gjsifyTransferredPort === true\n && typeof value.portId === 'number';\n}\n\n// Drain SharedBuffer fds attached to the init line (workerData may carry\n// SharedBuffer instances) before materialise() runs against it.\nif (init.sabSocketFd === 3) {\n try {\n drainSabFds(countSabPlaceholders(init.workerData));\n } catch (err) {\n send({ type: 'error', message: 'init fd drain failed: ' + (err && err.message ? err.message : err), stack: '' });\n }\n}\n\n// Simplified EventEmitter for parentPort\nconst _listeners = new Map();\nconst parentPort = {\n on(ev, fn) {\n if (!_listeners.has(ev)) _listeners.set(ev, []);\n _listeners.get(ev).push(fn);\n return this;\n },\n once(ev, fn) {\n const w = (...a) => { parentPort.off(ev, w); fn(...a); };\n return parentPort.on(ev, w);\n },\n off(ev, fn) {\n const a = _listeners.get(ev);\n if (a) _listeners.set(ev, a.filter(f => f !== fn));\n return this;\n },\n emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },\n postMessage(data) { send({ type: 'message', data }); },\n close() { send({ type: 'exit', code: 0 }); requestShutdown(); },\n removeAllListeners(ev) {\n if (ev) _listeners.delete(ev); else _listeners.clear();\n return this;\n },\n};\n\n// Set worker context globals (read by @gjsify/worker_threads when imported by user script)\nglobalThis.__gjsify_worker_context = {\n isMainThread: false,\n parentPort,\n workerData: materialise(init.workerData ?? null),\n threadId: init.threadId ?? 0,\n};\n\nsend({ type: 'online' });\n\n// Async stdin reader for messages from parent\nfunction readNext() {\n dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {\n try {\n const [line] = source.read_line_finish_utf8(result);\n if (line === null) {\n // EOF on stdin = the spawner is gone. Nothing can reach us again and\n // nobody is left to observe an exit code, so leave immediately \u2014\n // even from inside the worker script's own pending top-level await.\n requestShutdown();\n System.exit(0);\n return;\n }\n const msg = JSON.parse(line);\n if (typeof msg.__msgport === 'number') {\n // Cross-process MessagePort traffic \u2014 route to the local child-\n // side port from childPortRegistry by portId. Both 'send' and\n // 'close' dispatch on a microtask inside the port's helpers, so\n // user listeners fire after the current stdin-callback unwinds.\n const cpPort = childPortRegistry.get(msg.__msgport);\n if (cpPort) {\n if (msg.op === 'send') cpPort._receive(msg.data);\n else if (msg.op === 'close') cpPort._internalClose();\n }\n // Unknown portId or op silently dropped \u2014 could happen mid-close\n // race where the parent sends a final message after the child has\n // already torn down the local port.\n }\n else if (msg.type === 'message') {\n // Drain fds for any SharedBuffer placeholders BEFORE materialise.\n // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS\n // messages before writing this stdin line, so they're already\n // buffered.\n if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));\n parentPort.emit('message', materialise(msg.data));\n }\n else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); requestShutdown(); return; }\n readNext();\n } catch (err) {\n try {\n send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });\n } catch (_) { /* parent's end of stdout is gone too \u2014 nothing to report to */ }\n requestShutdown();\n }\n });\n}\nreadNext();\n\n// Execute worker code\ntry {\n if (init.eval) {\n const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;\n await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(\n parentPort, globalThis.__gjsify_worker_context.workerData, init.threadId\n );\n } else {\n await import(init.filename);\n }\n} catch (error) {\n send({ type: 'error', message: error.message, stack: error.stack || '' });\n}\n\n// Skip the loop entirely when shutdown was requested while the script above\n// was still running \u2014 otherwise that lost quit() strands the child forever.\nif (!shuttingDown) loop.run();\n";
|
|
1
|
+
export declare const BOOTSTRAP_CODE = "import GLib from 'gi://GLib';\nimport Gio from 'gi://Gio';\nimport System from 'system';\n\nconst loop = new GLib.MainLoop(null, false);\n\n// Shutdown bookkeeping.\n//\n// `loop.run()` is the LAST statement of this module \u2014 it only starts after\n// the worker's own script has finished. `loop.quit()` on a loop that is not\n// running yet is a silent NO-OP, so every shutdown request that arrives while\n// the worker script is still executing used to be lost: the module went on to\n// call `loop.run()` afterwards and the child ran forever.\n//\n// The parent's death is exactly that case. When the spawner dies its end of\n// our stdin pipe closes, the reader below sees EOF and asked the loop to quit\n// \u2014 too early to matter. The child then sat in `loop.run()` with no armed\n// source, or (when its script had a still-pending top-level await, which GJS\n// services by spinning the job queue) burned a full core, forever. Node's\n// workers are threads and die with the process that owns them; an orphan that\n// outlives \u2014 and out-CPUs \u2014 its parent is never right.\n//\n// So: record the request (`shuttingDown`) and honour it at both ends \u2014\n// `loop.run()` is skipped when shutdown was already requested, and the EOF\n// path exits the process outright so a worker script that never settles\n// cannot keep the orphan alive either. `terminate`/`parentPort.close()`\n// deliberately keep the softer flag-only path: their parent is alive and\n// waiting on the exit event, and `Worker.terminate()` already has a\n// `force_exit()` backstop for a wedged script.\nlet shuttingDown = false;\nfunction requestShutdown() {\n shuttingDown = true;\n loop.quit();\n}\nconst stdinStream = Gio.UnixInputStream.new(0, false);\nconst dataIn = Gio.DataInputStream.new(stdinStream);\nconst stdoutStream = Gio.UnixOutputStream.new(1, false);\n\nconst _encoder = new TextEncoder();\n\nfunction send(obj) {\n const line = JSON.stringify(obj) + '\\n';\n stdoutStream.write_all(_encoder.encode(line), null);\n}\n\n// Try to load the sab-native typelib synchronously. Absent prebuild\n// is fine \u2014 only fails the receive path when a SharedBuffer actually\n// crosses the wire.\nlet SabNative = null;\ntry { SabNative = imports.gi.GjsifySabNative; } catch (_) { /* prebuild missing */ }\n\n// Read init data (first line, blocking)\nconst [initLine] = dataIn.read_line_utf8(null);\nconst init = JSON.parse(initLine);\n\n// fd \u2192 SharedBuffer cache keyed by tag. Filled by drainSabFds() right\n// before materialise() walks the parsed JSON.\nconst sabFds = new Map();\n\n// Count how many SharedBuffer placeholders sit in a parsed JSON value.\n// The exact count is what we pass to drainSabFds().\nfunction countSabPlaceholders(value) {\n if (value === null || typeof value !== 'object') return 0;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') return 1;\n if (Array.isArray(value)) {\n let n = 0;\n for (const v of value) n += countSabPlaceholders(v);\n return n;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n let n = 0;\n for (const v of Object.values(value)) n += countSabPlaceholders(v);\n return n;\n }\n return 0;\n}\n\n// Synchronously drain exactly @count SCM_RIGHTS messages from fd 3,\n// indexing each received fd by its sender-encoded tag. Called once per\n// incoming JSON message that contains __sab placeholders. recv_fd\n// blocks at most until the kernel has the message \u2014 and the sender\n// always sends fds before writing to stdin, so by the time we get\n// here every fd is already buffered.\nfunction drainSabFds(count) {\n if (!SabNative || count <= 0) return;\n for (let i = 0; i < count; i++) {\n const [fd, tag] = SabNative.FdChannel.recv_fd(3);\n if (fd <= 0) throw new Error('FdChannel.recv_fd returned ' + fd + ' while draining ' + count + ' fds');\n sabFds.set(tag, fd);\n }\n}\n\nfunction materialise(value) {\n if (value === null || typeof value !== 'object') return value;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') {\n const fd = sabFds.get(value.__sab);\n if (fd === undefined) throw new Error('SharedBuffer placeholder \\'' + value.__sab + '\\' arrived before its fd');\n if (!SabNative) throw new Error('SharedBuffer placeholder arrived but @gjsify/sab-native typelib not loaded');\n const native = SabNative.SharedBuffer.from_fd(fd, value.size);\n sabFds.delete(value.__sab);\n return makeSharedBuffer(native);\n }\n if (isPortPlaceholder(value)) {\n return makeChildSidePort(value.portId);\n }\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) value[i] = materialise(value[i]);\n return value;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n for (const k of Object.keys(value)) value[k] = materialise(value[k]);\n return value;\n }\n return value;\n}\n\nfunction makeSharedBuffer(native) {\n return {\n get byteLength() { return native.byte_length; },\n get fd() { return native.fd; },\n getUint8(off) { return native.get_u8(off); },\n setUint8(off, v) { native.set_u8(off, v); },\n getInt32LE(off) { return native.get_i32_le(off); },\n setInt32LE(off, v) { native.set_i32_le(off, v); },\n getUint32LE(off) { return native.get_u32_le(off); },\n setUint32LE(off, v) { native.set_u32_le(off, v); },\n getUint64LE(off) { return native.get_u64_le(off); },\n setUint64LE(off, v) { native.set_u64_le(off, v); },\n readBytes(off, len) {\n const bytes = native.read_bytes(off, len);\n const data = bytes.get_data();\n return data ? new Uint8Array(data) : new Uint8Array(0);\n },\n writeBytes(off, data) { native.write_bytes(off, new GLib.Bytes(data)); },\n get _nativeHandle() { return native; },\n get constructor() { return { name: 'SharedBuffer' }; },\n };\n}\n\n// Cross-process MessagePort support (child side).\n//\n// When a parent's Worker.postMessage(value, [port2]) transfers a\n// MessagePort, the parent encodes the port as a placeholder\n// { __gjsifyTransferredPort: true, portId: <id> } and ships the rest of\n// the value as JSON. On this side, materialise() walks the parsed JSON\n// and substitutes each placeholder for a fresh \"child-side port\" \u2014 an\n// inline EventEmitter-shaped object that mirrors the worker_threads\n// MessagePort surface enough for user code (postMessage / on('message') /\n// on('close') / close()). Outbound traffic via this port travels over\n// stdout as { __msgport, op: 'send', data } JSON lines, which the parent's\n// stdout-reader routes back to the kept end via the parent-side registry.\n//\n// transferList chaining (port-in-port) is intentionally not supported\n// on the cross-process path in v1 \u2014 see status/open-todos.md.\nconst childPortRegistry = new Map();\n\nfunction makeChildSidePort(portId) {\n const listeners = new Map();\n let closed = false;\n\n function emit(ev /* , ...args */) {\n const args = Array.prototype.slice.call(arguments, 1);\n const fns = (listeners.get(ev) || []).slice();\n for (const fn of fns) {\n try { fn.apply(null, args); } catch (_) { /* swallow */ }\n }\n }\n\n const port = {\n _portId: portId,\n /** @internal Wire-side delivery \u2014 called by the bootstrap stdin\n * dispatcher when it receives a {__msgport: portId, op: 'send'}\n * line. Schedules dispatch on a microtask so listeners fire after\n * the current call stack unwinds \u2014 matches in-process MessagePort\n * semantics. */\n _receive(data) {\n if (closed) return;\n Promise.resolve().then(() => { if (!closed) emit('message', data); });\n },\n /** @internal Called by the dispatcher when the parent sends\n * {__msgport: portId, op: 'close'}. Fires the local 'close' event\n * and stops accepting further messages. */\n _internalClose() {\n if (closed) return;\n closed = true;\n childPortRegistry.delete(portId);\n Promise.resolve().then(() => emit('close'));\n },\n on(ev, fn) {\n if (!listeners.has(ev)) listeners.set(ev, []);\n listeners.get(ev).push(fn);\n return port;\n },\n once(ev, fn) {\n const w = function () { port.off(ev, w); fn.apply(null, arguments); };\n return port.on(ev, w);\n },\n off(ev, fn) {\n const arr = listeners.get(ev);\n if (arr) listeners.set(ev, arr.filter(function (f) { return f !== fn; }));\n return port;\n },\n addListener(ev, fn) { return port.on(ev, fn); },\n removeListener(ev, fn) { return port.off(ev, fn); },\n emit(ev /* , ...args */) {\n emit.apply(null, Array.prototype.slice.call(arguments));\n return port;\n },\n postMessage(data) {\n if (closed) return;\n send({ __msgport: portId, op: 'send', data });\n },\n close() {\n if (closed) return;\n closed = true;\n try { send({ __msgport: portId, op: 'close' }); } catch (_) {}\n childPortRegistry.delete(portId);\n Promise.resolve().then(() => emit('close'));\n },\n start() { /* no-op \u2014 child-side port dispatches on receive */ },\n ref() { return port; },\n unref() { return port; },\n get _isCrossProcess() { return true; },\n get constructor() { return { name: 'MessagePort' }; },\n };\n\n childPortRegistry.set(portId, port);\n return port;\n}\n\nfunction isPortPlaceholder(value) {\n return value !== null && typeof value === 'object'\n && value.__gjsifyTransferredPort === true\n && typeof value.portId === 'number';\n}\n\n// Drain SharedBuffer fds attached to the init line (workerData may carry\n// SharedBuffer instances) before materialise() runs against it.\nif (init.sabSocketFd === 3) {\n try {\n drainSabFds(countSabPlaceholders(init.workerData));\n } catch (err) {\n send({ type: 'error', message: 'init fd drain failed: ' + (err && err.message ? err.message : err), stack: '' });\n }\n}\n\n// Simplified EventEmitter for parentPort\nconst _listeners = new Map();\nconst parentPort = {\n on(ev, fn) {\n if (!_listeners.has(ev)) _listeners.set(ev, []);\n _listeners.get(ev).push(fn);\n return this;\n },\n once(ev, fn) {\n const w = (...a) => { parentPort.off(ev, w); fn(...a); };\n return parentPort.on(ev, w);\n },\n off(ev, fn) {\n const a = _listeners.get(ev);\n if (a) _listeners.set(ev, a.filter(f => f !== fn));\n return this;\n },\n emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },\n postMessage(data) { send({ type: 'message', data }); },\n close() { send({ type: 'exit', code: 0 }); requestShutdown(); },\n removeAllListeners(ev) {\n if (ev) _listeners.delete(ev); else _listeners.clear();\n return this;\n },\n};\n\n// Set worker context globals (read by @gjsify/worker_threads when imported by user script)\nglobalThis.__gjsify_worker_context = {\n isMainThread: false,\n parentPort,\n workerData: materialise(init.workerData ?? null),\n threadId: init.threadId ?? 0,\n};\n\nsend({ type: 'online' });\n\n// Async stdin reader for messages from parent\nfunction readNext() {\n dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {\n try {\n const [line] = source.read_line_finish_utf8(result);\n if (line === null) {\n // EOF on stdin = the spawner is gone. Nothing can reach us again and\n // nobody is left to observe an exit code, so leave immediately \u2014\n // even from inside the worker script's own pending top-level await.\n requestShutdown();\n System.exit(0);\n return;\n }\n const msg = JSON.parse(line);\n if (typeof msg.__msgport === 'number') {\n // Cross-process MessagePort traffic \u2014 route to the local child-\n // side port from childPortRegistry by portId. Both 'send' and\n // 'close' dispatch on a microtask inside the port's helpers, so\n // user listeners fire after the current stdin-callback unwinds.\n const cpPort = childPortRegistry.get(msg.__msgport);\n if (cpPort) {\n if (msg.op === 'send') cpPort._receive(msg.data);\n else if (msg.op === 'close') cpPort._internalClose();\n }\n // Unknown portId or op silently dropped \u2014 could happen mid-close\n // race where the parent sends a final message after the child has\n // already torn down the local port.\n }\n else if (msg.type === 'message') {\n // Drain fds for any SharedBuffer placeholders BEFORE materialise.\n // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS\n // messages before writing this stdin line, so they're already\n // buffered.\n if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));\n parentPort.emit('message', materialise(msg.data));\n }\n else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); requestShutdown(); return; }\n readNext();\n } catch (err) {\n try {\n send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });\n } catch (_) { /* parent's end of stdout is gone too \u2014 nothing to report to */ }\n requestShutdown();\n }\n });\n}\nreadNext();\n\n// Execute worker code\ntry {\n if (init.eval) {\n const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;\n await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(\n parentPort, globalThis.__gjsify_worker_context.workerData, init.threadId\n );\n } else {\n await import(init.filename);\n }\n} catch (error) {\n send({ type: 'error', message: error.message, stack: error.stack || '' });\n}\n\n// Skip the loop entirely when shutdown was requested while the script above\n// was still running \u2014 otherwise that lost quit() strands the child forever.\nif (!shuttingDown) loop.run();\n";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/worker_threads",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "Node.js worker_threads module for Gjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "lib/esm/index.js",
|
|
@@ -39,18 +39,18 @@
|
|
|
39
39
|
"worker_threads"
|
|
40
40
|
],
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@gjsify/cli": "^0.
|
|
43
|
-
"@gjsify/node-globals": "^0.
|
|
44
|
-
"@gjsify/unit": "^0.
|
|
42
|
+
"@gjsify/cli": "^0.26.1",
|
|
43
|
+
"@gjsify/node-globals": "^0.26.1",
|
|
44
|
+
"@gjsify/unit": "^0.26.1",
|
|
45
45
|
"@types/node": "^25.9.2",
|
|
46
46
|
"typescript": "^6.0.3"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@girs/gio-2.0": "^4.1.0",
|
|
50
50
|
"@girs/glib-2.0": "^4.1.0",
|
|
51
|
-
"@gjsify/events": "^0.
|
|
52
|
-
"@gjsify/message-channel": "^0.
|
|
53
|
-
"@gjsify/sab-native": "^0.
|
|
51
|
+
"@gjsify/events": "^0.26.1",
|
|
52
|
+
"@gjsify/message-channel": "^0.26.1",
|
|
53
|
+
"@gjsify/sab-native": "^0.26.1"
|
|
54
54
|
},
|
|
55
55
|
"gjsify": {
|
|
56
56
|
"runtimes": {
|