@gjsify/worker_threads 0.3.13 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/esm/index.js CHANGED
@@ -1,138 +1,120 @@
1
1
  import { MessagePort } from "./message-port.js";
2
2
  import { Worker } from "./worker.js";
3
- import { MessagePort as MessagePort2 } from "./message-port.js";
4
- import { Worker as Worker2 } from "./worker.js";
3
+
4
+ //#region src/index.ts
5
5
  const _ctx = globalThis.__gjsify_worker_context;
6
6
  const isMainThread = !_ctx;
7
7
  const parentPort = _ctx?.parentPort ?? null;
8
8
  const workerData = _ctx?.workerData ?? null;
9
9
  const threadId = _ctx?.threadId ?? 0;
10
10
  const resourceLimits = {};
11
- const SHARE_ENV = /* @__PURE__ */ Symbol("worker_threads.SHARE_ENV");
12
- class MessageChannel {
13
- port1;
14
- port2;
15
- constructor() {
16
- this.port1 = new MessagePort2();
17
- this.port2 = new MessagePort2();
18
- this.port1._otherPort = this.port2;
19
- this.port2._otherPort = this.port1;
20
- }
21
- }
22
- const _broadcastRegistry = /* @__PURE__ */ new Map();
23
- class BroadcastChannel {
24
- name;
25
- _closed = false;
26
- onmessage = null;
27
- onmessageerror = null;
28
- constructor(name) {
29
- this.name = String(name);
30
- if (!_broadcastRegistry.has(this.name)) {
31
- _broadcastRegistry.set(this.name, /* @__PURE__ */ new Set());
32
- }
33
- _broadcastRegistry.get(this.name).add(this);
34
- }
35
- postMessage(message) {
36
- if (this._closed) {
37
- throw new Error("BroadcastChannel is closed");
38
- }
39
- const channels = _broadcastRegistry.get(this.name);
40
- if (!channels) return;
41
- for (const channel of channels) {
42
- if (channel !== this && !channel._closed) {
43
- const cloned = structuredClone(message);
44
- Promise.resolve().then(() => {
45
- if (!channel._closed && channel.onmessage) {
46
- channel.onmessage({ data: cloned });
47
- }
48
- });
49
- }
50
- }
51
- }
52
- close() {
53
- if (this._closed) return;
54
- this._closed = true;
55
- const channels = _broadcastRegistry.get(this.name);
56
- if (channels) {
57
- channels.delete(this);
58
- if (channels.size === 0) {
59
- _broadcastRegistry.delete(this.name);
60
- }
61
- }
62
- this.onmessage = null;
63
- this.onmessageerror = null;
64
- }
65
- addEventListener(type, listener) {
66
- if (type === "message") {
67
- this.onmessage = listener;
68
- } else if (type === "messageerror") {
69
- this.onmessageerror = listener;
70
- }
71
- }
72
- removeEventListener(type, _listener) {
73
- if (type === "message") this.onmessage = null;
74
- else if (type === "messageerror") this.onmessageerror = null;
75
- }
76
- dispatchEvent(_event) {
77
- return true;
78
- }
79
- }
80
- const _environmentData = /* @__PURE__ */ new Map();
11
+ const SHARE_ENV = Symbol("worker_threads.SHARE_ENV");
12
+ var MessageChannel = class {
13
+ port1;
14
+ port2;
15
+ constructor() {
16
+ this.port1 = new MessagePort();
17
+ this.port2 = new MessagePort();
18
+ this.port1._otherPort = this.port2;
19
+ this.port2._otherPort = this.port1;
20
+ }
21
+ };
22
+ const _broadcastRegistry = new Map();
23
+ var BroadcastChannel = class {
24
+ name;
25
+ _closed = false;
26
+ onmessage = null;
27
+ onmessageerror = null;
28
+ constructor(name) {
29
+ this.name = String(name);
30
+ if (!_broadcastRegistry.has(this.name)) {
31
+ _broadcastRegistry.set(this.name, new Set());
32
+ }
33
+ _broadcastRegistry.get(this.name).add(this);
34
+ }
35
+ postMessage(message) {
36
+ if (this._closed) {
37
+ throw new Error("BroadcastChannel is closed");
38
+ }
39
+ const channels = _broadcastRegistry.get(this.name);
40
+ if (!channels) return;
41
+ for (const channel of channels) {
42
+ if (channel !== this && !channel._closed) {
43
+ const cloned = structuredClone(message);
44
+ Promise.resolve().then(() => {
45
+ if (!channel._closed && channel.onmessage) {
46
+ channel.onmessage({ data: cloned });
47
+ }
48
+ });
49
+ }
50
+ }
51
+ }
52
+ close() {
53
+ if (this._closed) return;
54
+ this._closed = true;
55
+ const channels = _broadcastRegistry.get(this.name);
56
+ if (channels) {
57
+ channels.delete(this);
58
+ if (channels.size === 0) {
59
+ _broadcastRegistry.delete(this.name);
60
+ }
61
+ }
62
+ this.onmessage = null;
63
+ this.onmessageerror = null;
64
+ }
65
+ addEventListener(type, listener) {
66
+ if (type === "message") {
67
+ this.onmessage = listener;
68
+ } else if (type === "messageerror") {
69
+ this.onmessageerror = listener;
70
+ }
71
+ }
72
+ removeEventListener(type, _listener) {
73
+ if (type === "message") this.onmessage = null;
74
+ else if (type === "messageerror") this.onmessageerror = null;
75
+ }
76
+ dispatchEvent(_event) {
77
+ return true;
78
+ }
79
+ };
80
+ const _environmentData = new Map();
81
81
  function setEnvironmentData(key, value) {
82
- if (value === void 0) {
83
- _environmentData.delete(key);
84
- } else {
85
- _environmentData.set(key, value);
86
- }
82
+ if (value === undefined) {
83
+ _environmentData.delete(key);
84
+ } else {
85
+ _environmentData.set(key, value);
86
+ }
87
87
  }
88
88
  function getEnvironmentData(key) {
89
- return _environmentData.get(key);
89
+ return _environmentData.get(key);
90
90
  }
91
91
  function receiveMessageOnPort(port) {
92
- if (!port._hasQueuedMessages) return void 0;
93
- return { message: port._dequeueMessage() };
94
- }
95
- function markAsUntransferable(_object) {
96
- }
97
- function markAsUncloneable(_object) {
92
+ if (!port._hasQueuedMessages) return undefined;
93
+ return { message: port._dequeueMessage() };
98
94
  }
95
+ function markAsUntransferable(_object) {}
96
+ function markAsUncloneable(_object) {}
99
97
  function moveMessagePortToContext(port, _context) {
100
- return port;
98
+ return port;
101
99
  }
102
- var index_default = {
103
- isMainThread,
104
- parentPort,
105
- workerData,
106
- threadId,
107
- resourceLimits,
108
- SHARE_ENV,
109
- Worker: Worker2,
110
- MessageChannel,
111
- MessagePort: MessagePort2,
112
- BroadcastChannel,
113
- setEnvironmentData,
114
- getEnvironmentData,
115
- receiveMessageOnPort,
116
- markAsUntransferable,
117
- markAsUncloneable,
118
- moveMessagePortToContext
119
- };
120
- export {
121
- BroadcastChannel,
122
- MessageChannel,
123
- MessagePort,
124
- SHARE_ENV,
125
- Worker,
126
- index_default as default,
127
- getEnvironmentData,
128
- isMainThread,
129
- markAsUncloneable,
130
- markAsUntransferable,
131
- moveMessagePortToContext,
132
- parentPort,
133
- receiveMessageOnPort,
134
- resourceLimits,
135
- setEnvironmentData,
136
- threadId,
137
- workerData
100
+ var src_default = {
101
+ isMainThread,
102
+ parentPort,
103
+ workerData,
104
+ threadId,
105
+ resourceLimits,
106
+ SHARE_ENV,
107
+ Worker,
108
+ MessageChannel,
109
+ MessagePort,
110
+ BroadcastChannel,
111
+ setEnvironmentData,
112
+ getEnvironmentData,
113
+ receiveMessageOnPort,
114
+ markAsUntransferable,
115
+ markAsUncloneable,
116
+ moveMessagePortToContext
138
117
  };
118
+
119
+ //#endregion
120
+ export { BroadcastChannel, MessageChannel, MessagePort, SHARE_ENV, Worker, src_default as default, getEnvironmentData, isMainThread, markAsUncloneable, markAsUntransferable, moveMessagePortToContext, parentPort, receiveMessageOnPort, resourceLimits, setEnvironmentData, threadId, workerData };
@@ -1,118 +1,123 @@
1
1
  import { EventEmitter } from "node:events";
2
- class MessagePort extends EventEmitter {
3
- _started = false;
4
- _closed = false;
5
- _messageQueue = [];
6
- /** @internal Linked port for in-process communication */
7
- _otherPort = null;
8
- /** @internal Maps addEventListener listeners to their internal wrappers */
9
- _aeWrappers = /* @__PURE__ */ new Map();
10
- start() {
11
- if (this._started || this._closed) return;
12
- this._started = true;
13
- this._drainQueue();
14
- }
15
- close() {
16
- if (this._closed) return;
17
- this._closed = true;
18
- const other = this._otherPort;
19
- this._otherPort = null;
20
- if (other) other._otherPort = null;
21
- this.emit("close");
22
- this.removeAllListeners();
23
- }
24
- postMessage(value, _transferList) {
25
- if (this._closed) return;
26
- const target = this._otherPort;
27
- if (!target) return;
28
- let cloned;
29
- try {
30
- cloned = structuredClone(value);
31
- } catch (err) {
32
- this.emit("messageerror", err instanceof Error ? err : new Error("Could not clone message"));
33
- return;
34
- }
35
- target._receiveMessage(cloned);
36
- }
37
- ref() {
38
- return this;
39
- }
40
- unref() {
41
- return this;
42
- }
43
- _receiveMessage(message) {
44
- if (this._closed) return;
45
- if (!this._started) {
46
- this._messageQueue.push(message);
47
- return;
48
- }
49
- this._dispatchMessage(message);
50
- }
51
- get _hasQueuedMessages() {
52
- return this._messageQueue.length > 0;
53
- }
54
- _dequeueMessage() {
55
- return this._messageQueue.shift();
56
- }
57
- _drainQueue() {
58
- while (this._messageQueue.length > 0) {
59
- this._dispatchMessage(this._messageQueue.shift());
60
- }
61
- }
62
- _dispatchMessage(message) {
63
- Promise.resolve().then(() => {
64
- if (!this._closed) {
65
- this.emit("message", message);
66
- }
67
- });
68
- }
69
- /**
70
- * Web-compatible addEventListener. Wraps message data in a MessageEvent-like
71
- * object `{ data, type }` before calling the listener.
72
- * Requires explicit `port.start()` call (unlike `on('message')` which auto-starts).
73
- */
74
- addEventListener(type, listener) {
75
- if (!listener) return;
76
- if (type === "message") {
77
- const wrapper = (data) => {
78
- listener({ data, type: "message" });
79
- };
80
- this._aeWrappers.set(listener, wrapper);
81
- super.on("message", wrapper);
82
- } else {
83
- super.on(type, listener);
84
- }
85
- }
86
- removeEventListener(type, listener) {
87
- if (!listener) return;
88
- if (type === "message") {
89
- const wrapper = this._aeWrappers.get(listener);
90
- if (wrapper) {
91
- super.off("message", wrapper);
92
- this._aeWrappers.delete(listener);
93
- }
94
- } else {
95
- super.off(type, listener);
96
- }
97
- }
98
- on(event, listener) {
99
- super.on(event, listener);
100
- if (event === "message" && !this._started) {
101
- this.start();
102
- }
103
- return this;
104
- }
105
- addListener(event, listener) {
106
- return this.on(event, listener);
107
- }
108
- once(event, listener) {
109
- super.once(event, listener);
110
- if (event === "message" && !this._started) {
111
- this.start();
112
- }
113
- return this;
114
- }
115
- }
116
- export {
117
- MessagePort
2
+
3
+ //#region src/message-port.ts
4
+ var MessagePort = class extends EventEmitter {
5
+ _started = false;
6
+ _closed = false;
7
+ _messageQueue = [];
8
+ /** @internal Linked port for in-process communication */
9
+ _otherPort = null;
10
+ /** @internal Maps addEventListener listeners to their internal wrappers */
11
+ _aeWrappers = new Map();
12
+ start() {
13
+ if (this._started || this._closed) return;
14
+ this._started = true;
15
+ this._drainQueue();
16
+ }
17
+ close() {
18
+ if (this._closed) return;
19
+ this._closed = true;
20
+ const other = this._otherPort;
21
+ this._otherPort = null;
22
+ if (other) other._otherPort = null;
23
+ this.emit("close");
24
+ this.removeAllListeners();
25
+ }
26
+ postMessage(value, _transferList) {
27
+ if (this._closed) return;
28
+ const target = this._otherPort;
29
+ if (!target) return;
30
+ let cloned;
31
+ try {
32
+ cloned = structuredClone(value);
33
+ } catch (err) {
34
+ this.emit("messageerror", err instanceof Error ? err : new Error("Could not clone message"));
35
+ return;
36
+ }
37
+ target._receiveMessage(cloned);
38
+ }
39
+ ref() {
40
+ return this;
41
+ }
42
+ unref() {
43
+ return this;
44
+ }
45
+ _receiveMessage(message) {
46
+ if (this._closed) return;
47
+ if (!this._started) {
48
+ this._messageQueue.push(message);
49
+ return;
50
+ }
51
+ this._dispatchMessage(message);
52
+ }
53
+ get _hasQueuedMessages() {
54
+ return this._messageQueue.length > 0;
55
+ }
56
+ _dequeueMessage() {
57
+ return this._messageQueue.shift();
58
+ }
59
+ _drainQueue() {
60
+ while (this._messageQueue.length > 0) {
61
+ this._dispatchMessage(this._messageQueue.shift());
62
+ }
63
+ }
64
+ _dispatchMessage(message) {
65
+ Promise.resolve().then(() => {
66
+ if (!this._closed) {
67
+ this.emit("message", message);
68
+ }
69
+ });
70
+ }
71
+ /**
72
+ * Web-compatible addEventListener. Wraps message data in a MessageEvent-like
73
+ * object `{ data, type }` before calling the listener.
74
+ * Requires explicit `port.start()` call (unlike `on('message')` which auto-starts).
75
+ */
76
+ addEventListener(type, listener) {
77
+ if (!listener) return;
78
+ if (type === "message") {
79
+ const wrapper = (data) => {
80
+ listener({
81
+ data,
82
+ type: "message"
83
+ });
84
+ };
85
+ this._aeWrappers.set(listener, wrapper);
86
+ super.on("message", wrapper);
87
+ } else {
88
+ super.on(type, listener);
89
+ }
90
+ }
91
+ removeEventListener(type, listener) {
92
+ if (!listener) return;
93
+ if (type === "message") {
94
+ const wrapper = this._aeWrappers.get(listener);
95
+ if (wrapper) {
96
+ super.off("message", wrapper);
97
+ this._aeWrappers.delete(listener);
98
+ }
99
+ } else {
100
+ super.off(type, listener);
101
+ }
102
+ }
103
+ on(event, listener) {
104
+ super.on(event, listener);
105
+ if (event === "message" && !this._started) {
106
+ this.start();
107
+ }
108
+ return this;
109
+ }
110
+ addListener(event, listener) {
111
+ return this.on(event, listener);
112
+ }
113
+ once(event, listener) {
114
+ super.once(event, listener);
115
+ if (event === "message" && !this._started) {
116
+ this.start();
117
+ }
118
+ return this;
119
+ }
118
120
  };
121
+
122
+ //#endregion
123
+ export { MessagePort };
package/lib/esm/worker.js CHANGED
@@ -1,9 +1,12 @@
1
+ import { EventEmitter } from "node:events";
1
2
  import Gio from "@girs/gio-2.0";
2
3
  import GLib from "@girs/glib-2.0";
3
- import { EventEmitter } from "node:events";
4
+
5
+ //#region src/worker.ts
4
6
  let _nextThreadId = 1;
5
7
  const _encoder = new TextEncoder();
6
- const BOOTSTRAP_CODE = `import GLib from 'gi://GLib';
8
+ const BOOTSTRAP_CODE = `\
9
+ import GLib from 'gi://GLib';
7
10
  import Gio from 'gi://Gio';
8
11
 
9
12
  const loop = new GLib.MainLoop(null, false);
@@ -88,228 +91,213 @@ try {
88
91
  loop.run();
89
92
  `;
90
93
  const BOOTSTRAP_BYTES = _encoder.encode(BOOTSTRAP_CODE);
91
- class Worker extends EventEmitter {
92
- threadId;
93
- resourceLimits;
94
- _subprocess = null;
95
- _stdinPipe = null;
96
- _exited = false;
97
- _bootstrapFile = null;
98
- constructor(filename, options) {
99
- super();
100
- this.threadId = _nextThreadId++;
101
- this.resourceLimits = options?.resourceLimits || {};
102
- const isEval = options?.eval === true;
103
- const resolvedFilename = Worker._resolveFilename(filename, isEval);
104
- const tmpDir = GLib.get_tmp_dir();
105
- const bootstrapPath = `${tmpDir}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;
106
- this._bootstrapFile = Gio.File.new_for_path(bootstrapPath);
107
- try {
108
- this._bootstrapFile.replace_contents(
109
- BOOTSTRAP_BYTES,
110
- null,
111
- false,
112
- Gio.FileCreateFlags.REPLACE_DESTINATION,
113
- null
114
- );
115
- } catch (err) {
116
- throw new Error(`Failed to create worker bootstrap: ${err instanceof Error ? err.message : err}`);
117
- }
118
- const launcher = new Gio.SubprocessLauncher({
119
- flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
120
- });
121
- if (options?.env && typeof options.env === "object") {
122
- for (const [key, value] of Object.entries(options.env)) {
123
- launcher.setenv(key, String(value), true);
124
- }
125
- }
126
- try {
127
- this._subprocess = launcher.spawnv(["gjs", "-m", bootstrapPath]);
128
- } catch (err) {
129
- this._cleanup();
130
- throw new Error(`Failed to spawn worker: ${err instanceof Error ? err.message : err}`);
131
- }
132
- this._stdinPipe = this._subprocess.get_stdin_pipe();
133
- const stdoutPipe = this._subprocess.get_stdout_pipe();
134
- if (!isEval) {
135
- const filePath = resolvedFilename.startsWith("file://") ? resolvedFilename.slice(7) : resolvedFilename;
136
- const file = Gio.File.new_for_path(filePath);
137
- if (!file.query_exists(null)) {
138
- this._cleanup();
139
- const err = new Error(`Cannot find module '${filePath}'`);
140
- err.code = "ERR_MODULE_NOT_FOUND";
141
- Promise.resolve().then(() => {
142
- this.emit("error", err);
143
- this._exited = true;
144
- this.emit("exit", 1);
145
- });
146
- return;
147
- }
148
- }
149
- const initData = JSON.stringify({
150
- threadId: this.threadId,
151
- workerData: options?.workerData ?? null,
152
- eval: isEval,
153
- filename: isEval ? void 0 : resolvedFilename,
154
- code: isEval ? resolvedFilename : void 0
155
- }) + "\n";
156
- try {
157
- this._stdinPipe.write_all(_encoder.encode(initData), null);
158
- } catch (err) {
159
- this._cleanup();
160
- throw new Error(`Failed to send init data: ${err instanceof Error ? err.message : err}`);
161
- }
162
- if (stdoutPipe) {
163
- const dataStream = Gio.DataInputStream.new(stdoutPipe);
164
- this._readMessages(dataStream);
165
- }
166
- const stderrPipe = this._subprocess.get_stderr_pipe();
167
- if (stderrPipe) {
168
- this._readStderr(Gio.DataInputStream.new(stderrPipe));
169
- }
170
- this._subprocess.wait_async(null, () => {
171
- this._onExit();
172
- });
173
- }
174
- postMessage(value, _transferList) {
175
- if (this._exited || !this._stdinPipe) return;
176
- try {
177
- const line = JSON.stringify({ type: "message", data: value }) + "\n";
178
- this._stdinPipe.write_all(_encoder.encode(line), null);
179
- } catch {
180
- }
181
- }
182
- terminate() {
183
- if (this._exited) return Promise.resolve(0);
184
- const exitPromise = new Promise((resolve) => {
185
- this.once("exit", (code) => resolve(code));
186
- });
187
- try {
188
- if (this._stdinPipe) {
189
- const msg = JSON.stringify({ type: "terminate" }) + "\n";
190
- this._stdinPipe.write_all(_encoder.encode(msg), null);
191
- }
192
- } catch {
193
- }
194
- setTimeout(() => {
195
- if (!this._exited && this._subprocess) {
196
- this._subprocess.force_exit();
197
- }
198
- }, 500);
199
- return exitPromise;
200
- }
201
- ref() {
202
- return this;
203
- }
204
- unref() {
205
- return this;
206
- }
207
- /**
208
- * Resolve a worker filename to an absolute path or file:// URL.
209
- *
210
- * - URL instanceshref string
211
- * - file:// URLskept as-is
212
- * - Absolute paths → converted to file:// URL
213
- * - Relative paths (./foo, ../bar) → resolved relative to cwd, converted to file:// URL
214
- * - eval mode → returned as-is (it's code, not a path)
215
- */
216
- static _resolveFilename(filename, isEval) {
217
- if (isEval) return String(filename);
218
- if (filename instanceof URL) return filename.href;
219
- const str = String(filename);
220
- if (str.startsWith("file://") || str.startsWith("http://") || str.startsWith("https://")) {
221
- return str;
222
- }
223
- if (str.startsWith("/")) {
224
- return "file://" + str;
225
- }
226
- if (str.startsWith("./") || str.startsWith("../") || !str.includes("/")) {
227
- const cwd = GLib.get_current_dir();
228
- const resolved = GLib.build_filenamev([cwd, str]);
229
- const file = Gio.File.new_for_path(resolved);
230
- const canonical = file.get_path();
231
- return "file://" + (canonical || resolved);
232
- }
233
- return "file://" + str;
234
- }
235
- _readMessages(dataStream) {
236
- dataStream.read_line_async(
237
- GLib.PRIORITY_DEFAULT,
238
- null,
239
- (_source, result) => {
240
- try {
241
- const [line] = dataStream.read_line_finish_utf8(result);
242
- if (line === null) return;
243
- const msg = JSON.parse(line);
244
- switch (msg.type) {
245
- case "online":
246
- this.emit("online");
247
- break;
248
- case "message":
249
- this.emit("message", msg.data);
250
- break;
251
- case "error": {
252
- const err = new Error(msg.message);
253
- if (msg.stack) err.stack = msg.stack;
254
- this.emit("error", err);
255
- break;
256
- }
257
- }
258
- this._readMessages(dataStream);
259
- } catch {
260
- }
261
- }
262
- );
263
- }
264
- _stderrChunks = [];
265
- _readStderr(dataStream) {
266
- dataStream.read_line_async(
267
- GLib.PRIORITY_DEFAULT,
268
- null,
269
- (_source, result) => {
270
- try {
271
- const [line] = dataStream.read_line_finish_utf8(result);
272
- if (line === null) {
273
- if (this._stderrChunks.length > 0) {
274
- const stderrText = this._stderrChunks.join("\n");
275
- if (this.listenerCount("error") === 0) {
276
- this.emit("error", new Error(stderrText));
277
- }
278
- }
279
- return;
280
- }
281
- this._stderrChunks.push(line);
282
- this._readStderr(dataStream);
283
- } catch {
284
- }
285
- }
286
- );
287
- }
288
- _onExit() {
289
- if (this._exited) return;
290
- this._exited = true;
291
- const exitCode = this._subprocess?.get_if_exited() ? this._subprocess.get_exit_status() : 1;
292
- this._cleanup();
293
- this.emit("exit", exitCode);
294
- }
295
- _cleanup() {
296
- if (this._bootstrapFile) {
297
- try {
298
- this._bootstrapFile.delete(null);
299
- } catch {
300
- }
301
- this._bootstrapFile = null;
302
- }
303
- if (this._stdinPipe) {
304
- try {
305
- this._stdinPipe.close(null);
306
- } catch {
307
- }
308
- this._stdinPipe = null;
309
- }
310
- this._subprocess = null;
311
- }
312
- }
313
- export {
314
- Worker
94
+ var Worker = class Worker extends EventEmitter {
95
+ threadId;
96
+ resourceLimits;
97
+ _subprocess = null;
98
+ _stdinPipe = null;
99
+ _exited = false;
100
+ _bootstrapFile = null;
101
+ constructor(filename, options) {
102
+ super();
103
+ this.threadId = _nextThreadId++;
104
+ this.resourceLimits = options?.resourceLimits || {};
105
+ const isEval = options?.eval === true;
106
+ const resolvedFilename = Worker._resolveFilename(filename, isEval);
107
+ const tmpDir = GLib.get_tmp_dir();
108
+ const bootstrapPath = `${tmpDir}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;
109
+ this._bootstrapFile = Gio.File.new_for_path(bootstrapPath);
110
+ try {
111
+ this._bootstrapFile.replace_contents(BOOTSTRAP_BYTES, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null);
112
+ } catch (err) {
113
+ throw new Error(`Failed to create worker bootstrap: ${err instanceof Error ? err.message : err}`);
114
+ }
115
+ const launcher = new Gio.SubprocessLauncher({ flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE });
116
+ if (options?.env && typeof options.env === "object") {
117
+ for (const [key, value] of Object.entries(options.env)) {
118
+ launcher.setenv(key, String(value), true);
119
+ }
120
+ }
121
+ try {
122
+ this._subprocess = launcher.spawnv([
123
+ "gjs",
124
+ "-m",
125
+ bootstrapPath
126
+ ]);
127
+ } catch (err) {
128
+ this._cleanup();
129
+ throw new Error(`Failed to spawn worker: ${err instanceof Error ? err.message : err}`);
130
+ }
131
+ this._stdinPipe = this._subprocess.get_stdin_pipe();
132
+ const stdoutPipe = this._subprocess.get_stdout_pipe();
133
+ if (!isEval) {
134
+ const filePath = resolvedFilename.startsWith("file://") ? resolvedFilename.slice(7) : resolvedFilename;
135
+ const file = Gio.File.new_for_path(filePath);
136
+ if (!file.query_exists(null)) {
137
+ this._cleanup();
138
+ const err = new Error(`Cannot find module '${filePath}'`);
139
+ err.code = "ERR_MODULE_NOT_FOUND";
140
+ Promise.resolve().then(() => {
141
+ this.emit("error", err);
142
+ this._exited = true;
143
+ this.emit("exit", 1);
144
+ });
145
+ return;
146
+ }
147
+ }
148
+ const initData = JSON.stringify({
149
+ threadId: this.threadId,
150
+ workerData: options?.workerData ?? null,
151
+ eval: isEval,
152
+ filename: isEval ? undefined : resolvedFilename,
153
+ code: isEval ? resolvedFilename : undefined
154
+ }) + "\n";
155
+ try {
156
+ this._stdinPipe.write_all(_encoder.encode(initData), null);
157
+ } catch (err) {
158
+ this._cleanup();
159
+ throw new Error(`Failed to send init data: ${err instanceof Error ? err.message : err}`);
160
+ }
161
+ if (stdoutPipe) {
162
+ const dataStream = Gio.DataInputStream.new(stdoutPipe);
163
+ this._readMessages(dataStream);
164
+ }
165
+ const stderrPipe = this._subprocess.get_stderr_pipe();
166
+ if (stderrPipe) {
167
+ this._readStderr(Gio.DataInputStream.new(stderrPipe));
168
+ }
169
+ this._subprocess.wait_async(null, () => {
170
+ this._onExit();
171
+ });
172
+ }
173
+ postMessage(value, _transferList) {
174
+ if (this._exited || !this._stdinPipe) return;
175
+ try {
176
+ const line = JSON.stringify({
177
+ type: "message",
178
+ data: value
179
+ }) + "\n";
180
+ this._stdinPipe.write_all(_encoder.encode(line), null);
181
+ } catch {}
182
+ }
183
+ terminate() {
184
+ if (this._exited) return Promise.resolve(0);
185
+ const exitPromise = new Promise((resolve) => {
186
+ this.once("exit", (code) => resolve(code));
187
+ });
188
+ try {
189
+ if (this._stdinPipe) {
190
+ const msg = JSON.stringify({ type: "terminate" }) + "\n";
191
+ this._stdinPipe.write_all(_encoder.encode(msg), null);
192
+ }
193
+ } catch {}
194
+ setTimeout(() => {
195
+ if (!this._exited && this._subprocess) {
196
+ this._subprocess.force_exit();
197
+ }
198
+ }, 500);
199
+ return exitPromise;
200
+ }
201
+ ref() {
202
+ return this;
203
+ }
204
+ unref() {
205
+ return this;
206
+ }
207
+ /**
208
+ * Resolve a worker filename to an absolute path or file:// URL.
209
+ *
210
+ * - URL instances → href string
211
+ * - file:// URLs kept as-is
212
+ * - Absolute paths → converted to file:// URL
213
+ * - Relative paths (./foo, ../bar) resolved relative to cwd, converted to file:// URL
214
+ * - eval modereturned as-is (it's code, not a path)
215
+ */
216
+ static _resolveFilename(filename, isEval) {
217
+ if (isEval) return String(filename);
218
+ if (filename instanceof URL) return filename.href;
219
+ const str = String(filename);
220
+ if (str.startsWith("file://") || str.startsWith("http://") || str.startsWith("https://")) {
221
+ return str;
222
+ }
223
+ if (str.startsWith("/")) {
224
+ return "file://" + str;
225
+ }
226
+ if (str.startsWith("./") || str.startsWith("../") || !str.includes("/")) {
227
+ const cwd = GLib.get_current_dir();
228
+ const resolved = GLib.build_filenamev([cwd, str]);
229
+ const file = Gio.File.new_for_path(resolved);
230
+ const canonical = file.get_path();
231
+ return "file://" + (canonical || resolved);
232
+ }
233
+ return "file://" + str;
234
+ }
235
+ _readMessages(dataStream) {
236
+ dataStream.read_line_async(GLib.PRIORITY_DEFAULT, null, (_source, result) => {
237
+ try {
238
+ const [line] = dataStream.read_line_finish_utf8(result);
239
+ if (line === null) return;
240
+ const msg = JSON.parse(line);
241
+ switch (msg.type) {
242
+ case "online":
243
+ this.emit("online");
244
+ break;
245
+ case "message":
246
+ this.emit("message", msg.data);
247
+ break;
248
+ case "error": {
249
+ const err = new Error(msg.message);
250
+ if (msg.stack) err.stack = msg.stack;
251
+ this.emit("error", err);
252
+ break;
253
+ }
254
+ }
255
+ this._readMessages(dataStream);
256
+ } catch {}
257
+ });
258
+ }
259
+ _stderrChunks = [];
260
+ _readStderr(dataStream) {
261
+ dataStream.read_line_async(GLib.PRIORITY_DEFAULT, null, (_source, result) => {
262
+ try {
263
+ const [line] = dataStream.read_line_finish_utf8(result);
264
+ if (line === null) {
265
+ if (this._stderrChunks.length > 0) {
266
+ const stderrText = this._stderrChunks.join("\n");
267
+ if (this.listenerCount("error") === 0) {
268
+ this.emit("error", new Error(stderrText));
269
+ }
270
+ }
271
+ return;
272
+ }
273
+ this._stderrChunks.push(line);
274
+ this._readStderr(dataStream);
275
+ } catch {}
276
+ });
277
+ }
278
+ _onExit() {
279
+ if (this._exited) return;
280
+ this._exited = true;
281
+ const exitCode = this._subprocess?.get_if_exited() ? this._subprocess.get_exit_status() : 1;
282
+ this._cleanup();
283
+ this.emit("exit", exitCode);
284
+ }
285
+ _cleanup() {
286
+ if (this._bootstrapFile) {
287
+ try {
288
+ this._bootstrapFile.delete(null);
289
+ } catch {}
290
+ this._bootstrapFile = null;
291
+ }
292
+ if (this._stdinPipe) {
293
+ try {
294
+ this._stdinPipe.close(null);
295
+ } catch {}
296
+ this._stdinPipe = null;
297
+ }
298
+ this._subprocess = null;
299
+ }
315
300
  };
301
+
302
+ //#endregion
303
+ export { Worker };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/worker_threads",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "Node.js worker_threads module for Gjs",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -30,15 +30,15 @@
30
30
  "worker_threads"
31
31
  ],
32
32
  "devDependencies": {
33
- "@gjsify/cli": "^0.3.13",
34
- "@gjsify/node-globals": "^0.3.13",
35
- "@gjsify/unit": "^0.3.13",
33
+ "@gjsify/cli": "^0.3.14",
34
+ "@gjsify/node-globals": "^0.3.14",
35
+ "@gjsify/unit": "^0.3.14",
36
36
  "@types/node": "^25.6.0",
37
37
  "typescript": "^6.0.3"
38
38
  },
39
39
  "dependencies": {
40
- "@girs/gio-2.0": "^2.88.0-4.0.0-rc.9",
41
- "@girs/glib-2.0": "^2.88.0-4.0.0-rc.9",
42
- "@gjsify/events": "^0.3.13"
40
+ "@girs/gio-2.0": "2.88.0-4.0.0-rc.9",
41
+ "@girs/glib-2.0": "2.88.0-4.0.0-rc.9",
42
+ "@gjsify/events": "^0.3.14"
43
43
  }
44
44
  }