@gjsify/worker_threads 0.4.13 → 0.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{isSharedBuffer as e}from"./sab-transfer.js";import{EventEmitter as t}from"node:events";function isTransferredPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0}var n=class MessagePort extends t{_started=!1;_closed=!1;_detached=!1;_messageQueue=[];_otherPort=null;_aeWrappers=new Map;start(){this._started||this._closed||(this._started=!0,this._drainQueue())}close(){if(this._closed)return;this._closed=!0;let e=this._otherPort;this._otherPort=null,e&&(e._otherPort=null),this.emit(`close`),this.removeAllListeners()}postMessage(t,n){if(this._closed)return;let r=this._otherPort;if(!r)return;let i=[],a=[];if(n&&n.length>0){let t=new Set;for(let r of n){if(t.has(r))throw createDataCloneError(`Transfer list contains duplicate entries`);if(t.add(r),r instanceof MessagePort){if(r===this)throw createDataCloneError(`Cannot transfer source port`);if(r._closed||r._detached)throw createDataCloneError(`MessagePort in transfer list is already detached`);a.push(r);continue}let o=Object.prototype.toString.call(r).slice(8,-1);if(o===`ArrayBuffer`){let e=r;if(e.detached===!0)throw createDataCloneError(`ArrayBuffer in transfer list is detached`);i.push(e);continue}if(o===`SharedArrayBuffer`)throw createDataCloneError(`SharedArrayBuffer cannot appear in transfer list (it is shared, not transferred)`);if(!e(r))throw createDataCloneError(`Value at index ${n.indexOf(r)} of transfer list is not transferable`)}}let o=a.slice(),s=t;a.length>0&&(s=substitutePortsWithPlaceholders(t,a));let c;try{c=structuredClone(s,{transfer:i.length>0?i:void 0})}catch(e){this.emit(`messageerror`,e instanceof Error?e:Error(`Could not clone message`));return}let l=c;if(o.length>0){let e=o.map(e=>{let t=e._otherPort,n=e._messageQueue.slice();e._messageQueue.length=0,e._otherPort=null,e._detached=!0,e._closed=!0;let r=new MessagePort;r._otherPort=t,t&&(t._otherPort=r);for(let e of n)r._messageQueue.push(e);return r});l=replacePlaceholdersWithPorts(c,e)}r._receiveMessage(l)}ref(){return this}unref(){return this}_receiveMessage(e){if(!this._closed){if(!this._started){this._messageQueue.push(e);return}this._dispatchMessage(e)}}get _hasQueuedMessages(){return this._messageQueue.length>0}_dequeueMessage(){return this._messageQueue.shift()}get _wasTransferred(){return this._detached}_drainQueue(){for(;this._messageQueue.length>0;)this._dispatchMessage(this._messageQueue.shift())}_dispatchMessage(e){Promise.resolve().then(()=>{this._closed||this.emit(`message`,e)})}addEventListener(e,t){if(t)if(e===`message`){let wrapper=e=>{t({data:e,type:`message`})};this._aeWrappers.set(t,wrapper),super.on(`message`,wrapper)}else super.on(e,t)}removeEventListener(e,t){if(t)if(e===`message`){let e=this._aeWrappers.get(t);e&&(super.off(`message`,e),this._aeWrappers.delete(t))}else super.off(e,t)}on(e,t){return super.on(e,t),e===`message`&&!this._started&&this.start(),this}addListener(e,t){return this.on(e,t)}once(e,t){return super.once(e,t),e===`message`&&!this._started&&this.start(),this}};function createDataCloneError(e){let t=globalThis.DOMException;if(typeof t==`function`){let n=new t(e,`DataCloneError`);if(n.code===void 0)try{Object.defineProperty(n,`code`,{value:25,configurable:!0})}catch{}return n}let n=Error(e);return n.name=`DataCloneError`,n.code=25,n}function substitutePortsWithPlaceholders(e,t){let r=new Map;for(let e=0;e<t.length;e++)r.set(t[e],e);function walk(e,t){if(typeof e!=`object`||!e)return e;if(e instanceof n){let t=r.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{n as MessagePort};
1
+ import"./_virtual/_rolldown/runtime.js";import{isSharedBuffer as e}from"./sab-transfer.js";import{EventEmitter as t}from"node:events";import{MessagePort as n}from"@gjsify/message-channel";function isTransferredPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0}var r=class MessagePort extends t{_closed=!1;_detached=!1;_otherPort=null;_inner=new n;start(){if(this._closed||this._inner._started)return;let e=this._inner._queue.slice();this._inner.start();for(let t of e)this._dispatchEmit(t)}close(){if(this._closed)return;this._closed=!0;let e=this._otherPort;this._otherPort=null,e&&(e._otherPort=null),this._inner.close(),this.emit(`close`),this.removeAllListeners()}postMessage(t,n){if(this._closed)return;let r=this._otherPort;if(!r&&this._inner._transport!==null){if(n&&n.length>0)throw createDataCloneError(`transferList is not supported on cross-process MessagePort yet`);this._inner.postMessage(t);return}if(!r)return;let i=[],a=[];if(n&&n.length>0){let t=new Set;for(let r of n){if(t.has(r))throw createDataCloneError(`Transfer list contains duplicate entries`);if(t.add(r),r instanceof MessagePort){if(r===this)throw createDataCloneError(`Cannot transfer source port`);if(r._closed||r._detached)throw createDataCloneError(`MessagePort in transfer list is already detached`);a.push(r);continue}let o=Object.prototype.toString.call(r).slice(8,-1);if(o===`ArrayBuffer`){let e=r;if(e.detached===!0)throw createDataCloneError(`ArrayBuffer in transfer list is detached`);i.push(e);continue}if(o===`SharedArrayBuffer`)throw createDataCloneError(`SharedArrayBuffer cannot appear in transfer list (it is shared, not transferred)`);if(!e(r))throw createDataCloneError(`Value at index ${n.indexOf(r)} of transfer list is not transferable`)}}let o=a.slice(),s=t;a.length>0&&(s=substitutePortsWithPlaceholders(t,a));let c;try{c=structuredClone(s,{transfer:i.length>0?i:void 0})}catch(e){this.emit(`messageerror`,e instanceof Error?e:Error(`Could not clone message`));return}let l=c;if(o.length>0){let e=o.map(e=>{let t=e._otherPort;e._otherPort=null,e._detached=!0,e._closed=!0;let n=new MessagePort;return n._otherPort=t,t&&(t._otherPort=n),n});l=replacePlaceholdersWithPorts(c,e)}r._receiveMessage(l)}ref(){return this}unref(){return this}_receiveMessage(e){this._closed||(this._inner._receive(e),this._inner._started&&this._dispatchEmit(e))}get _hasQueuedMessages(){return this._inner._queue.length>0}_dequeueMessage(){return this._inner._queue.shift()}get _wasTransferred(){return this._detached}_dispatchEmit(e){Promise.resolve().then(()=>{this._closed||this.emit(`message`,e)})}addEventListener(e,t,n){if(t){if(e===`message`||e===`messageerror`){Object.getPrototypeOf(Object.getPrototypeOf(this._inner)).addEventListener.call(this._inner,e,t,n);return}super.on(e,t)}}removeEventListener(e,t,n){if(t){if(e===`message`||e===`messageerror`){Object.getPrototypeOf(Object.getPrototypeOf(this._inner)).removeEventListener.call(this._inner,e,t,n);return}super.off(e,t)}}get onmessage(){return this._inner.onmessage}set onmessage(e){if(e!==null&&!this._inner._started){let t=this._inner._queue.slice();this._inner.onmessage=e;for(let e of t)this._dispatchEmit(e)}else this._inner.onmessage=e}get onmessageerror(){return this._inner.onmessageerror}set onmessageerror(e){this._inner.onmessageerror=e}on(e,t){return super.on(e,t),e===`message`&&this.start(),this}addListener(e,t){return this.on(e,t)}once(e,t){return super.once(e,t),e===`message`&&this.start(),this}};function createDataCloneError(e){let t=globalThis.DOMException;if(typeof t==`function`){let n=new t(e,`DataCloneError`);if(n.code===void 0)try{Object.defineProperty(n,`code`,{value:25,configurable:!0})}catch{}return n}let n=Error(e);return n.name=`DataCloneError`,n.code=25,n}function substitutePortsWithPlaceholders(e,t){let n=new Map;for(let e=0;e<t.length;e++)n.set(t[e],e);function walk(e,t){if(typeof e!=`object`||!e)return e;if(e instanceof r){let t=n.get(e);return t===void 0?e:{__gjsifyTransferredPort:!0,index:t}}if(t.has(e))return t.get(e);if(Array.isArray(e)){let n=[];t.set(e,n);for(let r=0;r<e.length;r++)r in e&&(n[r]=walk(e[r],t));return n}if(Object.prototype.toString.call(e).slice(8,-1)===`Object`){let n={};t.set(e,n);for(let r of Object.keys(e))n[r]=walk(e[r],t);return n}return e}return walk(e,new Map)}function replacePlaceholdersWithPorts(e,t){function walk(e,n){if(typeof e!=`object`||!e)return e;if(isTransferredPortPlaceholder(e))return t[e.index];if(n.has(e))return n.get(e);if(Array.isArray(e)){n.set(e,e);for(let t=0;t<e.length;t++)t in e&&(e[t]=walk(e[t],n));return e}if(Object.prototype.toString.call(e).slice(8,-1)===`Object`){n.set(e,e);for(let t of Object.keys(e))e[t]=walk(e[t],n);return e}return e}return walk(e,new Map)}export{r as MessagePort};
@@ -0,0 +1,3 @@
1
+ import"./_virtual/_rolldown/runtime.js";var SubprocessPortTransport=class{_sendLine;_registry;constructor(e,t){this._sendLine=e,this._registry=t}send(e,t){let n=JSON.stringify({__msgport:e,op:`send`,data:t})+`
2
+ `;this._sendLine(n)}close(e){let t=JSON.stringify({__msgport:e,op:`close`})+`
3
+ `;try{this._sendLine(t)}catch{}this._registry.delete(e)}};let e=1,t=2;function nextParentPortId(){let t=e;return e+=2,t}function nextChildPortId(){let e=t;return t+=2,e}function isCrossProcessPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0&&typeof e.portId==`number`}export{SubprocessPortTransport,isCrossProcessPortPlaceholder,nextChildPortId,nextParentPortId};
package/lib/esm/worker.js CHANGED
@@ -1,4 +1,4 @@
1
- import"./_virtual/_rolldown/runtime.js";import{extractSharedBuffers as e}from"./sab-transfer.js";import{EventEmitter as t}from"node:events";import n from"@girs/gio-2.0";import r from"@girs/glib-2.0";import{fdChannel as i}from"@gjsify/sab-native";let a=1;const o=new TextEncoder,s=o.encode(`import GLib from 'gi://GLib';
1
+ import"./_virtual/_rolldown/runtime.js";import{extractSharedBuffers as e}from"./sab-transfer.js";import{MessagePort as t}from"./message-port.js";import{SubprocessPortTransport as n,nextParentPortId as r}from"./subprocess-port-transport.js";import{EventEmitter as i}from"node:events";import a from"@girs/gio-2.0";import o from"@girs/glib-2.0";import{fdChannel as s}from"@gjsify/sab-native";let c=1;const l=new TextEncoder,u=l.encode(`import GLib from 'gi://GLib';
2
2
  import Gio from 'gi://Gio';
3
3
 
4
4
  const loop = new GLib.MainLoop(null, false);
@@ -70,6 +70,9 @@ function materialise(value) {
70
70
  sabFds.delete(value.__sab);
71
71
  return makeSharedBuffer(native);
72
72
  }
73
+ if (isPortPlaceholder(value)) {
74
+ return makeChildSidePort(value.portId);
75
+ }
73
76
  if (Array.isArray(value)) {
74
77
  for (let i = 0; i < value.length; i++) value[i] = materialise(value[i]);
75
78
  return value;
@@ -104,6 +107,103 @@ function makeSharedBuffer(native) {
104
107
  };
105
108
  }
106
109
 
110
+ // Cross-process MessagePort support (child side).
111
+ //
112
+ // When a parent's Worker.postMessage(value, [port2]) transfers a
113
+ // MessagePort, the parent encodes the port as a placeholder
114
+ // { __gjsifyTransferredPort: true, portId: <id> } and ships the rest of
115
+ // the value as JSON. On this side, materialise() walks the parsed JSON
116
+ // and substitutes each placeholder for a fresh "child-side port" — an
117
+ // inline EventEmitter-shaped object that mirrors the worker_threads
118
+ // MessagePort surface enough for user code (postMessage / on('message') /
119
+ // on('close') / close()). Outbound traffic via this port travels over
120
+ // stdout as { __msgport, op: 'send', data } JSON lines, which the parent's
121
+ // stdout-reader routes back to the kept end via the parent-side registry.
122
+ //
123
+ // transferList chaining (port-in-port) is intentionally not supported
124
+ // on the cross-process path in v1 — see STATUS.md Open TODOs.
125
+ const childPortRegistry = new Map();
126
+
127
+ function makeChildSidePort(portId) {
128
+ const listeners = new Map();
129
+ let closed = false;
130
+
131
+ function emit(ev /* , ...args */) {
132
+ const args = Array.prototype.slice.call(arguments, 1);
133
+ const fns = (listeners.get(ev) || []).slice();
134
+ for (const fn of fns) {
135
+ try { fn.apply(null, args); } catch (_) { /* swallow */ }
136
+ }
137
+ }
138
+
139
+ const port = {
140
+ _portId: portId,
141
+ /** @internal Wire-side delivery — called by the bootstrap stdin
142
+ * dispatcher when it receives a {__msgport: portId, op: 'send'}
143
+ * line. Schedules dispatch on a microtask so listeners fire after
144
+ * the current call stack unwinds — matches in-process MessagePort
145
+ * semantics. */
146
+ _receive(data) {
147
+ if (closed) return;
148
+ Promise.resolve().then(() => { if (!closed) emit('message', data); });
149
+ },
150
+ /** @internal Called by the dispatcher when the parent sends
151
+ * {__msgport: portId, op: 'close'}. Fires the local 'close' event
152
+ * and stops accepting further messages. */
153
+ _internalClose() {
154
+ if (closed) return;
155
+ closed = true;
156
+ childPortRegistry.delete(portId);
157
+ Promise.resolve().then(() => emit('close'));
158
+ },
159
+ on(ev, fn) {
160
+ if (!listeners.has(ev)) listeners.set(ev, []);
161
+ listeners.get(ev).push(fn);
162
+ return port;
163
+ },
164
+ once(ev, fn) {
165
+ const w = function () { port.off(ev, w); fn.apply(null, arguments); };
166
+ return port.on(ev, w);
167
+ },
168
+ off(ev, fn) {
169
+ const arr = listeners.get(ev);
170
+ if (arr) listeners.set(ev, arr.filter(function (f) { return f !== fn; }));
171
+ return port;
172
+ },
173
+ addListener(ev, fn) { return port.on(ev, fn); },
174
+ removeListener(ev, fn) { return port.off(ev, fn); },
175
+ emit(ev /* , ...args */) {
176
+ emit.apply(null, Array.prototype.slice.call(arguments));
177
+ return port;
178
+ },
179
+ postMessage(data) {
180
+ if (closed) return;
181
+ send({ __msgport: portId, op: 'send', data });
182
+ },
183
+ close() {
184
+ if (closed) return;
185
+ closed = true;
186
+ try { send({ __msgport: portId, op: 'close' }); } catch (_) {}
187
+ childPortRegistry.delete(portId);
188
+ Promise.resolve().then(() => emit('close'));
189
+ },
190
+ start() { /* no-op — child-side port dispatches on receive */ },
191
+ ref() { return port; },
192
+ unref() { return port; },
193
+ get _isCrossProcess() { return true; },
194
+ get constructor() { return { name: 'MessagePort' }; },
195
+ };
196
+
197
+ childPortRegistry.set(portId, port);
198
+ return port;
199
+ }
200
+
201
+ function isPortPlaceholder(value) {
202
+ return value !== null && typeof value === 'object'
203
+ && value.__gjsifyTransferredPort === true
204
+ && typeof value.portId === 'number';
205
+ }
206
+
107
207
  // Drain SharedBuffer fds attached to the init line (workerData may carry
108
208
  // SharedBuffer instances) before materialise() runs against it.
109
209
  if (init.sabSocketFd === 3) {
@@ -157,7 +257,21 @@ function readNext() {
157
257
  const [line] = source.read_line_finish_utf8(result);
158
258
  if (line === null) { loop.quit(); return; }
159
259
  const msg = JSON.parse(line);
160
- if (msg.type === 'message') {
260
+ if (typeof msg.__msgport === 'number') {
261
+ // Cross-process MessagePort traffic — route to the local child-
262
+ // side port from childPortRegistry by portId. Both 'send' and
263
+ // 'close' dispatch on a microtask inside the port's helpers, so
264
+ // user listeners fire after the current stdin-callback unwinds.
265
+ const cpPort = childPortRegistry.get(msg.__msgport);
266
+ if (cpPort) {
267
+ if (msg.op === 'send') cpPort._receive(msg.data);
268
+ else if (msg.op === 'close') cpPort._internalClose();
269
+ }
270
+ // Unknown portId or op silently dropped — could happen mid-close
271
+ // race where the parent sends a final message after the child has
272
+ // already torn down the local port.
273
+ }
274
+ else if (msg.type === 'message') {
161
275
  // Drain fds for any SharedBuffer placeholders BEFORE materialise.
162
276
  // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS
163
277
  // messages before writing this stdin line, so they're already
@@ -190,8 +304,8 @@ try {
190
304
  }
191
305
 
192
306
  loop.run();
193
- `);var c=class Worker extends t{threadId;resourceLimits;_subprocess=null;_stdinPipe=null;_exited=!1;_bootstrapFile=null;_sabSocketFd=-1;_sabNextTag=0;constructor(e,t){super(),this.threadId=a++,this.resourceLimits=t?.resourceLimits||{};let c=t?.eval===!0,l=Worker._resolveFilename(e,c),u=`${r.get_tmp_dir()}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;this._bootstrapFile=n.File.new_for_path(u);try{this._bootstrapFile.replace_contents(s,null,!1,n.FileCreateFlags.REPLACE_DESTINATION,null)}catch(e){throw Error(`Failed to create worker bootstrap: ${e instanceof Error?e.message:e}`)}let d=new n.SubprocessLauncher({flags:n.SubprocessFlags.STDIN_PIPE|n.SubprocessFlags.STDOUT_PIPE|n.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(i){let e=i.makePair();e&&(this._sabSocketFd=e.parentFd,f=e.childFd,d.take_fd(f,3))}try{this._subprocess=d.spawnv([`gjs`,`-m`,u])}catch(e){throw this._cleanup(),Error(`Failed to spawn worker: ${e instanceof Error?e.message:e}`)}this._stdinPipe=this._subprocess.get_stdin_pipe();let p=this._subprocess.get_stdout_pipe();if(!c){let e=l.startsWith(`file://`)?l.slice(7):l;if(!n.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}}let m=this._serializeWithSabTransfer(t?.workerData??null),h=JSON.stringify({threadId:this.threadId,workerData:m,eval:c,filename:c?void 0:l,code:c?l:void 0,sabSocketFd:this._sabSocketFd===-1?-1:3})+`
194
- `;try{this._stdinPipe.write_all(o.encode(h),null)}catch(e){throw this._cleanup(),Error(`Failed to send init data: ${e instanceof Error?e.message:e}`)}if(p){let e=n.DataInputStream.new(p);this._readMessages(e)}let g=this._subprocess.get_stderr_pipe();g&&this._readStderr(n.DataInputStream.new(g)),this._subprocess.wait_async(null,()=>{this._onExit()})}postMessage(e,t){if(!(this._exited||!this._stdinPipe))try{let t=this._serializeWithSabTransfer(e),n=JSON.stringify({type:`message`,data:t})+`
195
- `;this._stdinPipe.write_all(o.encode(n),null)}catch{}}_serializeWithSabTransfer(t){if(!i||this._sabSocketFd===-1)return t;let{value:n,table:r,nextTag:a}=e(t,this._sabNextTag);this._sabNextTag=a;for(let{tag:e,buffer:t}of r)if(!i.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`})+`
196
- `;this._stdinPipe.write_all(o.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 i=String(e);if(i.startsWith(`file://`)||i.startsWith(`http://`)||i.startsWith(`https://`))return i;if(i.startsWith(`/`))return`file://`+i;if(i.startsWith(`./`)||i.startsWith(`../`)||!i.includes(`/`)){let e=r.get_current_dir(),t=r.build_filenamev([e,i]);return`file://`+(n.File.new_for_path(t).get_path()||t)}return`file://`+i}_readMessages(e){e.read_line_async(r.PRIORITY_DEFAULT,null,(t,n)=>{try{let[t]=e.read_line_finish_utf8(n);if(t===null)return;let r=JSON.parse(t);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(r.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(`
197
- `);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}if(this._sabSocketFd!==-1&&i?.closeFd){try{i.closeFd(this._sabSocketFd)}catch{}this._sabSocketFd=-1}this._subprocess=null}};export{c as Worker};
307
+ `);var d=class Worker extends i{threadId;resourceLimits;_subprocess=null;_stdinPipe=null;_exited=!1;_bootstrapFile=null;_sabSocketFd=-1;_sabNextTag=0;_portRegistry=new Map;constructor(e,t){super(),this.threadId=c++,this.resourceLimits=t?.resourceLimits||{};let n=t?.eval===!0,r=Worker._resolveFilename(e,n),i=`${o.get_tmp_dir()}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;this._bootstrapFile=a.File.new_for_path(i);try{this._bootstrapFile.replace_contents(u,null,!1,a.FileCreateFlags.REPLACE_DESTINATION,null)}catch(e){throw Error(`Failed to create worker bootstrap: ${e instanceof Error?e.message:e}`)}let d=new a.SubprocessLauncher({flags:a.SubprocessFlags.STDIN_PIPE|a.SubprocessFlags.STDOUT_PIPE|a.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(s){let e=s.makePair();e&&(this._sabSocketFd=e.parentFd,f=e.childFd,d.take_fd(f,3))}try{this._subprocess=d.spawnv([`gjs`,`-m`,i])}catch(e){throw this._cleanup(),Error(`Failed to spawn worker: ${e instanceof Error?e.message:e}`)}this._stdinPipe=this._subprocess.get_stdin_pipe();let p=this._subprocess.get_stdout_pipe();if(!n){let e=r.startsWith(`file://`)?r.slice(7):r;if(!a.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}}let m=this._serializeWithSabTransfer(t?.workerData??null),h=JSON.stringify({threadId:this.threadId,workerData:m,eval:n,filename:n?void 0:r,code:n?r:void 0,sabSocketFd:this._sabSocketFd===-1?-1:3})+`
308
+ `;try{this._stdinPipe.write_all(l.encode(h),null)}catch(e){throw this._cleanup(),Error(`Failed to send init data: ${e instanceof Error?e.message:e}`)}if(p){let e=a.DataInputStream.new(p);this._readMessages(e)}let g=this._subprocess.get_stderr_pipe();g&&this._readStderr(a.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})+`
309
+ `;this._stdinPipe.write_all(l.encode(i),null)}catch{}}_writeStdinLine=e=>{if(!(this._exited||!this._stdinPipe))try{this._stdinPipe.write_all(l.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(!s||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(!s.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`})+`
310
+ `;this._stdinPipe.write_all(l.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=o.get_current_dir(),t=o.build_filenamev([e,n]);return`file://`+(a.File.new_for_path(t).get_path()||t)}return`file://`+n}_readMessages(e){e.read_line_async(o.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(o.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(`
311
+ `);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}if(this._sabSocketFd!==-1&&s?.closeFd){try{s.closeFd(this._sabSocketFd)}catch{}this._sabSocketFd=-1}for(let e of this._portRegistry.values())try{e._inner._closed=!0,e._inner._transport=null}catch{}this._portRegistry.clear(),this._subprocess=null}};export{d as Worker};
@@ -1,13 +1,14 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ import { MessagePort as SharedMessagePort } from '@gjsify/message-channel';
2
3
  export declare class MessagePort extends EventEmitter {
3
- private _started;
4
4
  private _closed;
5
5
  private _detached;
6
- private _messageQueue;
7
6
  /** @internal Linked port for in-process communication */
8
7
  _otherPort: MessagePort | null;
9
- /** @internal Maps addEventListener listeners to their internal wrappers */
10
- private _aeWrappers;
8
+ /** @internal W3C-surface delegate from `@gjsify/message-channel`. Owns the
9
+ * canonical started/queue/closed state — the wrapper mirrors lifecycle
10
+ * events to the EventEmitter side. */
11
+ _inner: SharedMessagePort;
11
12
  start(): void;
12
13
  close(): void;
13
14
  postMessage(value: unknown, transferList?: unknown[]): void;
@@ -18,15 +19,25 @@ export declare class MessagePort extends EventEmitter {
18
19
  _dequeueMessage(): unknown | undefined;
19
20
  /** @internal Has this port been transferred elsewhere? */
20
21
  get _wasTransferred(): boolean;
21
- private _drainQueue;
22
- private _dispatchMessage;
22
+ private _dispatchEmit;
23
23
  /**
24
- * Web-compatible addEventListener. Wraps message data in a MessageEvent-like
25
- * object `{ data, type }` before calling the listener.
26
- * Requires explicit `port.start()` call (unlike `on('message')` which auto-starts).
24
+ * Web-compatible addEventListener. For `'message'` (and `'messageerror'`),
25
+ * routes through the inner shared MessagePort which dispatches a
26
+ * MessageEvent. For Node-only signals like `'close'`, routes to the
27
+ * EventEmitter side so `port.emit('close')` reaches the listener.
28
+ *
29
+ * Does NOT auto-start the port — matches Node + W3C HTML §9.4.4. Use
30
+ * `port.start()` explicitly, or attach via `port.on('message')` /
31
+ * `port.onmessage = fn` (both auto-start).
27
32
  */
28
- addEventListener(type: string, listener: ((event: unknown) => void) | null): void;
29
- removeEventListener(type: string, listener: ((event: unknown) => void) | null): void;
33
+ addEventListener(type: string, listener: any, options?: any): void;
34
+ removeEventListener(type: string, listener: any, options?: any): void;
35
+ /** W3C `onmessage` IDL attribute — delegated to the inner. Assigning a
36
+ * non-null handler auto-starts both surfaces (matches W3C HTML spec). */
37
+ get onmessage(): any;
38
+ set onmessage(fn: any);
39
+ get onmessageerror(): any;
40
+ set onmessageerror(fn: any);
30
41
  on(event: string | symbol, listener: (...args: unknown[]) => void): this;
31
42
  addListener(event: string | symbol, listener: (...args: unknown[]) => void): this;
32
43
  once(event: string | symbol, listener: (...args: unknown[]) => void): this;
@@ -0,0 +1,43 @@
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;
@@ -27,8 +27,41 @@ export declare class Worker extends EventEmitter {
27
27
  * to 0 at spawn; each postMessage call increments it by the number of
28
28
  * unique SharedBuffer instances in the value tree. */
29
29
  private _sabNextTag;
30
+ /** Parent-side registry of cross-process MessagePort instances keyed by
31
+ * the portId allocated when each port was transferred to the child.
32
+ * Incoming `{ __msgport, op }` JSON lines from the child stdout look
33
+ * up the kept-end port here and dispatch via the wrapper's
34
+ * `_receiveMessage()` / `_inner.close()`. Cleared when the Worker
35
+ * exits — `SubprocessPortTransport.close()` also removes individual
36
+ * entries on lifecycle close. */
37
+ private _portRegistry;
30
38
  constructor(filename: string | URL, options?: WorkerOptions);
31
- postMessage(value: unknown, _transferList?: unknown[]): void;
39
+ postMessage(value: unknown, transferList?: unknown[]): void;
40
+ /**
41
+ * Write a raw JSON line to the worker's stdin. Used by
42
+ * SubprocessPortTransport (via its sendLine callback) so a kept-end
43
+ * MessagePort's `postMessage` can travel over the same wire as
44
+ * `Worker.postMessage` without each port carrying a write-stream
45
+ * reference of its own. No-op when the worker has exited or the pipe
46
+ * is closed.
47
+ */
48
+ private _writeStdinLine;
49
+ /**
50
+ * Walk transferList for MessagePort entries and convert each to a
51
+ * cross-process port: detach the in-process partnership, assign a
52
+ * fresh portId, wire the kept-end's `_inner._transport`, register the
53
+ * kept end, and substitute every occurrence of the transferred port
54
+ * in the value tree with the placeholder. Returns the (possibly
55
+ * substituted) value tree.
56
+ *
57
+ * Validation is intentionally narrow: duplicate transfers, transfer
58
+ * of an already-closed port, and self-references currently surface as
59
+ * silently-dropped — full DataCloneError-shaped validation lives in
60
+ * the in-process `MessagePort.postMessage` (which Worker.postMessage
61
+ * does NOT call, so we accept the more permissive shape here). Bring
62
+ * up the same validation in a follow-up if a real bug shakes loose.
63
+ */
64
+ private _extractCrossProcessPorts;
32
65
  /**
33
66
  * Walk `value` for SharedBuffer instances, ship each fd over the
34
67
  * parent-side end of the FdChannel socketpair, return the placeholder-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/worker_threads",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
4
4
  "description": "Node.js worker_threads module for Gjs",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -33,16 +33,17 @@
33
33
  "worker_threads"
34
34
  ],
35
35
  "devDependencies": {
36
- "@gjsify/cli": "^0.4.13",
37
- "@gjsify/node-globals": "^0.4.13",
38
- "@gjsify/unit": "^0.4.13",
36
+ "@gjsify/cli": "^0.4.15",
37
+ "@gjsify/node-globals": "^0.4.15",
38
+ "@gjsify/unit": "^0.4.15",
39
39
  "@types/node": "^25.6.2",
40
40
  "typescript": "^6.0.3"
41
41
  },
42
42
  "dependencies": {
43
43
  "@girs/gio-2.0": "2.88.0-4.0.0-rc.15",
44
44
  "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
45
- "@gjsify/events": "^0.4.13",
46
- "@gjsify/sab-native": "^0.4.13"
45
+ "@gjsify/events": "^0.4.15",
46
+ "@gjsify/message-channel": "^0.4.15",
47
+ "@gjsify/sab-native": "^0.4.15"
47
48
  }
48
49
  }