Streaming Market Data in the Browser: JS, TypeScript, and HTML as Three Layers of One Pipe
JavaScript runs it. TypeScript constrains it. HTML chassises it.
§ IFrame
The Ops lesson set the stage. A 300 ms operator-budget. Engine, edge, browser. Three tiers, each owning its measurement. Today's Dev lesson lives entirely in the browser's portion of that budget. The final 100 ms is where bytes off the wire become pixels on the screen, and where the stream a trader sees becomes the data they can trade against.
The three languages of the browser do three different things in this pipe.
JavaScript runs the pipe. It accepts incoming events from the network, accumulates them into a buffer, runs the coherence check at every paint, and orchestrates the requestAnimationFrame callback. JavaScript is the runtime.
TypeScript constrains the pipe. It declares what every message coming off the wire can be, what every state of the buffer can hold, and what every render-callback may receive. TypeScript is the contract that runs at compile time so the browser does not have to fail at run time.
HTML chassises the pipe. It declares which connections to open before the page has even asked for the stream, which components own which render scope, and which fonts and styles must not leak into which dashboards when the operator runs three of them in one tab. HTML is the page's frame.
The lesson covers each in its turn. The discipline at every layer is the same discipline: the browser owes the operator the freshest in-band tick at every paint, and nothing else.
§ IIJavaScript — The Runtime Streaming Primitives
The browser has shipped three streaming-receive shapes for years. Each fits a different geometry. Picking right reduces the connection-management code to a thin shell around the primitive; picking wrong forces the operator to re-implement what the platform already provides.
EventSource (Server-Sent Events). One-way streaming over HTTP. The browser opens a long-lived connection to the server, the server pushes text messages, the browser dispatches them to handlers. Automatic reconnect on connection-drop with configurable backoff. The simplest primitive in the family. The right choice when the operator never sends data back through the same channel.
const source = new EventSource('/orderbook/stream')
source.addEventListener('tick', (event) => {
const tick = JSON.parse(event.data)
buffer.push(tick)
})
source.addEventListener('error', (event) => {
if (source.readyState === EventSource.CLOSED) {
onConnectionLost()
}
})
The browser implements the reconnect loop. The operator implements the on-connection-lost telemetry callback.
WebSocket. Bidirectional streaming. The browser opens a single connection, both sides push messages, both sides receive. No automatic reconnect; the operator owns the reconnect loop. The right choice when the operator does send data back through the same channel: order submissions on the same connection as order-book streaming, agent commands on the same connection as agent-state telemetry.
const socket = new WebSocket('wss://api.example/stream')
socket.addEventListener('message', (event) => {
const tick = JSON.parse(event.data)
buffer.push(tick)
})
socket.addEventListener('close', (event) => {
scheduleReconnect(event.code, event.reason)
})
fetch with ReadableStream. HTTP response body consumed as a stream rather than as a complete payload. The browser opens an ordinary HTTP request; the response arrives in chunks; the operator reads chunks as they arrive. The right choice when the data is intrinsically chunked but does not require a persistent push channel.
const response = await fetch('/orderbook/snapshot')
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { value, done } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
for (const line of chunk.split('\n')) {
if (line) buffer.push(JSON.parse(line))
}
}
requestAnimationFrame. The paint-loop coordinator. The browser invokes the callback once before every frame. The callback drains the buffer, applies the coherence check, and renders.
function paintLoop() {
const now = performance.now()
while (buffer.length > 1) {
const tick = buffer.shift()
if (now - tick.edgeTimestamp > 500) continue
render(tick)
break
}
if (buffer.length === 1) {
const tick = buffer[0]
if (now - tick.edgeTimestamp <= 500) {
render(tick)
} else {
renderStale(tick, now - tick.edgeTimestamp)
}
buffer.length = 0
}
requestAnimationFrame(paintLoop)
}
requestAnimationFrame(paintLoop)
JavaScript hands the operator four primitives and one coordinator. Each carries clean semantics. The operator does not assemble streaming infrastructure from raw sockets; the browser ships it built.
§ IIITypeScript — The Type-Safety Contract at the Data Boundary
A malformed tick from the edge is a production incident at the browser. TypeScript catches the malformation at compile time so the runtime does not have to.
The contract has three parts: the wire format, the buffer state, and the render input.
Wire format as discriminated union. The matching engine emits several tick types. A bid update. An ask update. A trade. A snapshot. Each carries different fields. A discriminated union names every shape the wire can produce and forces every consumer to handle every case.
type WireTick =
| { kind: 'bid'; price: number; size: number; edgeTimestamp: number }
| { kind: 'ask'; price: number; size: number; edgeTimestamp: number }
| { kind: 'trade'; price: number; size: number; side: 'buy' | 'sell'; edgeTimestamp: number }
| { kind: 'snapshot'; bids: PriceLevel[]; asks: PriceLevel[]; edgeTimestamp: number }
Every consumer of WireTick must exhaust the kind field. A switch over kind without a default case forces TypeScript to verify that every branch is handled. Adding a fifth kind to the union without updating the consumer is a compile error.
Buffer state as branded type. The buffer holds parsed ticks. A bare array of WireTick is too loose. A branded TickBuffer type ties the buffer to the discipline that produces it.
type TickBuffer = WireTick[] & { readonly __brand: 'TickBuffer' }
function createBuffer(): TickBuffer {
return [] as TickBuffer
}
function pushTick(buffer: TickBuffer, tick: WireTick): void {
buffer.push(tick)
}
The brand prevents an arbitrary array from being passed where a TickBuffer is expected. The operator who declares a local array of WireTick cannot accidentally feed it into the paint loop where TickBuffer is the contract.
Render input as narrowed type. The render function does not accept a WireTick. It accepts a narrowed type that has already passed the freshness check. The narrowing happens in the paint loop, once, before render is ever called.
type FreshTick = WireTick & { readonly __fresh: true }
function checkFreshness(tick: WireTick, now: number): FreshTick | null {
return (now - tick.edgeTimestamp <= 500)
? (tick as FreshTick)
: null
}
function render(tick: FreshTick): void {
/* body does not need to re-check freshness */
}
The render function's signature alone enforces that no path from buffer to screen bypasses the freshness check. A new component author who writes render(buffer[0]) gets a compile error. The contract is in the type, not in the documentation.
TypeScript turns the cache-coherence rule from a runtime check into a compile-time contract. The check still runs at runtime, but the contract that the check ran cannot be bypassed.
§ IVHTML — The Chassis
HTML is the page's frame. Two facilities matter for streaming dashboards specifically: connection priming through resource hints, and component isolation through custom elements with shadow DOM.
Resource hints prime the network handshake. A streaming connection requires DNS resolution, TCP handshake, TLS handshake, and HTTP upgrade before any data flows. On a fresh tab, that round trip can cost 200 ms of operator-budget before the first tick is even possible. Resource hints run those handshakes before the JavaScript asks for them.
<link rel="preconnect" href="https://api.example" crossorigin>
<link rel="preconnect" href="https://stream.example" crossorigin>
<link rel="modulepreload" href="/dashboard.js">
The preconnect hints open the network handshakes during HTML parse, before the JavaScript loads. The modulepreload hint loads the dashboard module in parallel with the network handshakes. By the time the JavaScript runs, the connections are warm and the module is parsed. The streaming connection opens against a primed network.
Custom elements isolate render scope. A dashboard with three panels (order book, recent trades, position summary) is three independent render trees. A bare div-based layout shares CSS scope and event-listener scope across the panels; a style change in one panel can leak into another, an event handler in one can fire into another. Custom elements scope each panel to its own DOM tree.
<order-book-panel symbol="0xabc..."></order-book-panel>
<recent-trades-panel symbol="0xabc..."></recent-trades-panel>
<position-summary-panel></position-summary-panel>
Each custom element registers a class that extends HTMLElement, attaches a shadow DOM root, renders its own template into that root, and runs its own paint loop. The three panels share nothing except the page's network connection.
class OrderBookPanel extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<style>:host { display: block; font-family: monospace; }</style>
<div class="bids"></div>
<div class="asks"></div>
`
this.startStreaming(this.getAttribute('symbol'))
}
startStreaming(symbol) {
const source = new EventSource(`/orderbook/${symbol}/stream`)
source.addEventListener('tick', (event) => this.onTick(JSON.parse(event.data)))
}
onTick(tick) {
this.buffer = this.buffer || []
this.buffer.push(tick)
}
}
customElements.define('order-book-panel', OrderBookPanel)
The HTML chassis is not decoration. The chassis is what the latency budget runs on top of, and what makes the three frontend languages cooperate without interference.
§ VConnection to Today's Ops Lesson
The Ops lesson named three foundations: the latency budget as a contract, the streaming-connection topology as a choice, and the cache-coherence rule as a defense. The Dev lesson implements each in turn.
The latency budget's browser tier (100 ms of the 300 ms operator-budget) is held by the requestAnimationFrame paint loop. The loop runs at 60 fps on a healthy machine, which is 16.6 ms per frame. The browser tier's 100 ms budget is six frames worth of headroom. The paint loop's job is to surface the freshest in-band tick at every frame and to never paint stale data without surfacing the staleness visibly.
The streaming-connection topology maps to the JavaScript primitives. The Ops lesson's worked example chose Server-Sent Events from edge to browser. The Dev lesson's first JavaScript primitive is EventSource. The Ops lesson's mention of bidirectional channels maps to WebSocket; the Ops lesson's mention of intentionally short-lived chunked deliveries maps to fetch with ReadableStream.
The cache-coherence rule (500 ms yellow, 2 s red, 10 s reconnect) is enforced in two places. At runtime in the paint loop's freshness check (the now - tick.edgeTimestamp comparisons in the JavaScript section), and at compile time in the TypeScript section through the FreshTick branded type. Both belt and suspenders. The runtime check enforces the contract at every frame; the compile-time check enforces that no path from buffer to screen bypasses the runtime check.
The Ops lesson said: name each tier, measure each tier, set the cache-coherence rule before the first operator sees the first tick. The Dev lesson says: here are the language primitives that let you do that without writing the platform yourself.
Paired Ops lesson → Archmagus-Stack/01-Earth-DevOps/Synthesis-Lessons/2026-05-24-edge-deployed-frontend-discipline-for-real-time-market-data-...
§ VIPrior-Lesson Reach
Go Channels and Pipeline Patterns (Friday). The signal pipeline in the Go lesson built fan-out / fan-in geometry server-side. The frontend's buffer in today's lesson is the consumer side of the same shape: ticks flow into the buffer (fan-in from the network), the paint loop selects the freshest (filter), the render call emits the chosen value (fan-out to the DOM). The geometry is the same; the language primitives differ. Go channels handle backpressure with capacity; JavaScript's buffer handles backpressure with the freshness rule (older ticks dropped).
Rust's Ownership and Type-State Model (Saturday). The validator-signing-key type-state pattern enforced single-sign-per-round at compile time. Today's FreshTick type follows the same shape: a branded type tied to a checked condition, with the type-system enforcing that the condition has been checked before the value flows further. The Rust lesson used affine types to make double-spend impossible; the TypeScript lesson uses brands to make freshness-bypass impossible. Different type systems, same architectural move.
§ VIIClosing
JavaScript runs the pipe. TypeScript constrains it. HTML chassises it. Each language carries one of the three jobs the Ops lesson named, and each does that job without the operator re-implementing the platform.
The browser ships these primitives built. The discipline is not building them; the discipline is choosing the right primitive for the right tier and holding the contract at every layer.
Open the browser DevTools on the next dashboard you ship. Filter the network panel to streaming connections. Watch the message rate, the per-message size, and the time between the edge timestamp and the paint callback. The numbers say whether the discipline is being held.
Filed 2026-05-24 Sunday Fajr · First Web unitary lesson on Praetor cycle · JS + TypeScript + HTML as three layers of one pipe
Backward-Synergy-Reach → Go channels + pipeline (Fri γ) · Rust ownership + type-state (Sat δ) · today's Ops Edge-Deployed Frontend
HTML render backfilled 2026-05-25 under approved scaffold + sea-green aether palette