Hyperlinkv0.8.0-beta.28
DraftMigration checklist — 0/6
  • Current API — no legacy surface
  • LSP code previews
  • Clean example types
  • Verified examples
  • Follows the docs standards
  • Owner-reviewed

Dashboard

The web dashboard renders a live view over your resources — a drill-down of queue / process / API / group cards, each with stats, charts, controls, and streaming logs. You point it at a reactive runtime and a root Group:

import { Dashboard } from "hyperlink-ts/web"
import { Atom } from "effect/unstable/reactivity"

const runtime = Atom.runtime(appLayer) // appLayer: your resource clients over a transport
<Dashboard runtime={runtime} group={ServicesHub} />

Transport: use a WebSocket in the browser

This is the one setup decision you must get right, and the default is not what a browser wants.

A resource client reaches its node over an RPC transport. The library ships two:

  • Hyperlink.http(node, { url }) — HTTP. The default, and correct for a server, CLI, or backend-to-backend caller, or any client that opens only a handful of streams.

  • Hyperlink.ws(node, { url }) — a single WebSocket. Use this for a browser dashboard.

Why the browser needs the WebSocket

The dashboard is stream-heavy: every visible resource holds several live streams open at once — its status, its metrics, its logs — and the grid shows many resources. That is easily 15–20 concurrent streams.

Over the HTTP transport, each stream is its own browser connection, and a browser allows only ~6 connections per origin on HTTP/1.1. Past that, the extra streams are starved — they never even get a response. The visible symptom is a dashboard that half-works: some cards update, others sit frozen, no charts, no logs — with no error, because the requests are simply queued forever.

A WebSocket multiplexes every stream for a node over one connection, so the 6-connection cap never applies. ws is the browser-shaped transport; http is not.

HTTP/2 (a TLS production server) multiplexes too, so the cap is an HTTP/1.1 phenomenon — but your dev server (Vite) is HTTP/1.1, and any plain-HTTP deployment is as well. The WebSocket is correct across all of them, so prefer it for the browser unconditionally.

Setup

Three matching pieces — the server must serve the same protocol the client speaks:

1. Server — serve WebSocket with Node.wsServer (the Node.httpServer sibling; same serve list and options, it just speaks WebSocket):

Node.wsServer(
  [Hyperlink.serve(Jobs, jobsImpl) /* … */],
)

2. Client — one ws per node. A same-origin path resolves against the page location (its host, and http→ws / https→wss), so you just pass "/rpc" — no URL assembly, and its safe in a module a server also imports (resolution is lazy):

const transport = Hyperlink.ws(JobsNode, { url: "/rpc" }) // → ws(s)://<page host>/rpc

const appLayer = Layer.mergeAll(
  transport,
  Hyperlink.client(Jobs).pipe(Layer.provide(transport)),
)

(url also accepts an absolute ws:// / wss:// url, or an http(s):// url with the scheme swapped — for a Node client, or a cross-origin server.)

3. Dev proxy — if you proxy the RPC path in dev (same-origin, no CORS), enable WebSocket upgrades (ws: true). For Vite:

server: {
  proxy: {
    "/rpc": { target: "http://localhost:7780", changeOrigin: true, ws: true },
  },
}

A worked end-to-end setup is in examples/resource-web (three nodes, one WebSocket each).

Fleets — peers dial the same wire

When a nodes resource folds across peers (a fleet field like fleetActive — see multi-node), those server-to-server calls have their own transport. They default to HTTP, so a fleet whose nodes serve WebSocket must move the peer mesh onto WebSocket too, or the fold 404s against the ws-only /rpc. One knob per node, alongside peersLayer:

Node.wsServer([Hyperlink.serve(WorkerPool, poolImpl) /* … */]).pipe(
  Layer.provide(Hyperlink.peersLayer(WorkerPool, ThisNode)),
  Layer.provide(Hyperlink.layerPeerProtocol(Hyperlink.protocolWebsocket)), // peers speak ws too
)

Under the hood every client transport is the same seam — Hyperlink.layerProtocol(protocol) sets a client wire from an RpcClient.Protocol (build one with Hyperlink.protocolHttp / Hyperlink.protocolWebsocket), and ws / http are per-node shortcuts over it.

Widgets

Each resource kind renders a hand-crafted card, chosen by the tags kind and extensible per kind or per exact resource — see React components.

Edit this page on GitHub