Buffer reference
BufferReference is a zero-copy handle for moving ArrayBuffer bytes to
thread workers. Import it from the subpath:
import { BufferReference } from "knitting/unsafe";Move semantics
Section titled “Move semantics”Constructing a BufferReference detaches the source immediately. The
bytes now belong to the reference; reading or writing the original buffer
after construction returns zero-length or stale data.
const pixels = new Uint8Array([0, 64, 128, 192, 255]);
const ref = new BufferReference(pixels); // pixels.buffer is now detached
console.log(pixels.byteLength); // 0 — the source was movedconsole.log(ref.byteLength); // 5This is intentional: the move is what makes the transfer zero-copy.
Sending to a worker
Section titled “Sending to a worker”Use BufferReference as a task argument to move bytes to a thread worker
without serializing them through the transport:
import { createPool, isMain, task } from "knitting";import { BufferReference } from "knitting/unsafe";
export const invert = task<BufferReference, BufferReference>({ f: (ref) => { const pixels = ref.toUint8Array(); // the moved bytes, no copy const out = new Uint8Array(pixels.length); for (let i = 0; i < pixels.length; i++) out[i] = 255 - pixels[i]; return new BufferReference(out); // move the result back },});
if (isMain) { const pixels = new Uint8Array([0, 64, 128, 192, 255]); using pool = createPool({ threads: 1 })({ invert });
const result = await pool.call.invert(new BufferReference(pixels)); console.log([...result.toUint8Array()]); // [255, 191, 127, 63, 0]}Reading the bytes
Section titled “Reading the bytes”Two accessors materialize the moved region:
| Method | Returns | Notes |
|---|---|---|
toUint8Array() | Uint8Array | View over the moved bytes. |
toArrayBuffer() | ArrayBuffer | The underlying buffer. |
These views are backed by the reference, so use them while it is alive and let
using / release() clean up when you are done. In the default copy mode this
is ordinary lifecycle hygiene, not a safety hazard — the stricter rules only
apply to the borrow return mode below.
Disposing
Section titled “Disposing”BufferReference implements Symbol.dispose. Use using to release
automatically, or call release() explicitly:
{ using result = await pool.call.invert(new BufferReference(pixels)); const out = result.toUint8Array(); console.log([...out]);} // result is released hereAfter release() the reference no longer owns the bytes, so stop using any view
you took from it. In the default copy mode this is just normal cleanup; the only
mode where reading too late is an actual use-after-free is borrow on Deno and
Bun (see below).
Constraints
Section titled “Constraints”- Thread workers only. The handle is a process-local pointer. Sending a
BufferReferenceto a process worker throws. For cross-process sharing useProcessSharedBuffer(see Shared memory). ArrayBuffersources only.SharedArrayBuffercannot be detached and is rejected. SAB-backed typed-array views are also rejected.- One-shot. Each reference is used once. The worker materializes the bytes
with
toUint8Array()/toArrayBuffer(), and those values are borrowed for the duration of the call. Do not use them from fire-and-forget work after the task returns.
Return copy vs borrow
Section titled “Return copy vs borrow”When a worker returns a BufferReference, the host must take possession of the
bytes. The default is copy (safe on all runtimes):
using pool = createPool({ threads: 1, unsafe: { BufferReferenceReturn: "copy" },})({ invert });On Node.js the return is always zero-copy regardless of this setting — the
engine co-owns the backing store across threads, so there is no use-after-free
risk even with "borrow". On Deno and Bun, "copy" takes a single copy of the
returned bytes so they safely outlive the worker. Set it to "borrow" to skip
that copy:
import { BufferReferenceReturn } from "knitting/unsafe";
using pool = createPool({ threads: 1, unsafe: { BufferReferenceReturn: BufferReferenceReturn.Borrow },})({ invert });
{ using result = await pool.call.invert(new BufferReference(pixels)); const out = result.toUint8Array(); // borrowed — valid only while result lives console.log([...out]);} // result is released here; do not read out after this pointThe named constants BufferReferenceReturn.Copy ("copy") and
BufferReferenceReturn.Borrow ("borrow") are exported from knitting/unsafe.
As an Envelope body
Section titled “As an Envelope body”BufferReference can be the body of an Envelope, combining a JSON header
with zero-copy bytes:
import { Envelope } from "knitting";import { BufferReference } from "knitting/unsafe";
export const process = task< Envelope<{ op: string }, BufferReference>, Envelope<{ done: boolean }, BufferReference>>({ f: (env) => { const pixels = env.payload.toUint8Array(); const out = new Uint8Array(pixels.length); for (let i = 0; i < pixels.length; i++) out[i] = 255 - pixels[i]; return new Envelope({ done: true }, new BufferReference(out)); },});Disposing the envelope also disposes a BufferReference body. Disposing is a
no-op for ArrayBuffer and SharedArrayBuffer bodies.
See Payloads — Envelope for the full body type table.
When to reach for it
Section titled “When to reach for it”Below roughly 256 KiB the per-call pointer setup tends to cost more than just
copying through the shared transport. Reach for BufferReference only when the
copy cost of a large buffer to a thread worker actually matters in profiling.
For process workers, use ProcessSharedBuffer instead.
For smaller buffers, a plain ArrayBuffer or typed array is simpler and works
for both thread and process workers.