knitting 0.1.63 → 0.1.70

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 (80) hide show
  1. package/README.md +525 -335
  2. package/knitting.browser.js +1 -1
  3. package/map.md +0 -6
  4. package/package.json +3 -3
  5. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  6. package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
  7. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  8. package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
  9. package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
  11. package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
  12. package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
  13. package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
  14. package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
  15. package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
  16. package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
  17. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  18. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  19. package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
  20. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  21. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  22. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  23. package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
  24. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  25. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  26. package/scripts/build-native-addons.ts +5 -0
  27. package/shared-memory.d.ts +3 -0
  28. package/shared-memory.js +3 -0
  29. package/src/api.js +105 -42
  30. package/src/common/with-resolvers.js +2 -5
  31. package/src/common/worker-runtime.d.ts +7 -0
  32. package/src/common/worker-runtime.js +7 -0
  33. package/src/connections/buffer-reference.d.ts +10 -36
  34. package/src/connections/buffer-reference.js +15 -170
  35. package/src/connections/node-addons.d.ts +1 -1
  36. package/src/connections/shared-array-buffer-payload.d.ts +7 -0
  37. package/src/connections/shared-array-buffer-payload.js +27 -11
  38. package/src/knitting_buffer_pointer.cc +57 -2
  39. package/src/knitting_doorbell.cc +220 -0
  40. package/src/memory/knitting-body.d.ts +44 -0
  41. package/src/memory/knitting-body.js +51 -0
  42. package/src/memory/knitting-buffer-http.d.ts +116 -0
  43. package/src/memory/knitting-buffer-http.js +255 -0
  44. package/src/memory/knitting-buffer.d.ts +250 -0
  45. package/src/memory/knitting-buffer.js +695 -0
  46. package/src/memory/lazy-region-registry.d.ts +83 -0
  47. package/src/memory/lazy-region-registry.js +355 -0
  48. package/src/memory/lock.d.ts +38 -15
  49. package/src/memory/lock.js +205 -79
  50. package/src/memory/payloadCodec.d.ts +18 -2
  51. package/src/memory/payloadCodec.js +309 -65
  52. package/src/memory/regionRegistry.d.ts +6 -0
  53. package/src/memory/regionRegistry.js +125 -240
  54. package/src/memory/shared-buffer-io.d.ts +7 -0
  55. package/src/memory/shared-buffer-io.js +34 -8
  56. package/src/runtime/deno-doorbell.d.ts +26 -0
  57. package/src/runtime/deno-doorbell.js +117 -0
  58. package/src/runtime/dispatcher.d.ts +8 -6
  59. package/src/runtime/dispatcher.js +80 -58
  60. package/src/runtime/host-arg-arena.d.ts +3 -0
  61. package/src/runtime/host-arg-arena.js +16 -0
  62. package/src/runtime/node-doorbell.d.ts +14 -0
  63. package/src/runtime/node-doorbell.js +84 -0
  64. package/src/runtime/pool.d.ts +21 -15
  65. package/src/runtime/pool.js +104 -116
  66. package/src/runtime/process-worker.d.ts +9 -0
  67. package/src/runtime/process-worker.js +22 -2
  68. package/src/runtime/tx-queue.d.ts +2 -5
  69. package/src/runtime/tx-queue.js +52 -48
  70. package/src/types.d.ts +35 -71
  71. package/src/worker/loop.js +79 -57
  72. package/src/worker/rx-queue.d.ts +2 -3
  73. package/src/worker/rx-queue.js +34 -40
  74. package/src/worker/shared-return.d.ts +9 -0
  75. package/src/worker/shared-return.js +22 -0
  76. package/src/worker/task-loader.js +1 -2
  77. package/src/worker/timers.d.ts +2 -6
  78. package/src/worker/timers.js +14 -19
  79. package/unsafe.d.ts +2 -1
  80. package/unsafe.js +2 -1
@@ -1,12 +1,12 @@
1
1
  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
- import { BUFFER_REFERENCE_RETURN_RELEASE_TOKEN } from "../connections/buffer-reference.js";
5
- export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, returnLock, borrowReturnedBufferReferences, hasAborted, now, stealing, }) => {
4
+ /** Tasks run per `serviceBatchImmediate` call before returning to the loop. */
5
+ const SERVICE_BATCH_MAX = 32;
6
+ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, returnLock, hasAborted, now, stealing, }) => {
6
7
  const PLACE_HOLDER = (_) => {
7
8
  throw ("UNREACHABLE FROM PLACE HOLDER (thread)");
8
9
  };
9
- let hasAnythingFinished = 0;
10
10
  let awaiting = 0;
11
11
  const jobs = listOfFunctions.reduce((acc, fixed) => (acc.push(fixed.run), acc), []);
12
12
  const toWork = new RingQueue();
@@ -26,12 +26,7 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
26
26
  const returnHostBits = returnLock.hostBits;
27
27
  const returnWorkerBits = returnLock.workerBits;
28
28
  const deferredReleases = [];
29
- const explicitReturnReleases = new Map();
30
- const drainReturnReleases = () => {
31
- if (deferredReleases.length === 0)
32
- return;
33
- if ((a_load(returnHostBits, 0) ^ a_load(returnWorkerBits, 0)) !== 0)
34
- return;
29
+ const releaseDeferredReturns = () => {
35
30
  for (let i = 0; i < deferredReleases.length; i++) {
36
31
  try {
37
32
  deferredReleases[i]();
@@ -42,18 +37,17 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
42
37
  }
43
38
  deferredReleases.length = 0;
44
39
  };
45
- const releaseReturnedBufferReference = (token) => {
46
- const key = token.toString();
47
- const release = explicitReturnReleases.get(key);
48
- if (release === undefined)
40
+ const drainReturnReleases = () => {
41
+ if (deferredReleases.length === 0)
49
42
  return;
50
- explicitReturnReleases.delete(key);
51
- try {
52
- release();
53
- }
54
- catch {
55
- // best effort
56
- }
43
+ // `returnHostBits` is the publication word of the return lock, and only
44
+ // this thread ever writes it (encode() toggles hostBits, the host toggles
45
+ // workerBits). Reading our own word plainly skips an `Atomics.load`, which
46
+ // is a non-inlined call an order of magnitude dearer than a plain read;
47
+ // the peer's word still needs the atomic.
48
+ if ((returnHostBits[0] ^ a_load(returnWorkerBits, 0)) !== 0)
49
+ return;
50
+ releaseDeferredReturns();
57
51
  };
58
52
  const runByIndex = listOfFunctions.reduce((acc, fixed, idx) => {
59
53
  const job = jobs[idx];
@@ -65,9 +59,13 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
65
59
  }));
66
60
  return acc;
67
61
  }, []);
62
+ // A settled task is either written into the return lock straight away or
63
+ // parked in `pendingFrames`, so the queue's own size already answers "is
64
+ // there anything left to flush" — the separate counter this used to keep was
65
+ // an increment and a decrement per task tracking a value it could read.
68
66
  const hasCompleted = workerOptions?.resolveAfterFinishingAll === true
69
- ? () => hasAnythingFinished !== 0 && toWork.size === 0
70
- : () => hasAnythingFinished !== 0;
67
+ ? () => pendingFrames.size !== 0 && toWork.size === 0
68
+ : () => pendingFrames.size !== 0;
71
69
  const { decode, resolved } = lock;
72
70
  const resolvedShift = () => resolved.shiftNoClear();
73
71
  const enqueueLock = () => {
@@ -86,34 +84,22 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
86
84
  }
87
85
  return true;
88
86
  };
89
- const encodeReturnSafe = (slot) => {
90
- if (!returnLock.encode(slot))
91
- return false;
92
- return true;
93
- };
87
+ const returnEncode = returnLock.encode;
94
88
  const sendReturn = (slot, shouldReject) => {
95
89
  slot[IDX_FLAGS] = shouldReject ? FLAG_REJECT : 0;
96
- if (!encodeReturnSafe(slot))
90
+ if (!returnEncode(slot))
97
91
  return false;
98
92
  // Preserve return finalizers past slot recycle; release after drain.
99
93
  if (slot.finalize !== undefined) {
100
- const token = slot.finalize[BUFFER_REFERENCE_RETURN_RELEASE_TOKEN];
101
- if (token === undefined || borrowReturnedBufferReferences !== true) {
102
- deferredReleases.push(slot.finalize);
103
- }
104
- else {
105
- explicitReturnReleases.set(token.toString(), slot.finalize);
106
- }
94
+ deferredReleases.push(slot.finalize);
107
95
  slot.finalize = undefined;
108
96
  }
109
- hasAnythingFinished--;
110
97
  recyclePush(slot);
111
98
  return true;
112
99
  };
113
100
  const settleNow = (slot, isError, value, wasAwaited) => {
114
101
  runTaskFinalizers(slot);
115
102
  slot.value = value;
116
- hasAnythingFinished++;
117
103
  if (wasAwaited && awaiting > 0)
118
104
  awaiting--;
119
105
  const shouldReject = isError ||
@@ -145,14 +131,20 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
145
131
  },
146
132
  serviceBatchImmediate: () => {
147
133
  let processed = 0;
148
- while (processed < 5 && toWork.size !== 0) {
134
+ // Every trip back out to the dispatch loop costs a claim attempt and the
135
+ // per-pass bookkeeping, so a low cap makes that scaffolding a per-task
136
+ // tax on cheap tasks. Run a longer run when the return lock is keeping
137
+ // up, and cut it short the moment a settle lands in `pendingFrames` --
138
+ // that means the return region is full and the right move is to go
139
+ // flush it rather than pile up more finished work.
140
+ while (processed < SERVICE_BATCH_MAX && toWork.size !== 0) {
149
141
  const slot = toWorkShift();
150
142
  try {
151
143
  const fnIndex = slot[TaskIndex.FunctionID] & FUNCTION_ID_MASK;
152
144
  const result = runByIndex[fnIndex](slot);
153
145
  slot[IDX_FLAGS] = 0;
154
- slot.value = null;
155
146
  if (result instanceof Promise) {
147
+ slot.value = null;
156
148
  awaiting++;
157
149
  result.then((value) => settleNow(slot, false, value, true), (err) => settleNow(slot, true, err, true));
158
150
  }
@@ -164,12 +156,14 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
164
156
  settleNow(slot, true, err, false);
165
157
  }
166
158
  ++processed;
159
+ if (pendingFrames.size !== 0)
160
+ break;
167
161
  }
168
162
  return processed;
169
163
  },
170
164
  enqueueLock,
171
165
  drainReturnReleases,
172
- releaseReturnedBufferReference,
166
+ releaseDeferredReturns,
173
167
  hasAwaiting: () => awaiting > 0,
174
168
  getAwaiting: () => awaiting,
175
169
  };
@@ -0,0 +1,9 @@
1
+ /** Point `sharedBytes` at the arena of `payload`, this worker's return lane. */
2
+ export declare const installSharedReturn: (payload: object) => void;
3
+ export declare const resetSharedReturn: () => void;
4
+ /**
5
+ * Allocate an uninitialized borrowed return buffer. The caller must write every
6
+ * byte it returns and must not retain the view beyond the borrow window.
7
+ * Ordinary allocation is used when borrowing is unavailable.
8
+ */
9
+ export declare const sharedBytes: (byteLength: number, zeroFill?: boolean) => Uint8Array;
@@ -0,0 +1,22 @@
1
+ import { getSharedReturnAllocator } from "../memory/payloadCodec.js";
2
+ // `sharedBytes` uses the worker's return-lane arena; outside that context it
3
+ // falls back to an ordinary Uint8Array.
4
+ let allocate;
5
+ /** Point `sharedBytes` at the arena of `payload`, this worker's return lane. */
6
+ export const installSharedReturn = (payload) => {
7
+ allocate = getSharedReturnAllocator(payload);
8
+ };
9
+ export const resetSharedReturn = () => {
10
+ allocate = undefined;
11
+ };
12
+ /**
13
+ * Allocate an uninitialized borrowed return buffer. The caller must write every
14
+ * byte it returns and must not retain the view beyond the borrow window.
15
+ * Ordinary allocation is used when borrowing is unavailable.
16
+ */
17
+ export const sharedBytes = (byteLength, zeroFill = false) => {
18
+ if (!Number.isInteger(byteLength) || byteLength < 0) {
19
+ throw new RangeError("sharedBytes(byteLength) requires a non-negative integer");
20
+ }
21
+ return allocate?.(byteLength, zeroFill) ?? new Uint8Array(byteLength);
22
+ };
@@ -1,7 +1,6 @@
1
1
  import { endpointSymbol } from "../common/task-symbol.js";
2
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.
3
+ // Use a const object; Andromeda's Nova engine cannot parse `enum`.
5
4
  export const TimeoutKind = {
6
5
  Reject: 0,
7
6
  Resolve: 1,
@@ -2,6 +2,7 @@ type PauseOptions = {
2
2
  pauseInNanoseconds?: number;
3
3
  };
4
4
  export type NativeWaitU32 = (buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, expected: number, timeoutMs?: number) => unknown;
5
+ export declare const IS_BUN_WINDOWS: boolean;
5
6
  export declare const whilePausing: ({ pauseInNanoseconds }: PauseOptions) => () => void;
6
7
  export declare const pauseGeneric: () => void;
7
8
  export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait, flushBeforeClaim, }: {
@@ -14,12 +15,7 @@ export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxSta
14
15
  write?: () => number | boolean;
15
16
  nativeWaitU32?: NativeWaitU32;
16
17
  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
- */
18
+ /** Stealing only: flush results before claiming more work. */
23
19
  flushBeforeClaim?: boolean;
24
20
  }) => (value: number, spinMicroseconds: number, parkMs?: number) => void;
25
21
  export {};
@@ -30,8 +30,7 @@ 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
- // `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.
33
+ // Some pages do not define SharedArrayBuffer; keep this module loadable there.
35
34
  const waitFallbackView = a_wait === undefined || typeof SharedArrayBuffer !== "function"
36
35
  ? undefined
37
36
  : new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
@@ -43,17 +42,16 @@ const isPlainNodeWindows = runtimeGlobals.process?.platform === "win32" &&
43
42
  typeof runtimeGlobals.process?.versions?.node === "string" &&
44
43
  runtimeGlobals.process?.versions?.bun === undefined &&
45
44
  runtimeGlobals.Deno === undefined;
46
- // Windows has no working cross-process wake. The native wake (WakeByAddress)
47
- // is keyed to a virtual address, so a host cannot wake a process worker parked
48
- // on the same physical page mapped at a different address in the child — the
49
- // addon's FutexWake is a no-op there. A parked Windows Node process worker can
50
- // therefore never be signalled and must rediscover work by polling. Cap the
51
- // native wait at 1ms so it re-checks every millisecond instead of sleeping the
52
- // full parkMs (up to seconds); the long park with no wake is what made CI
53
- // runners appear to hang. The native wait already polls internally, so this is
54
- // just bounding each poll slice.
45
+ // Bun on Windows requires special handling for short waits and process rings.
46
+ export const IS_BUN_WINDOWS = runtimeGlobals.process?.platform === "win32" &&
47
+ typeof runtimeGlobals.process?.versions?.bun === "string";
48
+ // Bun on Windows busy-spins for waits of 1ms or less; use a 2ms minimum.
49
+ const BUN_WINDOWS_MIN_WAIT_MS = 2;
50
+ const floorBunWindowsWait = (ms) => IS_BUN_WINDOWS && ms < BUN_WINDOWS_MIN_WAIT_MS ? BUN_WINDOWS_MIN_WAIT_MS : ms;
51
+ // Windows process workers cannot be woken through the native address-based
52
+ // wait, so cap their wait to 1ms and let polling rediscover work.
55
53
  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
+ const pollingWaitTimeoutMs = (parkMs) => floorBunWindowsWait(Number.isFinite(parkMs) ? Math.min(Math.max(parkMs, 0), 1) : 1);
57
55
  export const whilePausing = ({ pauseInNanoseconds }) => {
58
56
  const forNanoseconds = pauseInNanoseconds ?? DEFAULT_PAUSE_TIME;
59
57
  if (!a_pause || forNanoseconds <= 0)
@@ -105,15 +103,12 @@ export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, tx
105
103
  else if (useSharedMemoryWait &&
106
104
  a_wait &&
107
105
  opView.buffer instanceof SharedArrayBuffer) {
108
- // This is the notify-able path: a host Atomics.notify on opView wakes it.
109
- // An infinite parkMs (debug) is intentional here — the worker blocks until
110
- // a real signal instead of re-polling on a timeout.
111
- a_wait(opView, at, value, parkMs ?? Infinity);
106
+ // This path can be woken with Atomics.notify; an infinite debug wait is
107
+ // intentional.
108
+ a_wait(opView, at, value, floorBunWindowsWait(parkMs ?? Infinity));
112
109
  }
113
110
  else if (a_wait && waitFallbackView) {
114
- // waitFallbackView is never notified — this wait only ever times out and
115
- // re-polls. Keep the slice short and finite so FFI-backed process workers
116
- // stay responsive without a cross-process native wake primitive.
111
+ // This fallback is timeout-driven; keep the slice short for responsiveness.
117
112
  a_wait(waitFallbackView, 0, 0, pollingWaitTimeoutMs(parkMs));
118
113
  }
119
114
  a_store(rxStatus, 0, 1);
package/unsafe.d.ts CHANGED
@@ -1 +1,2 @@
1
- export { BUFFER_REFERENCE_CODEC_ID, BUFFER_REFERENCE_KIND, BufferReference, type BufferReferenceMetadata, BufferReferenceReturn, type BufferReferenceRuntime, isBufferReferenceMetadata, } from "./src/connections/buffer-reference.js";
1
+ export { BUFFER_REFERENCE_CODEC_ID, BUFFER_REFERENCE_KIND, BufferReference, type BufferReferenceMetadata, type BufferReferenceRuntime, isBufferReferenceMetadata, } from "./src/connections/buffer-reference.js";
2
+ export { sharedBytes } from "./src/worker/shared-return.js";
package/unsafe.js CHANGED
@@ -1 +1,2 @@
1
- export { BUFFER_REFERENCE_CODEC_ID, BUFFER_REFERENCE_KIND, BufferReference, BufferReferenceReturn, isBufferReferenceMetadata, } from "./src/connections/buffer-reference.js";
1
+ export { BUFFER_REFERENCE_CODEC_ID, BUFFER_REFERENCE_KIND, BufferReference, isBufferReferenceMetadata, } from "./src/connections/buffer-reference.js";
2
+ export { sharedBytes } from "./src/worker/shared-return.js";