io.eezo.live
Members list
Type members
Classlikes
Work declared now, applied later: the capability a component that calls out is built with.
Work declared now, applied later: the capability a component that calls out is built with.
async { work } runs work on its own virtual thread and posts the transition it returns through the page's mailbox — the topic-delivery path, so it is serialized with everything else, coalesced under load, and dropped without noise if the page has closed by the time it lands. work is ordinary blocking direct-style code: an outbound call through io.eezo.http.client.Http, a transact block, both.
The capability arrives by constructor, through Live.mount(request, async => new Weather(async)), so Component's three methods stay exactly as they are; Init extends this, so init can start a load and return a loading state. The page responds instantly and the data patches in. render must not call it: render runs per event, and work started there is work started per keystroke.
get and post are apply pre-composed with the HTTP client, shaped so the reaction is a total function over Reply's four arms: the compiler itself asks what happens when credentials die (design/live.md M6).
Attributes
- Source
- Component.scala
- Supertypes
- Known subtypes
-
class Init[S]
A live component: server-held state, rendered to HTML, updated over patches.
A live component: server-held state, rendered to HTML, updated over patches.
The mental model is a server template that re-renders itself (design/live.md §1): an event arrives from the browser or a Topic, the state moves, render runs again, and the differ ships the difference. The three methods are the three phases, and what each receives is what that phase may do — the split is enforced by the arguments, not by documentation:
- init runs once, at mount, and is the only place subscriptions exist: it receives the Init context, and
subscribeafterinitreturns throws.renderruns on every event, so a subscription there would be a duplicate per re-render. - handle moves the state. It runs on the page's own thread, one event at a time, so it needs no synchronisation of its own. Database work happens here the way it does in any handler: open a scope with
transact { ... }orread { ... }per event — the installedDatabasereaches every thread, and a connection captured across events is already a loud error (EscapedScope). - render is a pure function of the state and receives nothing else. Exactly one root element, in canonical form; the differ refuses anything else by naming the broken rule.
State is any immutable value the component chooses. It lives on the server, owned by one page thread; the browser only ever holds its rendering.
Attributes
- Source
- Component.scala
- Supertypes
(before, after) → patches, under the one contract that matters (design/live.md §1.1): applying the patches to a DOM holding before produces the DOM that rendering after would. The round trip harness in research/harnesses/live-roundtrip holds it against the real client applier; RoundTripSuite holds it against the reference applier on every generated case.
(before, after) → patches, under the one contract that matters (design/live.md §1.1): applying the patches to a DOM holding before produces the DOM that rendering after would. The round trip harness in research/harnesses/live-roundtrip holds it against the real client applier; RoundTripSuite holds it against the reference applier on every generated case.
The recursion prefers the finest patch that is still correct — a text edit over a subtree replace, an attribute set over either — because replacing a node destroys focus, caret, scroll and playback state the user can see (design/live.md §1.1 on §4.6). It bails to a coarser move at exactly two seams, each for a stated reason: a changed tag name or node kind is Patch.ReplaceNode, because no finer target survives the change; and a child list holding raw markup diffs as one Patch.SetChildren, because one Raw tree child can parse into any number of DOM nodes, so no sibling index past it can be trusted (design/live.md §2.2).
Child lists are diffed positionally: index against index, trailing surplus removed or appended. "Insert one row at the top" therefore patches every row, which is the cost design/live.md accepts until M5's keyed reconciliation; the keys are already carried, loudly validated, and diffed as ordinary attributes until then.
Attributes are compared as name → value maps, not as ordered lists: attribute order has no DOM meaning, setAttribute cannot reorder what exists, and a differ that chased order would replace nodes for a difference no browser can observe. For the same reason a bare attribute and an empty-valued one (disabled vs disabled="") are the same attribute here, as they are in the DOM.
Every function returns its patches; nothing accumulates by mutation. Concatenation order is the application-order contract Patch documents: within one child list, in-place updates first, then removals highest index first, then the append.
Attributes
- Source
- Differ.scala
- Supertypes
- Self type
-
Differ.type
Attributes
- Companion
- class
- Source
- Component.scala
- Supertypes
- Self type
-
Event.type
What the browser sent: the handler name from the event binding, and the payload the client collected (an input's value, a form's fields). Both are untrusted input; the wire layer caps and validates before this exists.
What the browser sent: the handler name from the event binding, and the payload the client collected (an input's value, a form's fields). Both are untrusted input; the wire layer caps and validates before this exists.
Attributes
- Companion
- object
- Source
- Component.scala
- Supertypes
The mount-time context: what Component.init may do beyond computing state.
The mount-time context: what Component.init may do beyond computing state.
subscribe registers a transition to run when a topic delivers: the message and the state at delivery time in, the next state out, applied on the page's own thread like any event. The window closes when init returns — the page seals the context — so the phase rule "subscriptions happen in init" is a thrown exception rather than a convention.
Attributes
- Source
- Component.scala
- Supertypes
The live layer's front door: mount in a handler, routes on the server, the event binding attributes in views.
The live layer's front door: mount in a handler, routes on the server, the event binding attributes in views.
A handler embeds a component wherever it wants in its own page:
def board(request: Request): Response =
Response.Ok(layout(Live.mount(request, Board())))
mount runs init, registers the page, and returns a fragment of two nodes: the anchor `
` holding the first render, and the `` that brings it to life. Carrying the script in the fragment is what lets http stay blind to live (design/live.md §2.3): it never scans a response for markers, because the mount is just a value in whatever the handler returns. A whole document goes out as written and content comes back inside the application's Layout; the script travels with the mount either way. The script's address is a plain string on purpose — the socket and the script are served unmounted under the reserved prefix, so a mounted page must not have them rewritten.
One mount per response in v1. A second mount renders (each is an ordinary value) but the client drives only the first anchor it finds; multi-mount is on the backlog.
The registry and the reaper are process-wide the way the installed Database is: built once, reached from the routes below, invisible to user code.
Attributes
- Source
- Live.scala
- Supertypes
- Self type
-
Live.type
The live layer, mixed into an http-layer application: everything Live.mount needs on the server, contributed through frameworkRoutes so every way of serving — boot's default, an overridden boot calling serve, dev — carries it without knowing.
The live layer, mixed into an http-layer application: everything Live.mount needs on the server, contributed through frameworkRoutes so every way of serving — boot's default, an overridden boot calling serve, dev — carries it without knowing.
The umbrella's EezoApp extends this, so an application on eezo mounts components with no configuration at all. An application on eezo-http alone opts in by writing extends LiveApp instead of extends HttpApp. The cost when unused is one WebSocket route, one script route, and an idle registry.
Attributes
- Source
- LiveApp.scala
- Supertypes
- Known subtypes
-
trait EezoApp
A tree the differ refuses: built by hand outside Tag.apply's canonicalisation, or nested in a way the HTML parser would restructure.
A tree the differ refuses: built by hand outside Tag.apply's canonicalisation, or nested in a way the HTML parser would restructure.
Attributes
- Source
- Canonical.scala
- Supertypes
-
trait Producttrait Equalsclass IllegalArgumentExceptionclass RuntimeExceptionclass Exceptionclass Throwabletrait Serializableclass Objecttrait Matchableclass AnyShow all
One DOM edit, as the differ emits it and the client applies it.
One DOM edit, as the differ emits it and the client applies it.
A patch names its target by a path of childNodes indices from the page's mount anchor, which is design/live.md's Option A: the tree the server diffs is canonical (Canonical), so tree child index equals DOM child index and the two sides count the same list. The anchor's children hold the component's root element at index 0, so every path into the component starts with 0. Payloads carry Html rather than rendered strings, so the reference applier in this module's tests can apply them structurally; Wire renders at the boundary, once.
expect is the integrity check (design/live.md §1.1, fail loud): the node name the patch believes sits at its path, "#text" for a text node. The client refuses a patch whose target disagrees, records it, and never applies it — the check exists to catch the exact bug this design historically breeds, both sides counting children differently. SetText carries none because its op admits only one node kind, which the applier checks by node type; the two Option[String] cases are None only at the anchor, which is found by page id, not by walking, so a walk of zero steps has nothing to verify.
Application order is part of the contract. The applier applies a frame's patches in list order, and the differ guarantees each patch's path is valid at its turn: within one positional child list it emits in-place updates first (counts unchanged), then removals from the highest index down (so earlier indices stay true), then a single append. Within one keyed child list the order is: removals (highest index first), then moves (each from/to read against the list as the previous move left it), then inserts at ascending final positions, then content patches addressed at final positions — structural before content, so every content path is true by the time it applies. DiffSuite pins both orders.
Attributes
- Source
- Patch.scala
- Supertypes
-
trait Enumtrait Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
An in-process publish/subscribe topic, typed by its message.
An in-process publish/subscribe topic, typed by its message.
A Topic[A] is a value the application creates and shares — a field on the app object, a per-entity lookup, whatever names it for both sides. Anything may publish: a handler, a component's handle, boot's background work. Only Component.init may subscribe, through Init, which is what keeps the subscription set from growing by one per re-render.
Delivery is a hand-off, never work: publish runs each subscriber's callback on the publisher's thread, and that callback — built by Init.subscribe — only enqueues into the page's mailbox. The re-render happens on the page's own thread (design/live.md §2.4), so a publisher holding a scarce resource (a database connection, mid-transaction) never renders pages with it.
Cross-node fan-out is a later implementation behind this same surface (design/live.md §2.8); nothing here names a transport.
Attributes
- Source
- Topic.scala
- Supertypes
The patch protocol's outbound half: what the server writes onto the socket.
The patch protocol's outbound half: what the server writes onto the socket.
One message kind so far. The envelope carries kind from the first message on, so the client dispatches on one field forever and a new kind is a new arm rather than a new format. Names are words, not initials: the byte budget is a goal, not a test (design/live.md §1.1), and a frame someone reads in DevTools should say what it is.
Html payloads are rendered here, at the boundary, and nowhere earlier: patches carry trees so the reference applier can apply them structurally, and the wire carries markup because that is what template.innerHTML on the other side wants. Every op spells path the same way, expect is the node name or null (null only at the anchor), and node payloads are always under html — the applier reads one shape, not seven.
Attributes
- Source
- Wire.scala
- Supertypes
- Self type
-
Wire.type