Not long ago we have entered the era of single-page applications. Some people say that we no longer need a server. They say that JavaScript applications can connect to DBMS directly. Fat clients. We disagree with this. This project is an attempt to solve the problems of modern fat web.
Korolev runs a single-page application on the server side, keeping in the browser only a bridge to receive commands and send events. The page loads instantly and works fast, because it does a minimal amount of computation. It's important that Korolev provides a unified environment for full stack development. Client and server are now combined into a single app without any REST protocol or something else in the middle.
- Lightning-fast page loading speed (~6kB of uncompressed JS)
- Comparable to static HTML client-side RAM consumption
- Indexable pages out of the box
- Routing out of the box
- Build extremely large app without increasing size of the page
- No need to make CRUD REST service
- Connect to infrastructure (DBMS, Message queue) directly from application
Korolev is a server-side SPA framework built on top of Levsha. The browser runs a small JS bridge. The server owns the application state and renders UI updates.
Korolev 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
flowchart LR
Browser["Browser<br/>Korolev JS bridge"] <-->|WebSocket| Frontend["Frontend"]
Frontend --> App["ApplicationInstance<br/>(one session)"]
App --> State["StateManager / StateStorage"]
App --> Render["Levsha DiffRenderContext<br/>render + diff"]
App --> Router["Router (optional)"]
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 Levsha document to the render context, collects event handlers, and runs transitions.Effect[F[_]]: Korolev abstracts over the effect type (Future / cats-effect / ZIO / etc).
When state changes, Korolev re-renders and computes a diff using Levsha. The server then ships a compact “DOM patch” to the browser.
sequenceDiagram
participant B as Browser
participant F as Frontend
participant A as ApplicationInstance
participant C as ComponentInstance
participant L as Levsha DiffRenderContext
B->>F: DomEventMessage(targetId, eventType, eventCounter)
F->>A: domEventMessages stream
A->>C: look up handlers by (targetId,eventType)
C->>C: transition() -> enqueue state update
A->>A: onState(...) render tick
A->>L: swap() + reset()
A->>C: applyRenderContext(...)
A->>L: finalizeDocument()
A->>F: performDomChanges(diff)
F->>B: apply DOM patch
Korolev relies on Levsha’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.
ApplicationInstance.initialize() has two important startup modes:
- pre-rendered page: the browser already shows the initial DOM, so Korolev registers handlers and starts streams without needing an initial diff.
- dev mode saved render-context: when
korolev.dev=trueand a saved render-context exists, Korolev loads it and diffs against it to update the browser after reloads.
flowchart TD
Init["initialize()"] --> Check{"dev mode saved?"}
Check -- yes --> Saved["load saved render-context<br/>diff + apply changes"]
Check -- no --> Pre["pre-rendered page<br/>no initial diff"]
Saved --> Streams["start dom/history/state streams"]
Pre --> Streams