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.
Breaking a build-time cycle between two struct coders
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);
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.
Usage
import * as mod from "lazy/lazy.ts";