knitting 0.1.55 → 0.1.56
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/knitting.d.ts +6 -797
- package/knitting.js +6 -9780
- package/map.md +12 -8
- package/package.json +14 -6
- package/process-shared-buffer.d.ts +1 -0
- package/process-shared-buffer.js +1 -0
- package/shared-memory.d.ts +1 -155
- package/shared-memory.js +1 -1342
- package/src/api.d.ts +69 -0
- package/src/api.js +619 -0
- package/src/common/envelope.d.ts +17 -0
- package/src/common/envelope.js +22 -0
- package/src/common/module-url.d.ts +1 -0
- package/src/common/module-url.js +24 -0
- package/src/common/node-compat.d.ts +20 -0
- package/src/common/node-compat.js +24 -0
- package/src/common/path-canonical.d.ts +6 -0
- package/src/common/path-canonical.js +41 -0
- package/src/common/runtime.d.ts +15 -0
- package/src/common/runtime.js +91 -0
- package/src/common/shared-buffer-region.d.ts +11 -0
- package/src/common/shared-buffer-region.js +21 -0
- package/src/common/shared-buffer-text.d.ts +16 -0
- package/src/common/shared-buffer-text.js +65 -0
- package/src/common/task-source.d.ts +3 -0
- package/src/common/task-source.js +84 -0
- package/src/common/task-symbol.d.ts +1 -0
- package/src/common/task-symbol.js +1 -0
- package/src/common/with-resolvers.d.ts +9 -0
- package/src/common/with-resolvers.js +23 -0
- package/src/common/worker-runtime.d.ts +42 -0
- package/src/common/worker-runtime.js +61 -0
- package/src/connections/buffer-reference-native.d.ts +56 -0
- package/src/connections/buffer-reference-native.js +217 -0
- package/src/connections/buffer-reference.d.ts +78 -0
- package/src/connections/buffer-reference.js +461 -0
- package/src/connections/bun.d.ts +22 -0
- package/src/connections/bun.js +214 -0
- package/src/connections/deno.d.ts +22 -0
- package/src/connections/deno.js +205 -0
- package/src/connections/file-descriptor.d.ts +39 -0
- package/src/connections/file-descriptor.js +161 -0
- package/src/connections/index.d.ts +4 -0
- package/src/connections/index.js +4 -0
- package/src/connections/node-addons.d.ts +5 -0
- package/src/connections/node-addons.js +44 -0
- package/src/connections/node-buffer-pointer.d.ts +20 -0
- package/src/connections/node-buffer-pointer.js +16 -0
- package/src/connections/node.d.ts +30 -0
- package/src/connections/node.js +62 -0
- package/src/connections/numeric-array.d.ts +5 -0
- package/src/connections/numeric-array.js +17 -0
- package/src/connections/package-assets.d.ts +2 -0
- package/src/connections/package-assets.js +45 -0
- package/src/connections/posix.d.ts +32 -0
- package/src/connections/posix.js +77 -0
- package/src/connections/process-shared-buffer.d.ts +75 -0
- package/src/connections/process-shared-buffer.js +279 -0
- package/src/connections/shared-array-buffer-payload.d.ts +36 -0
- package/src/connections/shared-array-buffer-payload.js +235 -0
- package/src/connections/types.d.ts +50 -0
- package/src/connections/types.js +37 -0
- package/src/connections/windows.d.ts +28 -0
- package/src/connections/windows.js +224 -0
- package/src/debug/env-diff.d.ts +26 -0
- package/src/debug/env-diff.js +49 -0
- package/src/debug/gate.d.ts +18 -0
- package/src/debug/gate.js +69 -0
- package/src/debug/handle.d.ts +23 -0
- package/src/debug/handle.js +48 -0
- package/src/error.d.ts +13 -0
- package/src/error.js +49 -0
- package/src/ipc/tools/ring-queue.d.ts +33 -0
- package/src/ipc/tools/ring-queue.js +159 -0
- package/src/ipc/transport/shared-memory.d.ts +23 -0
- package/src/ipc/transport/shared-memory.js +35 -0
- package/src/memory/byte-carpet.d.ts +73 -0
- package/src/memory/byte-carpet.js +157 -0
- package/src/memory/lock.d.ts +203 -0
- package/src/memory/lock.js +741 -0
- package/src/memory/payload-config.d.ts +31 -0
- package/src/memory/payload-config.js +67 -0
- package/src/memory/payloadCodec.d.ts +46 -0
- package/src/memory/payloadCodec.js +1377 -0
- package/src/memory/regionRegistry.d.ts +17 -0
- package/src/memory/regionRegistry.js +285 -0
- package/src/memory/shared-buffer-io.d.ts +55 -0
- package/src/memory/shared-buffer-io.js +403 -0
- package/src/permission/index.d.ts +2 -0
- package/src/permission/index.js +2 -0
- package/src/permission/protocol.d.ts +166 -0
- package/src/permission/protocol.js +650 -0
- package/src/runtime/balancer.d.ts +19 -0
- package/src/runtime/balancer.js +149 -0
- package/src/runtime/dispatcher.d.ts +40 -0
- package/src/runtime/dispatcher.js +142 -0
- package/src/runtime/inline-executor.d.ts +10 -0
- package/src/runtime/inline-executor.js +282 -0
- package/src/runtime/pool.d.ts +39 -0
- package/src/runtime/pool.js +404 -0
- package/src/runtime/process-worker.d.ts +92 -0
- package/src/runtime/process-worker.js +673 -0
- package/src/runtime/tx-queue.d.ts +26 -0
- package/src/runtime/tx-queue.js +155 -0
- package/src/shared/abortSignal.d.ts +23 -0
- package/src/shared/abortSignal.js +134 -0
- package/src/types.d.ts +357 -0
- package/src/types.js +2 -0
- package/src/utils/http.d.ts +29 -0
- package/src/utils/http.js +100 -0
- package/src/worker/bootstrap.d.ts +5 -0
- package/src/worker/bootstrap.js +78 -0
- package/src/worker/composable-runners.d.ts +12 -0
- package/src/worker/composable-runners.js +97 -0
- package/src/worker/loop.d.ts +2 -0
- package/src/worker/loop.js +382 -0
- package/src/worker/process-worker-bootstrap.d.ts +8 -0
- package/src/worker/process-worker-bootstrap.js +160 -0
- package/src/worker/rx-queue.d.ts +25 -0
- package/src/worker/rx-queue.js +173 -0
- package/src/worker/safety/index.d.ts +4 -0
- package/src/worker/safety/index.js +4 -0
- package/src/worker/safety/performance.d.ts +1 -0
- package/src/worker/safety/performance.js +17 -0
- package/src/worker/safety/process.d.ts +2 -0
- package/src/worker/safety/process.js +79 -0
- package/src/worker/safety/startup.d.ts +16 -0
- package/src/worker/safety/startup.js +30 -0
- package/src/worker/safety/worker-data.d.ts +2 -0
- package/src/worker/safety/worker-data.js +36 -0
- package/src/worker/task-loader.d.ts +27 -0
- package/src/worker/task-loader.js +70 -0
- package/src/worker/timers.d.ts +18 -0
- package/src/worker/timers.js +116 -0
- package/unsafe.d.ts +1 -71
- package/unsafe.js +1 -878
- package/utils.d.ts +1 -31
- package/utils.js +1 -116
package/src/api.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { endpointSymbol } from "./common/task-symbol.js";
|
|
2
|
+
import type { AbortSignalConfig, AbortSignalOption, AbortSignalToolkit, Args, ComposedWithKey, CreatePool, FixPoint, ImportTaskOptions, MaybePromise, Pool, ReturnFixed, TaskInput, tasks, TaskTimeout } from "./types.js";
|
|
3
|
+
type ToListAndIds = {
|
|
4
|
+
list: string[];
|
|
5
|
+
ids: number[];
|
|
6
|
+
names: string[];
|
|
7
|
+
at: number[];
|
|
8
|
+
};
|
|
9
|
+
type ToListAndIdsFn = (args: ComposedWithKey[]) => ToListAndIds;
|
|
10
|
+
type CreatePoolFactory = (options: CreatePool) => <T extends tasks>(tasks: T) => Pool<T>;
|
|
11
|
+
type InferredTaskFunction = (...args: any[]) => MaybePromise<Args>;
|
|
12
|
+
type InferredTaskInput<F extends InferredTaskFunction, AS extends AbortSignalOption> = Parameters<F> extends [] ? void : AS extends undefined ? Parameters<F> extends [infer A] ? A extends TaskInput ? A : never : never : Parameters<F> extends [infer A] ? A extends TaskInput ? A : never : Parameters<F> extends [infer A, AbortSignalToolkit<AS>] ? A extends TaskInput ? A : never : never;
|
|
13
|
+
type InferredTaskOutput<F extends InferredTaskFunction> = Awaited<ReturnType<F>> extends infer R ? R extends Blob ? never : R extends Args ? R : never : never;
|
|
14
|
+
type InferredTaskShape<F extends InferredTaskFunction, AS extends AbortSignalOption> = [InferredTaskInput<F, AS>] extends [never] ? never : [InferredTaskOutput<F>] extends [never] ? never : {
|
|
15
|
+
readonly f: F;
|
|
16
|
+
readonly timeout?: TaskTimeout;
|
|
17
|
+
} & (AS extends undefined ? {
|
|
18
|
+
readonly abortSignal?: undefined;
|
|
19
|
+
} : {
|
|
20
|
+
readonly abortSignal: AS;
|
|
21
|
+
});
|
|
22
|
+
export declare const isMain: boolean;
|
|
23
|
+
export { endpointSymbol as endpointSymbol };
|
|
24
|
+
/**
|
|
25
|
+
* Reconstructs stable task order from top-level exports before names are bound.
|
|
26
|
+
*/
|
|
27
|
+
export declare const toListAndIds: ToListAndIdsFn;
|
|
28
|
+
/**
|
|
29
|
+
* Create a typed worker pool from module-scope exported tasks/functions.
|
|
30
|
+
*
|
|
31
|
+
* Install/import as `knitting` on npm or `@vixeny/knitting` on JSR. Requires
|
|
32
|
+
* Node 22+, Deno 2+, or Bun 1+.
|
|
33
|
+
*
|
|
34
|
+
* Use `createPool(options)({ taskA })`, call `await pool.call.taskA(arg)`, and
|
|
35
|
+
* clean up with `using pool = ...` or `await pool.shutdown()`.
|
|
36
|
+
*
|
|
37
|
+
* Guard host-only pool setup with `isMain`: workers re-import task modules, and
|
|
38
|
+
* top-level imports in those modules run in every worker. Keep task modules
|
|
39
|
+
* lean and separate from server/framework setup. Each task receives one
|
|
40
|
+
* argument; use an object or tuple for multiple values.
|
|
41
|
+
*/
|
|
42
|
+
export declare const createPool: CreatePoolFactory;
|
|
43
|
+
/**
|
|
44
|
+
* Define a worker task with options.
|
|
45
|
+
*
|
|
46
|
+
* Pass raw module-scope exported functions to `createPool` directly when you do
|
|
47
|
+
* not need options. Use `task({ f })` for timeouts, abort signals, or the
|
|
48
|
+
* single-task `.createPool()` shorthand.
|
|
49
|
+
*
|
|
50
|
+
* The function receives one argument; use a tuple/object for multiple values.
|
|
51
|
+
* Inputs may be direct values or native Promises, so `request.arrayBuffer()` can
|
|
52
|
+
* be forwarded without awaiting on the host.
|
|
53
|
+
*/
|
|
54
|
+
export declare function task<F extends InferredTaskFunction>(I: InferredTaskShape<F, undefined>): ReturnFixed<InferredTaskInput<F, undefined>, InferredTaskOutput<F>, undefined>;
|
|
55
|
+
export declare function task<F extends InferredTaskFunction>(I: InferredTaskShape<F, true>): ReturnFixed<InferredTaskInput<F, true>, InferredTaskOutput<F>, true>;
|
|
56
|
+
export declare function task<AS extends AbortSignalConfig, F extends InferredTaskFunction>(I: InferredTaskShape<F, AS>): ReturnFixed<InferredTaskInput<F, AS>, InferredTaskOutput<F>, AS>;
|
|
57
|
+
export declare function task<A extends TaskInput = void, B extends Args = void>(I: FixPoint<A, B, true>): ReturnFixed<A, B, true>;
|
|
58
|
+
export declare function task<A extends TaskInput = void, B extends Args = void, AS extends AbortSignalConfig = AbortSignalConfig>(I: FixPoint<A, B, AS>): ReturnFixed<A, B, AS>;
|
|
59
|
+
export declare function task<A extends TaskInput = void, B extends Args = void>(I: FixPoint<A, B, undefined>): ReturnFixed<A, B, undefined>;
|
|
60
|
+
/**
|
|
61
|
+
* Define a task whose worker-side code is imported only by workers.
|
|
62
|
+
*
|
|
63
|
+
* Use this for untrusted or security-sensitive code: the host keeps a typed
|
|
64
|
+
* wrapper and does not import/evaluate `href`. The target export must be a
|
|
65
|
+
* plain function, not a `task()` wrapper.
|
|
66
|
+
*/
|
|
67
|
+
export declare function importTask<A extends TaskInput = void, B extends Args = void>(options: ImportTaskOptions<A, B, true>): ReturnFixed<A, B, true>;
|
|
68
|
+
export declare function importTask<A extends TaskInput = void, B extends Args = void, AS extends AbortSignalConfig = AbortSignalConfig>(options: ImportTaskOptions<A, B, AS>): ReturnFixed<A, B, AS>;
|
|
69
|
+
export declare function importTask<A extends TaskInput = void, B extends Args = void>(options: ImportTaskOptions<A, B, undefined>): ReturnFixed<A, B, undefined>;
|
package/src/api.js
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { getCallerFilePath, getCallerHref } from "./common/task-source.js";
|
|
2
|
+
import { DEBUG_ENABLED, resolveDebugNamespaces } from "./debug/gate.js";
|
|
3
|
+
import { genTaskID } from "./common/task-source.js";
|
|
4
|
+
import { toModuleUrl } from "./common/module-url.js";
|
|
5
|
+
import { endpointSymbol } from "./common/task-symbol.js";
|
|
6
|
+
import { spawnWorkerContext } from "./runtime/pool.js";
|
|
7
|
+
import { ChannelHandler } from "./runtime/dispatcher.js";
|
|
8
|
+
import { RUNTIME } from "./common/runtime.js";
|
|
9
|
+
import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
|
|
10
|
+
import { resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
|
|
11
|
+
import { getNodeProcess } from "./common/node-compat.js";
|
|
12
|
+
import { managerMethod } from "./runtime/balancer.js";
|
|
13
|
+
import { createInlineExecutor } from "./runtime/inline-executor.js";
|
|
14
|
+
const hasDebugNamespace = (namespaces, namespace) => namespaces.has("*") || namespaces.has(namespace);
|
|
15
|
+
const createHostDebug = (namespaces) => {
|
|
16
|
+
const enabled = (namespace) => hasDebugNamespace(namespaces, namespace);
|
|
17
|
+
if (!enabled("host"))
|
|
18
|
+
return undefined;
|
|
19
|
+
const base = performance.now();
|
|
20
|
+
const tag = `host·${RUNTIME}`;
|
|
21
|
+
const log = (message) => {
|
|
22
|
+
const elapsed = (performance.now() - base).toFixed(1);
|
|
23
|
+
console.error(`[${tag}·+${elapsed}ms] host: ${message}`);
|
|
24
|
+
};
|
|
25
|
+
return { log };
|
|
26
|
+
};
|
|
27
|
+
const readHostCwd = () => {
|
|
28
|
+
const denoCwd = globalThis.Deno?.cwd;
|
|
29
|
+
if (typeof denoCwd === "function") {
|
|
30
|
+
try {
|
|
31
|
+
return denoCwd();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const nodeProcess = getNodeProcess();
|
|
37
|
+
if (typeof nodeProcess?.cwd === "function") {
|
|
38
|
+
try {
|
|
39
|
+
return nodeProcess.cwd();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
};
|
|
47
|
+
const formatDebugList = (values, empty = "(none)") => values && values.length > 0 ? values.join(",") : empty;
|
|
48
|
+
const MAX_FUNCTION_ID = 0xFFFF;
|
|
49
|
+
const MAX_FUNCTION_COUNT = MAX_FUNCTION_ID + 1;
|
|
50
|
+
const DEFAULT_IMPORT_EXPORT_NAME = "default";
|
|
51
|
+
export const isMain = RUNTIME_IS_MAIN_THREAD;
|
|
52
|
+
export { endpointSymbol as endpointSymbol };
|
|
53
|
+
/**
|
|
54
|
+
* Reconstructs stable task order from top-level exports before names are bound.
|
|
55
|
+
*/
|
|
56
|
+
export const toListAndIds = (args) => {
|
|
57
|
+
const result = args
|
|
58
|
+
.reduce((acc, v) => (acc[0].add(v.importedFrom),
|
|
59
|
+
acc[1].add(v.id),
|
|
60
|
+
acc[2].add(v.at),
|
|
61
|
+
acc[3].push(v.name),
|
|
62
|
+
acc), [
|
|
63
|
+
new Set(),
|
|
64
|
+
new Set(),
|
|
65
|
+
new Set(),
|
|
66
|
+
[],
|
|
67
|
+
]);
|
|
68
|
+
return {
|
|
69
|
+
list: [...result[0]],
|
|
70
|
+
ids: [...result[1]],
|
|
71
|
+
at: [...result[2]],
|
|
72
|
+
names: result[3],
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
const resolveImportHref = (href, callerHref) => {
|
|
76
|
+
try {
|
|
77
|
+
return new URL(href, callerHref).href;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return toModuleUrl(href);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const resolveWorkerBootstrapSettings = (worker, callerHref) => {
|
|
84
|
+
const bootstrap = worker?.bootstrap;
|
|
85
|
+
if (bootstrap === undefined)
|
|
86
|
+
return worker;
|
|
87
|
+
const name = bootstrap.name ?? DEFAULT_IMPORT_EXPORT_NAME;
|
|
88
|
+
if (typeof bootstrap.href !== "string" || bootstrap.href.length === 0) {
|
|
89
|
+
throw new TypeError("worker.bootstrap.href must be a non-empty string");
|
|
90
|
+
}
|
|
91
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
92
|
+
throw new TypeError("worker.bootstrap.name must be a non-empty string");
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
...worker,
|
|
96
|
+
bootstrap: {
|
|
97
|
+
...bootstrap,
|
|
98
|
+
href: resolveImportHref(bootstrap.href, callerHref),
|
|
99
|
+
name,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const isTaskDefinition = (value) => value != null &&
|
|
104
|
+
typeof value === "object" &&
|
|
105
|
+
typeof value.f === "function";
|
|
106
|
+
const toPoolTaskEntries = (input, callerHref) => Object.entries(input).map(([name, value]) => {
|
|
107
|
+
if (isTaskDefinition(value)) {
|
|
108
|
+
return {
|
|
109
|
+
...value,
|
|
110
|
+
name,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (typeof value === "function") {
|
|
114
|
+
return {
|
|
115
|
+
f: value,
|
|
116
|
+
id: -1,
|
|
117
|
+
importedFrom: new URL(callerHref).href,
|
|
118
|
+
at: -1,
|
|
119
|
+
name,
|
|
120
|
+
[endpointSymbol]: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
throw new TypeError(`createPool task "${name}" must be a task definition or exported function`);
|
|
124
|
+
});
|
|
125
|
+
/**
|
|
126
|
+
* Create a typed worker pool from module-scope exported tasks/functions.
|
|
127
|
+
*
|
|
128
|
+
* Install/import as `knitting` on npm or `@vixeny/knitting` on JSR. Requires
|
|
129
|
+
* Node 22+, Deno 2+, or Bun 1+.
|
|
130
|
+
*
|
|
131
|
+
* Use `createPool(options)({ taskA })`, call `await pool.call.taskA(arg)`, and
|
|
132
|
+
* clean up with `using pool = ...` or `await pool.shutdown()`.
|
|
133
|
+
*
|
|
134
|
+
* Guard host-only pool setup with `isMain`: workers re-import task modules, and
|
|
135
|
+
* top-level imports in those modules run in every worker. Keep task modules
|
|
136
|
+
* lean and separate from server/framework setup. Each task receives one
|
|
137
|
+
* argument; use an object or tuple for multiple values.
|
|
138
|
+
*/
|
|
139
|
+
export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe, abortSignalCapacity, source, worker, workerExecArgv, permission, host, }) => (tasks) => {
|
|
140
|
+
const bufferReferenceReturn = unsafe?.BufferReferenceReturn;
|
|
141
|
+
const debugRequested = DEBUG_ENABLED ||
|
|
142
|
+
(debug !== undefined && debug !== false);
|
|
143
|
+
let debugNamespaces;
|
|
144
|
+
const getDebugNamespaces = () => debugNamespaces ??= resolveDebugNamespaces(debug);
|
|
145
|
+
const hostDebug = debugRequested
|
|
146
|
+
? createHostDebug(getDebugNamespaces())
|
|
147
|
+
: undefined;
|
|
148
|
+
const debugEnabled = (namespace) => debugRequested && hasDebugNamespace(getDebugNamespaces(), namespace);
|
|
149
|
+
/**
|
|
150
|
+
* This functions is only available in the main thread.
|
|
151
|
+
* Also triggers when debug extra is enabled.
|
|
152
|
+
*/
|
|
153
|
+
if (RUNTIME_IS_MAIN_THREAD === false) {
|
|
154
|
+
if (debugEnabled("lifecycle")) {
|
|
155
|
+
console.warn("createPool has been called with : " + JSON.stringify(RUNTIME_WORKER_DATA));
|
|
156
|
+
}
|
|
157
|
+
const notMainThreadError = () => {
|
|
158
|
+
throw new Error("createPool can only be called in the main thread.");
|
|
159
|
+
};
|
|
160
|
+
const throwingProxyTarget = function () {
|
|
161
|
+
return notMainThreadError();
|
|
162
|
+
};
|
|
163
|
+
const throwingProxyHandler = {
|
|
164
|
+
get: function () {
|
|
165
|
+
return notMainThreadError;
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
const mainThreadOnlyProxy = new Proxy(throwingProxyTarget, throwingProxyHandler);
|
|
169
|
+
//@ts-ignore
|
|
170
|
+
return {
|
|
171
|
+
shutdown: mainThreadOnlyProxy,
|
|
172
|
+
[Symbol.dispose]: () => { },
|
|
173
|
+
call: mainThreadOnlyProxy,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const callerHref = getCallerHref(3);
|
|
177
|
+
const listOfFunctions = toPoolTaskEntries(tasks, callerHref)
|
|
178
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
179
|
+
const { list, ids, names, at } = toListAndIds(listOfFunctions);
|
|
180
|
+
hostDebug?.log(`cwd=${readHostCwd() ?? "(unknown)"} caller=${callerHref}`);
|
|
181
|
+
listOfFunctions.forEach((fn) => {
|
|
182
|
+
hostDebug?.log(`task name=${fn.name} id=${fn.id} from=${fn.importedFrom}`);
|
|
183
|
+
});
|
|
184
|
+
if (listOfFunctions.length > MAX_FUNCTION_COUNT) {
|
|
185
|
+
throw new RangeError(`Too many tasks: received ${listOfFunctions.length}. ` +
|
|
186
|
+
`Maximum is ${MAX_FUNCTION_COUNT} (Uint16 function IDs: 0..${MAX_FUNCTION_ID}).`);
|
|
187
|
+
}
|
|
188
|
+
const usingInliner = typeof inliner === "object" && inliner != null;
|
|
189
|
+
const totalNumberOfThread = (threads ?? 1) +
|
|
190
|
+
(usingInliner ? 1 : 0);
|
|
191
|
+
const permissionProtocol = resolvePermissionProtocol({
|
|
192
|
+
permission: permission ?? {
|
|
193
|
+
mode: "strict",
|
|
194
|
+
allowImport: true,
|
|
195
|
+
},
|
|
196
|
+
modules: list,
|
|
197
|
+
});
|
|
198
|
+
const permissionExecArgv = toRuntimePermissionFlags(permissionProtocol);
|
|
199
|
+
const nodeProcess = getNodeProcess();
|
|
200
|
+
const allowedFlags = nodeProcess?.allowedNodeEnvironmentFlags ?? null;
|
|
201
|
+
const isNodePermissionFlag = (flag) => {
|
|
202
|
+
const key = flag.split("=", 1)[0];
|
|
203
|
+
return key === "--permission" ||
|
|
204
|
+
key === "--experimental-permission" ||
|
|
205
|
+
key === "--allow-fs-read" ||
|
|
206
|
+
key === "--allow-fs-write" ||
|
|
207
|
+
key === "--allow-worker" ||
|
|
208
|
+
key === "--allow-child-process" ||
|
|
209
|
+
key === "--allow-addons" ||
|
|
210
|
+
key === "--allow-wasi";
|
|
211
|
+
};
|
|
212
|
+
const stripNodePermissionFlags = (flags) => flags?.filter((flag) => !isNodePermissionFlag(flag));
|
|
213
|
+
const dedupeFlags = (flags) => {
|
|
214
|
+
const out = [];
|
|
215
|
+
const seen = new Set();
|
|
216
|
+
for (const flag of flags) {
|
|
217
|
+
if (seen.has(flag))
|
|
218
|
+
continue;
|
|
219
|
+
seen.add(flag);
|
|
220
|
+
out.push(flag);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
};
|
|
224
|
+
const sanitizeExecArgv = (flags) => {
|
|
225
|
+
if (!flags || flags.length === 0)
|
|
226
|
+
return undefined;
|
|
227
|
+
if (!allowedFlags)
|
|
228
|
+
return flags;
|
|
229
|
+
const filtered = flags.filter((flag) => {
|
|
230
|
+
const key = flag.split("=", 1)[0];
|
|
231
|
+
return allowedFlags.has(key);
|
|
232
|
+
});
|
|
233
|
+
return filtered.length > 0 ? filtered : undefined;
|
|
234
|
+
};
|
|
235
|
+
const inheritedExecArgv = Array.isArray(nodeProcess?.execArgv)
|
|
236
|
+
? nodeProcess.execArgv
|
|
237
|
+
: undefined;
|
|
238
|
+
const defaultExecArgvCandidate = workerExecArgv ??
|
|
239
|
+
(inheritedExecArgv
|
|
240
|
+
? (allowedFlags?.has("--expose-gc") === true
|
|
241
|
+
? (inheritedExecArgv.includes("--expose-gc")
|
|
242
|
+
? inheritedExecArgv
|
|
243
|
+
: [...inheritedExecArgv, "--expose-gc"])
|
|
244
|
+
: inheritedExecArgv)
|
|
245
|
+
: undefined);
|
|
246
|
+
const defaultExecArgv = permissionProtocol?.unsafe === true
|
|
247
|
+
? stripNodePermissionFlags(defaultExecArgvCandidate)
|
|
248
|
+
: defaultExecArgvCandidate;
|
|
249
|
+
const combinedExecArgv = dedupeFlags([
|
|
250
|
+
...permissionExecArgv,
|
|
251
|
+
...(defaultExecArgv ?? []),
|
|
252
|
+
]);
|
|
253
|
+
const execArgv = sanitizeExecArgv(combinedExecArgv.length > 0 ? combinedExecArgv : undefined);
|
|
254
|
+
hostDebug?.log(`pool runtime=${RUNTIME} workers=${threads ?? 1}` +
|
|
255
|
+
` lanes=${totalNumberOfThread} inliner=${usingInliner ? "on" : "off"}`);
|
|
256
|
+
hostDebug?.log(`modules=${formatDebugList(list)}`);
|
|
257
|
+
hostDebug?.log(`permission=${permissionProtocol?.mode ?? "off"} execArgv=${formatDebugList(execArgv)}`);
|
|
258
|
+
const usesAbortSignal = listOfFunctions.some((fn) => fn.abortSignal !== undefined);
|
|
259
|
+
const resolvedWorker = resolveWorkerBootstrapSettings(worker, callerHref);
|
|
260
|
+
if (resolvedWorker?.bootstrap !== undefined) {
|
|
261
|
+
hostDebug?.log(`bootstrap href=${resolvedWorker.bootstrap.href}` +
|
|
262
|
+
` name=${resolvedWorker.bootstrap.name}`);
|
|
263
|
+
}
|
|
264
|
+
if (usingInliner && resolvedWorker?.bootstrap !== undefined) {
|
|
265
|
+
throw new Error("worker.bootstrap cannot be used with the inliner");
|
|
266
|
+
}
|
|
267
|
+
const hardTimeoutMs = Number.isFinite(resolvedWorker?.hardTimeoutMs)
|
|
268
|
+
? Math.max(1, Math.floor(resolvedWorker?.hardTimeoutMs))
|
|
269
|
+
: undefined;
|
|
270
|
+
if (RUNTIME_POOL_DEPTH >= 1) {
|
|
271
|
+
throw new Error(`createPool() tried to spawn workers from inside a worker process ` +
|
|
272
|
+
`(pool depth ${RUNTIME_POOL_DEPTH}). This usually means a pool is ` +
|
|
273
|
+
`created at module scope in a module your workers import, so every ` +
|
|
274
|
+
`worker spawns its own pool recursively. Is your createPool protected ` +
|
|
275
|
+
`by isMain? Guard pool creation behind \`if (isMain) { ... }\` ` +
|
|
276
|
+
`(import { isMain } from "knitting") so only the main program starts ` +
|
|
277
|
+
`the pool.`);
|
|
278
|
+
}
|
|
279
|
+
const dispatcherEnv = nodeProcess?.env?.KNITTING_DISPATCHER;
|
|
280
|
+
const explicitDispatcher = host?.dispatcher ??
|
|
281
|
+
(dispatcherEnv === "serial-channel" || dispatcherEnv === "per-thread"
|
|
282
|
+
? dispatcherEnv
|
|
283
|
+
: undefined);
|
|
284
|
+
const autoDispatcher = (() => {
|
|
285
|
+
// Experimental default for HTTP-style bursts: Bun still favors direct
|
|
286
|
+
// per-thread channels, while Node/Deno multi-worker pools favor one shared
|
|
287
|
+
// macro channel over the per-lane dispatcher checks.
|
|
288
|
+
if (RUNTIME === "bun")
|
|
289
|
+
return "per-thread";
|
|
290
|
+
if ((threads ?? 1) <= 1)
|
|
291
|
+
return "per-thread";
|
|
292
|
+
return "serial-channel";
|
|
293
|
+
})();
|
|
294
|
+
const dispatcher = explicitDispatcher ?? autoDispatcher;
|
|
295
|
+
const serialChannel = dispatcher === "serial-channel";
|
|
296
|
+
const serialDispatcherChannel = serialChannel
|
|
297
|
+
? new ChannelHandler()
|
|
298
|
+
: undefined;
|
|
299
|
+
let workers = Array.from({
|
|
300
|
+
length: threads ?? 1,
|
|
301
|
+
}).map((_, thread) => spawnWorkerContext({
|
|
302
|
+
list,
|
|
303
|
+
ids,
|
|
304
|
+
names,
|
|
305
|
+
at,
|
|
306
|
+
thread,
|
|
307
|
+
debug,
|
|
308
|
+
hostDebug: hostDebug?.log,
|
|
309
|
+
totalNumberOfThread,
|
|
310
|
+
source,
|
|
311
|
+
workerOptions: resolvedWorker,
|
|
312
|
+
workerExecArgv: execArgv,
|
|
313
|
+
host,
|
|
314
|
+
payload,
|
|
315
|
+
bufferReferenceReturn,
|
|
316
|
+
abortSignalCapacity,
|
|
317
|
+
usesAbortSignal,
|
|
318
|
+
permission: permissionProtocol,
|
|
319
|
+
sharedChannelHandler: serialDispatcherChannel,
|
|
320
|
+
}));
|
|
321
|
+
const sharedDispatcherChannel = serialDispatcherChannel;
|
|
322
|
+
if (serialChannel) {
|
|
323
|
+
const channel = serialDispatcherChannel;
|
|
324
|
+
const checks = workers.map((context) => context.dispatcherCheck);
|
|
325
|
+
let serialScheduled = false;
|
|
326
|
+
let serialInFlight = false;
|
|
327
|
+
let serialRerun = false;
|
|
328
|
+
const runSerialChecks = () => {
|
|
329
|
+
if (serialInFlight) {
|
|
330
|
+
serialRerun = true;
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
serialInFlight = true;
|
|
334
|
+
do {
|
|
335
|
+
serialRerun = false;
|
|
336
|
+
serialScheduled = false;
|
|
337
|
+
for (let index = 0; index < checks.length; index++) {
|
|
338
|
+
const check = checks[index];
|
|
339
|
+
if (check.isRunning !== true)
|
|
340
|
+
check.isRunning = true;
|
|
341
|
+
check();
|
|
342
|
+
}
|
|
343
|
+
} while (serialRerun);
|
|
344
|
+
serialInFlight = false;
|
|
345
|
+
};
|
|
346
|
+
const scheduleSerialCheck = () => {
|
|
347
|
+
if (serialInFlight) {
|
|
348
|
+
serialRerun = true;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (serialScheduled)
|
|
352
|
+
return;
|
|
353
|
+
serialScheduled = true;
|
|
354
|
+
Promise.resolve().then(runSerialChecks);
|
|
355
|
+
};
|
|
356
|
+
channel.open(runSerialChecks);
|
|
357
|
+
workers.forEach((context) => {
|
|
358
|
+
const wake = context.laneWake;
|
|
359
|
+
context.bindSend(() => {
|
|
360
|
+
scheduleSerialCheck();
|
|
361
|
+
wake();
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
hostDebug?.log(`dispatcher=serial-channel lanes=${checks.length}`);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
hostDebug?.log(`dispatcher=per-thread lanes=${workers.length}`);
|
|
368
|
+
}
|
|
369
|
+
if (usingInliner) {
|
|
370
|
+
const mainThread = createInlineExecutor({
|
|
371
|
+
tasks: listOfFunctions,
|
|
372
|
+
genTaskID,
|
|
373
|
+
batchSize: inliner?.batchSize ?? 1,
|
|
374
|
+
});
|
|
375
|
+
if (inliner?.position === "first") {
|
|
376
|
+
workers = [
|
|
377
|
+
//@ts-ignore
|
|
378
|
+
mainThread,
|
|
379
|
+
...workers,
|
|
380
|
+
];
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
workers.push(
|
|
384
|
+
//@ts-ignore
|
|
385
|
+
mainThread);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const inlinerIndex = usingInliner
|
|
389
|
+
? (inliner?.position === "first" ? 0 : workers.length - 1)
|
|
390
|
+
: -1;
|
|
391
|
+
const inlinerDispatchThreshold = Number.isFinite(inliner?.dispatchThreshold)
|
|
392
|
+
? Math.max(1, Math.floor(inliner?.dispatchThreshold ?? 1))
|
|
393
|
+
: 1;
|
|
394
|
+
let closing = false;
|
|
395
|
+
let closePromise;
|
|
396
|
+
let shutdownPromise;
|
|
397
|
+
const closePoolNow = () => {
|
|
398
|
+
if (closePromise)
|
|
399
|
+
return closePromise;
|
|
400
|
+
closing = true;
|
|
401
|
+
closePromise = Promise.allSettled(workers.map((context) => context.kills()))
|
|
402
|
+
.then(() => {
|
|
403
|
+
sharedDispatcherChannel?.close();
|
|
404
|
+
});
|
|
405
|
+
return closePromise;
|
|
406
|
+
};
|
|
407
|
+
const wrapGuardedInvoke = ({ invoke, taskName, }) => (args) => {
|
|
408
|
+
if (closing) {
|
|
409
|
+
return Promise.reject(new Error("Pool is shut down"));
|
|
410
|
+
}
|
|
411
|
+
const pending = invoke(args);
|
|
412
|
+
if (!hardTimeoutMs)
|
|
413
|
+
return pending;
|
|
414
|
+
return new Promise((resolve, reject) => {
|
|
415
|
+
let settled = false;
|
|
416
|
+
const timeoutId = setTimeout(() => {
|
|
417
|
+
if (settled)
|
|
418
|
+
return;
|
|
419
|
+
settled = true;
|
|
420
|
+
reject(new Error(`Task hard timeout after ${hardTimeoutMs}ms (${taskName}); pool force-shutdown`));
|
|
421
|
+
void closePoolNow();
|
|
422
|
+
}, hardTimeoutMs);
|
|
423
|
+
pending.then((value) => {
|
|
424
|
+
if (settled)
|
|
425
|
+
return;
|
|
426
|
+
settled = true;
|
|
427
|
+
clearTimeout(timeoutId);
|
|
428
|
+
resolve(value);
|
|
429
|
+
}, (error) => {
|
|
430
|
+
if (settled)
|
|
431
|
+
return;
|
|
432
|
+
settled = true;
|
|
433
|
+
clearTimeout(timeoutId);
|
|
434
|
+
reject(error);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
const shutdownWithDelay = (delayMs) => {
|
|
439
|
+
if (closePromise)
|
|
440
|
+
return closePromise;
|
|
441
|
+
if (shutdownPromise)
|
|
442
|
+
return shutdownPromise;
|
|
443
|
+
const ms = Number.isFinite(delayMs)
|
|
444
|
+
? Math.max(0, Math.floor(delayMs))
|
|
445
|
+
: 0;
|
|
446
|
+
shutdownPromise = (async () => {
|
|
447
|
+
if (closePromise)
|
|
448
|
+
return await closePromise;
|
|
449
|
+
if (ms > 0) {
|
|
450
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
451
|
+
}
|
|
452
|
+
if (closePromise)
|
|
453
|
+
return await closePromise;
|
|
454
|
+
await closePoolNow();
|
|
455
|
+
})();
|
|
456
|
+
return shutdownPromise;
|
|
457
|
+
};
|
|
458
|
+
const disposePool = () => {
|
|
459
|
+
void shutdownWithDelay();
|
|
460
|
+
};
|
|
461
|
+
const indexedFunctions = listOfFunctions.map((fn, index) => ({
|
|
462
|
+
name: fn.name,
|
|
463
|
+
index,
|
|
464
|
+
timeout: fn.timeout,
|
|
465
|
+
abortSignal: fn.abortSignal,
|
|
466
|
+
imported: fn.imported === true,
|
|
467
|
+
}));
|
|
468
|
+
const callHandlers = new Map();
|
|
469
|
+
for (const { name } of indexedFunctions) {
|
|
470
|
+
callHandlers.set(name, []);
|
|
471
|
+
}
|
|
472
|
+
for (const worker of workers) {
|
|
473
|
+
for (const { name, index, timeout, abortSignal } of indexedFunctions) {
|
|
474
|
+
callHandlers.get(name).push(wrapGuardedInvoke({
|
|
475
|
+
taskName: name,
|
|
476
|
+
invoke: worker.call({
|
|
477
|
+
fnNumber: index,
|
|
478
|
+
timeout,
|
|
479
|
+
abortSignal,
|
|
480
|
+
}),
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const useDirectHandler = (threads ?? 1) === 1 && !usingInliner;
|
|
485
|
+
// Imported tasks must never execute on the host inliner lane: their module
|
|
486
|
+
// import is meant to happen inside the worker so worker permission policies
|
|
487
|
+
// apply. When the inliner is active we strip the inline lane from their
|
|
488
|
+
// handler set so they only ever reach real worker lanes.
|
|
489
|
+
const buildImportedInvoker = (handlers) => {
|
|
490
|
+
const workerHandlers = [];
|
|
491
|
+
const workerContexts = [];
|
|
492
|
+
for (let lane = 0; lane < handlers.length; lane += 1) {
|
|
493
|
+
if (lane === inlinerIndex)
|
|
494
|
+
continue;
|
|
495
|
+
workerHandlers.push(handlers[lane]);
|
|
496
|
+
workerContexts.push(workers[lane]);
|
|
497
|
+
}
|
|
498
|
+
if (workerHandlers.length === 0) {
|
|
499
|
+
throw new Error("Imported task has no worker lane to run on: the pool only has the " +
|
|
500
|
+
"host inliner. Imported tasks are never inlined on the host; add at " +
|
|
501
|
+
"least one worker thread.");
|
|
502
|
+
}
|
|
503
|
+
if (workerHandlers.length === 1)
|
|
504
|
+
return workerHandlers[0];
|
|
505
|
+
return managerMethod({
|
|
506
|
+
contexts: workerContexts,
|
|
507
|
+
balancer,
|
|
508
|
+
handlers: workerHandlers,
|
|
509
|
+
// No inlinerGate: the inline lane is intentionally excluded here.
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
const buildInvoker = (handlers, imported) => {
|
|
513
|
+
if (imported && usingInliner) {
|
|
514
|
+
return buildImportedInvoker(handlers);
|
|
515
|
+
}
|
|
516
|
+
return useDirectHandler ? handlers[0] : managerMethod({
|
|
517
|
+
contexts: workers,
|
|
518
|
+
balancer,
|
|
519
|
+
handlers,
|
|
520
|
+
inlinerGate: usingInliner
|
|
521
|
+
? {
|
|
522
|
+
index: inlinerIndex,
|
|
523
|
+
threshold: inlinerDispatchThreshold,
|
|
524
|
+
}
|
|
525
|
+
: undefined,
|
|
526
|
+
});
|
|
527
|
+
};
|
|
528
|
+
let callEntries;
|
|
529
|
+
try {
|
|
530
|
+
callEntries = indexedFunctions.map(({ name, imported }) => [name, buildInvoker(callHandlers.get(name), imported)]);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
// Building invokers can throw (e.g. an imported task with no worker lane,
|
|
534
|
+
// or a balancer that needs >=2 lanes). The inline executor and any spawned
|
|
535
|
+
// workers are already live here, so tear them down before propagating —
|
|
536
|
+
// otherwise their MessageChannel ports / worker handles keep the event
|
|
537
|
+
// loop alive and hang the process.
|
|
538
|
+
void closePoolNow();
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
shutdown: shutdownWithDelay,
|
|
543
|
+
[Symbol.dispose]: disposePool,
|
|
544
|
+
call: Object.fromEntries(callEntries),
|
|
545
|
+
};
|
|
546
|
+
};
|
|
547
|
+
const SINGLE_TASK_KEY = "__task__";
|
|
548
|
+
const createSingleTaskPool = (single, options) => {
|
|
549
|
+
const pool = createPool(options ?? {})({
|
|
550
|
+
[SINGLE_TASK_KEY]: single,
|
|
551
|
+
});
|
|
552
|
+
return {
|
|
553
|
+
call: pool.call[SINGLE_TASK_KEY],
|
|
554
|
+
shutdown: pool.shutdown,
|
|
555
|
+
[Symbol.dispose]: pool[Symbol.dispose],
|
|
556
|
+
};
|
|
557
|
+
};
|
|
558
|
+
const buildTaskDefinitionFromCaller = (input, callerHref, at, imported = false) => {
|
|
559
|
+
const importedFrom = new URL(callerHref).href;
|
|
560
|
+
const out = ({
|
|
561
|
+
...input,
|
|
562
|
+
id: genTaskID(),
|
|
563
|
+
importedFrom,
|
|
564
|
+
at,
|
|
565
|
+
imported,
|
|
566
|
+
[endpointSymbol]: true,
|
|
567
|
+
});
|
|
568
|
+
out.createPool = (options) => {
|
|
569
|
+
if (RUNTIME_IS_MAIN_THREAD === false) {
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
return createSingleTaskPool(out, options);
|
|
573
|
+
};
|
|
574
|
+
return out;
|
|
575
|
+
};
|
|
576
|
+
const buildTaskDefinition = (input, callerOffset) => {
|
|
577
|
+
const [href, at] = getCallerFilePath(callerOffset);
|
|
578
|
+
return buildTaskDefinitionFromCaller(input, href, at);
|
|
579
|
+
};
|
|
580
|
+
const createImportedTaskFn = (href, exportName) => {
|
|
581
|
+
let cachedFn;
|
|
582
|
+
let cachedLoad;
|
|
583
|
+
const loadFn = async () => {
|
|
584
|
+
if (cachedFn)
|
|
585
|
+
return cachedFn;
|
|
586
|
+
if (!cachedLoad) {
|
|
587
|
+
cachedLoad = import(href).then((module) => {
|
|
588
|
+
const record = module;
|
|
589
|
+
const selected = exportName === DEFAULT_IMPORT_EXPORT_NAME
|
|
590
|
+
? record.default
|
|
591
|
+
: record[exportName];
|
|
592
|
+
if (typeof selected !== "function") {
|
|
593
|
+
const available = Object.keys(record).join(", ");
|
|
594
|
+
throw new TypeError(`importTask expected export "${exportName}" from "${href}" to be a function.` +
|
|
595
|
+
` Available exports: ${available || "(none)"}`);
|
|
596
|
+
}
|
|
597
|
+
cachedFn = selected;
|
|
598
|
+
return cachedFn;
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return cachedLoad;
|
|
602
|
+
};
|
|
603
|
+
return (async (...args) => {
|
|
604
|
+
const fn = await loadFn();
|
|
605
|
+
return fn(...args);
|
|
606
|
+
});
|
|
607
|
+
};
|
|
608
|
+
export function task(I) {
|
|
609
|
+
return buildTaskDefinition(I, 4);
|
|
610
|
+
}
|
|
611
|
+
export function importTask(options) {
|
|
612
|
+
const [callerHref, at] = getCallerFilePath(3);
|
|
613
|
+
const { href, name = DEFAULT_IMPORT_EXPORT_NAME, ...rest } = options;
|
|
614
|
+
const resolvedHref = resolveImportHref(href, callerHref);
|
|
615
|
+
return buildTaskDefinitionFromCaller({
|
|
616
|
+
...rest,
|
|
617
|
+
f: createImportedTaskFn(resolvedHref, name),
|
|
618
|
+
}, callerHref, at, true);
|
|
619
|
+
}
|