Buffer reference
BufferReference is useful when you need to send a large ArrayBuffer to a
thread worker without paying for a copy. It moves the bytes instead of
sharing them. Import it from the knitting/unsafe subpath:
import { BufferReference } from "knitting/unsafe";Moving the buffer
Section titled “Moving the buffer”Creating a BufferReference detaches the source immediately. The bytes now
belong to the reference, so the original view can no longer access them. For a
typed-array view, byteLength and length become zero; APIs that require an
attached ArrayBuffer may throw.
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); // 5That is the trade-off that makes the transfer zero-copy: after the move, there is only one owner of the bytes.
Send it to a worker
Section titled “Send it to a worker”Wrap the buffer and pass the reference as a task argument:
import { createPool, isMain, task } from "knitting";import { BufferReference } from "knitting/unsafe";
export const invert = task<BufferReference, BufferReference>({ f: (ref) => { const pixels = ref.toUint8Array(); const out = new Uint8Array(pixels.length); for (let i = 0; i < pixels.length; i++) out[i] = 255 - pixels[i]; return new BufferReference(out); },});
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”Use either accessor to read the bytes:
| Method | Returns | Notes |
|---|---|---|
toUint8Array() | Uint8Array | A view over the bytes. |
toArrayBuffer() | ArrayBuffer | An ArrayBuffer containing the bytes; a subview may require a copy. |
Both methods can be called more than once while the reference is active. Keep
the reference alive while you use the returned view or buffer, and call
release() when you are done. Releasing the reference detaches any views it
created first, so an escaped view becomes empty or throws instead of reading
freed memory.
Releasing the reference
Section titled “Releasing the reference”BufferReference implements Symbol.dispose, so using is usually the easiest
way to clean it up:
{ using result = await pool.call.invert(new BufferReference(pixels)); const out = result.toUint8Array(); console.log([...out]);} // result is released hereIf you are not using using, call release() yourself.
After release(), stop using any view you took from the reference. On runtimes
where the host and worker cannot safely keep the same backing store alive,
Knitting detaches those views before releasing the worker’s memory. If
detaching fails, it keeps the memory alive rather than risk a use-after-free.
Important constraints
Section titled “Important constraints”- Thread workers only. The handle refers to memory in the current process.
Sending it to a process worker throws. For cross-process sharing, use
ProcessSharedBuffer(see Shared memory). ArrayBuffer-backed views only.SharedArrayBuffercannot be detached and is rejected. SAB-backed typed-array views are also rejected.- The move is one-way. A reference may be read more than once while it is
active, but it cannot be used after
release(). Do not hand its view to a timer, stream, or other work that continues after the task.
BufferReference is intended for trusted, same-process code. It is not a
security boundary, so do not accept raw metadata or native pointers from
untrusted code.
Copying and borrowing returned buffers
Section titled “Copying and borrowing returned buffers”When a worker returns a BufferReference, Knitting must make those bytes
readable on the host. By default, it chooses the safe option. You can make that
choice explicit with BufferReferenceReturn: "copy":
using pool = createPool({ threads: 1, unsafe: { BufferReferenceReturn: "copy" },})({ invert });On Node 22/24 with the current native support, the host can share the returned
memory, so no copy is needed. On Deno, Bun, and Node builds without that
support, Knitting makes one safe copy. Choose "borrow" when you want to skip
that copy and can keep the result’s lifetime under control:
import { BufferReferenceReturn } from "knitting/unsafe";
using pool = createPool({ threads: 1, unsafe: { BufferReferenceReturn: BufferReferenceReturn.Borrow },})({ invert });
const input = new Uint8Array([0, 64, 128, 192, 255]);{ using result = await pool.call.invert(new BufferReference(input)); const out = result.toUint8Array(); // valid while result is alive console.log([...out]);} // result is released here; do not read out after this pointIf you prefer named constants, BufferReferenceReturn.Copy ("copy") and
BufferReferenceReturn.Borrow ("borrow") are exported from
knitting/unsafe.
Use it in an Envelope
Section titled “Use it in an Envelope”An Envelope can carry a BufferReference body when you need a JSON header
alongside binary data:
import { Envelope, task } from "knitting";import { BufferReference } from "knitting/unsafe";
export const processImage = 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. An
ArrayBuffer or SharedArrayBuffer body has nothing to dispose.
See Payloads — Envelope for the full body type table.
When to use it
Section titled “When to use it”For smaller buffers, the setup cost can outweigh the time saved by avoiding a
copy. Use BufferReference when profiling shows that copying large buffers is
actually a bottleneck; this is usually more relevant for buffers that are
hundreds of kilobytes or several megabytes in size.
For process workers, use ProcessSharedBuffer instead. For smaller payloads, a
plain ArrayBuffer or typed array is simpler and works with both worker types.