Lazily-built coder for mutually-recursive coder graphs.

lazy() defers calling its factory until the first encode/decode, then caches the result forever. This breaks the build-time recursion that happens when two or more coders reference each other (a needs b, b needs a): without lazy(), building either one eagerly walks into the other before it has a value to return, and JavaScript's temporal dead zone turns that into a ReferenceError (or, if the cycle is expressed through a factory function instead of a const, an unbounded RangeError: Maximum call stack size exceeded).

lazy() does not bound runtime recursion. Decoding a self-referential structure still recurses once per level actually present in the input; termination is the protocol's job (a discriminator that stops matching, or the buffer running out), not lazy()'s.

Examples

Breaking a build-time cycle between two struct coders

import { assertEquals } from "@std/assert";
import { struct, lazy, type Coder } from "@hertzg/binstruct";
import { u8 } from "@hertzg/binstruct/numeric";

interface Ping {
  kind: 0;
  next: Pong;
}
interface Pong {
  kind: 1;
  ttl: number;
}

// `pingCoder` needs `pongCoder` while building its own `next` field, but
// `pongCoder` is declared afterwards and does not exist yet at that point.
// Without `lazy()` this is a ReferenceError (reading `pongCoder` inside
// its own temporal dead zone) once the graph grows past two coders and
// closes an actual cycle, as it does for tunneling protocols.
const pingCoder: Coder<Ping> = struct({
  kind: u8() as unknown as Coder<0>,
  next: lazy(() => pongCoder),
});

const pongCoder: Coder<Pong> = struct({
  kind: u8() as unknown as Coder<1>,
  ttl: u8(),
});

const buffer = new Uint8Array(8);
const value: Ping = { kind: 0, next: { kind: 1, ttl: 64 } };

const written = pingCoder.encode(value, buffer);
const [decoded, read] = pingCoder.decode(buffer);

assertEquals(decoded, value);
assertEquals(written, read);

Functions

f
lazy<TDecoded>(factory: () => Coder<TDecoded>): Coder<TDecoded>

Wraps a coder factory so the coder it produces is built on first use instead of when lazy() is called, and only ever built once.