knitting 0.1.56 → 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 (101) hide show
  1. package/BROWSER.md +134 -0
  2. package/README.md +347 -46
  3. package/knitting.browser.d.ts +6 -0
  4. package/knitting.browser.js +1 -0
  5. package/knitting.d.ts +3 -2
  6. package/knitting.js +3 -3
  7. package/map.md +21 -9
  8. package/package.json +17 -2
  9. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
  11. package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
  12. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
  14. package/prebuilds/darwin-arm64-node-137/knitting_shm.node +0 -0
  15. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  16. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  18. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  19. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  20. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  21. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  22. package/scripts/build-compiled-worker.ts +276 -0
  23. package/scripts/compiled-worker/porffor.d.ts +5 -0
  24. package/scripts/compiled-worker/runtime.ts +529 -0
  25. package/scripts/compiled-worker/task-shim.ts +32 -0
  26. package/src/api.d.ts +24 -1
  27. package/src/api.js +311 -27
  28. package/src/common/module-url.d.ts +1 -0
  29. package/src/common/module-url.js +30 -2
  30. package/src/common/node-compat.d.ts +2 -0
  31. package/src/common/path-canonical.js +20 -6
  32. package/src/common/runtime.d.ts +3 -1
  33. package/src/common/runtime.js +25 -4
  34. package/src/common/task-source.d.ts +2 -0
  35. package/src/common/task-source.js +17 -1
  36. package/src/common/worker-runtime.js +16 -7
  37. package/src/connections/buffer-reference-native.js +25 -1
  38. package/src/connections/buffer-reference.d.ts +23 -2
  39. package/src/connections/buffer-reference.js +143 -29
  40. package/src/connections/bun.d.ts +1 -1
  41. package/src/connections/bun.js +25 -14
  42. package/src/connections/deno.d.ts +3 -1
  43. package/src/connections/deno.js +35 -14
  44. package/src/connections/external-array-buffer.d.ts +2 -0
  45. package/src/connections/external-array-buffer.js +33 -0
  46. package/src/connections/node-addons.d.ts +11 -0
  47. package/src/connections/node-addons.js +49 -1
  48. package/src/connections/node-ffi-api.d.ts +19 -0
  49. package/src/connections/node-ffi-api.js +28 -0
  50. package/src/connections/node-ffi.d.ts +27 -0
  51. package/src/connections/node-ffi.js +262 -0
  52. package/src/connections/node.d.ts +1 -1
  53. package/src/connections/node.js +20 -20
  54. package/src/connections/package-assets.js +25 -15
  55. package/src/connections/posix.d.ts +42 -0
  56. package/src/connections/posix.js +101 -0
  57. package/src/connections/process-shared-buffer.d.ts +1 -0
  58. package/src/connections/process-shared-buffer.js +11 -45
  59. package/src/connections/types.d.ts +4 -0
  60. package/src/connections/windows.d.ts +19 -10
  61. package/src/connections/windows.js +87 -66
  62. package/src/error.d.ts +7 -6
  63. package/src/error.js +8 -7
  64. package/src/memory/lock.d.ts +112 -77
  65. package/src/memory/lock.js +314 -87
  66. package/src/memory/payloadCodec.d.ts +11 -2
  67. package/src/memory/payloadCodec.js +129 -37
  68. package/src/memory/regionRegistry.d.ts +1 -3
  69. package/src/memory/regionRegistry.js +16 -14
  70. package/src/permission/compatibility.d.ts +28 -0
  71. package/src/permission/compatibility.js +239 -0
  72. package/src/permission/index.d.ts +2 -0
  73. package/src/permission/index.js +1 -0
  74. package/src/permission/protocol.d.ts +1 -0
  75. package/src/permission/protocol.js +59 -15
  76. package/src/runtime/compiled-artifact.d.ts +21 -0
  77. package/src/runtime/compiled-artifact.js +260 -0
  78. package/src/runtime/compiled-builder.d.ts +8 -0
  79. package/src/runtime/compiled-builder.js +42 -0
  80. package/src/runtime/compiled-worker.d.ts +27 -0
  81. package/src/runtime/compiled-worker.js +562 -0
  82. package/src/runtime/dispatcher.js +9 -0
  83. package/src/runtime/inline-executor.js +27 -15
  84. package/src/runtime/pool.d.ts +121 -5
  85. package/src/runtime/pool.js +263 -49
  86. package/src/runtime/process-worker.d.ts +3 -13
  87. package/src/runtime/process-worker.js +97 -101
  88. package/src/runtime/tx-queue.d.ts +18 -2
  89. package/src/runtime/tx-queue.js +56 -16
  90. package/src/runtime/worker-common.d.ts +13 -0
  91. package/src/runtime/worker-common.js +102 -0
  92. package/src/types.d.ts +89 -9
  93. package/src/worker/composable-runners.js +3 -2
  94. package/src/worker/loop.d.ts +1 -0
  95. package/src/worker/loop.js +52 -17
  96. package/src/worker/rx-queue.d.ts +11 -1
  97. package/src/worker/rx-queue.js +4 -1
  98. package/src/worker/task-loader.d.ts +5 -4
  99. package/src/worker/task-loader.js +13 -7
  100. package/src/worker/timers.d.ts +8 -1
  101. package/src/worker/timers.js +28 -23
@@ -49,7 +49,7 @@ 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 makeToolkitCache = (hasAborted) => {
52
+ const makeToolkitCache = (hasAborted, now) => {
53
53
  const bySignal = [];
54
54
  return (signal) => {
55
55
  let toolkit = bySignal[signal];
@@ -58,6 +58,7 @@ const makeToolkitCache = (hasAborted) => {
58
58
  const hasAbortedMethod = () => hasAborted(signal);
59
59
  toolkit = {
60
60
  hasAborted: hasAbortedMethod,
61
+ now,
61
62
  };
62
63
  bySignal[signal] = toolkit;
63
64
  return toolkit;
@@ -76,7 +77,7 @@ export const composeWorkerRunner = ({ job, timeout, hasAborted, now, }) => {
76
77
  return applyTimeoutBudget(result, slot, timeout, nowTime);
77
78
  };
78
79
  }
79
- const getToolkit = makeToolkitCache(hasAborted);
80
+ const getToolkit = makeToolkitCache(hasAborted, nowTime);
80
81
  if (!timeout) {
81
82
  return (slot) => {
82
83
  const signal = readSignal(slot);
@@ -1,2 +1,3 @@
1
+ import "../memory/payloadCodec.js";
1
2
  import type { WorkerData } from "../types.js";
2
3
  export declare const workerMainLoop: (startupData: WorkerData) => Promise<void>;
@@ -4,9 +4,12 @@ import { isLockBufferTextCompat } from "../common/shared-buffer-text.js";
4
4
  import { createWorkerRxQueue } from "./rx-queue.js";
5
5
  import { createSharedMemoryTransport } from "../ipc/transport/shared-memory.js";
6
6
  import { lock2 } from "../memory/lock.js";
7
+ // Side-effect import: registers the payload codec (cycle break for Andromeda;
8
+ // see lock.ts). Must run before any lock2() call.
9
+ import "../memory/payloadCodec.js";
7
10
  import { getFunctions } from "./task-loader.js";
8
11
  import { pauseGeneric, sleepUntilChanged, whilePausing } from "./timers.js";
9
- import { RUNTIME, SET_IMMEDIATE } from "../common/runtime.js";
12
+ import { IS_ANDROMEDA, RUNTIME, SET_IMMEDIATE } from "../common/runtime.js";
10
13
  import { getNodeProcess } from "../common/node-compat.js";
11
14
  import { assertWorkerImportsResolved, assertWorkerSharedMemoryBootData, installPerformanceNowGuard, installTerminationGuard, installUnhandledRejectionSilencer, scrubWorkerDataSensitiveBuffers, } from "./safety/index.js";
12
15
  import { signalAbortFactory } from "../shared/abortSignal.js";
@@ -70,7 +73,7 @@ export const workerMainLoop = async (startupData) => {
70
73
  installTerminationGuard();
71
74
  installUnhandledRejectionSilencer();
72
75
  installPerformanceNowGuard();
73
- const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, bufferReferenceReturn, permission, totalNumberOfThread, list, ids, names, at, } = startupData;
76
+ const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, bufferReferenceReturn, permission, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
74
77
  scrubWorkerDataSensitiveBuffers(startupData);
75
78
  assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
76
79
  const debugNamespaces = resolveDebugNamespaces(debug);
@@ -81,10 +84,10 @@ export const workerMainLoop = async (startupData) => {
81
84
  namespaces: debugNamespaces,
82
85
  }))
83
86
  : undefined;
84
- let Comment;
85
- (function (Comment) {
86
- Comment[Comment["thisIsAHint"] = 0] = "thisIsAHint";
87
- })(Comment || (Comment = {}));
87
+ // const object, not `enum`: Andromeda's Nova engine can't parse `enum`.
88
+ const Comment = {
89
+ thisIsAHint: 0,
90
+ };
88
91
  const signals = createSharedMemoryTransport({
89
92
  sabObject: {
90
93
  sharedSab: sab,
@@ -101,6 +104,12 @@ export const workerMainLoop = async (startupData) => {
101
104
  payloadSector: lock.payloadSector,
102
105
  payloadConfig,
103
106
  textCompat: lock.textCompat,
107
+ processBoundary: RUNTIME_IS_PROCESS_WORKER,
108
+ // Under stealing the request region is shared, so decode() becomes a
109
+ // region-Dekker claim against the other endpoints.
110
+ consumers: steal?.consumers,
111
+ consumerId: steal?.consumerId,
112
+ regionLanes: steal?.regionLanes,
104
113
  });
105
114
  const returnLockState = lock2({
106
115
  headers: returnLock.headers,
@@ -110,6 +119,7 @@ export const workerMainLoop = async (startupData) => {
110
119
  payloadSector: returnLock.payloadSector,
111
120
  payloadConfig,
112
121
  textCompat: returnLock.textCompat,
122
+ processBoundary: RUNTIME_IS_PROCESS_WORKER,
113
123
  });
114
124
  const timers = workerOptions?.timers;
115
125
  const spinMicroseconds = timers?.spinMicroseconds ??
@@ -158,6 +168,7 @@ export const workerMainLoop = async (startupData) => {
158
168
  returnLock: returnLockState,
159
169
  borrowReturnedBufferReferences: bufferReferenceReturn === "borrow",
160
170
  hasAborted: abortSignals?.hasAborted,
171
+ stealing: steal !== undefined,
161
172
  });
162
173
  installBufferReferenceReleaseListener(releaseReturnedBufferReference);
163
174
  a_store(rxStatus, 0, 1);
@@ -170,6 +181,7 @@ export const workerMainLoop = async (startupData) => {
170
181
  pauseInNanoseconds: timers?.pauseNanoseconds,
171
182
  enqueueLock,
172
183
  write: () => hasCompleted() ? writeBatch(WRITE_MAX) : 0,
184
+ flushBeforeClaim: steal !== undefined,
173
185
  nativeWaitU32,
174
186
  useSharedMemoryWait: !(RUNTIME_IS_PROCESS_WORKER &&
175
187
  RUNTIME === "node" &&
@@ -245,15 +257,30 @@ export const workerMainLoop = async (startupData) => {
245
257
  pauseUntil(value, spinMicroseconds, parkMs);
246
258
  }
247
259
  : pauseUntil;
260
+ const flushBeforeClaim = steal !== undefined;
248
261
  const loop = () => {
249
262
  isInMacro = false;
250
263
  let progressed = true;
251
264
  let awaiting = 0;
252
265
  while (true) {
253
- progressed = _enqueueLock();
254
- if (_hasCompleted()) {
255
- if (_writeBatch(WRITE_MAX) > 0)
256
- progressed = true;
266
+ if (flushBeforeClaim) {
267
+ // Stealing only. Flush finished work before taking more on: a computed
268
+ // response should not wait behind a claim, and that claim can block on
269
+ // a peer withdrawing its intent. Measured to hurt the per-lane path,
270
+ // where a claim is a cheap private decode and picking work up promptly
271
+ // matters more, so the classic order is kept below.
272
+ if (_hasCompleted()) {
273
+ if (_writeBatch(WRITE_MAX) > 0)
274
+ progressed = true;
275
+ }
276
+ progressed = _enqueueLock() || progressed;
277
+ }
278
+ else {
279
+ progressed = _enqueueLock();
280
+ if (_hasCompleted()) {
281
+ if (_writeBatch(WRITE_MAX) > 0)
282
+ progressed = true;
283
+ }
257
284
  }
258
285
  _drainReturnReleases();
259
286
  if (_hasPending()) {
@@ -295,15 +322,23 @@ export const workerMainLoop = async (startupData) => {
295
322
  };
296
323
  const isWorkerGlobalScope = () => {
297
324
  const scopeCtor = globalThis.WorkerGlobalScope;
298
- if (typeof scopeCtor !== "function")
299
- return false;
300
- try {
301
- return globalThis instanceof
302
- scopeCtor;
325
+ if (typeof scopeCtor === "function") {
326
+ try {
327
+ if (globalThis instanceof scopeCtor) {
328
+ return true;
329
+ }
330
+ }
331
+ catch {
332
+ // fall through to runtime-specific checks
333
+ }
303
334
  }
304
- catch {
305
- return false;
335
+ // Andromeda has no `WorkerGlobalScope`; its worker scope carries a `self`
336
+ // global that the main thread lacks.
337
+ if (IS_ANDROMEDA &&
338
+ typeof globalThis.self !== "undefined") {
339
+ return true;
306
340
  }
341
+ return false;
307
342
  };
308
343
  const isLockBuffers = (value) => {
309
344
  if (!value || typeof value !== "object")
@@ -9,9 +9,19 @@ type ArgumentsForCreateWorkerQueue = {
9
9
  borrowReturnedBufferReferences?: boolean;
10
10
  hasAborted?: (signal: number) => boolean;
11
11
  now?: () => number;
12
+ /**
13
+ * Work-stealing discipline: only claim when this worker has nothing left to
14
+ * run. Without it a worker keeps claiming whole regions on top of a backlog
15
+ * it has not started, which is exactly the hoarding stealing exists to
16
+ * prevent — the queued tasks would be better taken by an idle peer.
17
+ *
18
+ * Off for the classic per-lane path, where the host has already assigned
19
+ * these tasks to this lane and batching them is a win.
20
+ */
21
+ stealing?: boolean;
12
22
  };
13
23
  export type CreateWorkerRxQueue = ReturnType<typeof createWorkerRxQueue>;
14
- export declare const createWorkerRxQueue: ({ listOfFunctions, workerOptions, lock, returnLock, borrowReturnedBufferReferences, hasAborted, now, }: ArgumentsForCreateWorkerQueue) => {
24
+ export declare const createWorkerRxQueue: ({ listOfFunctions, workerOptions, lock, returnLock, borrowReturnedBufferReferences, hasAborted, now, stealing, }: ArgumentsForCreateWorkerQueue) => {
15
25
  hasCompleted: () => boolean;
16
26
  hasPending: () => boolean;
17
27
  writeBatch: (max: number) => number;
@@ -2,7 +2,7 @@ import RingQueue from "../ipc/tools/ring-queue.js";
2
2
  import { attachPayloadTransportFinalizer, runTaskFinalizers, TaskFlag, TaskIndex, } from "../memory/lock.js";
3
3
  import { composeWorkerRunner } from "./composable-runners.js";
4
4
  import { BUFFER_REFERENCE_RETURN_RELEASE_TOKEN } from "../connections/buffer-reference.js";
5
- export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, returnLock, borrowReturnedBufferReferences, hasAborted, now, }) => {
5
+ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, returnLock, borrowReturnedBufferReferences, hasAborted, now, stealing, }) => {
6
6
  const PLACE_HOLDER = (_) => {
7
7
  throw ("UNREACHABLE FROM PLACE HOLDER (thread)");
8
8
  };
@@ -71,6 +71,9 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
71
71
  const { decode, resolved } = lock;
72
72
  const resolvedShift = () => resolved.shiftNoClear();
73
73
  const enqueueLock = () => {
74
+ // Steal only when idle: leaving work unclaimed lets a free peer take it.
75
+ if (stealing && toWork.size !== 0)
76
+ return false;
74
77
  if (!decode())
75
78
  return false;
76
79
  let task = resolvedShift();
@@ -9,10 +9,11 @@ type GetFunctionParams = {
9
9
  permission?: ResolvedPermissionProtocol;
10
10
  };
11
11
  type WorkerCallable = (args: unknown, abortToolkit?: unknown) => unknown;
12
- export declare const enum TimeoutKind {
13
- Reject = 0,
14
- Resolve = 1
15
- }
12
+ export declare const TimeoutKind: {
13
+ readonly Reject: 0;
14
+ readonly Resolve: 1;
15
+ };
16
+ export type TimeoutKind = typeof TimeoutKind[keyof typeof TimeoutKind];
16
17
  export type TimeoutSpec = {
17
18
  ms: number;
18
19
  kind: TimeoutKind;
@@ -1,27 +1,33 @@
1
1
  import { endpointSymbol } from "../common/task-symbol.js";
2
- import { toModuleUrl } from "../common/module-url.js";
2
+ import { toImportSpecifier, toModuleUrl } from "../common/module-url.js";
3
+ // const object, not `const enum`: Andromeda's Nova engine can't parse `enum`.
4
+ // No longer inlined, but only read on cold paths.
5
+ export const TimeoutKind = {
6
+ Reject: 0,
7
+ Resolve: 1,
8
+ };
3
9
  const normalizeTimeout = (timeout) => {
4
10
  if (timeout == null)
5
11
  return undefined;
6
12
  if (typeof timeout === "number") {
7
13
  const ms = Math.floor(timeout);
8
14
  return ms >= 0
9
- ? { ms, kind: 0 /* TimeoutKind.Reject */, value: new Error("Task timeout") }
15
+ ? { ms, kind: TimeoutKind.Reject, value: new Error("Task timeout") }
10
16
  : undefined;
11
17
  }
12
18
  const ms = Math.floor(timeout.time);
13
19
  if (!(ms >= 0))
14
20
  return undefined;
15
21
  if ("default" in timeout) {
16
- return { ms, kind: 1 /* TimeoutKind.Resolve */, value: timeout.default };
22
+ return { ms, kind: TimeoutKind.Resolve, value: timeout.default };
17
23
  }
18
24
  if (timeout.maybe === true) {
19
- return { ms, kind: 1 /* TimeoutKind.Resolve */, value: undefined };
25
+ return { ms, kind: TimeoutKind.Resolve, value: undefined };
20
26
  }
21
27
  if ("error" in timeout) {
22
- return { ms, kind: 0 /* TimeoutKind.Reject */, value: timeout.error };
28
+ return { ms, kind: TimeoutKind.Reject, value: timeout.error };
23
29
  }
24
- return { ms, kind: 0 /* TimeoutKind.Reject */, value: new Error("Task timeout") };
30
+ return { ms, kind: TimeoutKind.Reject, value: new Error("Task timeout") };
25
31
  };
26
32
  const composeWorkerCallable = (fixed, _permission) => {
27
33
  return fixed.f;
@@ -30,7 +36,7 @@ export const getFunctions = async ({ list, ids, names, at, permission }) => {
30
36
  const modules = list.map((specifier) => toModuleUrl(specifier));
31
37
  const nameSet = new Set(names);
32
38
  const results = await Promise.all(modules.map(async (imports) => {
33
- const module = (await import(imports));
39
+ const module = (await import(toImportSpecifier(imports)));
34
40
  const fixedTasks = Object.entries(module)
35
41
  .filter(([_, value]) => value != null && typeof value === "object" &&
36
42
  //@ts-ignore Reason -> trust me
@@ -4,7 +4,7 @@ type PauseOptions = {
4
4
  export type NativeWaitU32 = (buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, expected: number, timeoutMs?: number) => unknown;
5
5
  export declare const whilePausing: ({ pauseInNanoseconds }: PauseOptions) => () => void;
6
6
  export declare const pauseGeneric: () => void;
7
- export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait, }: {
7
+ export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait, flushBeforeClaim, }: {
8
8
  opView: Int32Array;
9
9
  rxStatus: Int32Array;
10
10
  txStatus: Int32Array;
@@ -14,5 +14,12 @@ export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxSta
14
14
  write?: () => number | boolean;
15
15
  nativeWaitU32?: NativeWaitU32;
16
16
  useSharedMemoryWait?: boolean;
17
+ /**
18
+ * Stealing only: write results back before attempting to claim more work.
19
+ * A claim can spin waiting for a peer to withdraw its intent, so claiming
20
+ * first puts arbitration latency in front of a finished response. Measured
21
+ * to hurt the per-lane path, so it stays off by default.
22
+ */
23
+ flushBeforeClaim?: boolean;
17
24
  }) => (value: number, spinMicroseconds: number, parkMs?: number) => void;
18
25
  export {};
@@ -1,7 +1,7 @@
1
- var Comment;
2
- (function (Comment) {
3
- Comment[Comment["thisIsAHint"] = 0] = "thisIsAHint";
4
- })(Comment || (Comment = {}));
1
+ // const object, not `enum`: Andromeda's Nova engine can't parse `enum`.
2
+ const Comment = {
3
+ thisIsAHint: 0,
4
+ };
5
5
  const maybeGc = (() => {
6
6
  const host = globalThis;
7
7
  const gc = typeof host.gc === "function"
@@ -30,7 +30,9 @@ const a_load = Atomics.load;
30
30
  const a_store = Atomics.store;
31
31
  const a_wait = typeof Atomics.wait === "function" ? Atomics.wait : undefined;
32
32
  const p_now = performance.now.bind(performance);
33
- const waitFallbackView = a_wait === undefined
33
+ // `SharedArrayBuffer` is undeclared on pages that are not cross-origin
34
+ // isolated, and this module has to load anyway so the pool can report why.
35
+ const waitFallbackView = a_wait === undefined || typeof SharedArrayBuffer !== "function"
34
36
  ? undefined
35
37
  : new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
36
38
  const a_pause = "pause" in Atomics
@@ -51,6 +53,7 @@ const isPlainNodeWindows = runtimeGlobals.process?.platform === "win32" &&
51
53
  // runners appear to hang. The native wait already polls internally, so this is
52
54
  // just bounding each poll slice.
53
55
  const nativeWaitTimeoutMs = (parkMs) => isPlainNodeWindows ? 1 : Number.isFinite(parkMs) ? parkMs : Infinity;
56
+ const pollingWaitTimeoutMs = (parkMs) => Number.isFinite(parkMs) ? Math.min(Math.max(parkMs, 0), 1) : 1;
54
57
  export const whilePausing = ({ pauseInNanoseconds }) => {
55
58
  const forNanoseconds = pauseInNanoseconds ?? DEFAULT_PAUSE_TIME;
56
59
  if (!a_pause || forNanoseconds <= 0)
@@ -58,26 +61,27 @@ export const whilePausing = ({ pauseInNanoseconds }) => {
58
61
  return () => a_pause(forNanoseconds);
59
62
  };
60
63
  export const pauseGeneric = whilePausing({});
61
- export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait = true, }) => {
64
+ export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait = true, flushBeforeClaim = false, }) => {
62
65
  const pause = pauseInNanoseconds === undefined
63
66
  ? pauseGeneric
64
67
  : whilePausing({ pauseInNanoseconds });
65
- const tryProgress = () => {
66
- let progressed = false;
67
- if (enqueueLock())
68
- progressed = true;
69
- if (write) {
70
- const wrote = write();
71
- if (typeof wrote === "number") {
72
- if (wrote > 0)
73
- progressed = true;
74
- }
75
- else if (wrote === true) {
76
- progressed = true;
77
- }
78
- }
79
- return progressed;
68
+ const flushWrite = () => {
69
+ if (!write)
70
+ return false;
71
+ const wrote = write();
72
+ if (typeof wrote === "number")
73
+ return wrote > 0;
74
+ return wrote === true;
80
75
  };
76
+ const tryProgress = flushBeforeClaim
77
+ ? () => {
78
+ const wrote = flushWrite();
79
+ return enqueueLock() || wrote;
80
+ }
81
+ : () => {
82
+ const claimed = enqueueLock();
83
+ return flushWrite() || claimed;
84
+ };
81
85
  return (value, spinMicroseconds, parkMs) => {
82
86
  const until = p_now() + (spinMicroseconds / 1000);
83
87
  maybeGc();
@@ -108,8 +112,9 @@ export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, tx
108
112
  }
109
113
  else if (a_wait && waitFallbackView) {
110
114
  // waitFallbackView is never notified — this wait only ever times out and
111
- // re-polls, so it must stay finite or the worker would hang forever.
112
- a_wait(waitFallbackView, 0, 0, Number.isFinite(parkMs) ? parkMs : Infinity);
115
+ // re-polls. Keep the slice short and finite so FFI-backed process workers
116
+ // stay responsive without a cross-process native wake primitive.
117
+ a_wait(waitFallbackView, 0, 0, pollingWaitTimeoutMs(parkMs));
113
118
  }
114
119
  a_store(rxStatus, 0, 1);
115
120
  };