<div align="center">

# `@playfast/reform-remote`

**Run a reform scene's logic on the server; render its UI on a thin client over a transport.**

</div>

---

A fourth consumer of a reform [`Scene`](https://www.npmjs.com/package/@playfast/reform), alongside
[`@playfast/reform-react`](https://www.npmjs.com/package/@playfast/reform-react),
[`@playfast/reform-react-native`](https://www.npmjs.com/package/@playfast/reform-react-native), and
[`@playfast/reform-proof`](https://www.npmjs.com/package/@playfast/reform-proof). State, events, reducers,
async/remote data, and compositions all run **server-side**; the client receives a serialized
tree of rendered UI contracts and renders them with local presentations. The wire carries only
data — UI-tree patches one way, trigger invocations the other — so there is no API layer to write.

This builds on reform's existing seams: the **`ui` contract** already separates logic from
presentation, the **`CaptureSink`** already serializes the rendered surface headlessly (the same
mechanism proofs use), and the **schema-first `ui`** form (`ui(name, { props, events })`) carries
the wire schemas that make props and trigger payloads typed _and_ runtime-validated at the seam.
See [`REMOTE_UI.md`](../../REMOTE_UI.md) for the design.

## Install

```sh
npm install @playfast/reform-remote @playfast/reform effect react
```

Add a transport adapter for the wire you want — `inMemoryTransportPair` ships here; for real
sockets pair with [`@playfast/reform-remote-node`](https://www.npmjs.com/package/@playfast/reform-remote-node),
[`@playfast/reform-remote-bun`](https://www.npmjs.com/package/@playfast/reform-remote-bun), or
[`@playfast/reform-remote-web`](https://www.npmjs.com/package/@playfast/reform-remote-web).

## Key concepts

| Concept                                                         | What it does                                                                                                                    |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `makeRemoteServer(scene)`                                       | Renders a `Scene` to a `WireTree`; `render()`/`renderDiff()` emit full tree/patches; `invoke(handle, payload)` fires a trigger. |
| `renderWireTree(tree, { views, invoke })`                       | Folds a `WireTree` back into React using local presentations.                                                                   |
| `remoteContract({...})` / `remoteViews<C>({...})`               | The trpc-style typesafe seam — server declares the contract, client implements exactly it.                                      |
| `serve({ scene, transport })` / `connect({ transport, views })` | Bind both ends to any `RemoteTransport`.                                                                                        |
| `inMemoryTransportPair()`                                       | In-process duplex `RemoteTransport` (the simplest concrete adapter).                                                            |
| `<RemoteUI transport views />` / `useRemoteUI`                  | React binding that owns connect, subscription, re-render, teardown.                                                             |
| `useConnectionStatus(reporter)`                                 | Reads a transport's live `StatusReporter` status.                                                                               |

## How it fits together

```
            server                                   client
  ┌────────────────────────┐                ┌──────────────────────────┐
  Scene ─► makeRemoteServer ─► WirePatch[] ──►  Wire.apply ─► renderWireTree
   ▲          (renders,                          (folds tree)   (local views)
   │           encodes props,                                        │
   │           registers triggers)                                   ▼
   └──────────── invoke(handle, payload) ◄──── event callback fires ─┘
```

- **`makeRemoteServer(scene)`** — renders the scene to a `WireTree`, encoding each contract's
  props via its schema and registering triggers behind stable `${nodeId}:${event}` handles.
  `render()` / `renderDiff()` produce a full tree / patches; `invoke(handle, payload)` decodes the
  payload and fires the trigger (a `High`-priority dispatch into the Bus).
- **`renderWireTree(tree, { views, invoke })`** — turns a `WireTree` back into React using the
  presentations in `views` (the bundled vocabulary), reconstituting event props as callbacks.
- **`remoteContract({...})` / `remoteViews<AppContract>({...})`** — the trpc-style typesafe seam.
  The SERVER declares its UI shape ONCE and exports `typeof` it; the CLIENT imports only that TYPE
  and implements it. `remoteViews<AppContract>` type-checks the client's view set to implement
  EXACTLY the server's contracts — a missing, extra, or wrong-contract view is a compile error.
  Each view is a plain `Ui.make` (props, slots, events all typed from its contract); the render
  name + props schema come from the contract, so they can't drift. The result is a branded
  `RemoteViewSet<AppContract>` that `connect`/`renderWireTree`/`<RemoteUI>` accept (an unbranded
  `Record` is rejected — the contract check is end-to-end, with no widening):
  ```ts
  // server (or shared) — the AppRouter analog:
  export const AppContract = remoteContract({ TodoApp: TodoAppUi, TodoItem: TodoItemUi })
  export type AppContract = typeof AppContract

  // client — imports the TYPE only:
  import type { AppContract } from '…/contracts'
  const TodoItem = Ui.make(TodoItemUi, (props, _slots, events) => (
    <li onClick={() => events.toggle({})}>{props.text}</li> // props/events fully typed
  ))
  export const views = remoteViews<AppContract>({ TodoApp: /* … */, TodoItem })
  ```
- **`serve({ scene, transport })` / `connect({ transport, views })`** — bind both ends to any
  `RemoteTransport` (WebSocket, postMessage, in-memory). The server sends a `Snapshot` (full tree,
  replace) on connect and `Patches` (apply) thereafter, so a reconnecting client re-syncs cleanly.
- **`inMemoryTransportPair()`** — the in-process duplex adapter the tests drive `serve`/`connect`
  over without a socket; the simplest concrete `RemoteTransport`.
- **`<RemoteUI transport views />` / `useRemoteUI(transport, views)`** — the React binding. It owns
  the whole client side — `connect`, the patch subscription, re-render on each frame, and teardown
  on unmount — so the call site holds no `useSyncExternalStore` or `binding.node()`. Pair it with
  **`useConnectionStatus(transport)`** to read a transport's live status (e.g. a reconnecting badge):
  ```tsx
  const App = () => {
    const status = useConnectionStatus(transport) // 'connecting' | 'open' | 'reconnecting' | 'closed'
    return (
      <>
        {status !== 'open' ? <div className={`conn ${status}`}>{status}…</div> : null}
        <RemoteUI transport={transport} views={views} />
      </>
    )
  }
  ```

## Transport adapters

| Package                                                                                      | Role                                       | Built on           |
| -------------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------ |
| `inMemoryTransportPair` (here)                                                               | in-process duplex                          | —                  |
| [`@playfast/reform-remote-node`](https://www.npmjs.com/package/@playfast/reform-remote-node) | WebSocket server                           | `ws`               |
| [`@playfast/reform-remote-bun`](https://www.npmjs.com/package/@playfast/reform-remote-bun)   | WebSocket server                           | `Bun.serve`        |
| [`@playfast/reform-remote-web`](https://www.npmjs.com/package/@playfast/reform-remote-web)   | WebSocket client (factory, auto-reconnect) | global `WebSocket` |

## License

MIT
