knitting 0.1.50 → 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 +268 -75
- package/knitting.d.ts +1 -0
- package/map.md +56 -6
- package/package.json +16 -4
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_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 +5 -11
- package/src/api.js +103 -22
- package/src/common/envelope.d.ts +9 -3
- package/src/common/envelope.js +14 -0
- package/src/common/task-source.js +4 -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/process-shared-buffer.js +2 -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/knitting_shared_memory.cc +9 -2
- package/src/memory/lock.d.ts +12 -1
- package/src/memory/lock.js +55 -172
- package/src/memory/payloadCodec.js +241 -65
- package/src/memory/shared-buffer-io.d.ts +2 -0
- package/src/memory/shared-buffer-io.js +23 -0
- package/src/runtime/inline-executor.d.ts +2 -2
- package/src/runtime/inline-executor.js +15 -3
- package/src/runtime/pool.d.ts +3 -1
- package/src/runtime/pool.js +9 -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 +39 -18
- package/src/utils/http.d.ts +21 -0
- package/src/utils/http.js +93 -0
- package/src/worker/loop.js +26 -4
- package/src/worker/process-worker-bootstrap.js +1 -0
- package/src/worker/rx-queue.d.ts +4 -1
- package/src/worker/rx-queue.js +53 -4
- package/src/worker/safety/startup.d.ts +2 -1
- package/src/worker/safety/startup.js +5 -2
- package/src/worker/task-loader.d.ts +2 -1
- package/src/worker/task-loader.js +14 -2
- package/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,43 +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
|
-
You
|
|
27
|
+
You export a function or task, spin up a pool, and call it like a normal async
|
|
28
|
+
function:
|
|
21
29
|
|
|
22
30
|
```ts
|
|
23
|
-
|
|
24
31
|
const result = await pool.call.resizeImage(file);
|
|
25
|
-
|
|
26
32
|
```
|
|
27
33
|
|
|
28
34
|
So you only have to take care of 4 things:
|
|
29
|
-
- Create a task
|
|
30
|
-
- Create a pool
|
|
31
|
-
- Call your task
|
|
32
|
-
- Terminate the pool
|
|
33
35
|
|
|
36
|
+
- Export a function or task
|
|
37
|
+
- Create a pool
|
|
38
|
+
- Call it
|
|
39
|
+
- Let `using` or `shutdown()` close the pool
|
|
34
40
|
|
|
35
|
-
Under the hood, we take care of scheduling and orchestration across worker
|
|
36
|
-
|
|
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.
|
|
37
45
|
|
|
38
46
|
## Why use it?
|
|
39
47
|
|
|
40
|
-
- Easy to use: Have a multithreaded environment or process with few lines of
|
|
41
|
-
|
|
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`).
|
|
42
52
|
- Runtime flexibility: the same API across Node.js, Deno, and Bun.
|
|
43
|
-
- Worker choices: use threads for fast pools or processes for stronger
|
|
44
|
-
|
|
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.
|
|
45
57
|
|
|
46
58
|
## Requirements
|
|
47
59
|
|
|
48
|
-
- Node.js 22+
|
|
49
|
-
- Deno 2+
|
|
50
|
-
- Bun 1+
|
|
60
|
+
- Node.js 22+
|
|
61
|
+
- Deno 2+
|
|
62
|
+
- Bun 1+
|
|
51
63
|
|
|
52
64
|
## Install
|
|
53
65
|
|
|
@@ -60,41 +72,33 @@ npm install knitting
|
|
|
60
72
|
For Deno projects:
|
|
61
73
|
|
|
62
74
|
```bash
|
|
63
|
-
deno add --npm
|
|
75
|
+
deno add --npm knitting
|
|
64
76
|
```
|
|
65
77
|
|
|
66
78
|
## Quick Start
|
|
67
79
|
|
|
68
80
|
```ts
|
|
69
|
-
import { createPool, isMain
|
|
81
|
+
import { createPool, isMain } from "knitting";
|
|
70
82
|
|
|
71
|
-
export const square =
|
|
72
|
-
f: (value) => value * value,
|
|
73
|
-
});
|
|
83
|
+
export const square = (value: number) => value * value;
|
|
74
84
|
|
|
75
|
-
export const greet =
|
|
76
|
-
f: (name) => `hello ${name}`,
|
|
77
|
-
});
|
|
85
|
+
export const greet = (name: string) => `hello ${name}`;
|
|
78
86
|
|
|
79
87
|
if (isMain) {
|
|
80
|
-
|
|
88
|
+
using pool = createPool({ threads: 2 })({ square, greet });
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
]);
|
|
90
|
+
const [four, message] = await Promise.all([
|
|
91
|
+
pool.call.square(2),
|
|
92
|
+
pool.call.greet("knitting"),
|
|
93
|
+
]);
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
} finally {
|
|
90
|
-
await pool.shutdown();
|
|
91
|
-
}
|
|
95
|
+
console.log({ four, message });
|
|
92
96
|
}
|
|
93
97
|
```
|
|
94
98
|
|
|
95
99
|
The `isMain` guard when the same module is loaded by workers or process. Export
|
|
96
|
-
exposes the tasks or functions at module scope, so knitting maps down the
|
|
97
|
-
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.
|
|
98
102
|
|
|
99
103
|
## The Mental Model
|
|
100
104
|
|
|
@@ -107,7 +111,7 @@ import { createPool, isMain, task } from "knitting";
|
|
|
107
111
|
- `task(...)` describes a callable worker function (types + implementation).
|
|
108
112
|
|
|
109
113
|
- `createPool(options)({ tasks })` starts workers and gives you a typed `call`
|
|
110
|
-
object for invoking tasks.
|
|
114
|
+
object for invoking tasks.
|
|
111
115
|
|
|
112
116
|
- `pool.shutdown()` stops workers when you're done.
|
|
113
117
|
|
|
@@ -128,6 +132,39 @@ if (isMain) {
|
|
|
128
132
|
}
|
|
129
133
|
```
|
|
130
134
|
|
|
135
|
+
On TypeScript or runtimes that support explicit resource management, the pool is
|
|
136
|
+
also a synchronous disposable:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
if (isMain) {
|
|
140
|
+
using pool = createPool({ threads: 4 })({ add });
|
|
141
|
+
|
|
142
|
+
const value = await pool.call.add([1, 2]);
|
|
143
|
+
console.log(value);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`using` starts pool shutdown when the scope exits and does not wait for it.
|
|
148
|
+
TypeScript 5.2+ can compile this pattern for runtimes that do not parse `using`
|
|
149
|
+
syntax directly. Use `await pool.shutdown()` when you need to wait for shutdown
|
|
150
|
+
or pass a shutdown delay.
|
|
151
|
+
|
|
152
|
+
For simple tasks that do not need timeout or abort metadata, exported functions
|
|
153
|
+
can be used directly:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
export const add = ([a, b]: [number, number]) => a + b;
|
|
157
|
+
|
|
158
|
+
if (isMain) {
|
|
159
|
+
using pool = createPool({ threads: 1 })({ add });
|
|
160
|
+
console.log(await pool.call.add([1, 2]));
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Bare functions must be exported from the module that creates the pool. Inline
|
|
165
|
+
anonymous functions cannot be imported by workers; use `task(...)` when you need
|
|
166
|
+
metadata or a more explicit task definition.
|
|
167
|
+
|
|
131
168
|
Once you have a pool, calls are just promises, so batching looks like normal
|
|
132
169
|
JavaScript:
|
|
133
170
|
|
|
@@ -156,8 +193,8 @@ export const pixels = task<ResizeInput, number>({
|
|
|
156
193
|
```
|
|
157
194
|
|
|
158
195
|
Supported payloads are listed below. For large binary data, prefer
|
|
159
|
-
`ArrayBuffer`, typed arrays, or `ProcessSharedBuffer` instead of serializing
|
|
160
|
-
|
|
196
|
+
`ArrayBuffer`, typed arrays, or `ProcessSharedBuffer` instead of serializing big
|
|
197
|
+
objects.
|
|
161
198
|
|
|
162
199
|
### Task timeouts
|
|
163
200
|
|
|
@@ -241,6 +278,14 @@ When workers import files, keep the pool's permission settings in mind. The
|
|
|
241
278
|
default strict mode allows task imports, but custom permission policies can
|
|
242
279
|
limit reads, writes, environment access, networking, and process execution.
|
|
243
280
|
|
|
281
|
+
Imported tasks are never run on the host inline lane, even when the pool enables
|
|
282
|
+
the `inliner`. Inlining would evaluate the imported module on the host and
|
|
283
|
+
bypass the worker permissions that `importTask` exists to enforce, so Knitting
|
|
284
|
+
always routes imported tasks to a worker. You can freely mix `importTask` and
|
|
285
|
+
the `inliner` in one pool — regular tasks get inlined while imported ones stay
|
|
286
|
+
on worker lanes — but the pool needs at least one worker thread for them to run,
|
|
287
|
+
otherwise `createPool` throws.
|
|
288
|
+
|
|
244
289
|
### Single-task shorthand
|
|
245
290
|
|
|
246
291
|
For quick scripts, a task can create its own pool:
|
|
@@ -278,20 +323,20 @@ const pool = createPool({
|
|
|
278
323
|
|
|
279
324
|
Common options you might tweak:
|
|
280
325
|
|
|
281
|
-
| Option
|
|
282
|
-
|
|
|
283
|
-
| `threads`
|
|
284
|
-
| `balancer`
|
|
285
|
-
| `payload`
|
|
286
|
-
| `abortSignalCapacity`
|
|
287
|
-
| `worker.resolveAfterFinishingAll` | Let submitted calls finish before shutdown resolves.
|
|
288
|
-
| `worker.bootstrap`
|
|
289
|
-
| `worker.hardTimeoutMs`
|
|
290
|
-
| `worker.runtime`
|
|
291
|
-
| `worker.processSharedMemory`
|
|
292
|
-
| `permission`
|
|
293
|
-
| `debug`
|
|
294
|
-
| `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. |
|
|
295
340
|
|
|
296
341
|
### Worker bootstrap
|
|
297
342
|
|
|
@@ -340,10 +385,9 @@ const pool = createPool({
|
|
|
340
385
|
})({ add });
|
|
341
386
|
```
|
|
342
387
|
|
|
343
|
-
`processRuntime` can be `"node"`, `"deno"`, or `"bun"` and defaults to
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
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.
|
|
347
391
|
|
|
348
392
|
That prefix is also useful for sandbox and resource-control tools. The one
|
|
349
393
|
important detail is that process workers receive their shared-memory handle on
|
|
@@ -394,8 +438,8 @@ const pool = createPool({
|
|
|
394
438
|
|
|
395
439
|
On Windows, Knitting automatically uses named shared memory for the
|
|
396
440
|
process-worker control channel. You do not need to set
|
|
397
|
-
`processSharedMemory: "named"` yourself — the runtime detects Windows and
|
|
398
|
-
|
|
441
|
+
`processSharedMemory: "named"` yourself — the runtime detects Windows and forces
|
|
442
|
+
it.
|
|
399
443
|
|
|
400
444
|
```ts
|
|
401
445
|
// Works on Windows without extra options.
|
|
@@ -422,7 +466,6 @@ the inherited fd:
|
|
|
422
466
|
|
|
423
467
|
```ts
|
|
424
468
|
const pool = createPool({
|
|
425
|
-
threads: 2,
|
|
426
469
|
worker: {
|
|
427
470
|
runtime: "process",
|
|
428
471
|
processRuntime: "bun",
|
|
@@ -492,7 +535,10 @@ Worker calls can carry the following values across the shared-memory transport:
|
|
|
492
535
|
- Plain objects and arrays made from supported values.
|
|
493
536
|
- `ArrayBuffer`, Node `Buffer`, `DataView`, and supported typed arrays.
|
|
494
537
|
- `ProcessSharedBuffer`.
|
|
495
|
-
- `
|
|
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`).
|
|
496
542
|
- `Error`, `Date`, and global symbols created with `Symbol.for(...)`.
|
|
497
543
|
- Native `Promise<supported-value>` inputs. The promise is awaited before
|
|
498
544
|
dispatch.
|
|
@@ -502,7 +548,8 @@ If it isn't on that list, assume it isn't portable. Some things don't (or
|
|
|
502
548
|
shouldn't) cross the boundary:
|
|
503
549
|
|
|
504
550
|
- DOM objects and platform handles.
|
|
505
|
-
- Functions, unless they are part of a `task` or
|
|
551
|
+
- Functions, unless they are exported pool tasks or part of a `task` or
|
|
552
|
+
`importTask` definition.
|
|
506
553
|
- Cyclic object graphs.
|
|
507
554
|
- `Map`, `Set`, `WeakMap`, and non-global symbols.
|
|
508
555
|
- Objects with behavior that depends on prototypes, getters, setters, or hidden
|
|
@@ -510,12 +557,13 @@ shouldn't) cross the boundary:
|
|
|
510
557
|
|
|
511
558
|
### Envelope
|
|
512
559
|
|
|
513
|
-
`Envelope` pairs a JSON-serializable header with a binary
|
|
514
|
-
|
|
515
|
-
|
|
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.
|
|
516
564
|
|
|
517
565
|
```ts
|
|
518
|
-
import {
|
|
566
|
+
import { createPool, Envelope, isMain, task } from "knitting";
|
|
519
567
|
|
|
520
568
|
export const processImage = task<
|
|
521
569
|
Envelope<{ format: string }>,
|
|
@@ -543,6 +591,58 @@ if (isMain) {
|
|
|
543
591
|
}
|
|
544
592
|
```
|
|
545
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
|
+
|
|
546
646
|
If a payload is large, set `payload.maxPayloadBytes` deliberately and prefer
|
|
547
647
|
binary/shared-memory shapes over deeply nested objects.
|
|
548
648
|
|
|
@@ -709,8 +809,8 @@ There are three moving parts:
|
|
|
709
809
|
|
|
710
810
|
- `processSharedMemory: "named"` lets the Docker worker find Knitting's control
|
|
711
811
|
channel.
|
|
712
|
-
- `ProcessSharedBuffer.create({ mode: "create", name, size })` makes the
|
|
713
|
-
|
|
812
|
+
- `ProcessSharedBuffer.create({ mode: "create", name, size })` makes the payload
|
|
813
|
+
buffer reopenable by name.
|
|
714
814
|
- `--ipc=host` lets the container see the same POSIX shared-memory namespace.
|
|
715
815
|
|
|
716
816
|
This is same-host communication. It is fast because both sides map the same
|
|
@@ -718,10 +818,103 @@ bytes, but it is not a network transport and it deliberately shares IPC with the
|
|
|
718
818
|
container. Use names like capabilities: generate them, keep them private, and
|
|
719
819
|
unlink them when the shared memory is no longer needed.
|
|
720
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
|
+
|
|
721
915
|
### Current support
|
|
722
916
|
|
|
723
|
-
Knitting supports Node.js 22+, Deno 2+, and Bun 1+ on Linux, macOS, and
|
|
724
|
-
Windows.
|
|
917
|
+
Knitting supports Node.js 22+, Deno 2+, and Bun 1+ on Linux, macOS, and Windows.
|
|
725
918
|
|
|
726
919
|
Thread workers work without native pieces. Process workers and
|
|
727
920
|
`ProcessSharedBuffer` use the platform's shared-memory APIs. Release packages
|
|
@@ -732,8 +925,8 @@ FFI path; if you are developing locally on a new Node ABI or architecture, run:
|
|
|
732
925
|
bun run build:native
|
|
733
926
|
```
|
|
734
927
|
|
|
735
|
-
For Deno projects with permissions enabled, allow FFI when using process
|
|
736
|
-
|
|
928
|
+
For Deno projects with permissions enabled, allow FFI when using process workers
|
|
929
|
+
or `ProcessSharedBuffer`.
|
|
737
930
|
|
|
738
931
|
## Runtime Safety
|
|
739
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
|
|
@@ -32,6 +35,10 @@ The core flow is:
|
|
|
32
35
|
## Build And Scripts
|
|
33
36
|
|
|
34
37
|
- `build.ts`: Bundles `knitting.ts` to `out/` with Bun for a Node ESM target.
|
|
38
|
+
- `tsconfig.npm.json`: TypeScript config for the npm release build. Emits
|
|
39
|
+
`.js` and `.d.ts` files beside the source tree.
|
|
40
|
+
- `scripts/rewrite-declaration-imports.mjs`: Post-processes emitted `.d.ts`
|
|
41
|
+
files so declaration imports point at `.js` files for npm consumers.
|
|
35
42
|
- `scripts/build-native-addons.ts`: Compiles the native Node addons into
|
|
36
43
|
`build/Release/` on Linux, macOS, and Windows. It finds Node headers/libs,
|
|
37
44
|
splits user flags, builds the shared-memory and futex addons, and emits the
|
|
@@ -39,6 +46,27 @@ The core flow is:
|
|
|
39
46
|
- `run.sh`: Runs every top-level benchmark in `bench/` across Node, Deno, and
|
|
40
47
|
Bun. `--json` writes JSON result files.
|
|
41
48
|
|
|
49
|
+
## CI And Release
|
|
50
|
+
|
|
51
|
+
- `.github/workflows/test.yml`: Push/PR test matrix for Deno, Node, and Bun
|
|
52
|
+
across Linux, macOS, and Windows.
|
|
53
|
+
- `.github/workflows/coverage.yml`: Node coverage workflow with a 90% line
|
|
54
|
+
coverage gate.
|
|
55
|
+
- `.github/workflows/build-and-test.yml`: Manual workflow that builds native
|
|
56
|
+
prebuild artifacts and runs runtime tests against them.
|
|
57
|
+
- `.github/workflows/publish.yml`: Manual native-prebuild workflow. Verifies
|
|
58
|
+
Node and Windows FFI prebuilds, checks the JSR package contents, and commits
|
|
59
|
+
updated `prebuilds/` artifacts back to the branch.
|
|
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.
|
|
69
|
+
|
|
42
70
|
## Public API Layer
|
|
43
71
|
|
|
44
72
|
- `src/api.ts`: Defines `task`, `importTask`, `createPool`, `isMain`, task id
|
|
@@ -70,6 +98,8 @@ The core flow is:
|
|
|
70
98
|
- `src/common/with-resolvers.ts`: `Promise.withResolvers` compatibility helper.
|
|
71
99
|
- `src/common/worker-runtime.ts`: Runtime-neutral worker/thread/process-worker
|
|
72
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.
|
|
73
103
|
|
|
74
104
|
## Runtime Host Side
|
|
75
105
|
|
|
@@ -85,12 +115,20 @@ The core flow is:
|
|
|
85
115
|
first-idle, random, and first-idle-random.
|
|
86
116
|
- `src/runtime/inline-executor.ts`: Optional in-process executor used by the
|
|
87
117
|
inliner path to run tasks without crossing the worker boundary.
|
|
118
|
+
- `src/runtime/process-worker.ts`: Process-worker spawning, command/runtime
|
|
119
|
+
selection, process shared-memory layout, inherited/named mapping metadata, and
|
|
120
|
+
child boot payload construction.
|
|
88
121
|
|
|
89
122
|
## Worker Side
|
|
90
123
|
|
|
124
|
+
- `src/worker/bootstrap.ts`: Optional user bootstrap hook that runs before task
|
|
125
|
+
modules import and revives process-shared-buffer metadata in bootstrap data.
|
|
91
126
|
- `src/worker/loop.ts`: Worker entrypoint and main loop. Boots worker contexts,
|
|
92
127
|
installs safety guards, receives tasks, executes batches, writes completions,
|
|
93
128
|
and supports process-worker bootstrapping.
|
|
129
|
+
- `src/worker/process-worker-bootstrap.ts`: Child-side process-worker boot
|
|
130
|
+
payload validation, shared-memory remapping, runtime primitive setup, and
|
|
131
|
+
startup handoff into the worker loop.
|
|
94
132
|
- `src/worker/task-loader.ts`: Imports task modules inside workers, finds
|
|
95
133
|
exported task definitions, filters by id/caller position, and normalizes
|
|
96
134
|
timeout metadata.
|
|
@@ -154,6 +192,8 @@ The core flow is:
|
|
|
154
192
|
payload-codec registration.
|
|
155
193
|
- `src/connections/file-descriptor.ts`: File descriptor wrapper, metadata
|
|
156
194
|
parsing, mapping support, and descriptor lifecycle helpers.
|
|
195
|
+
- `src/connections/node-addons.ts`: Native addon specifier resolution for
|
|
196
|
+
committed Node ABI prebuilds with `build/Release` fallback loading.
|
|
157
197
|
- `src/connections/node.ts`: Loads POSIX Node native addons and exposes Node
|
|
158
198
|
shared memory, mapping, unlink, and futex primitives.
|
|
159
199
|
- `src/connections/bun.ts`: Bun FFI implementation for POSIX shared memory.
|
|
@@ -169,6 +209,10 @@ The core flow is:
|
|
|
169
209
|
- `src/knitting_windows_shared_memory.cc`: Runtime-neutral Windows DLL exports
|
|
170
210
|
for creating, opening, mapping, and closing named shared-memory objects from
|
|
171
211
|
FFI runtimes.
|
|
212
|
+
- `prebuilds/*/*.node`: Tracked Node native-addon prebuilds for supported
|
|
213
|
+
platform/Node ABI combinations.
|
|
214
|
+
- `prebuilds/win32-x64/*.dll`: Tracked Windows FFI DLL prebuild used by Bun and
|
|
215
|
+
Deno shared-memory primitives on Windows.
|
|
172
216
|
|
|
173
217
|
## Permissions
|
|
174
218
|
|
|
@@ -190,6 +234,9 @@ The core flow is:
|
|
|
190
234
|
- `bench/withload.ts`: Measures behavior under main-thread load.
|
|
191
235
|
- `bench/call-growth.ts`: Measures call cost as payload size grows.
|
|
192
236
|
- `bench/call-growth-batch.ts`: Batch-focused version of call-growth tests.
|
|
237
|
+
- `bench/startup.ts`: Measures `createPool` to first-response startup latency
|
|
238
|
+
across thread and process workers, with optional cross-runtime and named
|
|
239
|
+
shared-memory candidates.
|
|
193
240
|
- `bench/tokio-mpsc-knitting.ts`: Batch latency benchmark for string, number,
|
|
194
241
|
and Uint8Array echo tasks.
|
|
195
242
|
- `bench/payload-sweep.ts`: Uint8Array payload-size sweep promoted from the old
|
|
@@ -219,6 +266,7 @@ The core flow is:
|
|
|
219
266
|
|
|
220
267
|
## Tests
|
|
221
268
|
|
|
269
|
+
- `test/_runner.ts`: Runtime-neutral test runner shim used by the test suite.
|
|
222
270
|
- `test/abortSignal.test.ts`: Shared abort bitset behavior.
|
|
223
271
|
- `test/api-cap.test.ts`: API limits such as maximum task id count.
|
|
224
272
|
- `test/shared-buffer-io.test.ts`: Shared-buffer IO read/write behavior.
|
|
@@ -250,6 +298,9 @@ The core flow is:
|
|
|
250
298
|
- `test/task-abort-context-api.test.ts`: Worker abort toolkit/context behavior.
|
|
251
299
|
- `test/tx-queue.test.ts`: Host transmit queue behavior and late-result safety.
|
|
252
300
|
- `test/type-inference.test.ts`: Public type inference guarantees.
|
|
301
|
+
- `test/utils.test.ts`: Public utility conversion helpers.
|
|
302
|
+
- `test/worker-bootstrap.test.ts`: Worker bootstrap hook behavior, shared-buffer
|
|
303
|
+
metadata revival, startup failure propagation, and inliner incompatibility.
|
|
253
304
|
- `test/fixtures/*.ts`: Task modules used by tests.
|
|
254
305
|
- `test/fixtures/probes/*.ts`: Probe programs for crash, permission, process,
|
|
255
306
|
file-descriptor, and shared-memory-corruption safety cases.
|
|
@@ -259,12 +310,11 @@ The core flow is:
|
|
|
259
310
|
- `build/Release/*.node`: Native addon output produced by
|
|
260
311
|
`scripts/build-native-addons.ts`.
|
|
261
312
|
- `out/`: Bundled output produced by `build.ts`.
|
|
313
|
+
- `dest/`: Scratch output used by the `build:node` package script.
|
|
314
|
+
- `knitting.js`, `knitting.d.ts`, `process-shared-buffer.js`,
|
|
315
|
+
`process-shared-buffer.d.ts`, and `src/**/*.js` / `src/**/*.d.ts`: npm
|
|
316
|
+
release build artifacts produced by `tsconfig.npm.json`.
|
|
262
317
|
- `results/`: Benchmark output produced by `run.sh`.
|
|
318
|
+
- `log/`, `logs`, and `*.log`: Local runtime/log output.
|
|
263
319
|
- `node_modules/`: Installed dependencies. Not part of the source map.
|
|
264
320
|
|
|
265
|
-
## Deleted Or Intentionally Absent
|
|
266
|
-
|
|
267
|
-
- Browser-mode build/smoke files are no longer part of the project.
|
|
268
|
-
- The old top-level scratch files `uwu.ts` and `examples.ts` are removed.
|
|
269
|
-
- Python graph scripts under `graphs/` were removed; current benchmark output is
|
|
270
|
-
kept in the TypeScript benchmark suite.
|