Quick Start
This quick start walks the whole loop: define tasks, create a pool, call tasks
like async functions, and let the pool shut down cleanly with using.
Introduction
Section titled “Introduction”In a few minutes you’ll have your first tasks running in parallel.
If you already know what workers are, think of Knitting as:
“workers, but with a function-call API and much lower overhead.”
Quick definitions
Section titled “Quick definitions”A few words we’ll use a lot:
- Task: a function the workers can run. The simplest task is just an
exported function; wrap it with
task({ f })when you want options like timeouts or aborts. call.*(): runs a task on the pool and returns aPromisewith the result.isMain: a boolean that’strueonly on the host. Workers re-import your module, so guard host-only code — like creating the pool — with it.createPool(): starts the workers and returns a typedcallobject plusshutdown. The pool is also disposable, sousingcan close it for you.- Host ↔ Worker: the host is the process that creates the pool; workers are the threads (or processes) that run the tasks.
Examples
Section titled “Examples”Now that we share the vocabulary, the snippets should feel familiar.
import { createPool, isMain } from "knitting";
// A task is just an exported function the workers can run.export const greet = (name: string) => `hello ${name}`;
if (isMain) { // `using` shuts the pool down automatically when this block ends. using pool = createPool({ threads: 1 })({ greet });
console.log(await pool.call.greet("knitting")); // hello knitting}import { createPool, isMain } from "knitting";
export const hello = () => "hello ";export const world = (prefix: string) => `${prefix}world!`;
if (isMain) { using pool = createPool({ threads: 2 })({ hello, world });
// call.hello() returns a promise; Knitting resolves it before world runs. const lines = await Promise.all( Array.from({ length: 3 }, () => pool.call.world(pool.call.hello())), );
console.log(lines.join(" ")); // hello world! hello world! hello world!}import { createPool, isMain } from "knitting";
// Several tasks share one pool. Calls are promises, so you can chain them.export const double = (n: number) => n * 2;export const square = (n: number) => n * n;
if (isMain) { using pool = createPool({ threads: 2 })({ double, square });
const results = await Promise.all( [1, 2, 3, 4, 5].map(async (n) => pool.call.square(await pool.call.double(n))), );
console.log(results); // [4, 16, 36, 64, 100]}import { createPool, isMain, task } from "knitting";
// Wrap a function with task() when you want options like a timeout.// This call is too slow, so it falls back to the default instead of hanging.export const slow = task({ timeout: { time: 100, default: "timed out" }, f: async (name: string) => { await new Promise((resolve) => setTimeout(resolve, 1_000)); return `hello ${name}`; },});
if (isMain) { using pool = createPool({ threads: 1 })({ slow });
console.log(await pool.call.slow("knitting")); // timed out}Build it step by step
Section titled “Build it step by step”-
Import what you need:
import { createPool, isMain } from "knitting"; -
Export your tasks. The simplest task is just an exported function, kept at module scope so workers can load it by name:
export const square = (n: number) => n * n;export const greet = (name: string) => `hello ${name}`; -
Create the pool inside an
isMainguard. Workers re-import this module, and the guard keeps them from re-running your host code:if (isMain) {using pool = createPool({ threads: 2 })({ square, greet });}usingmakes the pool close itself when the block ends — no manual cleanup. -
Call tasks like async functions. They’re just promises, so batch them with
Promise.all:if (isMain) {using pool = createPool({ threads: 2 })({ square, greet });const [n, message] = await Promise.all([pool.call.square(8),pool.call.greet("knitting"),]);console.log({ n, message }); // { n: 64, message: "hello knitting" }} -
Shut down when you’re done.
With
using, that already happens at the end of the block: workers stop and the process can exit. When you need to control the timing — or you’re on a runtime withoutusing— callshutdown()yourself:const pool = createPool({ threads: 2 })({ square, greet });try {console.log(await pool.call.square(8));} finally {await pool.shutdown();}
A task can make its own pool
Section titled “A task can make its own pool”For a quick script with a single task, skip the separate createPool call:
import { isMain, task } from "knitting";
export const double = task({ f: (n: number) => n * 2,}).createPool({ threads: 2 });
if (isMain) { try { console.log(await double.call(21)); // 42 } finally { await double.shutdown(); }}Good habits
Section titled “Good habits”A few things that keep Knitting code clean and fast.
Keep tasks in their own module(s)
Section titled “Keep tasks in their own module(s)”It keeps your app code tidy and lets workers load only what they need.
package.json
deno.json
Directory
src
Directory
knitting
- database.ts
- img_parsing.ts
- jwt.ts
Directory
app/
- …
Directory
pages/
- …
Promise inputs are awaited on the host
Section titled “Promise inputs are awaited on the host”call.*() accepts Promise<supported> inputs. Knitting resolves them on the
host before dispatch, so unresolved promise state never crosses the thread
boundary. That is handy in request handlers — you can pass a body read directly:
app.post("/validate", async (c) => { const result = await pool.call.validate(c.req.text()); return c.json(result);});If the input promise rejects, the host call rejects and the worker never runs.
Pick the cheapest payload that fits
Section titled “Pick the cheapest payload that fits”Smaller, flatter values move fastest: numbers, booleans, and short strings,
then typed arrays and Buffer, then compact JSON. For bytes-plus-metadata, use
Envelope. See Supported payloads.
Start strict, open up later
Section titled “Start strict, open up later”Worker permissions are restricted by default — sensitive paths like .env,
.git, ~/.ssh, and /etc are blocked, and node_modules is deny-write.
Open only what your tasks actually need. See Permissions.
Footguns
Section titled “Footguns”The handful of things that trip people up most — most of them follow from one fact: workers re-import the module that defines your tasks.
- One argument per task.
pool.call.add(a, b)won’t work — pass a tuple or object:pool.call.add([a, b])for([a, b]) => a + b. - Guard host code with
isMain. Without it, pool creation (and any other host-only code) re-runs inside every worker. - Top-level imports run in every worker.
importis hoisted, so it executes before anyisMaincheck. Keep tasks in their own lean module so workers don’t load your whole server framework. - Export your tasks. An unexported
task()/importTask()is invisible to the worker loader, so the call just hangs — no handler is ever registered. importTasktargets are plain functions, nottask()wrappers (that throws aTypeError). Puttimeout/abortSignaloptions on theimportTaskcall instead.- Worker
console.*is silent by default in strict mode. Passpermission: { console: true }to surface worker logs. - Can’t tell what the pool is doing? Pass
debug: truetocreatePool(or set theKNITTING_DEBUG=*env var) to stream setup, import, and lifecycle diagnostics to stderr — each line tagged with the worker and a millisecond timer. Narrow the noise with namespaces:debug: { host: true, imports: true }. It costs nothing when off. - Only supported payloads cross the boundary.
Map,Set, class instances, and functions are rejected — see Payloads. - Dynamic payloads cap at ~8 MiB by default; raise
payload.maxPayloadBytes(andpayload.payloadMaxByteLength) for larger ones.
Where to go next
Section titled “Where to go next”- Defining tasks —
task(),importTask(), timeouts, and aborts. - Creating pools — threads, balancers, and shutdown.
- Payloads — what crosses the boundary, and
Envelope. - Performance and the inliner — when to let the host run some work too.
Going further: Knitting can also run each worker as a separate process — even
inside a bwrap sandbox or a container — for stronger isolation (see
Process workers), and move large buffers with
ProcessSharedBuffer (shared memory, no copy — see
Shared memory).