knitting 0.1.60 → 0.1.62

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 (46) hide show
  1. package/BROWSER.md +134 -0
  2. package/README.md +94 -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 +136 -25
  20. package/src/common/runtime.d.ts +2 -1
  21. package/src/common/runtime.js +8 -1
  22. package/src/common/task-source.d.ts +9 -0
  23. package/src/common/task-source.js +15 -0
  24. package/src/memory/lock.d.ts +37 -1
  25. package/src/memory/lock.js +262 -5
  26. package/src/memory/payloadCodec.js +2 -6
  27. package/src/memory/regionRegistry.d.ts +1 -3
  28. package/src/memory/regionRegistry.js +16 -14
  29. package/src/permission/protocol.js +5 -1
  30. package/src/runtime/dispatcher.d.ts +11 -8
  31. package/src/runtime/dispatcher.js +163 -21
  32. package/src/runtime/inline-executor.js +6 -1
  33. package/src/runtime/pool.d.ts +127 -4
  34. package/src/runtime/pool.js +262 -73
  35. package/src/runtime/process-worker.d.ts +24 -12
  36. package/src/runtime/process-worker.js +185 -115
  37. package/src/runtime/tx-queue.d.ts +17 -1
  38. package/src/runtime/tx-queue.js +120 -15
  39. package/src/runtime/worker-common.d.ts +13 -0
  40. package/src/runtime/worker-common.js +103 -0
  41. package/src/types.d.ts +75 -4
  42. package/src/worker/loop.js +34 -5
  43. package/src/worker/rx-queue.d.ts +11 -1
  44. package/src/worker/rx-queue.js +4 -1
  45. package/src/worker/timers.d.ts +8 -1
  46. package/src/worker/timers.js +20 -17
@@ -0,0 +1,103 @@
1
+ // Worker helpers shared by every worker mode. Kept out of `process-worker.ts`
2
+ // so the thread and web-worker paths do not pull the process-worker module —
3
+ // and, through it, the whole native connections tree — into their bundles.
4
+ import { ProcessSharedBuffer } from "../connections/process-shared-buffer.js";
5
+ const execFlagKey = (flag) => flag.split("=", 1)[0];
6
+ const NODE_PERMISSION_EXEC_FLAGS = new Set([
7
+ "--permission",
8
+ "--experimental-permission",
9
+ "--allow-fs-read",
10
+ "--allow-fs-write",
11
+ "--allow-worker",
12
+ "--allow-child-process",
13
+ "--allow-net",
14
+ "--allow-addons",
15
+ "--allow-ffi",
16
+ "--allow-wasi",
17
+ ]);
18
+ const NODE_WORKER_SAFE_EXEC_FLAGS = new Set([
19
+ "--experimental-ffi",
20
+ "--experimental-transform-types",
21
+ "--expose-gc",
22
+ "--no-warnings",
23
+ ...NODE_PERMISSION_EXEC_FLAGS,
24
+ ]);
25
+ const isNodeWorkerSafeExecFlag = (flag) => NODE_WORKER_SAFE_EXEC_FLAGS.has(execFlagKey(flag));
26
+ const isNodePermissionExecFlag = (flag) => NODE_PERMISSION_EXEC_FLAGS.has(execFlagKey(flag));
27
+ export const toWorkerSafeExecArgv = (flags) => {
28
+ if (!flags || flags.length === 0)
29
+ return undefined;
30
+ const filtered = flags.filter(isNodeWorkerSafeExecFlag);
31
+ if (filtered.length === 0)
32
+ return undefined;
33
+ const seen = new Set();
34
+ const deduped = [];
35
+ for (const flag of filtered) {
36
+ if (seen.has(flag))
37
+ continue;
38
+ seen.add(flag);
39
+ deduped.push(flag);
40
+ }
41
+ return deduped;
42
+ };
43
+ export const toWorkerCompatExecArgv = (flags) => {
44
+ const safe = toWorkerSafeExecArgv(flags);
45
+ if (!safe || safe.length === 0)
46
+ return undefined;
47
+ const compat = safe.filter((flag) => !isNodePermissionExecFlag(flag));
48
+ return compat.length > 0 ? compat : undefined;
49
+ };
50
+ const isPlainRecord = (value) => {
51
+ if (value === null || typeof value !== "object")
52
+ return false;
53
+ const prototype = Object.getPrototypeOf(value);
54
+ return prototype === Object.prototype || prototype === null;
55
+ };
56
+ const serializeWorkerBootstrapValue = (value, seen = new WeakMap()) => {
57
+ if (value instanceof ProcessSharedBuffer)
58
+ return value.toMetadata();
59
+ if (value === null || typeof value !== "object")
60
+ return value;
61
+ const existing = seen.get(value);
62
+ if (existing !== undefined)
63
+ return existing;
64
+ if (Array.isArray(value)) {
65
+ const out = [];
66
+ seen.set(value, out);
67
+ for (const item of value) {
68
+ out.push(serializeWorkerBootstrapValue(item, seen));
69
+ }
70
+ return out;
71
+ }
72
+ if (!isPlainRecord(value))
73
+ return value;
74
+ const out = {};
75
+ seen.set(value, out);
76
+ for (const [key, item] of Object.entries(value)) {
77
+ out[key] = serializeWorkerBootstrapValue(item, seen);
78
+ }
79
+ return out;
80
+ };
81
+ export const serializeWorkerBootstrapData = (options) => {
82
+ const bootstrap = options.bootstrap;
83
+ if (bootstrap === undefined || bootstrap.data === undefined)
84
+ return options;
85
+ return {
86
+ ...options,
87
+ bootstrap: {
88
+ ...bootstrap,
89
+ data: serializeWorkerBootstrapValue(bootstrap.data),
90
+ },
91
+ };
92
+ };
93
+ export const terminateWorkerQuietly = (worker) => {
94
+ try {
95
+ // Runaway worker termination can be slow or stuck on some runtimes; once the
96
+ // pool is closing it must not keep the host process alive.
97
+ worker.unref?.();
98
+ return Promise.resolve(worker.terminate()).then(() => { }, () => { });
99
+ }
100
+ catch {
101
+ return Promise.resolve();
102
+ }
103
+ };
package/src/types.d.ts CHANGED
@@ -34,6 +34,19 @@ type WorkerData = {
34
34
  payloadConfig?: PayloadBufferOptions;
35
35
  bufferReferenceReturn?: "copy" | "borrow";
36
36
  permission?: ResolvedPermissionProtocol;
37
+ /** Whether this host can arm an async completion waiter on the return lock. */
38
+ notifyOnHostPublish?: boolean;
39
+ /**
40
+ * Work stealing. When present, `lock` is a submit region shared by every
41
+ * worker and this worker claims from it as consumer `consumerId` of
42
+ * `consumers`, rather than owning a private request lane. `returnLock` stays
43
+ * private — the endpoint that claims a task owns its response.
44
+ */
45
+ steal?: {
46
+ consumers: number;
47
+ consumerId: number;
48
+ regionLanes: number;
49
+ };
37
50
  };
38
51
  type UnsafeOptions = {
39
52
  /**
@@ -330,24 +343,82 @@ type WorkerTimers = {
330
343
  };
331
344
  type DispatcherSettings = {
332
345
  /**
333
- * How many immediate notify loops before backoff kicks in.
346
+ * How many immediate notify loops before the dispatcher stops re-arming the
347
+ * pump for free.
348
+ *
349
+ * The default depends on what the dispatcher escalates *to*. Polling
350
+ * escalates to a `setTimeout` ladder costing ~1.1ms even at delay 0, so it
351
+ * defaults to 128 — escalating is expensive and the wide window also batches
352
+ * completions. A doorbell escalates to `Atomics.waitAsync` at roughly the
353
+ * price of one hop, so pools that have one default to 1. Pools without a
354
+ * doorbell — Deno, or `doorbell: false` — keep 128.
355
+ *
356
+ * Setting this explicitly opts out of that coupling for every pool shape.
334
357
  */
335
358
  stallFreeLoops?: number;
336
359
  /**
337
360
  * Max backoff delay (milliseconds).
338
361
  */
339
362
  maxBackoffMs?: number;
363
+ /**
364
+ * Replace idle completion polling with an `Atomics.waitAsync` doorbell when
365
+ * the host runtime supports it.
366
+ *
367
+ * Defaults to enabled on Node and Bun at any worker count, where it costs
368
+ * 1.6-3.9x less host CPU per completed call. Under HTTP load, where the host
369
+ * has real work of its own, that converts to +13% to +36% throughput.
370
+ *
371
+ * Turn it off for a pool that oversubscribes its machine. The doorbell only
372
+ * progresses when the host gets scheduled, so once workers occupy every core
373
+ * a wake must preempt one: measured +5% to +12.6% rps while workers+host fit
374
+ * within the cores, -22% to -32% once they do not. That is not gated
375
+ * automatically because the core count cannot be probed portably.
376
+ *
377
+ * Forced off, overriding an explicit `true`, where a doorbell cannot work:
378
+ * on Deno, whose `waitAsync` does not wake an idle event loop, and for
379
+ * process workers, which live in another process and so cannot ring a host
380
+ * waiter at all — V8 keeps its Atomics waiter list per isolate, which is why
381
+ * they wake through a native futex addon instead. Compiled (Porffor) workers
382
+ * never reach this path: they reject `host` outright and use pipes rather
383
+ * than shared memory.
384
+ */
385
+ doorbell?: boolean;
340
386
  /**
341
387
  * Host dispatcher topology.
342
388
  * - `"per-thread"`: each worker owns its dispatcher and macro channel.
343
389
  * - `"serial-channel"`: each worker keeps its own dispatcher check state, but
344
390
  * one shared channel runs all lane checks from first to last.
345
391
  *
346
- * Experimental default: `"per-thread"` on Bun or with one worker, otherwise
347
- * `"serial-channel"` for multi-worker Node/Deno pools. Can also be forced
348
- * with the `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
392
+ * Used by private-lane pools. Its experimental default is `"per-thread"` on
393
+ * Bun or with one worker, otherwise `"serial-channel"` for multi-worker
394
+ * Node/Deno pools. Selecting it explicitly keeps private lanes unless
395
+ * `steal: true` is also explicit. Can also be forced with the
396
+ * `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
349
397
  */
350
398
  dispatcher?: "per-thread" | "serial-channel";
399
+ /**
400
+ * Work stealing: one shared submit region that any worker may
401
+ * claim from, private return lanes, and a pool-global pending registry. The
402
+ * endpoint that claims a task owns its response.
403
+ *
404
+ * Enabled by default for compatible multi-worker thread and process pools
405
+ * unless a balancer or private-lane dispatcher was explicitly selected.
406
+ * One-worker pools, inliners, compiled/Porffor workers, and pools above the
407
+ * current 31-claimant protocol limit retain their existing transport. Set
408
+ * `false` (or `KNITTING_STEAL=0`) to opt out; `KNITTING_STEAL=1` explicitly
409
+ * opts in and overrides a balancer/dispatcher selection.
410
+ */
411
+ steal?: boolean;
412
+ /**
413
+ * Lanes claimed per stealing handshake (a power of two, `slots / g >=
414
+ * workers + 1`). Defaults to the widest region the lane budget allows, which
415
+ * amortises arbitration best for cheap tasks.
416
+ *
417
+ * **A region is a batch.** For expensive tasks, a wide region lets one worker
418
+ * claim work the others could have run in parallel; set this to `1` (or a
419
+ * small value) when per-task cost dominates arbitration cost.
420
+ */
421
+ stealRegionLanes?: number;
351
422
  };
352
423
  type CreatePool = {
353
424
  /** Number of workers. Default: 1. */
@@ -73,7 +73,7 @@ export const workerMainLoop = async (startupData) => {
73
73
  installTerminationGuard();
74
74
  installUnhandledRejectionSilencer();
75
75
  installPerformanceNowGuard();
76
- 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, notifyOnHostPublish, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
77
77
  scrubWorkerDataSensitiveBuffers(startupData);
78
78
  assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
79
79
  const debugNamespaces = resolveDebugNamespaces(debug);
@@ -105,6 +105,11 @@ export const workerMainLoop = async (startupData) => {
105
105
  payloadConfig,
106
106
  textCompat: lock.textCompat,
107
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,
108
113
  });
109
114
  const returnLockState = lock2({
110
115
  headers: returnLock.headers,
@@ -115,6 +120,9 @@ export const workerMainLoop = async (startupData) => {
115
120
  payloadConfig,
116
121
  textCompat: returnLock.textCompat,
117
122
  processBoundary: RUNTIME_IS_PROCESS_WORKER,
123
+ // The host parks on this lock's publication word when it has no work to
124
+ // flush. Request locks are host-produced and must not wake that waiter.
125
+ notifyOnHostPublish,
118
126
  });
119
127
  const timers = workerOptions?.timers;
120
128
  const spinMicroseconds = timers?.spinMicroseconds ??
@@ -163,6 +171,7 @@ export const workerMainLoop = async (startupData) => {
163
171
  returnLock: returnLockState,
164
172
  borrowReturnedBufferReferences: bufferReferenceReturn === "borrow",
165
173
  hasAborted: abortSignals?.hasAborted,
174
+ stealing: steal !== undefined,
166
175
  });
167
176
  installBufferReferenceReleaseListener(releaseReturnedBufferReference);
168
177
  a_store(rxStatus, 0, 1);
@@ -175,6 +184,7 @@ export const workerMainLoop = async (startupData) => {
175
184
  pauseInNanoseconds: timers?.pauseNanoseconds,
176
185
  enqueueLock,
177
186
  write: () => hasCompleted() ? writeBatch(WRITE_MAX) : 0,
187
+ flushBeforeClaim: steal !== undefined,
178
188
  nativeWaitU32,
179
189
  useSharedMemoryWait: !(RUNTIME_IS_PROCESS_WORKER &&
180
190
  RUNTIME === "node" &&
@@ -250,15 +260,34 @@ export const workerMainLoop = async (startupData) => {
250
260
  pauseUntil(value, spinMicroseconds, parkMs);
251
261
  }
252
262
  : pauseUntil;
263
+ const flushBeforeClaim = steal !== undefined;
253
264
  const loop = () => {
254
265
  isInMacro = false;
255
266
  let progressed = true;
256
267
  let awaiting = 0;
257
268
  while (true) {
258
- progressed = _enqueueLock();
259
- if (_hasCompleted()) {
260
- if (_writeBatch(WRITE_MAX) > 0)
261
- progressed = true;
269
+ if (flushBeforeClaim) {
270
+ // Stealing only. Flush finished work before taking more on: a computed
271
+ // response should not wait behind a claim, and that claim can block on
272
+ // a peer withdrawing its intent. Measured to hurt the per-lane path,
273
+ // where a claim is a cheap private decode and picking work up promptly
274
+ // matters more, so the classic order is kept below.
275
+ // Reordering must not make `progressed` sticky: it answers "did this
276
+ // iteration move anything", so it has to start false every pass or the
277
+ // park below is unreachable and the worker spins a core forever.
278
+ progressed = false;
279
+ if (_hasCompleted()) {
280
+ if (_writeBatch(WRITE_MAX) > 0)
281
+ progressed = true;
282
+ }
283
+ progressed = _enqueueLock() || progressed;
284
+ }
285
+ else {
286
+ progressed = _enqueueLock();
287
+ if (_hasCompleted()) {
288
+ if (_writeBatch(WRITE_MAX) > 0)
289
+ progressed = true;
290
+ }
262
291
  }
263
292
  _drainReturnReleases();
264
293
  if (_hasPending()) {
@@ -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();
@@ -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 {};
@@ -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
@@ -59,26 +61,27 @@ export const whilePausing = ({ pauseInNanoseconds }) => {
59
61
  return () => a_pause(forNanoseconds);
60
62
  };
61
63
  export const pauseGeneric = whilePausing({});
62
- 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, }) => {
63
65
  const pause = pauseInNanoseconds === undefined
64
66
  ? pauseGeneric
65
67
  : whilePausing({ pauseInNanoseconds });
66
- const tryProgress = () => {
67
- let progressed = false;
68
- if (enqueueLock())
69
- progressed = true;
70
- if (write) {
71
- const wrote = write();
72
- if (typeof wrote === "number") {
73
- if (wrote > 0)
74
- progressed = true;
75
- }
76
- else if (wrote === true) {
77
- progressed = true;
78
- }
79
- }
80
- 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;
81
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
+ };
82
85
  return (value, spinMicroseconds, parkMs) => {
83
86
  const until = p_now() + (spinMicroseconds / 1000);
84
87
  maybeGc();