Motivation
Server signals and Store signals provide reactive server-side values. However, without a way to surface them in views, they can only be consumed manually over raw WebSocket routes — which requires boilerplate and leaves the frontend unaware of the reactive nature of the data.
This proposal closes that gap: when a server signal is passed as a prop to response.view, Primate automatically uses its current value for SSR and opens a WebSocket connection to keep the prop live on the client. From the frontend's perspective, the prop is just a normal reactive prop — no special API, no useSignal, no createSignal. The framework's existing prop reactivity does the rest.
This proposal, together with Store signals, fully replaces the earlier @primate/live concept at #62 .
Proposed Solution
1) Signal detection in response.view
When response.view is called with props, it inspects each prop value. If a prop implements ReadableSignal — detected via a well-known symbol or interface — it is treated as a live prop:
import response from "primate/response";
import Post from "#store/Post";
const livePosts = Post.signal({ sort: { createdAt: "desc" }, limit: 20 });
route.get(() => response.view("Index.svelte", {
posts: livePosts, // ReadableSignal — treated as live prop
title: "My Blog", // plain value — passed through as-is
}));
Plain props are passed through unchanged. Signal props are resolved for SSR and kept live over WebSocket.
2) SSR with authoritative initial value
For each signal prop, response.view calls await signal.get() to resolve the current value before rendering. The initial HTML is always rendered with the authoritative latest known value — SSR integrity is never compromised:
// internally, before rendering:
const resolved = {
posts: await livePosts.get(), // authoritative async read
title: "My Blog", // plain, passed through
};
// render with resolved props
3) WebSocket connection and multiplexing
After SSR, Primate opens a single shared WebSocket connection per page for all live props. Each signal is identified by its prop key, which serves as the wire identity:
// client subscribes on connect
{ type: "subscribe", prop: "posts" }
// server pushes update when signal changes
{ type: "update", prop: "posts", value: [...] }
Multiple signal props share the same connection — Primate does not open one connection per signal.
4) Client-side prop update
When the client receives an update message, Primate's frontend binding passes the new value into the framework's normal prop update mechanism. No special client API is needed — the framework treats it as a regular prop change:
- Svelte: prop is a reactive
$prop, update triggers re-render automatically
- React: prop is passed into state,
setState triggers re-render
- Solid: prop flows into a signal, reactive graph updates automatically
- Vue: prop is reactive ref, template re-renders automatically
- Angular: prop flows into an
input() signal, reactive graph updates automatically
The frontend binding for each framework (@primate/svelte, @primate/react etc.) handles this transparently. From the component author's perspective, a live prop looks and behaves identically to a plain prop.
5) Connection lifecycle and ref-counting
The shared WS connection is opened when the first signal prop is active on a page, and closed when the last one is no longer needed. Primate's client runtime maintains a reference count per route:
- navigating to a page with signal props → open connection, subscribe to signals
- navigating away → unsubscribe all signals, close connection if ref count reaches zero
6) Wire protocol
The multiplexing protocol is minimal:
// client → server: subscribe to a signal prop
{ type: "subscribe", prop: string }
// server → client: push updated value
{ type: "update", prop: string, value: unknown }
// client → server: unsubscribe (on navigate away)
{ type: "unsubscribe", prop: string }
Values are JSON-serialized. The protocol is internal to Primate and not part of the public API.
Security
Signal props expose whatever the signal's value contains over WebSocket. The same access controls that apply to the route apply to the signal — if a route is behind a guard, the signal updates are also gated by that guard. Consumers are responsible for not passing sensitive data as signal props to unauthenticated routes.
Summary of changes
| Area |
Change |
response.view |
Detect ReadableSignal props, resolve via get() for SSR, register for WS push |
| WS multiplexing |
Single shared connection per page, prop key as wire identity |
| Client runtime |
Per-framework binding update to receive update messages and feed into prop system |
@primate/svelte, @primate/react, @primate/solid, @primate/vue @primate/angular |
Each updated to handle live prop updates transparently |
| Documentation |
Add "View Signals" section covering live props, SSR behaviour, and connection lifecycle |
| Tests |
Add tests for SSR resolution, WS connection lifecycle, multiplexing, and per-framework prop updates |
Open questions
- How is
ReadableSignal detected on a prop value — via a well-known symbol (e.g. Symbol.for("primate.signal")) or via duck-typing (checking for .get, .subscribe)? A symbol is more robust; duck-typing is more flexible.
- Should the wire protocol support batching — sending multiple prop updates in a single message — for efficiency when several signals update simultaneously?
- What is the reconnection strategy if the WebSocket drops mid-session?
Motivation
Server signals and Store signals provide reactive server-side values. However, without a way to surface them in views, they can only be consumed manually over raw WebSocket routes — which requires boilerplate and leaves the frontend unaware of the reactive nature of the data.
This proposal closes that gap: when a server signal is passed as a prop to
response.view, Primate automatically uses its current value for SSR and opens a WebSocket connection to keep the prop live on the client. From the frontend's perspective, the prop is just a normal reactive prop — no special API, nouseSignal, nocreateSignal. The framework's existing prop reactivity does the rest.This proposal, together with Store signals, fully replaces the earlier
@primate/liveconcept at #62 .Proposed Solution
1) Signal detection in
response.viewWhen
response.viewis called with props, it inspects each prop value. If a prop implementsReadableSignal— detected via a well-known symbol or interface — it is treated as a live prop:Plain props are passed through unchanged. Signal props are resolved for SSR and kept live over WebSocket.
2) SSR with authoritative initial value
For each signal prop,
response.viewcallsawait signal.get()to resolve the current value before rendering. The initial HTML is always rendered with the authoritative latest known value — SSR integrity is never compromised:3) WebSocket connection and multiplexing
After SSR, Primate opens a single shared WebSocket connection per page for all live props. Each signal is identified by its prop key, which serves as the wire identity:
Multiple signal props share the same connection — Primate does not open one connection per signal.
4) Client-side prop update
When the client receives an update message, Primate's frontend binding passes the new value into the framework's normal prop update mechanism. No special client API is needed — the framework treats it as a regular prop change:
$prop, update triggers re-render automaticallysetStatetriggers re-renderinput()signal, reactive graph updates automaticallyThe frontend binding for each framework (
@primate/svelte,@primate/reactetc.) handles this transparently. From the component author's perspective, a live prop looks and behaves identically to a plain prop.5) Connection lifecycle and ref-counting
The shared WS connection is opened when the first signal prop is active on a page, and closed when the last one is no longer needed. Primate's client runtime maintains a reference count per route:
6) Wire protocol
The multiplexing protocol is minimal:
Values are JSON-serialized. The protocol is internal to Primate and not part of the public API.
Security
Signal props expose whatever the signal's value contains over WebSocket. The same access controls that apply to the route apply to the signal — if a route is behind a guard, the signal updates are also gated by that guard. Consumers are responsible for not passing sensitive data as signal props to unauthenticated routes.
Summary of changes
response.viewReadableSignalprops, resolve viaget()for SSR, register for WS pushupdatemessages and feed into prop system@primate/svelte,@primate/react,@primate/solid,@primate/vue@primate/angularOpen questions
ReadableSignaldetected on a prop value — via a well-known symbol (e.g.Symbol.for("primate.signal")) or via duck-typing (checking for.get,.subscribe)? A symbol is more robust; duck-typing is more flexible.