knitting 0.1.46
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/LICENSE +202 -0
- package/README.md +632 -0
- package/knitting.d.ts +4 -0
- package/knitting.js +5 -0
- package/map.md +264 -0
- package/package.json +77 -0
- package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shm.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_shm.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_shm.node +0 -0
- package/process-shared-buffer.d.ts +1 -0
- package/process-shared-buffer.js +1 -0
- package/scripts/build-native-addons.ts +295 -0
- package/src/api.d.ts +55 -0
- package/src/api.js +384 -0
- package/src/common/envelope.d.ts +11 -0
- package/src/common/envelope.js +8 -0
- package/src/common/module-url.d.ts +1 -0
- package/src/common/module-url.js +24 -0
- package/src/common/node-compat.d.ts +20 -0
- package/src/common/node-compat.js +24 -0
- package/src/common/path-canonical.d.ts +6 -0
- package/src/common/path-canonical.js +41 -0
- package/src/common/runtime.d.ts +15 -0
- package/src/common/runtime.js +91 -0
- package/src/common/shared-buffer-region.d.ts +11 -0
- package/src/common/shared-buffer-region.js +21 -0
- package/src/common/shared-buffer-text.d.ts +16 -0
- package/src/common/shared-buffer-text.js +65 -0
- package/src/common/task-source.d.ts +2 -0
- package/src/common/task-source.js +79 -0
- package/src/common/task-symbol.d.ts +1 -0
- package/src/common/task-symbol.js +1 -0
- package/src/common/with-resolvers.d.ts +9 -0
- package/src/common/with-resolvers.js +23 -0
- package/src/common/worker-runtime.d.ts +40 -0
- package/src/common/worker-runtime.js +52 -0
- package/src/connections/bun.d.ts +20 -0
- package/src/connections/bun.js +159 -0
- package/src/connections/deno.d.ts +20 -0
- package/src/connections/deno.js +150 -0
- package/src/connections/file-descriptor.d.ts +37 -0
- package/src/connections/file-descriptor.js +139 -0
- package/src/connections/index.d.ts +3 -0
- package/src/connections/index.js +3 -0
- package/src/connections/node-addons.d.ts +5 -0
- package/src/connections/node-addons.js +43 -0
- package/src/connections/node.d.ts +29 -0
- package/src/connections/node.js +59 -0
- package/src/connections/posix.d.ts +31 -0
- package/src/connections/posix.js +71 -0
- package/src/connections/process-shared-buffer.d.ts +67 -0
- package/src/connections/process-shared-buffer.js +267 -0
- package/src/connections/types.d.ts +48 -0
- package/src/connections/types.js +37 -0
- package/src/error.d.ts +13 -0
- package/src/error.js +49 -0
- package/src/ipc/tools/ring-queue.d.ts +33 -0
- package/src/ipc/tools/ring-queue.js +159 -0
- package/src/ipc/transport/shared-memory.d.ts +25 -0
- package/src/ipc/transport/shared-memory.js +35 -0
- package/src/knitting_shared_memory.cc +436 -0
- package/src/knitting_shm.cc +476 -0
- package/src/memory/byte-carpet.d.ts +73 -0
- package/src/memory/byte-carpet.js +157 -0
- package/src/memory/lock.d.ts +190 -0
- package/src/memory/lock.js +856 -0
- package/src/memory/payload-config.d.ts +22 -0
- package/src/memory/payload-config.js +67 -0
- package/src/memory/payloadCodec.d.ts +46 -0
- package/src/memory/payloadCodec.js +1157 -0
- package/src/memory/regionRegistry.d.ts +17 -0
- package/src/memory/regionRegistry.js +285 -0
- package/src/memory/shared-buffer-io.d.ts +53 -0
- package/src/memory/shared-buffer-io.js +380 -0
- package/src/permission/index.d.ts +2 -0
- package/src/permission/index.js +2 -0
- package/src/permission/protocol.d.ts +166 -0
- package/src/permission/protocol.js +640 -0
- package/src/runtime/balancer.d.ts +19 -0
- package/src/runtime/balancer.js +149 -0
- package/src/runtime/dispatcher.d.ts +34 -0
- package/src/runtime/dispatcher.js +142 -0
- package/src/runtime/inline-executor.d.ts +10 -0
- package/src/runtime/inline-executor.js +270 -0
- package/src/runtime/pool.d.ts +43 -0
- package/src/runtime/pool.js +922 -0
- package/src/runtime/tx-queue.d.ts +25 -0
- package/src/runtime/tx-queue.js +144 -0
- package/src/shared/abortSignal.d.ts +23 -0
- package/src/shared/abortSignal.js +126 -0
- package/src/types.d.ts +283 -0
- package/src/types.js +2 -0
- package/src/worker/composable-runners.d.ts +12 -0
- package/src/worker/composable-runners.js +105 -0
- package/src/worker/loop.d.ts +2 -0
- package/src/worker/loop.js +453 -0
- package/src/worker/rx-queue.d.ts +22 -0
- package/src/worker/rx-queue.js +124 -0
- package/src/worker/safety/index.d.ts +4 -0
- package/src/worker/safety/index.js +4 -0
- package/src/worker/safety/performance.d.ts +1 -0
- package/src/worker/safety/performance.js +17 -0
- package/src/worker/safety/process.d.ts +2 -0
- package/src/worker/safety/process.js +79 -0
- package/src/worker/safety/startup.d.ts +16 -0
- package/src/worker/safety/startup.js +30 -0
- package/src/worker/safety/worker-data.d.ts +2 -0
- package/src/worker/safety/worker-data.js +36 -0
- package/src/worker/task-loader.d.ts +26 -0
- package/src/worker/task-loader.js +66 -0
- package/src/worker/timers.d.ts +18 -0
- package/src/worker/timers.js +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
# knitting
|
|
2
|
+
|
|
3
|
+
[](https://jsr.io/@vixeny/knitting)
|
|
4
|
+
[](https://jsr.io/@vixeny/knitting)
|
|
5
|
+
[](https://github.com/mimiMonads/knitting/actions/workflows/test.yml)
|
|
6
|
+
[](https://github.com/mimiMonads/knitting/actions/workflows/coverage.yml)
|
|
7
|
+
[](https://github.com/mimiMonads/knitting/actions/workflows/coverage.yml)
|
|
8
|
+
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
[](https://deno.com/)
|
|
11
|
+
[](https://bun.sh/)
|
|
12
|
+
|
|
13
|
+
Knitting is a worker-pool over shared-memory IPC runtime for Node.js,
|
|
14
|
+
Deno, and Bun. Which mission is make javascript a real multicore language,
|
|
15
|
+
Thanks to its memory design, it can be from 5x to 25x faster than using `postMessages`
|
|
16
|
+
bypassing OS sockect comunnication entielly with a novel protocol written from scratch.
|
|
17
|
+
|
|
18
|
+
Use it for the parts of your program that should run somewhere else: CPU-heavy to small
|
|
19
|
+
work, runtime-isolated jobs, long-running tools, or anything that needs to move
|
|
20
|
+
speed and type felixbility.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
You define a task once, spin up a pool, and call it like a normal async
|
|
24
|
+
function:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const result = await pool.call.resizeImage(file);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Under the hood, Knitting schedules work across worker threads or separate
|
|
31
|
+
processes (depending on runtime and your settings), keeps arguments typed, and
|
|
32
|
+
uses shared memory for transport.
|
|
33
|
+
|
|
34
|
+
## So
|
|
35
|
+
|
|
36
|
+
- Call worker code like `pool.call.myTask(arg)`.
|
|
37
|
+
- Keep the resolved types end-to-end
|
|
38
|
+
- Choose `threads` for speed or `processes` for stronger isolation.
|
|
39
|
+
- Move supported payloads via shared memory (great for binary data).
|
|
40
|
+
- Run on Node.js, Deno, or Bun.
|
|
41
|
+
|
|
42
|
+
## Why use it?
|
|
43
|
+
|
|
44
|
+
- Easy to use: Have a multi-thread enviroment or process with few lines of code.
|
|
45
|
+
- Great type support: pass primitives, JSON, Promise of these and special types (typed arrays, Node
|
|
46
|
+
`Buffer`, `Envelope`, and `ProcessSharedBuffer`).
|
|
47
|
+
- Runtime flexibility: the same API across Node.js, Deno, and Bun.
|
|
48
|
+
- Worker choices: use threads for fast pools, or processes for stronger
|
|
49
|
+
isolation.
|
|
50
|
+
- All out-of-the-box expierince: strict-by-default permissions, payload-size limits, task
|
|
51
|
+
timeouts, abort-aware tasks, and worker hard timeouts.
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
- Node.js 22+
|
|
56
|
+
- Deno 2+
|
|
57
|
+
- Bun 1+
|
|
58
|
+
|
|
59
|
+
Process workers and `ProcessSharedBuffer` use the native shared-memory layer.
|
|
60
|
+
On Linux and macOS, build it before running process/shared-memory tests.
|
|
61
|
+
If you're only using thread workers, you can typically ignore this and use
|
|
62
|
+
Knitting without building native addons.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install knitting
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Via JSR's npm compatibility:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
jsr add --npm @vixeny/knitting
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For Deno projects:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
deno add jsr:@vixeny/knitting
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Quick Start
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { createPool, isMain, task } from "knitting";
|
|
86
|
+
|
|
87
|
+
export const square = task<number, number>({
|
|
88
|
+
f: (value) => value * value,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const greet = task<string, string>({
|
|
92
|
+
f: (name) => `hello ${name}`,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (isMain) {
|
|
96
|
+
const pool = createPool({ threads: 2 })({ square, greet });
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const [four, message] = await Promise.all([
|
|
100
|
+
pool.call.square(2),
|
|
101
|
+
pool.call.greet("knitting"),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
console.log({ four, message });
|
|
105
|
+
} finally {
|
|
106
|
+
await pool.shutdown();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The `isMain` guard when the same module is loaded by workers or process. Export
|
|
112
|
+
exposes the tasks or functions at module scope, so knitting maps down the imports,
|
|
113
|
+
then use and use the pool only from the main program.
|
|
114
|
+
|
|
115
|
+
## The Mental Model
|
|
116
|
+
|
|
117
|
+
There are three core pieces, plus `isMain` for modules that workers may import:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { createPool, isMain, task } from "knitting";
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- `task(...)` describes a callable worker function (types + implementation).
|
|
124
|
+
|
|
125
|
+
- `createPool(options)({ tasks })` starts workers and gives you a typed `call`
|
|
126
|
+
object for invoking tasks.
|
|
127
|
+
|
|
128
|
+
- `pool.shutdown()` stops workers when you're done.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
export const add = task<[number, number], number>({
|
|
132
|
+
f: ([a, b]) => a + b,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (isMain) {
|
|
136
|
+
const pool = createPool({ threads: 4 })({ add });
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const value = await pool.call.add([1, 2]);
|
|
140
|
+
console.log(value);
|
|
141
|
+
} finally {
|
|
142
|
+
await pool.shutdown();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Once you have a pool, calls are just promises, so batching looks like normal
|
|
148
|
+
JavaScript:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const values = await Promise.all(
|
|
152
|
+
Array.from({ length: 1_000 }, (_, index) => pool.call.add([index, 1])),
|
|
153
|
+
);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Defining Tasks
|
|
157
|
+
|
|
158
|
+
### Arguments and return values
|
|
159
|
+
|
|
160
|
+
Each task receives one argument and returns one value. If you need multiple
|
|
161
|
+
inputs, pass an object or tuple.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
type ResizeInput = {
|
|
165
|
+
width: number;
|
|
166
|
+
height: number;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const pixels = task<ResizeInput, number>({
|
|
170
|
+
f: ({ width, height }) => width * height,
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Supported payloads are listed below. For large binary data, prefer
|
|
175
|
+
`ArrayBuffer`, typed arrays, or `ProcessSharedBuffer` instead of serializing
|
|
176
|
+
big objects.
|
|
177
|
+
|
|
178
|
+
### Task timeouts
|
|
179
|
+
|
|
180
|
+
Use a task timeout when a worker call should not wait forever.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
export const maybeSlow = task<string, string>({
|
|
184
|
+
timeout: { time: 500, default: "timed out" },
|
|
185
|
+
f: async (value) => {
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
187
|
+
return value.toUpperCase();
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Timeouts can reject the call, resolve with a default value, or use a custom
|
|
193
|
+
error depending on the timeout options you choose.
|
|
194
|
+
|
|
195
|
+
### Abort-aware tasks
|
|
196
|
+
|
|
197
|
+
If a task is long-running, opt into an abort signal and check it inside the
|
|
198
|
+
worker function.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
export const countUntilStopped = task({
|
|
202
|
+
abortSignal: { hasAborted: true },
|
|
203
|
+
f: async (limit: number, signal) => {
|
|
204
|
+
for (let index = 0; index < limit; index += 1) {
|
|
205
|
+
if (signal.hasAborted()) return index;
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return limit;
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The pool also has an `abortSignalCapacity` option for sizing the shared abort
|
|
215
|
+
signal storage when many abort-aware calls may be in flight.
|
|
216
|
+
|
|
217
|
+
### Importing worker-side functions
|
|
218
|
+
|
|
219
|
+
`importTask` lets the worker import a normal function from another module. The
|
|
220
|
+
host gets a typed task wrapper, but it does not import or evaluate that worker
|
|
221
|
+
module itself.
|
|
222
|
+
|
|
223
|
+
That matters for process workers and sandboxing: if the code is supposed to run
|
|
224
|
+
inside the worker's permissions, keep it in a separate file and point
|
|
225
|
+
`importTask()` at that file.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
// worker-tasks.ts
|
|
229
|
+
export const add = ([left, right]: [number, number]) => left + right;
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// main.ts
|
|
234
|
+
import { createPool, importTask, isMain } from "knitting";
|
|
235
|
+
|
|
236
|
+
export const add = importTask<[number, number], number>({
|
|
237
|
+
href: "./worker-tasks.ts",
|
|
238
|
+
name: "add",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (isMain) {
|
|
242
|
+
const pool = createPool({ threads: 2 })({ add });
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
console.log(await pool.call.add([2, 3]));
|
|
246
|
+
} finally {
|
|
247
|
+
await pool.shutdown();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`href` can be a local relative path like `"./worker-tasks.ts"`, an absolute file
|
|
253
|
+
path, or a URL. Relative paths are resolved from the module that calls
|
|
254
|
+
`importTask()`.
|
|
255
|
+
|
|
256
|
+
When workers import files, keep the pool's permission settings in mind. The
|
|
257
|
+
default strict mode allows task imports, but custom permission policies can
|
|
258
|
+
limit reads, writes, environment access, networking, and process execution.
|
|
259
|
+
|
|
260
|
+
### Single-task shorthand
|
|
261
|
+
|
|
262
|
+
For quick scripts, a task can create its own pool:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { isMain, task } from "knitting";
|
|
266
|
+
|
|
267
|
+
export const double = task<number, number>({
|
|
268
|
+
f: (value) => value * 2,
|
|
269
|
+
}).createPool({ threads: 2 });
|
|
270
|
+
|
|
271
|
+
if (isMain) {
|
|
272
|
+
try {
|
|
273
|
+
console.log(await double.call(21));
|
|
274
|
+
} finally {
|
|
275
|
+
await double.shutdown();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Creating Pools
|
|
281
|
+
|
|
282
|
+
You typically create one pool per set of tasks and reuse it.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
const pool = createPool({
|
|
286
|
+
threads: 4,
|
|
287
|
+
balancer: "firstIdle",
|
|
288
|
+
payload: {
|
|
289
|
+
payloadMaxByteLength: 64 * 1024 * 1024,
|
|
290
|
+
maxPayloadBytes: 8 * 1024 * 1024,
|
|
291
|
+
},
|
|
292
|
+
})({ add, pixels });
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Common options you might tweak:
|
|
296
|
+
|
|
297
|
+
| Option | What it does |
|
|
298
|
+
| --- | --- |
|
|
299
|
+
| `threads` | Number of workers to start. |
|
|
300
|
+
| `balancer` | Scheduling strategy: `"roundRobin"`, `"firstIdle"`, `"randomLane"`, `"firstIdleOrRandom"`, or the legacy alias `"robinRound"`. |
|
|
301
|
+
| `payload` | Shared payload-buffer settings: `mode`, `payloadInitialBytes`, `payloadMaxByteLength`, and `maxPayloadBytes`. |
|
|
302
|
+
| `abortSignalCapacity` | Number of shared abort slots available to abort-aware calls. |
|
|
303
|
+
| `worker.resolveAfterFinishingAll` | Let submitted calls finish before shutdown resolves. |
|
|
304
|
+
| `worker.hardTimeoutMs` | Force pool shutdown when a task exceeds this many milliseconds. |
|
|
305
|
+
| `worker.runtime` | Choose `"thread"` or `"process"` workers. |
|
|
306
|
+
| `permission` | Runtime permission policy for workers. |
|
|
307
|
+
| `debug` | Enable extra diagnostics. |
|
|
308
|
+
| `source` | Worker source override for advanced runtimes. |
|
|
309
|
+
|
|
310
|
+
## Worker Runtimes
|
|
311
|
+
|
|
312
|
+
By default, workers use runtime-local threads where possible (the lowest
|
|
313
|
+
overhead option).
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
const pool = createPool({
|
|
317
|
+
threads: 4,
|
|
318
|
+
})({ add });
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
If you want stronger isolation, or you need to run workers through a specific
|
|
322
|
+
runtime executable, use process workers.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
const pool = createPool({
|
|
326
|
+
threads: 2,
|
|
327
|
+
worker: {
|
|
328
|
+
runtime: "process",
|
|
329
|
+
processRuntime: "node",
|
|
330
|
+
},
|
|
331
|
+
})({ add });
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`processRuntime` can be `"node"`, `"deno"`, or `"bun"`. You can also provide a
|
|
335
|
+
`processCommandPrefix` when workers need to be launched through a wrapper such
|
|
336
|
+
as a package manager, container command, or runtime shim.
|
|
337
|
+
|
|
338
|
+
That prefix is also useful for sandbox and resource-control tools. The one
|
|
339
|
+
important detail is that process workers receive their shared-memory handle on
|
|
340
|
+
stdin, which is file descriptor 0. Wrappers that leave stdin alone usually work;
|
|
341
|
+
wrappers that replace, close, or proxy stdin without passing the fd through will
|
|
342
|
+
stop the worker from booting.
|
|
343
|
+
|
|
344
|
+
When the goal is isolation, define the worker code with `importTask()` instead
|
|
345
|
+
of importing the task function directly into the host. That keeps the code you
|
|
346
|
+
want to isolate out of the host process; only the worker imports and runs it.
|
|
347
|
+
|
|
348
|
+
For example, this runs Bun process workers through Bubblewrap while preserving
|
|
349
|
+
the inherited fd:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
const pool = createPool({
|
|
353
|
+
threads: 2,
|
|
354
|
+
worker: {
|
|
355
|
+
runtime: "process",
|
|
356
|
+
processRuntime: "bun",
|
|
357
|
+
processCommandPrefix: [
|
|
358
|
+
"bwrap",
|
|
359
|
+
"--unshare-all",
|
|
360
|
+
"--ro-bind",
|
|
361
|
+
"/",
|
|
362
|
+
"/",
|
|
363
|
+
"--dev-bind",
|
|
364
|
+
"/dev",
|
|
365
|
+
"/dev",
|
|
366
|
+
"--proc",
|
|
367
|
+
"/proc",
|
|
368
|
+
"--tmpfs",
|
|
369
|
+
"/tmp",
|
|
370
|
+
"--die-with-parent",
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
})({ add });
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Permissions
|
|
377
|
+
|
|
378
|
+
Knitting defaults to a strict worker permission policy:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
permission: { mode: "strict", allowImport: true }
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
That default is meant to be safe enough for normal task imports without giving
|
|
385
|
+
workers broad ambient access.
|
|
386
|
+
|
|
387
|
+
For trusted local scripts, you can opt out:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
const pool = createPool({
|
|
391
|
+
permission: "unsafe",
|
|
392
|
+
})({ add });
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
For production or plugin-like workloads, prefer an explicit policy:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
const pool = createPool({
|
|
399
|
+
permission: {
|
|
400
|
+
mode: "strict",
|
|
401
|
+
allowImport: true,
|
|
402
|
+
read: ["./data"],
|
|
403
|
+
write: ["./out"],
|
|
404
|
+
net: ["api.example.com"],
|
|
405
|
+
env: { allow: ["NODE_ENV"] },
|
|
406
|
+
console: true,
|
|
407
|
+
},
|
|
408
|
+
})({ add });
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Permissions are enforced using the runtime features available in Node.js, Deno,
|
|
412
|
+
and Bun. The exact mechanics vary by runtime, so treat them as a guardrail, not
|
|
413
|
+
as the only security boundary for hostile code.
|
|
414
|
+
|
|
415
|
+
## Payloads
|
|
416
|
+
|
|
417
|
+
Worker calls can carry the following values across the shared-memory transport:
|
|
418
|
+
|
|
419
|
+
- `string`, `number`, `boolean`, `bigint`, `null`, and `undefined`.
|
|
420
|
+
- Plain objects and arrays made from supported values.
|
|
421
|
+
- `ArrayBuffer`, Node `Buffer`, `DataView`, and supported typed arrays.
|
|
422
|
+
- `ProcessSharedBuffer`.
|
|
423
|
+
- `Envelope` for metadata plus binary payloads.
|
|
424
|
+
- `Error`, `Date`, and global symbols created with `Symbol.for(...)`.
|
|
425
|
+
- Native `Promise<supported-value>` inputs. The promise is awaited before
|
|
426
|
+
dispatch.
|
|
427
|
+
- Thenables are not awaited by the transport.
|
|
428
|
+
|
|
429
|
+
If it isn't on that list, assume it isn't portable. Some things don't (or
|
|
430
|
+
shouldn't) cross the boundary:
|
|
431
|
+
|
|
432
|
+
- DOM objects and platform handles.
|
|
433
|
+
- Functions, unless they are part of a `task` or `importTask` definition.
|
|
434
|
+
- Cyclic object graphs.
|
|
435
|
+
- `Map`, `Set`, `WeakMap`, and non-global symbols.
|
|
436
|
+
- Objects with behavior that depends on prototypes, getters, setters, or hidden
|
|
437
|
+
process-local state.
|
|
438
|
+
|
|
439
|
+
If a payload is large, set `payload.maxPayloadBytes` deliberately and prefer
|
|
440
|
+
binary/shared-memory shapes over deeply nested objects.
|
|
441
|
+
|
|
442
|
+
## Shared Memory Channels
|
|
443
|
+
|
|
444
|
+
`ProcessSharedBuffer` is the lower-level building block for process-safe shared
|
|
445
|
+
memory. Use it when two workers or processes need to see the same bytes without
|
|
446
|
+
copying the whole payload for every call.
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
import { ProcessSharedBuffer } from "knitting/process-shared-buffer";
|
|
450
|
+
import { createPool, isMain, task } from "knitting";
|
|
451
|
+
|
|
452
|
+
export const readFirstCell = task<ProcessSharedBuffer, number>({
|
|
453
|
+
f: (buffer) => Atomics.load(buffer.view(Int32Array), 0),
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
if (isMain) {
|
|
457
|
+
const pool = createPool({ threads: 1 })({ readFirstCell });
|
|
458
|
+
const shared = ProcessSharedBuffer.create(64);
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
Atomics.store(shared.view(Int32Array), 0, 42);
|
|
462
|
+
console.log(await pool.call.readFirstCell(shared));
|
|
463
|
+
} finally {
|
|
464
|
+
shared.close();
|
|
465
|
+
await pool.shutdown();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Private parent-child buffers
|
|
471
|
+
|
|
472
|
+
The default mode is anonymous:
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
const shared = ProcessSharedBuffer.create(64);
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Anonymous buffers are the safest default. They are private handles that are
|
|
479
|
+
passed intentionally through Knitting's transport. They are also created with
|
|
480
|
+
close-on-exec style hardening where the platform supports it, so unrelated
|
|
481
|
+
programs do not accidentally inherit them.
|
|
482
|
+
|
|
483
|
+
### Named channels for independent processes
|
|
484
|
+
|
|
485
|
+
Sometimes you do want two unrelated processes to rendezvous on purpose. Use a
|
|
486
|
+
named channel for that.
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import { ProcessSharedBuffer } from "knitting/process-shared-buffer";
|
|
490
|
+
|
|
491
|
+
const name = "knitting-demo-channel";
|
|
492
|
+
|
|
493
|
+
const owner = ProcessSharedBuffer.create({
|
|
494
|
+
name,
|
|
495
|
+
size: 64,
|
|
496
|
+
mode: "create",
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
Atomics.store(owner.view(Int32Array), 0, 7);
|
|
501
|
+
|
|
502
|
+
const peer = ProcessSharedBuffer.create({
|
|
503
|
+
name,
|
|
504
|
+
size: 64,
|
|
505
|
+
mode: "open",
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
console.log(Atomics.load(peer.view(Int32Array), 0));
|
|
510
|
+
} finally {
|
|
511
|
+
peer.close();
|
|
512
|
+
}
|
|
513
|
+
} finally {
|
|
514
|
+
owner.close();
|
|
515
|
+
ProcessSharedBuffer.unlink(name);
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Use `"create"` for the process that owns the channel and `"open"` for peers.
|
|
520
|
+
Treat the channel name like a capability: make it unique, do not accept it from
|
|
521
|
+
untrusted input without validation, and clean it up when the channel is no
|
|
522
|
+
longer needed. On POSIX runtimes `unlink` removes the name. On platforms where
|
|
523
|
+
named mappings are lifetime-managed by the OS, closing the last handle is the
|
|
524
|
+
important cleanup step.
|
|
525
|
+
|
|
526
|
+
### Current support
|
|
527
|
+
|
|
528
|
+
Thread workers are the broadest path: they do not need native prebuilds or FFI.
|
|
529
|
+
Process workers and `ProcessSharedBuffer` both use OS-backed shared memory, so
|
|
530
|
+
their support follows the native backend for each runtime.
|
|
531
|
+
|
|
532
|
+
For now, Windows support means thread workers only. Process workers and
|
|
533
|
+
`ProcessSharedBuffer` are supported on POSIX targets: Linux and macOS.
|
|
534
|
+
|
|
535
|
+
| Runtime and target | Thread workers | Process workers | `ProcessSharedBuffer` | Native path |
|
|
536
|
+
| --- | --- | --- | --- | --- |
|
|
537
|
+
| Node.js 22 / 24 on Linux x64 | Supported | Supported | Supported | Shipped Node `.node` prebuilds. |
|
|
538
|
+
| Node.js 22 / 24 on macOS x64 | Supported | Supported | Supported | Shipped Node `.node` prebuilds. |
|
|
539
|
+
| Node.js 22 / 24 on macOS arm64 | Supported | Supported | Supported | Shipped Node `.node` prebuilds. |
|
|
540
|
+
| Node.js 22 / 24 on Windows x64 | Supported | Not supported | Not supported | Native shared memory is POSIX-only. |
|
|
541
|
+
| Other POSIX Node.js ABI or arch | Supported | Local native build needed | Local native build needed | Run `bun run build:native` before using native shared memory. |
|
|
542
|
+
| Deno 2+ on Linux/macOS, runtime-supported arch | Supported | Supported | Supported | Uses Deno FFI into libc; allow FFI permission when permissions are enabled. |
|
|
543
|
+
| Deno 2+ on Windows | Supported | Not supported | Not supported | Current Deno backend is POSIX-only. |
|
|
544
|
+
| Bun 1+ on Linux/macOS, runtime-supported arch | Supported | Supported | Supported | Uses Bun FFI into libc. |
|
|
545
|
+
| Bun 1+ on Windows | Supported | Not supported | Not supported | Current Bun backend is POSIX-only. |
|
|
546
|
+
|
|
547
|
+
## Runtime Safety
|
|
548
|
+
|
|
549
|
+
Knitting aims to make the safer path the default:
|
|
550
|
+
|
|
551
|
+
- Strict worker permissions are the default.
|
|
552
|
+
- Anonymous shared memory is the default.
|
|
553
|
+
- Named shared memory requires an explicit `mode`.
|
|
554
|
+
- Payload sizes are bounded.
|
|
555
|
+
- Abort-aware tasks reserve shared abort slots.
|
|
556
|
+
- Workers can be guarded with `worker.hardTimeoutMs`.
|
|
557
|
+
- Shutdown can stop immediately or wait for submitted work with
|
|
558
|
+
`worker.resolveAfterFinishingAll`.
|
|
559
|
+
|
|
560
|
+
That said, workers still run code. If you treat tasks like plugins, keep
|
|
561
|
+
permissions tight, keep named shared-memory names hard to guess, and avoid
|
|
562
|
+
passing broad capabilities into worker code.
|
|
563
|
+
|
|
564
|
+
## Scheduling and Tuning
|
|
565
|
+
|
|
566
|
+
Choose a balancer based on the shape of your work:
|
|
567
|
+
|
|
568
|
+
- `"roundRobin"` is simple and works well for similarly sized tasks.
|
|
569
|
+
- `"firstIdle"` helps when task durations vary.
|
|
570
|
+
- `"randomLane"` is useful for simple spreading and experiments.
|
|
571
|
+
- `"firstIdleOrRandom"` prefers an idle worker, then falls back to random.
|
|
572
|
+
- `"robinRound"` is kept as a legacy alias of `"roundRobin"`.
|
|
573
|
+
|
|
574
|
+
Useful tuning options:
|
|
575
|
+
|
|
576
|
+
- Increase `threads` for parallel CPU-heavy work.
|
|
577
|
+
- Increase `payload.payloadMaxByteLength` only when the transport buffer needs
|
|
578
|
+
more room.
|
|
579
|
+
- Increase `payload.maxPayloadBytes` only when individual calls genuinely need
|
|
580
|
+
larger payloads.
|
|
581
|
+
- Use process workers when isolation matters more than startup cost.
|
|
582
|
+
|
|
583
|
+
## Benchmarks
|
|
584
|
+
|
|
585
|
+
```bash
|
|
586
|
+
bun run bench
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
The benchmark suite compares scheduling and payload behavior across supported
|
|
590
|
+
runtimes. Treat numbers as local guidance: CPU, runtime version, payload shape,
|
|
591
|
+
and worker type all matter.
|
|
592
|
+
|
|
593
|
+
## Development
|
|
594
|
+
|
|
595
|
+
Install dependencies:
|
|
596
|
+
|
|
597
|
+
```bash
|
|
598
|
+
bun install
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
Build the package:
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
bun run build
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Build the native shared-memory addon on Linux or macOS:
|
|
608
|
+
|
|
609
|
+
```bash
|
|
610
|
+
bun run build:native
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Run tests:
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
npm run test:node
|
|
617
|
+
npm run test:deno
|
|
618
|
+
npm run test:bun
|
|
619
|
+
npm run test:all
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Emit JSON benchmark results:
|
|
623
|
+
|
|
624
|
+
```bash
|
|
625
|
+
./run.sh --json
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
For a file-by-file orientation, see [map.md](./map.md).
|
|
629
|
+
|
|
630
|
+
## License
|
|
631
|
+
|
|
632
|
+
Apache-2.0
|
package/knitting.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { workerMainLoop } from "./src/worker/loop.js";
|
|
2
|
+
import { createPool, importTask, isMain, task } from "./src/api.js";
|
|
3
|
+
import { Envelope } from "./src/common/envelope.js";
|
|
4
|
+
export { createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, task as task, workerMainLoop as workerMainLoop, };
|
package/knitting.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Exportables
|
|
2
|
+
import { workerMainLoop } from "./src/worker/loop.js";
|
|
3
|
+
import { createPool, importTask, isMain, task } from "./src/api.js";
|
|
4
|
+
import { Envelope } from "./src/common/envelope.js";
|
|
5
|
+
export { createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, task as task, workerMainLoop as workerMainLoop, };
|