knitting 0.1.60 → 0.1.61

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 (42) hide show
  1. package/BROWSER.md +134 -0
  2. package/README.md +93 -6
  3. package/knitting.browser.d.ts +6 -0
  4. package/knitting.browser.js +1 -0
  5. package/package.json +11 -2
  6. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  7. package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
  8. package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
  9. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
  11. package/prebuilds/darwin-arm64-node-137/knitting_shm.node +0 -0
  12. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  13. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  14. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  15. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  16. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  18. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  19. package/src/api.js +127 -22
  20. package/src/common/runtime.d.ts +2 -1
  21. package/src/common/runtime.js +8 -1
  22. package/src/memory/lock.d.ts +26 -1
  23. package/src/memory/lock.js +212 -4
  24. package/src/memory/payloadCodec.js +2 -6
  25. package/src/memory/regionRegistry.d.ts +1 -3
  26. package/src/memory/regionRegistry.js +16 -14
  27. package/src/permission/protocol.js +5 -1
  28. package/src/runtime/dispatcher.js +9 -0
  29. package/src/runtime/pool.d.ts +114 -3
  30. package/src/runtime/pool.js +209 -63
  31. package/src/runtime/process-worker.d.ts +2 -12
  32. package/src/runtime/process-worker.js +69 -113
  33. package/src/runtime/tx-queue.d.ts +15 -1
  34. package/src/runtime/tx-queue.js +52 -15
  35. package/src/runtime/worker-common.d.ts +13 -0
  36. package/src/runtime/worker-common.js +102 -0
  37. package/src/types.d.ts +41 -3
  38. package/src/worker/loop.js +27 -5
  39. package/src/worker/rx-queue.d.ts +11 -1
  40. package/src/worker/rx-queue.js +4 -1
  41. package/src/worker/timers.d.ts +8 -1
  42. package/src/worker/timers.js +20 -17
@@ -4,7 +4,7 @@ import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
4
4
  // Low 5 bits = slot index, high 27 bits = caller meta.
5
5
  // Inlined from setTaskSlotIndex to avoid cross-closure call on hot path.
6
6
  const SLOT_META_PACKED_MASK = 0xFFFFFFE0; // (~0x1F) >>> 0
7
- export const register = ({ lockSector, publishMode = "plain", }) => {
7
+ export const register = ({ lockSector, }) => {
8
8
  const lockRegion = toSharedBufferRegion(lockSector ?? createWasmSharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH));
9
9
  const lockSAB = lockRegion.sab;
10
10
  const hostBits = new Int32Array(lockSAB, lockRegion.byteOffset + PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, 1);
@@ -14,7 +14,7 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
14
14
  const clz32 = Math.clz32;
15
15
  const a_load = Atomics.load;
16
16
  const a_store = Atomics.store;
17
- const useAtomicPublish = publishMode === "atomic";
17
+ const a_xor = Atomics.xor;
18
18
  const EMPTY = 0xFFFFFFFF >>> 0;
19
19
  const SLOT_MASK = TASK_SLOT_INDEX_MASK;
20
20
  const START_MASK = (~SLOT_MASK) >>> 0;
@@ -23,7 +23,6 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
23
23
  let usedBits = 0 | 0;
24
24
  // scalar state (faster than Uint32Array(1))
25
25
  let hostLast = 0 | 0;
26
- let workerLast = 0 | 0;
27
26
  const startAndIndexToArray = (length) => startAndIndex.slice(0, length);
28
27
  const compactFreeBitsStable = (b, freeBits) => {
29
28
  const sai = startAndIndex;
@@ -92,10 +91,14 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
92
91
  let freeBits = (~(hostLast ^ w)) >>> 0;
93
92
  if (freeBits !== 0)
94
93
  freeBits &= usedBits;
95
- if (freeBits === EMPTY) {
94
+ // Every tracked slot is free, so restart the arena from offset 0. A free
95
+ // racing after this snapshot can only delay reclamation until the next
96
+ // allocation; it cannot make a live slot appear free.
97
+ if (freeBits === (usedBits >>> 0)) {
96
98
  tableLength = 0;
97
99
  usedBits = 0 | 0;
98
100
  tl = 0;
101
+ freeBits = 0 >>> 0;
99
102
  }
100
103
  else if (freeBits !== 0) {
101
104
  for (let i = 0; i < tl; i++) {
@@ -247,10 +250,7 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
247
250
  const slotIndex = findAndInsert(task, size);
248
251
  if (slotIndex === -1)
249
252
  return -1;
250
- if (useAtomicPublish)
251
- a_store(hostBits, 0, hostLast);
252
- else
253
- hostBits[0] = hostLast;
253
+ a_store(hostBits, 0, hostLast);
254
254
  return slotIndex;
255
255
  };
256
256
  const setSlotLength = (slotIndex, payloadLen) => {
@@ -264,13 +264,15 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
264
264
  size64bit[slotIndex] = aligned >>> 0;
265
265
  return true;
266
266
  };
267
+ // Always a read-modify-write. `free()` genuinely has two callers even without
268
+ // work stealing: the encode-side rollback (`failDynamicWriteAfterReserve`)
269
+ // and the decode-side release (`freeTaskSlot`). Each held a private
270
+ // `workerLast` shadow and blind-stored the whole word, so one overwrote the
271
+ // other's toggles — the allocator then handed out a region that was still
272
+ // live, aliasing payloads. XOR of distinct bits commutes, so one atomic
273
+ // toggle per free is correct for any number of writers.
267
274
  const free = (index) => {
268
- index = index & TASK_SLOT_INDEX_MASK;
269
- workerLast ^= 1 << index;
270
- if (useAtomicPublish)
271
- a_store(workerBits, 0, workerLast);
272
- else
273
- workerBits[0] = workerLast;
275
+ a_xor(workerBits, 0, 1 << (index & TASK_SLOT_INDEX_MASK));
274
276
  };
275
277
  return {
276
278
  allocTask,
@@ -1,4 +1,4 @@
1
- import { RUNTIME } from "../common/runtime.js";
1
+ import { IS_BROWSER, RUNTIME } from "../common/runtime.js";
2
2
  import { toCanonicalPath as toSharedCanonicalPath } from "../common/path-canonical.js";
3
3
  import { getNodeBuiltinModule, getNodeProcess } from "../common/node-compat.js";
4
4
  const requireNodeModule = (specifier) => {
@@ -436,6 +436,10 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
436
436
  const input = normalizeProtocolInput(permission);
437
437
  if (!input)
438
438
  return undefined;
439
+ // Browsers have no filesystem, process, or runtime flags to police, and the
440
+ // strict default would otherwise reach for `node:path` and throw.
441
+ if (IS_BROWSER)
442
+ return undefined;
439
443
  const rawMode = input.mode;
440
444
  const mode = (rawMode === "unsafe" || rawMode === "off")
441
445
  ? "unsafe"
@@ -24,6 +24,15 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
24
24
  check.rerun = true;
25
25
  return;
26
26
  }
27
+ // Nothing enqueued and nothing outstanding at the worker: skip the drain.
28
+ // Without this an idle lane still pays a txStatus write, an rxStatus load
29
+ // and — worst of all — an Atomics.notify that wakes a parked worker for no
30
+ // reason. Under the serial-channel dispatcher every lane is checked on
31
+ // every tick, so that waste is what makes latency grow with thread count.
32
+ if (txIdle()) {
33
+ check.isRunning = false;
34
+ return;
35
+ }
27
36
  inFlight = true;
28
37
  if (backoffTimer !== undefined) {
29
38
  clearTimeout(backoffTimer);
@@ -1,12 +1,104 @@
1
+ import { createHostTxQueue } from "./tx-queue.js";
1
2
  import { type ProcessSharedMemoryBacking } from "./process-worker.js";
2
3
  import { type Sab } from "../ipc/transport/shared-memory.js";
3
4
  import { ChannelHandler, type DispatcherCheck } from "./dispatcher.js";
4
- import { lock2 } from "../memory/lock.js";
5
+ import { lock2, type Task } from "../memory/lock.js";
5
6
  import "../memory/payloadCodec.js";
6
- import type { DebugOptions, DispatcherSettings, WorkerContext, WorkerData, WorkerSettings } from "../types.js";
7
+ import type { DebugOptions, DispatcherSettings, LockBuffers, WorkerContext, WorkerData, WorkerSettings } from "../types.js";
7
8
  import "../worker/loop.js";
8
9
  import { type PayloadBufferOptions } from "../memory/payload-config.js";
9
- export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, }: {
10
+ /**
11
+ * Build the buffers, host-side locks and pending registry that a stealing pool
12
+ * shares. One submit region for every worker to claim from, one private return
13
+ * region per worker, and a single pool-global queue so a response arriving on
14
+ * any lane settles the right promise.
15
+ *
16
+ * Region width follows paper §6.1: the largest power-of-two `g` leaving a spare
17
+ * region for a delayed claimant (`slots / g >= consumers + 1`). At 32 slots that
18
+ * caps how wide a region can be well before it caps the worker count.
19
+ */
20
+ export declare const resolveStealRegionLanes: (consumers: number) => number;
21
+ /** Maximum claimants that leave the protocol's required spare region. */
22
+ export declare const MAX_STEAL_CONSUMERS: number;
23
+ export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, }: {
24
+ threads: number;
25
+ payload?: PayloadBufferOptions;
26
+ regionLanes?: number;
27
+ abortSignalCapacity?: number;
28
+ usesAbortSignal?: boolean;
29
+ }) => {
30
+ submitBuffers: LockBuffers;
31
+ returnBuffers: LockBuffers[];
32
+ hostSubmitLock: {
33
+ enlist: (task: Task) => true;
34
+ encode: (task: Task, state?: number) => boolean;
35
+ encodeManyFrom: (list: import("../ipc/tools/ring-queue.js").default<Task>) => number;
36
+ encodeAll: () => boolean;
37
+ publish: (task: Task) => boolean;
38
+ flushPending: () => boolean;
39
+ decode: () => boolean;
40
+ hasSpace: () => boolean;
41
+ resolved: import("../ipc/tools/ring-queue.js").default<Task>;
42
+ hostBits: Int32Array<import("../common/shared-buffer-region.js").SharedBuffer>;
43
+ workerBits: Int32Array<import("../common/shared-buffer-region.js").SharedBuffer>;
44
+ recyclecList: import("../ipc/tools/ring-queue.js").default<Task>;
45
+ resolveHost: ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }: {
46
+ queue: Task[];
47
+ onResolved?: (task: Task) => void;
48
+ shouldSettle?: (task: Task) => boolean;
49
+ activeRejectPlaceholder?: Task["reject"];
50
+ }) => () => number;
51
+ hasPendingFrames: () => boolean;
52
+ getPendingFrameCount: () => number;
53
+ getPendingPromiseCount: () => number;
54
+ resetPendingState: () => void;
55
+ deactivateStealConsumer: (id: number) => boolean;
56
+ takeDeferredCount: () => number;
57
+ setPromiseHandler: (handler?: import("../memory/lock.js").PromisePayloadHandler) => void;
58
+ };
59
+ hostReturnLocks: {
60
+ enlist: (task: Task) => true;
61
+ encode: (task: Task, state?: number) => boolean;
62
+ encodeManyFrom: (list: import("../ipc/tools/ring-queue.js").default<Task>) => number;
63
+ encodeAll: () => boolean;
64
+ publish: (task: Task) => boolean;
65
+ flushPending: () => boolean;
66
+ decode: () => boolean;
67
+ hasSpace: () => boolean;
68
+ resolved: import("../ipc/tools/ring-queue.js").default<Task>;
69
+ hostBits: Int32Array<import("../common/shared-buffer-region.js").SharedBuffer>;
70
+ workerBits: Int32Array<import("../common/shared-buffer-region.js").SharedBuffer>;
71
+ recyclecList: import("../ipc/tools/ring-queue.js").default<Task>;
72
+ resolveHost: ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }: {
73
+ queue: Task[];
74
+ onResolved?: (task: Task) => void;
75
+ shouldSettle?: (task: Task) => boolean;
76
+ activeRejectPlaceholder?: Task["reject"];
77
+ }) => () => number;
78
+ hasPendingFrames: () => boolean;
79
+ getPendingFrameCount: () => number;
80
+ getPendingPromiseCount: () => number;
81
+ resetPendingState: () => void;
82
+ deactivateStealConsumer: (id: number) => boolean;
83
+ takeDeferredCount: () => number;
84
+ setPromiseHandler: (handler?: import("../memory/lock.js").PromisePayloadHandler) => void;
85
+ }[];
86
+ sharedQueue: {
87
+ rejectAll: (reason: string) => void;
88
+ hasPendingFrames: () => boolean;
89
+ txIdle: () => boolean;
90
+ completeFrame: () => number;
91
+ setReturnHooks: (lane: number, hooks: (import("../connections/buffer-reference.js").BufferReferenceReturnHooks | ((token: bigint) => void)) | undefined) => void;
92
+ enqueue: (functionID: number, timeout?: import("../types.js").TaskTimeout, abortSignal?: import("../types.js").AbortSignalOption) => (rawArgs: unknown) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
93
+ flushToWorker: () => boolean;
94
+ enqueueKnown: (task: Task) => boolean;
95
+ settlePromisePayload: (task: Task, isRejected: boolean, value: unknown) => boolean;
96
+ };
97
+ regionLanes: number;
98
+ abortSignalSAB: SharedArrayBuffer;
99
+ abortSignalMax: number;
100
+ };
101
+ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, stealPool, }: {
10
102
  list: string[];
11
103
  ids: number[];
12
104
  names: string[];
@@ -30,6 +122,25 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
30
122
  * macro channel that runs all lane checks.
31
123
  */
32
124
  sharedChannelHandler?: ChannelHandler;
125
+ /**
126
+ * Work-stealing wiring. When present the submit region, both host-side locks
127
+ * and the pending registry are owned by `createPool` and shared: every worker
128
+ * claims from one request region, and the host demultiplexes responses by
129
+ * task id. This lane only contributes its private return region and its
130
+ * worker. The dispatcher is owned by the pool too, so none is built here.
131
+ */
132
+ stealPool?: {
133
+ submitBuffers: LockBuffers;
134
+ returnBuffers: LockBuffers;
135
+ hostSubmitLock: ReturnType<typeof lock2>;
136
+ hostReturnLock: ReturnType<typeof lock2>;
137
+ sharedQueue: ReturnType<typeof createHostTxQueue>;
138
+ consumers: number;
139
+ consumerId: number;
140
+ regionLanes: number;
141
+ abortSignalSAB?: LockBuffers["headers"];
142
+ abortSignalMax?: number;
143
+ };
33
144
  }) => (WorkerContext & {
34
145
  dispatcherCheck?: never;
35
146
  laneWake?: never;
@@ -1,5 +1,6 @@
1
1
  import { createHostTxQueue } from "./tx-queue.js";
2
- import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, serializeWorkerBootstrapData, spawnProcessWorker, terminateWorkerQuietly, toProcessWorkerBootPayload, toWorkerCompatExecArgv, toWorkerSafeExecArgv, } from "./process-worker.js";
2
+ import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
3
+ import { serializeWorkerBootstrapData, terminateWorkerQuietly, toWorkerCompatExecArgv, toWorkerSafeExecArgv, } from "./worker-common.js";
3
4
  import { createSharedMemoryTransport, TRANSPORT_SIGNAL_BYTES, } from "../ipc/transport/shared-memory.js";
4
5
  import { ChannelHandler, hostDispatcherLoop, } from "./dispatcher.js";
5
6
  import { HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
@@ -7,7 +8,7 @@ import { HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } fr
7
8
  // see lock.ts). Must run before any lock2() call.
8
9
  import "../memory/payloadCodec.js";
9
10
  import "../worker/loop.js";
10
- import { createSharedArrayBuffer, createWasmSharedArrayBuffer, } from "../common/runtime.js";
11
+ import { createSharedArrayBuffer, createWasmSharedArrayBuffer, IS_BROWSER, } from "../common/runtime.js";
11
12
  import { HAS_NODE_WORKER_THREADS, RUNTIME_WORKER, } from "../common/worker-runtime.js";
12
13
  import { probeLockBufferTextCompat } from "../common/shared-buffer-text.js";
13
14
  import { signalAbortFactory } from "../shared/abortSignal.js";
@@ -21,6 +22,15 @@ const isWorkerFatalMessage = (value) => !!value &&
21
22
  typeof value[WORKER_FATAL_MESSAGE_KEY] === "string";
22
23
  // Keep idle workers self-healing if an Atomics.notify wake is missed.
23
24
  const DEFAULT_WORKER_PARK_MS = 1;
25
+ const DEFAULT_ABORT_SIGNAL_CAPACITY = 258;
26
+ const sanitizePositiveInteger = (value) => {
27
+ if (!Number.isFinite(value))
28
+ return undefined;
29
+ const parsed = Math.floor(value);
30
+ return parsed > 0 ? parsed : undefined;
31
+ };
32
+ const resolveAbortSignalCapacity = (value) => sanitizePositiveInteger(value) ?? DEFAULT_ABORT_SIGNAL_CAPACITY;
33
+ const abortSignalByteLength = (capacity) => Math.max(1, Math.ceil(capacity / 32)) * Uint32Array.BYTES_PER_ELEMENT;
24
34
  const withDefaultWorkerTimers = (options) => {
25
35
  const parkMs = options?.timers?.parkMs ?? DEFAULT_WORKER_PARK_MS;
26
36
  if (options === undefined)
@@ -38,7 +48,111 @@ const withFixedPayloadConfig = (config) => ({
38
48
  mode: "fixed",
39
49
  payloadInitialBytes: config.payloadMaxByteLength,
40
50
  });
41
- export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, }) => {
51
+ /**
52
+ * Build the buffers, host-side locks and pending registry that a stealing pool
53
+ * shares. One submit region for every worker to claim from, one private return
54
+ * region per worker, and a single pool-global queue so a response arriving on
55
+ * any lane settles the right promise.
56
+ *
57
+ * Region width follows paper §6.1: the largest power-of-two `g` leaving a spare
58
+ * region for a delayed claimant (`slots / g >= consumers + 1`). At 32 slots that
59
+ * caps how wide a region can be well before it caps the worker count.
60
+ */
61
+ export const resolveStealRegionLanes = (consumers) => {
62
+ for (let lanes = LockBound.slots; lanes >= 1; lanes >>= 1) {
63
+ if (LockBound.slots / lanes >= consumers + 1)
64
+ return lanes;
65
+ }
66
+ throw new RangeError(`${consumers} stealing workers need more than ${LockBound.slots} lanes`);
67
+ };
68
+ /** Maximum claimants that leave the protocol's required spare region. */
69
+ export const MAX_STEAL_CONSUMERS = LockBound.slots - 1;
70
+ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, }) => {
71
+ const payloadConfig = resolvePayloadBufferOptions({ options: payload });
72
+ const makePayload = () => payloadConfig.mode === "growable"
73
+ ? createSharedArrayBuffer(payloadConfig.payloadInitialBytes, payloadConfig.payloadMaxByteLength)
74
+ : createSharedArrayBuffer(payloadConfig.payloadInitialBytes);
75
+ const carpet = () => createLockControlCarpet({
76
+ signalBytes: 0,
77
+ abortBytes: 0,
78
+ lockSectorBytes: LOCK_SECTOR_BYTE_LENGTH,
79
+ headerSlotStrideU32: HEADER_SLOT_STRIDE_U32,
80
+ slotCount: LockBound.slots,
81
+ headerLayout: "split",
82
+ });
83
+ const toBuffers = (half, payloadSab) => ({
84
+ ...half,
85
+ payload: payloadSab,
86
+ textCompat: probeLockBufferTextCompat({
87
+ headers: half.headers,
88
+ payload: payloadSab,
89
+ }),
90
+ });
91
+ const maxLanes = resolveStealRegionLanes(threads);
92
+ const lanes = regionLanes === undefined
93
+ ? maxLanes
94
+ : Math.min(Math.max(1, regionLanes | 0), maxLanes);
95
+ const submitCarpet = carpet();
96
+ const submitBuffers = toBuffers(submitCarpet.lock, makePayload());
97
+ const hostSubmitLock = lock2({
98
+ headers: submitBuffers.headers,
99
+ headerSlotStrideU32: submitBuffers.headerSlotStrideU32,
100
+ LockBoundSector: submitBuffers.lockSector,
101
+ payload: submitBuffers.payload,
102
+ payloadSector: submitBuffers.payloadSector,
103
+ payloadConfig,
104
+ textCompat: submitBuffers.textCompat,
105
+ consumers: threads,
106
+ regionLanes: lanes,
107
+ });
108
+ const returnBuffers = [];
109
+ const hostReturnLocks = [];
110
+ for (let i = 0; i < threads; i++) {
111
+ const buffers = toBuffers(carpet().returnLock, makePayload());
112
+ returnBuffers.push(buffers);
113
+ hostReturnLocks.push(lock2({
114
+ headers: buffers.headers,
115
+ headerSlotStrideU32: buffers.headerSlotStrideU32,
116
+ LockBoundSector: buffers.lockSector,
117
+ payload: buffers.payload,
118
+ payloadSector: buffers.payloadSector,
119
+ payloadConfig,
120
+ textCompat: buffers.textCompat,
121
+ }));
122
+ }
123
+ // Abort-aware tasks can execute on any claimant, so all workers and the
124
+ // pool-global queue must observe one bitmap. Per-lane abort buffers would
125
+ // encode an id from one allocator and test it against another lane's bits.
126
+ const resolvedAbortSignalCapacity = resolveAbortSignalCapacity(abortSignalCapacity);
127
+ const abortSignalSAB = usesAbortSignal === true
128
+ ? createSharedArrayBuffer(abortSignalByteLength(resolvedAbortSignalCapacity))
129
+ : undefined;
130
+ const abortSignals = abortSignalSAB === undefined
131
+ ? undefined
132
+ : signalAbortFactory({
133
+ sab: abortSignalSAB,
134
+ maxSignals: resolvedAbortSignalCapacity,
135
+ });
136
+ const sharedQueue = createHostTxQueue({
137
+ lock: hostSubmitLock,
138
+ returnLock: hostReturnLocks[0],
139
+ extraReturnLocks: hostReturnLocks.slice(1),
140
+ abortSignals,
141
+ });
142
+ return {
143
+ submitBuffers,
144
+ returnBuffers,
145
+ hostSubmitLock,
146
+ hostReturnLocks,
147
+ sharedQueue,
148
+ regionLanes: lanes,
149
+ abortSignalSAB,
150
+ abortSignalMax: abortSignalSAB === undefined
151
+ ? undefined
152
+ : resolvedAbortSignalCapacity,
153
+ };
154
+ };
155
+ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, stealPool, }) => {
42
156
  if (workerOptions?.runtime === "compiled") {
43
157
  return spawnCompiledWorkerContext({
44
158
  list,
@@ -65,32 +179,32 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
65
179
  if (!useProcessWorkerRuntime && typeof poliWorker !== "function") {
66
180
  throw new Error("Worker is not available in this runtime");
67
181
  }
182
+ // Without this the failure is a `ReferenceError` from whichever module
183
+ // happens to allocate first.
184
+ if (IS_BROWSER && typeof SharedArrayBuffer !== "function") {
185
+ throw new Error("SharedArrayBuffer is unavailable: serve the page cross-origin isolated " +
186
+ "(Cross-Origin-Opener-Policy: same-origin, " +
187
+ "Cross-Origin-Embedder-Policy: require-corp).");
188
+ }
68
189
  const WorkerCtor = poliWorker;
69
190
  // Lock buffers must be shared between host and worker.
70
- const sanitizeBytes = (value) => {
71
- if (!Number.isFinite(value))
72
- return undefined;
73
- const bytes = Math.floor(value);
74
- return bytes > 0 ? bytes : undefined;
75
- };
191
+ const sanitizeBytes = sanitizePositiveInteger;
76
192
  const basePayloadConfig = resolvePayloadBufferOptions({
77
193
  options: payload,
78
194
  });
79
195
  const resolvedPayloadConfig = useProcessWorkerRuntime
80
196
  ? withFixedPayloadConfig(basePayloadConfig)
81
197
  : basePayloadConfig;
82
- const defaultAbortSignalCapacity = 258;
83
- const requestedAbortSignalCapacity = sanitizeBytes(abortSignalCapacity);
84
- const resolvedAbortSignalCapacity = requestedAbortSignalCapacity ??
85
- defaultAbortSignalCapacity;
86
- const abortSignalWords = Math.max(1, Math.ceil(resolvedAbortSignalCapacity / 32));
198
+ const resolvedAbortSignalCapacity = resolveAbortSignalCapacity(abortSignalCapacity);
87
199
  const requestedSignalBytes = sanitizeBytes(sab?.size);
88
200
  const externalSignalSab = sab?.sharedSab;
89
201
  if (useProcessWorkerRuntime && externalSignalSab != null) {
90
202
  throw new Error("process worker runtime cannot use an external signal buffer");
91
203
  }
92
204
  const signalBytes = Math.max(TRANSPORT_SIGNAL_BYTES, requestedSignalBytes ?? TRANSPORT_SIGNAL_BYTES);
93
- const abortBytes = abortSignalWords * Uint32Array.BYTES_PER_ELEMENT;
205
+ const abortBytes = stealPool === undefined && usesAbortSignal === true
206
+ ? abortSignalByteLength(resolvedAbortSignalCapacity)
207
+ : 0;
94
208
  const processWorkerMemory = useProcessWorkerRuntime
95
209
  ? createProcessWorkerMemoryLayout({
96
210
  signalBytes,
@@ -132,7 +246,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
132
246
  const controlLayout = processWorkerMemory?.controlLayout ??
133
247
  makeLockControlLayout();
134
248
  const lockPayload = processWorkerMemory?.lockPayload ?? makePayloadBuffer();
135
- const lockBuffers = {
249
+ const lockBuffers = stealPool?.submitBuffers ?? {
136
250
  ...controlLayout.lock,
137
251
  payload: lockPayload,
138
252
  textCompat: probeLockBufferTextCompat({
@@ -142,7 +256,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
142
256
  };
143
257
  const returnPayload = processWorkerMemory?.returnPayload ??
144
258
  makePayloadBuffer();
145
- const returnLockBuffers = {
259
+ const returnLockBuffers = stealPool?.returnBuffers ?? {
146
260
  ...controlLayout.returnLock,
147
261
  payload: returnPayload,
148
262
  textCompat: probeLockBufferTextCompat({
@@ -150,7 +264,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
150
264
  payload: returnPayload,
151
265
  }),
152
266
  };
153
- const lock = lock2({
267
+ const lock = stealPool?.hostSubmitLock ?? lock2({
154
268
  headers: lockBuffers.headers,
155
269
  headerSlotStrideU32: lockBuffers.headerSlotStrideU32,
156
270
  LockBoundSector: lockBuffers.lockSector,
@@ -160,7 +274,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
160
274
  textCompat: lockBuffers.textCompat,
161
275
  processBoundary: useProcessWorkerRuntime,
162
276
  });
163
- const returnLock = lock2({
277
+ const returnLock = stealPool?.hostReturnLock ?? lock2({
164
278
  headers: returnLockBuffers.headers,
165
279
  headerSlotStrideU32: returnLockBuffers.headerSlotStrideU32,
166
280
  LockBoundSector: returnLockBuffers.lockSector,
@@ -170,10 +284,9 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
170
284
  textCompat: returnLockBuffers.textCompat,
171
285
  processBoundary: useProcessWorkerRuntime,
172
286
  });
173
- const abortSignalSAB = usesAbortSignal === true
174
- ? controlLayout.abortSignals
175
- : undefined;
176
- const abortSignals = abortSignalSAB
287
+ const abortSignalSAB = stealPool?.abortSignalSAB ??
288
+ (usesAbortSignal === true ? controlLayout.abortSignals : undefined);
289
+ const abortSignals = abortSignalSAB && stealPool === undefined
177
290
  ? signalAbortFactory({
178
291
  sab: abortSignalSAB,
179
292
  maxSignals: resolvedAbortSignalCapacity,
@@ -224,34 +337,38 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
224
337
  }
225
338
  outstandingBorrowedReturns.clear();
226
339
  };
227
- const queue = createHostTxQueue({
340
+ const returnHooks = bufferReferenceReturn === "borrow"
341
+ ? {
342
+ release: releaseBorrowedReturnToken,
343
+ track: (ref, token, aliasBuffer) => {
344
+ if (outstandingBorrowedReturns === undefined)
345
+ return;
346
+ const record = outstandingBorrowedReturns.get(token);
347
+ if (record !== undefined) {
348
+ if (aliasBuffer !== undefined) {
349
+ record.aliasBuffer = new WeakRef(aliasBuffer);
350
+ }
351
+ return;
352
+ }
353
+ outstandingBorrowedReturns.set(token, {
354
+ ref: new WeakRef(ref),
355
+ aliasBuffer: aliasBuffer === undefined
356
+ ? undefined
357
+ : new WeakRef(aliasBuffer),
358
+ runtime: ref.runtime,
359
+ });
360
+ },
361
+ }
362
+ : undefined;
363
+ const queue = stealPool?.sharedQueue ?? createHostTxQueue({
228
364
  lock,
229
365
  returnLock,
230
366
  abortSignals,
231
- releaseBufferReferenceReturn: bufferReferenceReturn === "borrow"
232
- ? {
233
- release: releaseBorrowedReturnToken,
234
- track: (ref, token, aliasBuffer) => {
235
- if (outstandingBorrowedReturns === undefined)
236
- return;
237
- const record = outstandingBorrowedReturns.get(token);
238
- if (record !== undefined) {
239
- if (aliasBuffer !== undefined) {
240
- record.aliasBuffer = new WeakRef(aliasBuffer);
241
- }
242
- return;
243
- }
244
- outstandingBorrowedReturns.set(token, {
245
- ref: new WeakRef(ref),
246
- aliasBuffer: aliasBuffer === undefined
247
- ? undefined
248
- : new WeakRef(aliasBuffer),
249
- runtime: ref.runtime,
250
- });
251
- },
252
- }
253
- : undefined,
367
+ releaseBufferReferenceReturn: returnHooks,
254
368
  });
369
+ if (stealPool !== undefined) {
370
+ stealPool.sharedQueue.setReturnHooks(stealPool.consumerId, returnHooks);
371
+ }
255
372
  const { enqueue, rejectAll, txIdle, } = queue;
256
373
  const thisSignal = signalBox.opView;
257
374
  const a_add = Atomics.add;
@@ -271,16 +388,21 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
271
388
  let dispatchSend = () => { };
272
389
  const send = () => dispatchSend();
273
390
  let channelHandler;
274
- const ownsChannel = sharedChannelHandler === undefined;
391
+ const ownsChannel = sharedChannelHandler === undefined &&
392
+ stealPool === undefined;
275
393
  const ownChannel = sharedChannelHandler ?? new ChannelHandler();
276
- const { check: dispatcherCheck } = hostDispatcherLoop({
277
- signalBox,
278
- queue,
279
- channelHandler: ownChannel,
280
- dispatcherOptions: host,
281
- notifySignal: nativeNotifySignal,
282
- });
283
- if (ownsChannel) {
394
+ // Under stealing there is one queue, so one dispatcher drives it from the
395
+ // pool; a per-lane dispatcher would double-drain the shared registry.
396
+ const { check: dispatcherCheck } = stealPool !== undefined
397
+ ? { check: undefined }
398
+ : hostDispatcherLoop({
399
+ signalBox,
400
+ queue,
401
+ channelHandler: ownChannel,
402
+ dispatcherOptions: host,
403
+ notifySignal: nativeNotifySignal,
404
+ });
405
+ if (ownsChannel && dispatcherCheck !== undefined) {
284
406
  ownChannel.open(dispatcherCheck);
285
407
  channelHandler = ownChannel;
286
408
  dispatchSend = () => {
@@ -304,9 +426,8 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
304
426
  const workerDataPayload = {
305
427
  sab: signals.sab,
306
428
  abortSignalSAB,
307
- abortSignalMax: usesAbortSignal === true
308
- ? resolvedAbortSignalCapacity
309
- : undefined,
429
+ abortSignalMax: stealPool?.abortSignalMax ??
430
+ (usesAbortSignal === true ? resolvedAbortSignalCapacity : undefined),
310
431
  list,
311
432
  ids,
312
433
  names,
@@ -321,6 +442,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
321
442
  payloadConfig: resolvedPayloadConfig,
322
443
  bufferReferenceReturn,
323
444
  permission,
445
+ steal: stealPool === undefined ? undefined : {
446
+ consumers: stealPool.consumers,
447
+ consumerId: stealPool.consumerId,
448
+ regionLanes: stealPool.regionLanes,
449
+ },
324
450
  };
325
451
  const baseWorkerOptions = {
326
452
  //@ts-ignore Reason
@@ -389,6 +515,20 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
389
515
  worker.postMessage?.(workerDataPayload);
390
516
  }
391
517
  let closedReason;
518
+ const deactivateStealConsumer = () => {
519
+ stealPool?.hostSubmitLock.deactivateStealConsumer(stealPool.consumerId);
520
+ };
521
+ const terminateFailedWorker = () => {
522
+ try {
523
+ worker.unref?.();
524
+ void Promise.resolve(worker.terminate())
525
+ .catch(() => { })
526
+ .finally(deactivateStealConsumer);
527
+ }
528
+ catch {
529
+ deactivateStealConsumer();
530
+ }
531
+ };
392
532
  // `workerBytesReadable`: while the worker is still alive its pinned bytes
393
533
  // can be copied out; after a crash/exit they are gone and borrows are
394
534
  // detached so stale reads fail loud instead of aliasing freed memory.
@@ -404,18 +544,22 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
404
544
  if (!isWorkerFatalMessage(message))
405
545
  return;
406
546
  markWorkerClosed(`Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`, true);
407
- terminateWorkerQuietly(worker);
547
+ terminateFailedWorker();
408
548
  };
409
549
  const onWorkerError = (error) => {
410
550
  const message = String(error?.message ?? error);
411
551
  markWorkerClosed(`Worker crashed: ${message}`);
552
+ terminateFailedWorker();
412
553
  };
413
554
  const nodeWorker = worker;
414
555
  if (typeof nodeWorker.on === "function") {
415
556
  nodeWorker.on("message", onWorkerMessage);
416
557
  nodeWorker.on("error", onWorkerError);
417
558
  nodeWorker.on("exit", (code) => {
418
- if (typeof code === "number" && code === 0)
559
+ // Exit is the point at which this endpoint can no longer write WANT, so
560
+ // survivors may safely ignore any intent it left behind.
561
+ deactivateStealConsumer();
562
+ if (closedReason !== undefined)
419
563
  return;
420
564
  const normalized = typeof code === "number" ? code : -1;
421
565
  markWorkerClosed(`Worker exited with code ${normalized}`);
@@ -463,8 +607,10 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
463
607
  lock,
464
608
  processSharedMemoryBackings: processSharedMemory?.backings,
465
609
  dispatcherCheck,
466
- laneWake: sharedChannelHandler !== undefined ? laneWake : undefined,
467
- bindSend: sharedChannelHandler !== undefined
610
+ laneWake: sharedChannelHandler !== undefined || stealPool !== undefined
611
+ ? laneWake
612
+ : undefined,
613
+ bindSend: sharedChannelHandler !== undefined || stealPool !== undefined
468
614
  ? ((fn) => void (dispatchSend = fn))
469
615
  : undefined,
470
616
  };