@gjsify/worker_threads 0.4.0 → 0.4.3

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/src/index.ts DELETED
@@ -1,148 +0,0 @@
1
- // Reference: Node.js lib/worker_threads.js
2
- // Reimplemented for GJS — MessageChannel/BroadcastChannel are pure JS,
3
- // Worker uses Gio.Subprocess (subprocess-based workers)
4
-
5
- export { MessagePort } from './message-port.ts';
6
- export { Worker } from './worker.ts';
7
- export type { WorkerOptions } from './worker.ts';
8
-
9
- import { MessagePort } from './message-port.ts';
10
- import { Worker } from './worker.ts';
11
-
12
- // When running inside a Worker subprocess, the bootstrap script sets
13
- // globalThis.__gjsify_worker_context before importing the user's code.
14
- const _ctx = (globalThis as Record<string, unknown>).__gjsify_worker_context as
15
- { isMainThread: boolean; parentPort: unknown; workerData: unknown; threadId: number } | undefined;
16
-
17
- export const isMainThread: boolean = !_ctx;
18
- export const parentPort: MessagePort | null = (_ctx?.parentPort as MessagePort) ?? null;
19
- export const workerData: unknown = _ctx?.workerData ?? null;
20
- export const threadId: number = _ctx?.threadId ?? 0;
21
- export const resourceLimits: Record<string, unknown> = {};
22
- export const SHARE_ENV: unique symbol = Symbol('worker_threads.SHARE_ENV');
23
-
24
- export class MessageChannel {
25
- readonly port1: MessagePort;
26
- readonly port2: MessagePort;
27
-
28
- constructor() {
29
- this.port1 = new MessagePort();
30
- this.port2 = new MessagePort();
31
- this.port1._otherPort = this.port2;
32
- this.port2._otherPort = this.port1;
33
- }
34
- }
35
-
36
- const _broadcastRegistry = new Map<string, Set<BroadcastChannel>>();
37
-
38
- export class BroadcastChannel {
39
- readonly name: string;
40
- private _closed = false;
41
- onmessage: ((event: { data: unknown }) => void) | null = null;
42
- onmessageerror: ((event: { data: unknown }) => void) | null = null;
43
-
44
- constructor(name: string) {
45
- this.name = String(name);
46
- if (!_broadcastRegistry.has(this.name)) {
47
- _broadcastRegistry.set(this.name, new Set());
48
- }
49
- _broadcastRegistry.get(this.name)!.add(this);
50
- }
51
-
52
- postMessage(message: unknown): void {
53
- if (this._closed) {
54
- throw new Error('BroadcastChannel is closed');
55
- }
56
-
57
- const channels = _broadcastRegistry.get(this.name);
58
- if (!channels) return;
59
-
60
- for (const channel of channels) {
61
- if (channel !== this && !channel._closed) {
62
- // Clone per receiver per W3C spec — each recipient gets an independent copy
63
- const cloned = structuredClone(message);
64
- Promise.resolve().then(() => {
65
- if (!channel._closed && channel.onmessage) {
66
- channel.onmessage({ data: cloned });
67
- }
68
- });
69
- }
70
- }
71
- }
72
-
73
- close(): void {
74
- if (this._closed) return;
75
- this._closed = true;
76
- const channels = _broadcastRegistry.get(this.name);
77
- if (channels) {
78
- channels.delete(this);
79
- if (channels.size === 0) {
80
- _broadcastRegistry.delete(this.name);
81
- }
82
- }
83
- this.onmessage = null;
84
- this.onmessageerror = null;
85
- }
86
-
87
- addEventListener(type: string, listener: ((event: unknown) => void) | null): void {
88
- if (type === 'message') {
89
- this.onmessage = listener as ((event: { data: unknown }) => void) | null;
90
- } else if (type === 'messageerror') {
91
- this.onmessageerror = listener as ((event: { data: unknown }) => void) | null;
92
- }
93
- }
94
-
95
- removeEventListener(type: string, _listener: ((event: unknown) => void) | null): void {
96
- if (type === 'message') this.onmessage = null;
97
- else if (type === 'messageerror') this.onmessageerror = null;
98
- }
99
-
100
- dispatchEvent(_event: Event): boolean {
101
- return true;
102
- }
103
- }
104
-
105
- const _environmentData = new Map<string, unknown>();
106
-
107
- export function setEnvironmentData(key: string, value: unknown): void {
108
- if (value === undefined) {
109
- _environmentData.delete(key);
110
- } else {
111
- _environmentData.set(key, value);
112
- }
113
- }
114
-
115
- export function getEnvironmentData(key: string): unknown {
116
- return _environmentData.get(key);
117
- }
118
-
119
- export function receiveMessageOnPort(port: MessagePort): { message: unknown } | undefined {
120
- if (!port._hasQueuedMessages) return undefined;
121
- return { message: port._dequeueMessage() };
122
- }
123
-
124
- // No-ops in GJS — all messages are cloned, not transferred
125
- export function markAsUntransferable(_object: unknown): void {}
126
- export function markAsUncloneable(_object: unknown): void {}
127
- export function moveMessagePortToContext(port: MessagePort, _context: unknown): MessagePort {
128
- return port;
129
- }
130
-
131
- export default {
132
- isMainThread,
133
- parentPort,
134
- workerData,
135
- threadId,
136
- resourceLimits,
137
- SHARE_ENV,
138
- Worker,
139
- MessageChannel,
140
- MessagePort,
141
- BroadcastChannel,
142
- setEnvironmentData,
143
- getEnvironmentData,
144
- receiveMessageOnPort,
145
- markAsUntransferable,
146
- markAsUncloneable,
147
- moveMessagePortToContext,
148
- };
@@ -1,365 +0,0 @@
1
- // Reference: Node.js lib/internal/worker/io.js
2
- // Reference: HTML Living Standard §9.4.4 message ports + transferable objects
3
- // Reference: refs/node-test/parallel/test-worker-message-port-transfer*.js
4
- // Reimplemented for GJS using EventEmitter
5
- //
6
- // Transferable support:
7
- // - ArrayBuffer transfer uses the structured-clone layer's transfer hook
8
- // (SM140 ArrayBuffer.prototype.transfer() — zero-copy, sender detaches).
9
- // - MessagePort transfer is handled at this layer: a MessagePort listed in
10
- // transferList is detached from its current channel (close locally) and
11
- // re-attached on the receiver side. Wire format: a placeholder
12
- // `{ __gjsifyTransferredPort: true, queue: [...], hasOtherEnd: bool }`
13
- // is substituted into the cloned message tree, then materialised back into
14
- // a MessagePort on the receiver. Because the underlying linked-port pair
15
- // is in-process JS (no IPC), we move the surviving end of the channel
16
- // directly — there is no separate serialization step.
17
- //
18
- // Cross-process MessagePort transfer (i.e. via Worker subprocess IPC) is not
19
- // supported — see STATUS.md "Open TODOs".
20
-
21
- import { EventEmitter } from 'node:events';
22
-
23
- /**
24
- * Internal placeholder used while serializing a transferred MessagePort.
25
- * Replaced with a fresh local MessagePort by `_finalizeTransfer` after
26
- * structuredClone returns.
27
- */
28
- interface TransferredPortPlaceholder {
29
- readonly __gjsifyTransferredPort: true;
30
- /** Index into the materialisation array — unique per transfer. */
31
- readonly index: number;
32
- }
33
-
34
- function isTransferredPortPlaceholder(value: unknown): value is TransferredPortPlaceholder {
35
- return (
36
- typeof value === 'object'
37
- && value !== null
38
- && (value as { __gjsifyTransferredPort?: unknown }).__gjsifyTransferredPort === true
39
- );
40
- }
41
-
42
- export class MessagePort extends EventEmitter {
43
- private _started = false;
44
- private _closed = false;
45
- private _detached = false;
46
- private _messageQueue: unknown[] = [];
47
- /** @internal Linked port for in-process communication */
48
- _otherPort: MessagePort | null = null;
49
- /** @internal Maps addEventListener listeners to their internal wrappers */
50
- private _aeWrappers: Map<((event: unknown) => void), ((data: unknown) => void)> = new Map();
51
-
52
- start(): void {
53
- if (this._started || this._closed) return;
54
- this._started = true;
55
- this._drainQueue();
56
- }
57
-
58
- close(): void {
59
- if (this._closed) return;
60
- this._closed = true;
61
- const other = this._otherPort;
62
- this._otherPort = null;
63
- if (other) other._otherPort = null;
64
- this.emit('close');
65
- this.removeAllListeners();
66
- }
67
-
68
- postMessage(value: unknown, transferList?: unknown[]): void {
69
- if (this._closed) return;
70
- const target = this._otherPort;
71
- if (!target) return;
72
-
73
- // --- Transfer-list pre-flight ---
74
- // Validate transferable entries up front. Per HTML spec, validation must
75
- // happen before any side effects (no partial transfer on error).
76
- const arrayBufferTransfers: ArrayBuffer[] = [];
77
- const portTransfers: MessagePort[] = [];
78
-
79
- if (transferList && transferList.length > 0) {
80
- const seenInList = new Set<unknown>();
81
- for (const item of transferList) {
82
- if (seenInList.has(item)) {
83
- throw createDataCloneError('Transfer list contains duplicate entries');
84
- }
85
- seenInList.add(item);
86
-
87
- if (item instanceof MessagePort) {
88
- if (item === this) {
89
- throw createDataCloneError('Cannot transfer source port');
90
- }
91
- if (item._closed || item._detached) {
92
- throw createDataCloneError('MessagePort in transfer list is already detached');
93
- }
94
- portTransfers.push(item);
95
- continue;
96
- }
97
-
98
- // ArrayBuffer
99
- const tag = Object.prototype.toString.call(item).slice(8, -1);
100
- if (tag === 'ArrayBuffer') {
101
- const buf = item as ArrayBuffer & { detached?: boolean };
102
- if (buf.detached === true) {
103
- throw createDataCloneError('ArrayBuffer in transfer list is detached');
104
- }
105
- arrayBufferTransfers.push(buf);
106
- continue;
107
- }
108
-
109
- // SharedArrayBuffer must NOT appear in transfer list — it shares.
110
- if (tag === 'SharedArrayBuffer') {
111
- throw createDataCloneError('SharedArrayBuffer cannot appear in transfer list (it is shared, not transferred)');
112
- }
113
-
114
- throw createDataCloneError(`Value at index ${transferList.indexOf(item)} of transfer list is not transferable`);
115
- }
116
- }
117
-
118
- // --- Substitute MessagePort placeholders before clone ---
119
- // The structured-clone layer doesn't know about MessagePort; we walk the
120
- // value tree and replace each transferred port instance with a placeholder
121
- // (so the cloned tree reuses the same placeholder objects), then swap the
122
- // placeholders back to fresh local MessagePorts on the receiver side.
123
- const portMaterialisationOrder = portTransfers.slice();
124
- let substituted: unknown = value;
125
- if (portTransfers.length > 0) {
126
- substituted = substitutePortsWithPlaceholders(value, portTransfers);
127
- }
128
-
129
- // --- Clone (with ArrayBuffer transfer) ---
130
- let cloned: unknown;
131
- try {
132
- cloned = structuredClone(substituted, {
133
- transfer: arrayBufferTransfers.length > 0 ? arrayBufferTransfers : undefined,
134
- });
135
- } catch (err) {
136
- this.emit('messageerror', err instanceof Error ? err : new Error('Could not clone message'));
137
- return;
138
- }
139
-
140
- // --- Materialise transferred MessagePorts on the receiver ---
141
- // For each transferred port: detach the source MessagePort locally, then
142
- // create a new MessagePort on the receiver side that takes over the
143
- // surviving end of the channel.
144
- let receiverMessage = cloned;
145
- if (portMaterialisationOrder.length > 0) {
146
- const newPorts = portMaterialisationOrder.map((sourcePort) => {
147
- // The source port is being moved. Steal its channel partner.
148
- const partner = sourcePort._otherPort;
149
- const queued = sourcePort._messageQueue.slice();
150
- sourcePort._messageQueue.length = 0;
151
- sourcePort._otherPort = null;
152
- sourcePort._detached = true;
153
- // Mark closed locally — the original port is no longer usable.
154
- sourcePort._closed = true;
155
-
156
- const newPort = new MessagePort();
157
- newPort._otherPort = partner;
158
- if (partner) partner._otherPort = newPort;
159
- // Carry over any pending messages that the sourcePort had not drained yet.
160
- for (const msg of queued) newPort._messageQueue.push(msg);
161
- return newPort;
162
- });
163
- receiverMessage = replacePlaceholdersWithPorts(cloned, newPorts);
164
- }
165
-
166
- target._receiveMessage(receiverMessage);
167
- }
168
-
169
- ref(): this { return this; }
170
- unref(): this { return this; }
171
-
172
- _receiveMessage(message: unknown): void {
173
- if (this._closed) return;
174
- if (!this._started) {
175
- this._messageQueue.push(message);
176
- return;
177
- }
178
- this._dispatchMessage(message);
179
- }
180
-
181
- get _hasQueuedMessages(): boolean {
182
- return this._messageQueue.length > 0;
183
- }
184
-
185
- _dequeueMessage(): unknown | undefined {
186
- return this._messageQueue.shift();
187
- }
188
-
189
- /** @internal Has this port been transferred elsewhere? */
190
- get _wasTransferred(): boolean {
191
- return this._detached;
192
- }
193
-
194
- private _drainQueue(): void {
195
- while (this._messageQueue.length > 0) {
196
- this._dispatchMessage(this._messageQueue.shift());
197
- }
198
- }
199
-
200
- private _dispatchMessage(message: unknown): void {
201
- Promise.resolve().then(() => {
202
- if (!this._closed) {
203
- this.emit('message', message);
204
- }
205
- });
206
- }
207
-
208
- /**
209
- * Web-compatible addEventListener. Wraps message data in a MessageEvent-like
210
- * object `{ data, type }` before calling the listener.
211
- * Requires explicit `port.start()` call (unlike `on('message')` which auto-starts).
212
- */
213
- addEventListener(type: string, listener: ((event: unknown) => void) | null): void {
214
- if (!listener) return;
215
- if (type === 'message') {
216
- const wrapper = (data: unknown) => {
217
- listener({ data, type: 'message' });
218
- };
219
- this._aeWrappers.set(listener, wrapper);
220
- super.on('message', wrapper);
221
- } else {
222
- super.on(type, listener);
223
- }
224
- }
225
-
226
- removeEventListener(type: string, listener: ((event: unknown) => void) | null): void {
227
- if (!listener) return;
228
- if (type === 'message') {
229
- const wrapper = this._aeWrappers.get(listener);
230
- if (wrapper) {
231
- super.off('message', wrapper);
232
- this._aeWrappers.delete(listener);
233
- }
234
- } else {
235
- super.off(type, listener);
236
- }
237
- }
238
-
239
- on(event: string | symbol, listener: (...args: unknown[]) => void): this {
240
- super.on(event, listener);
241
- if (event === 'message' && !this._started) {
242
- this.start();
243
- }
244
- return this;
245
- }
246
-
247
- addListener(event: string | symbol, listener: (...args: unknown[]) => void): this {
248
- return this.on(event, listener);
249
- }
250
-
251
- once(event: string | symbol, listener: (...args: unknown[]) => void): this {
252
- super.once(event, listener);
253
- if (event === 'message' && !this._started) {
254
- this.start();
255
- }
256
- return this;
257
- }
258
- }
259
-
260
- // --- Helpers ---
261
-
262
- function createDataCloneError(message: string): Error {
263
- const DOMExceptionCtor = (globalThis as Record<string, unknown>).DOMException as
264
- (new (message: string, name: string) => Error) | undefined;
265
- if (typeof DOMExceptionCtor === 'function') {
266
- const err = new DOMExceptionCtor(message, 'DataCloneError');
267
- // Ensure the `code` property matches Node/W3C (25 = DATA_CLONE_ERR).
268
- if ((err as { code?: number }).code === undefined) {
269
- try {
270
- Object.defineProperty(err, 'code', { value: 25, configurable: true });
271
- } catch { /* ignore */ }
272
- }
273
- return err;
274
- }
275
- const err = new Error(message);
276
- err.name = 'DataCloneError';
277
- (err as { code?: number }).code = 25;
278
- return err;
279
- }
280
-
281
- /**
282
- * Walk `value`, replacing any MessagePort listed in `ports` with a
283
- * placeholder. Returns the new tree (copy-on-substitute). Mutates nothing in
284
- * the input.
285
- *
286
- * Handles plain objects and arrays. Other tagged types (Map, Set, …) are
287
- * passed through unchanged — the structured clone step that follows will
288
- * handle them, and ports inside Maps/Sets are uncommon enough in practice
289
- * that we don't expand the walk for them. (Node accepts the same constraint.)
290
- */
291
- function substitutePortsWithPlaceholders(value: unknown, ports: MessagePort[]): unknown {
292
- const portToIndex = new Map<MessagePort, number>();
293
- for (let i = 0; i < ports.length; i++) portToIndex.set(ports[i]!, i);
294
-
295
- function walk(v: unknown, seen: Map<object, unknown>): unknown {
296
- if (v === null || typeof v !== 'object') return v;
297
-
298
- // MessagePort substitution
299
- if (v instanceof MessagePort) {
300
- const idx = portToIndex.get(v);
301
- if (idx === undefined) return v; // not transferred
302
- const placeholder: TransferredPortPlaceholder = { __gjsifyTransferredPort: true, index: idx };
303
- return placeholder;
304
- }
305
-
306
- if (seen.has(v)) return seen.get(v);
307
-
308
- if (Array.isArray(v)) {
309
- const out: unknown[] = [];
310
- seen.set(v, out);
311
- for (let i = 0; i < v.length; i++) {
312
- if (i in v) out[i] = walk(v[i], seen);
313
- }
314
- return out;
315
- }
316
-
317
- const tag = Object.prototype.toString.call(v).slice(8, -1);
318
- if (tag === 'Object') {
319
- const out: Record<string, unknown> = {};
320
- seen.set(v, out);
321
- for (const k of Object.keys(v as Record<string, unknown>)) {
322
- out[k] = walk((v as Record<string, unknown>)[k], seen);
323
- }
324
- return out;
325
- }
326
-
327
- // Other tagged types: leave intact for structuredClone to handle.
328
- return v;
329
- }
330
-
331
- return walk(value, new Map());
332
- }
333
-
334
- /**
335
- * Walk `value` post-clone and replace each placeholder with the corresponding
336
- * receiver-side MessagePort.
337
- */
338
- function replacePlaceholdersWithPorts(value: unknown, newPorts: MessagePort[]): unknown {
339
- function walk(v: unknown, seen: Map<object, unknown>): unknown {
340
- if (v === null || typeof v !== 'object') return v;
341
- if (isTransferredPortPlaceholder(v)) {
342
- return newPorts[v.index];
343
- }
344
- if (seen.has(v)) return seen.get(v);
345
-
346
- if (Array.isArray(v)) {
347
- seen.set(v, v);
348
- for (let i = 0; i < v.length; i++) {
349
- if (i in v) v[i] = walk(v[i], seen);
350
- }
351
- return v;
352
- }
353
-
354
- const tag = Object.prototype.toString.call(v).slice(8, -1);
355
- if (tag === 'Object') {
356
- seen.set(v, v);
357
- for (const k of Object.keys(v as Record<string, unknown>)) {
358
- (v as Record<string, unknown>)[k] = walk((v as Record<string, unknown>)[k], seen);
359
- }
360
- return v;
361
- }
362
- return v;
363
- }
364
- return walk(value, new Map());
365
- }
package/src/test.mts DELETED
@@ -1,8 +0,0 @@
1
- import '@gjsify/node-globals/register/process';
2
- import '@gjsify/node-globals/register/buffer';
3
- import '@gjsify/node-globals/register/timers';
4
- import '@gjsify/node-globals/register/url';
5
- import '@gjsify/node-globals/register/structured-clone';
6
- import { run } from '@gjsify/unit';
7
- import testSuite from './index.spec.js';
8
- run({ testSuite });