Quick Start
This quick start shows how to define tasks, create a worker pool, execute task calls, and shut down cleanly.
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: an exported function wrapped with
task({ f }). Tasks live at module scope so workers can load them consistently. call.*(): runs a task on the pool and returns aPromisewith the result.isMain: a safe boolean that’strueonly on the host (main thread). Use it to avoid running “host code” inside workers.createPool(): creates the worker pool and returns helpers likecallandshutdown.- Thread pool: the set of workers available to execute tasks.
- Inlining / inliner: optionally includes the host as an extra “lane”, so some work can run locally instead of always going to workers.
- Host ↔ Worker: the host is the process that creates the pool; workers are the spawned threads.
Examples
Section titled “Examples”Now that we share the vocabulary, the snippets should feel much more familiar.
import { isMain, task } from "@vixeny/knitting";
export const world = task({ f: (args: string) => args + " world",}).createPool();
if (isMain) { world.call("hello") .then(console.log).finally(world.shutdown);}import { createPool, isMain, task } from "@vixeny/knitting";
export const hello = task({ f: () => "hello ",});
export const world = task({ f: (prefix: string) => `${prefix}world!`,});
const { call, shutdown } = createPool({ threads: 2 })({ hello, world,});
if (isMain) { // Promise input is accepted: call.hello() resolves before world is dispatched. Promise.all(Array.from({ length: 5 }, () => call.world(call.hello()))) .then((results) => console.log(results.join(" "))) .finally(shutdown);}import { createPool, isMain, task } from "@vixeny/knitting";
export const add = task({ f: ([a, b]: [number, number]) => a + b,});
const { call, shutdown } = createPool({ threads: 2, inliner: { position: "last", batchSize: 4 },})({ add });
if (isMain) { const jobs = Array.from({ length: 8 }, () => call.add([1, 2])); Promise.all(jobs) .then((results) => console.log(results)) .finally(shutdown);}import { createPool, isMain, task } from "@vixeny/knitting";
export const double = task({ f: (n: number) => n * 2,});
export const square = task({ f: (n: number) => n * n,});
const { call, shutdown } = createPool({ threads: 2, inliner: { position: "first", batchSize: 2 },})({ double, square });
if (isMain) { const jobs = Array.from({ length: 5 }, (_, i) => i + 1).map(async (n) => { const d = await call.double(n); return call.square(d); });
Promise.all(jobs) .then((results) => console.log(results)) .finally(shutdown);}Workflow
Section titled “Workflow”-
Import Knitting:
import { isMain, task, createPool } from "@vixeny/knitting"; -
Define your tasks:
import { task } from "@vixeny/knitting";export const hello = task({f: () => "hello ",});export const world = task({f: (args: string) => args + "world!",}); -
Create a pool:
import { task, createPool } from "@vixeny/knitting";export const hello = task({f: () => "hello ",});export const world = task({f: (args: string) => args + "world!",});const { call, shutdown } = createPool({threads: 1, // default})({hello,world,});Or create it from a single task:
import { task } from "@vixeny/knitting";export const world = task({f: (args: string) => args + "world",}).createPool(); -
Only run host code on the host (
isMain), then call tasks:import { isMain, task, createPool } from "@vixeny/knitting";export const hello = task({f: () => "hello ",});export const world = task({f: (args: string) => args + "world!",});const { call, shutdown } = createPool({ threads: 1 })({hello,world,});if (isMain) {Promise.all(Array.from({ length: 5 }, () => call.world(call.hello())),).then(console.log).finally(shutdown);}call.world(call.hello())is intentional:call.hello()returns a promise, and Knitting resolves promise inputs on the host before dispatchingworld.Or the “single task pool” version:
import { isMain, task } from "@vixeny/knitting";export const world = task({f: (args: string) => args + "world",}).createPool();if (isMain) {world.call("hello ").then(console.log).finally(world.shutdown);} -
Shut down when you’re done.
Workers don’t magically disappear. Call
shutdown()to clean up and let the process exit.
Best practices
Section titled “Best practices”Make it once, keep it tidy.
Keep tasks in their own module(s)
Section titled “Keep tasks in their own module(s)”It keeps your app code clean and helps 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/
- …
Prefer request -> transform -> response tasks
Section titled “Prefer request -> transform -> response tasks”Knitting is tuned for local server workloads: parse a request body, validate it, render something, compress it, sign it, return a compact result. That is why so many examples focus on SSR, JWTs, markdown, and validation.
If you are building an HTTP service, start with those shapes before reaching for more exotic worker patterns. See Hono server routes.
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 useful in request handlers because you can pass body reads directly:
app.post("/validate", async (c) => { const result = await pool.call.validate(c.req.text()); return c.json(result);});This keeps handler code short and avoids extra staging variables. If the input promise rejects, the host call rejects and the worker task does not run.
Pick the cheapest payload type that fits
Section titled “Pick the cheapest payload type that fits”For hot paths, simpler types are better:
- Best:
number,boolean,null,undefined,Date, smallstring - Fast:
Buffer,ArrayBuffer, typed arrays likeUint8Array - Good: compact JSON-like
Object/Array - Heavier: large objects, large strings,
Errorpayloads
If you are moving bytes plus a little metadata, use Envelope:
import { Envelope } from "@vixeny/knitting";
const payload = new Envelope( { route: "/upload", contentType: "image/png" }, fileBuffer,);
await pool.call.processUpload(payload);That shape is a good fit for server-style binary work because it keeps headers small and the binary payload explicit. See Supported payloads.
Avoid the inliner for HTTP/server workloads
Section titled “Avoid the inliner for HTTP/server workloads”For web servers, start with workers only. The point is usually to keep the main
thread free for routing and I/O. The inliner adds the host as another compute
lane, which can be great for tiny math-heavy loops but can also put work back
onto the event loop you are trying to protect.
So the default advice is simple: do not turn on inliner first. Add it
later only if you have measured a workload where it clearly helps. See the
Performance guide and Inliner guide.
Worker permissions start conservative
Section titled “Worker permissions start conservative”Worker access is restricted by default. Sensitive paths such as .env, .git,
.npmrc, ~/.ssh, ~/.aws, and system paths like /proc, /sys, /dev,
and /etc are blocked, and node_modules is deny-write.
That is the right default for server code: start strict, then open only what your tasks actually need. See Permissions.