knitting 0.1.53 → 0.1.54

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.
package/README.md CHANGED
@@ -11,18 +11,22 @@
11
11
  [![Deno](https://img.shields.io/badge/deno-2%2B-000000?logo=deno&logoColor=white)](https://deno.com/)
12
12
  [![Bun](https://img.shields.io/badge/bun-1%2B-f472b6?logo=bun&logoColor=white)](https://bun.sh/)
13
13
 
14
- Knitting is a worker pool over a shared-memory IPC runtime for Node.js, Deno,
15
- and Bun. Our mission is to make JavaScript a multicore language with real
16
- inter-runtime communication.
14
+ Website: [knittingdocs.netlify.app](https://knittingdocs.netlify.app/)
17
15
 
18
- Thanks to its memory design, it can be 5x to 25x faster than using
19
- `postMessages` , bypassing OS socket communication entirely with a novel
20
- protocol written from scratch.
16
+ If you are an agent trying to understand the project, the website also serves an
17
+ [`llms.txt`](https://knittingdocs.netlify.app/llms.txt) file with a compact map
18
+ of the docs.
21
19
 
22
- Use it for parts of your program that should run in different environments, such
23
- as CPU-intensive tasks, small jobs, runtime-isolated tasks, custom isolation for
24
- workers in Docker or bwrap environments, long-running tools, or any processes
25
- that require speed and type flexibility.
20
+ Knitting is a worker pool built on shared-memory IPC for Node.js, Deno, and Bun.
21
+ It lets you call work running on other threads or processes as if it were a
22
+ normal async function.
23
+
24
+ Because calls move through shared memory instead of `postMessage` or sockets,
25
+ some workloads can be 5x to 25x faster than the usual worker-message path.
26
+
27
+ Use it when part of your program should run somewhere else: CPU-heavy work,
28
+ bursty small jobs, runtime-isolated code, Docker or bwrap workers, long-running
29
+ tools, or cross-runtime process work that still needs to be fast and typed.
26
30
 
27
31
  You export a function or task, spin up a pool, and call it like a normal async
28
32
  function:
@@ -31,29 +35,28 @@ function:
31
35
  const result = await pool.call.resizeImage(file);
32
36
  ```
33
37
 
34
- So you only have to take care of 4 things:
38
+ Most of the time, you only have to take care of four things:
35
39
 
36
40
  - Export a function or task
37
41
  - Create a pool
38
42
  - Call it
39
43
  - Let `using` or `shutdown()` close the pool
40
44
 
41
- Under the hood, we take care of scheduling and orchestration across worker
42
- threads or separate processes, also handling signals, timeouts, life cycles,
43
- memory allocation, garbage collection, and cross-runtime memory over the
44
- processes.
45
+ Under the hood, Knitting handles scheduling across worker threads or separate
46
+ processes, plus signals, timeouts, lifecycles, memory allocation, cleanup, and
47
+ cross-runtime shared memory.
45
48
 
46
49
  ## Why use it?
47
50
 
48
- - Easy to use: Have a multithreaded environment or process with few lines of
49
- code.
50
- - Great type support: pass primitives, JSON, Promise of these, and special types
51
- (typed arrays, `Node Buffer`, `Envelope`, and `ProcessSharedBuffer`).
51
+ - Easy to use: spin up threads or processes with a small API.
52
+ - Great type support: pass primitives, JSON, promises of those values, and
53
+ special types like typed arrays, `Node Buffer`, `Envelope`, and
54
+ `ProcessSharedBuffer`.
52
55
  - Runtime flexibility: the same API across Node.js, Deno, and Bun.
53
56
  - Worker choices: use threads for fast pools or processes for stronger
54
57
  isolation.
55
- - All out-of-the-box experiences: strict-by-default permissions, payload-size
56
- limits, task timeouts, abort-aware tasks, and worker hard timeouts.
58
+ - Practical defaults: strict worker permissions, payload-size limits, task
59
+ timeouts, abort-aware tasks, and worker hard timeouts.
57
60
 
58
61
  ## Requirements
59
62
 
@@ -96,9 +99,9 @@ if (isMain) {
96
99
  }
97
100
  ```
98
101
 
99
- The `isMain` guard when the same module is loaded by workers or process. Export
100
- exposes the tasks or functions at module scope, so knitting maps down the
101
- imports, then use and use the pool only from the main program.
102
+ Use the `isMain` guard when a module can be loaded by both the host and its
103
+ workers. Export tasks at module scope so Knitting can find them, then create and
104
+ use the pool only from the main program.
102
105
 
103
106
  ## The Mental Model
104
107
 
@@ -335,9 +338,17 @@ Common options you might tweak:
335
338
  | `worker.runtime` | Choose `"thread"` or `"process"` workers. |
336
339
  | `worker.processSharedMemory` | Process-worker memory discovery: `"inherit"` by default on POSIX, or `"named"` for wrappers/containers that cannot preserve fd 0. |
337
340
  | `permission` | Runtime permission policy for workers. |
341
+ | `host.dispatcher` | Experimental host dispatcher topology: `"per-thread"` or `"serial-channel"`. |
338
342
  | `debug` | Enable diagnostics (`host`, `globals`, `signals`, `imports`, `lifecycle`) or use `KNITTING_DEBUG`. |
339
343
  | `source` | Worker source override for advanced runtimes. |
340
344
 
345
+ Most users can leave `host.dispatcher` alone. The current default is
346
+ experimental: Bun and single-worker pools use `"per-thread"`, while multi-worker
347
+ Node/Deno pools use `"serial-channel"` because it tends to behave well for
348
+ bursty HTTP-style fan-out. If you are tuning a server or comparing runtimes, you
349
+ can force either mode with `KNITTING_DISPATCHER=per-thread` or
350
+ `KNITTING_DISPATCHER=serial-channel`.
351
+
341
352
  ### Worker bootstrap
342
353
 
343
354
  Use `worker.bootstrap` when a worker needs privileged setup before task modules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knitting",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "description": "Shared-memory IPC runtime for Node.js, Deno, and Bun.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/api.js CHANGED
@@ -12,6 +12,7 @@ import { genTaskID } from "./common/task-source.js";
12
12
  import { toModuleUrl } from "./common/module-url.js";
13
13
  import { endpointSymbol } from "./common/task-symbol.js";
14
14
  import { spawnWorkerContext } from "./runtime/pool.js";
15
+ import { ChannelHandler } from "./runtime/dispatcher.js";
15
16
  import { RUNTIME } from "./common/runtime.js";
16
17
  import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
17
18
  import { resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
@@ -283,6 +284,26 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
283
284
  `(import { isMain } from "knitting") so only the main program starts ` +
284
285
  `the pool.`);
285
286
  }
287
+ const dispatcherEnv = nodeProcess?.env?.KNITTING_DISPATCHER;
288
+ const explicitDispatcher = host?.dispatcher ??
289
+ (dispatcherEnv === "serial-channel" || dispatcherEnv === "per-thread"
290
+ ? dispatcherEnv
291
+ : undefined);
292
+ const autoDispatcher = (() => {
293
+ // Experimental default for HTTP-style bursts: Bun still favors direct
294
+ // per-thread channels, while Node/Deno multi-worker pools favor one shared
295
+ // macro channel over the per-lane dispatcher checks.
296
+ if (RUNTIME === "bun")
297
+ return "per-thread";
298
+ if ((threads ?? 1) <= 1)
299
+ return "per-thread";
300
+ return "serial-channel";
301
+ })();
302
+ const dispatcher = explicitDispatcher ?? autoDispatcher;
303
+ const serialChannel = dispatcher === "serial-channel";
304
+ const serialDispatcherChannel = serialChannel
305
+ ? new ChannelHandler()
306
+ : undefined;
286
307
  let workers = Array.from({
287
308
  length: threads ?? 1,
288
309
  }).map((_, thread) => spawnWorkerContext({
@@ -303,7 +324,56 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
303
324
  abortSignalCapacity,
304
325
  usesAbortSignal,
305
326
  permission: permissionProtocol,
327
+ sharedChannelHandler: serialDispatcherChannel,
306
328
  }));
329
+ const sharedDispatcherChannel = serialDispatcherChannel;
330
+ if (serialChannel) {
331
+ const channel = serialDispatcherChannel;
332
+ const checks = workers.map((context) => context.dispatcherCheck);
333
+ let serialScheduled = false;
334
+ let serialInFlight = false;
335
+ let serialRerun = false;
336
+ const runSerialChecks = () => {
337
+ if (serialInFlight) {
338
+ serialRerun = true;
339
+ return;
340
+ }
341
+ serialInFlight = true;
342
+ do {
343
+ serialRerun = false;
344
+ serialScheduled = false;
345
+ for (let index = 0; index < checks.length; index++) {
346
+ const check = checks[index];
347
+ if (check.isRunning !== true)
348
+ check.isRunning = true;
349
+ check();
350
+ }
351
+ } while (serialRerun);
352
+ serialInFlight = false;
353
+ };
354
+ const scheduleSerialCheck = () => {
355
+ if (serialInFlight) {
356
+ serialRerun = true;
357
+ return;
358
+ }
359
+ if (serialScheduled)
360
+ return;
361
+ serialScheduled = true;
362
+ Promise.resolve().then(runSerialChecks);
363
+ };
364
+ channel.open(runSerialChecks);
365
+ workers.forEach((context) => {
366
+ const wake = context.laneWake;
367
+ context.bindSend(() => {
368
+ scheduleSerialCheck();
369
+ wake();
370
+ });
371
+ });
372
+ hostDebug?.log(`dispatcher=serial-channel lanes=${checks.length}`);
373
+ }
374
+ else {
375
+ hostDebug?.log(`dispatcher=per-thread lanes=${workers.length}`);
376
+ }
307
377
  if (usingInliner) {
308
378
  const mainThread = createInlineExecutor({
309
379
  tasks: listOfFunctions,
@@ -337,7 +407,9 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
337
407
  return closePromise;
338
408
  closing = true;
339
409
  closePromise = Promise.allSettled(workers.map((context) => context.kills()))
340
- .then(() => undefined);
410
+ .then(() => {
411
+ sharedDispatcherChannel?.close();
412
+ });
341
413
  return closePromise;
342
414
  };
343
415
  const wrapGuardedInvoke = ({ invoke, taskName, }) => (args) => {
@@ -15,6 +15,11 @@ export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxSta
15
15
  rerun: boolean;
16
16
  };
17
17
  };
18
+ type CheckWithState = (() => void) & {
19
+ isRunning: boolean;
20
+ rerun: boolean;
21
+ };
22
+ export type DispatcherCheck = CheckWithState;
18
23
  export declare class ChannelHandler {
19
24
  #private;
20
25
  channel: RuntimeMessageChannelLike;
@@ -32,3 +37,4 @@ export declare class ChannelHandler {
32
37
  */
33
38
  close(): void;
34
39
  }
40
+ export {};
@@ -1,10 +1,11 @@
1
1
  import { type ProcessSharedMemoryBacking } from "./process-worker.js";
2
2
  import { type Sab } from "../ipc/transport/shared-memory.js";
3
+ import { ChannelHandler, type DispatcherCheck } from "./dispatcher.js";
3
4
  import { lock2 } from "../memory/lock.js";
4
5
  import type { DebugOptions, DispatcherSettings, WorkerContext, WorkerData, WorkerSettings } from "../types.js";
5
6
  import "../worker/loop.js";
6
7
  import { type PayloadBufferOptions } from "../memory/payload-config.js";
7
- export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, }: {
8
+ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, }: {
8
9
  list: string[];
9
10
  ids: number[];
10
11
  names: string[];
@@ -23,8 +24,16 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
23
24
  bufferReferenceReturn?: "copy" | "borrow";
24
25
  abortSignalCapacity?: number;
25
26
  usesAbortSignal?: boolean;
27
+ /**
28
+ * When set, this lane keeps its dispatcher state while the pool owns the
29
+ * macro channel that runs all lane checks.
30
+ */
31
+ sharedChannelHandler?: ChannelHandler;
26
32
  }) => WorkerContext & {
27
33
  lock: ReturnType<typeof lock2>;
28
34
  processSharedMemoryBackings?: readonly ProcessSharedMemoryBacking[];
35
+ dispatcherCheck?: DispatcherCheck;
36
+ laneWake?: () => void;
37
+ bindSend?: (fn: () => void) => void;
29
38
  };
30
39
  export type CreateContext = WorkerContext;
@@ -1,7 +1,7 @@
1
1
  import { createHostTxQueue } from "./tx-queue.js";
2
2
  import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, serializeWorkerBootstrapData, spawnProcessWorker, terminateWorkerQuietly, toProcessWorkerBootPayload, toWorkerCompatExecArgv, toWorkerSafeExecArgv, } from "./process-worker.js";
3
3
  import { createSharedMemoryTransport, TRANSPORT_SIGNAL_BYTES, } from "../ipc/transport/shared-memory.js";
4
- import { ChannelHandler, hostDispatcherLoop } from "./dispatcher.js";
4
+ import { ChannelHandler, hostDispatcherLoop, } from "./dispatcher.js";
5
5
  import { HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
6
6
  import "../worker/loop.js";
7
7
  import { createSharedArrayBuffer, createWasmSharedArrayBuffer, } from "../common/runtime.js";
@@ -34,7 +34,7 @@ const withFixedPayloadConfig = (config) => ({
34
34
  mode: "fixed",
35
35
  payloadInitialBytes: config.payloadMaxByteLength,
36
36
  });
37
- export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, }) => {
37
+ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, }) => {
38
38
  const tsFileUrl = new URL(import.meta.url);
39
39
  const poliWorker = RUNTIME_WORKER;
40
40
  const resolvedWorkerOptions = serializeWorkerBootstrapData(withDefaultWorkerTimers(workerOptions));
@@ -189,15 +189,44 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
189
189
  : undefined,
190
190
  });
191
191
  const { enqueue, rejectAll, txIdle, } = queue;
192
- const channelHandler = new ChannelHandler();
193
- const { check } = hostDispatcherLoop({
192
+ const thisSignal = signalBox.opView;
193
+ const a_add = Atomics.add;
194
+ const a_load = Atomics.load;
195
+ const a_notify = Atomics.notify;
196
+ const canNotifySignal = thisSignal.buffer instanceof SharedArrayBuffer;
197
+ const notifySignal = nativeNotifySignal ??
198
+ (canNotifySignal ? (() => a_notify(thisSignal, 0, 1)) : undefined);
199
+ // Wakes this lane's worker when it is parked (rxStatus 0). Used by send()
200
+ // (new work just arrived) — the dispatcher drain wakes it the same way.
201
+ const laneWake = () => {
202
+ if (a_load(signalBox.rxStatus, 0) === 0) {
203
+ a_add(thisSignal, 0, 1);
204
+ notifySignal?.();
205
+ }
206
+ };
207
+ let dispatchSend = () => { };
208
+ const send = () => dispatchSend();
209
+ let channelHandler;
210
+ const ownsChannel = sharedChannelHandler === undefined;
211
+ const ownChannel = sharedChannelHandler ?? new ChannelHandler();
212
+ const { check: dispatcherCheck } = hostDispatcherLoop({
194
213
  signalBox,
195
214
  queue,
196
- channelHandler,
215
+ channelHandler: ownChannel,
197
216
  dispatcherOptions: host,
198
217
  notifySignal: nativeNotifySignal,
199
218
  });
200
- channelHandler.open(check);
219
+ if (ownsChannel) {
220
+ ownChannel.open(dispatcherCheck);
221
+ channelHandler = ownChannel;
222
+ dispatchSend = () => {
223
+ if (dispatcherCheck.isRunning === true)
224
+ return;
225
+ dispatcherCheck.isRunning = true;
226
+ Promise.resolve().then(dispatcherCheck);
227
+ laneWake();
228
+ };
229
+ }
201
230
  let worker;
202
231
  const workerUrl = source ?? tsFileUrl;
203
232
  const workerMode = useProcessWorkerRuntime
@@ -301,7 +330,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
301
330
  return;
302
331
  closedReason = reason;
303
332
  rejectAll(reason);
304
- channelHandler.close();
333
+ channelHandler?.close();
305
334
  };
306
335
  const onWorkerMessage = (message) => {
307
336
  if (!isWorkerFatalMessage(message))
@@ -343,27 +372,6 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
343
372
  };
344
373
  }
345
374
  }
346
- const thisSignal = signalBox.opView;
347
- const a_add = Atomics.add;
348
- const a_load = Atomics.load;
349
- const a_notify = Atomics.notify;
350
- const canNotifySignal = thisSignal.buffer instanceof SharedArrayBuffer;
351
- const notifySignal = nativeNotifySignal ??
352
- (canNotifySignal ? (() => a_notify(thisSignal, 0, 1)) : undefined);
353
- //const scheduleFastCheck = queueMicrotask;
354
- const send = () => {
355
- if (check.isRunning === true)
356
- return;
357
- check.isRunning = true;
358
- Promise.resolve().then(check);
359
- // Macro lane: dispatcher check is driven by the channel callback.
360
- // channelHandler.notify();
361
- // Use opView as a wake token in lock2 mode to avoid lost wakeups.
362
- if (a_load(signalBox.rxStatus, 0) === 0) {
363
- a_add(thisSignal, 0, 1);
364
- notifySignal?.();
365
- }
366
- };
367
375
  lock.setPromiseHandler((task, isRejected, value) => {
368
376
  queue.settlePromisePayload(task, isRejected, value);
369
377
  send();
@@ -386,6 +394,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
386
394
  },
387
395
  lock,
388
396
  processSharedMemoryBackings: processSharedMemory?.backings,
397
+ dispatcherCheck,
398
+ laneWake: sharedChannelHandler !== undefined ? laneWake : undefined,
399
+ bindSend: sharedChannelHandler !== undefined
400
+ ? ((fn) => void (dispatchSend = fn))
401
+ : undefined,
389
402
  };
390
403
  return context;
391
404
  };
@@ -50,7 +50,11 @@ export function createHostTxQueue({ max, lock, returnLock, releaseBufferReferenc
50
50
  const completeFrame = releaseBufferReferenceReturn === undefined
51
51
  ? resolveReturn
52
52
  : () => withBufferReferenceReturnReleaser(releaseBufferReferenceReturn, resolveReturn);
53
- const txIdle = () => getPendingFrameCount() === 0 && inUsed === getPendingPromiseCount();
53
+ const hasActiveTasks = () => {
54
+ const count = (inUsed - getPendingPromiseCount()) | 0;
55
+ return count > 0;
56
+ };
57
+ const txIdle = () => getPendingFrameCount() === 0 && !hasActiveTasks();
54
58
  const rejectAll = (reason) => {
55
59
  for (let index = 0; index < queue.length; index++) {
56
60
  const slot = queue[index];
package/src/types.d.ts CHANGED
@@ -98,7 +98,7 @@ type TaskLike<AS extends AbortSignalOption = AbortSignalOption> = {
98
98
  });
99
99
  type TaskFunctionLike = (...args: any[]) => any;
100
100
  type Composed<A extends TaskInput = Args, B extends Args = Args, AS extends AbortSignalOption = undefined> = FixPoint<A, B, AS> & SecondPart;
101
- type tasks = Record<string, Composed<any, any, AbortSignalOption> | TaskFunctionLike>;
101
+ type tasks = Record<string, Composed<any, any, any> | TaskFunctionLike>;
102
102
  type ComposedWithKey = Composed<any, any, AbortSignalOption> & {
103
103
  name: string;
104
104
  };
@@ -296,6 +296,17 @@ type DispatcherSettings = {
296
296
  * Max backoff delay (milliseconds).
297
297
  */
298
298
  maxBackoffMs?: number;
299
+ /**
300
+ * Host dispatcher topology.
301
+ * - `"per-thread"`: each worker owns its dispatcher and macro channel.
302
+ * - `"serial-channel"`: each worker keeps its own dispatcher check state, but
303
+ * one shared channel runs all lane checks from first to last.
304
+ *
305
+ * Experimental default: `"per-thread"` on Bun or with one worker, otherwise
306
+ * `"serial-channel"` for multi-worker Node/Deno pools. Can also be forced
307
+ * with the `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
308
+ */
309
+ dispatcher?: "per-thread" | "serial-channel";
299
310
  };
300
311
  type CreatePool = {
301
312
  /** Number of workers. Default: 1. */
@@ -50,7 +50,7 @@ const isPlainNodeWindows = runtimeGlobals.process?.platform === "win32" &&
50
50
  // full parkMs (up to seconds); the long park with no wake is what made CI
51
51
  // runners appear to hang. The native wait already polls internally, so this is
52
52
  // just bounding each poll slice.
53
- const nativeWaitTimeoutMs = (parkMs) => isPlainNodeWindows ? 1 : Number.isFinite(parkMs) ? parkMs : 60;
53
+ const nativeWaitTimeoutMs = (parkMs) => isPlainNodeWindows ? 1 : Number.isFinite(parkMs) ? parkMs : Infinity;
54
54
  export const whilePausing = ({ pauseInNanoseconds }) => {
55
55
  const forNanoseconds = pauseInNanoseconds ?? DEFAULT_PAUSE_TIME;
56
56
  if (!a_pause || forNanoseconds <= 0)
@@ -104,12 +104,12 @@ export const sleepUntilChanged = ({ at, opView, pauseInNanoseconds, rxStatus, tx
104
104
  // This is the notify-able path: a host Atomics.notify on opView wakes it.
105
105
  // An infinite parkMs (debug) is intentional here — the worker blocks until
106
106
  // a real signal instead of re-polling on a timeout.
107
- a_wait(opView, at, value, parkMs ?? 60);
107
+ a_wait(opView, at, value, parkMs ?? Infinity);
108
108
  }
109
109
  else if (a_wait && waitFallbackView) {
110
110
  // waitFallbackView is never notified — this wait only ever times out and
111
111
  // re-polls, so it must stay finite or the worker would hang forever.
112
- a_wait(waitFallbackView, 0, 0, Number.isFinite(parkMs) ? parkMs : 1);
112
+ a_wait(waitFallbackView, 0, 0, Number.isFinite(parkMs) ? parkMs : Infinity);
113
113
  }
114
114
  a_store(rxStatus, 0, 1);
115
115
  };