knitting 0.1.46 → 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.
- package/README.md +326 -95
- package/map.md +52 -8
- package/package.json +4 -3
- package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/scripts/build-native-addons.ts +31 -5
- package/src/api.d.ts +3 -2
- package/src/api.js +135 -34
- package/src/common/task-source.d.ts +1 -0
- package/src/common/task-source.js +5 -0
- package/src/connections/bun.d.ts +2 -0
- package/src/connections/bun.js +64 -9
- package/src/connections/deno.d.ts +2 -0
- package/src/connections/deno.js +64 -9
- package/src/connections/file-descriptor.d.ts +2 -0
- package/src/connections/file-descriptor.js +24 -2
- package/src/connections/node.d.ts +2 -1
- package/src/connections/node.js +17 -14
- package/src/connections/posix.d.ts +1 -0
- package/src/connections/posix.js +6 -0
- package/src/connections/process-shared-buffer.d.ts +3 -1
- package/src/connections/process-shared-buffer.js +19 -13
- package/src/connections/types.d.ts +2 -0
- package/src/connections/windows.d.ts +28 -0
- package/src/connections/windows.js +224 -0
- package/src/knitting_shared_memory.cc +319 -26
- package/src/knitting_windows_shared_memory.cc +156 -0
- package/src/memory/lock.js +8 -168
- package/src/memory/payloadCodec.js +28 -34
- package/src/memory/shared-buffer-io.d.ts +2 -0
- package/src/memory/shared-buffer-io.js +23 -0
- package/src/runtime/inline-executor.d.ts +2 -2
- package/src/runtime/inline-executor.js +15 -3
- package/src/runtime/pool.d.ts +3 -14
- package/src/runtime/pool.js +12 -543
- package/src/runtime/process-worker.d.ts +92 -0
- package/src/runtime/process-worker.js +670 -0
- package/src/runtime/tx-queue.d.ts +1 -1
- package/src/runtime/tx-queue.js +3 -1
- package/src/shared/abortSignal.d.ts +1 -1
- package/src/shared/abortSignal.js +10 -2
- package/src/types.d.ts +67 -8
- package/src/worker/bootstrap.d.ts +5 -0
- package/src/worker/bootstrap.js +78 -0
- package/src/worker/composable-runners.js +0 -8
- package/src/worker/loop.js +23 -156
- package/src/worker/process-worker-bootstrap.d.ts +8 -0
- package/src/worker/process-worker-bootstrap.js +160 -0
- package/src/worker/safety/startup.d.ts +2 -1
- package/src/worker/safety/startup.js +5 -2
- package/src/worker/task-loader.d.ts +2 -1
- package/src/worker/task-loader.js +14 -2
- package/src/worker/timers.js +19 -5
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://jsr.io/@vixeny/knitting)
|
|
4
4
|
[](https://jsr.io/@vixeny/knitting)
|
|
5
|
+
[](https://www.npmjs.com/package/knitting)
|
|
5
6
|
[](https://github.com/mimiMonads/knitting/actions/workflows/test.yml)
|
|
6
7
|
[](https://github.com/mimiMonads/knitting/actions/workflows/coverage.yml)
|
|
7
8
|
[](https://github.com/mimiMonads/knitting/actions/workflows/coverage.yml)
|
|
@@ -10,45 +11,38 @@
|
|
|
10
11
|
[](https://deno.com/)
|
|
11
12
|
[](https://bun.sh/)
|
|
12
13
|
|
|
13
|
-
Knitting is a worker
|
|
14
|
-
Deno, and Bun. Which mission is make javascript a real multicore language,
|
|
15
|
-
Thanks to its memory design, it can be from 5x to 25x faster than using `postMessages`
|
|
16
|
-
bypassing OS sockect comunnication entielly with a novel protocol written from scratch.
|
|
14
|
+
Knitting is a worker pool over a shared-memory IPC runtime for Node.js, Deno, and Bun. Our mission is to make JavaScript a multicore language with real inter-runtime communication.
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
work, runtime-isolated jobs, long-running tools, or anything that needs to move
|
|
20
|
-
speed and type felixbility.
|
|
16
|
+
Thanks to its memory design, it can be 5x to 25x faster than using `postMessages` , bypassing OS socket communication entirely with a novel protocol written from scratch.
|
|
21
17
|
|
|
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.
|
|
22
19
|
|
|
23
|
-
You
|
|
20
|
+
You export a function or task, spin up a pool, and call it like a normal async
|
|
24
21
|
function:
|
|
25
22
|
|
|
26
23
|
```ts
|
|
24
|
+
|
|
27
25
|
const result = await pool.call.resizeImage(file);
|
|
26
|
+
|
|
28
27
|
```
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
So you only have to take care of 4 things:
|
|
30
|
+
- Export a function or task
|
|
31
|
+
- Create a pool
|
|
32
|
+
- Call it
|
|
33
|
+
- Let `using` or `shutdown()` close the pool
|
|
33
34
|
|
|
34
|
-
## So
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- Choose `threads` for speed or `processes` for stronger isolation.
|
|
39
|
-
- Move supported payloads via shared memory (great for binary data).
|
|
40
|
-
- Run on Node.js, Deno, or Bun.
|
|
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.
|
|
37
|
+
|
|
41
38
|
|
|
42
39
|
## Why use it?
|
|
43
40
|
|
|
44
|
-
- Easy to use: Have a
|
|
45
|
-
- Great type support: pass primitives, JSON, Promise of these and special types (typed arrays, Node
|
|
46
|
-
`Buffer`, `Envelope`, and `ProcessSharedBuffer`).
|
|
41
|
+
- Easy to use: Have a multithreaded environment or process with few lines of code.
|
|
42
|
+
- Great type support: pass primitives, JSON, Promise of these, and special types (typed arrays, `Node Buffer`, `Envelope`, and `ProcessSharedBuffer`).
|
|
47
43
|
- Runtime flexibility: the same API across Node.js, Deno, and Bun.
|
|
48
|
-
- Worker choices: use threads for fast pools
|
|
49
|
-
|
|
50
|
-
- All out-of-the-box expierince: strict-by-default permissions, payload-size limits, task
|
|
51
|
-
timeouts, abort-aware tasks, and worker hard timeouts.
|
|
44
|
+
- Worker choices: use threads for fast pools or processes for stronger isolation.
|
|
45
|
+
- All out-of-the-box experiences: strict-by-default permissions, payload-size limits, task timeouts, abort-aware tasks, and worker hard timeouts.
|
|
52
46
|
|
|
53
47
|
## Requirements
|
|
54
48
|
|
|
@@ -56,55 +50,38 @@ uses shared memory for transport.
|
|
|
56
50
|
- Deno 2+
|
|
57
51
|
- Bun 1+
|
|
58
52
|
|
|
59
|
-
Process workers and `ProcessSharedBuffer` use the native shared-memory layer.
|
|
60
|
-
On Linux and macOS, build it before running process/shared-memory tests.
|
|
61
|
-
If you're only using thread workers, you can typically ignore this and use
|
|
62
|
-
Knitting without building native addons.
|
|
63
|
-
|
|
64
53
|
## Install
|
|
65
54
|
|
|
66
|
-
|
|
67
|
-
npm install knitting
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Via JSR's npm compatibility:
|
|
55
|
+
From npm:
|
|
71
56
|
|
|
72
57
|
```bash
|
|
73
|
-
|
|
58
|
+
npm install knitting
|
|
74
59
|
```
|
|
75
60
|
|
|
76
61
|
For Deno projects:
|
|
77
62
|
|
|
78
63
|
```bash
|
|
79
|
-
deno add
|
|
64
|
+
deno add --npm knitting
|
|
80
65
|
```
|
|
81
66
|
|
|
82
67
|
## Quick Start
|
|
83
68
|
|
|
84
69
|
```ts
|
|
85
|
-
import { createPool, isMain
|
|
70
|
+
import { createPool, isMain } from "knitting";
|
|
86
71
|
|
|
87
|
-
export const square =
|
|
88
|
-
f: (value) => value * value,
|
|
89
|
-
});
|
|
72
|
+
export const square = (value: number) => value * value;
|
|
90
73
|
|
|
91
|
-
export const greet =
|
|
92
|
-
f: (name) => `hello ${name}`,
|
|
93
|
-
});
|
|
74
|
+
export const greet = (name: string) => `hello ${name}`;
|
|
94
75
|
|
|
95
76
|
if (isMain) {
|
|
96
|
-
|
|
77
|
+
using pool = createPool({ threads: 2 })({ square, greet });
|
|
97
78
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
]);
|
|
79
|
+
const [four, message] = await Promise.all([
|
|
80
|
+
pool.call.square(2),
|
|
81
|
+
pool.call.greet("knitting"),
|
|
82
|
+
]);
|
|
103
83
|
|
|
104
|
-
|
|
105
|
-
} finally {
|
|
106
|
-
await pool.shutdown();
|
|
107
|
-
}
|
|
84
|
+
console.log({ four, message });
|
|
108
85
|
}
|
|
109
86
|
```
|
|
110
87
|
|
|
@@ -144,6 +121,39 @@ if (isMain) {
|
|
|
144
121
|
}
|
|
145
122
|
```
|
|
146
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
|
+
|
|
147
157
|
Once you have a pool, calls are just promises, so batching looks like normal
|
|
148
158
|
JavaScript:
|
|
149
159
|
|
|
@@ -257,6 +267,14 @@ When workers import files, keep the pool's permission settings in mind. The
|
|
|
257
267
|
default strict mode allows task imports, but custom permission policies can
|
|
258
268
|
limit reads, writes, environment access, networking, and process execution.
|
|
259
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
|
+
|
|
260
278
|
### Single-task shorthand
|
|
261
279
|
|
|
262
280
|
For quick scripts, a task can create its own pool:
|
|
@@ -301,12 +319,37 @@ Common options you might tweak:
|
|
|
301
319
|
| `payload` | Shared payload-buffer settings: `mode`, `payloadInitialBytes`, `payloadMaxByteLength`, and `maxPayloadBytes`. |
|
|
302
320
|
| `abortSignalCapacity` | Number of shared abort slots available to abort-aware calls. |
|
|
303
321
|
| `worker.resolveAfterFinishingAll` | Let submitted calls finish before shutdown resolves. |
|
|
322
|
+
| `worker.bootstrap` | Privileged async hook imported and awaited before task modules load. |
|
|
304
323
|
| `worker.hardTimeoutMs` | Force pool shutdown when a task exceeds this many milliseconds. |
|
|
305
324
|
| `worker.runtime` | Choose `"thread"` or `"process"` workers. |
|
|
325
|
+
| `worker.processSharedMemory` | Process-worker memory discovery: `"inherit"` by default on POSIX, or `"named"` for wrappers/containers that cannot preserve fd 0. |
|
|
306
326
|
| `permission` | Runtime permission policy for workers. |
|
|
307
327
|
| `debug` | Enable extra diagnostics. |
|
|
308
328
|
| `source` | Worker source override for advanced runtimes. |
|
|
309
329
|
|
|
330
|
+
### Worker bootstrap
|
|
331
|
+
|
|
332
|
+
Use `worker.bootstrap` when a worker needs privileged setup before task modules
|
|
333
|
+
are imported. The bootstrap module is imported once per worker, and its selected
|
|
334
|
+
export is awaited before Knitting imports task definitions.
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
const pool = createPool({
|
|
338
|
+
worker: {
|
|
339
|
+
bootstrap: {
|
|
340
|
+
href: "./worker-bootstrap.ts",
|
|
341
|
+
name: "setup",
|
|
342
|
+
data: { env: "worker-only" },
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
})({ add });
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Bootstrap code runs with worker startup privileges, so keep it trusted. It is a
|
|
349
|
+
good place to remove environment variables, install runtime guards, open shared
|
|
350
|
+
memory metadata, or prepare globals that task modules should see at import time.
|
|
351
|
+
Bootstrap is worker-only and cannot be combined with the inline host lane.
|
|
352
|
+
|
|
310
353
|
## Worker Runtimes
|
|
311
354
|
|
|
312
355
|
By default, workers use runtime-local threads where possible (the lowest
|
|
@@ -326,14 +369,15 @@ const pool = createPool({
|
|
|
326
369
|
threads: 2,
|
|
327
370
|
worker: {
|
|
328
371
|
runtime: "process",
|
|
329
|
-
processRuntime: "
|
|
372
|
+
processRuntime: "deno",
|
|
330
373
|
},
|
|
331
374
|
})({ add });
|
|
332
375
|
```
|
|
333
376
|
|
|
334
|
-
`processRuntime` can be `"node"`, `"deno"`, or `"bun"
|
|
335
|
-
`processCommandPrefix` when workers need to be
|
|
336
|
-
as a package manager, container command, or
|
|
377
|
+
`processRuntime` can be `"node"`, `"deno"`, or `"bun"` and defaults to
|
|
378
|
+
`"deno"`. You can also provide a `processCommandPrefix` when workers need to be
|
|
379
|
+
launched through a wrapper such as a package manager, container command, or
|
|
380
|
+
runtime shim.
|
|
337
381
|
|
|
338
382
|
That prefix is also useful for sandbox and resource-control tools. The one
|
|
339
383
|
important detail is that process workers receive their shared-memory handle on
|
|
@@ -341,6 +385,68 @@ stdin, which is file descriptor 0. Wrappers that leave stdin alone usually work;
|
|
|
341
385
|
wrappers that replace, close, or proxy stdin without passing the fd through will
|
|
342
386
|
stop the worker from booting.
|
|
343
387
|
|
|
388
|
+
For wrappers that cannot preserve fd 0, use named process-worker memory instead.
|
|
389
|
+
The worker process must share the same OS IPC namespace as the host so it can
|
|
390
|
+
reopen the named mapping.
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
const pool = createPool({
|
|
394
|
+
threads: 2,
|
|
395
|
+
worker: {
|
|
396
|
+
runtime: "process",
|
|
397
|
+
processRuntime: "node",
|
|
398
|
+
processSharedMemory: {
|
|
399
|
+
mode: "named",
|
|
400
|
+
namePrefix: "knit_worker",
|
|
401
|
+
},
|
|
402
|
+
processCommandPrefix: [
|
|
403
|
+
// The prefix runs before Knitting appends:
|
|
404
|
+
// node --no-warnings --experimental-transform-types <worker-file>
|
|
405
|
+
"docker",
|
|
406
|
+
"run",
|
|
407
|
+
// Remove the container when the worker exits.
|
|
408
|
+
"--rm",
|
|
409
|
+
// Required for named POSIX shared memory across host/container.
|
|
410
|
+
"--ipc=host",
|
|
411
|
+
// The worker imports the same files as the host, at the same path.
|
|
412
|
+
"-v",
|
|
413
|
+
`${process.cwd()}:${process.cwd()}`,
|
|
414
|
+
"-w",
|
|
415
|
+
process.cwd(),
|
|
416
|
+
// Forward Knitting's process-worker boot metadata into the container.
|
|
417
|
+
"-e",
|
|
418
|
+
"KNITTING_PROCESS_WORKER",
|
|
419
|
+
"-e",
|
|
420
|
+
"KNITTING_PROCESS_WORKER_BOOT",
|
|
421
|
+
"knitting-node-worker",
|
|
422
|
+
],
|
|
423
|
+
},
|
|
424
|
+
})({ add });
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Windows process workers
|
|
428
|
+
|
|
429
|
+
On Windows, Knitting automatically uses named shared memory for the
|
|
430
|
+
process-worker control channel. You do not need to set
|
|
431
|
+
`processSharedMemory: "named"` yourself — the runtime detects Windows and
|
|
432
|
+
forces it.
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
// Works on Windows without extra options.
|
|
436
|
+
const pool = createPool({
|
|
437
|
+
threads: 2,
|
|
438
|
+
worker: {
|
|
439
|
+
runtime: "process",
|
|
440
|
+
processRuntime: "node",
|
|
441
|
+
},
|
|
442
|
+
})({ add });
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
If you also pass `ProcessSharedBuffer` payloads to Docker workers running on
|
|
446
|
+
Windows, create the payload buffer with `mode: "create"` and a name, and add
|
|
447
|
+
`--ipc=host` to the Docker prefix — the pool-level control channel is already
|
|
448
|
+
named, but the payload buffer needs its own name so the container can reopen it.
|
|
449
|
+
|
|
344
450
|
When the goal is isolation, define the worker code with `importTask()` instead
|
|
345
451
|
of importing the task function directly into the host. That keeps the code you
|
|
346
452
|
want to isolate out of the host process; only the worker imports and runs it.
|
|
@@ -350,7 +456,6 @@ the inherited fd:
|
|
|
350
456
|
|
|
351
457
|
```ts
|
|
352
458
|
const pool = createPool({
|
|
353
|
-
threads: 2,
|
|
354
459
|
worker: {
|
|
355
460
|
runtime: "process",
|
|
356
461
|
processRuntime: "bun",
|
|
@@ -430,12 +535,48 @@ If it isn't on that list, assume it isn't portable. Some things don't (or
|
|
|
430
535
|
shouldn't) cross the boundary:
|
|
431
536
|
|
|
432
537
|
- DOM objects and platform handles.
|
|
433
|
-
- Functions, unless they are part of a `task` or
|
|
538
|
+
- Functions, unless they are exported pool tasks or part of a `task` or
|
|
539
|
+
`importTask` definition.
|
|
434
540
|
- Cyclic object graphs.
|
|
435
541
|
- `Map`, `Set`, `WeakMap`, and non-global symbols.
|
|
436
542
|
- Objects with behavior that depends on prototypes, getters, setters, or hidden
|
|
437
543
|
process-local state.
|
|
438
544
|
|
|
545
|
+
### Envelope
|
|
546
|
+
|
|
547
|
+
`Envelope` pairs a JSON-serializable header with a binary `ArrayBuffer`
|
|
548
|
+
payload. Use it when a call needs both structured metadata and raw bytes in a
|
|
549
|
+
single argument.
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
import { Envelope, createPool, isMain, task } from "knitting";
|
|
553
|
+
|
|
554
|
+
export const processImage = task<
|
|
555
|
+
Envelope<{ format: string }>,
|
|
556
|
+
Envelope<{ width: number; height: number }>
|
|
557
|
+
>({
|
|
558
|
+
f: (envelope) => {
|
|
559
|
+
const pixels = new Uint8Array(envelope.payload);
|
|
560
|
+
// ... process pixels
|
|
561
|
+
return new Envelope({ width: 800, height: 600 }, pixels.buffer);
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
if (isMain) {
|
|
566
|
+
const pool = createPool({ threads: 2 })({ processImage });
|
|
567
|
+
|
|
568
|
+
try {
|
|
569
|
+
const buffer = new ArrayBuffer(1024);
|
|
570
|
+
const result = await pool.call.processImage(
|
|
571
|
+
new Envelope({ format: "png" }, buffer),
|
|
572
|
+
);
|
|
573
|
+
console.log(result.header); // { width: 800, height: 600 }
|
|
574
|
+
} finally {
|
|
575
|
+
await pool.shutdown();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
439
580
|
If a payload is large, set `payload.maxPayloadBytes` deliberately and prefer
|
|
440
581
|
binary/shared-memory shapes over deeply nested objects.
|
|
441
582
|
|
|
@@ -446,7 +587,10 @@ memory. Use it when two workers or processes need to see the same bytes without
|
|
|
446
587
|
copying the whole payload for every call.
|
|
447
588
|
|
|
448
589
|
```ts
|
|
449
|
-
import {
|
|
590
|
+
import {
|
|
591
|
+
getDefaultProcessSharedBufferPrimitives,
|
|
592
|
+
ProcessSharedBuffer,
|
|
593
|
+
} from "knitting/process-shared-buffer";
|
|
450
594
|
import { createPool, isMain, task } from "knitting";
|
|
451
595
|
|
|
452
596
|
export const readFirstCell = task<ProcessSharedBuffer, number>({
|
|
@@ -455,13 +599,14 @@ export const readFirstCell = task<ProcessSharedBuffer, number>({
|
|
|
455
599
|
|
|
456
600
|
if (isMain) {
|
|
457
601
|
const pool = createPool({ threads: 1 })({ readFirstCell });
|
|
458
|
-
const
|
|
602
|
+
const primitives = getDefaultProcessSharedBufferPrimitives();
|
|
603
|
+
const shared = ProcessSharedBuffer.create(64, primitives);
|
|
459
604
|
|
|
460
605
|
try {
|
|
461
606
|
Atomics.store(shared.view(Int32Array), 0, 42);
|
|
462
607
|
console.log(await pool.call.readFirstCell(shared));
|
|
463
608
|
} finally {
|
|
464
|
-
shared.close();
|
|
609
|
+
shared.descriptor.mapping?.close?.();
|
|
465
610
|
await pool.shutdown();
|
|
466
611
|
}
|
|
467
612
|
}
|
|
@@ -482,19 +627,24 @@ programs do not accidentally inherit them.
|
|
|
482
627
|
|
|
483
628
|
### Named channels for independent processes
|
|
484
629
|
|
|
485
|
-
|
|
486
|
-
|
|
630
|
+
Use a named channel when two processes need to find the same shared memory
|
|
631
|
+
without inheriting an fd from each other. One process creates the channel by
|
|
632
|
+
name; the other opens that same name.
|
|
487
633
|
|
|
488
634
|
```ts
|
|
489
|
-
import {
|
|
635
|
+
import {
|
|
636
|
+
getDefaultProcessSharedBufferPrimitives,
|
|
637
|
+
ProcessSharedBuffer,
|
|
638
|
+
} from "knitting/process-shared-buffer";
|
|
490
639
|
|
|
491
640
|
const name = "knitting-demo-channel";
|
|
641
|
+
const primitives = getDefaultProcessSharedBufferPrimitives();
|
|
492
642
|
|
|
493
643
|
const owner = ProcessSharedBuffer.create({
|
|
494
644
|
name,
|
|
495
645
|
size: 64,
|
|
496
646
|
mode: "create",
|
|
497
|
-
});
|
|
647
|
+
}, primitives);
|
|
498
648
|
|
|
499
649
|
try {
|
|
500
650
|
Atomics.store(owner.view(Int32Array), 0, 7);
|
|
@@ -503,46 +653,121 @@ try {
|
|
|
503
653
|
name,
|
|
504
654
|
size: 64,
|
|
505
655
|
mode: "open",
|
|
506
|
-
});
|
|
656
|
+
}, primitives);
|
|
507
657
|
|
|
508
658
|
try {
|
|
509
659
|
console.log(Atomics.load(peer.view(Int32Array), 0));
|
|
510
660
|
} finally {
|
|
511
|
-
peer.close();
|
|
661
|
+
peer.descriptor.mapping?.close?.();
|
|
512
662
|
}
|
|
513
663
|
} finally {
|
|
514
|
-
owner.close();
|
|
515
|
-
|
|
664
|
+
owner.descriptor.mapping?.close?.();
|
|
665
|
+
primitives.unlinkSharedMemory?.(name);
|
|
666
|
+
}
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Use `"create"` on the owner side and `"open"` on the peer side. The name is the
|
|
670
|
+
thing that grants access, so generate a hard-to-guess name and keep it private.
|
|
671
|
+
When you are done, close the mappings and unlink the name where the runtime
|
|
672
|
+
supports it.
|
|
673
|
+
|
|
674
|
+
### Sending `ProcessSharedBuffer` to Docker workers
|
|
675
|
+
|
|
676
|
+
Docker process workers can receive a `ProcessSharedBuffer`, but it needs to be
|
|
677
|
+
named. The default anonymous form is fd-backed and private to the parent-child
|
|
678
|
+
process path; Docker does not inherit that fd in a way the worker can reopen.
|
|
679
|
+
|
|
680
|
+
Use a named buffer for the payload and named process-worker memory for the pool:
|
|
681
|
+
|
|
682
|
+
```ts
|
|
683
|
+
import { createPool, isMain, task } from "knitting";
|
|
684
|
+
import {
|
|
685
|
+
getDefaultProcessSharedBufferPrimitives,
|
|
686
|
+
ProcessSharedBuffer,
|
|
687
|
+
} from "knitting/process-shared-buffer";
|
|
688
|
+
|
|
689
|
+
export const readCounter = task<ProcessSharedBuffer, number>({
|
|
690
|
+
f: (shared) => Atomics.load(shared.view(Int32Array), 0),
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
if (isMain) {
|
|
694
|
+
const cwd = process.cwd();
|
|
695
|
+
const name = `knitting-docker-counter-${process.pid}`;
|
|
696
|
+
const primitives = getDefaultProcessSharedBufferPrimitives();
|
|
697
|
+
const shared = ProcessSharedBuffer.create({
|
|
698
|
+
mode: "create",
|
|
699
|
+
name,
|
|
700
|
+
size: 64,
|
|
701
|
+
}, primitives);
|
|
702
|
+
|
|
703
|
+
const pool = createPool({
|
|
704
|
+
threads: 1,
|
|
705
|
+
worker: {
|
|
706
|
+
runtime: "process",
|
|
707
|
+
processRuntime: "node",
|
|
708
|
+
processSharedMemory: "named",
|
|
709
|
+
processCommandPrefix: [
|
|
710
|
+
// Knitting appends the actual Node worker command after this prefix.
|
|
711
|
+
"docker",
|
|
712
|
+
"run",
|
|
713
|
+
// Named shared memory needs a shared IPC namespace.
|
|
714
|
+
"--ipc=host",
|
|
715
|
+
// Mount the project so the container can import the worker module.
|
|
716
|
+
"-v",
|
|
717
|
+
`${cwd}:${cwd}`,
|
|
718
|
+
"-w",
|
|
719
|
+
cwd,
|
|
720
|
+
// Pass Knitting's boot payload through Docker.
|
|
721
|
+
"-e",
|
|
722
|
+
"KNITTING_PROCESS_WORKER",
|
|
723
|
+
"-e",
|
|
724
|
+
"KNITTING_PROCESS_WORKER_BOOT",
|
|
725
|
+
"node:24-trixie-slim",
|
|
726
|
+
],
|
|
727
|
+
},
|
|
728
|
+
permission: "unsafe",
|
|
729
|
+
})({ readCounter });
|
|
730
|
+
|
|
731
|
+
try {
|
|
732
|
+
Atomics.store(shared.view(Int32Array), 0, 42);
|
|
733
|
+
console.log(await pool.call.readCounter(shared));
|
|
734
|
+
} finally {
|
|
735
|
+
await pool.shutdown();
|
|
736
|
+
shared.descriptor.mapping?.close?.();
|
|
737
|
+
primitives.unlinkSharedMemory?.(name);
|
|
738
|
+
}
|
|
516
739
|
}
|
|
517
740
|
```
|
|
518
741
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
742
|
+
There are three moving parts:
|
|
743
|
+
|
|
744
|
+
- `processSharedMemory: "named"` lets the Docker worker find Knitting's control
|
|
745
|
+
channel.
|
|
746
|
+
- `ProcessSharedBuffer.create({ mode: "create", name, size })` makes the
|
|
747
|
+
payload buffer reopenable by name.
|
|
748
|
+
- `--ipc=host` lets the container see the same POSIX shared-memory namespace.
|
|
749
|
+
|
|
750
|
+
This is same-host communication. It is fast because both sides map the same
|
|
751
|
+
bytes, but it is not a network transport and it deliberately shares IPC with the
|
|
752
|
+
container. Use names like capabilities: generate them, keep them private, and
|
|
753
|
+
unlink them when the shared memory is no longer needed.
|
|
525
754
|
|
|
526
755
|
### Current support
|
|
527
756
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
| Deno 2+ on Linux/macOS, runtime-supported arch | Supported | Supported | Supported | Uses Deno FFI into libc; allow FFI permission when permissions are enabled. |
|
|
543
|
-
| Deno 2+ on Windows | Supported | Not supported | Not supported | Current Deno backend is POSIX-only. |
|
|
544
|
-
| Bun 1+ on Linux/macOS, runtime-supported arch | Supported | Supported | Supported | Uses Bun FFI into libc. |
|
|
545
|
-
| Bun 1+ on Windows | Supported | Not supported | Not supported | Current Bun backend is POSIX-only. |
|
|
757
|
+
Knitting supports Node.js 22+, Deno 2+, and Bun 1+ on Linux, macOS, and
|
|
758
|
+
Windows.
|
|
759
|
+
|
|
760
|
+
Thread workers work without native pieces. Process workers and
|
|
761
|
+
`ProcessSharedBuffer` use the platform's shared-memory APIs. Release packages
|
|
762
|
+
include the native prebuilds needed for the supported Node targets and Windows
|
|
763
|
+
FFI path; if you are developing locally on a new Node ABI or architecture, run:
|
|
764
|
+
|
|
765
|
+
```bash
|
|
766
|
+
bun run build:native
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
For Deno projects with permissions enabled, allow FFI when using process
|
|
770
|
+
workers or `ProcessSharedBuffer`.
|
|
546
771
|
|
|
547
772
|
## Runtime Safety
|
|
548
773
|
|
|
@@ -604,7 +829,7 @@ Build the package:
|
|
|
604
829
|
bun run build
|
|
605
830
|
```
|
|
606
831
|
|
|
607
|
-
Build the native shared-memory addon
|
|
832
|
+
Build the native shared-memory addon/prebuild for the current platform:
|
|
608
833
|
|
|
609
834
|
```bash
|
|
610
835
|
bun run build:native
|
|
@@ -625,6 +850,12 @@ Emit JSON benchmark results:
|
|
|
625
850
|
./run.sh --json
|
|
626
851
|
```
|
|
627
852
|
|
|
853
|
+
Compare inherited-fd and named-shared-memory process-worker startup:
|
|
854
|
+
|
|
855
|
+
```bash
|
|
856
|
+
node --no-warnings --experimental-transform-types bench/startup.ts --named-process-shm
|
|
857
|
+
```
|
|
858
|
+
|
|
628
859
|
For a file-by-file orientation, see [map.md](./map.md).
|
|
629
860
|
|
|
630
861
|
## License
|