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.
- package/README.md +213 -54
- package/knitting.d.ts +1 -0
- package/map.md +15 -3
- package/package.json +14 -2
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +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_buffer_pointer.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 +5 -0
- package/src/api.d.ts +3 -10
- package/src/api.js +25 -21
- package/src/common/envelope.d.ts +9 -3
- package/src/common/envelope.js +14 -0
- package/src/common/worker-runtime.d.ts +2 -0
- package/src/common/worker-runtime.js +9 -0
- package/src/connections/buffer-reference-native.d.ts +56 -0
- package/src/connections/buffer-reference-native.js +217 -0
- package/src/connections/buffer-reference.d.ts +76 -0
- package/src/connections/buffer-reference.js +459 -0
- package/src/connections/index.d.ts +1 -0
- package/src/connections/index.js +1 -0
- package/src/connections/node-addons.d.ts +1 -1
- package/src/connections/node-buffer-pointer.d.ts +20 -0
- package/src/connections/node-buffer-pointer.js +16 -0
- package/src/connections/shared-array-buffer-payload.d.ts +36 -0
- package/src/connections/shared-array-buffer-payload.js +235 -0
- package/src/knitting_buffer_pointer.cc +425 -0
- package/src/memory/lock.d.ts +12 -1
- package/src/memory/lock.js +47 -4
- package/src/memory/payloadCodec.js +220 -37
- package/src/runtime/pool.d.ts +2 -1
- package/src/runtime/pool.js +8 -1
- package/src/runtime/process-worker.js +3 -1
- package/src/runtime/tx-queue.d.ts +3 -2
- package/src/runtime/tx-queue.js +18 -13
- package/src/types.d.ts +26 -18
- package/src/utils/http.d.ts +21 -0
- package/src/utils/http.js +93 -0
- package/src/worker/loop.js +23 -3
- package/src/worker/rx-queue.d.ts +4 -1
- package/src/worker/rx-queue.js +53 -4
- package/unsafe.d.ts +1 -0
- package/unsafe.js +1 -0
- package/utils.d.ts +1 -0
- package/utils.js +1 -0
package/README.md
CHANGED
|
@@ -11,44 +11,55 @@
|
|
|
11
11
|
[](https://deno.com/)
|
|
12
12
|
[](https://bun.sh/)
|
|
13
13
|
|
|
14
|
-
Knitting is a worker pool over a shared-memory IPC runtime for Node.js, Deno,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
42
|
-
|
|
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
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
316
|
-
|
|
|
317
|
-
| `threads`
|
|
318
|
-
| `balancer`
|
|
319
|
-
| `payload`
|
|
320
|
-
| `abortSignalCapacity`
|
|
321
|
-
| `worker.resolveAfterFinishingAll` | Let submitted calls finish before shutdown resolves.
|
|
322
|
-
| `worker.bootstrap`
|
|
323
|
-
| `worker.hardTimeoutMs`
|
|
324
|
-
| `worker.runtime`
|
|
325
|
-
| `worker.processSharedMemory`
|
|
326
|
-
| `permission`
|
|
327
|
-
| `debug`
|
|
328
|
-
| `source`
|
|
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
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
|
548
|
-
|
|
549
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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.
|
|
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",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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 {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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 {
|