@gjsify/worker_threads 0.64.4 → 0.64.6

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.
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";const e=[`Int8Array`,`Uint8Array`,`Uint8ClampedArray`,`Int16Array`,`Uint16Array`,`Int32Array`,`Uint32Array`,`Float32Array`,`Float64Array`,`BigInt64Array`,`BigUint64Array`];function toBase64(e){let t=new Uint8Array(e),n=``;for(let e=0;e<t.length;e++)n+=String.fromCharCode(t[e]);return btoa(n)}function fromBase64(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return n}function encode(t,n=new Map){if(t==null)return t;if(typeof t==`bigint`)return{__gjsify:`BigInt`,value:t.toString()};if(t===1/0)return{__gjsify:`Infinity`};if(t===-1/0)return{__gjsify:`NegInfinity`};if(typeof t==`number`&&Number.isNaN(t))return{__gjsify:`NaN`};if(typeof t!=`object`)return t;if(n.has(t))throw Error(`DataCloneError: circular reference`);if(t instanceof Date)return{__gjsify:`Date`,value:t.getTime()};if(t instanceof RegExp){let e=t;return{__gjsify:`RegExp`,source:e.source,flags:e.flags}}if(t instanceof Error){let e=t;return{__gjsify:`Error`,name:e.name,message:e.message,stack:e.stack}}if(t instanceof Map){n.set(t,null);let e=[];for(let[r,i]of t)e.push([encode(r,n),encode(i,n)]);return{__gjsify:`Map`,entries:e}}if(t instanceof Set){n.set(t,null);let e=[];for(let r of t)e.push(encode(r,n));return{__gjsify:`Set`,values:e}}let r=Object.prototype.toString.call(t).slice(8,-1);if(r===`ArrayBuffer`)return{__gjsify:`ArrayBuffer`,data:toBase64(t)};if(e.includes(r)){let e=t;return{__gjsify:r,data:toBase64(e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength))}}if(Array.isArray(t))return n.set(t,null),t.map(e=>encode(e,n));if(r===`Object`){n.set(t,null);let e={};for(let r of Object.keys(t))e[r]=encode(t[r],n);return e}throw Error(`DataCloneError: value could not be cloned`)}function decode(t){if(typeof t!=`object`||!t)return t;if(Array.isArray(t))return t.map(e=>decode(e));let n=t;if(typeof n.__gjsify==`string`)switch(n.__gjsify){case`BigInt`:return BigInt(n.value);case`Infinity`:return 1/0;case`NegInfinity`:return-1/0;case`NaN`:return NaN;case`Date`:return new Date(n.value);case`RegExp`:return new RegExp(n.source,n.flags);case`Error`:{let e=Error(n.message);return e.name=n.name,n.stack&&(e.stack=n.stack),e}case`Map`:{let e=new Map;for(let[t,r]of n.entries)e.set(decode(t),decode(r));return e}case`Set`:{let e=new Set;for(let t of n.values)e.add(decode(t));return e}case`ArrayBuffer`:return fromBase64(n.data).buffer;default:if(e.includes(n.__gjsify)){let e=fromBase64(n.data),t=globalThis[n.__gjsify];return new t(e.buffer)}return t}if(Object.prototype.toString.call(t).slice(8,-1)===`Object`){let e={};for(let t of Object.keys(n))e[t]=decode(n[t]);return e}return t}function createDataCloneError(e){let t=globalThis.DOMException;if(typeof t==`function`)return new t(e,`DataCloneError`);let n=Error(e);return n.name=`DataCloneError`,n.code=25,n}export{createDataCloneError,decode,encode};
@@ -1,3 +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};
1
+ import"./_virtual/_rolldown/runtime.js";import{encode as e}from"./structured-clone-wire.js";var SubprocessPortTransport=class{_sendLine;_registry;constructor(e,t){this._sendLine=e,this._registry=t}send(t,n){let r=JSON.stringify({__msgport:t,op:`send`,data:e(n)})+`
2
+ `;this._sendLine(r)}close(e){let t=JSON.stringify({__msgport:e,op:`close`})+`
3
+ `;try{this._sendLine(t)}catch{}this._registry.delete(e)}};let t=1,n=2;function nextParentPortId(){let e=t;return t+=2,e}function nextChildPortId(){let e=n;return n+=2,e}function isCrossProcessPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0&&typeof e.portId==`number`}export{SubprocessPortTransport,isCrossProcessPortPlaceholder,nextChildPortId,nextParentPortId};
@@ -90,6 +90,61 @@ function drainSabFds(count) {
90
90
  }
91
91
  }
92
92
 
93
+ // Tagged JSON codec for structured-clone types (mirrors structured-clone-wire.ts).
94
+ const __TYPED_ARRAYS = ['Int8Array','Uint8Array','Uint8ClampedArray','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array','BigInt64Array','BigUint64Array'];
95
+ function __toB64(buf){const b=new Uint8Array(buf);let s='';for(let i=0;i<b.length;i++)s+=String.fromCharCode(b[i]);return btoa(s);}
96
+ function __fromB64(s){const bin=atob(s);const out=new Uint8Array(bin.length);for(let i=0;i<bin.length;i++)out[i]=bin.charCodeAt(i);return out;}
97
+ function encodeWire(value, seen){
98
+ if(value===null||value===undefined) return value;
99
+ if(typeof value==='bigint') return {__gjsify:'BigInt', value:value.toString()};
100
+ if(value===Infinity) return {__gjsify:'Infinity'};
101
+ if(value===-Infinity) return {__gjsify:'NegInfinity'};
102
+ if(typeof value==='number' && isNaN(value)) return {__gjsify:'NaN'};
103
+ if(typeof value!=='object') return value;
104
+ if(value instanceof Date) return {__gjsify:'Date', value:value.getTime()};
105
+ if(value instanceof RegExp) return {__gjsify:'RegExp', source:value.source, flags:value.flags};
106
+ if(value instanceof Error) return {__gjsify:'Error', name:value.name, message:value.message, stack:value.stack};
107
+ if(value instanceof Map){const e=[];for(const [k,v] of value) e.push([encodeWire(k,seen),encodeWire(v,seen)]);return {__gjsify:'Map', entries:e};}
108
+ if(value instanceof Set){const v=[];for(const x of value) v.push(encodeWire(x,seen));return {__gjsify:'Set', values:v};}
109
+ const tag=Object.prototype.toString.call(value).slice(8,-1);
110
+ if(tag==='ArrayBuffer') return {__gjsify:'ArrayBuffer', data:__toB64(value)};
111
+ if(__TYPED_ARRAYS.indexOf(tag)!==-1){const ta=value;return {__gjsify:tag, data:__toB64(ta.buffer.slice(ta.byteOffset, ta.byteOffset+ta.byteLength))};}
112
+ if(Array.isArray(value)) return value.map(function(x){return encodeWire(x,seen);});
113
+ if(tag==='Object'){const out={};for(const k of Object.keys(value)) out[k]=encodeWire(value[k],seen);return out;}
114
+ throw new Error('DataCloneError: value could not be cloned');
115
+ }
116
+ function decodeWire(value){
117
+ if(value===null||value===undefined) return value;
118
+ if(typeof value!=='object') return value;
119
+ if(Array.isArray(value)) return value.map(decodeWire);
120
+ const obj=value;
121
+ if(typeof obj.__gjsify==='string'){
122
+ switch(obj.__gjsify){
123
+ case 'BigInt': return BigInt(obj.value);
124
+ case 'Infinity': return Infinity;
125
+ case 'NegInfinity': return -Infinity;
126
+ case 'NaN': return NaN;
127
+ case 'Date': return new Date(obj.value);
128
+ case 'RegExp': return new RegExp(obj.source, obj.flags);
129
+ case 'Error': {const e=new Error(obj.message); e.name=obj.name; if(obj.stack) e.stack=obj.stack; return e;}
130
+ case 'Map': {const m=new Map(); for(const [k,v] of obj.entries) m.set(decodeWire(k),decodeWire(v)); return m;}
131
+ case 'Set': {const s=new Set(); for(const v of obj.values) s.add(decodeWire(v)); return s;}
132
+ case 'ArrayBuffer': return __fromB64(obj.data).buffer;
133
+ default: {
134
+ if(__TYPED_ARRAYS.indexOf(obj.__gjsify)!==-1){
135
+ const bytes=__fromB64(obj.data);
136
+ const Ctor=globalThis[obj.__gjsify];
137
+ return new Ctor(bytes.buffer);
138
+ }
139
+ return value;
140
+ }
141
+ }
142
+ }
143
+ const tag=Object.prototype.toString.call(value).slice(8,-1);
144
+ if(tag==='Object'){const out={};for(const k of Object.keys(obj)) out[k]=decodeWire(obj[k]);return out;}
145
+ return value;
146
+ }
147
+
93
148
  function materialise(value) {
94
149
  if (value === null || typeof value !== 'object') return value;
95
150
  if (typeof value.__sab === 'number' && typeof value.size === 'number') {
@@ -175,7 +230,7 @@ function makeChildSidePort(portId) {
175
230
  * semantics. */
176
231
  _receive(data) {
177
232
  if (closed) return;
178
- Promise.resolve().then(() => { if (!closed) emit('message', data); });
233
+ Promise.resolve().then(() => { if (!closed) emit('message', decodeWire(data)); });
179
234
  },
180
235
  /** @internal Called by the dispatcher when the parent sends
181
236
  * {__msgport: portId, op: 'close'}. Fires the local 'close' event
@@ -208,7 +263,7 @@ function makeChildSidePort(portId) {
208
263
  },
209
264
  postMessage(data) {
210
265
  if (closed) return;
211
- send({ __msgport: portId, op: 'send', data });
266
+ send({ __msgport: portId, op: 'send', data: encodeWire(data) });
212
267
  },
213
268
  close() {
214
269
  if (closed) return;
@@ -246,6 +301,7 @@ if (init.sabSocketFd === 3) {
246
301
 
247
302
  // Simplified EventEmitter for parentPort
248
303
  const _listeners = new Map();
304
+ function _listenersChanged() { maybeExit(); }
249
305
  const parentPort = {
250
306
  on(ev, fn) {
251
307
  if (!_listeners.has(ev)) _listeners.set(ev, []);
@@ -259,28 +315,172 @@ const parentPort = {
259
315
  off(ev, fn) {
260
316
  const a = _listeners.get(ev);
261
317
  if (a) _listeners.set(ev, a.filter(f => f !== fn));
318
+ _listenersChanged();
262
319
  return this;
263
320
  },
264
321
  emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },
265
- postMessage(data) { send({ type: 'message', data }); },
322
+ postMessage(data) { send({ type: 'message', data: encodeWire(data) }); },
266
323
  close() { send({ type: 'exit', code: 0 }); requestShutdown(); },
267
324
  removeAllListeners(ev) {
268
325
  if (ev) _listeners.delete(ev); else _listeners.clear();
326
+ _listenersChanged();
269
327
  return this;
270
328
  },
271
329
  };
272
330
 
331
+ // Worker-crash contract and the fallback timers behind it.
332
+ //
333
+ // GJS exposes no global error surface a bootstrap could catch into: measured
334
+ // on gjs 1.88.1, \`globalThis.addEventListener\` does not exist, an uncaught
335
+ // throw inside a GLib callback only prints a Gjs-CRITICAL while the loop keeps
336
+ // running, and \`System\` has no uncaughtException/unhandledRejection hook. So a
337
+ // worker that crashes outside a message handler is a silent orphan — the
338
+ // parent waits on \`error\`/\`exit\` and never gets either (issue #2336). Every
339
+ // crash path below funnels through \`crash()\`: one \`error\` line to the parent,
340
+ // then exit 1, which is Node's \`error\` then \`exit\` 1.
341
+ // Node ends a worker once its script is done and nothing keeps the event loop
342
+ // alive: no parentPort 'message' listener, no pending timer, no open
343
+ // MessagePort. The child is a subprocess that would otherwise park in
344
+ // \`loop.run()\` on its stdin forever, so its parent would never see \`exit\`.
345
+ // Only this bootstrap's own timers can be counted: a child whose timers were
346
+ // replaced by a richer provider (the globals register) or that has other
347
+ // handles we cannot see keeps the old parked behaviour.
348
+ let scriptDone = false;
349
+ const shimTimers = { nextId: 0, byId: new Map() };
350
+ let shimSetTimeout = null;
351
+ function maybeExit() {
352
+ if (!scriptDone || shuttingDown) return;
353
+ if (globalThis.setTimeout !== shimSetTimeout) return;
354
+ if (shimTimers.byId.size > 0 || childPortRegistry.size > 0) return;
355
+ for (const fns of _listeners.values()) if (fns.length > 0) return;
356
+ requestShutdown();
357
+ System.exit(0);
358
+ }
359
+
360
+ function crash(error) {
361
+ try {
362
+ send({ type: 'error', message: (error && error.message) ? error.message : String(error), stack: (error && error.stack) || '' });
363
+ } catch (_) { /* parent's stdout is gone — the exit below is what it still sees */ }
364
+ requestShutdown();
365
+ System.exit(1);
366
+ }
367
+
273
368
  // Set worker context globals (read by @gjsify/worker_threads when imported by user script)
274
369
  globalThis.__gjsify_worker_context = {
275
370
  isMainThread: false,
276
371
  parentPort,
277
- workerData: materialise(init.workerData ?? null),
372
+ workerData: materialise(decodeWire(init.workerData ?? null)),
278
373
  threadId: init.threadId ?? 0,
374
+ // The crash contract. Called by the fallback shim below for a raw .mjs
375
+ // child and by the timers provider in a bundled one (globals register).
376
+ // Only ever referenced inside a worker child — the main thread has no
377
+ // worker context, so its stderr-only reporting stays untouched.
378
+ crash,
279
379
  };
280
380
 
381
+ // Child-local timers for every spawned child.
382
+ //
383
+ // Bare gjs ships its own setTimeout from its ESM modules
384
+ // (\`resource:///org/gnome/gjs/modules/esm/_timers.js\`, measured on
385
+ // gjs 1.88.1), but a throw inside one of its
386
+ // callbacks only prints a Gjs-CRITICAL while the loop keeps running, so a
387
+ // crashing worker sits invisible. The shim OVERWRITES the built-in: its
388
+ // callbacks report through the crash contract and then the child dies the
389
+ // Node way. A richer provider (the timers register of a bundled child) still
390
+ // wins — it replaces these globals on import, and its dispatch carries the
391
+ // same contract (reportTimerThrow + context.crash).
392
+ {
393
+ const timers = shimTimers;
394
+ function clearById(id) {
395
+ const gid = timers.byId.get(id);
396
+ if (gid === undefined) return;
397
+ timers.byId.delete(id);
398
+ maybeExit();
399
+ // g_source_remove has no throw path: a stale id logs a GLib CRITICAL and
400
+ // returns false, it never raises (same contract the globals register
401
+ // states for its own _cancel()).
402
+ GLib.source_remove(gid);
403
+ }
404
+ globalThis.setTimeout = function (fn, delay) {
405
+ if (typeof fn !== 'function') throw new TypeError('Callback must be a function. Received ' + typeof fn);
406
+ const id = ++timers.nextId;
407
+ const args = Array.prototype.slice.call(arguments, 2);
408
+ const gid = GLib.timeout_add(GLib.PRIORITY_DEFAULT, Math.max(0, Number(delay) || 0), () => {
409
+ timers.byId.delete(id);
410
+ try { fn.apply(null, args); } catch (error) { crash(error); }
411
+ maybeExit();
412
+ return GLib.SOURCE_REMOVE;
413
+ });
414
+ timers.byId.set(id, gid);
415
+ return id;
416
+ };
417
+ globalThis.setInterval = function (fn, delay) {
418
+ if (typeof fn !== 'function') throw new TypeError('Callback must be a function. Received ' + typeof fn);
419
+ const id = ++timers.nextId;
420
+ const args = Array.prototype.slice.call(arguments, 2);
421
+ const gid = GLib.timeout_add(GLib.PRIORITY_DEFAULT, Math.max(0, Number(delay) || 0), () => {
422
+ try { fn.apply(null, args); } catch (error) { crash(error); return GLib.SOURCE_REMOVE; }
423
+ return GLib.SOURCE_CONTINUE;
424
+ });
425
+ timers.byId.set(id, gid);
426
+ return id;
427
+ };
428
+ shimSetTimeout = globalThis.setTimeout;
429
+ globalThis.clearTimeout = clearById;
430
+ globalThis.clearInterval = clearById;
431
+ }
432
+
281
433
  send({ type: 'online' });
282
434
 
283
- // Async stdin reader for messages from parent
435
+ // Lines arriving on stdin before the worker script settles. The dynamic
436
+ // import below needs a quiet main loop to resolve (GJS quirk), so a line
437
+ // read while it is still pending is queued instead of dispatched into
438
+ // parentPort's message handler — drained once the script is done.
439
+ const pendingLines = [];
440
+
441
+ // Dispatch one parsed-as-JSON-eligible stdin line. Returns false for
442
+ // 'terminate', which requests shutdown and must not be followed by
443
+ // another read — the process is on its way out.
444
+ function dispatchLine(line) {
445
+ const msg = JSON.parse(line);
446
+ if (typeof msg.__msgport === 'number') {
447
+ // Cross-process MessagePort traffic — route to the local child-
448
+ // side port from childPortRegistry by portId. Both 'send' and
449
+ // 'close' dispatch on a microtask inside the port's helpers, so
450
+ // user listeners fire after the current stdin-callback unwinds.
451
+ const cpPort = childPortRegistry.get(msg.__msgport);
452
+ if (cpPort) {
453
+ if (msg.op === 'send') cpPort._receive(msg.data);
454
+ else if (msg.op === 'close') cpPort._internalClose();
455
+ }
456
+ // Unknown portId or op silently dropped — could happen mid-close
457
+ // race where the parent sends a final message after the child has
458
+ // already torn down the local port.
459
+ }
460
+ else if (msg.type === 'message') {
461
+ // Drain fds for any SharedBuffer placeholders BEFORE materialise.
462
+ // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS
463
+ // messages before writing this stdin line, so they're already
464
+ // buffered.
465
+ if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));
466
+ parentPort.emit('message', materialise(decodeWire(msg.data)));
467
+ }
468
+ else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); requestShutdown(); return false; }
469
+ return true;
470
+ }
471
+
472
+ // Async stdin reader for messages from parent. Runs from the start rather
473
+ // than only once the worker script settles: EOF on stdin means the spawner
474
+ // is gone, and that can arrive while the script's own top-level await never
475
+ // settles on its own, so EOF has to be caught independently of dispatch.
476
+ //
477
+ // macOS's poll() never reports POLLHUP for a pipe whose write end closed —
478
+ // only POLLIN, backed by a zero-length read (measured on darwin; Linux sets
479
+ // both), so a HUP-only watch armed before the script settles never fires
480
+ // there and the orphan outlives its spawner. Reading the stream the normal
481
+ // way sees that same zero-length read as EOF on either OS, which is why
482
+ // this one reader replaces the separate HUP watch this bootstrap used to
483
+ // run alongside it.
284
484
  function readNext() {
285
485
  dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {
286
486
  try {
@@ -293,31 +493,12 @@ function readNext() {
293
493
  System.exit(0);
294
494
  return;
295
495
  }
296
- const msg = JSON.parse(line);
297
- if (typeof msg.__msgport === 'number') {
298
- // Cross-process MessagePort traffic — route to the local child-
299
- // side port from childPortRegistry by portId. Both 'send' and
300
- // 'close' dispatch on a microtask inside the port's helpers, so
301
- // user listeners fire after the current stdin-callback unwinds.
302
- const cpPort = childPortRegistry.get(msg.__msgport);
303
- if (cpPort) {
304
- if (msg.op === 'send') cpPort._receive(msg.data);
305
- else if (msg.op === 'close') cpPort._internalClose();
306
- }
307
- // Unknown portId or op silently dropped — could happen mid-close
308
- // race where the parent sends a final message after the child has
309
- // already torn down the local port.
310
- }
311
- else if (msg.type === 'message') {
312
- // Drain fds for any SharedBuffer placeholders BEFORE materialise.
313
- // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS
314
- // messages before writing this stdin line, so they're already
315
- // buffered.
316
- if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));
317
- parentPort.emit('message', materialise(msg.data));
496
+ if (!scriptDone) {
497
+ pendingLines.push(line);
498
+ readNext();
499
+ return;
318
500
  }
319
- else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); requestShutdown(); return; }
320
- readNext();
501
+ if (dispatchLine(line)) readNext();
321
502
  } catch (err) {
322
503
  try {
323
504
  send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });
@@ -326,23 +507,44 @@ function readNext() {
326
507
  }
327
508
  });
328
509
  }
329
- readNext();
330
-
331
- // Execute worker code
332
- try {
333
- if (init.eval) {
334
- const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;
335
- await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(
336
- parentPort, globalThis.__gjsify_worker_context.workerData, init.threadId
337
- );
338
- } else {
339
- await import(init.filename);
340
- }
341
- } catch (error) {
342
- send({ type: 'error', message: error.message, stack: error.stack || '' });
510
+ // Start the main loop so await import() has a running loop to resolve
511
+ // against, and run the worker code from an idle callback: idle_add fires
512
+ // as soon as the loop is running (even with pending I/O in the stdin
513
+ // buffer), whereas timeout_add(0) can be starved by a pending async read
514
+ // that never completes because import() hung.
515
+ if (!shuttingDown) {
516
+ readNext();
517
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, function() {
518
+ (async function() {
519
+ try {
520
+ if (init.eval) {
521
+ const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;
522
+ await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(
523
+ parentPort, globalThis.__gjsify_worker_context.workerData, init.threadId
524
+ );
525
+ } else {
526
+ await import(init.filename);
527
+ }
528
+ scriptDone = true;
529
+ // Drain whatever arrived on stdin while the import above was still
530
+ // settling — queued instead of dispatched, see pendingLines above.
531
+ for (const queued of pendingLines) {
532
+ if (!dispatchLine(queued)) break;
533
+ }
534
+ pendingLines.length = 0;
535
+ maybeExit();
536
+ } catch (error) {
537
+ // A crashed script limps no further: reporting the error and then
538
+ // keeping the loop alive strands the parent on \`exit\` the same way the
539
+ // silent orphan does. Node emits \`error\` and then \`exit\` 1 (the exit
540
+ // reaches the parent through wait_async, not through a line).
541
+ try { send({ type: 'error', message: error.message, stack: error.stack || '' }); } catch (_) {}
542
+ requestShutdown();
543
+ System.exit(1);
544
+ }
545
+ })();
546
+ return GLib.SOURCE_REMOVE;
547
+ });
548
+ loop.run();
343
549
  }
344
-
345
- // Skip the loop entirely when shutdown was requested while the script above
346
- // was still running — otherwise that lost quit() strands the child forever.
347
- if (!shuttingDown) loop.run();
348
550
  `;export{e as BOOTSTRAP_CODE};
package/lib/esm/worker.js CHANGED
@@ -1,5 +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{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}this._sabSocketFd!==-1&&c?.closeFd&&(c.closeFd(this._sabSocketFd),this._sabSocketFd=-1);for(let e of this._portRegistry.values())e._inner._closed=!0,e._inner._transport=null;this._portRegistry.clear(),this._subprocess=null}};export{f as Worker};
1
+ import"./_virtual/_rolldown/runtime.js";import{extractSharedBuffers as e}from"./sab-transfer.js";import{MessagePort as t}from"./message-port.js";import{decode as n,encode as r}from"./structured-clone-wire.js";import{SubprocessPortTransport as i,nextParentPortId as a}from"./subprocess-port-transport.js";import{BOOTSTRAP_CODE as o}from"./worker-bootstrap.js";import{EventEmitter as s}from"node:events";import c from"@girs/gio-2.0";import l from"@girs/glib-2.0";import{fdChannel as u}from"@gjsify/sab-native";import{acquireMainLoop as d,releaseMainLoop as f}from"@gjsify/utils/main-loop";let p=1;const m=new TextEncoder,h=m.encode(o);var g=class Worker extends s{threadId;resourceLimits;_subprocess=null;_stdinPipe=null;_exited=!1;_bootstrapFile=null;_sabSocketFd=-1;_holdsProcess=!1;_sabNextTag=0;_portRegistry=new Map;constructor(e,t){super(),this.threadId=p++,this.resourceLimits=t?.resourceLimits||{};let n=t?.eval===!0,i=Worker._resolveFilename(e,n),a=`${l.get_tmp_dir()}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;this._bootstrapFile=c.File.new_for_path(a);try{this._bootstrapFile.replace_contents(h,null,!1,c.FileCreateFlags.REPLACE_DESTINATION,null)}catch(e){throw Error(`Failed to create worker bootstrap: ${e instanceof Error?e.message:e}`)}let o=new c.SubprocessLauncher({flags:c.SubprocessFlags.STDIN_PIPE|c.SubprocessFlags.STDOUT_PIPE|c.SubprocessFlags.STDERR_PIPE});if(t?.env&&typeof t.env==`object`)for(let[e,n]of Object.entries(t.env))o.setenv(e,String(n),!0);let s=-1;if(u){let e=u.makePair();e&&(this._sabSocketFd=e.parentFd,s=e.childFd,o.take_fd(s,3))}let f=`gjs`;try{let e=l.file_read_link(`/proc/self/exe`);e&&(f=e)}catch{}try{this._subprocess=o.spawnv([f,`-m`,a])}catch(e){throw this._cleanup(),Error(`Failed to spawn worker: ${e instanceof Error?e.message:e}`)}this._stdinPipe=this._subprocess.get_stdin_pipe();let g=this._subprocess.get_stdout_pipe();if(!n){let e=i.startsWith(`file://`)?i.slice(7):i;if(!c.File.new_for_path(e).query_exists(null)){this._cleanup();let t=Error(`Cannot find module '${e}'`);t.code=`ERR_MODULE_NOT_FOUND`,Promise.resolve().then(()=>{this.emit(`error`,t),this._exited=!0,this.emit(`exit`,1)});return}}this._holdsProcess=!0,d();let _=this._serializeWithSabTransfer(t?.workerData??null),v=JSON.stringify({threadId:this.threadId,workerData:r(_),eval:n,filename:n?void 0:i,code:n?i:void 0,sabSocketFd:this._sabSocketFd===-1?-1:3})+`
2
+ `;try{this._stdinPipe.write_all(m.encode(v),null)}catch(e){throw this._cleanup(),Error(`Failed to send init data: ${e instanceof Error?e.message:e}`)}if(g){let e=c.DataInputStream.new(g);this._readMessages(e)}let y=this._subprocess.get_stderr_pipe();y&&this._readStderr(c.DataInputStream.new(y)),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 i=r(this._serializeWithSabTransfer(n)),a=JSON.stringify({type:`message`,data:i})+`
3
+ `;this._stdinPipe.write_all(m.encode(a),null)}catch{}}_writeStdinLine=e=>{if(!(this._exited||!this._stdinPipe))try{this._stdinPipe.write_all(m.encode(e),null)}catch{}};_extractCrossProcessPorts(e,n){let r=new Map;for(let e of n){if(!(e instanceof t))continue;let n=e,o=a(),s=n._otherPort;s&&(s._otherPort=null,n._otherPort=null,s._inner._portId=o,s._inner._partner=null,s._inner._transport=new i(this._writeStdinLine,this._portRegistry),this._portRegistry.set(o,s)),n._detached=!0,n._closed=!0,n._inner.close(),r.set(n,{__gjsifyTransferredPort:!0,portId:o})}if(r.size===0)return e;function walk(e,n){if(typeof e!=`object`||!e)return e;if(e instanceof t)return r.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(!u||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(!u.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(m.encode(e),null)}}catch{}return setTimeout(()=>{!this._exited&&this._subprocess&&this._subprocess.force_exit()},500),e}ref(){return!this._exited&&!this._holdsProcess&&(this._holdsProcess=!0,d()),this}unref(){return this._releaseProcessHold(),this}static _resolveFilename(e,t){if(t)return String(e);if(e instanceof URL)return e.href;let n=String(e);if(n.startsWith(`file://`)||n.startsWith(`http://`)||n.startsWith(`https://`))return n;if(n.startsWith(`/`))return`file://`+n;if(n.startsWith(`./`)||n.startsWith(`../`)||!n.includes(`/`)){let e=l.get_current_dir(),t=l.build_filenamev([e,n]);return`file://`+(c.File.new_for_path(t).get_path()||t)}return`file://`+n}_readMessages(e){e.read_line_async(l.PRIORITY_DEFAULT,null,(t,r)=>{try{let[t]=e.read_line_finish_utf8(r);if(t===null)return;let i=JSON.parse(t);if(typeof i.__msgport==`number`){let t=this._portRegistry.get(i.__msgport);t&&(i.op===`send`?t._receiveMessage(n(i.data)):i.op===`close`&&(this._portRegistry.delete(i.__msgport),t.close())),this._readMessages(e);return}switch(i.type){case`online`:this.emit(`online`);break;case`message`:this.emit(`message`,n(i.data));break;case`error`:{let e=Error(i.message);i.stack&&(e.stack=i.stack),this.emit(`error`,e);break}}this._readMessages(e)}catch{}})}_stderrChunks=[];_readStderr(e){e.read_line_async(l.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._releaseProcessHold(),this._cleanup(),this.emit(`exit`,e)}_releaseProcessHold(){this._holdsProcess&&(this._holdsProcess=!1,f())}_cleanup(){if(this._bootstrapFile){try{this._bootstrapFile.delete(null)}catch{}this._bootstrapFile=null}if(this._stdinPipe){try{this._stdinPipe.close(null)}catch{}this._stdinPipe=null}this._sabSocketFd!==-1&&u?.closeFd&&(u.closeFd(this._sabSocketFd),this._sabSocketFd=-1);for(let e of this._portRegistry.values())e._inner._closed=!0,e._inner._transport=null;this._portRegistry.clear(),this._subprocess=null}};export{g as Worker};
@@ -0,0 +1,3 @@
1
+ export declare function encode(value: unknown, seen?: Map<object, unknown>): unknown;
2
+ export declare function decode(value: unknown): unknown;
3
+ export declare function createDataCloneError(message: string): Error;
@@ -1 +1 @@
1
- export declare const BOOTSTRAP_CODE = "import GLib from 'gi://GLib?version=2.0';\nimport Gio from 'gi://Gio?version=2.0';\nimport System from 'system';\n\nconst loop = new GLib.MainLoop(null, false);\n\n// Shutdown bookkeeping.\n//\n// `loop.run()` is the LAST statement of this module \u2014 it only starts after\n// the worker's own script has finished. `loop.quit()` on a loop that is not\n// running yet is a silent NO-OP, so every shutdown request that arrives while\n// the worker script is still executing used to be lost: the module went on to\n// call `loop.run()` afterwards and the child ran forever.\n//\n// The parent's death is exactly that case. When the spawner dies its end of\n// our stdin pipe closes, the reader below sees EOF and asked the loop to quit\n// \u2014 too early to matter. The child then sat in `loop.run()` with no armed\n// source, or (when its script had a still-pending top-level await, which GJS\n// services by spinning the job queue) burned a full core, forever. Node's\n// workers are threads and die with the process that owns them; an orphan that\n// outlives \u2014 and out-CPUs \u2014 its parent is never right.\n//\n// So: record the request (`shuttingDown`) and honour it at both ends \u2014\n// `loop.run()` is skipped when shutdown was already requested, and the EOF\n// path exits the process outright so a worker script that never settles\n// cannot keep the orphan alive either. `terminate`/`parentPort.close()`\n// deliberately keep the softer flag-only path: their parent is alive and\n// waiting on the exit event, and `Worker.terminate()` already has a\n// `force_exit()` backstop for a wedged script.\nlet shuttingDown = false;\nfunction requestShutdown() {\n shuttingDown = true;\n loop.quit();\n}\nconst stdinStream = Gio.UnixInputStream.new(0, false);\nconst dataIn = Gio.DataInputStream.new(stdinStream);\nconst stdoutStream = Gio.UnixOutputStream.new(1, false);\n\nconst _encoder = new TextEncoder();\n\nfunction send(obj) {\n const line = JSON.stringify(obj) + '\\n';\n stdoutStream.write_all(_encoder.encode(line), null);\n}\n\n// Try to load the sab-native typelib synchronously. Absent prebuild\n// is fine \u2014 only fails the receive path when a SharedBuffer actually\n// crosses the wire.\nlet SabNative = null;\ntry { SabNative = imports.gi.GjsifySabNative; } catch (_) { /* prebuild missing */ }\n\n// Read init data (first line, blocking)\nconst [initLine] = dataIn.read_line_utf8(null);\nconst init = JSON.parse(initLine);\n\n// fd \u2192 SharedBuffer cache keyed by tag. Filled by drainSabFds() right\n// before materialise() walks the parsed JSON.\nconst sabFds = new Map();\n\n// Count how many SharedBuffer placeholders sit in a parsed JSON value.\n// The exact count is what we pass to drainSabFds().\nfunction countSabPlaceholders(value) {\n if (value === null || typeof value !== 'object') return 0;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') return 1;\n if (Array.isArray(value)) {\n let n = 0;\n for (const v of value) n += countSabPlaceholders(v);\n return n;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n let n = 0;\n for (const v of Object.values(value)) n += countSabPlaceholders(v);\n return n;\n }\n return 0;\n}\n\n// Synchronously drain exactly @count SCM_RIGHTS messages from fd 3,\n// indexing each received fd by its sender-encoded tag. Called once per\n// incoming JSON message that contains __sab placeholders. recv_fd\n// blocks at most until the kernel has the message \u2014 and the sender\n// always sends fds before writing to stdin, so by the time we get\n// here every fd is already buffered.\nfunction drainSabFds(count) {\n if (!SabNative || count <= 0) return;\n for (let i = 0; i < count; i++) {\n const [fd, tag] = SabNative.FdChannel.recv_fd(3);\n if (fd <= 0) throw new Error('FdChannel.recv_fd returned ' + fd + ' while draining ' + count + ' fds');\n sabFds.set(tag, fd);\n }\n}\n\nfunction materialise(value) {\n if (value === null || typeof value !== 'object') return value;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') {\n const fd = sabFds.get(value.__sab);\n if (fd === undefined) throw new Error('SharedBuffer placeholder \\'' + value.__sab + '\\' arrived before its fd');\n if (!SabNative) throw new Error('SharedBuffer placeholder arrived but @gjsify/sab-native typelib not loaded');\n const native = SabNative.SharedBuffer.from_fd(fd, value.size);\n sabFds.delete(value.__sab);\n return makeSharedBuffer(native);\n }\n if (isPortPlaceholder(value)) {\n return makeChildSidePort(value.portId);\n }\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) value[i] = materialise(value[i]);\n return value;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n for (const k of Object.keys(value)) value[k] = materialise(value[k]);\n return value;\n }\n return value;\n}\n\nfunction makeSharedBuffer(native) {\n return {\n get byteLength() { return native.byte_length; },\n get fd() { return native.fd; },\n getUint8(off) { return native.get_u8(off); },\n setUint8(off, v) { native.set_u8(off, v); },\n getInt32LE(off) { return native.get_i32_le(off); },\n setInt32LE(off, v) { native.set_i32_le(off, v); },\n getUint32LE(off) { return native.get_u32_le(off); },\n setUint32LE(off, v) { native.set_u32_le(off, v); },\n getUint64LE(off) { return native.get_u64_le(off); },\n setUint64LE(off, v) { native.set_u64_le(off, v); },\n readBytes(off, len) {\n const bytes = native.read_bytes(off, len);\n const data = bytes.get_data();\n return data ? new Uint8Array(data) : new Uint8Array(0);\n },\n writeBytes(off, data) { native.write_bytes(off, new GLib.Bytes(data)); },\n get _nativeHandle() { return native; },\n get constructor() { return { name: 'SharedBuffer' }; },\n };\n}\n\n// Cross-process MessagePort support (child side).\n//\n// When a parent's Worker.postMessage(value, [port2]) transfers a\n// MessagePort, the parent encodes the port as a placeholder\n// { __gjsifyTransferredPort: true, portId: <id> } and ships the rest of\n// the value as JSON. On this side, materialise() walks the parsed JSON\n// and substitutes each placeholder for a fresh \"child-side port\" \u2014 an\n// inline EventEmitter-shaped object that mirrors the worker_threads\n// MessagePort surface enough for user code (postMessage / on('message') /\n// on('close') / close()). Outbound traffic via this port travels over\n// stdout as { __msgport, op: 'send', data } JSON lines, which the parent's\n// stdout-reader routes back to the kept end via the parent-side registry.\n//\n// transferList chaining (port-in-port) is intentionally not supported\n// on the cross-process path in v1 \u2014 see status/open-todos/README.md.\nconst childPortRegistry = new Map();\n\nfunction makeChildSidePort(portId) {\n const listeners = new Map();\n let closed = false;\n\n function emit(ev /* , ...args */) {\n const args = Array.prototype.slice.call(arguments, 1);\n const fns = (listeners.get(ev) || []).slice();\n for (const fn of fns) {\n try { fn.apply(null, args); } catch (_) { /* swallow */ }\n }\n }\n\n const port = {\n _portId: portId,\n /** @internal Wire-side delivery \u2014 called by the bootstrap stdin\n * dispatcher when it receives a {__msgport: portId, op: 'send'}\n * line. Schedules dispatch on a microtask so listeners fire after\n * the current call stack unwinds \u2014 matches in-process MessagePort\n * semantics. */\n _receive(data) {\n if (closed) return;\n Promise.resolve().then(() => { if (!closed) emit('message', data); });\n },\n /** @internal Called by the dispatcher when the parent sends\n * {__msgport: portId, op: 'close'}. Fires the local 'close' event\n * and stops accepting further messages. */\n _internalClose() {\n if (closed) return;\n closed = true;\n childPortRegistry.delete(portId);\n Promise.resolve().then(() => emit('close'));\n },\n on(ev, fn) {\n if (!listeners.has(ev)) listeners.set(ev, []);\n listeners.get(ev).push(fn);\n return port;\n },\n once(ev, fn) {\n const w = function () { port.off(ev, w); fn.apply(null, arguments); };\n return port.on(ev, w);\n },\n off(ev, fn) {\n const arr = listeners.get(ev);\n if (arr) listeners.set(ev, arr.filter(function (f) { return f !== fn; }));\n return port;\n },\n addListener(ev, fn) { return port.on(ev, fn); },\n removeListener(ev, fn) { return port.off(ev, fn); },\n emit(ev /* , ...args */) {\n emit.apply(null, Array.prototype.slice.call(arguments));\n return port;\n },\n postMessage(data) {\n if (closed) return;\n send({ __msgport: portId, op: 'send', data });\n },\n close() {\n if (closed) return;\n closed = true;\n try { send({ __msgport: portId, op: 'close' }); } catch (_) {}\n childPortRegistry.delete(portId);\n Promise.resolve().then(() => emit('close'));\n },\n start() { /* no-op \u2014 child-side port dispatches on receive */ },\n ref() { return port; },\n unref() { return port; },\n get _isCrossProcess() { return true; },\n get constructor() { return { name: 'MessagePort' }; },\n };\n\n childPortRegistry.set(portId, port);\n return port;\n}\n\nfunction isPortPlaceholder(value) {\n return value !== null && typeof value === 'object'\n && value.__gjsifyTransferredPort === true\n && typeof value.portId === 'number';\n}\n\n// Drain SharedBuffer fds attached to the init line (workerData may carry\n// SharedBuffer instances) before materialise() runs against it.\nif (init.sabSocketFd === 3) {\n try {\n drainSabFds(countSabPlaceholders(init.workerData));\n } catch (err) {\n send({ type: 'error', message: 'init fd drain failed: ' + (err && err.message ? err.message : err), stack: '' });\n }\n}\n\n// Simplified EventEmitter for parentPort\nconst _listeners = new Map();\nconst parentPort = {\n on(ev, fn) {\n if (!_listeners.has(ev)) _listeners.set(ev, []);\n _listeners.get(ev).push(fn);\n return this;\n },\n once(ev, fn) {\n const w = (...a) => { parentPort.off(ev, w); fn(...a); };\n return parentPort.on(ev, w);\n },\n off(ev, fn) {\n const a = _listeners.get(ev);\n if (a) _listeners.set(ev, a.filter(f => f !== fn));\n return this;\n },\n emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },\n postMessage(data) { send({ type: 'message', data }); },\n close() { send({ type: 'exit', code: 0 }); requestShutdown(); },\n removeAllListeners(ev) {\n if (ev) _listeners.delete(ev); else _listeners.clear();\n return this;\n },\n};\n\n// Set worker context globals (read by @gjsify/worker_threads when imported by user script)\nglobalThis.__gjsify_worker_context = {\n isMainThread: false,\n parentPort,\n workerData: materialise(init.workerData ?? null),\n threadId: init.threadId ?? 0,\n};\n\nsend({ type: 'online' });\n\n// Async stdin reader for messages from parent\nfunction readNext() {\n dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {\n try {\n const [line] = source.read_line_finish_utf8(result);\n if (line === null) {\n // EOF on stdin = the spawner is gone. Nothing can reach us again and\n // nobody is left to observe an exit code, so leave immediately \u2014\n // even from inside the worker script's own pending top-level await.\n requestShutdown();\n System.exit(0);\n return;\n }\n const msg = JSON.parse(line);\n if (typeof msg.__msgport === 'number') {\n // Cross-process MessagePort traffic \u2014 route to the local child-\n // side port from childPortRegistry by portId. Both 'send' and\n // 'close' dispatch on a microtask inside the port's helpers, so\n // user listeners fire after the current stdin-callback unwinds.\n const cpPort = childPortRegistry.get(msg.__msgport);\n if (cpPort) {\n if (msg.op === 'send') cpPort._receive(msg.data);\n else if (msg.op === 'close') cpPort._internalClose();\n }\n // Unknown portId or op silently dropped \u2014 could happen mid-close\n // race where the parent sends a final message after the child has\n // already torn down the local port.\n }\n else if (msg.type === 'message') {\n // Drain fds for any SharedBuffer placeholders BEFORE materialise.\n // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS\n // messages before writing this stdin line, so they're already\n // buffered.\n if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));\n parentPort.emit('message', materialise(msg.data));\n }\n else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); requestShutdown(); return; }\n readNext();\n } catch (err) {\n try {\n send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });\n } catch (_) { /* parent's end of stdout is gone too \u2014 nothing to report to */ }\n requestShutdown();\n }\n });\n}\nreadNext();\n\n// Execute worker code\ntry {\n if (init.eval) {\n const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;\n await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(\n parentPort, globalThis.__gjsify_worker_context.workerData, init.threadId\n );\n } else {\n await import(init.filename);\n }\n} catch (error) {\n send({ type: 'error', message: error.message, stack: error.stack || '' });\n}\n\n// Skip the loop entirely when shutdown was requested while the script above\n// was still running \u2014 otherwise that lost quit() strands the child forever.\nif (!shuttingDown) loop.run();\n";
1
+ export declare const BOOTSTRAP_CODE = "import GLib from 'gi://GLib?version=2.0';\nimport Gio from 'gi://Gio?version=2.0';\nimport System from 'system';\n\nconst loop = new GLib.MainLoop(null, false);\n\n// Shutdown bookkeeping.\n//\n// `loop.run()` is the LAST statement of this module \u2014 it only starts after\n// the worker's own script has finished. `loop.quit()` on a loop that is not\n// running yet is a silent NO-OP, so every shutdown request that arrives while\n// the worker script is still executing used to be lost: the module went on to\n// call `loop.run()` afterwards and the child ran forever.\n//\n// The parent's death is exactly that case. When the spawner dies its end of\n// our stdin pipe closes, the reader below sees EOF and asked the loop to quit\n// \u2014 too early to matter. The child then sat in `loop.run()` with no armed\n// source, or (when its script had a still-pending top-level await, which GJS\n// services by spinning the job queue) burned a full core, forever. Node's\n// workers are threads and die with the process that owns them; an orphan that\n// outlives \u2014 and out-CPUs \u2014 its parent is never right.\n//\n// So: record the request (`shuttingDown`) and honour it at both ends \u2014\n// `loop.run()` is skipped when shutdown was already requested, and the EOF\n// path exits the process outright so a worker script that never settles\n// cannot keep the orphan alive either. `terminate`/`parentPort.close()`\n// deliberately keep the softer flag-only path: their parent is alive and\n// waiting on the exit event, and `Worker.terminate()` already has a\n// `force_exit()` backstop for a wedged script.\nlet shuttingDown = false;\nfunction requestShutdown() {\n shuttingDown = true;\n loop.quit();\n}\nconst stdinStream = Gio.UnixInputStream.new(0, false);\nconst dataIn = Gio.DataInputStream.new(stdinStream);\nconst stdoutStream = Gio.UnixOutputStream.new(1, false);\n\nconst _encoder = new TextEncoder();\n\nfunction send(obj) {\n const line = JSON.stringify(obj) + '\\n';\n stdoutStream.write_all(_encoder.encode(line), null);\n}\n\n// Try to load the sab-native typelib synchronously. Absent prebuild\n// is fine \u2014 only fails the receive path when a SharedBuffer actually\n// crosses the wire.\nlet SabNative = null;\ntry { SabNative = imports.gi.GjsifySabNative; } catch (_) { /* prebuild missing */ }\n\n// Read init data (first line, blocking)\nconst [initLine] = dataIn.read_line_utf8(null);\nconst init = JSON.parse(initLine);\n\n// fd \u2192 SharedBuffer cache keyed by tag. Filled by drainSabFds() right\n// before materialise() walks the parsed JSON.\nconst sabFds = new Map();\n\n// Count how many SharedBuffer placeholders sit in a parsed JSON value.\n// The exact count is what we pass to drainSabFds().\nfunction countSabPlaceholders(value) {\n if (value === null || typeof value !== 'object') return 0;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') return 1;\n if (Array.isArray(value)) {\n let n = 0;\n for (const v of value) n += countSabPlaceholders(v);\n return n;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n let n = 0;\n for (const v of Object.values(value)) n += countSabPlaceholders(v);\n return n;\n }\n return 0;\n}\n\n// Synchronously drain exactly @count SCM_RIGHTS messages from fd 3,\n// indexing each received fd by its sender-encoded tag. Called once per\n// incoming JSON message that contains __sab placeholders. recv_fd\n// blocks at most until the kernel has the message \u2014 and the sender\n// always sends fds before writing to stdin, so by the time we get\n// here every fd is already buffered.\nfunction drainSabFds(count) {\n if (!SabNative || count <= 0) return;\n for (let i = 0; i < count; i++) {\n const [fd, tag] = SabNative.FdChannel.recv_fd(3);\n if (fd <= 0) throw new Error('FdChannel.recv_fd returned ' + fd + ' while draining ' + count + ' fds');\n sabFds.set(tag, fd);\n }\n}\n\n// Tagged JSON codec for structured-clone types (mirrors structured-clone-wire.ts).\nconst __TYPED_ARRAYS = ['Int8Array','Uint8Array','Uint8ClampedArray','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array','BigInt64Array','BigUint64Array'];\nfunction __toB64(buf){const b=new Uint8Array(buf);let s='';for(let i=0;i<b.length;i++)s+=String.fromCharCode(b[i]);return btoa(s);}\nfunction __fromB64(s){const bin=atob(s);const out=new Uint8Array(bin.length);for(let i=0;i<bin.length;i++)out[i]=bin.charCodeAt(i);return out;}\nfunction encodeWire(value, seen){\n if(value===null||value===undefined) return value;\n if(typeof value==='bigint') return {__gjsify:'BigInt', value:value.toString()};\n if(value===Infinity) return {__gjsify:'Infinity'};\n if(value===-Infinity) return {__gjsify:'NegInfinity'};\n if(typeof value==='number' && isNaN(value)) return {__gjsify:'NaN'};\n if(typeof value!=='object') return value;\n if(value instanceof Date) return {__gjsify:'Date', value:value.getTime()};\n if(value instanceof RegExp) return {__gjsify:'RegExp', source:value.source, flags:value.flags};\n if(value instanceof Error) return {__gjsify:'Error', name:value.name, message:value.message, stack:value.stack};\n if(value instanceof Map){const e=[];for(const [k,v] of value) e.push([encodeWire(k,seen),encodeWire(v,seen)]);return {__gjsify:'Map', entries:e};}\n if(value instanceof Set){const v=[];for(const x of value) v.push(encodeWire(x,seen));return {__gjsify:'Set', values:v};}\n const tag=Object.prototype.toString.call(value).slice(8,-1);\n if(tag==='ArrayBuffer') return {__gjsify:'ArrayBuffer', data:__toB64(value)};\n if(__TYPED_ARRAYS.indexOf(tag)!==-1){const ta=value;return {__gjsify:tag, data:__toB64(ta.buffer.slice(ta.byteOffset, ta.byteOffset+ta.byteLength))};}\n if(Array.isArray(value)) return value.map(function(x){return encodeWire(x,seen);});\n if(tag==='Object'){const out={};for(const k of Object.keys(value)) out[k]=encodeWire(value[k],seen);return out;}\n throw new Error('DataCloneError: value could not be cloned');\n}\nfunction decodeWire(value){\n if(value===null||value===undefined) return value;\n if(typeof value!=='object') return value;\n if(Array.isArray(value)) return value.map(decodeWire);\n const obj=value;\n if(typeof obj.__gjsify==='string'){\n switch(obj.__gjsify){\n case 'BigInt': return BigInt(obj.value);\n case 'Infinity': return Infinity;\n case 'NegInfinity': return -Infinity;\n case 'NaN': return NaN;\n case 'Date': return new Date(obj.value);\n case 'RegExp': return new RegExp(obj.source, obj.flags);\n case 'Error': {const e=new Error(obj.message); e.name=obj.name; if(obj.stack) e.stack=obj.stack; return e;}\n case 'Map': {const m=new Map(); for(const [k,v] of obj.entries) m.set(decodeWire(k),decodeWire(v)); return m;}\n case 'Set': {const s=new Set(); for(const v of obj.values) s.add(decodeWire(v)); return s;}\n case 'ArrayBuffer': return __fromB64(obj.data).buffer;\n default: {\n if(__TYPED_ARRAYS.indexOf(obj.__gjsify)!==-1){\n const bytes=__fromB64(obj.data);\n const Ctor=globalThis[obj.__gjsify];\n return new Ctor(bytes.buffer);\n }\n return value;\n }\n }\n }\n const tag=Object.prototype.toString.call(value).slice(8,-1);\n if(tag==='Object'){const out={};for(const k of Object.keys(obj)) out[k]=decodeWire(obj[k]);return out;}\n return value;\n}\n\nfunction materialise(value) {\n if (value === null || typeof value !== 'object') return value;\n if (typeof value.__sab === 'number' && typeof value.size === 'number') {\n const fd = sabFds.get(value.__sab);\n if (fd === undefined) throw new Error('SharedBuffer placeholder \\'' + value.__sab + '\\' arrived before its fd');\n if (!SabNative) throw new Error('SharedBuffer placeholder arrived but @gjsify/sab-native typelib not loaded');\n const native = SabNative.SharedBuffer.from_fd(fd, value.size);\n sabFds.delete(value.__sab);\n return makeSharedBuffer(native);\n }\n if (isPortPlaceholder(value)) {\n return makeChildSidePort(value.portId);\n }\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) value[i] = materialise(value[i]);\n return value;\n }\n if (Object.prototype.toString.call(value) === '[object Object]') {\n for (const k of Object.keys(value)) value[k] = materialise(value[k]);\n return value;\n }\n return value;\n}\n\nfunction makeSharedBuffer(native) {\n return {\n get byteLength() { return native.byte_length; },\n get fd() { return native.fd; },\n getUint8(off) { return native.get_u8(off); },\n setUint8(off, v) { native.set_u8(off, v); },\n getInt32LE(off) { return native.get_i32_le(off); },\n setInt32LE(off, v) { native.set_i32_le(off, v); },\n getUint32LE(off) { return native.get_u32_le(off); },\n setUint32LE(off, v) { native.set_u32_le(off, v); },\n getUint64LE(off) { return native.get_u64_le(off); },\n setUint64LE(off, v) { native.set_u64_le(off, v); },\n readBytes(off, len) {\n const bytes = native.read_bytes(off, len);\n const data = bytes.get_data();\n return data ? new Uint8Array(data) : new Uint8Array(0);\n },\n writeBytes(off, data) { native.write_bytes(off, new GLib.Bytes(data)); },\n get _nativeHandle() { return native; },\n get constructor() { return { name: 'SharedBuffer' }; },\n };\n}\n\n// Cross-process MessagePort support (child side).\n//\n// When a parent's Worker.postMessage(value, [port2]) transfers a\n// MessagePort, the parent encodes the port as a placeholder\n// { __gjsifyTransferredPort: true, portId: <id> } and ships the rest of\n// the value as JSON. On this side, materialise() walks the parsed JSON\n// and substitutes each placeholder for a fresh \"child-side port\" \u2014 an\n// inline EventEmitter-shaped object that mirrors the worker_threads\n// MessagePort surface enough for user code (postMessage / on('message') /\n// on('close') / close()). Outbound traffic via this port travels over\n// stdout as { __msgport, op: 'send', data } JSON lines, which the parent's\n// stdout-reader routes back to the kept end via the parent-side registry.\n//\n// transferList chaining (port-in-port) is intentionally not supported\n// on the cross-process path in v1 \u2014 see status/open-todos/README.md.\nconst childPortRegistry = new Map();\n\nfunction makeChildSidePort(portId) {\n const listeners = new Map();\n let closed = false;\n\n function emit(ev /* , ...args */) {\n const args = Array.prototype.slice.call(arguments, 1);\n const fns = (listeners.get(ev) || []).slice();\n for (const fn of fns) {\n try { fn.apply(null, args); } catch (_) { /* swallow */ }\n }\n }\n\n const port = {\n _portId: portId,\n /** @internal Wire-side delivery \u2014 called by the bootstrap stdin\n * dispatcher when it receives a {__msgport: portId, op: 'send'}\n * line. Schedules dispatch on a microtask so listeners fire after\n * the current call stack unwinds \u2014 matches in-process MessagePort\n * semantics. */\n _receive(data) {\n if (closed) return;\n Promise.resolve().then(() => { if (!closed) emit('message', decodeWire(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: encodeWire(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();\nfunction _listenersChanged() { maybeExit(); }\nconst parentPort = {\n on(ev, fn) {\n if (!_listeners.has(ev)) _listeners.set(ev, []);\n _listeners.get(ev).push(fn);\n return this;\n },\n once(ev, fn) {\n const w = (...a) => { parentPort.off(ev, w); fn(...a); };\n return parentPort.on(ev, w);\n },\n off(ev, fn) {\n const a = _listeners.get(ev);\n if (a) _listeners.set(ev, a.filter(f => f !== fn));\n _listenersChanged();\n return this;\n },\n emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },\n postMessage(data) { send({ type: 'message', data: encodeWire(data) }); },\n close() { send({ type: 'exit', code: 0 }); requestShutdown(); },\n removeAllListeners(ev) {\n if (ev) _listeners.delete(ev); else _listeners.clear();\n _listenersChanged();\n return this;\n },\n};\n\n// Worker-crash contract and the fallback timers behind it.\n//\n// GJS exposes no global error surface a bootstrap could catch into: measured\n// on gjs 1.88.1, `globalThis.addEventListener` does not exist, an uncaught\n// throw inside a GLib callback only prints a Gjs-CRITICAL while the loop keeps\n// running, and `System` has no uncaughtException/unhandledRejection hook. So a\n// worker that crashes outside a message handler is a silent orphan \u2014 the\n// parent waits on `error`/`exit` and never gets either (issue #2336). Every\n// crash path below funnels through `crash()`: one `error` line to the parent,\n// then exit 1, which is Node's `error` then `exit` 1.\n// Node ends a worker once its script is done and nothing keeps the event loop\n// alive: no parentPort 'message' listener, no pending timer, no open\n// MessagePort. The child is a subprocess that would otherwise park in\n// `loop.run()` on its stdin forever, so its parent would never see `exit`.\n// Only this bootstrap's own timers can be counted: a child whose timers were\n// replaced by a richer provider (the globals register) or that has other\n// handles we cannot see keeps the old parked behaviour.\nlet scriptDone = false;\nconst shimTimers = { nextId: 0, byId: new Map() };\nlet shimSetTimeout = null;\nfunction maybeExit() {\n if (!scriptDone || shuttingDown) return;\n if (globalThis.setTimeout !== shimSetTimeout) return;\n if (shimTimers.byId.size > 0 || childPortRegistry.size > 0) return;\n for (const fns of _listeners.values()) if (fns.length > 0) return;\n requestShutdown();\n System.exit(0);\n}\n\nfunction crash(error) {\n try {\n send({ type: 'error', message: (error && error.message) ? error.message : String(error), stack: (error && error.stack) || '' });\n } catch (_) { /* parent's stdout is gone \u2014 the exit below is what it still sees */ }\n requestShutdown();\n System.exit(1);\n}\n\n// Set worker context globals (read by @gjsify/worker_threads when imported by user script)\nglobalThis.__gjsify_worker_context = {\n isMainThread: false,\n parentPort,\n workerData: materialise(decodeWire(init.workerData ?? null)),\n threadId: init.threadId ?? 0,\n // The crash contract. Called by the fallback shim below for a raw .mjs\n // child and by the timers provider in a bundled one (globals register).\n // Only ever referenced inside a worker child \u2014 the main thread has no\n // worker context, so its stderr-only reporting stays untouched.\n crash,\n};\n\n// Child-local timers for every spawned child.\n//\n// Bare gjs ships its own setTimeout from its ESM modules\n// (`resource:///org/gnome/gjs/modules/esm/_timers.js`, measured on\n// gjs 1.88.1), but a throw inside one of its\n// callbacks only prints a Gjs-CRITICAL while the loop keeps running, so a\n// crashing worker sits invisible. The shim OVERWRITES the built-in: its\n// callbacks report through the crash contract and then the child dies the\n// Node way. A richer provider (the timers register of a bundled child) still\n// wins \u2014 it replaces these globals on import, and its dispatch carries the\n// same contract (reportTimerThrow + context.crash).\n{\n const timers = shimTimers;\n function clearById(id) {\n const gid = timers.byId.get(id);\n if (gid === undefined) return;\n timers.byId.delete(id);\n maybeExit();\n // g_source_remove has no throw path: a stale id logs a GLib CRITICAL and\n // returns false, it never raises (same contract the globals register\n // states for its own _cancel()).\n GLib.source_remove(gid);\n }\n globalThis.setTimeout = function (fn, delay) {\n if (typeof fn !== 'function') throw new TypeError('Callback must be a function. Received ' + typeof fn);\n const id = ++timers.nextId;\n const args = Array.prototype.slice.call(arguments, 2);\n const gid = GLib.timeout_add(GLib.PRIORITY_DEFAULT, Math.max(0, Number(delay) || 0), () => {\n timers.byId.delete(id);\n try { fn.apply(null, args); } catch (error) { crash(error); }\n maybeExit();\n return GLib.SOURCE_REMOVE;\n });\n timers.byId.set(id, gid);\n return id;\n };\n globalThis.setInterval = function (fn, delay) {\n if (typeof fn !== 'function') throw new TypeError('Callback must be a function. Received ' + typeof fn);\n const id = ++timers.nextId;\n const args = Array.prototype.slice.call(arguments, 2);\n const gid = GLib.timeout_add(GLib.PRIORITY_DEFAULT, Math.max(0, Number(delay) || 0), () => {\n try { fn.apply(null, args); } catch (error) { crash(error); return GLib.SOURCE_REMOVE; }\n return GLib.SOURCE_CONTINUE;\n });\n timers.byId.set(id, gid);\n return id;\n };\n shimSetTimeout = globalThis.setTimeout;\n globalThis.clearTimeout = clearById;\n globalThis.clearInterval = clearById;\n}\n\nsend({ type: 'online' });\n\n// Lines arriving on stdin before the worker script settles. The dynamic\n// import below needs a quiet main loop to resolve (GJS quirk), so a line\n// read while it is still pending is queued instead of dispatched into\n// parentPort's message handler \u2014 drained once the script is done.\nconst pendingLines = [];\n\n// Dispatch one parsed-as-JSON-eligible stdin line. Returns false for\n// 'terminate', which requests shutdown and must not be followed by\n// another read \u2014 the process is on its way out.\nfunction dispatchLine(line) {\n const msg = JSON.parse(line);\n if (typeof msg.__msgport === 'number') {\n // Cross-process MessagePort traffic \u2014 route to the local child-\n // side port from childPortRegistry by portId. Both 'send' and\n // 'close' dispatch on a microtask inside the port's helpers, so\n // user listeners fire after the current stdin-callback unwinds.\n const cpPort = childPortRegistry.get(msg.__msgport);\n if (cpPort) {\n if (msg.op === 'send') cpPort._receive(msg.data);\n else if (msg.op === 'close') cpPort._internalClose();\n }\n // Unknown portId or op silently dropped \u2014 could happen mid-close\n // race where the parent sends a final message after the child has\n // already torn down the local port.\n }\n else if (msg.type === 'message') {\n // Drain fds for any SharedBuffer placeholders BEFORE materialise.\n // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS\n // messages before writing this stdin line, so they're already\n // buffered.\n if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));\n parentPort.emit('message', materialise(decodeWire(msg.data)));\n }\n else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); requestShutdown(); return false; }\n return true;\n}\n\n// Async stdin reader for messages from parent. Runs from the start rather\n// than only once the worker script settles: EOF on stdin means the spawner\n// is gone, and that can arrive while the script's own top-level await never\n// settles on its own, so EOF has to be caught independently of dispatch.\n//\n// macOS's poll() never reports POLLHUP for a pipe whose write end closed \u2014\n// only POLLIN, backed by a zero-length read (measured on darwin; Linux sets\n// both), so a HUP-only watch armed before the script settles never fires\n// there and the orphan outlives its spawner. Reading the stream the normal\n// way sees that same zero-length read as EOF on either OS, which is why\n// this one reader replaces the separate HUP watch this bootstrap used to\n// run alongside it.\nfunction readNext() {\n dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {\n try {\n const [line] = source.read_line_finish_utf8(result);\n if (line === null) {\n // EOF on stdin = the spawner is gone. Nothing can reach us again and\n // nobody is left to observe an exit code, so leave immediately \u2014\n // even from inside the worker script's own pending top-level await.\n requestShutdown();\n System.exit(0);\n return;\n }\n if (!scriptDone) {\n pendingLines.push(line);\n readNext();\n return;\n }\n if (dispatchLine(line)) readNext();\n } catch (err) {\n try {\n send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });\n } catch (_) { /* parent's end of stdout is gone too \u2014 nothing to report to */ }\n requestShutdown();\n }\n });\n}\n// Start the main loop so await import() has a running loop to resolve\n// against, and run the worker code from an idle callback: idle_add fires\n// as soon as the loop is running (even with pending I/O in the stdin\n// buffer), whereas timeout_add(0) can be starved by a pending async read\n// that never completes because import() hung.\nif (!shuttingDown) {\n readNext();\n GLib.idle_add(GLib.PRIORITY_DEFAULT, function() {\n (async function() {\n try {\n if (init.eval) {\n const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;\n await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(\n parentPort, globalThis.__gjsify_worker_context.workerData, init.threadId\n );\n } else {\n await import(init.filename);\n }\n scriptDone = true;\n // Drain whatever arrived on stdin while the import above was still\n // settling \u2014 queued instead of dispatched, see pendingLines above.\n for (const queued of pendingLines) {\n if (!dispatchLine(queued)) break;\n }\n pendingLines.length = 0;\n maybeExit();\n } catch (error) {\n // A crashed script limps no further: reporting the error and then\n // keeping the loop alive strands the parent on `exit` the same way the\n // silent orphan does. Node emits `error` and then `exit` 1 (the exit\n // reaches the parent through wait_async, not through a line).\n try { send({ type: 'error', message: error.message, stack: error.stack || '' }); } catch (_) {}\n requestShutdown();\n System.exit(1);\n }\n })();\n return GLib.SOURCE_REMOVE;\n });\n loop.run();\n}\n";
@@ -23,6 +23,8 @@ export declare class Worker extends EventEmitter {
23
23
  * -1 when sab-native's prebuild isn't loaded → SharedBuffer transfer is
24
24
  * unavailable but everything else works. Closed in `_cleanup()`. */
25
25
  private _sabSocketFd;
26
+ /** A spawned, referenced worker holds the process like a Node active handle. */
27
+ private _holdsProcess;
26
28
  /** Per-Worker sequence counter for SharedBuffer transfer tags. Resets
27
29
  * to 0 at spawn; each postMessage call increments it by the number of
28
30
  * unique SharedBuffer instances in the value tree. */
@@ -87,5 +89,7 @@ export declare class Worker extends EventEmitter {
87
89
  private _stderrChunks;
88
90
  private _readStderr;
89
91
  private _onExit;
92
+ /** Drop the process hold this worker kept (spawn/unref parity). */
93
+ private _releaseProcessHold;
90
94
  private _cleanup;
91
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/worker_threads",
3
- "version": "0.64.4",
3
+ "version": "0.64.6",
4
4
  "description": "Node.js worker_threads module for Gjs",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -39,18 +39,19 @@
39
39
  "worker_threads"
40
40
  ],
41
41
  "devDependencies": {
42
- "@gjsify/cli": "^0.64.4",
43
- "@gjsify/node-globals": "^0.64.4",
44
- "@gjsify/unit": "^0.64.4",
42
+ "@gjsify/cli": "^0.64.6",
43
+ "@gjsify/node-globals": "^0.64.6",
44
+ "@gjsify/unit": "^0.64.6",
45
45
  "@types/node": "^25.9.2",
46
46
  "typescript": "^6.0.3"
47
47
  },
48
48
  "dependencies": {
49
49
  "@girs/gio-2.0": "5.4.0",
50
50
  "@girs/glib-2.0": "5.4.0",
51
- "@gjsify/events": "^0.64.4",
52
- "@gjsify/message-channel": "^0.64.4",
53
- "@gjsify/sab-native": "^0.64.4"
51
+ "@gjsify/events": "^0.64.6",
52
+ "@gjsify/message-channel": "^0.64.6",
53
+ "@gjsify/sab-native": "^0.64.6",
54
+ "@gjsify/utils": "^0.64.6"
54
55
  },
55
56
  "gjsify": {
56
57
  "runtimes": {