import type { StandardSchemaV1 } from "@nifrajs/core/server"; import type { BoundaryStates } from "./boundary.js"; import type { ClientRouteHooks } from "./manifest.js"; /** * Request header that asks a nifra route's GET to return just the loader data as JSON (instead of * the full HTML document). Set by client-side navigation; read by `createWebApp`'s GET handler. */ export declare const DATA_HEADER = "x-nifra-data"; /** * Global the server injects (`createWebApp({ prerenderedPaths })`) listing the SSG-prerendered paths. * The client's default data fetch reads it: a soft-nav INTO a prerendered route fetches its static * `/_data.json` (a CDN file - no worker round-trip) instead of the dynamic header-GET. */ export declare const PRERENDERED_GLOBAL = "__NIFRA_PRERENDERED__"; /** * Response header a data-mode action POST uses to convey a redirect (`redirect(...)`) to the * client - fetch would otherwise silently follow a 3xx to its HTML, losing the target. The * client reads this and performs a client-side navigation instead. */ export declare const REDIRECT_HEADER = "x-nifra-redirect"; /** * Response header an action sets (via the `revalidate(paths, data)` helper) to tell the client which * routes the mutation changed - a comma-separated list of paths. After the submit, the client marks * those cached routes stale (refetching any that are mounted) so a mutation can refresh views beyond * the active one. The client validates each path against the manifest matcher before acting on it. */ export declare const REVALIDATE_HEADER = "x-nifra-revalidate"; /** * Response header carrying a **terminal status** a loader signalled with `notFound()` / `gone()` / * `statusPage(n)` during a client-side navigation's data fetch. * * A soft-nav fetches data, not a document, so the server cannot answer by rendering the `_404` page - * it has only JSON to return. Without this channel the two halves disagree: a crawler doing a hard * navigation gets a correct 404 page while a user who clicked a link gets an error or a blank screen. * The client reads it and renders the same boundary the server would have. * * Carried in a header rather than the body so it survives the NDJSON deferred-data path unchanged, * exactly like {@link REDIRECT_HEADER}. */ export declare const STATUS_HEADER = "x-nifra-status"; /** * Layout indices a client navigation is asking the server NOT to re-run, comma separated. * * The client owns the decision because it holds both the old and new match plus each layout's scoped * params. The server treats it as a hint and refuses to skip a `gate` regardless - a guard that runs * only when the client says so is not a guard. */ export declare const RETAIN_HEADER = "x-nifra-retain"; /** * The path a client navigation is coming FROM, sent on the data-mode GET. * * Lets the SERVER decide which layout loaders to skip: it already knows each layout's scoped params * (derived at build time), so given both paths it can tell whether a layout's own prefix changed. * Doing it server-side means the client never needs the scope table, and the decision lives next to * the data it is about. * * Sent only when the client actually holds layout data to retain, so a request without it is simply * the full-chain case. */ export declare const NAV_FROM_HEADER = "x-nifra-from"; /** * The data-mode payload once a chain can carry layout data. * * Versioned because a prerendered `_data.json` is a static file on a CDN and outlives the deploy that * wrote it: a browser running new client code can be handed an envelope written by the previous * build. The reader therefore accepts BOTH the bare pre-envelope value and this shape, and decides by * structure rather than by assuming the deploy was atomic. */ export interface RouteDataEnvelope { readonly v: 1; readonly data: unknown; readonly layoutData?: readonly unknown[]; /** Indices whose loader was SKIPPED because the layout's own params did not change. The client * keeps its existing value at each of these, so an unchanged layout is neither refetched nor lost. */ readonly retained?: readonly number[]; /** Dynamic-boundary states returned alongside loader/layout data. */ readonly boundaries?: BoundaryStates; /** Terminal status signalled by a loader. Added by the browser transport after reading * `X-Nifra-Status`; old servers and static data files simply omit it. */ readonly status?: number; } /** Recognise the envelope without mistaking a plain loader object that happens to have a `data` key. */ export declare function isRouteDataEnvelope(value: unknown): value is RouteDataEnvelope; /** A URL matched against the manifest patterns: which route + its extracted params. */ export interface RouteMatch { readonly routeId: string; readonly params: Record; } /** An in-flight client submit - the action it targets + the `FormData` being sent. Set while the * submit is pending, cleared when it settles. A component reads `submission.formData` to render an * **optimistic** view (the expected result) before the server responds. */ export interface Submission { readonly action: string; readonly formData: FormData; } /** The router's observable state. A new object is published on every transition. */ export interface RouterState { readonly routeId: string; readonly params: Record; /** The current URL path (used to revalidate the active loader after an action). */ readonly path: string; readonly data: unknown; /** Per-layout loader data, aligned with the matched chain's leading layout prefix. Absent when no * layout in that chain has a loader. */ readonly layoutData?: readonly unknown[] | undefined; /** Neutral named-boundary states for the active route. */ readonly boundaries?: BoundaryStates; /** An action's data return after a client-side submit (cleared on navigation). */ readonly actionData?: unknown; /** True while a navigation or submit is in flight (drives loading UI). */ readonly pending: boolean; /** The path a navigation is transitioning TO while `pending` (cleared when it settles). Lets a * `NavLink` know whether its own `to` is the one loading; `undefined` when idle. */ readonly pendingPath?: string | undefined; /** The in-flight submit (set during a `submit`, cleared when it settles) - for optimistic UI. */ readonly submission?: Submission; } /** A route id paired with its nifra pattern (e.g. `":id"` segments) - the matcher input. */ export interface RoutePattern { readonly routeId: string; readonly pattern: string; } /** * Build a matcher from route patterns (built from the SAME manifest the server routes from, so * client and server agree). Returns the first matching route + decoded params, or null. The * query string is ignored for matching (it is not part of the route pattern). */ export declare function createMatcher(patterns: readonly RoutePattern[]): (path: string) => RouteMatch | null; /** How a router fetches a route's loader data on navigation. `signal` aborts a superseded fetch * (and its deferred stream). */ export type FetchRouteData = (path: string, match: RouteMatch, signal?: AbortSignal, navigation?: { readonly from: string; readonly retain: readonly number[]; }) => Promise; /** Per-submit options. `revalidate: false` opts out of the post-action loader re-fetch. */ export interface SubmitOptions { /** Re-run the active route's loader after the action settles (default `true`). Set `false` to * keep the current `data` and rely on the action's `actionData` alone. */ readonly revalidate?: boolean; } /** A fetcher's observable state - independent of the main router. `pending` covers its in-flight * load/submit; `data` is its last `load()` result; `actionData` its last `submit()` result; * `submission` the in-flight submit (for optimistic UI). Client-only (never SSR'd). */ export interface FetcherState { readonly pending: boolean; readonly data: unknown; readonly actionData?: unknown; readonly submission?: Submission; } /** * An independent load/submit state machine, retrieved by `router.fetcher(key)`. Runs **concurrently** * with the main router and with other fetchers - each is single-flight against *itself* (its own * monotonic generation), so N row-level mutations / side-channel loads can be in flight at once without * disturbing the active view. Loads/submits write the shared cache and honor `X-Nifra-Revalidate`. */ export interface Fetcher { /** Current state; stable reference between transitions. */ snapshot: () => FetcherState; /** Subscribe to this fetcher's transitions; returns an unsubscribe fn. */ subscribe: (listener: () => void) => () => void; /** Load a route path's loader data into this fetcher's own `data` (also writes the shared cache). * A no-op for an unmatched path. */ load: (path: string) => Promise; /** Submit an action into this fetcher's own state; honors `X-Nifra-Revalidate` by refreshing the * active route + any mounted fetcher showing a changed path. Rejects on failure (caller falls back). * (No `revalidate` opt-out - a fetcher has no active loader of its own to skip.) */ submit: (action: string, body: NonNullable) => Promise; } /** The agnostic router store consumed by per-adapter Router bindings. */ export interface ClientRouter { /** Current state; stable reference between transitions (so `useSyncExternalStore` can bail). */ snapshot: () => RouterState; /** Subscribe to transitions; returns an unsubscribe fn. */ subscribe: (listener: () => void) => () => void; /** Navigate to a path: match → fetch loader data → publish. No-op for an unmatched path. */ navigate: (path: string) => Promise; /** * Submit an action (POST `body` to `action` in data mode): a redirect becomes a client * navigation; otherwise the data return is published as `actionData` and the active route's * loader is revalidated so the mutation is reflected. Pass `{ revalidate: false }` to SKIP that * revalidation - keep the current `data` and just publish the action's `actionData` (useful when * the action already returned everything that changed, saving the extra round-trip). A redirect * always loads its target regardless. Rejects on failure (caller falls back). */ submit: (action: string, body: NonNullable, opts?: SubmitOptions) => Promise; /** Run the initial route's client loader after the adapter has hydrated the SSR markup. */ hydrate: () => Promise; /** * Mark cached route data stale and refresh the active view. With `paths`, target exactly those * (e.g. the routes a mutation changed); without, invalidate the whole cache. The active route * refreshes immediately - refetched + republished - whenever it's in scope (an explicit list that * includes it, or an invalidate-all); other stale entries refetch lazily when next read (a * fetcher, or the next navigation/access). Rejects if the active refetch fails (like `navigate`). * The keyed substrate for targeted revalidation (the `X-Nifra-Revalidate` header) and fetchers. */ invalidate: (paths?: readonly string[]) => Promise; /** * Warm a path's chunk + loader data into a bounded one-shot cache without publishing state - * a later `navigate` to it transitions with no network round-trip. Best-effort: failures and * unmatched paths are no-ops. Wired to link hover/focus by `installHistory`. */ prefetch: (path: string) => Promise; /** * Get (lazily creating) the stable {@link Fetcher} for `key` - an independent, concurrent * load/submit state machine for row-level mutations or side-channel loads that must not disturb * the active view. The same `key` always returns the same fetcher (so a binding can subscribe to a * stable store). Keys are app-chosen and typically stable (e.g. a row id). */ fetcher: (key: string) => Fetcher; /** All live fetchers - for a global busy view (e.g. a `useFetchers` binding). */ fetchers: () => readonly Fetcher[]; /** Subscribe to any-fetcher-changed (a transition on any fetcher, or a new one created) - backs a * `useFetchers` binding; returns an unsubscribe fn. */ subscribeFetchers: (listener: () => void) => () => void; /** Match a path against the manifest patterns (exposed for history/link wiring). */ match: (path: string) => RouteMatch | null; } export interface ClientRouterOptions { readonly patterns: readonly RoutePattern[]; readonly initial: RouterState; /** Override the loader-data fetch (tests inject a stub; defaults to a same-origin JSON GET). */ readonly fetchData?: FetchRouteData; /** Ensure a route's code chunk is loaded before rendering (code-splitting). Awaited in parallel * with the loader data, so `pending` covers both. Omit when the bundle isn't split. */ readonly loadModule?: (routeId: string) => Promise; /** Terminal status → client route id. Generated entries populate this from `_404` and * `_` files so soft navigation renders the same boundary as a hard request. */ readonly statusRoutes?: Readonly>; /** routeId → the route's client-only search keys (its `searchClientKeys` export). When a soft * navigation stays on the same route + pathname and changes ONLY these keys, `navigate` publishes the * new URL WITHOUT re-running the loader (re-render, not revalidate). Populated lazily by the generated * entry's `loadModule` (so a route's keys are present once it has been visited, which is exactly when a * same-route nav can consult them). Omit ⇒ every search change revalidates (the safe default). */ readonly searchClientKeys?: Readonly>; /** routeId → client-only loader/action hooks, populated by the generated route entry. */ readonly routeHooks?: Readonly>; } /** Options for a per-adapter `mountRouter` (the Router binding that hydrates + re-renders). */ export interface MountRouterOptions { readonly router: ClientRouter; /** routeId → layout chain (outermost layout → page); built by `generateClientEntry`. */ readonly routes: Record; /** routeId → the route's search-schema CHAIN (its layout chain's `searchSchema` exports, outermost * first, then the page's; each entry `undefined` when that module declares none); built by * `generateClientEntry`. The mount derives each route's typed `search` from this chain plus the URL via * `searchOfChain` (layout keys merged with page keys, page-wins), matching the server's * `ctx.search`/`RenderProps.search`. Omitted by callers with no typed search (tests, a hand-built mount) * ⇒ every route sees the raw parsed query. */ readonly searchSchemas?: Readonly>; /** Hydration container (opaque - the adapter casts it to its DOM element type). */ readonly container: unknown; } export declare function createClientRouter(options: ClientRouterOptions): ClientRouter; //# sourceMappingURL=router.d.ts.map