knitting 0.1.51 → 0.1.52

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 (54) hide show
  1. package/README.md +213 -54
  2. package/knitting.d.ts +1 -0
  3. package/map.md +15 -3
  4. package/package.json +14 -2
  5. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  6. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  7. package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
  8. package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
  9. package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
  11. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  12. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  14. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  15. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  16. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  17. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  18. package/scripts/build-native-addons.ts +5 -0
  19. package/src/api.d.ts +3 -10
  20. package/src/api.js +25 -21
  21. package/src/common/envelope.d.ts +9 -3
  22. package/src/common/envelope.js +14 -0
  23. package/src/common/worker-runtime.d.ts +2 -0
  24. package/src/common/worker-runtime.js +9 -0
  25. package/src/connections/buffer-reference-native.d.ts +56 -0
  26. package/src/connections/buffer-reference-native.js +217 -0
  27. package/src/connections/buffer-reference.d.ts +76 -0
  28. package/src/connections/buffer-reference.js +459 -0
  29. package/src/connections/index.d.ts +1 -0
  30. package/src/connections/index.js +1 -0
  31. package/src/connections/node-addons.d.ts +1 -1
  32. package/src/connections/node-buffer-pointer.d.ts +20 -0
  33. package/src/connections/node-buffer-pointer.js +16 -0
  34. package/src/connections/shared-array-buffer-payload.d.ts +36 -0
  35. package/src/connections/shared-array-buffer-payload.js +235 -0
  36. package/src/knitting_buffer_pointer.cc +425 -0
  37. package/src/memory/lock.d.ts +12 -1
  38. package/src/memory/lock.js +47 -4
  39. package/src/memory/payloadCodec.js +220 -37
  40. package/src/runtime/pool.d.ts +2 -1
  41. package/src/runtime/pool.js +8 -1
  42. package/src/runtime/process-worker.js +3 -1
  43. package/src/runtime/tx-queue.d.ts +3 -2
  44. package/src/runtime/tx-queue.js +18 -13
  45. package/src/types.d.ts +26 -18
  46. package/src/utils/http.d.ts +21 -0
  47. package/src/utils/http.js +93 -0
  48. package/src/worker/loop.js +23 -3
  49. package/src/worker/rx-queue.d.ts +4 -1
  50. package/src/worker/rx-queue.js +53 -4
  51. package/unsafe.d.ts +1 -0
  52. package/unsafe.js +1 -0
  53. package/utils.d.ts +1 -0
  54. package/utils.js +1 -0
package/README.md CHANGED
@@ -11,44 +11,55 @@
11
11
  [![Deno](https://img.shields.io/badge/deno-2%2B-000000?logo=deno&logoColor=white)](https://deno.com/)
12
12
  [![Bun](https://img.shields.io/badge/bun-1%2B-f472b6?logo=bun&logoColor=white)](https://bun.sh/)
13
13
 
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.
14
+ Knitting is a worker pool over a shared-memory IPC runtime for Node.js, Deno,
15
+ and Bun. Our mission is to make JavaScript a multicore language with real
16
+ inter-runtime communication.
15
17
 
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.
18
+ Thanks to its memory design, it can be 5x to 25x faster than using
19
+ `postMessages` , bypassing OS socket communication entirely with a novel
20
+ protocol written from scratch.
17
21
 
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
+ Use it for parts of your program that should run in different environments, such
23
+ as CPU-intensive tasks, small jobs, runtime-isolated tasks, custom isolation for
24
+ workers in Docker or bwrap environments, long-running tools, or any processes
25
+ that require speed and type flexibility.
19
26
 
20
27
  You export a function or task, spin up a pool, and call it like a normal async
21
28
  function:
22
29
 
23
30
  ```ts
24
-
25
31
  const result = await pool.call.resizeImage(file);
26
-
27
32
  ```
28
33
 
29
34
  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
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.
36
+ - Export a function or task
37
+ - Create a pool
38
+ - Call it
39
+ - Let `using` or `shutdown()` close the pool
37
40
 
41
+ Under the hood, we take care of scheduling and orchestration across worker
42
+ threads or separate processes, also handling signals, timeouts, life cycles,
43
+ memory allocation, garbage collection, and cross-runtime memory over the
44
+ processes.
38
45
 
39
46
  ## Why use it?
40
47
 
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`).
48
+ - Easy to use: Have a multithreaded environment or process with few lines of
49
+ code.
50
+ - Great type support: pass primitives, JSON, Promise of these, and special types
51
+ (typed arrays, `Node Buffer`, `Envelope`, and `ProcessSharedBuffer`).
43
52
  - Runtime flexibility: the same API across Node.js, Deno, and Bun.
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.
53
+ - Worker choices: use threads for fast pools or processes for stronger
54
+ isolation.
55
+ - All out-of-the-box experiences: strict-by-default permissions, payload-size
56
+ limits, task timeouts, abort-aware tasks, and worker hard timeouts.
46
57
 
47
58
  ## Requirements
48
59
 
49
- - Node.js 22+
50
- - Deno 2+
51
- - Bun 1+
60
+ - Node.js 22+
61
+ - Deno 2+
62
+ - Bun 1+
52
63
 
53
64
  ## Install
54
65
 
@@ -86,8 +97,8 @@ if (isMain) {
86
97
  ```
87
98
 
88
99
  The `isMain` guard when the same module is loaded by workers or process. Export
89
- exposes the tasks or functions at module scope, so knitting maps down the imports,
90
- then use and use the pool only from the main program.
100
+ exposes the tasks or functions at module scope, so knitting maps down the
101
+ imports, then use and use the pool only from the main program.
91
102
 
92
103
  ## The Mental Model
93
104
 
@@ -100,7 +111,7 @@ import { createPool, isMain, task } from "knitting";
100
111
  - `task(...)` describes a callable worker function (types + implementation).
101
112
 
102
113
  - `createPool(options)({ tasks })` starts workers and gives you a typed `call`
103
- object for invoking tasks.
114
+ object for invoking tasks.
104
115
 
105
116
  - `pool.shutdown()` stops workers when you're done.
106
117
 
@@ -182,8 +193,8 @@ export const pixels = task<ResizeInput, number>({
182
193
  ```
183
194
 
184
195
  Supported payloads are listed below. For large binary data, prefer
185
- `ArrayBuffer`, typed arrays, or `ProcessSharedBuffer` instead of serializing
186
- big objects.
196
+ `ArrayBuffer`, typed arrays, or `ProcessSharedBuffer` instead of serializing big
197
+ objects.
187
198
 
188
199
  ### Task timeouts
189
200
 
@@ -312,20 +323,20 @@ const pool = createPool({
312
323
 
313
324
  Common options you might tweak:
314
325
 
315
- | Option | What it does |
316
- | --- | --- |
317
- | `threads` | Number of workers to start. |
318
- | `balancer` | Scheduling strategy: `"roundRobin"`, `"firstIdle"`, `"randomLane"`, `"firstIdleOrRandom"`, or the legacy alias `"robinRound"`. |
319
- | `payload` | Shared payload-buffer settings: `mode`, `payloadInitialBytes`, `payloadMaxByteLength`, and `maxPayloadBytes`. |
320
- | `abortSignalCapacity` | Number of shared abort slots available to abort-aware calls. |
321
- | `worker.resolveAfterFinishingAll` | Let submitted calls finish before shutdown resolves. |
322
- | `worker.bootstrap` | Privileged async hook imported and awaited before task modules load. |
323
- | `worker.hardTimeoutMs` | Force pool shutdown when a task exceeds this many milliseconds. |
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. |
326
- | `permission` | Runtime permission policy for workers. |
327
- | `debug` | Enable extra diagnostics. |
328
- | `source` | Worker source override for advanced runtimes. |
326
+ | Option | What it does |
327
+ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
328
+ | `threads` | Number of workers to start. |
329
+ | `balancer` | Scheduling strategy: `"roundRobin"`, `"firstIdle"`, `"randomLane"`, `"firstIdleOrRandom"`, or the legacy alias `"robinRound"`. |
330
+ | `payload` | Shared payload-buffer settings: `mode`, `payloadInitialBytes`, `payloadMaxByteLength`, and `maxPayloadBytes`. |
331
+ | `abortSignalCapacity` | Number of shared abort slots available to abort-aware calls. |
332
+ | `worker.resolveAfterFinishingAll` | Let submitted calls finish before shutdown resolves. |
333
+ | `worker.bootstrap` | Privileged async hook imported and awaited before task modules load. |
334
+ | `worker.hardTimeoutMs` | Force pool shutdown when a task exceeds this many milliseconds. |
335
+ | `worker.runtime` | Choose `"thread"` or `"process"` workers. |
336
+ | `worker.processSharedMemory` | Process-worker memory discovery: `"inherit"` by default on POSIX, or `"named"` for wrappers/containers that cannot preserve fd 0. |
337
+ | `permission` | Runtime permission policy for workers. |
338
+ | `debug` | Enable extra diagnostics. |
339
+ | `source` | Worker source override for advanced runtimes. |
329
340
 
330
341
  ### Worker bootstrap
331
342
 
@@ -374,10 +385,9 @@ const pool = createPool({
374
385
  })({ add });
375
386
  ```
376
387
 
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.
388
+ `processRuntime` can be `"node"`, `"deno"`, or `"bun"` and defaults to `"deno"`.
389
+ You can also provide a `processCommandPrefix` when workers need to be launched
390
+ through a wrapper such as a package manager, container command, or runtime shim.
381
391
 
382
392
  That prefix is also useful for sandbox and resource-control tools. The one
383
393
  important detail is that process workers receive their shared-memory handle on
@@ -428,8 +438,8 @@ const pool = createPool({
428
438
 
429
439
  On Windows, Knitting automatically uses named shared memory for the
430
440
  process-worker control channel. You do not need to set
431
- `processSharedMemory: "named"` yourself — the runtime detects Windows and
432
- forces it.
441
+ `processSharedMemory: "named"` yourself — the runtime detects Windows and forces
442
+ it.
433
443
 
434
444
  ```ts
435
445
  // Works on Windows without extra options.
@@ -525,7 +535,10 @@ Worker calls can carry the following values across the shared-memory transport:
525
535
  - Plain objects and arrays made from supported values.
526
536
  - `ArrayBuffer`, Node `Buffer`, `DataView`, and supported typed arrays.
527
537
  - `ProcessSharedBuffer`.
528
- - `Envelope` for metadata plus binary payloads.
538
+ - `BufferReference` from `knitting/unsafe` for experimental zero-copy buffers to
539
+ thread workers (same process only; see below).
540
+ - `Envelope` for a JSON header plus a binary body (`ArrayBuffer`,
541
+ `SharedArrayBuffer`, `ProcessSharedBuffer`, or `BufferReference`).
529
542
  - `Error`, `Date`, and global symbols created with `Symbol.for(...)`.
530
543
  - Native `Promise<supported-value>` inputs. The promise is awaited before
531
544
  dispatch.
@@ -544,12 +557,13 @@ shouldn't) cross the boundary:
544
557
 
545
558
  ### Envelope
546
559
 
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.
560
+ `Envelope` pairs a JSON-serializable header with a binary body. Use it when a
561
+ call needs both structured metadata and raw bytes in a single argument — the
562
+ transport carries one special binary value per call, so an envelope is the way
563
+ to attach a header to one.
550
564
 
551
565
  ```ts
552
- import { Envelope, createPool, isMain, task } from "knitting";
566
+ import { createPool, Envelope, isMain, task } from "knitting";
553
567
 
554
568
  export const processImage = task<
555
569
  Envelope<{ format: string }>,
@@ -577,6 +591,58 @@ if (isMain) {
577
591
  }
578
592
  ```
579
593
 
594
+ #### Body types
595
+
596
+ The body is generic — `Envelope<Header, Body>` — and accepts any of the binary
597
+ shapes the transport understands:
598
+
599
+ | Body | Copy? | Workers | Notes |
600
+ | -------------------- | -------------------- | ---------------- | ------------------------------------------------ |
601
+ | `ArrayBuffer` | copied | thread + process | The default body; works everywhere. |
602
+ | `SharedArrayBuffer` | zero-copy, shared | thread only | Shared by reference; process workers reject it. |
603
+ | `ProcessSharedBuffer`| zero-copy, shared | thread + process | Cross-process shared memory. |
604
+ | `BufferReference` | zero-copy, moved | thread only | From `knitting/unsafe`; same constraints as bare `BufferReference`. |
605
+
606
+ The header keeps its fast paths regardless of the body: a small header is
607
+ written inline, and only large headers spill to the dynamic payload region. A
608
+ zero-copy body keeps its own semantics — a `SharedArrayBuffer` stays shared by
609
+ reference, and a `BufferReference` body is still moved (its source is detached)
610
+ and joins the same borrow/copy/release flow it follows on its own.
611
+
612
+ ```ts
613
+ import { createPool, Envelope, isMain, task } from "knitting";
614
+ import { BufferReference } from "knitting/unsafe";
615
+
616
+ export const invert = task<
617
+ Envelope<{ op: string }, BufferReference>,
618
+ Envelope<{ op: string }, BufferReference>
619
+ >({
620
+ f: (envelope) => {
621
+ const pixels = envelope.payload.toUint8Array();
622
+ const out = new Uint8Array(pixels.length);
623
+ for (let i = 0; i < pixels.length; i++) out[i] = 255 - pixels[i];
624
+ return new Envelope({ op: "inverted" }, new BufferReference(out));
625
+ },
626
+ });
627
+
628
+ if (isMain) {
629
+ using pool = createPool({ threads: 1 })({ invert });
630
+ const pixels = new Uint8Array([0, 64, 128, 192, 255]);
631
+
632
+ using result = await pool.call.invert(
633
+ new Envelope({ op: "invert" }, new BufferReference(pixels)),
634
+ );
635
+ console.log(result.header, [...result.payload.toUint8Array()]);
636
+ }
637
+ ```
638
+
639
+ `Envelope` is disposable: disposing it (via `using` or `Symbol.dispose`)
640
+ disposes a disposable body such as a `BufferReference`, and is a harmless no-op
641
+ for `ArrayBuffer` / `SharedArrayBuffer` bodies. See
642
+ [Experimental zero-copy buffers for thread workers](#experimental-zero-copy-buffers-for-thread-workers)
643
+ for the full `BufferReference` constraints, which apply unchanged to a
644
+ `BufferReference` body.
645
+
580
646
  If a payload is large, set `payload.maxPayloadBytes` deliberately and prefer
581
647
  binary/shared-memory shapes over deeply nested objects.
582
648
 
@@ -743,8 +809,8 @@ There are three moving parts:
743
809
 
744
810
  - `processSharedMemory: "named"` lets the Docker worker find Knitting's control
745
811
  channel.
746
- - `ProcessSharedBuffer.create({ mode: "create", name, size })` makes the
747
- payload buffer reopenable by name.
812
+ - `ProcessSharedBuffer.create({ mode: "create", name, size })` makes the payload
813
+ buffer reopenable by name.
748
814
  - `--ipc=host` lets the container see the same POSIX shared-memory namespace.
749
815
 
750
816
  This is same-host communication. It is fast because both sides map the same
@@ -752,10 +818,103 @@ bytes, but it is not a network transport and it deliberately shares IPC with the
752
818
  container. Use names like capabilities: generate them, keep them private, and
753
819
  unlink them when the shared memory is no longer needed.
754
820
 
821
+ ### Experimental zero-copy buffers for thread workers
822
+
823
+ `BufferReference` lives in `knitting/unsafe`. It is experimental and may be
824
+ changed or removed if its safety tradeoffs are not acceptable. It **moves** a
825
+ buffer's ownership to a **thread** worker: constructing one detaches the source,
826
+ so the bytes travel to the worker without being serialized through the
827
+ transport. Send a result back the same way — return a `BufferReference` from the
828
+ worker. It is the same-process counterpart to `ProcessSharedBuffer`: reach for
829
+ it when you hold a large `ArrayBuffer` or typed array and the copy cost to a
830
+ thread worker actually matters.
831
+
832
+ ```ts
833
+ import { createPool, isMain, task } from "knitting";
834
+ import { BufferReference } from "knitting/unsafe";
835
+
836
+ export const invert = task<BufferReference, BufferReference>({
837
+ f: (ref) => {
838
+ const pixels = ref.toUint8Array(); // the moved bytes, no copy
839
+ const out = new Uint8Array(pixels.length);
840
+ for (let i = 0; i < pixels.length; i++) out[i] = 255 - pixels[i];
841
+ return new BufferReference(out); // move the result back to the host
842
+ },
843
+ });
844
+
845
+ if (isMain) {
846
+ const pixels = new Uint8Array([0, 64, 128, 192, 255]);
847
+ using pool = createPool({ threads: 1 })({ invert });
848
+
849
+ // `pixels` is detached by the move; the result comes back as a BufferReference.
850
+ const result = await pool.call.invert(new BufferReference(pixels));
851
+ console.log([...result.toUint8Array()]); // [255, 191, 127, 63, 0]
852
+ }
853
+ ```
854
+
855
+ Read these constraints before reaching for it:
856
+
857
+ - **Thread workers only.** The handle is a process-local pointer. Process
858
+ workers do not share it, so a `BufferReference` sent to a process worker
859
+ throws. For cross-process sharing use `ProcessSharedBuffer`.
860
+ - **ArrayBuffer only.** `SharedArrayBuffer` is already shareable and cannot be
861
+ detached, so `BufferReference` rejects SAB sources and SAB-backed typed-array
862
+ views.
863
+ - **Move semantics.** Constructing a `BufferReference` detaches its source — the
864
+ original buffer is empty afterward, and reads/writes through it are gone. The
865
+ bytes now belong to the reference; to get a result back, the worker returns
866
+ its own `BufferReference`. Each handle is one-shot. Forward inputs the worker
867
+ materializes with `.toArrayBuffer()`/`.toUint8Array()` are borrowed for the
868
+ duration of the call and detached once it settles; do not keep using them from
869
+ fire-and-forget work after the task returns.
870
+ - **Forward is zero-copy everywhere; the return is zero-copy on Node.** Sending
871
+ a buffer to the worker never copies. On Node the returned buffer is also
872
+ handed back with no copy (the engine co-owns the backing store across
873
+ threads); on Deno and Bun the host takes a single copy of the returned bytes,
874
+ because their FFI cannot co-own a worker-thread backing store. Both are far
875
+ cheaper than serializing a large buffer through the transport.
876
+ - **Borrowed Deno/Bun returns are opt-in.** The default is
877
+ `unsafe: { BufferReferenceReturn: "copy" }` — the safe single copy described
878
+ above. Set it to `"borrow"` on `createPool` to skip that copy on Deno/Bun by
879
+ borrowing the worker's backing store until the returned `BufferReference` is
880
+ released. Call `ref.release()` or use `using`, and do it before shutting down
881
+ the producing worker. **After `release()` the borrowed bytes are gone —
882
+ reading the reference, or any view you took from it, is a use-after-free.**
883
+ If the bytes escape into HTTP responses, streams, timers, callbacks, or caches,
884
+ copy them before the borrowed reference is released.
885
+ - **Unsafe escape hatch.** This is not a security boundary. Forged metadata or
886
+ unsynchronized host/worker mutation can still be unsafe.
887
+ - **Node uses a native addon.** Bun and Deno go through their FFI; Node uses the
888
+ `knitting_buffer_pointer` prebuild shipped with the package (or
889
+ `bun run build:native` when developing on a new ABI). Without it, constructing
890
+ a `BufferReference` on Node throws.
891
+
892
+ Borrowed returns, end to end (Node is always zero-copy; this opts Deno/Bun in):
893
+
894
+ ```ts
895
+ using pool = createPool({
896
+ threads: 1,
897
+ unsafe: {
898
+ BufferReferenceReturn: "borrow",
899
+ },
900
+ })({ invert });
901
+
902
+ {
903
+ using result = await pool.call.invert(new BufferReference(pixels));
904
+ const out = result.toUint8Array(); // borrowed — valid only while `result` lives
905
+ console.log([...out]);
906
+ } // `using` releases the borrow here; do not read `out` after this point
907
+ ```
908
+
909
+ When in doubt, a plain `ArrayBuffer` or typed-array payload — which knitting
910
+ copies through the shared transport — is simpler and works for both thread and
911
+ process workers. Reach for `BufferReference` only when the copy cost of a large
912
+ buffer to a thread worker actually matters: below roughly 256 KiB the per-call
913
+ pointer setup tends to cost more than just copying, so the plain transport wins.
914
+
755
915
  ### Current support
756
916
 
757
- Knitting supports Node.js 22+, Deno 2+, and Bun 1+ on Linux, macOS, and
758
- Windows.
917
+ Knitting supports Node.js 22+, Deno 2+, and Bun 1+ on Linux, macOS, and Windows.
759
918
 
760
919
  Thread workers work without native pieces. Process workers and
761
920
  `ProcessSharedBuffer` use the platform's shared-memory APIs. Release packages
@@ -766,8 +925,8 @@ FFI path; if you are developing locally on a new Node ABI or architecture, run:
766
925
  bun run build:native
767
926
  ```
768
927
 
769
- For Deno projects with permissions enabled, allow FFI when using process
770
- workers or `ProcessSharedBuffer`.
928
+ For Deno projects with permissions enabled, allow FFI when using process workers
929
+ or `ProcessSharedBuffer`.
771
930
 
772
931
  ## Runtime Safety
773
932
 
package/knitting.d.ts CHANGED
@@ -2,3 +2,4 @@ import { workerMainLoop } from "./src/worker/loop.js";
2
2
  import { createPool, importTask, isMain, task } from "./src/api.js";
3
3
  import { Envelope } from "./src/common/envelope.js";
4
4
  export { createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, task as task, workerMainLoop as workerMainLoop, };
5
+ export type { EnvelopeBody as EnvelopeBody, EnvelopeHeader as EnvelopeHeader, } from "./src/common/envelope.js";
package/map.md CHANGED
@@ -20,6 +20,9 @@ The core flow is:
20
20
  `importTask`, `isMain`, `Envelope`, and `workerMainLoop`.
21
21
  - `process-shared-buffer.ts`: Secondary public export for process-shared-buffer
22
22
  helpers.
23
+ - `unsafe.ts`: Public export for experimental lower-level capabilities such as
24
+ `BufferReference`.
25
+ - `utils.ts`: Public export for request/response buffer conversion helpers.
23
26
  - `README.md`: User-facing documentation, examples, configuration, and command
24
27
  reference.
25
28
  - `package.json`: Package metadata, export map, shipped files, scripts, runtime
@@ -54,8 +57,15 @@ The core flow is:
54
57
  - `.github/workflows/publish.yml`: Manual native-prebuild workflow. Verifies
55
58
  Node and Windows FFI prebuilds, checks the JSR package contents, and commits
56
59
  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.
60
+
61
+ ## Project Guidance And Docs
62
+
63
+ - `docs/AGENTS.md`: Agent/contributor orientation, invariants, and workflow
64
+ notes.
65
+ - `docs/CLAUDE.md`: Pointer to the shared agent guidance.
66
+ - `docs/buffer-reference-ownership-move.md`: Design record for
67
+ `BufferReference` ownership transfer.
68
+ - `docs/knitting.pdf`: Longer-form project design/theory document.
59
69
 
60
70
  ## Public API Layer
61
71
 
@@ -88,6 +98,8 @@ The core flow is:
88
98
  - `src/common/with-resolvers.ts`: `Promise.withResolvers` compatibility helper.
89
99
  - `src/common/worker-runtime.ts`: Runtime-neutral worker/thread/process-worker
90
100
  detection, parent-port access, and message-channel creation.
101
+ - `src/utils/http.ts`: Buffer conversion helpers for HTTP/request-response
102
+ boundaries, including raw bytes, strings, JSON, and numeric arrays.
91
103
 
92
104
  ## Runtime Host Side
93
105
 
@@ -286,6 +298,7 @@ The core flow is:
286
298
  - `test/task-abort-context-api.test.ts`: Worker abort toolkit/context behavior.
287
299
  - `test/tx-queue.test.ts`: Host transmit queue behavior and late-result safety.
288
300
  - `test/type-inference.test.ts`: Public type inference guarantees.
301
+ - `test/utils.test.ts`: Public utility conversion helpers.
289
302
  - `test/worker-bootstrap.test.ts`: Worker bootstrap hook behavior, shared-buffer
290
303
  metadata revival, startup failure propagation, and inliner incompatibility.
291
304
  - `test/fixtures/*.ts`: Task modules used by tests.
@@ -305,4 +318,3 @@ The core flow is:
305
318
  - `log/`, `logs`, and `*.log`: Local runtime/log output.
306
319
  - `node_modules/`: Installed dependencies. Not part of the source map.
307
320
 
308
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knitting",
3
- "version": "0.1.51",
3
+ "version": "0.1.52",
4
4
  "description": "Shared-memory IPC runtime for Node.js, Deno, and Bun.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -23,6 +23,14 @@
23
23
  "./process-shared-buffer": {
24
24
  "types": "./process-shared-buffer.d.ts",
25
25
  "default": "./process-shared-buffer.js"
26
+ },
27
+ "./unsafe": {
28
+ "types": "./unsafe.d.ts",
29
+ "default": "./unsafe.js"
30
+ },
31
+ "./utils": {
32
+ "types": "./utils.d.ts",
33
+ "default": "./utils.js"
26
34
  }
27
35
  },
28
36
  "files": [
@@ -38,7 +46,11 @@
38
46
  "knitting.d.ts",
39
47
  "knitting.js",
40
48
  "process-shared-buffer.d.ts",
41
- "process-shared-buffer.js"
49
+ "process-shared-buffer.js",
50
+ "unsafe.d.ts",
51
+ "unsafe.js",
52
+ "utils.d.ts",
53
+ "utils.js"
42
54
  ],
43
55
  "scripts": {
44
56
  "build": "bun run build.ts",
@@ -232,6 +232,11 @@ const addons = [
232
232
  source: "src/knitting_shm.cc",
233
233
  output: "build/Release/knitting_shm.node",
234
234
  },
235
+ {
236
+ name: "knitting_buffer_pointer",
237
+ source: "src/knitting_buffer_pointer.cc",
238
+ output: "build/Release/knitting_buffer_pointer.node",
239
+ },
235
240
  ];
236
241
  const ffiLibraries = isWindows
237
242
  ? [
package/src/api.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { endpointSymbol } from "./common/task-symbol.js";
2
- import type { Args, AbortSignalConfig, AbortSignalOption, AbortSignalToolkit, ComposedWithKey, CreatePool, FixPoint, MaybePromise, Pool, TaskInput, ReturnFixed, ImportTaskOptions, TaskTimeout, tasks } from "./types.js";
2
+ import type { AbortSignalConfig, AbortSignalOption, AbortSignalToolkit, Args, ComposedWithKey, CreatePool, FixPoint, ImportTaskOptions, MaybePromise, Pool, ReturnFixed, TaskInput, tasks, TaskTimeout } from "./types.js";
3
3
  type ToListAndIds = {
4
4
  list: string[];
5
5
  ids: number[];
@@ -11,9 +11,7 @@ type CreatePoolFactory = (options: CreatePool) => <T extends tasks>(tasks: T) =>
11
11
  type InferredTaskFunction = (...args: any[]) => MaybePromise<Args>;
12
12
  type InferredTaskInput<F extends InferredTaskFunction, AS extends AbortSignalOption> = Parameters<F> extends [] ? void : AS extends undefined ? Parameters<F> extends [infer A] ? A extends TaskInput ? A : never : never : Parameters<F> extends [infer A] ? A extends TaskInput ? A : never : Parameters<F> extends [infer A, AbortSignalToolkit<AS>] ? A extends TaskInput ? A : never : never;
13
13
  type InferredTaskOutput<F extends InferredTaskFunction> = Awaited<ReturnType<F>> extends infer R ? R extends Blob ? never : R extends Args ? R : never : never;
14
- type InferredTaskShape<F extends InferredTaskFunction, AS extends AbortSignalOption> = [
15
- InferredTaskInput<F, AS>
16
- ] extends [never] ? never : [InferredTaskOutput<F>] extends [never] ? never : {
14
+ type InferredTaskShape<F extends InferredTaskFunction, AS extends AbortSignalOption> = [InferredTaskInput<F, AS>] extends [never] ? never : [InferredTaskOutput<F>] extends [never] ? never : {
17
15
  readonly f: F;
18
16
  readonly timeout?: TaskTimeout;
19
17
  } & (AS extends undefined ? {
@@ -24,12 +22,7 @@ type InferredTaskShape<F extends InferredTaskFunction, AS extends AbortSignalOpt
24
22
  export declare const isMain: boolean;
25
23
  export { endpointSymbol as endpointSymbol };
26
24
  /**
27
- * With this information we can recreate the logical order of
28
- * relevant exported functions from a file, also it helps to
29
- * track a task before naming, ` export ` elements have to be declared
30
- * at top level and without branching, we take advantage of this to
31
- * correctly map them.
32
- *
25
+ * Reconstructs stable task order from top-level exports before names are bound.
33
26
  */
34
27
  export declare const toListAndIds: ToListAndIdsFn;
35
28
  export declare const createPool: CreatePoolFactory;
package/src/api.js CHANGED
@@ -11,7 +11,7 @@ import { genTaskID } from "./common/task-source.js";
11
11
  import { toModuleUrl } from "./common/module-url.js";
12
12
  import { endpointSymbol } from "./common/task-symbol.js";
13
13
  import { spawnWorkerContext } from "./runtime/pool.js";
14
- import { RUNTIME_IS_MAIN_THREAD, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
14
+ import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
15
15
  import { resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
16
16
  import { getNodeProcess } from "./common/node-compat.js";
17
17
  import { managerMethod } from "./runtime/balancer.js";
@@ -22,12 +22,7 @@ const DEFAULT_IMPORT_EXPORT_NAME = "default";
22
22
  export const isMain = RUNTIME_IS_MAIN_THREAD;
23
23
  export { endpointSymbol as endpointSymbol };
24
24
  /**
25
- * With this information we can recreate the logical order of
26
- * relevant exported functions from a file, also it helps to
27
- * track a task before naming, ` export ` elements have to be declared
28
- * at top level and without branching, we take advantage of this to
29
- * correctly map them.
30
- *
25
+ * Reconstructs stable task order from top-level exports before names are bound.
31
26
  */
32
27
  export const toListAndIds = (args) => {
33
28
  const result = args
@@ -98,7 +93,8 @@ const toPoolTaskEntries = (input, callerHref) => Object.entries(input).map(([nam
98
93
  }
99
94
  throw new TypeError(`createPool task "${name}" must be a task definition or exported function`);
100
95
  });
101
- export const createPool = ({ threads, debug, inliner, balancer, payload, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, source, worker, workerExecArgv, permission, dispatcher, host, }) => (tasks) => {
96
+ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe, payloadInitialBytes, payloadMaxBytes, bufferMode, maxPayloadBytes, abortSignalCapacity, source, worker, workerExecArgv, permission, dispatcher, host, }) => (tasks) => {
97
+ const bufferReferenceReturn = unsafe?.BufferReferenceReturn;
102
98
  /**
103
99
  * This functions is only available in the main thread.
104
100
  * Also triggers when debug extra is enabled.
@@ -209,6 +205,15 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
209
205
  const hardTimeoutMs = Number.isFinite(resolvedWorker?.hardTimeoutMs)
210
206
  ? Math.max(1, Math.floor(resolvedWorker?.hardTimeoutMs))
211
207
  : undefined;
208
+ if (RUNTIME_POOL_DEPTH >= 1) {
209
+ throw new Error(`createPool() tried to spawn workers from inside a worker process ` +
210
+ `(pool depth ${RUNTIME_POOL_DEPTH}). This usually means a pool is ` +
211
+ `created at module scope in a module your workers import, so every ` +
212
+ `worker spawns its own pool recursively. Is your createPool protected ` +
213
+ `by isMain? Guard pool creation behind \`if (isMain) { ... }\` ` +
214
+ `(import { isMain } from "knitting") so only the main program starts ` +
215
+ `the pool.`);
216
+ }
212
217
  let workers = Array.from({
213
218
  length: threads ?? 1,
214
219
  }).map((_, thread) => spawnWorkerContext({
@@ -224,6 +229,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
224
229
  workerExecArgv: execArgv,
225
230
  host: hostDispatcher,
226
231
  payload,
232
+ bufferReferenceReturn,
227
233
  payloadInitialBytes,
228
234
  payloadMaxBytes,
229
235
  bufferMode,
@@ -377,19 +383,17 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, payload
377
383
  if (imported && usingInliner) {
378
384
  return buildImportedInvoker(handlers);
379
385
  }
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
- });
386
+ return useDirectHandler ? handlers[0] : managerMethod({
387
+ contexts: workers,
388
+ balancer,
389
+ handlers,
390
+ inlinerGate: usingInliner
391
+ ? {
392
+ index: inlinerIndex,
393
+ threshold: inlinerDispatchThreshold,
394
+ }
395
+ : undefined,
396
+ });
393
397
  };
394
398
  let callEntries;
395
399
  try {