knitting 0.1.46 → 0.1.50

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.
Files changed (52) hide show
  1. package/README.md +274 -77
  2. package/map.md +9 -3
  3. package/package.json +2 -1
  4. package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
  5. package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
  6. package/prebuilds/darwin-x64-node-127/knitting_shared_memory.node +0 -0
  7. package/prebuilds/darwin-x64-node-137/knitting_shared_memory.node +0 -0
  8. package/prebuilds/linux-x64-node-127/knitting_shared_memory.node +0 -0
  9. package/prebuilds/linux-x64-node-137/knitting_shared_memory.node +0 -0
  10. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  11. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  12. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  13. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  14. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  15. package/scripts/build-native-addons.ts +31 -5
  16. package/src/api.js +37 -13
  17. package/src/common/task-source.d.ts +1 -0
  18. package/src/common/task-source.js +1 -0
  19. package/src/connections/bun.d.ts +2 -0
  20. package/src/connections/bun.js +64 -9
  21. package/src/connections/deno.d.ts +2 -0
  22. package/src/connections/deno.js +64 -9
  23. package/src/connections/file-descriptor.d.ts +2 -0
  24. package/src/connections/file-descriptor.js +24 -2
  25. package/src/connections/node.d.ts +2 -1
  26. package/src/connections/node.js +17 -14
  27. package/src/connections/posix.d.ts +1 -0
  28. package/src/connections/posix.js +6 -0
  29. package/src/connections/process-shared-buffer.d.ts +3 -1
  30. package/src/connections/process-shared-buffer.js +17 -13
  31. package/src/connections/types.d.ts +2 -0
  32. package/src/connections/windows.d.ts +28 -0
  33. package/src/connections/windows.js +224 -0
  34. package/src/knitting_shared_memory.cc +310 -24
  35. package/src/knitting_windows_shared_memory.cc +156 -0
  36. package/src/memory/payloadCodec.js +1 -0
  37. package/src/runtime/pool.d.ts +1 -13
  38. package/src/runtime/pool.js +10 -542
  39. package/src/runtime/process-worker.d.ts +92 -0
  40. package/src/runtime/process-worker.js +670 -0
  41. package/src/runtime/tx-queue.d.ts +1 -1
  42. package/src/runtime/tx-queue.js +3 -1
  43. package/src/shared/abortSignal.d.ts +1 -1
  44. package/src/shared/abortSignal.js +10 -2
  45. package/src/types.d.ts +49 -3
  46. package/src/worker/bootstrap.d.ts +5 -0
  47. package/src/worker/bootstrap.js +78 -0
  48. package/src/worker/composable-runners.js +0 -8
  49. package/src/worker/loop.js +19 -154
  50. package/src/worker/process-worker-bootstrap.d.ts +8 -0
  51. package/src/worker/process-worker-bootstrap.js +159 -0
  52. package/src/worker/timers.js +19 -5
@@ -111,7 +111,7 @@ export const signalAbortFactory = ({ sab, maxSignals, }) => {
111
111
  };
112
112
  export class OneShotDeferred {
113
113
  #triggered = false;
114
- constructor(deferred, onSettle) {
114
+ constructor(deferred, onSettle, onEmptyReject) {
115
115
  const settleOnce = (fn) => (...args) => {
116
116
  if (this.#triggered)
117
117
  return;
@@ -121,6 +121,14 @@ export class OneShotDeferred {
121
121
  };
122
122
  deferred.resolve = settleOnce(deferred.resolve);
123
123
  deferred.reject = settleOnce(deferred.reject);
124
- deferred.promise.reject = deferred.reject;
124
+ deferred.promise.reject = (reason) => {
125
+ if (this.#triggered)
126
+ return;
127
+ if (reason === undefined && onEmptyReject !== undefined) {
128
+ onEmptyReject();
129
+ return;
130
+ }
131
+ deferred.reject(reason);
132
+ };
125
133
  }
126
134
  }
package/src/types.d.ts CHANGED
@@ -164,8 +164,34 @@ type DebugOptions = {
164
164
  logHref?: boolean;
165
165
  logImportedUrl?: boolean;
166
166
  };
167
+ type WorkerBootstrapContext = {
168
+ readonly thread: number;
169
+ readonly totalNumberOfThread: number;
170
+ readonly runtime: "node" | "deno" | "bun" | "unknown";
171
+ };
172
+ type WorkerBootstrapFunction<Data = unknown> = (data: Data, context: WorkerBootstrapContext) => MaybePromise<void>;
173
+ type WorkerBootstrapOptions<Data = unknown> = {
174
+ /**
175
+ * Module imported inside the worker before task modules are imported.
176
+ * Relative paths are resolved from the `createPool(...)` caller.
177
+ */
178
+ href: string;
179
+ /**
180
+ * Exported bootstrap function name. Defaults to `"default"`.
181
+ */
182
+ name?: string;
183
+ /**
184
+ * Structured data passed to the bootstrap function.
185
+ */
186
+ data?: Data;
187
+ };
167
188
  type WorkerSettings = {
168
189
  resolveAfterFinishingAll?: true;
190
+ /**
191
+ * Privileged async worker hook that runs once before task modules import.
192
+ * Use it to shape the worker environment before user task code loads.
193
+ */
194
+ bootstrap?: WorkerBootstrapOptions;
169
195
  /**
170
196
  * Experimental worker runtime.
171
197
  * "thread" uses Worker/worker_threads. "process" spawns another JavaScript
@@ -173,17 +199,31 @@ type WorkerSettings = {
173
199
  */
174
200
  runtime?: "thread" | "process";
175
201
  /**
176
- * Runtime executable to use when runtime is "process". Defaults to "bun".
202
+ * Runtime executable to use when runtime is "process". Defaults to "deno".
177
203
  */
178
204
  processRuntime?: "bun" | "deno" | "node";
179
205
  /**
180
206
  * Command argv to prepend before the process worker runtime command.
181
- * Useful for wrappers such as systemd-run, cgexec, nice, or taskset.
207
+ * Useful for wrappers such as systemd-run, cgexec, nice, taskset, or
208
+ * docker. Knitting appends the runtime command after this prefix.
182
209
  *
183
210
  * Example:
184
211
  * ["systemd-run", "--scope", "-p", "MemoryMax=500M", "-p", "CPUQuota=25%"]
212
+ *
213
+ * With containers, use processSharedMemory: "named", share the IPC
214
+ * namespace, mount the worker files at the same path, and forward
215
+ * KNITTING_PROCESS_WORKER plus KNITTING_PROCESS_WORKER_BOOT.
185
216
  */
186
217
  processCommandPrefix?: string[];
218
+ /**
219
+ * How process workers discover their shared-memory control channel.
220
+ *
221
+ * "inherit" keeps the POSIX fd-inheritance path and is the default outside
222
+ * Windows. "named" creates an OS-named shared-memory object that wrappers
223
+ * such as containers can reopen by name when they share the same IPC
224
+ * namespace.
225
+ */
226
+ processSharedMemory?: ProcessSharedMemoryMode | ProcessSharedMemorySettings;
187
227
  timers?: WorkerTimers;
188
228
  /**
189
229
  * Hard task execution timeout in milliseconds.
@@ -191,6 +231,12 @@ type WorkerSettings = {
191
231
  */
192
232
  hardTimeoutMs?: number;
193
233
  };
234
+ type ProcessSharedMemoryMode = "inherit" | "named";
235
+ type ProcessSharedMemorySettings = {
236
+ mode?: ProcessSharedMemoryMode;
237
+ namePrefix?: string;
238
+ unlinkOnShutdown?: boolean;
239
+ };
194
240
  type WorkerTimers = {
195
241
  /**
196
242
  * Busy-spin budget before parking (microseconds).
@@ -277,7 +323,7 @@ type CreatePool = {
277
323
  debug?: DebugOptions;
278
324
  source?: string;
279
325
  };
280
- export type { WorkerCall as WorkerCall, WorkerInvoke as WorkerInvoke, WorkerContext as WorkerContext, CreateContext as CreateContext, WorkerData as WorkerData, LockBuffers as LockBuffers, ValidInput as ValidInput, Args as Args, MaybePromise as MaybePromise, TaskInput as TaskInput, TaskTimeout as TaskTimeout, TaskFn as TaskFn, AbortSignalConfig as AbortSignalConfig, AbortSignalOption as AbortSignalOption, AbortSignalMethods as AbortSignalMethods, AbortSignalToolkit as AbortSignalToolkit, Composed as Composed, tasks as tasks, ComposedWithKey as ComposedWithKey, FunctionMapType as FunctionMapType, FixPoint as FixPoint, ImportTaskOptions as ImportTaskOptions, SecondPart as SecondPart, SingleTaskPool as SingleTaskPool, Pool as Pool, ReturnFixed as ReturnFixed, External as External, Inliner as Inliner, BalancerStrategy as BalancerStrategy, Balancer as Balancer, DebugOptions as DebugOptions, WorkerSettings as WorkerSettings, WorkerTimers as WorkerTimers, DispatcherSettings as DispatcherSettings, CreatePool as CreatePool, PayloadBufferMode as PayloadBufferMode, PayloadBufferOptions as PayloadBufferOptions, PermissionProtocol as PermissionProtocol, PermissionProtocolInput as PermissionProtocolInput, ResolvedPermissionProtocol as ResolvedPermissionProtocol, Envelope as Envelope, SharedBufferTextCompat as SharedBufferTextCompat, LockBufferTextCompat as LockBufferTextCompat, SharedBufferRegion as SharedBufferRegion, SharedBufferSource as SharedBufferSource, };
326
+ export type { WorkerCall as WorkerCall, WorkerInvoke as WorkerInvoke, WorkerContext as WorkerContext, CreateContext as CreateContext, WorkerData as WorkerData, LockBuffers as LockBuffers, ValidInput as ValidInput, Args as Args, MaybePromise as MaybePromise, TaskInput as TaskInput, TaskTimeout as TaskTimeout, TaskFn as TaskFn, AbortSignalConfig as AbortSignalConfig, AbortSignalOption as AbortSignalOption, AbortSignalMethods as AbortSignalMethods, AbortSignalToolkit as AbortSignalToolkit, Composed as Composed, tasks as tasks, ComposedWithKey as ComposedWithKey, FunctionMapType as FunctionMapType, FixPoint as FixPoint, ImportTaskOptions as ImportTaskOptions, SecondPart as SecondPart, SingleTaskPool as SingleTaskPool, Pool as Pool, ReturnFixed as ReturnFixed, External as External, Inliner as Inliner, BalancerStrategy as BalancerStrategy, Balancer as Balancer, DebugOptions as DebugOptions, WorkerBootstrapContext as WorkerBootstrapContext, WorkerBootstrapFunction as WorkerBootstrapFunction, WorkerBootstrapOptions as WorkerBootstrapOptions, WorkerSettings as WorkerSettings, ProcessSharedMemoryMode as ProcessSharedMemoryMode, ProcessSharedMemorySettings as ProcessSharedMemorySettings, WorkerTimers as WorkerTimers, DispatcherSettings as DispatcherSettings, CreatePool as CreatePool, PayloadBufferMode as PayloadBufferMode, PayloadBufferOptions as PayloadBufferOptions, PermissionProtocol as PermissionProtocol, PermissionProtocolInput as PermissionProtocolInput, ResolvedPermissionProtocol as ResolvedPermissionProtocol, Envelope as Envelope, SharedBufferTextCompat as SharedBufferTextCompat, LockBufferTextCompat as LockBufferTextCompat, SharedBufferRegion as SharedBufferRegion, SharedBufferSource as SharedBufferSource, };
281
327
  export type { Task as Task } from "./memory/lock.js";
282
328
  export { LockBound as LockBound, PayloadBuffer as PayloadBuffer, PayloadSignal as PayloadSignal, TaskIndex as TaskIndex, } from "./memory/lock.js";
283
329
  export type { RegisterMalloc as RegisterMalloc } from "./memory/regionRegistry.js";
@@ -0,0 +1,5 @@
1
+ export declare const runWorkerBootstrap: ({ bootstrap, thread, totalNumberOfThread, }: {
2
+ bootstrap: unknown;
3
+ thread: number;
4
+ totalNumberOfThread: number;
5
+ }) => Promise<void>;
@@ -0,0 +1,78 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { RUNTIME } from "../common/runtime.js";
10
+ import { toModuleUrl } from "../common/module-url.js";
11
+ import { ProcessSharedBuffer } from "../connections/process-shared-buffer.js";
12
+ const DEFAULT_BOOTSTRAP_EXPORT_NAME = "default";
13
+ const isWorkerBootstrapOptions = (value) => {
14
+ if (!value || typeof value !== "object")
15
+ return false;
16
+ const candidate = value;
17
+ return typeof candidate.href === "string" &&
18
+ candidate.href.length > 0 &&
19
+ (candidate.name === undefined ||
20
+ (typeof candidate.name === "string" && candidate.name.length > 0));
21
+ };
22
+ const isProcessSharedBufferMetadata = (value) => {
23
+ try {
24
+ ProcessSharedBuffer.fromMetadata(value);
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ };
31
+ const reviveWorkerBootstrapValue = (value, seen = new WeakMap()) => {
32
+ if (isProcessSharedBufferMetadata(value)) {
33
+ return ProcessSharedBuffer.fromMetadata(value);
34
+ }
35
+ if (value === null || typeof value !== "object")
36
+ return value;
37
+ const existing = seen.get(value);
38
+ if (existing !== undefined)
39
+ return existing;
40
+ if (Array.isArray(value)) {
41
+ const out = [];
42
+ seen.set(value, out);
43
+ for (const item of value) {
44
+ out.push(reviveWorkerBootstrapValue(item, seen));
45
+ }
46
+ return out;
47
+ }
48
+ const prototype = Object.getPrototypeOf(value);
49
+ if (prototype !== Object.prototype && prototype !== null)
50
+ return value;
51
+ const out = {};
52
+ seen.set(value, out);
53
+ for (const [key, item] of Object.entries(value)) {
54
+ out[key] = reviveWorkerBootstrapValue(item, seen);
55
+ }
56
+ return out;
57
+ };
58
+ export const runWorkerBootstrap = async ({ bootstrap, thread, totalNumberOfThread, }) => {
59
+ if (bootstrap === undefined)
60
+ return;
61
+ if (!isWorkerBootstrapOptions(bootstrap)) {
62
+ throw new TypeError("worker.bootstrap must include a non-empty href");
63
+ }
64
+ const module = await import(__rewriteRelativeImportExtension(toModuleUrl(bootstrap.href)));
65
+ const name = bootstrap.name ?? DEFAULT_BOOTSTRAP_EXPORT_NAME;
66
+ const selected = module[name];
67
+ if (typeof selected !== "function") {
68
+ const available = Object.keys(module).join(", ");
69
+ throw new TypeError(`worker.bootstrap expected export "${name}" from "${bootstrap.href}" to be a function.` +
70
+ ` Available exports: ${available || "(none)"}`);
71
+ }
72
+ const context = Object.freeze({
73
+ thread,
74
+ totalNumberOfThread,
75
+ runtime: RUNTIME,
76
+ });
77
+ await selected(reviveWorkerBootstrapValue(bootstrap.data), context);
78
+ };
@@ -49,12 +49,6 @@ const readSignal = (slot) => {
49
49
  const signal = (encodedSignal - ABORT_SIGNAL_META_OFFSET) | 0;
50
50
  return signal >= 0 ? signal : NO_ABORT_SIGNAL;
51
51
  };
52
- const throwIfAborted = (signal, hasAborted) => {
53
- if (signal === NO_ABORT_SIGNAL)
54
- return;
55
- if (hasAborted(signal))
56
- throw new Error("Task aborted");
57
- };
58
52
  const makeToolkitCache = (hasAborted) => {
59
53
  const bySignal = [];
60
54
  return (signal) => {
@@ -86,7 +80,6 @@ export const composeWorkerRunner = ({ job, timeout, hasAborted, now, }) => {
86
80
  if (!timeout) {
87
81
  return (slot) => {
88
82
  const signal = readSignal(slot);
89
- throwIfAborted(signal, hasAborted);
90
83
  if (signal === NO_ABORT_SIGNAL)
91
84
  return job(slot.value);
92
85
  return job(slot.value, getToolkit(signal));
@@ -94,7 +87,6 @@ export const composeWorkerRunner = ({ job, timeout, hasAborted, now, }) => {
94
87
  }
95
88
  return (slot) => {
96
89
  const signal = readSignal(slot);
97
- throwIfAborted(signal, hasAborted);
98
90
  const result = signal === NO_ABORT_SIGNAL
99
91
  ? job(slot.value)
100
92
  : job(slot.value, getToolkit(signal));
@@ -1,30 +1,18 @@
1
- import { RUNTIME_IS_MAIN_THREAD, RUNTIME_IS_PROCESS_WORKER, RUNTIME_PARENT_PORT, RUNTIME_PROCESS_WORKER_BOOT_ENV, RUNTIME_PROCESS_WORKER_BOOT_VERSION, RUNTIME_WORKER_DATA, createRuntimeMessageChannel, } from "../common/worker-runtime.js";
2
- import { isSharedBufferSource, } from "../common/shared-buffer-region.js";
1
+ import { createRuntimeMessageChannel, RUNTIME_IS_MAIN_THREAD, RUNTIME_IS_PROCESS_WORKER, RUNTIME_PARENT_PORT, RUNTIME_WORKER_DATA, } from "../common/worker-runtime.js";
2
+ import { isSharedBufferSource } from "../common/shared-buffer-region.js";
3
3
  import { isLockBufferTextCompat } from "../common/shared-buffer-text.js";
4
4
  import { createWorkerRxQueue } from "./rx-queue.js";
5
- import { createSharedMemoryTransport, } from "../ipc/transport/shared-memory.js";
5
+ import { createSharedMemoryTransport } from "../ipc/transport/shared-memory.js";
6
6
  import { lock2 } from "../memory/lock.js";
7
7
  import { getFunctions } from "./task-loader.js";
8
- import { pauseGeneric, sleepUntilChanged, whilePausing, } from "./timers.js";
8
+ import { pauseGeneric, sleepUntilChanged, whilePausing } from "./timers.js";
9
9
  import { RUNTIME, SET_IMMEDIATE } from "../common/runtime.js";
10
10
  import { getNodeProcess } from "../common/node-compat.js";
11
- import { getDefaultProcessSharedBufferPrimitives, ProcessSharedBuffer, setDefaultProcessSharedBufferPrimitives, } from "../connections/process-shared-buffer.js";
12
- import { createBunConnectionPrimitives } from "../connections/bun.js";
13
- import { createDenoConnectionPrimitives } from "../connections/deno.js";
14
- import { loadNodeFutexAddon } from "../connections/node.js";
15
- import { installTerminationGuard, installUnhandledRejectionSilencer, installPerformanceNowGuard, scrubWorkerDataSensitiveBuffers, assertWorkerSharedMemoryBootData, assertWorkerImportsResolved, } from "./safety/index.js";
11
+ import { assertWorkerImportsResolved, assertWorkerSharedMemoryBootData, installPerformanceNowGuard, installTerminationGuard, installUnhandledRejectionSilencer, scrubWorkerDataSensitiveBuffers, } from "./safety/index.js";
16
12
  import { signalAbortFactory } from "../shared/abortSignal.js";
13
+ import { runWorkerBootstrap } from "./bootstrap.js";
14
+ import { getProcessWorkerNativeWaitU32, installProcessWorkerBootstrap, } from "./process-worker-bootstrap.js";
17
15
  const WORKER_FATAL_MESSAGE_KEY = "__knittingWorkerFatal";
18
- const getProcessWorkerNativeWaitU32 = () => {
19
- if (!RUNTIME_IS_PROCESS_WORKER || RUNTIME !== "node")
20
- return undefined;
21
- try {
22
- return loadNodeFutexAddon().waitU32;
23
- }
24
- catch {
25
- return undefined;
26
- }
27
- };
28
16
  const reportWorkerStartupFatal = (error) => {
29
17
  const message = String(error?.message ?? error);
30
18
  const payload = {
@@ -114,6 +102,11 @@ export const workerMainLoop = async (startupData) => {
114
102
  const a_store = Atomics.store;
115
103
  const a_load = Atomics.load;
116
104
  const nativeWaitU32 = getProcessWorkerNativeWaitU32();
105
+ await runWorkerBootstrap({
106
+ bootstrap: workerOptions?.bootstrap,
107
+ thread,
108
+ totalNumberOfThread,
109
+ });
117
110
  const listOfFunctions = await getFunctions({
118
111
  list,
119
112
  isWorker: true,
@@ -307,146 +300,18 @@ const installWorkerGlobalBootstrap = () => {
307
300
  start(data);
308
301
  };
309
302
  };
310
- const isProcessSharedBufferMetadata = (value) => {
311
- try {
312
- ProcessSharedBuffer.fromMetadata(value);
313
- return true;
314
- }
315
- catch {
316
- return false;
317
- }
318
- };
319
- const isProcessWorkerWireLockBuffers = (value) => {
320
- if (!value || typeof value !== "object")
321
- return false;
322
- const candidate = value;
323
- return isProcessSharedBufferMetadata(candidate.headers) &&
324
- isProcessSharedBufferMetadata(candidate.lockSector) &&
325
- isProcessSharedBufferMetadata(candidate.payload) &&
326
- isProcessSharedBufferMetadata(candidate.payloadSector) &&
327
- (candidate.textCompat === undefined ||
328
- isLockBufferTextCompat(candidate.textCompat));
329
- };
330
- const isProcessWorkerBootPayload = (value) => {
331
- if (!value || typeof value !== "object")
332
- return false;
333
- const candidate = value;
334
- const workerData = candidate.workerData;
335
- return candidate.version === RUNTIME_PROCESS_WORKER_BOOT_VERSION &&
336
- !!workerData &&
337
- isProcessSharedBufferMetadata(workerData.sab) &&
338
- Array.isArray(workerData.list) &&
339
- Array.isArray(workerData.ids) &&
340
- Array.isArray(workerData.at) &&
341
- typeof workerData.thread === "number" &&
342
- typeof workerData.totalNumberOfThread === "number" &&
343
- typeof workerData.startAt === "number" &&
344
- (workerData.abortSignalSAB === undefined ||
345
- isProcessSharedBufferMetadata(workerData.abortSignalSAB)) &&
346
- isProcessWorkerWireLockBuffers(workerData.lock) &&
347
- isProcessWorkerWireLockBuffers(workerData.returnLock);
348
- };
349
- const getProcessWorkerPrimitives = () => {
350
- const primitives = RUNTIME === "bun"
351
- ? createBunConnectionPrimitives()
352
- : RUNTIME === "deno"
353
- ? createDenoConnectionPrimitives()
354
- : getDefaultProcessSharedBufferPrimitives();
355
- setDefaultProcessSharedBufferPrimitives(primitives);
356
- return primitives;
357
- };
358
- const reviveProcessWorkerData = (wire) => {
359
- const primitives = getProcessWorkerPrimitives();
360
- const mappings = new Map();
361
- const reviveRegion = (metadata) => {
362
- const processBuffer = ProcessSharedBuffer.fromMetadata(metadata);
363
- const descriptor = processBuffer.descriptor;
364
- const key = `${descriptor.fd}:${descriptor.size}:${descriptor.runtime ?? ""}`;
365
- let mapping = mappings.get(key);
366
- if (mapping === undefined) {
367
- mapping = descriptor.map(primitives);
368
- mappings.set(key, mapping);
369
- }
370
- else {
371
- descriptor.attach(mapping);
372
- }
373
- return {
374
- sab: mapping.buffer,
375
- byteOffset: processBuffer.byteOffset,
376
- byteLength: processBuffer.byteLength,
377
- };
378
- };
379
- const reviveLockBuffers = (lock) => ({
380
- ...lock,
381
- headers: reviveRegion(lock.headers),
382
- lockSector: reviveRegion(lock.lockSector),
383
- payload: reviveRegion(lock.payload),
384
- payloadSector: reviveRegion(lock.payloadSector),
385
- });
386
- return {
387
- ...wire,
388
- sab: reviveRegion(wire.sab),
389
- abortSignalSAB: wire.abortSignalSAB === undefined
390
- ? undefined
391
- : reviveRegion(wire.abortSignalSAB),
392
- lock: reviveLockBuffers(wire.lock),
393
- returnLock: reviveLockBuffers(wire.returnLock),
394
- };
395
- };
396
- const installProcessWorkerBootstrap = () => {
397
- const processLike = getNodeProcess();
398
- const start = (payload) => {
399
- const data = reviveProcessWorkerData(payload.workerData);
400
- if (!isWorkerBootPayload(data)) {
401
- throw new TypeError("invalid process worker boot payload");
402
- }
403
- void workerMainLoop(data).catch(reportWorkerStartupFatal);
404
- };
405
- const envBoot = processLike?.env?.[RUNTIME_PROCESS_WORKER_BOOT_ENV];
406
- if (typeof envBoot === "string" && envBoot.length > 0) {
407
- try {
408
- const payload = JSON.parse(envBoot);
409
- if (!isProcessWorkerBootPayload(payload)) {
410
- throw new TypeError("invalid process worker boot payload");
411
- }
412
- try {
413
- delete processLike?.env?.[RUNTIME_PROCESS_WORKER_BOOT_ENV];
414
- }
415
- catch {
416
- }
417
- start(payload);
418
- }
419
- catch (error) {
420
- reportWorkerStartupFatal(error);
421
- }
422
- return;
423
- }
424
- if (typeof processLike?.on !== "function")
425
- return;
426
- const onMessage = (message) => {
427
- if (!isProcessWorkerBootPayload(message))
428
- return;
429
- try {
430
- processLike.off?.("message", onMessage);
431
- processLike.removeListener?.("message", onMessage);
432
- }
433
- catch {
434
- }
435
- try {
436
- start(message);
437
- }
438
- catch (error) {
439
- reportWorkerStartupFatal(error);
440
- }
441
- };
442
- processLike.on("message", onMessage);
443
- };
444
303
  if (RUNTIME_IS_MAIN_THREAD === false &&
445
304
  isWorkerBootPayload(RUNTIME_WORKER_DATA)) {
446
305
  void workerMainLoop(RUNTIME_WORKER_DATA).catch(reportWorkerStartupFatal);
447
306
  }
448
307
  else if (RUNTIME_IS_PROCESS_WORKER) {
449
- installProcessWorkerBootstrap();
308
+ installProcessWorkerBootstrap({
309
+ isWorkerBootPayload,
310
+ reportWorkerStartupFatal,
311
+ startWorker: (data) => {
312
+ void workerMainLoop(data).catch(reportWorkerStartupFatal);
313
+ },
314
+ });
450
315
  }
451
316
  else if (isWorkerGlobalScope()) {
452
317
  installWorkerGlobalBootstrap();
@@ -0,0 +1,8 @@
1
+ import type { WorkerData } from "../types.js";
2
+ import type { NativeWaitU32 } from "./timers.js";
3
+ export declare const getProcessWorkerNativeWaitU32: () => NativeWaitU32 | undefined;
4
+ export declare const installProcessWorkerBootstrap: ({ isWorkerBootPayload, reportWorkerStartupFatal, startWorker, }: {
5
+ isWorkerBootPayload: (value: unknown) => value is WorkerData;
6
+ reportWorkerStartupFatal: (error: unknown) => void;
7
+ startWorker: (data: WorkerData) => void;
8
+ }) => void;
@@ -0,0 +1,159 @@
1
+ import { RUNTIME } from "../common/runtime.js";
2
+ import { RUNTIME_IS_PROCESS_WORKER, RUNTIME_PARENT_PORT, RUNTIME_PROCESS_WORKER_BOOT_ENV, RUNTIME_PROCESS_WORKER_BOOT_VERSION, } from "../common/worker-runtime.js";
3
+ import { getNodeProcess } from "../common/node-compat.js";
4
+ import { isLockBufferTextCompat } from "../common/shared-buffer-text.js";
5
+ import { getDefaultProcessSharedBufferPrimitives, ProcessSharedBuffer, setDefaultProcessSharedBufferPrimitives, } from "../connections/process-shared-buffer.js";
6
+ import { createBunConnectionPrimitives } from "../connections/bun.js";
7
+ import { createDenoConnectionPrimitives } from "../connections/deno.js";
8
+ import { loadNodeFutexAddon } from "../connections/node.js";
9
+ export const getProcessWorkerNativeWaitU32 = () => {
10
+ if (!RUNTIME_IS_PROCESS_WORKER || RUNTIME !== "node")
11
+ return undefined;
12
+ try {
13
+ return loadNodeFutexAddon().waitU32;
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ };
19
+ const isProcessSharedBufferMetadata = (value) => {
20
+ try {
21
+ ProcessSharedBuffer.fromMetadata(value);
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ };
28
+ const isProcessWorkerWireLockBuffers = (value) => {
29
+ if (!value || typeof value !== "object")
30
+ return false;
31
+ const candidate = value;
32
+ return isProcessSharedBufferMetadata(candidate.headers) &&
33
+ isProcessSharedBufferMetadata(candidate.lockSector) &&
34
+ isProcessSharedBufferMetadata(candidate.payload) &&
35
+ isProcessSharedBufferMetadata(candidate.payloadSector) &&
36
+ (candidate.textCompat === undefined ||
37
+ isLockBufferTextCompat(candidate.textCompat));
38
+ };
39
+ const isProcessWorkerBootPayload = (value) => {
40
+ if (!value || typeof value !== "object")
41
+ return false;
42
+ const candidate = value;
43
+ const workerData = candidate.workerData;
44
+ return candidate.version === RUNTIME_PROCESS_WORKER_BOOT_VERSION &&
45
+ !!workerData &&
46
+ isProcessSharedBufferMetadata(workerData.sab) &&
47
+ Array.isArray(workerData.list) &&
48
+ Array.isArray(workerData.ids) &&
49
+ Array.isArray(workerData.at) &&
50
+ typeof workerData.thread === "number" &&
51
+ typeof workerData.totalNumberOfThread === "number" &&
52
+ typeof workerData.startAt === "number" &&
53
+ (workerData.abortSignalSAB === undefined ||
54
+ isProcessSharedBufferMetadata(workerData.abortSignalSAB)) &&
55
+ isProcessWorkerWireLockBuffers(workerData.lock) &&
56
+ isProcessWorkerWireLockBuffers(workerData.returnLock);
57
+ };
58
+ const getProcessWorkerPrimitives = () => {
59
+ const primitives = RUNTIME === "bun"
60
+ ? createBunConnectionPrimitives()
61
+ : RUNTIME === "deno"
62
+ ? createDenoConnectionPrimitives()
63
+ : getDefaultProcessSharedBufferPrimitives();
64
+ setDefaultProcessSharedBufferPrimitives(primitives);
65
+ return primitives;
66
+ };
67
+ const reviveProcessWorkerData = (wire) => {
68
+ const primitives = getProcessWorkerPrimitives();
69
+ const mappings = new Map();
70
+ const mappingKey = (descriptor) => descriptor.name === undefined
71
+ ? `fd:${descriptor.fd}:${descriptor.size}:${descriptor.runtime ?? ""}`
72
+ : `name:${descriptor.name}:${descriptor.size}:${descriptor.runtime ?? ""}`;
73
+ const reviveRegion = (metadata) => {
74
+ const processBuffer = ProcessSharedBuffer.fromMetadata(metadata);
75
+ const descriptor = processBuffer.descriptor;
76
+ const key = mappingKey(descriptor);
77
+ let mapping = mappings.get(key);
78
+ if (mapping === undefined) {
79
+ mapping = descriptor.map(primitives);
80
+ mappings.set(key, mapping);
81
+ }
82
+ else {
83
+ descriptor.attach(mapping);
84
+ }
85
+ return {
86
+ sab: mapping.buffer,
87
+ byteOffset: processBuffer.byteOffset,
88
+ byteLength: processBuffer.byteLength,
89
+ };
90
+ };
91
+ const reviveLockBuffers = (lock) => ({
92
+ ...lock,
93
+ headers: reviveRegion(lock.headers),
94
+ lockSector: reviveRegion(lock.lockSector),
95
+ payload: reviveRegion(lock.payload),
96
+ payloadSector: reviveRegion(lock.payloadSector),
97
+ });
98
+ return {
99
+ ...wire,
100
+ sab: reviveRegion(wire.sab),
101
+ abortSignalSAB: wire.abortSignalSAB === undefined
102
+ ? undefined
103
+ : reviveRegion(wire.abortSignalSAB),
104
+ lock: reviveLockBuffers(wire.lock),
105
+ returnLock: reviveLockBuffers(wire.returnLock),
106
+ };
107
+ };
108
+ export const installProcessWorkerBootstrap = ({ isWorkerBootPayload, reportWorkerStartupFatal, startWorker, }) => {
109
+ const processLike = getNodeProcess();
110
+ const start = (payload) => {
111
+ const data = reviveProcessWorkerData(payload.workerData);
112
+ if (!isWorkerBootPayload(data)) {
113
+ throw new TypeError("invalid process worker boot payload");
114
+ }
115
+ startWorker(data);
116
+ };
117
+ const envBoot = processLike?.env?.[RUNTIME_PROCESS_WORKER_BOOT_ENV];
118
+ if (typeof envBoot === "string" && envBoot.length > 0) {
119
+ try {
120
+ const payload = JSON.parse(envBoot);
121
+ if (!isProcessWorkerBootPayload(payload)) {
122
+ throw new TypeError("invalid process worker boot payload");
123
+ }
124
+ try {
125
+ delete processLike?.env?.[RUNTIME_PROCESS_WORKER_BOOT_ENV];
126
+ }
127
+ catch {
128
+ }
129
+ start(payload);
130
+ }
131
+ catch (error) {
132
+ reportWorkerStartupFatal(error);
133
+ }
134
+ return;
135
+ }
136
+ if (RUNTIME_PARENT_PORT === undefined) {
137
+ reportWorkerStartupFatal(new TypeError("missing process worker boot payload"));
138
+ return;
139
+ }
140
+ if (typeof processLike?.on !== "function")
141
+ return;
142
+ const onMessage = (message) => {
143
+ if (!isProcessWorkerBootPayload(message))
144
+ return;
145
+ try {
146
+ processLike.off?.("message", onMessage);
147
+ processLike.removeListener?.("message", onMessage);
148
+ }
149
+ catch {
150
+ }
151
+ try {
152
+ start(message);
153
+ }
154
+ catch (error) {
155
+ reportWorkerStartupFatal(error);
156
+ }
157
+ };
158
+ processLike.on("message", onMessage);
159
+ };
@@ -36,6 +36,21 @@ const waitFallbackView = a_wait === undefined
36
36
  const a_pause = "pause" in Atomics
37
37
  ? Atomics.pause
38
38
  : undefined;
39
+ const runtimeGlobals = globalThis;
40
+ const isPlainNodeWindows = runtimeGlobals.process?.platform === "win32" &&
41
+ typeof runtimeGlobals.process?.versions?.node === "string" &&
42
+ runtimeGlobals.process?.versions?.bun === undefined &&
43
+ runtimeGlobals.Deno === undefined;
44
+ // Windows has no working cross-process wake. The native wake (WakeByAddress)
45
+ // is keyed to a virtual address, so a host cannot wake a process worker parked
46
+ // on the same physical page mapped at a different address in the child — the
47
+ // addon's FutexWake is a no-op there. A parked Windows Node process worker can
48
+ // therefore never be signalled and must rediscover work by polling. Cap the
49
+ // native wait at 1ms so it re-checks every millisecond instead of sleeping the
50
+ // full parkMs (up to seconds); the long park with no wake is what made CI
51
+ // runners appear to hang. The native wait already polls internally, so this is
52
+ // just bounding each poll slice.
53
+ const nativeWaitTimeoutMs = (parkMs) => isPlainNodeWindows ? 1 : parkMs ?? 60;
39
54
  export const whilePausing = ({ pauseInNanoseconds }) => {
40
55
  const forNanoseconds = pauseInNanoseconds ?? DEFAULT_PAUSE_TIME;
41
56
  if (!a_pause || forNanoseconds <= 0)
@@ -44,10 +59,9 @@ export const whilePausing = ({ pauseInNanoseconds }) => {
44
59
  };
45
60
  export const pauseGeneric = whilePausing({});
46
61
  export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait = true, }) => {
47
- const pause = pauseInNanoseconds
48
- !== undefined
49
- ? whilePausing({ pauseInNanoseconds })
50
- : pauseGeneric;
62
+ const pause = pauseInNanoseconds === undefined
63
+ ? pauseGeneric
64
+ : whilePausing({ pauseInNanoseconds });
51
65
  const tryProgress = () => {
52
66
  let progressed = false;
53
67
  if (enqueueLock())
@@ -82,7 +96,7 @@ export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, tx
82
96
  return;
83
97
  a_store(rxStatus, 0, 0);
84
98
  if (nativeWaitU32 !== undefined) {
85
- nativeWaitU32(opView.buffer, opView.byteOffset + (at * Int32Array.BYTES_PER_ELEMENT), value >>> 0, parkMs ?? 60);
99
+ nativeWaitU32(opView.buffer, opView.byteOffset + (at * Int32Array.BYTES_PER_ELEMENT), value >>> 0, nativeWaitTimeoutMs(parkMs));
86
100
  }
87
101
  else if (useSharedMemoryWait &&
88
102
  a_wait &&