import { type MountTarget } from './mount.js'; import type { Renderable } from './element.js'; import { type BindingError } from './runtime.js'; import type { Signal } from './types.js'; declare global { interface ImportMeta { env?: { DEV?: boolean; MODE?: string; }; } } /** The bag's `state` is a `Signal` so authored handler code reads it the same * way as the view (`state.at('x').peek()`). At runtime it's a read handle: `.at` * narrows, `.peek` reads the current value; `.map` is a view-build-time concept * and throws if reached on the handle. */ export type StateHandle = Signal; export interface ComponentBag { state: Signal; send: (msg: M) => void; /** Coalesce a burst of `send`s into ONE reconcile (see the handle's `batch`). * Reducers/effects still run per message; only the DOM commit is deferred to the * outermost `batch` exit. Use it to drain a burst of dispatches (e.g. a stream * frame) from a handler/subscription as a single re-render. */ batch: (fn: () => void) => void; } export interface EffectApi { send: (msg: M) => void; state: Signal; /** Coalesce a burst of `send`s into ONE reconcile (see {@link ComponentBag.batch}). */ batch: (fn: () => void) => void; /** This mount's lifecycle {@link AbortSignal}. Aborted exactly once, on THIS * mount's `dispose()`. Each mount of a definition owns a distinct signal, so an * effect handler can key per-mount resources off it — pass it to `fetch`, or * `signal.addEventListener('abort', cleanup)` — and disposing one mount never * aborts a concurrent mount of the same definition. Prefer this over a * definition-level controller for anything whose lifetime is the mount's. */ signal: AbortSignal; } export interface SignalComponentDef { /** optional component name (for the debug registry / agent identity) */ readonly name?: string; /** initial state, optionally with initial effects */ init: () => S | [S, E[]]; /** pure reducer; returns the next state, optionally with effects. A bare `S` * (non-tuple) return is accepted for convenience. */ update: (state: S, msg: M) => [S, E[]] | S; /** build the view once; reactive reads are signal bindings (they don't close * over `state`). The bag's `state` handle is for handlers/effects. */ view: (bag: ComponentBag) => Renderable; /** handle an effect; may return a cleanup function */ onEffect?: (effect: E, api: EffectApi) => void | (() => void); /** discriminated-union schema of Msg ({ discriminant, variants }) */ readonly __msgSchema?: object; /** discriminated-union schema of Effect */ readonly __effectSchema?: object; /** state shape schema */ readonly __stateSchema?: object; /** per-message JSDoc annotations (intent, affordability, …) */ readonly __msgAnnotations?: Record; /** stable hash of the schemas, for hot-reload schema-change detection */ readonly __schemaHash?: string; /** dev-only source location */ readonly __componentMeta?: { file: string; line: number; }; } export interface SignalComponentHandle { send(msg: M): void; /** Coalesce a burst of `send`s into ONE reconcile + commit. Every message's * reducer still runs in order (state advances message-by-message, effects fire * per message), but the DOM reconcile + subscriber notification are deferred to * a single pass against the FINAL state when the outermost `batch` returns. * For N synchronous sends this turns N reconciles into 1 — the streaming / * bulk-dispatch fast path (e.g. draining a websocket frame of ticks). State is * applied by the time `batch` returns, so the synchronous-`send` contract holds * at the batch boundary. Nested `batch` calls flush only at the outermost exit. */ batch(fn: () => void): void; getState(): S; /** no-op: signal `send` applies updates synchronously (kept for harness/agent * parity with the legacy handle). */ flush(): void; /** run all pending effect cleanups (subscriptions etc.) */ dispose(): void; /** Register a listener called synchronously after every update cycle that * changes state, with the new state. Returns an unsubscribe. No-op after * dispose. Backs the agent protocol's state-update frames. */ subscribe(listener: (state: S) => void): () => void; /** Run the reducer in isolation against the current state — `{state, effects}` * with no commit/flush/effect dispatch. Backs the agent's `would_dispatch`. */ runReducer(msg: M): { state: S; effects: unknown[]; } | null; /** Snapshot the Msg variants dispatchable from currently-rendered UI (live * `tagSend` registrations). Backs the agent's `list_actions`. */ getBindingDescriptors(): Array<{ variant: string; }>; /** Hot-swap the reducer (and optionally onEffect) without rebuilding the DOM — * the HMR escape hatch for pure update.ts edits. State-type erased at this * boundary (`unknown`) so the handle stays assignable across state types. */ swapUpdate(newUpdate: (state: unknown, msg: unknown) => [unknown, unknown[]] | unknown, newOnEffect?: unknown): void; /** Install a hook called when a binding accessor throws during the update * cycle; the runtime leaves the binding's DOM at its prior value and continues * with siblings. Backs the agent's dispatch-envelope `drain.errors`. */ setOnBindingError(hook: ((e: BindingError) => void) | null): void; } /** * Normalize an `init()` / `update()` result — a `[state, effects]` tuple or a * bare `state` (the convenience return) — to a `[state, effects]` pair. This is * the ONE place the `[S, E[]] | S` heuristic lives; component, SSR, and (later) * `@llui/test` / `@llui/vike` all route through it so the shape decision never * diverges. * * The heuristic: a 2-element array whose SECOND element is itself an array is * read as `[state, effects]`; anything else is a bare state with no effects. * * KNOWN AMBIGUITY (deliberately unchanged — dropping the bare-`S` convenience is * a repo-wide breaking ripple, out of scope here): a state that is ITSELF a * 2-tuple whose second element is an array (e.g. the whole state is * `[number, string[]]`) is mis-read as `[state, effects]`. Such a state must be * returned explicitly as `[state, []]`. */ export declare function normalizeUpdateResult(r: [S, E[]] | S): [S, E[]]; /** Options for `mountSignalComponent`. */ export interface MountSignalOptions { /** Hydrate over server-rendered DOM instead of a fresh mount: seed the loop * with `serverState` (what the server rendered with) and atomically REPLACE the * server HTML with the freshly-built client tree. init()'s effects are skipped * by default (the server pass already ran them) — opt back in with * `runInitEffects` for init()s gated to no-op on the server. */ hydrate?: { serverState: S; runInitEffects?: boolean; }; /** Seed state to mount with instead of `init()`'s result (adapters that derive * the seed externally, e.g. per-route data). init() still runs so its effects * are captured; only the returned state is overridden. Ignored when hydrating * (use `hydrate.serverState` there). */ initialState?: S; /** Context values to expose at the root of this build (see `runBuild`'s * `seedContexts`). `@llui/vike` replays a layout's in-scope contexts here so a * nested page reads providers that live above its slot in a SEPARATE build. */ contexts?: ReadonlyMap; /** Commit scheduling. `'sync'` (the default) commits the DOM + notifies * subscribers inside every top-level `send` — the synchronous contract. * `'raf'` is the OPT-IN streaming/burst fast path: reducers and effects * still run synchronously per send (state and `getState()` advance * immediately — the data contract holds), but the DOM commit + subscriber * notification coalesce to ONE reconcile per animation frame (microtask * fallback where rAF doesn't exist: SSR, plain jsdom, the headless agent). * The DOM therefore lags state by up to a frame; `handle.flush()` forces * the pending commit synchronously (tests, the agent protocol, a * read-after-write). Measured on the ticker suite's 1k-send burst: * 14.1ms per-send vs 5.9ms coalesced (hand-written-vanilla parity). */ scheduler?: 'sync' | 'raf'; /** Register this component in the global devtools registry * (`__lluiComponents` / `__lluiDebug`). Default `true` in dev. Set `false` * for self-introspecting dev tooling (e.g. an in-app debug HUD authored with * LLui) so it doesn't pollute the host app's component list that external * tools — the MCP server, agent bridge, debug-collector — read. */ devtools?: boolean; } /** Mount a signal component and drive its update loop. The target is a container * `Element` (fresh mount appends; hydration replaces) OR a `MountTarget` * descriptor — including `{ anchor }` for adapters mounting a nested layer as * siblings of a slot anchor. With `opts.hydrate`, takes over server-rendered * HTML (see MountSignalOptions). */ export declare function mountSignalComponent(target: Element | MountTarget, def: SignalComponentDef, opts?: MountSignalOptions): SignalComponentHandle; /** * Hydrate a signal component over server-rendered HTML in `container`. Builds the * client tree against `serverState` (matching the SSR render) and atomically * swaps it in — server HTML stays visible until the swap, so no flash. init()'s * effects are skipped by default (already run on the server); pass * `runInitEffects: true` for init()s that no-op on the server. */ export declare function hydrateSignalApp(target: Element | MountTarget, def: SignalComponentDef, serverState: S, options?: { runInitEffects?: boolean; contexts?: ReadonlyMap; }): SignalComponentHandle; //# sourceMappingURL=component.d.ts.map