# @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<Input, Output>(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-<event>="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 `<a>` 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/<name>` over WebSocket (`ws`/`wss` chosen from `location.protocol`). Two usage modes.

Programmatic API via `window.wire`:

```ts
wire.room(name): Room          // open (or reuse) a room connection
wire.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="<name>"` container:

| Attribute                            | On              | Purpose                                                                  |
| ------------------------------------ | --------------- | ------------------------------------------------------------------------ |
| `data-room="<name>"`                 | container       | Connect to room `<name>`                                                 |
| `data-room-user="<id>"`              | container       | Identify the connection (`?user=<id>`)                                   |
| `data-room-log`                      | element         | Where incoming messages are appended                                     |
| `<template data-room-item="<type>">` | template        | Row template for messages of that `type` (empty = fallback)              |
| `%field%`                            | inside template | Placeholder filled from the message field (text/attr only, HTML-escaped) |
| `data-room-status`                   | element         | Reflects connection state text (`connected`/`disconnected`/`error`)      |
| `data-room-status-class`             | status element  | Base class; a state variant (`is-connected`, …) is appended              |
| `<form data-room-send>`              | form            | Submits named fields as a JSON message                                   |
| `data-room-reset`                    | form field      | Clears that field after send                                             |

Rebinds on `wrnexus:navigated` and closes rooms whose container has left the page.

## Usage

Server side — serve the runtime strings from your router (example with `Bun.serve`):

```ts
import { getReactiveRuntime, getNavRuntime, getRealtimeRuntime } from "@wrnexus/csr";

const routes: Record<string, string> = {
  "/__wrnexus/reactive.js": getReactiveRuntime(),
  "/__wrnexus/nav.js": getNavRuntime(),
  "/__wrnexus/realtime.js": getRealtimeRuntime(),
};

Bun.serve({
  fetch(req) {
    const body = routes[new URL(req.url).pathname];
    if (body) {
      return new Response(body, {
        headers: { "content-type": "text/javascript; charset=utf-8" },
      });
    }
    return new Response("Not found", { status: 404 });
  },
});
```

Browser side — server-rendered HTML that the reactive runtime hydrates:

```html
<div data-scope="count: 0, showPassword: false">
  <button data-on-click="count++">+1</button>
  <span data-text="count"></span>
  <p>Total: {{count}}</p>
  <input type="{showPassword ? 'text' : 'password'}" />
  <button
    data-on-click="showPassword = !showPassword"
    aria-label="{showPassword ? 'Hide password' : 'Show password'}"
  >
    Toggle password
  </button>
</div>
<script src="/__wrnexus/reactive.js"></script>
```

State interpolation in ordinary attributes is reactive. The compiler keeps the
initial SSR value and emits an internal binding so attributes such as `type`,
`aria-label`, `aria-pressed`, `class`, and `href` update after state changes.

A realtime chat, fully declarative:

```html
<div data-room="lobby" data-room-user="ada">
  <div data-room-status></div>
  <ul data-room-log></ul>
  <template data-room-item="chat"><li>%user%: %text%</li></template>
  <form data-room-send>
    <input name="text" data-room-reset />
    <input type="hidden" name="type" value="chat" />
    <button>Send</button>
  </form>
</div>
<script src="/__wrnexus/realtime.js"></script>
```

Or drive a room from code:

```ts
const room = wire.room("lobby");
room.on("chat", (msg) => console.log(msg.user, msg.text));
room.send({ type: "chat", user: "ada", text: "hi" });
```

## Requirements / Notes

- **Bun-only** on the server (the package integrates with Bun-based WrNexus servers); the emitted strings are plain browser JS with no dependencies.
- Browser runtimes are **self-contained** (no imports, no build step) and **idempotent**, so re-hydration after navigation or HMR is safe.
- Designed for a **strict CSP**: the reactive expression evaluator avoids `eval`/`new Function` (no `unsafe-eval`), and DOM swaps use `importNode`/attribute writes rather than `innerHTML` (Trusted-Types friendly).
- Peer packages: rendered `.wrn` components and the serving layer come from `@wrnexus/core` (the sole dependency); pages are rendered by the WrNexus dev/prod server.
