@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.
package/lib/esm/message-port.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{isSharedBuffer as e}from"./sab-transfer.js";import{EventEmitter as t}from"node:events";function isTransferredPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0}var
|
|
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{
|
|
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.
|
|
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
|
|
194
|
-
`;try{this._stdinPipe.write_all(
|
|
195
|
-
`;this._stdinPipe.write_all(
|
|
196
|
-
`;this._stdinPipe.write_all(
|
|
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&&
|
|
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
|
|
10
|
-
|
|
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
|
|
22
|
-
private _dispatchMessage;
|
|
22
|
+
private _dispatchEmit;
|
|
23
23
|
/**
|
|
24
|
-
* Web-compatible addEventListener.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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:
|
|
29
|
-
removeEventListener(type: string, listener:
|
|
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;
|
package/lib/types/worker.d.ts
CHANGED
|
@@ -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,
|
|
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.
|
|
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.
|
|
37
|
-
"@gjsify/node-globals": "^0.4.
|
|
38
|
-
"@gjsify/unit": "^0.4.
|
|
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.
|
|
46
|
-
"@gjsify/
|
|
45
|
+
"@gjsify/events": "^0.4.15",
|
|
46
|
+
"@gjsify/message-channel": "^0.4.15",
|
|
47
|
+
"@gjsify/sab-native": "^0.4.15"
|
|
47
48
|
}
|
|
48
49
|
}
|