# @wrnexus/csr ## Navigation state preservation Pages can opt into restoration across client navigation: ```wrn page Users { navigation { preserve = ["filters", "pagination", "scroll", "tabs", "expanded"] } } ``` Form-like categories restore named inputs, selects, and textareas. Password, file, hidden, CSRF/token/secret/credential fields, and elements marked `data-no-preserve` are never saved. For tab, expanded, or component UI state, mark stable elements with `data-wrn-preserve="key"`; their value and ARIA selected/expanded state are restored. State is scoped to pathname plus query. ## Typed server actions `createActionClient(route, name)` supports programmatic calls. Schema-backed WRN actions also export `__wrnexusActionClients`, whose input and output are inferred automatically. Enhanced forms expose `data-wrn-action-state="pending|success|error"` and dispatch bubbling `wrnexus:action:optimistic`, `:pending`, `:success`, and `:error` events. Success details contain returned data and invalidated cache tags; error details contain field errors. Without JavaScript, the same form posts to its page and receives a 303 redirect or accessible validation response. > The browser-side client runtime for WrNexus — generic, self-contained JS that hydrates server-rendered pages with reactivity, client-side navigation, and realtime rooms. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Overview `@wrnexus/csr` holds the three client runtimes that WrNexus serves to the browser. Components are authored as `.wrn` files and rendered on the **server**; this package provides the single, generic runtime that **hydrates** that HTML in the browser — there are no per-component browser bundles. Each runtime is exported as a plain-JS string (no build step, no imports) intended to be served verbatim from a well-known URL: - **reactive** at `/__wrnexus/reactive.js` — reactive directives (`data-scope`, `data-text`, `data-for`, …) - **nav** at `/__wrnexus/nav.js` — SPA-style client navigation with graceful fallback - **realtime** at `/__wrnexus/realtime.js` — WebSocket "rooms", declarative or programmatic The package itself runs on the server (it just returns strings); the strings it returns run in the browser. A dev/prod server (see `@wrnexus/core`) is responsible for actually serving them. ## Installation ```bash bun add @wrnexus/csr ``` > Private package — the machine must be authenticated to the `wrnexus` npm org > (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). ## API All exports come from the package root (`@wrnexus/csr`). The runtime source is delivered as strings, so the "API" on the server side is small; the real surface is the browser directives/globals each string installs. ### Runtime strings | Export | Type | Served at | Contents | | ------------------ | -------- | ------------------------ | ------------------------------ | | `REACTIVE_RUNTIME` | `string` | `/__wrnexus/reactive.js` | Reactive directive runtime | | `NAV_RUNTIME` | `string` | `/__wrnexus/nav.js` | Client-side navigation runtime | | `REALTIME_RUNTIME` | `string` | `/__wrnexus/realtime.js` | Realtime rooms runtime | ### Accessor functions Convenience getters that return the same strings. ```ts getReactiveRuntime(): string // → REACTIVE_RUNTIME getNavRuntime(): string // → NAV_RUNTIME getRealtimeRuntime(): string // → REALTIME_RUNTIME ``` ### Browser: reactive directives Applied to any subtree containing `data-scope`. Expressions are parsed by a tiny eval-free evaluator, so a strict CSP with no `unsafe-eval` works. | Directive | Purpose | | ---------------------------------- | ---------------------------------------------------- | | `data-scope="count: 0, name: 'x'"` | Declare reactive state on a subtree | | `data-on-="count++"` | Run a statement in scope on a DOM event | | `data-text="expr"` | Bind an element's `textContent` to an expression | | `data-show="expr"` | Toggle visibility while preserving interactive state | Compiled conditional rendering and dynamic component cases omit inactive elements from the live DOM. `data-show` is a visibility directive for stateful controls and keeps its element mounted. Neither mechanism is authorization: never place secrets in client-rendered branches. Authorize on the server and return only data the current request may access. | `data-for="item in list"` (opt. index and `key item.id`) | Per-item rendering; stable keys preserve DOM identity during reorder | | `data-key="item.id"` | Alternative key declaration for `data-for` templates | | `{{expr}}` or `{expr}` | Interpolation inside text nodes and attribute values | | `data-wrnexus-csr="id"` | Target for a generated CSR fetch binding (fetches `/__wrnexus/csr?...`) | Supported expression features: literals, identifiers, member access (`a.b`, `a[b]`), function/method calls, arrays, objects, arithmetic, comparison, equality, logical (`&& ||`), unary (`! - +`), and ternary. Statements support `++`/`--`, assignment operators (`= += -= *= /= %=`), and bare expression/method calls. Rendering is dependency-tracked: a signal change only re-runs the renderers that actually read it. Browser globals installed: `window.__wrnexusHydrateScopes(root)` and `window.__wrnexusHydrateCsrFetches(root)` — both idempotent, so re-running after a DOM swap or HMR morph is safe. Both run automatically on `DOMContentLoaded`. ### Browser: navigation Intercepts same-origin `` clicks, fetches the target page, and swaps the `#app` container in place (via `importNode` — not `innerHTML` — so it works under a Trusted-Types CSP), updating history, title, and scroll, then re-hydrates. Cross-origin links, modified clicks, `download`/`data-no-nav`/`rel="external"`/`target` links, non-HTML responses, or a missing `#app` fall back to a full browser navigation. - Programmatic navigation: `window.__wrnexusNavigate(url)` - Emits a `wrnexus:navigated` `CustomEvent` (`detail.url`) after each swap - Sends `x-wrnexus-nav: 1` on fetches so the server can return the page fragment - Appends any `/__wrnexus/*` runtime scripts the incoming page needs but the current document lacks ### Browser: realtime rooms Connects to `/realtime/` over WebSocket (`ws`/`wss` chosen from `location.protocol`). Two usage modes. Programmatic API via `window.wrn`: ```ts wrn.room(name): Room // open (or reuse) a room connection wrn.bindRooms(root?) // (re)bind declarative [data-room] containers interface Room { name: string; send(obj: object | string): Room; // JSON-stringifies objects; queues until open on(type: string, cb): Room; // filter by msg.type; "*" or a fn = all messages on(cb): Room; close(): Room; } ``` Internal lifecycle messages are emitted to listeners as `{ type }`: `__open`, `__close`, `__error`, and `__raw` (non-JSON frames, with `data`). Reconnect uses exponential backoff capped at 5s; queued sends flush on reconnect. Declarative binding (zero JS) on a `data-room=""` container: | Attribute | On | Purpose | | ------------------------------------ | --------------- | ------------------------------------------------------------------------ | | `data-room=""` | container | Connect to room `` | | `data-room-user=""` | container | Identify the connection (`?user=`) | | `data-room-log` | element | Where incoming messages are appended | | `