@gjsify/worker_threads 0.1.0
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/README.md +27 -0
- package/lib/esm/index.js +138 -0
- package/lib/esm/message-port.js +118 -0
- package/lib/esm/worker.js +315 -0
- package/lib/types/index.d.ts +59 -0
- package/lib/types/message-port.d.ts +30 -0
- package/lib/types/worker.d.ts +42 -0
- package/package.json +44 -0
- package/src/index.spec.ts +1591 -0
- package/src/index.ts +148 -0
- package/src/message-port.ts +130 -0
- package/src/test.mts +4 -0
- package/src/worker.ts +394 -0
- package/tsconfig.json +31 -0
- package/tsconfig.tsbuildinfo +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @gjsify/worker_threads
|
|
2
|
+
|
|
3
|
+
GJS stub implementation of the Node.js `worker_threads` module. Currently provides isMainThread only.
|
|
4
|
+
|
|
5
|
+
Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @gjsify/worker_threads
|
|
11
|
+
# or
|
|
12
|
+
yarn add @gjsify/worker_threads
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { isMainThread } from '@gjsify/worker_threads';
|
|
19
|
+
|
|
20
|
+
if (isMainThread) {
|
|
21
|
+
console.log('Running in the main thread');
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## License
|
|
26
|
+
|
|
27
|
+
MIT
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { MessagePort } from "./message-port.js";
|
|
2
|
+
import { Worker } from "./worker.js";
|
|
3
|
+
import { MessagePort as MessagePort2 } from "./message-port.js";
|
|
4
|
+
import { Worker as Worker2 } from "./worker.js";
|
|
5
|
+
const _ctx = globalThis.__gjsify_worker_context;
|
|
6
|
+
const isMainThread = !_ctx;
|
|
7
|
+
const parentPort = _ctx?.parentPort ?? null;
|
|
8
|
+
const workerData = _ctx?.workerData ?? null;
|
|
9
|
+
const threadId = _ctx?.threadId ?? 0;
|
|
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();
|
|
81
|
+
function setEnvironmentData(key, value) {
|
|
82
|
+
if (value === void 0) {
|
|
83
|
+
_environmentData.delete(key);
|
|
84
|
+
} else {
|
|
85
|
+
_environmentData.set(key, value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function getEnvironmentData(key) {
|
|
89
|
+
return _environmentData.get(key);
|
|
90
|
+
}
|
|
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) {
|
|
98
|
+
}
|
|
99
|
+
function moveMessagePortToContext(port, _context) {
|
|
100
|
+
return port;
|
|
101
|
+
}
|
|
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
|
|
138
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
|
118
|
+
};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import Gio from "@girs/gio-2.0";
|
|
2
|
+
import GLib from "@girs/glib-2.0";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
let _nextThreadId = 1;
|
|
5
|
+
const _encoder = new TextEncoder();
|
|
6
|
+
const BOOTSTRAP_CODE = `import GLib from 'gi://GLib';
|
|
7
|
+
import Gio from 'gi://Gio';
|
|
8
|
+
|
|
9
|
+
const loop = new GLib.MainLoop(null, false);
|
|
10
|
+
const stdinStream = Gio.UnixInputStream.new(0, false);
|
|
11
|
+
const dataIn = Gio.DataInputStream.new(stdinStream);
|
|
12
|
+
const stdoutStream = Gio.UnixOutputStream.new(1, false);
|
|
13
|
+
|
|
14
|
+
function send(obj) {
|
|
15
|
+
const line = JSON.stringify(obj) + '\\n';
|
|
16
|
+
stdoutStream.write_all(_encoder.encode(line), null);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Read init data (first line, blocking)
|
|
20
|
+
const [initLine] = dataIn.read_line_utf8(null);
|
|
21
|
+
const init = JSON.parse(initLine);
|
|
22
|
+
|
|
23
|
+
// Simplified EventEmitter for parentPort
|
|
24
|
+
const _listeners = new Map();
|
|
25
|
+
const parentPort = {
|
|
26
|
+
on(ev, fn) {
|
|
27
|
+
if (!_listeners.has(ev)) _listeners.set(ev, []);
|
|
28
|
+
_listeners.get(ev).push(fn);
|
|
29
|
+
return this;
|
|
30
|
+
},
|
|
31
|
+
once(ev, fn) {
|
|
32
|
+
const w = (...a) => { parentPort.off(ev, w); fn(...a); };
|
|
33
|
+
return parentPort.on(ev, w);
|
|
34
|
+
},
|
|
35
|
+
off(ev, fn) {
|
|
36
|
+
const a = _listeners.get(ev);
|
|
37
|
+
if (a) _listeners.set(ev, a.filter(f => f !== fn));
|
|
38
|
+
return this;
|
|
39
|
+
},
|
|
40
|
+
emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },
|
|
41
|
+
postMessage(data) { send({ type: 'message', data }); },
|
|
42
|
+
close() { send({ type: 'exit', code: 0 }); loop.quit(); },
|
|
43
|
+
removeAllListeners(ev) {
|
|
44
|
+
if (ev) _listeners.delete(ev); else _listeners.clear();
|
|
45
|
+
return this;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Set worker context globals (read by @gjsify/worker_threads when imported by user script)
|
|
50
|
+
globalThis.__gjsify_worker_context = {
|
|
51
|
+
isMainThread: false,
|
|
52
|
+
parentPort,
|
|
53
|
+
workerData: init.workerData ?? null,
|
|
54
|
+
threadId: init.threadId ?? 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
send({ type: 'online' });
|
|
58
|
+
|
|
59
|
+
// Async stdin reader for messages from parent
|
|
60
|
+
function readNext() {
|
|
61
|
+
dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {
|
|
62
|
+
try {
|
|
63
|
+
const [line] = source.read_line_finish_utf8(result);
|
|
64
|
+
if (line === null) { loop.quit(); return; }
|
|
65
|
+
const msg = JSON.parse(line);
|
|
66
|
+
if (msg.type === 'message') parentPort.emit('message', msg.data);
|
|
67
|
+
else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); loop.quit(); return; }
|
|
68
|
+
readNext();
|
|
69
|
+
} catch { loop.quit(); }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
readNext();
|
|
73
|
+
|
|
74
|
+
// Execute worker code
|
|
75
|
+
try {
|
|
76
|
+
if (init.eval) {
|
|
77
|
+
const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;
|
|
78
|
+
await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(
|
|
79
|
+
parentPort, init.workerData, init.threadId
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
await import(init.filename);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
send({ type: 'error', message: error.message, stack: error.stack || '' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
loop.run();
|
|
89
|
+
`;
|
|
90
|
+
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 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 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
|
|
315
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export { MessagePort } from './message-port.ts';
|
|
2
|
+
export { Worker } from './worker.ts';
|
|
3
|
+
export type { WorkerOptions } from './worker.ts';
|
|
4
|
+
import { MessagePort } from './message-port.ts';
|
|
5
|
+
import { Worker } from './worker.ts';
|
|
6
|
+
export declare const isMainThread: boolean;
|
|
7
|
+
export declare const parentPort: MessagePort | null;
|
|
8
|
+
export declare const workerData: unknown;
|
|
9
|
+
export declare const threadId: number;
|
|
10
|
+
export declare const resourceLimits: Record<string, unknown>;
|
|
11
|
+
export declare const SHARE_ENV: unique symbol;
|
|
12
|
+
export declare class MessageChannel {
|
|
13
|
+
readonly port1: MessagePort;
|
|
14
|
+
readonly port2: MessagePort;
|
|
15
|
+
constructor();
|
|
16
|
+
}
|
|
17
|
+
export declare class BroadcastChannel {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
private _closed;
|
|
20
|
+
onmessage: ((event: {
|
|
21
|
+
data: unknown;
|
|
22
|
+
}) => void) | null;
|
|
23
|
+
onmessageerror: ((event: {
|
|
24
|
+
data: unknown;
|
|
25
|
+
}) => void) | null;
|
|
26
|
+
constructor(name: string);
|
|
27
|
+
postMessage(message: unknown): void;
|
|
28
|
+
close(): void;
|
|
29
|
+
addEventListener(type: string, listener: ((event: unknown) => void) | null): void;
|
|
30
|
+
removeEventListener(type: string, _listener: ((event: unknown) => void) | null): void;
|
|
31
|
+
dispatchEvent(_event: Event): boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare function setEnvironmentData(key: string, value: unknown): void;
|
|
34
|
+
export declare function getEnvironmentData(key: string): unknown;
|
|
35
|
+
export declare function receiveMessageOnPort(port: MessagePort): {
|
|
36
|
+
message: unknown;
|
|
37
|
+
} | undefined;
|
|
38
|
+
export declare function markAsUntransferable(_object: unknown): void;
|
|
39
|
+
export declare function markAsUncloneable(_object: unknown): void;
|
|
40
|
+
export declare function moveMessagePortToContext(port: MessagePort, _context: unknown): MessagePort;
|
|
41
|
+
declare const _default: {
|
|
42
|
+
isMainThread: boolean;
|
|
43
|
+
parentPort: MessagePort;
|
|
44
|
+
workerData: unknown;
|
|
45
|
+
threadId: number;
|
|
46
|
+
resourceLimits: Record<string, unknown>;
|
|
47
|
+
SHARE_ENV: symbol;
|
|
48
|
+
Worker: typeof Worker;
|
|
49
|
+
MessageChannel: typeof MessageChannel;
|
|
50
|
+
MessagePort: typeof MessagePort;
|
|
51
|
+
BroadcastChannel: typeof BroadcastChannel;
|
|
52
|
+
setEnvironmentData: typeof setEnvironmentData;
|
|
53
|
+
getEnvironmentData: typeof getEnvironmentData;
|
|
54
|
+
receiveMessageOnPort: typeof receiveMessageOnPort;
|
|
55
|
+
markAsUntransferable: typeof markAsUntransferable;
|
|
56
|
+
markAsUncloneable: typeof markAsUncloneable;
|
|
57
|
+
moveMessagePortToContext: typeof moveMessagePortToContext;
|
|
58
|
+
};
|
|
59
|
+
export default _default;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
export declare class MessagePort extends EventEmitter {
|
|
3
|
+
private _started;
|
|
4
|
+
private _closed;
|
|
5
|
+
private _messageQueue;
|
|
6
|
+
/** @internal Linked port for in-process communication */
|
|
7
|
+
_otherPort: MessagePort | null;
|
|
8
|
+
/** @internal Maps addEventListener listeners to their internal wrappers */
|
|
9
|
+
private _aeWrappers;
|
|
10
|
+
start(): void;
|
|
11
|
+
close(): void;
|
|
12
|
+
postMessage(value: unknown, _transferList?: unknown[]): void;
|
|
13
|
+
ref(): this;
|
|
14
|
+
unref(): this;
|
|
15
|
+
_receiveMessage(message: unknown): void;
|
|
16
|
+
get _hasQueuedMessages(): boolean;
|
|
17
|
+
_dequeueMessage(): unknown | undefined;
|
|
18
|
+
private _drainQueue;
|
|
19
|
+
private _dispatchMessage;
|
|
20
|
+
/**
|
|
21
|
+
* Web-compatible addEventListener. Wraps message data in a MessageEvent-like
|
|
22
|
+
* object `{ data, type }` before calling the listener.
|
|
23
|
+
* Requires explicit `port.start()` call (unlike `on('message')` which auto-starts).
|
|
24
|
+
*/
|
|
25
|
+
addEventListener(type: string, listener: ((event: unknown) => void) | null): void;
|
|
26
|
+
removeEventListener(type: string, listener: ((event: unknown) => void) | null): void;
|
|
27
|
+
on(event: string | symbol, listener: (...args: unknown[]) => void): this;
|
|
28
|
+
addListener(event: string | symbol, listener: (...args: unknown[]) => void): this;
|
|
29
|
+
once(event: string | symbol, listener: (...args: unknown[]) => void): this;
|
|
30
|
+
}
|