/** * Compositor -- merge multiple quantizers into composite state. * * The compositor aggregates discrete + blended state from all * active quantizers into a single CompositeState, producing * typed output channels (css, glsl, aria). * * Wired: DirtyFlags (selective recomputation), CompositorStatePool * (zero-allocation), FrameBudget (priority scheduling), microtask batching, * and RuntimeCoordinator (Plan + ECS-backed runtime bookkeeping). * * ZERO-ALLOCATION HOT PATH — zero RETAINED **and** zero TRANSIENT. The per-frame * compose body (`computeStateSync`) is plain JS that mutates a POOLED * {@link CompositeState} in place: it acquires a recycled state from * {@link CompositorStatePool}, refills a REUSED dirty-name scratch array (never * `Array.from`/`getDirty` — those minted an array per tick), and walks the phases * with index loops + no per-tick closures. The result is no RETAINED per-op * allocation: the live heap (the growth that survives a forced GC) stays flat at * ≈ 0 bytes/op — proven by the allocation gate (`tests/property/compositor-zero-alloc.test.ts`). * * The reactive publish that feeds `changes` is a RAW, synchronous fan-out over a * compositor-owned listener set (`changeListeners`): the publish is * `live.current = state; for (const notify of changeListeners) notify(state)` — no * `Effect` node, no PubSub linked-list node, no replay-buffer node, nothing * allocated per publish. (The prior `SubscriptionRef.set` publish was a measured * ≈ 22 B/op TRANSIENT floor — NOT the semaphore wrapper as once assumed, but the * `PubSub`/`ReplayBuffer` node that `SubscriptionRef` mints on every publish even * with no subscriber. Measured: `scripts/micro-publish-probe.mjs`.) When NO * `changes` subscriber is attached (the common compose tick) the listener set is * empty and the publish allocates nothing — genuine zero transient. A live * subscriber adds only the `Queue.offerUnsafe` enqueue cost of the * {@link Stream.callback} bridge (≈ 7 B/op), still a ~6× reduction. * * SINGLE-WRITER PRECONDITION (why the raw fan-out is safe + contract-preserving). * `SubscriptionRef.set` wraps its publish in a semaphore to make concurrent * writers atomic. The compositor has exactly ONE writer of the live state — the * synchronous `computeStateSync`, reached only from `add` / `remove` / `compute` / * the `scheduleBatch` microtask, all of which run to completion on the single JS * thread with no `await`/`yield`/fork inside the compose body. There is never a * concurrent second writer, so the semaphore's atomicity guarantee is MOOT and * the raw publish loses nothing — it preserves the `changes: Stream` * contract exactly (replay-current-on-subscribe + per-subscriber fan-out), just * without the per-publish allocation. Everything else (create/scope) is off the * hot path. * * @module */ import type { Scope } from 'effect'; import { Effect, Stream } from 'effect'; import type { Boundary } from './boundary.js'; import type { PolicyNode, RuntimeSite } from './document-graph.js'; import type { FrameBudget } from './frame-budget.js'; import type { Quantizer } from './quantizer-types.js'; import { RuntimeCoordinator } from './runtime-coordinator.js'; /** * Snapshot of the compositor's output per tick: discrete state names for each * quantizer, their blend-weight vectors, and the compiled per-target output * maps (`css` / `glsl` / `wgsl` / `aria`). * * `wgsl` mirrors `glsl` (a per-quantizer numeric channel keyed by the * quantizer's bare snake_case projection key). D0 carries the channel through * the state shape, the pool, and the worker emit; D1-WGSL adds the live * `emit-wgsl` runtime phase (below) that populates it from the state index, * escalation-gated on the `wgsl` target (admitted only at the `gpu` rung). */ export interface CompositeState { readonly discrete: Record; readonly blend: Record>; readonly outputs: { readonly css: Record; readonly glsl: Record; readonly wgsl: Record; readonly aria: Record; }; } /** * Options accepted by `Compositor.create`: pool capacity, optional * frame-budget gating, whether to enable speculative pre-evaluation, and an * optional escalation gate ({@link getPolicy} + {@link runtimeSite}). */ export interface CompositorConfig { readonly poolCapacity?: number; readonly frameBudget?: FrameBudget.Shape; readonly speculative?: boolean; /** * Escalation gate: resolve the {@link PolicyNode} (if any) that governs a * projection, keyed by the quantizer's compositor registry name (the same * `name` passed to `add()` — the compositor knows names, not graph projection * ids, so a host wiring graph projections maps id → name here). When a policy applies, the compositor * computes `chooseRung(policy, runtimeSite)` at `add` time and emits ONLY the * targets that rung admits (`admittedTargets`). A projection with NO matching * policy is pass-through (all targets emit). A policy that matches but admits * no rung (the `{ error }` branch — site not admitted, or budgets/grants * exhaust every rung) DENIES every target for that projection: a constraint * that cannot be satisfied must not silently emit at full capability. */ readonly getPolicy?: (projectionName: string) => PolicyNode | undefined; /** * The runtime site the escalation gate evaluates policies against. Defaults to * an environment hint: `'browser'` when a `window` global is present, else * `'node'`. Ignored unless {@link getPolicy} is supplied. */ readonly runtimeSite?: RuntimeSite; } interface CompositorShape { add(name: string, quantizer: Quantizer): Effect.Effect; remove(name: string): Effect.Effect; compute(): Effect.Effect; setBlendWeights(name: string, weights: Record): Effect.Effect; evaluateSpeculative(name: string, value: number, velocity?: number): void; scheduleBatch(): void; readonly changes: Stream.Stream; readonly runtime: RuntimeCoordinator.Shape; } interface CompositorFactory { create(config?: CompositorConfig): Effect.Effect; } /** * Compositor — the live merge point for every attached {@link Quantizer}. * * `Compositor.create` hands back a scoped Effect that, when run inside a * `Scope`, produces a compositor bound to a {@link RuntimeCoordinator}. Adding * quantizers, marking dirty flags, and emitting CSS/GLSL/ARIA outputs all flow * through the zero-allocation hot path backed by {@link CompositorStatePool}. * * @example * ```ts * import { Effect } from 'effect'; * import { Compositor } from '@czap/core'; * * const program = Effect.scoped(Effect.gen(function* () { * const compositor = yield* Compositor.create({ poolCapacity: 64, speculative: true }); * yield* compositor.add('viewport', viewportQuantizer); * const state = yield* compositor.compute(); * // state.discrete.viewport === 'tablet' * // state.outputs.css['--czap-viewport'] === 'tablet' * })); * ``` */ export declare const Compositor: CompositorFactory; export declare namespace Compositor { /** Structural shape of a live compositor instance. */ type Shape = CompositorShape; /** Alias for {@link CompositorConfig}. */ type Config = CompositorConfig; } export {}; //# sourceMappingURL=compositor.d.ts.map