Design
Architecture, render/update loop, and runtime model
Design
Spoonbill is a server-side SPA framework built on top of Avocet. The browser runs a small JS bridge. The server owns the application state and renders UI updates.
High-level architecture
Spoonbill keeps the "real app" on the server. The client is responsible for:
applying DOM changes sent by the server
sending user events back to the server
Core runtime pieces
ApplicationInstance[F, S, M]: one running session. It owns the render loop, state stream, message stream, and theFrontend.Frontend[F]: parses incoming client messages into typed streams (domEventMessages,browserHistoryMessages) and sends DOM changes back.ComponentInstance[F, ...]: applies the Avocet document to the render context, collects event handlers, and runs transitions.Effect[F[_]]: Spoonbill abstracts over the effect type (Future / cats-effect / ZIO / etc).
Render/update loop
When state changes, Spoonbill re-renders and computes a diff using Avocet. The server then ships a compact "DOM patch" to the browser.
Deterministic ids and "outdated DOM" protection
Spoonbill relies on Avocet's deterministic ids (like 1_2_1) for:
mapping DOM events back to server-side handlers
ensuring events from an outdated DOM are ignored
The client attaches an eventCounter to each event. The server tracks counters per (targetId, eventType) and only accepts events that match the current counter. After handling a valid event, it increments the counter and tells the client the new value.
Startup paths (why dev mode is special)
ApplicationInstance.initialize() has two important startup modes:
pre-rendered page: the browser already shows the initial DOM, so Spoonbill registers handlers and starts streams without needing an initial diff.
dev mode saved render-context: when
spoonbill.dev=trueand a saved render-context exists, Spoonbill loads it and diffs against it to update the browser after reloads.