RPC + reflection for Preact Signals and Models: access reactive model state and methods from a server (or worker/tab/etc) as if they lived on the client. Type-safe, minimal magic, and an optimized transport-agnostic protocol (WebSocket, SSE, postMessage, etc).
Installation:
npm install mixed-signalsThe only dependency is @preact/signals-core (>=1.8.0).
mixed-signals reflects server-side Preact Models and Signals (anything created via @preact/signals-core) to connected clients in real-time. Signals on the server are serialized with identity markers, and the client reconstructs them as local signals that stay in sync via a lightweight wire protocol.
- Server models use
createModel()fromsignal-wire/server(a thin wrapper around@preact/signals-core'screateModel) - Client models use
createReflectedModel()fromsignal-wire/clientto create local proxies that mirror server state - An RPC layer handles method calls (client → server) and signal updates (server → client)
- Delta compression for arrays (append), objects (merge), and strings (append) minimizes bandwidth
import { WebSocketServer } from "ws";
import { signal } from "@preact/signals-core";
import { RPC, createModel } from "mixed-signals/server";
const Todo = createModel((_text = "") => {
const text = signal(_text);
const done = signal(false);
const toggle = () => done.value = !done.value;
return { text, done, toggle };
});
type Todo = InstanceType<typeof Todo>;
const Todos = createModel(() => {
const all = signal<Todo[]>([]);
function add(text: string) {
const todo = new Todo(text);
all.value = [...all.value, todo];
return todo;
}
return { all, add };
});
type Todos = InstanceType<typeof Todos>;
const todos = new Todos();
const rpc = new RPC({ todos });
rpc.registerModel("Todo", Todo);
rpc.registerModel("Todos", Todos);
const wss = new WebSocketServer();
wss.on("connection", (ws) => {
const dispose = rpc.addClient({
send: ws.send.bind(ws),
onMessage: ws.on.bind(ws, "message"),
});
ws.on("close", dispose);
});import { useSignal } from "@preact/signals";
import { RPCClient, createReflectedModel } from "mixed-signals/client";
import type { Todo, Todos } from "./server.ts";
const TodoModel = createReflectedModel<Todo>(["text", "done"], ["toggle"]);
const TodosModel = createReflectedModel<Todos>(["all"], ["add"]);
const ws = new WebSocket("/rpc");
const rpc = new RPCClient({
send: ws.send.bind(ws),
onMessage: ws.addEventListener.bind(ws, "message"),
ready: new Promise((r) => ws.addEventListener("open", r, { once: true })),
}, {});
rpc.registerModel("Todo", TodoModel);
rpc.registerModel("Todos", TodosModel);
function Demo({ ctx }) {
const text = useSignal('');
function add(e) {
e.preventDefault();
ctx.todos.add(text.value);
text.value = '';
}
return <>
<ul>
<For each={todos.all}>
{todo => (
<li>
<input type="checkbox" checked={todo.done} />
{todo.text}
</li>
)}
</For>
</ul>
<form onSubmit={add}>
<input value={text} onInput={e => text.value = e.target.value} />
</form>
</>;
}
rpc.ready.then(() => {
render(<Demo ctx={rpc.root} />, document.body);
});Generated from TypeScript declarations.
- Kind: Function
- Signatures:
() => tuple— Creates two linked Transport instances for in-process communication. Messages sent on one end are delivered to the other via queueMicrotask.
- Kind: Function
- Signatures:
(factory: ModelFactory<TModel, TFactoryArgs>) => ModelConstructor<TModel, TFactoryArgs>
- Kind: Class
- Constructor:
new RPC(root?: any) => RPC
- Methods:
addClient(transport: Transport, clientId?: string) => () => voidaddUpstream(transport: Transport) => () => void— Register an upstream mixed-signals connection whose models are forwarded to downstream clients. All models from the upstream are automatically forwarded — no per-model declaration needed.expose(root: any) => voidnotify(method: string, params: any[], clientId?: string) => voidregisterModel(name: string, Ctor: ModelConstructor) => void
- Kind: Function
- Signatures:
(signalProps: string[], methods: string[]) => ModelConstructor<T, tuple>
- Kind: Class
- Constructor:
new RPCClient(transport: Transport, ctx?: any) => RPCClient
- Methods:
call(method: string, params?: any) => Promise<any>notify(method: string, params?: any[]) => voidonNotification(cb: (method: string, params: any[]) => void) => () => voidregisterModel(typeName: string, ctor: any) => void
- Properties:
ready: Promise<void>root: any
- Kind: Interface
- Methods:
onMessage(cb: (data: { toString: unknown }) => void) => voidsend(data: string) => void
- Properties:
ready: Promise<void>