knitting 0.1.50 → 0.1.51

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 (34) hide show
  1. package/README.md +58 -24
  2. package/map.md +43 -5
  3. package/package.json +3 -3
  4. package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
  5. package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
  6. package/prebuilds/darwin-x64-node-127/knitting_shared_memory.node +0 -0
  7. package/prebuilds/darwin-x64-node-137/knitting_shared_memory.node +0 -0
  8. package/prebuilds/linux-x64-node-127/knitting_shared_memory.node +0 -0
  9. package/prebuilds/linux-x64-node-137/knitting_shared_memory.node +0 -0
  10. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  11. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  12. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  13. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  14. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  15. package/src/api.d.ts +3 -2
  16. package/src/api.js +99 -22
  17. package/src/common/task-source.js +4 -0
  18. package/src/connections/process-shared-buffer.js +2 -0
  19. package/src/knitting_shared_memory.cc +9 -2
  20. package/src/memory/lock.js +8 -168
  21. package/src/memory/payloadCodec.js +27 -34
  22. package/src/memory/shared-buffer-io.d.ts +2 -0
  23. package/src/memory/shared-buffer-io.js +23 -0
  24. package/src/runtime/inline-executor.d.ts +2 -2
  25. package/src/runtime/inline-executor.js +15 -3
  26. package/src/runtime/pool.d.ts +2 -1
  27. package/src/runtime/pool.js +2 -1
  28. package/src/types.d.ts +18 -5
  29. package/src/worker/loop.js +4 -2
  30. package/src/worker/process-worker-bootstrap.js +1 -0
  31. package/src/worker/safety/startup.d.ts +2 -1
  32. package/src/worker/safety/startup.js +5 -2
  33. package/src/worker/task-loader.d.ts +2 -1
  34. package/src/worker/task-loader.js +14 -2
package/README.md CHANGED
@@ -17,7 +17,8 @@ Thanks to its memory design, it can be 5x to 25x faster than using `postMessages
17
17
 
18
18
  Use it for parts of your program that should run in different environments, such as CPU-intensive tasks, small jobs, runtime-isolated tasks, custom isolation for workers in Docker or bwrap environments, long-running tools, or any processes that require speed and type flexibility.
19
19
 
20
- You define a task once, spin up a pool, and call it like a normal async function:
20
+ You export a function or task, spin up a pool, and call it like a normal async
21
+ function:
21
22
 
22
23
  ```ts
23
24
 
@@ -26,10 +27,10 @@ const result = await pool.call.resizeImage(file);
26
27
  ```
27
28
 
28
29
  So you only have to take care of 4 things:
29
- - Create a task
30
+ - Export a function or task
30
31
  - Create a pool
31
- - Call your task
32
- - Terminate the pool
32
+ - Call it
33
+ - Let `using` or `shutdown()` close the pool
33
34
 
34
35
 
35
36
  Under the hood, we take care of scheduling and orchestration across worker threads or separate processes, also handling signals, timeouts, life cycles, memory allocation, garbage collection, and cross-runtime memory over the processes.
@@ -60,35 +61,27 @@ npm install knitting
60
61
  For Deno projects:
61
62
 
62
63
  ```bash
63
- deno add --npm jsr:@vixeny/knitting
64
+ deno add --npm knitting
64
65
  ```
65
66
 
66
67
  ## Quick Start
67
68
 
68
69
  ```ts
69
- import { createPool, isMain, task } from "knitting";
70
+ import { createPool, isMain } from "knitting";
70
71
 
71
- export const square = task<number, number>({
72
- f: (value) => value * value,
73
- });
72
+ export const square = (value: number) => value * value;
74
73
 
75
- export const greet = task<string, string>({
76
- f: (name) => `hello ${name}`,
77
- });
74
+ export const greet = (name: string) => `hello ${name}`;
78
75
 
79
76
  if (isMain) {
80
- const pool = createPool({ threads: 2 })({ square, greet });
77
+ using pool = createPool({ threads: 2 })({ square, greet });
81
78
 
82
- try {
83
- const [four, message] = await Promise.all([
84
- pool.call.square(2),
85
- pool.call.greet("knitting"),
86
- ]);
79
+ const [four, message] = await Promise.all([
80
+ pool.call.square(2),
81
+ pool.call.greet("knitting"),
82
+ ]);
87
83
 
88
- console.log({ four, message });
89
- } finally {
90
- await pool.shutdown();
91
- }
84
+ console.log({ four, message });
92
85
  }
93
86
  ```
94
87
 
@@ -128,6 +121,39 @@ if (isMain) {
128
121
  }
129
122
  ```
130
123
 
124
+ On TypeScript or runtimes that support explicit resource management, the pool is
125
+ also a synchronous disposable:
126
+
127
+ ```ts
128
+ if (isMain) {
129
+ using pool = createPool({ threads: 4 })({ add });
130
+
131
+ const value = await pool.call.add([1, 2]);
132
+ console.log(value);
133
+ }
134
+ ```
135
+
136
+ `using` starts pool shutdown when the scope exits and does not wait for it.
137
+ TypeScript 5.2+ can compile this pattern for runtimes that do not parse `using`
138
+ syntax directly. Use `await pool.shutdown()` when you need to wait for shutdown
139
+ or pass a shutdown delay.
140
+
141
+ For simple tasks that do not need timeout or abort metadata, exported functions
142
+ can be used directly:
143
+
144
+ ```ts
145
+ export const add = ([a, b]: [number, number]) => a + b;
146
+
147
+ if (isMain) {
148
+ using pool = createPool({ threads: 1 })({ add });
149
+ console.log(await pool.call.add([1, 2]));
150
+ }
151
+ ```
152
+
153
+ Bare functions must be exported from the module that creates the pool. Inline
154
+ anonymous functions cannot be imported by workers; use `task(...)` when you need
155
+ metadata or a more explicit task definition.
156
+
131
157
  Once you have a pool, calls are just promises, so batching looks like normal
132
158
  JavaScript:
133
159
 
@@ -241,6 +267,14 @@ When workers import files, keep the pool's permission settings in mind. The
241
267
  default strict mode allows task imports, but custom permission policies can
242
268
  limit reads, writes, environment access, networking, and process execution.
243
269
 
270
+ Imported tasks are never run on the host inline lane, even when the pool enables
271
+ the `inliner`. Inlining would evaluate the imported module on the host and
272
+ bypass the worker permissions that `importTask` exists to enforce, so Knitting
273
+ always routes imported tasks to a worker. You can freely mix `importTask` and
274
+ the `inliner` in one pool — regular tasks get inlined while imported ones stay
275
+ on worker lanes — but the pool needs at least one worker thread for them to run,
276
+ otherwise `createPool` throws.
277
+
244
278
  ### Single-task shorthand
245
279
 
246
280
  For quick scripts, a task can create its own pool:
@@ -422,7 +456,6 @@ the inherited fd:
422
456
 
423
457
  ```ts
424
458
  const pool = createPool({
425
- threads: 2,
426
459
  worker: {
427
460
  runtime: "process",
428
461
  processRuntime: "bun",
@@ -502,7 +535,8 @@ If it isn't on that list, assume it isn't portable. Some things don't (or
502
535
  shouldn't) cross the boundary:
503
536
 
504
537
  - DOM objects and platform handles.
505
- - Functions, unless they are part of a `task` or `importTask` definition.
538
+ - Functions, unless they are exported pool tasks or part of a `task` or
539
+ `importTask` definition.
506
540
  - Cyclic object graphs.
507
541
  - `Map`, `Set`, `WeakMap`, and non-global symbols.
508
542
  - Objects with behavior that depends on prototypes, getters, setters, or hidden
package/map.md CHANGED
@@ -32,6 +32,10 @@ The core flow is:
32
32
  ## Build And Scripts
33
33
 
34
34
  - `build.ts`: Bundles `knitting.ts` to `out/` with Bun for a Node ESM target.
35
+ - `tsconfig.npm.json`: TypeScript config for the npm release build. Emits
36
+ `.js` and `.d.ts` files beside the source tree.
37
+ - `scripts/rewrite-declaration-imports.mjs`: Post-processes emitted `.d.ts`
38
+ files so declaration imports point at `.js` files for npm consumers.
35
39
  - `scripts/build-native-addons.ts`: Compiles the native Node addons into
36
40
  `build/Release/` on Linux, macOS, and Windows. It finds Node headers/libs,
37
41
  splits user flags, builds the shared-memory and futex addons, and emits the
@@ -39,6 +43,20 @@ The core flow is:
39
43
  - `run.sh`: Runs every top-level benchmark in `bench/` across Node, Deno, and
40
44
  Bun. `--json` writes JSON result files.
41
45
 
46
+ ## CI And Release
47
+
48
+ - `.github/workflows/test.yml`: Push/PR test matrix for Deno, Node, and Bun
49
+ across Linux, macOS, and Windows.
50
+ - `.github/workflows/coverage.yml`: Node coverage workflow with a 90% line
51
+ coverage gate.
52
+ - `.github/workflows/build-and-test.yml`: Manual workflow that builds native
53
+ prebuild artifacts and runs runtime tests against them.
54
+ - `.github/workflows/publish.yml`: Manual native-prebuild workflow. Verifies
55
+ Node and Windows FFI prebuilds, checks the JSR package contents, and commits
56
+ updated `prebuilds/` artifacts back to the branch.
57
+ - `docs/windows-process-worker-hang-fix.md`: Investigation notes for the
58
+ Windows process-worker shared-memory and parked-worker hang fixes.
59
+
42
60
  ## Public API Layer
43
61
 
44
62
  - `src/api.ts`: Defines `task`, `importTask`, `createPool`, `isMain`, task id
@@ -85,12 +103,20 @@ The core flow is:
85
103
  first-idle, random, and first-idle-random.
86
104
  - `src/runtime/inline-executor.ts`: Optional in-process executor used by the
87
105
  inliner path to run tasks without crossing the worker boundary.
106
+ - `src/runtime/process-worker.ts`: Process-worker spawning, command/runtime
107
+ selection, process shared-memory layout, inherited/named mapping metadata, and
108
+ child boot payload construction.
88
109
 
89
110
  ## Worker Side
90
111
 
112
+ - `src/worker/bootstrap.ts`: Optional user bootstrap hook that runs before task
113
+ modules import and revives process-shared-buffer metadata in bootstrap data.
91
114
  - `src/worker/loop.ts`: Worker entrypoint and main loop. Boots worker contexts,
92
115
  installs safety guards, receives tasks, executes batches, writes completions,
93
116
  and supports process-worker bootstrapping.
117
+ - `src/worker/process-worker-bootstrap.ts`: Child-side process-worker boot
118
+ payload validation, shared-memory remapping, runtime primitive setup, and
119
+ startup handoff into the worker loop.
94
120
  - `src/worker/task-loader.ts`: Imports task modules inside workers, finds
95
121
  exported task definitions, filters by id/caller position, and normalizes
96
122
  timeout metadata.
@@ -154,6 +180,8 @@ The core flow is:
154
180
  payload-codec registration.
155
181
  - `src/connections/file-descriptor.ts`: File descriptor wrapper, metadata
156
182
  parsing, mapping support, and descriptor lifecycle helpers.
183
+ - `src/connections/node-addons.ts`: Native addon specifier resolution for
184
+ committed Node ABI prebuilds with `build/Release` fallback loading.
157
185
  - `src/connections/node.ts`: Loads POSIX Node native addons and exposes Node
158
186
  shared memory, mapping, unlink, and futex primitives.
159
187
  - `src/connections/bun.ts`: Bun FFI implementation for POSIX shared memory.
@@ -169,6 +197,10 @@ The core flow is:
169
197
  - `src/knitting_windows_shared_memory.cc`: Runtime-neutral Windows DLL exports
170
198
  for creating, opening, mapping, and closing named shared-memory objects from
171
199
  FFI runtimes.
200
+ - `prebuilds/*/*.node`: Tracked Node native-addon prebuilds for supported
201
+ platform/Node ABI combinations.
202
+ - `prebuilds/win32-x64/*.dll`: Tracked Windows FFI DLL prebuild used by Bun and
203
+ Deno shared-memory primitives on Windows.
172
204
 
173
205
  ## Permissions
174
206
 
@@ -190,6 +222,9 @@ The core flow is:
190
222
  - `bench/withload.ts`: Measures behavior under main-thread load.
191
223
  - `bench/call-growth.ts`: Measures call cost as payload size grows.
192
224
  - `bench/call-growth-batch.ts`: Batch-focused version of call-growth tests.
225
+ - `bench/startup.ts`: Measures `createPool` to first-response startup latency
226
+ across thread and process workers, with optional cross-runtime and named
227
+ shared-memory candidates.
193
228
  - `bench/tokio-mpsc-knitting.ts`: Batch latency benchmark for string, number,
194
229
  and Uint8Array echo tasks.
195
230
  - `bench/payload-sweep.ts`: Uint8Array payload-size sweep promoted from the old
@@ -219,6 +254,7 @@ The core flow is:
219
254
 
220
255
  ## Tests
221
256
 
257
+ - `test/_runner.ts`: Runtime-neutral test runner shim used by the test suite.
222
258
  - `test/abortSignal.test.ts`: Shared abort bitset behavior.
223
259
  - `test/api-cap.test.ts`: API limits such as maximum task id count.
224
260
  - `test/shared-buffer-io.test.ts`: Shared-buffer IO read/write behavior.
@@ -250,6 +286,8 @@ The core flow is:
250
286
  - `test/task-abort-context-api.test.ts`: Worker abort toolkit/context behavior.
251
287
  - `test/tx-queue.test.ts`: Host transmit queue behavior and late-result safety.
252
288
  - `test/type-inference.test.ts`: Public type inference guarantees.
289
+ - `test/worker-bootstrap.test.ts`: Worker bootstrap hook behavior, shared-buffer
290
+ metadata revival, startup failure propagation, and inliner incompatibility.
253
291
  - `test/fixtures/*.ts`: Task modules used by tests.
254
292
  - `test/fixtures/probes/*.ts`: Probe programs for crash, permission, process,
255
293
  file-descriptor, and shared-memory-corruption safety cases.
@@ -259,12 +297,12 @@ The core flow is:
259
297
  - `build/Release/*.node`: Native addon output produced by
260
298
  `scripts/build-native-addons.ts`.
261
299
  - `out/`: Bundled output produced by `build.ts`.
300
+ - `dest/`: Scratch output used by the `build:node` package script.
301
+ - `knitting.js`, `knitting.d.ts`, `process-shared-buffer.js`,
302
+ `process-shared-buffer.d.ts`, and `src/**/*.js` / `src/**/*.d.ts`: npm
303
+ release build artifacts produced by `tsconfig.npm.json`.
262
304
  - `results/`: Benchmark output produced by `run.sh`.
305
+ - `log/`, `logs`, and `*.log`: Local runtime/log output.
263
306
  - `node_modules/`: Installed dependencies. Not part of the source map.
264
307
 
265
- ## Deleted Or Intentionally Absent
266
308
 
267
- - Browser-mode build/smoke files are no longer part of the project.
268
- - The old top-level scratch files `uwu.ts` and `examples.ts` are removed.
269
- - Python graph scripts under `graphs/` were removed; current benchmark output is
270
- kept in the TypeScript benchmark suite.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knitting",
3
- "version": "0.1.50",
3
+ "version": "0.1.51",
4
4
  "description": "Shared-memory IPC runtime for Node.js, Deno, and Bun.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -66,13 +66,13 @@
66
66
  "bun": ">=1.0.0"
67
67
  },
68
68
  "peerDependencies": {
69
- "typescript": "^5.0.0"
69
+ "typescript": "^5.2.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/bun": "latest",
73
73
  "@types/deno": "latest",
74
74
  "@types/node": "latest",
75
75
  "mitata": "^1.0.34",
76
- "typescript": "^5.0.0"
76
+ "typescript": "^5.2.0"
77
77
  }
78
78
  }
package/src/api.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { endpointSymbol } from "./common/task-symbol.js";
2
- import type { Args, AbortSignalConfig, AbortSignalOption, AbortSignalToolkit, CreatePool, FixPoint, MaybePromise, Pool, TaskInput, ReturnFixed, ImportTaskOptions, TaskTimeout, tasks } from "./types.js";
2
+ import type { Args, AbortSignalConfig, AbortSignalOption, AbortSignalToolkit, ComposedWithKey, CreatePool, FixPoint, MaybePromise, Pool, TaskInput, ReturnFixed, ImportTaskOptions, TaskTimeout, tasks } from "./types.js";
3
3
  type ToListAndIds = {
4
4
  list: string[];
5
5
  ids: number[];
6
+ names: string[];
6
7
  at: number[];
7
8
  };
8
- type ToListAndIdsFn = (args: tasks) => ToListAndIds;
9
+ type ToListAndIdsFn = (args: ComposedWithKey[]) => ToListAndIds;
9
10
  type CreatePoolFactory = (options: CreatePool) => <T extends tasks>(tasks: T) => Pool<T>;
10
11
  type InferredTaskFunction = (...args: any[]) => MaybePromise<Args>;
11
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;
package/src/api.js CHANGED
@@ -30,19 +30,22 @@ export { endpointSymbol as endpointSymbol };
30
30
  *
31
31
  */
32
32
  export const toListAndIds = (args) => {
33
- const result = Object.values(args)
33
+ const result = args
34
34
  .reduce((acc, v) => (acc[0].add(v.importedFrom),
35
35
  acc[1].add(v.id),
36
36
  acc[2].add(v.at),
37
+ acc[3].push(v.name),
37
38
  acc), [
38
39
  new Set(),
39
40
  new Set(),
40
- new Set()
41
+ new Set(),
42
+ [],
41
43
  ]);
42
44
  return {
43
45
  list: [...result[0]],
44
46
  ids: [...result[1]],
45
47
  at: [...result[2]],
48
+ names: result[3],
46
49
  };
47
50
  };
48
51
  const resolveImportHref = (href, callerHref) => {
@@ -73,6 +76,28 @@ const resolveWorkerBootstrapSettings = (worker, callerHref) => {
73
76
  },
74
77
  };
75
78
  };
79
+ const isTaskDefinition = (value) => value != null &&
80
+ typeof value === "object" &&
81
+ typeof value.f === "function";
82
+ const toPoolTaskEntries = (input, callerHref) => Object.entries(input).map(([name, value]) => {
83
+ if (isTaskDefinition(value)) {
84
+ return {
85
+ ...value,
86
+ name,
87
+ };
88
+ }
89
+ if (typeof value === "function") {
90
+ return {
91
+ f: value,
92
+ id: -1,
93
+ importedFrom: new URL(callerHref).href,
94
+ at: -1,
95
+ name,
96
+ [endpointSymbol]: true,
97
+ };
98
+ }
99
+ throw new TypeError(`createPool task "${name}" must be a task definition or exported function`);
100
+ });
76
101
  export const createPool = ({ threads, debug, inliner, balancer, payload, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, source, worker, workerExecArgv, permission, dispatcher, host, }) => (tasks) => {
77
102
  /**
78
103
  * This functions is only available in the main thread.
@@ -97,14 +122,14 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
97
122
  //@ts-ignore
98
123
  return {
99
124
  shutdown: mainThreadOnlyProxy,
125
+ [Symbol.dispose]: () => { },
100
126
  call: mainThreadOnlyProxy,
101
127
  };
102
128
  }
103
- const { list, ids, at } = toListAndIds(tasks), listOfFunctions = Object.entries(tasks).map(([k, v]) => ({
104
- ...v,
105
- name: k,
106
- }))
129
+ const callerHref = getCallerHref(3);
130
+ const listOfFunctions = toPoolTaskEntries(tasks, callerHref)
107
131
  .sort((a, b) => a.name.localeCompare(b.name));
132
+ const { list, ids, names, at } = toListAndIds(listOfFunctions);
108
133
  if (listOfFunctions.length > MAX_FUNCTION_COUNT) {
109
134
  throw new RangeError(`Too many tasks: received ${listOfFunctions.length}. ` +
110
135
  `Maximum is ${MAX_FUNCTION_COUNT} (Uint16 function IDs: 0..${MAX_FUNCTION_ID}).`);
@@ -177,7 +202,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
177
202
  const execArgv = sanitizeExecArgv(combinedExecArgv.length > 0 ? combinedExecArgv : undefined);
178
203
  const hostDispatcher = host ?? dispatcher;
179
204
  const usesAbortSignal = listOfFunctions.some((fn) => fn.abortSignal !== undefined);
180
- const resolvedWorker = resolveWorkerBootstrapSettings(worker, getCallerHref(3));
205
+ const resolvedWorker = resolveWorkerBootstrapSettings(worker, callerHref);
181
206
  if (usingInliner && resolvedWorker?.bootstrap !== undefined) {
182
207
  throw new Error("worker.bootstrap cannot be used with the inliner");
183
208
  }
@@ -189,6 +214,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
189
214
  }).map((_, thread) => spawnWorkerContext({
190
215
  list,
191
216
  ids,
217
+ names,
192
218
  at,
193
219
  thread,
194
220
  debug,
@@ -208,7 +234,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
208
234
  }));
209
235
  if (usingInliner) {
210
236
  const mainThread = createInlineExecutor({
211
- tasks,
237
+ tasks: listOfFunctions,
212
238
  genTaskID,
213
239
  batchSize: inliner?.batchSize ?? 1,
214
240
  });
@@ -293,11 +319,15 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
293
319
  })();
294
320
  return shutdownPromise;
295
321
  };
322
+ const disposePool = () => {
323
+ void shutdownWithDelay();
324
+ };
296
325
  const indexedFunctions = listOfFunctions.map((fn, index) => ({
297
326
  name: fn.name,
298
327
  index,
299
328
  timeout: fn.timeout,
300
329
  abortSignal: fn.abortSignal,
330
+ imported: fn.imported === true,
301
331
  }));
302
332
  const callHandlers = new Map();
303
333
  for (const { name } of indexedFunctions) {
@@ -316,22 +346,67 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
316
346
  }
317
347
  }
318
348
  const useDirectHandler = (threads ?? 1) === 1 && !usingInliner;
319
- const buildInvoker = (handlers) => useDirectHandler
320
- ? handlers[0]
321
- : managerMethod({
322
- contexts: workers,
349
+ // Imported tasks must never execute on the host inliner lane: their module
350
+ // import is meant to happen inside the worker so worker permission policies
351
+ // apply. When the inliner is active we strip the inline lane from their
352
+ // handler set so they only ever reach real worker lanes.
353
+ const buildImportedInvoker = (handlers) => {
354
+ const workerHandlers = [];
355
+ const workerContexts = [];
356
+ for (let lane = 0; lane < handlers.length; lane += 1) {
357
+ if (lane === inlinerIndex)
358
+ continue;
359
+ workerHandlers.push(handlers[lane]);
360
+ workerContexts.push(workers[lane]);
361
+ }
362
+ if (workerHandlers.length === 0) {
363
+ throw new Error("Imported task has no worker lane to run on: the pool only has the " +
364
+ "host inliner. Imported tasks are never inlined on the host; add at " +
365
+ "least one worker thread.");
366
+ }
367
+ if (workerHandlers.length === 1)
368
+ return workerHandlers[0];
369
+ return managerMethod({
370
+ contexts: workerContexts,
323
371
  balancer,
324
- handlers,
325
- inlinerGate: usingInliner
326
- ? {
327
- index: inlinerIndex,
328
- threshold: inlinerDispatchThreshold,
329
- }
330
- : undefined,
372
+ handlers: workerHandlers,
373
+ // No inlinerGate: the inline lane is intentionally excluded here.
331
374
  });
332
- const callEntries = Array.from(callHandlers.entries(), ([name, handlers]) => [name, buildInvoker(handlers)]);
375
+ };
376
+ const buildInvoker = (handlers, imported) => {
377
+ if (imported && usingInliner) {
378
+ return buildImportedInvoker(handlers);
379
+ }
380
+ return useDirectHandler
381
+ ? handlers[0]
382
+ : managerMethod({
383
+ contexts: workers,
384
+ balancer,
385
+ handlers,
386
+ inlinerGate: usingInliner
387
+ ? {
388
+ index: inlinerIndex,
389
+ threshold: inlinerDispatchThreshold,
390
+ }
391
+ : undefined,
392
+ });
393
+ };
394
+ let callEntries;
395
+ try {
396
+ callEntries = indexedFunctions.map(({ name, imported }) => [name, buildInvoker(callHandlers.get(name), imported)]);
397
+ }
398
+ catch (error) {
399
+ // Building invokers can throw (e.g. an imported task with no worker lane,
400
+ // or a balancer that needs >=2 lanes). The inline executor and any spawned
401
+ // workers are already live here, so tear them down before propagating —
402
+ // otherwise their MessageChannel ports / worker handles keep the event
403
+ // loop alive and hang the process.
404
+ void closePoolNow();
405
+ throw error;
406
+ }
333
407
  return {
334
408
  shutdown: shutdownWithDelay,
409
+ [Symbol.dispose]: disposePool,
335
410
  call: Object.fromEntries(callEntries),
336
411
  };
337
412
  };
@@ -343,15 +418,17 @@ const createSingleTaskPool = (single, options) => {
343
418
  return {
344
419
  call: pool.call[SINGLE_TASK_KEY],
345
420
  shutdown: pool.shutdown,
421
+ [Symbol.dispose]: pool[Symbol.dispose],
346
422
  };
347
423
  };
348
- const buildTaskDefinitionFromCaller = (input, callerHref, at) => {
424
+ const buildTaskDefinitionFromCaller = (input, callerHref, at, imported = false) => {
349
425
  const importedFrom = new URL(callerHref).href;
350
426
  const out = ({
351
427
  ...input,
352
428
  id: genTaskID(),
353
429
  importedFrom,
354
430
  at,
431
+ imported,
355
432
  [endpointSymbol]: true,
356
433
  });
357
434
  out.createPool = (options) => {
@@ -404,5 +481,5 @@ export function importTask(options) {
404
481
  return buildTaskDefinitionFromCaller({
405
482
  ...rest,
406
483
  f: createImportedTaskFn(resolvedHref, name),
407
- }, callerHref, at);
484
+ }, callerHref, at, true);
408
485
  }
@@ -2,9 +2,13 @@ import { toModuleUrl } from "./module-url.js";
2
2
  export const genTaskID = ((counter) => () => counter++)(0);
3
3
  const INTERNAL_CALLER_HINTS = [
4
4
  "/src/common/task-source.ts",
5
+ "/src/common/task-source.js",
5
6
  "\\src\\common\\task-source.ts",
7
+ "\\src\\common\\task-source.js",
6
8
  "/src/api.ts",
9
+ "/src/api.js",
7
10
  "\\src\\api.ts",
11
+ "\\src\\api.js",
8
12
  ];
9
13
  const INTERNAL_CALLER_FUNCTIONS = new Set([
10
14
  "collectStackFrames",
@@ -150,6 +150,8 @@ export class ProcessSharedBuffer {
150
150
  static parse(serialized) {
151
151
  return ProcessSharedBuffer.fromMetadata(serialized);
152
152
  }
153
+ // Rebuilds from the 8 raw words (no name -> no JSON):
154
+ // fd, size, descByteLength, byteOffset, byteLength, runtime, kind, baseAddressMod64
153
155
  static [PROCESS_SHARED_BUFFER_NUMERIC_TRANSFER](metadata) {
154
156
  const [fd, size, descriptorByteLength, byteOffset, byteLength, runtime, kind, baseAddressMod64,] = metadata;
155
157
  return new ProcessSharedBuffer(new FileDescriptor({
@@ -711,12 +711,19 @@ void UnlinkSharedMemory(const v8::FunctionCallbackInfo<v8::Value>& args) {
711
711
  #endif
712
712
  }
713
713
 
714
- void Initialize(v8::Local<v8::Object> exports) {
714
+ void Initialize(
715
+ v8::Local<v8::Object> exports,
716
+ v8::Local<v8::Value> /*module*/,
717
+ v8::Local<v8::Context> /*context*/
718
+ ) {
715
719
  NODE_SET_METHOD(exports, "createSharedMemory", CreateSharedMemory);
716
720
  NODE_SET_METHOD(exports, "mapSharedMemory", MapSharedMemory);
717
721
  NODE_SET_METHOD(exports, "unlinkSharedMemory", UnlinkSharedMemory);
718
722
  }
719
723
 
720
- NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
724
+ // This addon needs V8 APIs to expose mmap'd memory as a SharedArrayBuffer.
725
+ // Context-aware registration lets Node initialize it inside worker threads
726
+ // even after the main thread has already loaded the shared library.
727
+ NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize)
721
728
 
722
729
  } // namespace knitting_shared_memory
@@ -564,171 +564,9 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
564
564
  return modified;
565
565
  };
566
566
  }
567
- if (!shouldSettle) {
568
- if (!onResolved) {
569
- return () => {
570
- let diff = (a_load(hostBits, 0) ^ LastWorker) | 0;
571
- if (diff === 0)
572
- return 0;
573
- let modified = 0;
574
- let consumedBits = 0 | 0;
575
- let last = lastResolved;
576
- if (last === 32) {
577
- const idx = 31 - clz32(diff);
578
- const selectedBit = 1 << idx;
579
- const task = getTask(idx);
580
- decodeTask(task, idx);
581
- consumedBits = (consumedBits ^ selectedBit) | 0;
582
- settleTask(task);
583
- diff ^= selectedBit;
584
- modified++;
585
- if ((modified & 7) === 0 && consumedBits !== 0) {
586
- LastWorker = (LastWorker ^ consumedBits) | 0;
587
- a_store(workerBits, 0, LastWorker);
588
- consumedBits = 0 | 0;
589
- }
590
- last = idx;
591
- }
592
- while (diff !== 0) {
593
- const lowerMask = last === 31 ? 0x7fffffff : ((1 << last) - 1);
594
- let pick = diff & lowerMask;
595
- if (pick === 0)
596
- pick = diff;
597
- const idx = 31 - clz32(pick);
598
- const selectedBit = 1 << idx;
599
- const task = getTask(idx);
600
- decodeTask(task, idx);
601
- consumedBits = (consumedBits ^ selectedBit) | 0;
602
- settleTask(task);
603
- diff ^= selectedBit;
604
- modified++;
605
- if ((modified & 7) === 0 && consumedBits !== 0) {
606
- LastWorker = (LastWorker ^ consumedBits) | 0;
607
- a_store(workerBits, 0, LastWorker);
608
- consumedBits = 0 | 0;
609
- }
610
- last = idx;
611
- }
612
- if (consumedBits !== 0) {
613
- LastWorker = (LastWorker ^ consumedBits) | 0;
614
- a_store(workerBits, 0, LastWorker);
615
- }
616
- lastResolved = last;
617
- return modified;
618
- };
619
- }
620
- const onResolvedTask = onResolved;
621
- return () => {
622
- let diff = (a_load(hostBits, 0) ^ LastWorker) | 0;
623
- if (diff === 0)
624
- return 0;
625
- let modified = 0;
626
- let consumedBits = 0 | 0;
627
- let last = lastResolved;
628
- if (last === 32) {
629
- const idx = 31 - clz32(diff);
630
- const selectedBit = 1 << idx;
631
- const task = getTask(idx);
632
- decodeTask(task, idx);
633
- consumedBits = (consumedBits ^ selectedBit) | 0;
634
- settleTask(task);
635
- onResolvedTask(task);
636
- diff ^= selectedBit;
637
- modified++;
638
- if ((modified & 7) === 0 && consumedBits !== 0) {
639
- LastWorker = (LastWorker ^ consumedBits) | 0;
640
- a_store(workerBits, 0, LastWorker);
641
- consumedBits = 0 | 0;
642
- }
643
- last = idx;
644
- }
645
- while (diff !== 0) {
646
- const lowerMask = last === 31 ? 0x7fffffff : ((1 << last) - 1);
647
- let pick = diff & lowerMask;
648
- if (pick === 0)
649
- pick = diff;
650
- const idx = 31 - clz32(pick);
651
- const selectedBit = 1 << idx;
652
- const task = getTask(idx);
653
- decodeTask(task, idx);
654
- consumedBits = (consumedBits ^ selectedBit) | 0;
655
- settleTask(task);
656
- onResolvedTask(task);
657
- diff ^= selectedBit;
658
- modified++;
659
- if ((modified & 7) === 0 && consumedBits !== 0) {
660
- LastWorker = (LastWorker ^ consumedBits) | 0;
661
- a_store(workerBits, 0, LastWorker);
662
- consumedBits = 0 | 0;
663
- }
664
- last = idx;
665
- }
666
- if (consumedBits !== 0) {
667
- LastWorker = (LastWorker ^ consumedBits) | 0;
668
- a_store(workerBits, 0, LastWorker);
669
- }
670
- lastResolved = last;
671
- return modified;
672
- };
673
- }
567
+ const hasOnResolved = onResolved !== undefined;
568
+ const onResolvedTask = onResolved ?? def;
674
569
  const shouldSettleTask = shouldSettle;
675
- if (!onResolved) {
676
- return () => {
677
- let diff = (a_load(hostBits, 0) ^ LastWorker) | 0;
678
- if (diff === 0)
679
- return 0;
680
- let modified = 0;
681
- let consumedBits = 0 | 0;
682
- let last = lastResolved;
683
- if (last === 32) {
684
- const idx = 31 - clz32(diff);
685
- const selectedBit = 1 << idx;
686
- const task = getTask(idx);
687
- decodeTask(task, idx);
688
- consumedBits = (consumedBits ^ selectedBit) | 0;
689
- if (shouldSettleTask(task)) {
690
- settleTask(task);
691
- }
692
- diff ^= selectedBit;
693
- modified++;
694
- if ((modified & 7) === 0 && consumedBits !== 0) {
695
- LastWorker = (LastWorker ^ consumedBits) | 0;
696
- a_store(workerBits, 0, LastWorker);
697
- consumedBits = 0 | 0;
698
- }
699
- last = idx;
700
- }
701
- while (diff !== 0) {
702
- const lowerMask = last === 31 ? 0x7fffffff : ((1 << last) - 1);
703
- let pick = diff & lowerMask;
704
- if (pick === 0)
705
- pick = diff;
706
- const idx = 31 - clz32(pick);
707
- const selectedBit = 1 << idx;
708
- const task = getTask(idx);
709
- decodeTask(task, idx);
710
- consumedBits = (consumedBits ^ selectedBit) | 0;
711
- if (shouldSettleTask(task)) {
712
- settleTask(task);
713
- }
714
- diff ^= selectedBit;
715
- modified++;
716
- if ((modified & 7) === 0 && consumedBits !== 0) {
717
- LastWorker = (LastWorker ^ consumedBits) | 0;
718
- a_store(workerBits, 0, LastWorker);
719
- consumedBits = 0 | 0;
720
- }
721
- last = idx;
722
- }
723
- if (consumedBits !== 0) {
724
- LastWorker = (LastWorker ^ consumedBits) | 0;
725
- a_store(workerBits, 0, LastWorker);
726
- }
727
- lastResolved = last;
728
- return modified;
729
- };
730
- }
731
- const onResolvedTask = onResolved;
732
570
  return () => {
733
571
  let diff = (a_load(hostBits, 0) ^ LastWorker) | 0;
734
572
  if (diff === 0)
@@ -742,9 +580,10 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
742
580
  const task = getTask(idx);
743
581
  decodeTask(task, idx);
744
582
  consumedBits = (consumedBits ^ selectedBit) | 0;
745
- if (shouldSettleTask(task)) {
583
+ if (shouldSettleTask === undefined || shouldSettleTask(task)) {
746
584
  settleTask(task);
747
- onResolvedTask(task);
585
+ if (hasOnResolved)
586
+ onResolvedTask(task);
748
587
  }
749
588
  diff ^= selectedBit;
750
589
  modified++;
@@ -765,9 +604,10 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
765
604
  const task = getTask(idx);
766
605
  decodeTask(task, idx);
767
606
  consumedBits = (consumedBits ^ selectedBit) | 0;
768
- if (shouldSettleTask(task)) {
607
+ if (shouldSettleTask === undefined || shouldSettleTask(task)) {
769
608
  settleTask(task);
770
- onResolvedTask(task);
609
+ if (hasOnResolved)
610
+ onResolvedTask(task);
771
611
  }
772
612
  diff ^= selectedBit;
773
613
  modified++;
@@ -92,22 +92,14 @@ const decodeExternalPayload = (raw) => {
92
92
  : { codec: codecId, metadata };
93
93
  };
94
94
  const PROCESS_SHARED_BUFFER_NUMERIC_WORDS = 8;
95
- const PROCESS_SHARED_BUFFER_NUMERIC_BYTES = PROCESS_SHARED_BUFFER_NUMERIC_WORDS * Uint32Array.BYTES_PER_ELEMENT;
96
95
  const NUMERIC_SENTINEL = 0xffffffff;
97
- const readProcessSharedBufferNumericPayload = (bytes) => {
98
- const out = new Uint32Array(PROCESS_SHARED_BUFFER_NUMERIC_WORDS);
99
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
100
- for (let i = 0; i < PROCESS_SHARED_BUFFER_NUMERIC_WORDS; i++) {
101
- out[i] = view.getUint32(i * Uint32Array.BYTES_PER_ELEMENT, true);
102
- }
103
- return out;
104
- };
105
- const decodeProcessSharedBufferNumeric = (bytes) => {
106
- const metadata = readProcessSharedBufferNumericPayload(bytes);
96
+ const decodeProcessSharedBufferNumericWords = (words) => {
107
97
  const codec = externalPayloadGlobal.__KNITTING_PAYLOAD_CODECS__?.[PROCESS_SHARED_BUFFER_CODEC_ID];
108
- return typeof codec?.decodeNumeric === "function"
109
- ? codec.decodeNumeric(metadata)
110
- : { codec: PROCESS_SHARED_BUFFER_CODEC_ID, metadata };
98
+ if (typeof codec?.decodeNumeric === "function")
99
+ return codec.decodeNumeric(words);
100
+ // `words` is a reusable scratch; copy it on the (unreachable in practice)
101
+ // codec-missing diagnostic path so the escaped value never aliases it.
102
+ return { codec: PROCESS_SHARED_BUFFER_CODEC_ID, metadata: Array.from(words) };
111
103
  };
112
104
  const tryEncodePrimitiveTask = (task) => {
113
105
  const value = task.value;
@@ -299,7 +291,7 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
299
291
  payloadConfig: resolvedPayloadConfig,
300
292
  textCompat: textCompat?.payload,
301
293
  });
302
- const { maxBytes: staticMaxBytes, writeBinary: writeStaticBinary, writeBuffer: writeStaticBuffer, writeArrayBuffer: writeStaticArrayBuffer, writeExactUint8Array: writeStaticExactUint8Array, write8Binary: writeStatic8Binary, writeUtf8: writeStaticUtf8, } = requireStaticIO(headersBuffer, headerSlotStrideU32, textCompat?.headers);
294
+ const { maxBytes: staticMaxBytes, writeBinary: writeStaticBinary, writeBuffer: writeStaticBuffer, writeArrayBuffer: writeStaticArrayBuffer, writeExactUint8Array: writeStaticExactUint8Array, writeU32Words: writeStaticU32Words, write8Binary: writeStatic8Binary, writeUtf8: writeStaticUtf8, } = requireStaticIO(headersBuffer, headerSlotStrideU32, textCompat?.headers);
303
295
  const dynamicLimitError = (task, actualBytes, label) => encoderError({
304
296
  task,
305
297
  type: ErrorKnitting.Serializable,
@@ -523,11 +515,7 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
523
515
  task.value = null;
524
516
  return true;
525
517
  };
526
- const processSharedBufferScratch = new Uint8Array(PROCESS_SHARED_BUFFER_NUMERIC_BYTES);
527
- const processSharedBufferScratchView = new DataView(processSharedBufferScratch.buffer);
528
- const writeProcessSharedBufferWord = (index, value) => {
529
- processSharedBufferScratchView.setUint32(index * Uint32Array.BYTES_PER_ELEMENT, value, true);
530
- };
518
+ const processSharedBufferWords = new Uint32Array(PROCESS_SHARED_BUFFER_NUMERIC_WORDS);
531
519
  const tryEncodeProcessSharedBufferNumeric = (task, slotIndex, value) => {
532
520
  const descriptor = value.descriptor;
533
521
  if (descriptor === undefined ||
@@ -544,19 +532,20 @@ export const encodePayload = ({ lockSector, payload, sab, payloadConfig, headers
544
532
  !isU32(baseAddressMod64)) {
545
533
  return false;
546
534
  }
547
- writeProcessSharedBufferWord(0, descriptor.fd);
548
- writeProcessSharedBufferWord(1, descriptor.size);
549
- writeProcessSharedBufferWord(2, descriptor.byteLength);
550
- writeProcessSharedBufferWord(3, value.byteOffset);
551
- writeProcessSharedBufferWord(4, value.byteLength);
552
- writeProcessSharedBufferWord(5, runtimeCode(descriptor.runtime));
553
- writeProcessSharedBufferWord(6, kindCode(descriptor.kind));
554
- writeProcessSharedBufferWord(7, baseAddressMod64 === undefined ? NUMERIC_SENTINEL : baseAddressMod64);
555
- const written = writeStaticBinary(processSharedBufferScratch, slotIndex);
556
- if (written !== PROCESS_SHARED_BUFFER_NUMERIC_BYTES)
557
- return false;
535
+ processSharedBufferWords[0] = descriptor.fd;
536
+ processSharedBufferWords[1] = descriptor.size;
537
+ processSharedBufferWords[2] = descriptor.byteLength;
538
+ processSharedBufferWords[3] = value.byteOffset;
539
+ processSharedBufferWords[4] = value.byteLength;
540
+ processSharedBufferWords[5] = runtimeCode(descriptor.runtime);
541
+ processSharedBufferWords[6] = kindCode(descriptor.kind);
542
+ processSharedBufferWords[7] = baseAddressMod64 === undefined
543
+ ? NUMERIC_SENTINEL
544
+ : baseAddressMod64;
558
545
  task[TaskIndex.Type] = PayloadBuffer.ProcessSharedBuffer;
559
- task[TaskIndex.PayloadLen] = written;
546
+ // Static region is a Uint32Array shared in-process; write the descriptor
547
+ // words straight in instead of staging bytes through a DataView + copy.
548
+ task[TaskIndex.PayloadLen] = writeStaticU32Words(processSharedBufferWords, PROCESS_SHARED_BUFFER_NUMERIC_WORDS, slotIndex);
560
549
  task.value = null;
561
550
  return true;
562
551
  };
@@ -969,7 +958,11 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
969
958
  payloadConfig: resolvedPayloadConfig,
970
959
  textCompat: textCompat?.payload,
971
960
  });
972
- const { readUtf8: readStaticUtf8, readBytesCopy: readStaticBytesCopy, readBytesBufferCopy: readStaticBufferCopy, readBufferCopy: readStaticBuffer, readUint8ArrayCopy: readStaticUint8ArrayCopy, readBytesArrayBufferCopy: readStaticArrayBufferCopy, readArrayBufferCopy: readStaticArrayBuffer, read8BytesFloatCopy: readStatic8BytesFloatCopy, } = requireStaticIO(headersBuffer, headerSlotStrideU32, textCompat?.headers);
961
+ const { readUtf8: readStaticUtf8, readBytesBufferCopy: readStaticBufferCopy, readBufferCopy: readStaticBuffer, readUint8ArrayCopy: readStaticUint8ArrayCopy, readBytesArrayBufferCopy: readStaticArrayBufferCopy, readArrayBufferCopy: readStaticArrayBuffer, read8BytesFloatCopy: readStatic8BytesFloatCopy, readU32Words: readStaticU32Words, } = requireStaticIO(headersBuffer, headerSlotStrideU32, textCompat?.headers);
962
+ // Reusable scratch for the ProcessSharedBuffer raw-word decode. Safe to share:
963
+ // decode is single-consumer and not re-entrant, and the words are consumed
964
+ // synchronously when building the ProcessSharedBuffer.
965
+ const processSharedBufferWords = new Uint32Array(PROCESS_SHARED_BUFFER_NUMERIC_WORDS);
973
966
  // TODO: remove slotIndex and make that all their callers
974
967
  // store the slot in their Task, to just get it when it comes
975
968
  // to the static versions of decoding
@@ -1118,7 +1111,7 @@ export const decodePayload = ({ lockSector, payload, sab, payloadConfig, headers
1118
1111
  task.value = decodeExternalPayload(readStaticUtf8(0, task[TaskIndex.PayloadLen], slotIndex));
1119
1112
  return;
1120
1113
  case PayloadBuffer.ProcessSharedBuffer:
1121
- task.value = decodeProcessSharedBufferNumeric(readStaticBytesCopy(0, task[TaskIndex.PayloadLen], slotIndex));
1114
+ task.value = decodeProcessSharedBufferNumericWords(readStaticU32Words(processSharedBufferWords, PROCESS_SHARED_BUFFER_NUMERIC_WORDS, slotIndex));
1122
1115
  return;
1123
1116
  case PayloadBuffer.Date:
1124
1117
  Uint32View[0] = task[TaskIndex.Start];
@@ -37,6 +37,8 @@ export declare const createSharedStaticBufferIO: ({ headersBuffer, slotStrideU32
37
37
  writeArrayBuffer: (src: ArrayBuffer, at: number, start?: number) => number;
38
38
  writeExactUint8Array: (src: Uint8Array, at: number, start?: number) => number;
39
39
  writeUint8Array: (src: Uint8Array, at: number, start?: number) => number;
40
+ writeU32Words: (words: ArrayLike<number>, count: number, at: number) => number;
41
+ readU32Words: (out: Uint32Array, count: number, at: number) => Uint32Array;
40
42
  write8Binary: (src: Float64Array, at: number, start?: number) => number;
41
43
  readBytesCopy: (start: number, end: number, at: number) => Uint8Array<ArrayBuffer>;
42
44
  readBytesView: (start: number, end: number, at: number) => Uint8Array<SharedArrayBuffer>;
@@ -269,6 +269,27 @@ export const createSharedStaticBufferIO = ({ headersBuffer, slotStrideU32, textC
269
269
  for (let i = 0; i < LockBound.slots; i++) {
270
270
  slotByteOffsets[i] = slotStartBytes(i) - baseByteOffset;
271
271
  }
272
+ // The static region is itself a Uint32Array, shared in-process with native
273
+ // endianness. Fixed-shape numeric payloads (e.g. ProcessSharedBuffer
274
+ // descriptors) can be written/read as raw words straight into the slot,
275
+ // skipping the byte scratch + DataView + copy that the generic paths require.
276
+ const baseU32 = new Uint32Array(buffer, baseByteOffset, (buffer.byteLength - baseByteOffset) >>> 2);
277
+ const slotU32Offsets = new Uint32Array(LockBound.slots);
278
+ for (let i = 0; i < LockBound.slots; i++) {
279
+ slotU32Offsets[i] = slotByteOffsets[i] >>> 2;
280
+ }
281
+ const writeU32Words = (words, count, at) => {
282
+ const base = slotU32Offsets[at];
283
+ for (let i = 0; i < count; i++)
284
+ baseU32[base + i] = words[i];
285
+ return count * u32Bytes;
286
+ };
287
+ const readU32Words = (out, count, at) => {
288
+ const base = slotU32Offsets[at];
289
+ for (let i = 0; i < count; i++)
290
+ out[i] = baseU32[base + i];
291
+ return out;
292
+ };
272
293
  const canWrite = (start, length) => (start | 0) >= 0 && (start + length) <= writableBytes;
273
294
  const writeUtf8 = (str, at) => {
274
295
  const start = slotByteOffsets[at];
@@ -364,6 +385,8 @@ export const createSharedStaticBufferIO = ({ headersBuffer, slotStrideU32, textC
364
385
  writeArrayBuffer,
365
386
  writeExactUint8Array,
366
387
  writeUint8Array,
388
+ writeU32Words,
389
+ readU32Words,
367
390
  write8Binary,
368
391
  readBytesCopy,
369
392
  readBytesView,
@@ -1,6 +1,6 @@
1
- import type { WorkerCall, tasks } from "../types.js";
1
+ import type { ComposedWithKey, tasks, WorkerCall } from "../types.js";
2
2
  export declare const createInlineExecutor: ({ tasks, genTaskID, batchSize, }: {
3
- tasks: tasks;
3
+ tasks: tasks | ComposedWithKey[];
4
4
  genTaskID: () => number;
5
5
  batchSize?: number;
6
6
  }) => {
@@ -69,9 +69,21 @@ const composeInlineCallable = (fn, timeout, useAbortToolkit = false) => {
69
69
  };
70
70
  };
71
71
  export const createInlineExecutor = ({ tasks, genTaskID, batchSize, }) => {
72
- const entries = Object.values(tasks)
73
- .sort((a, b) => a.id - b.id);
74
- const runners = entries.map((entry) => composeInlineCallable(entry.f, entry.timeout, entry.abortSignal !== undefined));
72
+ const entries = Array.isArray(tasks)
73
+ ? tasks
74
+ : Object.values(tasks).sort((a, b) => a.id - b.id);
75
+ const runners = entries.map((entry) => {
76
+ // Imported tasks must never execute on the host inline lane: their module
77
+ // import is meant to stay inside the worker so worker permission policies
78
+ // apply. The pool already routes them to worker lanes, but guard here as
79
+ // defense in depth so a dispatch regression can't silently import on host.
80
+ if (entry.imported === true) {
81
+ return () => {
82
+ throw new Error("Imported task cannot run on the host inline lane");
83
+ };
84
+ }
85
+ return composeInlineCallable(entry.f, entry.timeout, entry.abortSignal !== undefined);
86
+ });
75
87
  const initCap = 16;
76
88
  let fnByIndex = new Int32Array(initCap);
77
89
  let stateByIndex = new Int8Array(initCap).fill(-1 /* SlotStateMacro.Free */);
@@ -4,9 +4,10 @@ import { lock2 } from "../memory/lock.js";
4
4
  import type { DebugOptions, DispatcherSettings, WorkerContext, WorkerData, WorkerSettings } from "../types.js";
5
5
  import "../worker/loop.js";
6
6
  import { type PayloadBufferOptions } from "../memory/payload-config.js";
7
- export declare const spawnWorkerContext: ({ list, ids, sab, thread, debug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, usesAbortSignal, }: {
7
+ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, usesAbortSignal, }: {
8
8
  list: string[];
9
9
  ids: number[];
10
+ names: string[];
10
11
  at: number[];
11
12
  sab?: Sab;
12
13
  thread: number;
@@ -33,7 +33,7 @@ const withFixedPayloadConfig = (config) => ({
33
33
  mode: "fixed",
34
34
  payloadInitialBytes: config.payloadMaxByteLength,
35
35
  });
36
- export const spawnWorkerContext = ({ list, ids, sab, thread, debug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, usesAbortSignal, }) => {
36
+ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, usesAbortSignal, }) => {
37
37
  const tsFileUrl = new URL(import.meta.url);
38
38
  const poliWorker = RUNTIME_WORKER;
39
39
  const resolvedWorkerOptions = serializeWorkerBootstrapData(withDefaultWorkerTimers(workerOptions));
@@ -217,6 +217,7 @@ export const spawnWorkerContext = ({ list, ids, sab, thread, debug, totalNumberO
217
217
  : undefined,
218
218
  list,
219
219
  ids,
220
+ names,
220
221
  at,
221
222
  thread,
222
223
  debug,
package/src/types.d.ts CHANGED
@@ -22,6 +22,7 @@ type WorkerData = {
22
22
  abortSignalMax?: number;
23
23
  list: string[];
24
24
  ids: number[];
25
+ names: string[];
25
26
  thread: number;
26
27
  totalNumberOfThread: number;
27
28
  debug?: DebugOptions;
@@ -81,8 +82,9 @@ type TaskLike<AS extends AbortSignalOption = AbortSignalOption> = {
81
82
  } : {
82
83
  readonly abortSignal: AS;
83
84
  });
85
+ type TaskFunctionLike = (...args: any[]) => any;
84
86
  type Composed<A extends TaskInput = Args, B extends Args = Args, AS extends AbortSignalOption = undefined> = FixPoint<A, B, AS> & SecondPart;
85
- type tasks = Record<string, Composed<any, any, AbortSignalOption>>;
87
+ type tasks = Record<string, Composed<any, any, AbortSignalOption> | TaskFunctionLike>;
86
88
  type ComposedWithKey = Composed<any, any, AbortSignalOption> & {
87
89
  name: string;
88
90
  };
@@ -95,11 +97,12 @@ type NormalizeUndefinedSingleArg<T extends unknown[]> = T extends [undefined] ?
95
97
  type AbortAwareCallArgs<T extends unknown[]> = T extends [...infer Head, AbortSignalToolkit<any>] ? NormalizeUndefinedSingleArg<Head> : NormalizeUndefinedSingleArg<T>;
96
98
  type HostCallArgs<F extends (...args: any[]) => any, AS extends AbortSignalOption> = AS extends undefined ? Parameters<F> : AbortAwareCallArgs<Parameters<F>>;
97
99
  type PromisifyCallArgs<F extends (...args: any[]) => any, AS extends AbortSignalOption> = HostCallArgs<F, AS> extends infer T ? T extends unknown[] ? PromisifyArgs<T> : never : never;
98
- type AbortSignalOfTask<T extends TaskLike<any>> = T extends {
100
+ type TaskCallable<T> = T extends TaskLike<any> ? T["f"] : T extends TaskFunctionLike ? T : never;
101
+ type AbortSignalOfTask<T> = T extends {
99
102
  readonly abortSignal: infer AS;
100
103
  } ? Extract<AS, AbortSignalOption> : undefined;
101
- type FunctionMapType<T extends Record<string, TaskLike<any>>> = {
102
- [K in keyof T]: PromiseWrapped<T[K]["f"], AbortSignalOfTask<T[K]>>;
104
+ type FunctionMapType<T extends Record<string, TaskLike<any> | TaskFunctionLike>> = {
105
+ [K in keyof T]: PromiseWrapped<TaskCallable<T[K]>, AbortSignalOfTask<T[K]>>;
103
106
  };
104
107
  interface FixPointBase<A extends TaskInput, B extends Args, AS extends AbortSignalOption = undefined> {
105
108
  readonly f: TaskFn<A, B, AS>;
@@ -125,13 +128,23 @@ type SecondPart = {
125
128
  */
126
129
  readonly at: number;
127
130
  readonly importedFrom: string;
131
+ /**
132
+ * Marks a task whose worker-side function is imported dynamically (via
133
+ * `importTask`). Such tasks must never run on the host inliner lane: their
134
+ * module import is meant to happen inside the worker so worker permission
135
+ * policies apply. The pool routes them to worker lanes only, even when the
136
+ * inliner is enabled for other tasks.
137
+ */
138
+ readonly imported?: boolean;
128
139
  };
129
140
  type SingleTaskPool<A extends TaskInput = Args, B extends Args = Args, AS extends AbortSignalOption = undefined> = {
130
141
  call: PromiseWrapped<TaskFn<A, B, AS>, AS>;
131
142
  shutdown: (delayMs?: number) => Promise<void>;
143
+ [Symbol.dispose]: () => void;
132
144
  };
133
- type Pool<T extends Record<string, TaskLike<any>>> = {
145
+ type Pool<T extends Record<string, TaskLike<any> | TaskFunctionLike>> = {
134
146
  shutdown: (delayMs?: number) => Promise<void>;
147
+ [Symbol.dispose]: () => void;
135
148
  call: FunctionMapType<T>;
136
149
  };
137
150
  type ReturnFixed<A extends TaskInput = undefined, B extends Args = undefined, AS extends AbortSignalOption = undefined> = FixPoint<A, B, AS> & SecondPart & {
@@ -53,7 +53,7 @@ export const workerMainLoop = async (startupData) => {
53
53
  installTerminationGuard();
54
54
  installUnhandledRejectionSilencer();
55
55
  installPerformanceNowGuard();
56
- const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, permission, totalNumberOfThread, list, ids, at, } = startupData;
56
+ const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, permission, totalNumberOfThread, list, ids, names, at, } = startupData;
57
57
  scrubWorkerDataSensitiveBuffers(startupData);
58
58
  assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
59
59
  let Comment;
@@ -111,10 +111,11 @@ export const workerMainLoop = async (startupData) => {
111
111
  list,
112
112
  isWorker: true,
113
113
  ids,
114
+ names,
114
115
  at,
115
116
  permission,
116
117
  });
117
- assertWorkerImportsResolved({ debug, list, ids, listOfFunctions });
118
+ assertWorkerImportsResolved({ debug, list, ids, names, listOfFunctions });
118
119
  const abortSignals = abortSignalSAB
119
120
  ? signalAbortFactory({
120
121
  sab: abortSignalSAB,
@@ -260,6 +261,7 @@ const isWorkerBootPayload = (value) => {
260
261
  return isSharedBufferSource(candidate.sab) &&
261
262
  Array.isArray(candidate.list) &&
262
263
  Array.isArray(candidate.ids) &&
264
+ Array.isArray(candidate.names) &&
263
265
  Array.isArray(candidate.at) &&
264
266
  typeof candidate.thread === "number" &&
265
267
  typeof candidate.totalNumberOfThread === "number" &&
@@ -46,6 +46,7 @@ const isProcessWorkerBootPayload = (value) => {
46
46
  isProcessSharedBufferMetadata(workerData.sab) &&
47
47
  Array.isArray(workerData.list) &&
48
48
  Array.isArray(workerData.ids) &&
49
+ Array.isArray(workerData.names) &&
49
50
  Array.isArray(workerData.at) &&
50
51
  typeof workerData.thread === "number" &&
51
52
  typeof workerData.totalNumberOfThread === "number" &&
@@ -9,8 +9,9 @@ type ImportedFunctionsState = {
9
9
  debug: DebugOptions | undefined;
10
10
  list: string[];
11
11
  ids: number[];
12
+ names?: string[];
12
13
  listOfFunctions: readonly unknown[];
13
14
  };
14
15
  export declare const assertWorkerSharedMemoryBootData: ({ sab, lock, returnLock }: SharedMemoryBootData) => void;
15
- export declare const assertWorkerImportsResolved: ({ debug, list, ids, listOfFunctions }: ImportedFunctionsState) => void;
16
+ export declare const assertWorkerImportsResolved: ({ debug, list, ids, names, listOfFunctions }: ImportedFunctionsState) => void;
16
17
  export {};
@@ -17,14 +17,17 @@ export const assertWorkerSharedMemoryBootData = ({ sab, lock, returnLock }) => {
17
17
  throw new Error("worker missing return lock SABs");
18
18
  }
19
19
  };
20
- export const assertWorkerImportsResolved = ({ debug, list, ids, listOfFunctions }) => {
20
+ export const assertWorkerImportsResolved = ({ debug, list, ids, names, listOfFunctions }) => {
21
21
  if (debug?.logImportedUrl === true) {
22
22
  console.log(list);
23
23
  }
24
- if (listOfFunctions.length > 0)
24
+ if (listOfFunctions.length > 0 &&
25
+ (names === undefined || listOfFunctions.length === names.length))
25
26
  return;
26
27
  console.log(list);
27
28
  console.log(ids);
29
+ if (names !== undefined)
30
+ console.log(names);
28
31
  console.log(listOfFunctions);
29
32
  throw new Error("No imports were found.");
30
33
  };
@@ -3,6 +3,7 @@ import type { ResolvedPermissionProtocol } from "../permission/protocol.js";
3
3
  type GetFunctionParams = {
4
4
  list: string[];
5
5
  ids: number[];
6
+ names: string[];
6
7
  at: number[];
7
8
  isWorker: boolean;
8
9
  permission?: ResolvedPermissionProtocol;
@@ -21,6 +22,6 @@ export type WorkerComposedWithKey = ComposedWithKey & {
21
22
  run: WorkerCallable;
22
23
  timeout?: TimeoutSpec;
23
24
  };
24
- export declare const getFunctions: ({ list, ids, at, permission }: GetFunctionParams) => Promise<WorkerComposedWithKey[]>;
25
+ export declare const getFunctions: ({ list, ids, names, at, permission }: GetFunctionParams) => Promise<WorkerComposedWithKey[]>;
25
26
  export type GetFunctions = ReturnType<typeof getFunctions>;
26
27
  export {};
@@ -34,11 +34,12 @@ const normalizeTimeout = (timeout) => {
34
34
  const composeWorkerCallable = (fixed, _permission) => {
35
35
  return fixed.f;
36
36
  };
37
- export const getFunctions = async ({ list, ids, at, permission }) => {
37
+ export const getFunctions = async ({ list, ids, names, at, permission }) => {
38
38
  const modules = list.map((specifier) => toModuleUrl(specifier));
39
+ const nameSet = new Set(names);
39
40
  const results = await Promise.all(modules.map(async (imports) => {
40
41
  const module = (await import(__rewriteRelativeImportExtension(imports)));
41
- return Object.entries(module)
42
+ const fixedTasks = Object.entries(module)
42
43
  .filter(([_, value]) => value != null && typeof value === "object" &&
43
44
  //@ts-ignore Reason -> trust me
44
45
  value?.[endpointSymbol] === true)
@@ -47,6 +48,17 @@ export const getFunctions = async ({ list, ids, at, permission }) => {
47
48
  ...value,
48
49
  name,
49
50
  }));
51
+ const functionTasks = Object.entries(module)
52
+ .filter(([name, value]) => nameSet.has(name) && typeof value === "function")
53
+ .map(([name, value]) => ({
54
+ f: value,
55
+ id: -1,
56
+ importedFrom: imports,
57
+ at: -1,
58
+ name,
59
+ [endpointSymbol]: true,
60
+ }));
61
+ return [...fixedTasks, ...functionTasks];
50
62
  }));
51
63
  // Flatten the results, filter by IDs, and sort
52
64
  const flattened = results.flat();