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 +35 -24
- package/package.json +1 -1
- package/src/api.js +73 -1
- package/src/runtime/dispatcher.d.ts +6 -0
- package/src/runtime/pool.d.ts +10 -1
- package/src/runtime/pool.js +41 -28
- package/src/runtime/tx-queue.js +5 -1
- package/src/types.d.ts +12 -1
- package/src/worker/timers.js +3 -3
package/README.md
CHANGED
|
@@ -11,18 +11,22 @@
|
|
|
11
11
|
[](https://deno.com/)
|
|
12
12
|
[](https://bun.sh/)
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
`
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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,
|
|
42
|
-
|
|
43
|
-
|
|
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:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
-
|
|
56
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
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(() =>
|
|
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 {};
|
package/src/runtime/pool.d.ts
CHANGED
|
@@ -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;
|
package/src/runtime/pool.js
CHANGED
|
@@ -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
|
|
193
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
};
|
package/src/runtime/tx-queue.js
CHANGED
|
@@ -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
|
|
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,
|
|
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. */
|
package/src/worker/timers.js
CHANGED
|
@@ -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 :
|
|
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 ??
|
|
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 :
|
|
112
|
+
a_wait(waitFallbackView, 0, 0, Number.isFinite(parkMs) ? parkMs : Infinity);
|
|
113
113
|
}
|
|
114
114
|
a_store(rxStatus, 0, 1);
|
|
115
115
|
};
|