brumate

Page format

The server writes markers into the HTML and the state into scripts. The core reads both to resume the page. This page describes that contract, for contributors and for tools that read brumate pages.

The attributes that brumate writes start with b-, also the b-c-<hash> attribute of scoped styles. The compiler rejects an element attribute that starts with b-, and a spread writes one only when brumate made it.

Scope IDs

Every scope that client code needs gets an ID at render time. The server gives IDs in order, from the base of the render, which is 0 for a normal page. Scopes that the client creates get negative IDs, so they never match IDs that the server sends later.

Some bodies have IDs of their own, numbered from 0 after an ID and a dot. The scopes inside them, and the blocks and boundaries inside them, take their IDs from there:

In the state, an ID with a dot is a string, and other IDs are numbers.

#loading bodies get boundary IDs from a second counter, which also starts at the base. A boundary inside a #loading body gets an ID with a dot, from that body's counter.

Anchors

An anchor is a node that client code must find. Each anchor has an index within its scope, and is written as <scope>:<index>.

Elements

<button b-b="0:2" b-on="click=Counter_24o7ge#h4">+</button>

A reference is <component ID>#<export name>. The component ID is the component name and a hash of its path relative to the root. Handlers export as h<n>, and $client callbacks as c<n>.

Text

<!--t0:3-->1<!--/t-->

The value of a live insert is between the two comments.

When the insert is the whole content of an element, the element gets the anchor instead, and the value is its children:

<span b-b="1:1">One</span>

A <textarea> or a <title> has no comments, because their content is text, where a comment would be text too:

Blocks

<!--b0:4:1--><p>B</p><!--/b-->

Items

<!--b0:2--><!--i1--><li>…</li><!--/i--><!--i2--><li>…</li><!--/i--><!--/b-->

Each item of a live #for or #repeat is between <!--i<scope>--> and <!--/i-->. The number is the item's scope ID.

When the body of the item is one element and nothing else, the element's b-b marks the item instead. The element always gets an anchor, and its scope is the item's scope:

<!--b0:2--><li b-b="1:0">…</li><li b-b="2:0">…</li><!--/b-->

The core tells these elements from the elements of the fallback by their scopes. An item scope's parent is the scope of the list, and its kind is not one of the fallback's. A <p>, a heading, a table section, or a <tr> with content other than cells keeps the comments, because the browser can end it early and split the item.

#loading

<!--l0--><p>Loading…</p><!--/l-->

The server writes the fallback between <!--l<id>--> and <!--/l-->, where the number is the boundary ID, and renders the body after the rest of the page. When the body is ready, the stream sends:

<template b-l="0"><blockquote>…</blockquote></template>
<script>$bS(0)</script>
<script b-state b-scopes b-next="5">$bR(($R,$b)=>(…))</script>

If the body fails, the fallback stays.

In a #reveal (collapsed) block, a fallback that waits for its turn is in <template b-f="<id>"> between the markers. The script that shows the body before it also calls $bF(<id>), which moves the fallback out of its template. The first such script defines $bF.

A body whose values are ready when the block renders, and that renders no component, dynamic tag, template or prop, renders in place: no markers, no <template> and no swap script. Its scopes get the IDs of the boundary, such as 0.0, as they would in a stream, and they are in the page state.

The body renders with the values of the server. If a value changed in the browser before the body arrived, the core runs the updates of the body's scopes once, so the body shows the current values. Effects do not run.

A #loading body that sets the head after the first render also sends <template b-h="<id>"> with its head markup. $bH(id, title) moves it into the head and sets the title.

#defer

<template b-b="0:1" b-defer="visible:2:0"></template><!--l2--><p>Loading…</p><!--/l-->

When the trigger fires, the browser asks for the URL of the page with two headers:

x-brumate-defer: 2 0:1
x-brumate-base: 0

The server renders the route again with that base, and the #loading and #defer bodies whose IDs the boundary's ID starts with, such as boundary 0 for 0.0. It answers with the markup that it streams for a #loading body, with the header x-brumate-defer: 2. The browser adds it to the page and runs its scripts with the page's nonce. Its state records have b-root="d<boundary>", so their references are apart from the page's. The answer is 409 when the render puts boundary 2 at another anchor, and 404 when it has no such boundary.

#portal

<!--P4--><dialog open>Saved</dialog><!--/P-->

The content of a portal is between <!--P<scope>--> and <!--/P-->, where the number is the scope ID of the portal's body. The server writes it after the rest of the page. When the scope is removed, the client removes the content by its markers. A portal into a target has a <template b-client="load=…"> in its place, and the client renders it into the target.

Listeners and $client callbacks

<template b-b="0:1" b-listen="window" b-on="keydown=Blocks_jlmev2#h6"></template>
<template b-b="0:0" b-client="idle=Blocks_jlmev2#c4"></template>

Route slots

<!--R0:children:3:%2Ftodos%2F%5Bid%5D%20%7B%22id%22%3A%223%22%7D--><article>…</article><!--/R-->

Each slot of a layout that a route renders into is marked with <!--R<layout scope>:<name>:<route scope>:<route key>--> and <!--/R-->.

Styles

The stylesheet of a component is <style b-style="<component ID>">, once per page, before the first instance of the component. See Style compilation.

State

<script b-state b-scopes b-next="5" b-route="…">function $bR(f){…}$bR(($R,$b)=>(($R[0]={0:$R[1]={k:1,m:"App_x1",s:$R[2]=[1]}})))</script>
<script b-state>$bR(($R,$b)=>(…))</script>

The state is a stream of seroval cross-reference records, one inline script each. The server writes them with crossSerializeStream. Each record is a function of $R, the references of its render, and $b, the functions that make brumate's values.

The first record turns into an object keyed by scope ID:

type PageState = Record<number, {
  /** The scope's kind: its number in the compiled component, which selects its updates. */
  k?: number;
  /** The ID of the scope that contains this one. A component's root scope has none. */
  p?: number;
  /** The component ID, on a component's root scope. */
  m?: string;
  /** The item, on an item scope of a list that can change. */
  i?: unknown;
  /** On a component's root scope, the scope that rendered it, for finding contexts. */
  o?: number;
  /** The values that the scope provides, by context key. */
  v?: Record<string, unknown>;
  /** The slots that hold the contexts that the scope reads, by context key. */
  x?: Record<string, number>;
  /** Slot values, in the order of the scope's layout. */
  s?: unknown[];
  /** The scope IDs of child components, by anchor index. */
  c?: Record<number, number>;
}>;

The server writes records that share their shape in groups, under the key g, such as the items of a list. Each scope in a group costs its slots and one number:

// Three todo items of kind 2 in scope 0, whose item is their first slot.
g: [[2, 0, 1, 1, 1, $R[4], 1, $R[5], 1, $R[6]]]
// The same items when their list is slot 0 of scope 0. Each one is only its gap.
g: [[2, 0, 33, 1, 0, 0, 1, 1, 1]]

Values with plugins of their own, and the call of $b that makes each one in a record:

Tag In a record Value
brumate/element $b.e(scope, anchor) ElementReference: a ref signal whose element the server rendered. It holds the element's scope and anchor.
brumate/function $b.f(reference, scope) FunctionReference: a client function passed as a prop. It holds its reference and the scope it reads.
brumate/raw $b.r(html) HTML that the client may insert again, such as children.
brumate/component $b.c(id) A component, as its component ID, for dynamic tags.
brumate/upload Not in the page A file in the arguments of a server function call. See Server function protocol.

Web values, such as URL, Headers and ReadableStream, use the plugins of seroval-plugins/web, which write code that makes them, such as new URL(href).

Attributes of records

Attribute On Meaning
nonce Every record, with the nonce option The Content Security Policy nonce of the render.
b-state Every record The script is a state record.
b-scopes Records with scopes The record adds scopes.
b-next Every record with scopes A number above every scope and boundary ID of the render.
b-route The first record of a page from routes The key of the root route.
b-root Every record of a render with a base above 0 The base, or d<boundary> for a #defer body. The core keeps a refs map for each root.
b-offset Records that the client moved The number the client added to the IDs in the markup. The core adds it to the scope IDs in the record.
b-drop Set by the client on navigation The route scopes that the new page replaces. The core drops them and the scopes inside them.
b-roots Set by the client on navigation The route scopes of the new page. The core adds only them and the scopes inside them.
b-alias Set by the client on navigation Pairs <new>:<kept>: layout scopes of the new page that stand for layouts the client keeps.

Props

A child component stores its props in its own slots. When a parent passes a value that can change, the parent records the child's scope ID in c, at the anchor index of the component. The parent's update code then writes the new value into the child's slot.

When the loader loads another page, it keeps the layouts that are the same on both pages.

  1. The request sends x-brumate-base, a number above every ID in the current page: the largest b-next.
  2. The server renders the page with that base, so its IDs never mix with the current page's. Its records carry b-root with the base.
  3. The client compares the route slots of the two pages. A slot whose route key is the same keeps its content. A slot whose key changed is replaced with the new content.
  4. The client marks the state record with b-drop, b-roots and b-alias, and the core adds only the new routes' scopes. New routes read contexts from the kept layouts through the aliases.
  5. The client makes a copy of each state script with the nonce of the loaded page, so the browser runs it. A page parsed with DOMParser does not run its scripts. The core reads the records when it starts again.

The client keeps the layouts only when the root route of both pages is the same, and the new page started its IDs at the requested base.

Pages that ignore the base

A prerendered file on a static host starts its IDs at 0, whatever the request asks. Before it reads such a page, the client moves its IDs up by the difference:

Markup that streams in later is moved the same way. IDs that $id() made are text, and are not moved.