@gjsify/worker_threads 0.4.10 → 0.4.12

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{EventEmitter as e}from"node:events";function isTransferredPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0}var t=class MessagePort extends e{_started=!1;_closed=!1;_detached=!1;_messageQueue=[];_otherPort=null;_aeWrappers=new Map;start(){this._started||this._closed||(this._started=!0,this._drainQueue())}close(){if(this._closed)return;this._closed=!0;let e=this._otherPort;this._otherPort=null,e&&(e._otherPort=null),this.emit(`close`),this.removeAllListeners()}postMessage(e,t){if(this._closed)return;let n=this._otherPort;if(!n)return;let r=[],i=[];if(t&&t.length>0){let e=new Set;for(let n of t){if(e.has(n))throw createDataCloneError(`Transfer list contains duplicate entries`);if(e.add(n),n instanceof MessagePort){if(n===this)throw createDataCloneError(`Cannot transfer source port`);if(n._closed||n._detached)throw createDataCloneError(`MessagePort in transfer list is already detached`);i.push(n);continue}let a=Object.prototype.toString.call(n).slice(8,-1);if(a===`ArrayBuffer`){let e=n;if(e.detached===!0)throw createDataCloneError(`ArrayBuffer in transfer list is detached`);r.push(e);continue}throw createDataCloneError(a===`SharedArrayBuffer`?`SharedArrayBuffer cannot appear in transfer list (it is shared, not transferred)`:`Value at index ${t.indexOf(n)} of transfer list is not transferable`)}}let a=i.slice(),o=e;i.length>0&&(o=substitutePortsWithPlaceholders(e,i));let s;try{s=structuredClone(o,{transfer:r.length>0?r:void 0})}catch(e){this.emit(`messageerror`,e instanceof Error?e:Error(`Could not clone message`));return}let c=s;if(a.length>0){let e=a.map(e=>{let t=e._otherPort,n=e._messageQueue.slice();e._messageQueue.length=0,e._otherPort=null,e._detached=!0,e._closed=!0;let r=new MessagePort;r._otherPort=t,t&&(t._otherPort=r);for(let e of n)r._messageQueue.push(e);return r});c=replacePlaceholdersWithPorts(s,e)}n._receiveMessage(c)}ref(){return this}unref(){return this}_receiveMessage(e){if(!this._closed){if(!this._started){this._messageQueue.push(e);return}this._dispatchMessage(e)}}get _hasQueuedMessages(){return this._messageQueue.length>0}_dequeueMessage(){return this._messageQueue.shift()}get _wasTransferred(){return this._detached}_drainQueue(){for(;this._messageQueue.length>0;)this._dispatchMessage(this._messageQueue.shift())}_dispatchMessage(e){Promise.resolve().then(()=>{this._closed||this.emit(`message`,e)})}addEventListener(e,t){if(t)if(e===`message`){let wrapper=e=>{t({data:e,type:`message`})};this._aeWrappers.set(t,wrapper),super.on(`message`,wrapper)}else super.on(e,t)}removeEventListener(e,t){if(t)if(e===`message`){let e=this._aeWrappers.get(t);e&&(super.off(`message`,e),this._aeWrappers.delete(t))}else super.off(e,t)}on(e,t){return super.on(e,t),e===`message`&&!this._started&&this.start(),this}addListener(e,t){return this.on(e,t)}once(e,t){return super.once(e,t),e===`message`&&!this._started&&this.start(),this}};function createDataCloneError(e){let t=globalThis.DOMException;if(typeof t==`function`){let n=new t(e,`DataCloneError`);if(n.code===void 0)try{Object.defineProperty(n,`code`,{value:25,configurable:!0})}catch{}return n}let n=Error(e);return n.name=`DataCloneError`,n.code=25,n}function substitutePortsWithPlaceholders(e,n){let r=new Map;for(let e=0;e<n.length;e++)r.set(n[e],e);function walk(e,n){if(typeof e!=`object`||!e)return e;if(e instanceof t){let t=r.get(e);return t===void 0?e:{__gjsifyTransferredPort:!0,index:t}}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)}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{t as MessagePort};
1
+ import"./_virtual/_rolldown/runtime.js";import{isSharedBuffer as e}from"./sab-transfer.js";import{EventEmitter as t}from"node:events";function isTransferredPortPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyTransferredPort===!0}var n=class MessagePort extends t{_started=!1;_closed=!1;_detached=!1;_messageQueue=[];_otherPort=null;_aeWrappers=new Map;start(){this._started||this._closed||(this._started=!0,this._drainQueue())}close(){if(this._closed)return;this._closed=!0;let e=this._otherPort;this._otherPort=null,e&&(e._otherPort=null),this.emit(`close`),this.removeAllListeners()}postMessage(t,n){if(this._closed)return;let r=this._otherPort;if(!r)return;let i=[],a=[];if(n&&n.length>0){let t=new Set;for(let r of n){if(t.has(r))throw createDataCloneError(`Transfer list contains duplicate entries`);if(t.add(r),r instanceof MessagePort){if(r===this)throw createDataCloneError(`Cannot transfer source port`);if(r._closed||r._detached)throw createDataCloneError(`MessagePort in transfer list is already detached`);a.push(r);continue}let o=Object.prototype.toString.call(r).slice(8,-1);if(o===`ArrayBuffer`){let e=r;if(e.detached===!0)throw createDataCloneError(`ArrayBuffer in transfer list is detached`);i.push(e);continue}if(o===`SharedArrayBuffer`)throw createDataCloneError(`SharedArrayBuffer cannot appear in transfer list (it is shared, not transferred)`);if(!e(r))throw createDataCloneError(`Value at index ${n.indexOf(r)} of transfer list is not transferable`)}}let o=a.slice(),s=t;a.length>0&&(s=substitutePortsWithPlaceholders(t,a));let c;try{c=structuredClone(s,{transfer:i.length>0?i:void 0})}catch(e){this.emit(`messageerror`,e instanceof Error?e:Error(`Could not clone message`));return}let l=c;if(o.length>0){let e=o.map(e=>{let t=e._otherPort,n=e._messageQueue.slice();e._messageQueue.length=0,e._otherPort=null,e._detached=!0,e._closed=!0;let r=new MessagePort;r._otherPort=t,t&&(t._otherPort=r);for(let e of n)r._messageQueue.push(e);return r});l=replacePlaceholdersWithPorts(c,e)}r._receiveMessage(l)}ref(){return this}unref(){return this}_receiveMessage(e){if(!this._closed){if(!this._started){this._messageQueue.push(e);return}this._dispatchMessage(e)}}get _hasQueuedMessages(){return this._messageQueue.length>0}_dequeueMessage(){return this._messageQueue.shift()}get _wasTransferred(){return this._detached}_drainQueue(){for(;this._messageQueue.length>0;)this._dispatchMessage(this._messageQueue.shift())}_dispatchMessage(e){Promise.resolve().then(()=>{this._closed||this.emit(`message`,e)})}addEventListener(e,t){if(t)if(e===`message`){let wrapper=e=>{t({data:e,type:`message`})};this._aeWrappers.set(t,wrapper),super.on(`message`,wrapper)}else super.on(e,t)}removeEventListener(e,t){if(t)if(e===`message`){let e=this._aeWrappers.get(t);e&&(super.off(`message`,e),this._aeWrappers.delete(t))}else super.off(e,t)}on(e,t){return super.on(e,t),e===`message`&&!this._started&&this.start(),this}addListener(e,t){return this.on(e,t)}once(e,t){return super.once(e,t),e===`message`&&!this._started&&this.start(),this}};function createDataCloneError(e){let t=globalThis.DOMException;if(typeof t==`function`){let n=new t(e,`DataCloneError`);if(n.code===void 0)try{Object.defineProperty(n,`code`,{value:25,configurable:!0})}catch{}return n}let n=Error(e);return n.name=`DataCloneError`,n.code=25,n}function substitutePortsWithPlaceholders(e,t){let r=new Map;for(let e=0;e<t.length;e++)r.set(t[e],e);function walk(e,t){if(typeof e!=`object`||!e)return e;if(e instanceof n){let t=r.get(e);return t===void 0?e:{__gjsifyTransferredPort:!0,index:t}}if(t.has(e))return t.get(e);if(Array.isArray(e)){let n=[];t.set(e,n);for(let r=0;r<e.length;r++)r in e&&(n[r]=walk(e[r],t));return n}if(Object.prototype.toString.call(e).slice(8,-1)===`Object`){let n={};t.set(e,n);for(let r of Object.keys(e))n[r]=walk(e[r],t);return n}return e}return walk(e,new Map)}function replacePlaceholdersWithPorts(e,t){function walk(e,n){if(typeof e!=`object`||!e)return e;if(isTransferredPortPlaceholder(e))return t[e.index];if(n.has(e))return n.get(e);if(Array.isArray(e)){n.set(e,e);for(let t=0;t<e.length;t++)t in e&&(e[t]=walk(e[t],n));return e}if(Object.prototype.toString.call(e).slice(8,-1)===`Object`){n.set(e,e);for(let t of Object.keys(e))e[t]=walk(e[t],n);return e}return e}return walk(e,new Map)}export{n as MessagePort};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";function isSharedBufferPlaceholder(e){return typeof e==`object`&&!!e&&typeof e.__sab==`number`&&typeof e.size==`number`}function isSharedBuffer(e){return typeof e==`object`&&!!e&&e.constructor?.name===`SharedBuffer`}function extractSharedBuffers(e,t){let n=[],r=new Map,i=t;function walk(e,t){if(typeof e!=`object`||!e)return e;if(isSharedBuffer(e)){let t=r.get(e);return t===void 0&&(t=i++,r.set(e,t),n.push({tag:t,buffer:e})),{__sab:t,size:e.byteLength}}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{value:walk(e,new Map),table:n,nextTag:i}}function materializeSharedBuffers(e,t){function walk(e,n){if(typeof e!=`object`||!e)return e;if(isSharedBufferPlaceholder(e))return t(e.__sab,e.size);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{extractSharedBuffers,isSharedBuffer,isSharedBufferPlaceholder,materializeSharedBuffers};
package/lib/esm/worker.js CHANGED
@@ -1,4 +1,4 @@
1
- import"./_virtual/_rolldown/runtime.js";import{EventEmitter as e}from"node:events";import t from"@girs/gio-2.0";import n from"@girs/glib-2.0";let r=1;const i=new TextEncoder,a=i.encode(`import GLib from 'gi://GLib';
1
+ import"./_virtual/_rolldown/runtime.js";import{extractSharedBuffers as e}from"./sab-transfer.js";import{EventEmitter as t}from"node:events";import n from"@girs/gio-2.0";import r from"@girs/glib-2.0";import{fdChannel as i}from"@gjsify/sab-native";let a=1;const o=new TextEncoder,s=o.encode(`import GLib from 'gi://GLib';
2
2
  import Gio from 'gi://Gio';
3
3
 
4
4
  const loop = new GLib.MainLoop(null, false);
@@ -6,15 +6,114 @@ const stdinStream = Gio.UnixInputStream.new(0, false);
6
6
  const dataIn = Gio.DataInputStream.new(stdinStream);
7
7
  const stdoutStream = Gio.UnixOutputStream.new(1, false);
8
8
 
9
+ const _encoder = new TextEncoder();
10
+
9
11
  function send(obj) {
10
12
  const line = JSON.stringify(obj) + '\\n';
11
13
  stdoutStream.write_all(_encoder.encode(line), null);
12
14
  }
13
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
+
14
22
  // Read init data (first line, blocking)
15
23
  const [initLine] = dataIn.read_line_utf8(null);
16
24
  const init = JSON.parse(initLine);
17
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 (Array.isArray(value)) {
74
+ for (let i = 0; i < value.length; i++) value[i] = materialise(value[i]);
75
+ return value;
76
+ }
77
+ if (Object.prototype.toString.call(value) === '[object Object]') {
78
+ for (const k of Object.keys(value)) value[k] = materialise(value[k]);
79
+ return value;
80
+ }
81
+ return value;
82
+ }
83
+
84
+ function makeSharedBuffer(native) {
85
+ return {
86
+ get byteLength() { return native.byte_length; },
87
+ get fd() { return native.fd; },
88
+ getUint8(off) { return native.get_u8(off); },
89
+ setUint8(off, v) { native.set_u8(off, v); },
90
+ getInt32LE(off) { return native.get_i32_le(off); },
91
+ setInt32LE(off, v) { native.set_i32_le(off, v); },
92
+ getUint32LE(off) { return native.get_u32_le(off); },
93
+ setUint32LE(off, v) { native.set_u32_le(off, v); },
94
+ getUint64LE(off) { return native.get_u64_le(off); },
95
+ setUint64LE(off, v) { native.set_u64_le(off, v); },
96
+ readBytes(off, len) {
97
+ const bytes = native.read_bytes(off, len);
98
+ const data = bytes.get_data();
99
+ return data ? new Uint8Array(data) : new Uint8Array(0);
100
+ },
101
+ writeBytes(off, data) { native.write_bytes(off, new GLib.Bytes(data)); },
102
+ get _nativeHandle() { return native; },
103
+ get constructor() { return { name: 'SharedBuffer' }; },
104
+ };
105
+ }
106
+
107
+ // Drain SharedBuffer fds attached to the init line (workerData may carry
108
+ // SharedBuffer instances) before materialise() runs against it.
109
+ if (init.sabSocketFd === 3) {
110
+ try {
111
+ drainSabFds(countSabPlaceholders(init.workerData));
112
+ } catch (err) {
113
+ send({ type: 'error', message: 'init fd drain failed: ' + (err && err.message ? err.message : err), stack: '' });
114
+ }
115
+ }
116
+
18
117
  // Simplified EventEmitter for parentPort
19
118
  const _listeners = new Map();
20
119
  const parentPort = {
@@ -45,7 +144,7 @@ const parentPort = {
45
144
  globalThis.__gjsify_worker_context = {
46
145
  isMainThread: false,
47
146
  parentPort,
48
- workerData: init.workerData ?? null,
147
+ workerData: materialise(init.workerData ?? null),
49
148
  threadId: init.threadId ?? 0,
50
149
  };
51
150
 
@@ -58,10 +157,20 @@ function readNext() {
58
157
  const [line] = source.read_line_finish_utf8(result);
59
158
  if (line === null) { loop.quit(); return; }
60
159
  const msg = JSON.parse(line);
61
- if (msg.type === 'message') parentPort.emit('message', msg.data);
160
+ if (msg.type === 'message') {
161
+ // Drain fds for any SharedBuffer placeholders BEFORE materialise.
162
+ // recv_fd is synchronous but the sender always wrote the SCM_RIGHTS
163
+ // messages before writing this stdin line, so they're already
164
+ // buffered.
165
+ if (init.sabSocketFd === 3) drainSabFds(countSabPlaceholders(msg.data));
166
+ parentPort.emit('message', materialise(msg.data));
167
+ }
62
168
  else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); loop.quit(); return; }
63
169
  readNext();
64
- } catch { loop.quit(); }
170
+ } catch (err) {
171
+ send({ type: 'error', message: 'bootstrap message error: ' + (err && err.message ? err.message : err), stack: err && err.stack || '' });
172
+ loop.quit();
173
+ }
65
174
  });
66
175
  }
67
176
  readNext();
@@ -71,7 +180,7 @@ try {
71
180
  if (init.eval) {
72
181
  const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;
73
182
  await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(
74
- parentPort, init.workerData, init.threadId
183
+ parentPort, globalThis.__gjsify_worker_context.workerData, init.threadId
75
184
  );
76
185
  } else {
77
186
  await import(init.filename);
@@ -81,8 +190,8 @@ try {
81
190
  }
82
191
 
83
192
  loop.run();
84
- `);var o=class Worker extends e{threadId;resourceLimits;_subprocess=null;_stdinPipe=null;_exited=!1;_bootstrapFile=null;constructor(e,o){super(),this.threadId=r++,this.resourceLimits=o?.resourceLimits||{};let s=o?.eval===!0,c=Worker._resolveFilename(e,s),l=`${n.get_tmp_dir()}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;this._bootstrapFile=t.File.new_for_path(l);try{this._bootstrapFile.replace_contents(a,null,!1,t.FileCreateFlags.REPLACE_DESTINATION,null)}catch(e){throw Error(`Failed to create worker bootstrap: ${e instanceof Error?e.message:e}`)}let u=new t.SubprocessLauncher({flags:t.SubprocessFlags.STDIN_PIPE|t.SubprocessFlags.STDOUT_PIPE|t.SubprocessFlags.STDERR_PIPE});if(o?.env&&typeof o.env==`object`)for(let[e,t]of Object.entries(o.env))u.setenv(e,String(t),!0);try{this._subprocess=u.spawnv([`gjs`,`-m`,l])}catch(e){throw this._cleanup(),Error(`Failed to spawn worker: ${e instanceof Error?e.message:e}`)}this._stdinPipe=this._subprocess.get_stdin_pipe();let d=this._subprocess.get_stdout_pipe();if(!s){let e=c.startsWith(`file://`)?c.slice(7):c;if(!t.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 f=JSON.stringify({threadId:this.threadId,workerData:o?.workerData??null,eval:s,filename:s?void 0:c,code:s?c:void 0})+`
85
- `;try{this._stdinPipe.write_all(i.encode(f),null)}catch(e){throw this._cleanup(),Error(`Failed to send init data: ${e instanceof Error?e.message:e}`)}if(d){let e=t.DataInputStream.new(d);this._readMessages(e)}let p=this._subprocess.get_stderr_pipe();p&&this._readStderr(t.DataInputStream.new(p)),this._subprocess.wait_async(null,()=>{this._onExit()})}postMessage(e,t){if(!(this._exited||!this._stdinPipe))try{let t=JSON.stringify({type:`message`,data:e})+`
86
- `;this._stdinPipe.write_all(i.encode(t),null)}catch{}}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`})+`
87
- `;this._stdinPipe.write_all(i.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,r){if(r)return String(e);if(e instanceof URL)return e.href;let i=String(e);if(i.startsWith(`file://`)||i.startsWith(`http://`)||i.startsWith(`https://`))return i;if(i.startsWith(`/`))return`file://`+i;if(i.startsWith(`./`)||i.startsWith(`../`)||!i.includes(`/`)){let e=n.get_current_dir(),r=n.build_filenamev([e,i]);return`file://`+(t.File.new_for_path(r).get_path()||r)}return`file://`+i}_readMessages(e){e.read_line_async(n.PRIORITY_DEFAULT,null,(t,n)=>{try{let[t]=e.read_line_finish_utf8(n);if(t===null)return;let r=JSON.parse(t);switch(r.type){case`online`:this.emit(`online`);break;case`message`:this.emit(`message`,r.data);break;case`error`:{let e=Error(r.message);r.stack&&(e.stack=r.stack),this.emit(`error`,e);break}}this._readMessages(e)}catch{}})}_stderrChunks=[];_readStderr(e){e.read_line_async(n.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(`
88
- `);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._subprocess=null}};export{o as Worker};
193
+ `);var c=class Worker extends t{threadId;resourceLimits;_subprocess=null;_stdinPipe=null;_exited=!1;_bootstrapFile=null;_sabSocketFd=-1;_sabNextTag=0;constructor(e,t){super(),this.threadId=a++,this.resourceLimits=t?.resourceLimits||{};let c=t?.eval===!0,l=Worker._resolveFilename(e,c),u=`${r.get_tmp_dir()}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;this._bootstrapFile=n.File.new_for_path(u);try{this._bootstrapFile.replace_contents(s,null,!1,n.FileCreateFlags.REPLACE_DESTINATION,null)}catch(e){throw Error(`Failed to create worker bootstrap: ${e instanceof Error?e.message:e}`)}let d=new n.SubprocessLauncher({flags:n.SubprocessFlags.STDIN_PIPE|n.SubprocessFlags.STDOUT_PIPE|n.SubprocessFlags.STDERR_PIPE});if(t?.env&&typeof t.env==`object`)for(let[e,n]of Object.entries(t.env))d.setenv(e,String(n),!0);let f=-1;if(i){let e=i.makePair();e&&(this._sabSocketFd=e.parentFd,f=e.childFd,d.take_fd(f,3))}try{this._subprocess=d.spawnv([`gjs`,`-m`,u])}catch(e){throw this._cleanup(),Error(`Failed to spawn worker: ${e instanceof Error?e.message:e}`)}this._stdinPipe=this._subprocess.get_stdin_pipe();let p=this._subprocess.get_stdout_pipe();if(!c){let e=l.startsWith(`file://`)?l.slice(7):l;if(!n.File.new_for_path(e).query_exists(null)){this._cleanup();let t=Error(`Cannot find module '${e}'`);t.code=`ERR_MODULE_NOT_FOUND`,Promise.resolve().then(()=>{this.emit(`error`,t),this._exited=!0,this.emit(`exit`,1)});return}}let m=this._serializeWithSabTransfer(t?.workerData??null),h=JSON.stringify({threadId:this.threadId,workerData:m,eval:c,filename:c?void 0:l,code:c?l:void 0,sabSocketFd:this._sabSocketFd===-1?-1:3})+`
194
+ `;try{this._stdinPipe.write_all(o.encode(h),null)}catch(e){throw this._cleanup(),Error(`Failed to send init data: ${e instanceof Error?e.message:e}`)}if(p){let e=n.DataInputStream.new(p);this._readMessages(e)}let g=this._subprocess.get_stderr_pipe();g&&this._readStderr(n.DataInputStream.new(g)),this._subprocess.wait_async(null,()=>{this._onExit()})}postMessage(e,t){if(!(this._exited||!this._stdinPipe))try{let t=this._serializeWithSabTransfer(e),n=JSON.stringify({type:`message`,data:t})+`
195
+ `;this._stdinPipe.write_all(o.encode(n),null)}catch{}}_serializeWithSabTransfer(t){if(!i||this._sabSocketFd===-1)return t;let{value:n,table:r,nextTag:a}=e(t,this._sabNextTag);this._sabNextTag=a;for(let{tag:e,buffer:t}of r)if(!i.sendFd(this._sabSocketFd,t.fd,e))throw Error(`Failed to send SharedBuffer fd over worker side-channel (tag ${e})`);return n}terminate(){if(this._exited)return Promise.resolve(0);let e=new Promise(e=>{this.once(`exit`,t=>e(t))});try{if(this._stdinPipe){let e=JSON.stringify({type:`terminate`})+`
196
+ `;this._stdinPipe.write_all(o.encode(e),null)}}catch{}return setTimeout(()=>{!this._exited&&this._subprocess&&this._subprocess.force_exit()},500),e}ref(){return this}unref(){return this}static _resolveFilename(e,t){if(t)return String(e);if(e instanceof URL)return e.href;let i=String(e);if(i.startsWith(`file://`)||i.startsWith(`http://`)||i.startsWith(`https://`))return i;if(i.startsWith(`/`))return`file://`+i;if(i.startsWith(`./`)||i.startsWith(`../`)||!i.includes(`/`)){let e=r.get_current_dir(),t=r.build_filenamev([e,i]);return`file://`+(n.File.new_for_path(t).get_path()||t)}return`file://`+i}_readMessages(e){e.read_line_async(r.PRIORITY_DEFAULT,null,(t,n)=>{try{let[t]=e.read_line_finish_utf8(n);if(t===null)return;let r=JSON.parse(t);switch(r.type){case`online`:this.emit(`online`);break;case`message`:this.emit(`message`,r.data);break;case`error`:{let e=Error(r.message);r.stack&&(e.stack=r.stack),this.emit(`error`,e);break}}this._readMessages(e)}catch{}})}_stderrChunks=[];_readStderr(e){e.read_line_async(r.PRIORITY_DEFAULT,null,(t,n)=>{try{let[t]=e.read_line_finish_utf8(n);if(t===null){if(this._stderrChunks.length>0){let e=this._stderrChunks.join(`
197
+ `);this.listenerCount(`error`)===0&&this.emit(`error`,Error(e))}return}this._stderrChunks.push(t),this._readStderr(e)}catch{}})}_onExit(){if(this._exited)return;this._exited=!0;let e=this._subprocess?.get_if_exited()?this._subprocess.get_exit_status():1;this._cleanup(),this.emit(`exit`,e)}_cleanup(){if(this._bootstrapFile){try{this._bootstrapFile.delete(null)}catch{}this._bootstrapFile=null}if(this._stdinPipe){try{this._stdinPipe.close(null)}catch{}this._stdinPipe=null}if(this._sabSocketFd!==-1&&i?.closeFd){try{i.closeFd(this._sabSocketFd)}catch{}this._sabSocketFd=-1}this._subprocess=null}};export{c as Worker};
@@ -0,0 +1,57 @@
1
+ import type { SharedBuffer } from '@gjsify/sab-native';
2
+ /**
3
+ * Placeholder for a SharedBuffer instance that has been transferred
4
+ * cross-process. The receiver reconstructs a SharedBuffer from the
5
+ * incoming fd indexed by `__sab` (the tag the sender attached to the
6
+ * SCM_RIGHTS message), with the original byteLength.
7
+ */
8
+ export interface SharedBufferPlaceholder {
9
+ readonly __sab: number;
10
+ readonly size: number;
11
+ }
12
+ export declare function isSharedBufferPlaceholder(value: unknown): value is SharedBufferPlaceholder;
13
+ /**
14
+ * Detect a SharedBuffer instance without holding a hard runtime reference
15
+ * to the constructor — keeps the check resilient when sab-native's
16
+ * prebuild is unavailable (the import resolves but the class never gets
17
+ * instantiated) and avoids paying the typelib load just to typecheck a
18
+ * message payload.
19
+ */
20
+ export declare function isSharedBuffer(value: unknown): value is SharedBuffer;
21
+ /**
22
+ * Walk `value`, replace every SharedBuffer instance with a placeholder
23
+ * carrying a freshly-allocated tag, and return the substituted tree
24
+ * alongside the table of (tag, SharedBuffer) pairs the caller must send
25
+ * over the SCM_RIGHTS side-channel before the JSON message itself.
26
+ *
27
+ * The walker handles plain objects + arrays. Other tagged types
28
+ * (Map, Set, …) are passed through unchanged — same constraint Node
29
+ * applies to its in-built clone walker.
30
+ *
31
+ * Tags start at `startTag` and increment per discovery. Callers should
32
+ * thread a per-Worker sequence counter so tags stay unique within a
33
+ * single FdChannel pair's lifetime (4 G tags = 2³² = plenty).
34
+ */
35
+ export declare function extractSharedBuffers(value: unknown, startTag: number): {
36
+ value: unknown;
37
+ table: {
38
+ tag: number;
39
+ buffer: SharedBuffer;
40
+ }[];
41
+ nextTag: number;
42
+ };
43
+ /**
44
+ * Receiver-side counterpart: walk a tree that may contain
45
+ * SharedBufferPlaceholder leaves and replace each with the corresponding
46
+ * SharedBuffer reconstructed from the fdMap. Mutates the input in place
47
+ * (callers always work on a freshly-parsed JSON tree, so this avoids an
48
+ * extra copy).
49
+ *
50
+ * If a placeholder references a tag that is not yet in `fdMap`, the
51
+ * caller should buffer the message and retry once the recv-loop fills
52
+ * the missing entry. In the current protocol the sender always
53
+ * sends fds before the JSON line, so the map is populated by the time
54
+ * the JSON arrives — but the bootstrap layer is responsible for that
55
+ * ordering.
56
+ */
57
+ export declare function materializeSharedBuffers(value: unknown, resolveTag: (tag: number, size: number) => SharedBuffer): unknown;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -19,8 +19,24 @@ export declare class Worker extends EventEmitter {
19
19
  private _stdinPipe;
20
20
  private _exited;
21
21
  private _bootstrapFile;
22
+ /** Parent-side end of the SCM_RIGHTS side-channel for SharedBuffer fds.
23
+ * -1 when sab-native's prebuild isn't loaded → SharedBuffer transfer is
24
+ * unavailable but everything else works. Closed in `_cleanup()`. */
25
+ private _sabSocketFd;
26
+ /** Per-Worker sequence counter for SharedBuffer transfer tags. Resets
27
+ * to 0 at spawn; each postMessage call increments it by the number of
28
+ * unique SharedBuffer instances in the value tree. */
29
+ private _sabNextTag;
22
30
  constructor(filename: string | URL, options?: WorkerOptions);
23
31
  postMessage(value: unknown, _transferList?: unknown[]): void;
32
+ /**
33
+ * Walk `value` for SharedBuffer instances, ship each fd over the
34
+ * parent-side end of the FdChannel socketpair, return the placeholder-
35
+ * substituted tree ready to JSON-serialise. No-op (returns value
36
+ * untouched) when sab-native's prebuild isn't loaded or the side-
37
+ * channel was never opened.
38
+ */
39
+ private _serializeWithSabTransfer;
24
40
  terminate(): Promise<number>;
25
41
  ref(): this;
26
42
  unref(): this;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/worker_threads",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "Node.js worker_threads module for Gjs",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -33,15 +33,16 @@
33
33
  "worker_threads"
34
34
  ],
35
35
  "devDependencies": {
36
- "@gjsify/cli": "^0.4.10",
37
- "@gjsify/node-globals": "^0.4.10",
38
- "@gjsify/unit": "^0.4.10",
36
+ "@gjsify/cli": "^0.4.12",
37
+ "@gjsify/node-globals": "^0.4.12",
38
+ "@gjsify/unit": "^0.4.12",
39
39
  "@types/node": "^25.6.2",
40
40
  "typescript": "^6.0.3"
41
41
  },
42
42
  "dependencies": {
43
43
  "@girs/gio-2.0": "2.88.0-4.0.0-rc.15",
44
44
  "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
45
- "@gjsify/events": "^0.4.10"
45
+ "@gjsify/events": "^0.4.12",
46
+ "@gjsify/sab-native": "^0.4.12"
46
47
  }
47
48
  }