@gjsify/worker_threads 0.4.20 → 0.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{isSharedBuffer as e}from"./sab-transfer.js";import{EventEmitter as t}from"node:events";import{MessagePort as n}from"@gjsify/message-channel";function isTransferredPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0}var r=class MessagePort extends t{_closed=!1;_detached=!1;_otherPort=null;_inner=new n;start(){if(this._closed||this._inner._started)return;let e=this._inner._queue.slice();this._inner.start();for(let t of e)this._dispatchEmit(t)}close(){if(this._closed)return;this._closed=!0;let e=this._otherPort;this._otherPort=null,e&&(e._otherPort=null),this._inner.close(),this.emit(`close`),this.removeAllListeners()}postMessage(t,n){if(this._closed)return;let r=this._otherPort;if(!r&&this._inner._transport!==null){if(n&&n.length>0)throw createDataCloneError(`transferList is not supported on cross-process MessagePort yet`);this._inner.postMessage(t);return}if(!r)return;let i=[],a=[];if(n&&n.length>0){let t=new Set;for(let r of n){if(t.has(r))throw createDataCloneError(`Transfer list contains duplicate entries`);if(t.add(r),r instanceof MessagePort){if(r===this)throw createDataCloneError(`Cannot transfer source port`);if(r._closed||r._detached)throw createDataCloneError(`MessagePort in transfer list is already detached`);a.push(r);continue}let o=Object.prototype.toString.call(r).slice(8,-1);if(o===`ArrayBuffer`){let e=r;if(e.detached===!0)throw createDataCloneError(`ArrayBuffer in transfer list is detached`);i.push(e);continue}if(o===`SharedArrayBuffer`)throw createDataCloneError(`SharedArrayBuffer cannot appear in transfer list (it is shared, not transferred)`);if(!e(r))throw createDataCloneError(`Value at index ${n.indexOf(r)} of transfer list is not transferable`)}}let o=a.slice(),s=t;a.length>0&&(s=substitutePortsWithPlaceholders(t,a));let c;try{c=structuredClone(s,{transfer:i.length>0?i:void 0})}catch(e){this.emit(`messageerror`,e instanceof Error?e:Error(`Could not clone message`));return}let l=c;if(o.length>0){let e=o.map(e=>{let t=e._otherPort;e._otherPort=null,e._detached=!0,e._closed=!0;let n=new MessagePort;return n._otherPort=t,t&&(t._otherPort=n),n});l=replacePlaceholdersWithPorts(c,e)}r._receiveMessage(l)}ref(){return this}unref(){return this}_receiveMessage(e){this._closed||(this._inner._receive(e),this._inner._started&&this._dispatchEmit(e))}get _hasQueuedMessages(){return this._inner._queue.length>0}_dequeueMessage(){return this._inner._queue.shift()}get _wasTransferred(){return this._detached}_dispatchEmit(e){Promise.resolve().then(()=>{this._closed||this.emit(`message`,e)})}addEventListener(e,t,n){if(t){if(e===`message`||e===`messageerror`){Object.getPrototypeOf(Object.getPrototypeOf(this._inner)).addEventListener.call(this._inner,e,t,n);return}super.on(e,t)}}removeEventListener(e,t,n){if(t){if(e===`message`||e===`messageerror`){Object.getPrototypeOf(Object.getPrototypeOf(this._inner)).removeEventListener.call(this._inner,e,t,n);return}super.off(e,t)}}get onmessage(){return this._inner.onmessage}set onmessage(e){if(e!==null&&!this._inner._started){let t=this._inner._queue.slice();this._inner.onmessage=e;for(let e of t)this._dispatchEmit(e)}else this._inner.onmessage=e}get onmessageerror(){return this._inner.onmessageerror}set onmessageerror(e){this._inner.onmessageerror=e}on(e,t){return super.on(e,t),e===`message`&&this.start(),this}addListener(e,t){return this.on(e,t)}once(e,t){return super.once(e,t),e===`message`&&this.start(),this}};function createDataCloneError(e){let t=globalThis.DOMException;if(typeof t==`function`){let n=new t(e,`DataCloneError`);if(n.code===void 0)try{Object.defineProperty(n,`code`,{value:25,configurable:!0})}catch{}return n}let n=Error(e);return n.name=`DataCloneError`,n.code=25,n}function substitutePortsWithPlaceholders(e,t){let n=new Map;for(let e=0;e<t.length;e++)n.set(t[e],e);function walk(e,t){if(typeof e!=`object`||!e)return e;if(e instanceof r){let t=n.get(e);return t===void 0?e:{__gjsifyTransferredPort:!0,index:t}}if(t.has(e))return t.get(e);if(Array.isArray(e)){let n=[];t.set(e,n);for(let r=0;r<e.length;r++)r in e&&(n[r]=walk(e[r],t));return n}if(Object.prototype.toString.call(e).slice(8,-1)===`Object`){let n={};t.set(e,n);for(let r of Object.keys(e))n[r]=walk(e[r],t);return n}return e}return walk(e,new Map)}function replacePlaceholdersWithPorts(e,t){function walk(e,n){if(typeof e!=`object`||!e)return e;if(isTransferredPortPlaceholder(e))return t[e.index];if(n.has(e))return n.get(e);if(Array.isArray(e)){n.set(e,e);for(let t=0;t<e.length;t++)t in e&&(e[t]=walk(e[t],n));return e}if(Object.prototype.toString.call(e).slice(8,-1)===`Object`){n.set(e,e);for(let t of Object.keys(e))e[t]=walk(e[t],n);return e}return e}return walk(e,new Map)}export{r as MessagePort};
1
+ import"./_virtual/_rolldown/runtime.js";import{isSharedBuffer as e}from"./sab-transfer.js";import{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,307 @@
1
+ const e=`import GLib from 'gi://GLib';
2
+ import Gio from 'gi://Gio';
3
+
4
+ const loop = new GLib.MainLoop(null, false);
5
+ const stdinStream = Gio.UnixInputStream.new(0, false);
6
+ const dataIn = Gio.DataInputStream.new(stdinStream);
7
+ const stdoutStream = Gio.UnixOutputStream.new(1, false);
8
+
9
+ const _encoder = new TextEncoder();
10
+
11
+ function send(obj) {
12
+ const line = JSON.stringify(obj) + '\\n';
13
+ stdoutStream.write_all(_encoder.encode(line), null);
14
+ }
15
+
16
+ // Try to load the sab-native typelib synchronously. Absent prebuild
17
+ // is fine — only fails the receive path when a SharedBuffer actually
18
+ // crosses the wire.
19
+ let SabNative = null;
20
+ try { SabNative = imports.gi.GjsifySabNative; } catch (_) { /* prebuild missing */ }
21
+
22
+ // Read init data (first line, blocking)
23
+ const [initLine] = dataIn.read_line_utf8(null);
24
+ const init = JSON.parse(initLine);
25
+
26
+ // fd → SharedBuffer cache keyed by tag. Filled by drainSabFds() right
27
+ // before materialise() walks the parsed JSON.
28
+ const sabFds = new Map();
29
+
30
+ // Count how many SharedBuffer placeholders sit in a parsed JSON value.
31
+ // The exact count is what we pass to drainSabFds().
32
+ function countSabPlaceholders(value) {
33
+ if (value === null || typeof value !== 'object') return 0;
34
+ if (typeof value.__sab === 'number' && typeof value.size === 'number') return 1;
35
+ if (Array.isArray(value)) {
36
+ let n = 0;
37
+ for (const v of value) n += countSabPlaceholders(v);
38
+ return n;
39
+ }
40
+ if (Object.prototype.toString.call(value) === '[object Object]') {
41
+ let n = 0;
42
+ for (const v of Object.values(value)) n += countSabPlaceholders(v);
43
+ return n;
44
+ }
45
+ return 0;
46
+ }
47
+
48
+ // Synchronously drain exactly @count SCM_RIGHTS messages from fd 3,
49
+ // indexing each received fd by its sender-encoded tag. Called once per
50
+ // incoming JSON message that contains __sab placeholders. recv_fd
51
+ // blocks at most until the kernel has the message — and the sender
52
+ // always sends fds before writing to stdin, so by the time we get
53
+ // here every fd is already buffered.
54
+ function drainSabFds(count) {
55
+ if (!SabNative || count <= 0) return;
56
+ for (let i = 0; i < count; i++) {
57
+ const [fd, tag] = SabNative.FdChannel.recv_fd(3);
58
+ if (fd <= 0) throw new Error('FdChannel.recv_fd returned ' + fd + ' while draining ' + count + ' fds');
59
+ sabFds.set(tag, fd);
60
+ }
61
+ }
62
+
63
+ function materialise(value) {
64
+ if (value === null || typeof value !== 'object') return value;
65
+ if (typeof value.__sab === 'number' && typeof value.size === 'number') {
66
+ const fd = sabFds.get(value.__sab);
67
+ if (fd === undefined) throw new Error('SharedBuffer placeholder \\'' + value.__sab + '\\' arrived before its fd');
68
+ if (!SabNative) throw new Error('SharedBuffer placeholder arrived but @gjsify/sab-native typelib not loaded');
69
+ const native = SabNative.SharedBuffer.from_fd(fd, value.size);
70
+ sabFds.delete(value.__sab);
71
+ return makeSharedBuffer(native);
72
+ }
73
+ if (isPortPlaceholder(value)) {
74
+ return makeChildSidePort(value.portId);
75
+ }
76
+ if (Array.isArray(value)) {
77
+ for (let i = 0; i < value.length; i++) value[i] = materialise(value[i]);
78
+ return value;
79
+ }
80
+ if (Object.prototype.toString.call(value) === '[object Object]') {
81
+ for (const k of Object.keys(value)) value[k] = materialise(value[k]);
82
+ return value;
83
+ }
84
+ return value;
85
+ }
86
+
87
+ function makeSharedBuffer(native) {
88
+ return {
89
+ get byteLength() { return native.byte_length; },
90
+ get fd() { return native.fd; },
91
+ getUint8(off) { return native.get_u8(off); },
92
+ setUint8(off, v) { native.set_u8(off, v); },
93
+ getInt32LE(off) { return native.get_i32_le(off); },
94
+ setInt32LE(off, v) { native.set_i32_le(off, v); },
95
+ getUint32LE(off) { return native.get_u32_le(off); },
96
+ setUint32LE(off, v) { native.set_u32_le(off, v); },
97
+ getUint64LE(off) { return native.get_u64_le(off); },
98
+ setUint64LE(off, v) { native.set_u64_le(off, v); },
99
+ readBytes(off, len) {
100
+ const bytes = native.read_bytes(off, len);
101
+ const data = bytes.get_data();
102
+ return data ? new Uint8Array(data) : new Uint8Array(0);
103
+ },
104
+ writeBytes(off, data) { native.write_bytes(off, new GLib.Bytes(data)); },
105
+ get _nativeHandle() { return native; },
106
+ get constructor() { return { name: 'SharedBuffer' }; },
107
+ };
108
+ }
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
+
207
+ // Drain SharedBuffer fds attached to the init line (workerData may carry
208
+ // SharedBuffer instances) before materialise() runs against it.
209
+ if (init.sabSocketFd === 3) {
210
+ try {
211
+ drainSabFds(countSabPlaceholders(init.workerData));
212
+ } catch (err) {
213
+ send({ type: 'error', message: 'init fd drain failed: ' + (err && err.message ? err.message : err), stack: '' });
214
+ }
215
+ }
216
+
217
+ // Simplified EventEmitter for parentPort
218
+ const _listeners = new Map();
219
+ const parentPort = {
220
+ on(ev, fn) {
221
+ if (!_listeners.has(ev)) _listeners.set(ev, []);
222
+ _listeners.get(ev).push(fn);
223
+ return this;
224
+ },
225
+ once(ev, fn) {
226
+ const w = (...a) => { parentPort.off(ev, w); fn(...a); };
227
+ return parentPort.on(ev, w);
228
+ },
229
+ off(ev, fn) {
230
+ const a = _listeners.get(ev);
231
+ if (a) _listeners.set(ev, a.filter(f => f !== fn));
232
+ return this;
233
+ },
234
+ emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },
235
+ postMessage(data) { send({ type: 'message', data }); },
236
+ close() { send({ type: 'exit', code: 0 }); loop.quit(); },
237
+ removeAllListeners(ev) {
238
+ if (ev) _listeners.delete(ev); else _listeners.clear();
239
+ return this;
240
+ },
241
+ };
242
+
243
+ // Set worker context globals (read by @gjsify/worker_threads when imported by user script)
244
+ globalThis.__gjsify_worker_context = {
245
+ isMainThread: false,
246
+ parentPort,
247
+ workerData: materialise(init.workerData ?? null),
248
+ threadId: init.threadId ?? 0,
249
+ };
250
+
251
+ send({ type: 'online' });
252
+
253
+ // Async stdin reader for messages from parent
254
+ function readNext() {
255
+ dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {
256
+ try {
257
+ const [line] = source.read_line_finish_utf8(result);
258
+ if (line === null) { loop.quit(); return; }
259
+ const msg = JSON.parse(line);
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') {
275
+ // Drain fds for any SharedBuffer placeholders BEFORE materialise.
276
+ // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS
277
+ // messages before writing this stdin line, so they're already
278
+ // buffered.
279
+ if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));
280
+ parentPort.emit('message', materialise(msg.data));
281
+ }
282
+ else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); loop.quit(); return; }
283
+ readNext();
284
+ } catch (err) {
285
+ send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });
286
+ loop.quit();
287
+ }
288
+ });
289
+ }
290
+ readNext();
291
+
292
+ // Execute worker code
293
+ try {
294
+ if (init.eval) {
295
+ const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;
296
+ await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(
297
+ parentPort, globalThis.__gjsify_worker_context.workerData, init.threadId
298
+ );
299
+ } else {
300
+ await import(init.filename);
301
+ }
302
+ } catch (error) {
303
+ send({ type: 'error', message: error.message, stack: error.stack || '' });
304
+ }
305
+
306
+ loop.run();
307
+ `;export{e as BOOTSTRAP_CODE};
package/lib/esm/worker.js CHANGED
@@ -1,311 +1,5 @@
1
- import"./_virtual/_rolldown/runtime.js";import{extractSharedBuffers as e}from"./sab-transfer.js";import{MessagePort as t}from"./message-port.js";import{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
- import Gio from 'gi://Gio';
3
-
4
- const loop = new GLib.MainLoop(null, false);
5
- const stdinStream = Gio.UnixInputStream.new(0, false);
6
- const dataIn = Gio.DataInputStream.new(stdinStream);
7
- const stdoutStream = Gio.UnixOutputStream.new(1, false);
8
-
9
- const _encoder = new TextEncoder();
10
-
11
- function send(obj) {
12
- const line = JSON.stringify(obj) + '\\n';
13
- stdoutStream.write_all(_encoder.encode(line), null);
14
- }
15
-
16
- // Try to load the sab-native typelib synchronously. Absent prebuild
17
- // is fine — only fails the receive path when a SharedBuffer actually
18
- // crosses the wire.
19
- let SabNative = null;
20
- try { SabNative = imports.gi.GjsifySabNative; } catch (_) { /* prebuild missing */ }
21
-
22
- // Read init data (first line, blocking)
23
- const [initLine] = dataIn.read_line_utf8(null);
24
- const init = JSON.parse(initLine);
25
-
26
- // fd → SharedBuffer cache keyed by tag. Filled by drainSabFds() right
27
- // before materialise() walks the parsed JSON.
28
- const sabFds = new Map();
29
-
30
- // Count how many SharedBuffer placeholders sit in a parsed JSON value.
31
- // The exact count is what we pass to drainSabFds().
32
- function countSabPlaceholders(value) {
33
- if (value === null || typeof value !== 'object') return 0;
34
- if (typeof value.__sab === 'number' && typeof value.size === 'number') return 1;
35
- if (Array.isArray(value)) {
36
- let n = 0;
37
- for (const v of value) n += countSabPlaceholders(v);
38
- return n;
39
- }
40
- if (Object.prototype.toString.call(value) === '[object Object]') {
41
- let n = 0;
42
- for (const v of Object.values(value)) n += countSabPlaceholders(v);
43
- return n;
44
- }
45
- return 0;
46
- }
47
-
48
- // Synchronously drain exactly @count SCM_RIGHTS messages from fd 3,
49
- // indexing each received fd by its sender-encoded tag. Called once per
50
- // incoming JSON message that contains __sab placeholders. recv_fd
51
- // blocks at most until the kernel has the message — and the sender
52
- // always sends fds before writing to stdin, so by the time we get
53
- // here every fd is already buffered.
54
- function drainSabFds(count) {
55
- if (!SabNative || count <= 0) return;
56
- for (let i = 0; i < count; i++) {
57
- const [fd, tag] = SabNative.FdChannel.recv_fd(3);
58
- if (fd <= 0) throw new Error('FdChannel.recv_fd returned ' + fd + ' while draining ' + count + ' fds');
59
- sabFds.set(tag, fd);
60
- }
61
- }
62
-
63
- function materialise(value) {
64
- if (value === null || typeof value !== 'object') return value;
65
- if (typeof value.__sab === 'number' && typeof value.size === 'number') {
66
- const fd = sabFds.get(value.__sab);
67
- if (fd === undefined) throw new Error('SharedBuffer placeholder \\'' + value.__sab + '\\' arrived before its fd');
68
- if (!SabNative) throw new Error('SharedBuffer placeholder arrived but @gjsify/sab-native typelib not loaded');
69
- const native = SabNative.SharedBuffer.from_fd(fd, value.size);
70
- sabFds.delete(value.__sab);
71
- return makeSharedBuffer(native);
72
- }
73
- if (isPortPlaceholder(value)) {
74
- return makeChildSidePort(value.portId);
75
- }
76
- if (Array.isArray(value)) {
77
- for (let i = 0; i < value.length; i++) value[i] = materialise(value[i]);
78
- return value;
79
- }
80
- if (Object.prototype.toString.call(value) === '[object Object]') {
81
- for (const k of Object.keys(value)) value[k] = materialise(value[k]);
82
- return value;
83
- }
84
- return value;
85
- }
86
-
87
- function makeSharedBuffer(native) {
88
- return {
89
- get byteLength() { return native.byte_length; },
90
- get fd() { return native.fd; },
91
- getUint8(off) { return native.get_u8(off); },
92
- setUint8(off, v) { native.set_u8(off, v); },
93
- getInt32LE(off) { return native.get_i32_le(off); },
94
- setInt32LE(off, v) { native.set_i32_le(off, v); },
95
- getUint32LE(off) { return native.get_u32_le(off); },
96
- setUint32LE(off, v) { native.set_u32_le(off, v); },
97
- getUint64LE(off) { return native.get_u64_le(off); },
98
- setUint64LE(off, v) { native.set_u64_le(off, v); },
99
- readBytes(off, len) {
100
- const bytes = native.read_bytes(off, len);
101
- const data = bytes.get_data();
102
- return data ? new Uint8Array(data) : new Uint8Array(0);
103
- },
104
- writeBytes(off, data) { native.write_bytes(off, new GLib.Bytes(data)); },
105
- get _nativeHandle() { return native; },
106
- get constructor() { return { name: 'SharedBuffer' }; },
107
- };
108
- }
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
-
207
- // Drain SharedBuffer fds attached to the init line (workerData may carry
208
- // SharedBuffer instances) before materialise() runs against it.
209
- if (init.sabSocketFd === 3) {
210
- try {
211
- drainSabFds(countSabPlaceholders(init.workerData));
212
- } catch (err) {
213
- send({ type: 'error', message: 'init fd drain failed: ' + (err && err.message ? err.message : err), stack: '' });
214
- }
215
- }
216
-
217
- // Simplified EventEmitter for parentPort
218
- const _listeners = new Map();
219
- const parentPort = {
220
- on(ev, fn) {
221
- if (!_listeners.has(ev)) _listeners.set(ev, []);
222
- _listeners.get(ev).push(fn);
223
- return this;
224
- },
225
- once(ev, fn) {
226
- const w = (...a) => { parentPort.off(ev, w); fn(...a); };
227
- return parentPort.on(ev, w);
228
- },
229
- off(ev, fn) {
230
- const a = _listeners.get(ev);
231
- if (a) _listeners.set(ev, a.filter(f => f !== fn));
232
- return this;
233
- },
234
- emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },
235
- postMessage(data) { send({ type: 'message', data }); },
236
- close() { send({ type: 'exit', code: 0 }); loop.quit(); },
237
- removeAllListeners(ev) {
238
- if (ev) _listeners.delete(ev); else _listeners.clear();
239
- return this;
240
- },
241
- };
242
-
243
- // Set worker context globals (read by @gjsify/worker_threads when imported by user script)
244
- globalThis.__gjsify_worker_context = {
245
- isMainThread: false,
246
- parentPort,
247
- workerData: materialise(init.workerData ?? null),
248
- threadId: init.threadId ?? 0,
249
- };
250
-
251
- send({ type: 'online' });
252
-
253
- // Async stdin reader for messages from parent
254
- function readNext() {
255
- dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {
256
- try {
257
- const [line] = source.read_line_finish_utf8(result);
258
- if (line === null) { loop.quit(); return; }
259
- const msg = JSON.parse(line);
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') {
275
- // Drain fds for any SharedBuffer placeholders BEFORE materialise.
276
- // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS
277
- // messages before writing this stdin line, so they're already
278
- // buffered.
279
- if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));
280
- parentPort.emit('message', materialise(msg.data));
281
- }
282
- else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); loop.quit(); return; }
283
- readNext();
284
- } catch (err) {
285
- send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });
286
- loop.quit();
287
- }
288
- });
289
- }
290
- readNext();
291
-
292
- // Execute worker code
293
- try {
294
- if (init.eval) {
295
- const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;
296
- await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(
297
- parentPort, globalThis.__gjsify_worker_context.workerData, init.threadId
298
- );
299
- } else {
300
- await import(init.filename);
301
- }
302
- } catch (error) {
303
- send({ type: 'error', message: error.message, stack: error.stack || '' });
304
- }
305
-
306
- loop.run();
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
+ 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{BOOTSTRAP_CODE as i}from"./worker-bootstrap.js";import{EventEmitter as a}from"node:events";import o from"@girs/gio-2.0";import s from"@girs/glib-2.0";import{fdChannel as c}from"@gjsify/sab-native";let l=1;const u=new TextEncoder,d=u.encode(i);var f=class Worker extends a{threadId;resourceLimits;_subprocess=null;_stdinPipe=null;_exited=!1;_bootstrapFile=null;_sabSocketFd=-1;_sabNextTag=0;_portRegistry=new Map;constructor(e,t){super(),this.threadId=l++,this.resourceLimits=t?.resourceLimits||{};let n=t?.eval===!0,r=Worker._resolveFilename(e,n),i=`${s.get_tmp_dir()}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;this._bootstrapFile=o.File.new_for_path(i);try{this._bootstrapFile.replace_contents(d,null,!1,o.FileCreateFlags.REPLACE_DESTINATION,null)}catch(e){throw Error(`Failed to create worker bootstrap: ${e instanceof Error?e.message:e}`)}let a=new o.SubprocessLauncher({flags:o.SubprocessFlags.STDIN_PIPE|o.SubprocessFlags.STDOUT_PIPE|o.SubprocessFlags.STDERR_PIPE});if(t?.env&&typeof t.env==`object`)for(let[e,n]of Object.entries(t.env))a.setenv(e,String(n),!0);let f=-1;if(c){let e=c.makePair();e&&(this._sabSocketFd=e.parentFd,f=e.childFd,a.take_fd(f,3))}try{this._subprocess=a.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(!o.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})+`
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
+ `;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
+ `;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}if(this._sabSocketFd!==-1&&c?.closeFd){try{c.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{f as Worker};
@@ -0,0 +1 @@
1
+ export declare const BOOTSTRAP_CODE = "import GLib from 'gi://GLib';\nimport Gio from 'gi://Gio';\n\nconst loop = new GLib.MainLoop(null, false);\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 }); loop.quit(); },\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) { loop.quit(); return; }\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 }); loop.quit(); return; }\n readNext();\n } catch (err) {\n send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });\n loop.quit();\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\nloop.run();\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/worker_threads",
3
- "version": "0.4.20",
3
+ "version": "0.4.22",
4
4
  "description": "Node.js worker_threads module for Gjs",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -34,17 +34,17 @@
34
34
  "worker_threads"
35
35
  ],
36
36
  "devDependencies": {
37
- "@gjsify/cli": "^0.4.20",
38
- "@gjsify/node-globals": "^0.4.20",
39
- "@gjsify/unit": "^0.4.20",
40
- "@types/node": "^25.6.2",
37
+ "@gjsify/cli": "^0.4.22",
38
+ "@gjsify/node-globals": "^0.4.22",
39
+ "@gjsify/unit": "^0.4.22",
40
+ "@types/node": "^25.9.1",
41
41
  "typescript": "^6.0.3"
42
42
  },
43
43
  "dependencies": {
44
- "@girs/gio-2.0": "2.88.0-4.0.0-rc.15",
45
- "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
46
- "@gjsify/events": "^0.4.20",
47
- "@gjsify/message-channel": "^0.4.20",
48
- "@gjsify/sab-native": "^0.4.20"
44
+ "@girs/gio-2.0": "2.88.0-4.0.1",
45
+ "@girs/glib-2.0": "2.88.0-4.0.1",
46
+ "@gjsify/events": "^0.4.22",
47
+ "@gjsify/message-channel": "^0.4.22",
48
+ "@gjsify/sab-native": "^0.4.22"
49
49
  }
50
50
  }