Local feels immediate.
The client applies your action optimistically, then reconciles with the hub’s response. If the hub rejects a change, the optimistic update rolls back.
Build realtime apps around shared state.
Define it once. Keep every client in sync.
seq 000Starting the shared counter…
Your state and the actions that change it belong together. Write a pure reducer. Run it on the server and every connected client.
The hub commits the change.
Your clients follow the same logic.
Start with the Deno workspace setup, then add this channel definition.
import * as v from "valibot";import { durableChannel, durableRoutes } from "durable_channel";export const counter = durableChannel() .state(v.object({ count: v.number() }), { count: 0 }) .action((a) => a.name("counter/incremented") .payload(v.object({ by: v.number() })) .client() .reduce((state, { by }) => ({ count: state.count + by, })) ) .build();export const routes = durableRoutes() .route("counter://", counter).build();Your interface responds immediately. Behind the scenes, the hub gives every committed action a place in the sequence.
The client applies your action optimistically, then reconciles with the hub’s response. If the hub rejects a change, the optimistic update rolls back.
Validate the payload. Run the reducer. Persist the state. Broadcast the envelope. Each commit happens in order.
Reconnect to replay missed actions. If the gap exceeds the replay window, a fresh snapshot brings the client up to date. Your app decides when to reconnect.
Try disconnecting a clientA small vocabulary for the moving parts
of your realtime application.
Start with a channel. Build from there.