Skip to content

Quick Start

This quick start shows how to define tasks, create a worker pool, execute task calls, and shut down cleanly.

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.”

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 a Promise with the result.
  • isMain: a safe boolean that’s true only on the host (main thread). Use it to avoid running “host code” inside workers.
  • createPool(): creates the worker pool and returns helpers like call and shutdown.
  • 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.

Now that we share the vocabulary, the snippets should feel much more familiar.

hello_world.ts
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);
}
  1. Import Knitting:

    import { isMain, task, createPool } from "@vixeny/knitting";
  2. Define your tasks:

    import { task } from "@vixeny/knitting";
    export const hello = task({
    f: () => "hello ",
    });
    export const world = task({
    f: (args: string) => args + "world!",
    });
  3. 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();
  4. 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 dispatching world.

    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);
    }
  5. Shut down when you’re done.

    Workers don’t magically disappear. Call shutdown() to clean up and let the process exit.

Make it once, keep it tidy.

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.

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.

For hot paths, simpler types are better:

  • Best: number, boolean, null, undefined, Date, small string
  • Fast: Buffer, ArrayBuffer, typed arrays like Uint8Array
  • Good: compact JSON-like Object / Array
  • Heavier: large objects, large strings, Error payloads

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 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.