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.
Files changed (62) hide show
  1. package/README.md +326 -95
  2. package/map.md +52 -8
  3. package/package.json +4 -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/scripts/build-native-addons.ts +31 -5
  16. package/src/api.d.ts +3 -2
  17. package/src/api.js +135 -34
  18. package/src/common/task-source.d.ts +1 -0
  19. package/src/common/task-source.js +5 -0
  20. package/src/connections/bun.d.ts +2 -0
  21. package/src/connections/bun.js +64 -9
  22. package/src/connections/deno.d.ts +2 -0
  23. package/src/connections/deno.js +64 -9
  24. package/src/connections/file-descriptor.d.ts +2 -0
  25. package/src/connections/file-descriptor.js +24 -2
  26. package/src/connections/node.d.ts +2 -1
  27. package/src/connections/node.js +17 -14
  28. package/src/connections/posix.d.ts +1 -0
  29. package/src/connections/posix.js +6 -0
  30. package/src/connections/process-shared-buffer.d.ts +3 -1
  31. package/src/connections/process-shared-buffer.js +19 -13
  32. package/src/connections/types.d.ts +2 -0
  33. package/src/connections/windows.d.ts +28 -0
  34. package/src/connections/windows.js +224 -0
  35. package/src/knitting_shared_memory.cc +319 -26
  36. package/src/knitting_windows_shared_memory.cc +156 -0
  37. package/src/memory/lock.js +8 -168
  38. package/src/memory/payloadCodec.js +28 -34
  39. package/src/memory/shared-buffer-io.d.ts +2 -0
  40. package/src/memory/shared-buffer-io.js +23 -0
  41. package/src/runtime/inline-executor.d.ts +2 -2
  42. package/src/runtime/inline-executor.js +15 -3
  43. package/src/runtime/pool.d.ts +3 -14
  44. package/src/runtime/pool.js +12 -543
  45. package/src/runtime/process-worker.d.ts +92 -0
  46. package/src/runtime/process-worker.js +670 -0
  47. package/src/runtime/tx-queue.d.ts +1 -1
  48. package/src/runtime/tx-queue.js +3 -1
  49. package/src/shared/abortSignal.d.ts +1 -1
  50. package/src/shared/abortSignal.js +10 -2
  51. package/src/types.d.ts +67 -8
  52. package/src/worker/bootstrap.d.ts +5 -0
  53. package/src/worker/bootstrap.js +78 -0
  54. package/src/worker/composable-runners.js +0 -8
  55. package/src/worker/loop.js +23 -156
  56. package/src/worker/process-worker-bootstrap.d.ts +8 -0
  57. package/src/worker/process-worker-bootstrap.js +160 -0
  58. package/src/worker/safety/startup.d.ts +2 -1
  59. package/src/worker/safety/startup.js +5 -2
  60. package/src/worker/task-loader.d.ts +2 -1
  61. package/src/worker/task-loader.js +14 -2
  62. package/src/worker/timers.js +19 -5
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![JSR Version](https://jsr.io/badges/@vixeny/knitting)](https://jsr.io/@vixeny/knitting)
4
4
  [![JSR Score](https://jsr.io/badges/@vixeny/knitting/score)](https://jsr.io/@vixeny/knitting)
5
+ [![npm Version](https://img.shields.io/npm/v/knitting?logo=npm&logoColor=white)](https://www.npmjs.com/package/knitting)
5
6
  [![Tests](https://github.com/mimiMonads/knitting/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/mimiMonads/knitting/actions/workflows/test.yml)
6
7
  [![Coverage Workflow](https://github.com/mimiMonads/knitting/actions/workflows/coverage.yml/badge.svg?branch=main)](https://github.com/mimiMonads/knitting/actions/workflows/coverage.yml)
7
8
  [![Coverage](https://img.shields.io/badge/coverage-92.10%25-brightgreen)](https://github.com/mimiMonads/knitting/actions/workflows/coverage.yml)
@@ -10,45 +11,38 @@
10
11
  [![Deno](https://img.shields.io/badge/deno-2%2B-000000?logo=deno&logoColor=white)](https://deno.com/)
11
12
  [![Bun](https://img.shields.io/badge/bun-1%2B-f472b6?logo=bun&logoColor=white)](https://bun.sh/)
12
13
 
13
- Knitting is a worker-pool over shared-memory IPC runtime for Node.js,
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
- Use it for the parts of your program that should run somewhere else: CPU-heavy to small
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 define a task once, spin up a pool, and call it like a normal async
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
- Under the hood, Knitting schedules work across worker threads or separate
31
- processes (depending on runtime and your settings), keeps arguments typed, and
32
- uses shared memory for transport.
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
- - Call worker code like `pool.call.myTask(arg)`.
37
- - Keep the resolved types end-to-end
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 multi-thread enviroment or process with few lines of code.
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, or processes for stronger
49
- isolation.
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
- ```bash
67
- npm install knitting
68
- ```
69
-
70
- Via JSR's npm compatibility:
55
+ From npm:
71
56
 
72
57
  ```bash
73
- jsr add --npm @vixeny/knitting
58
+ npm install knitting
74
59
  ```
75
60
 
76
61
  For Deno projects:
77
62
 
78
63
  ```bash
79
- deno add jsr:@vixeny/knitting
64
+ deno add --npm knitting
80
65
  ```
81
66
 
82
67
  ## Quick Start
83
68
 
84
69
  ```ts
85
- import { createPool, isMain, task } from "knitting";
70
+ import { createPool, isMain } from "knitting";
86
71
 
87
- export const square = task<number, number>({
88
- f: (value) => value * value,
89
- });
72
+ export const square = (value: number) => value * value;
90
73
 
91
- export const greet = task<string, string>({
92
- f: (name) => `hello ${name}`,
93
- });
74
+ export const greet = (name: string) => `hello ${name}`;
94
75
 
95
76
  if (isMain) {
96
- const pool = createPool({ threads: 2 })({ square, greet });
77
+ using pool = createPool({ threads: 2 })({ square, greet });
97
78
 
98
- try {
99
- const [four, message] = await Promise.all([
100
- pool.call.square(2),
101
- pool.call.greet("knitting"),
102
- ]);
79
+ const [four, message] = await Promise.all([
80
+ pool.call.square(2),
81
+ pool.call.greet("knitting"),
82
+ ]);
103
83
 
104
- console.log({ four, message });
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: "node",
372
+ processRuntime: "deno",
330
373
  },
331
374
  })({ add });
332
375
  ```
333
376
 
334
- `processRuntime` can be `"node"`, `"deno"`, or `"bun"`. You can also provide a
335
- `processCommandPrefix` when workers need to be launched through a wrapper such
336
- as a package manager, container command, or runtime shim.
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 `importTask` definition.
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 { ProcessSharedBuffer } from "knitting/process-shared-buffer";
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 shared = ProcessSharedBuffer.create(64);
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
- Sometimes you do want two unrelated processes to rendezvous on purpose. Use a
486
- named channel for that.
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 { ProcessSharedBuffer } from "knitting/process-shared-buffer";
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
- ProcessSharedBuffer.unlink(name);
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
- Use `"create"` for the process that owns the channel and `"open"` for peers.
520
- Treat the channel name like a capability: make it unique, do not accept it from
521
- untrusted input without validation, and clean it up when the channel is no
522
- longer needed. On POSIX runtimes `unlink` removes the name. On platforms where
523
- named mappings are lifetime-managed by the OS, closing the last handle is the
524
- important cleanup step.
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
- Thread workers are the broadest path: they do not need native prebuilds or FFI.
529
- Process workers and `ProcessSharedBuffer` both use OS-backed shared memory, so
530
- their support follows the native backend for each runtime.
531
-
532
- For now, Windows support means thread workers only. Process workers and
533
- `ProcessSharedBuffer` are supported on POSIX targets: Linux and macOS.
534
-
535
- | Runtime and target | Thread workers | Process workers | `ProcessSharedBuffer` | Native path |
536
- | --- | --- | --- | --- | --- |
537
- | Node.js 22 / 24 on Linux x64 | Supported | Supported | Supported | Shipped Node `.node` prebuilds. |
538
- | Node.js 22 / 24 on macOS x64 | Supported | Supported | Supported | Shipped Node `.node` prebuilds. |
539
- | Node.js 22 / 24 on macOS arm64 | Supported | Supported | Supported | Shipped Node `.node` prebuilds. |
540
- | Node.js 22 / 24 on Windows x64 | Supported | Not supported | Not supported | Native shared memory is POSIX-only. |
541
- | Other POSIX Node.js ABI or arch | Supported | Local native build needed | Local native build needed | Run `bun run build:native` before using native shared memory. |
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 on Linux or macOS:
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