@kolu/surface-app reference
The shell delivered around a @kolu/surface app — the static, installable
layer for apps you run against your own server (kolu, drishti). Surface is the
live wire; surface-app is the shell served fresh around it. Exports are grouped
by subpath.
Serving the fresh shell
installSurfaceApp (@kolu/surface-app/server) serves the shell: a no-store
index.html that names a content-hashed bundle, immutable /assets/*, a
404-on-miss for a stale hash, an SPA fallback, /sw.js, and the web manifest.
Staleness is structurally impossible because the one document that names the
bundle is always re-fetched.
| Export | Subpath | Role |
|---|---|---|
installSurfaceApp |
/server |
serve the no-store shell + immutable assets |
installFreshStatic |
/server |
fresh static + optional notification worker |
installPwaManifest |
/server |
the web manifest |
surfaceApp() |
/vite |
Vite plugin that injects the build commit onto the shell |
buildSurfaceClient() |
/bun |
the Bun build equivalent |
Accepting a browser socket
acceptSurfaceSocket (@kolu/surface-app/server) accepts a browser WebSocket
onto a served surface. It owns the server-side liveness reaper and sequences
stale-gate → enrol → dispatch in one accept(...), so a socket can never be
dispatched un-enrolled. It is the server twin of the client’s connectSurface
watchdog.
| Export | Role |
|---|---|
acceptSurfaceSocket |
accept + reap + sequence, in one call |
gateStaleSocket |
the WS-upgrade handshake gate (error-handler-first; close 4001) |
startWsHeartbeat |
the server-side liveness reaper |
Connecting from the client
connectSurface / connectSurfaces (@kolu/surface-app/solid) are the turnkey
single- and multi-surface seams: each builds a socket, a websocketLink, the
surfaceClient(s), and a default-on liveness heartbeat in one call. The
heartbeat probe defaults to the framework-reserved system.live
(@kolu/surface/liveness), so no app supplies — or forgets — a probe.
const { ws, client, status, dispose } = connectSurface({ surface, url });
createServerLifecycle({ ws, probe }) derives the connection lifecycle
(connecting → connected → disconnected → reconnected / restarted) from
transport open/close plus a processId probe. Deriving a lifecycle cannot leave
the socket un-watched: it owns a default-on half-open watchdog that reuses the
probe on an interval and forces ws.reconnect() when a probe times out —
catching a socket that is TCP-dead with no FIN/RST (laptop sleep, Wi-Fi roam,
NAT idle-eviction).
| Option | Effect |
|---|---|
heartbeat: false |
opt out of the default-on watchdog |
onProcessId |
publish each observed id to echo back as the next reconnect’s pid |
onStaleRestart |
teardown hook fired at the single stale-close decode site |
restartCloseCode |
that exact close code routes straight to restarted |
The client model
SurfaceAppProvider (@kolu/surface-app/solid) is the turnkey source that
handles the stale-tab handshake; its connection source is a union of { ws, probe }
(the provider derives the lifecycle) or { status } (you already did). It takes
a typed controlPlane whose buildInfo cell yields build identity, plus
clientCommit={shellCommit()}.
useSurfaceApp() is the headless model — no styled components ship:
| Accessor | Meaning |
|---|---|
status() |
"live" | "reconnecting" | "restarted" | "down" |
presentingDown() |
grace-windowed down state |
server() |
{ commit, … } server identity |
reload() |
reload for an update |
setAttention(n) |
OS app badge + title |
canInstallPwa() |
true only in a secure context and not already installed |
Build identity
Build identity is an interface. defineBuildInfo({ schema, default, isStale? })
declares it; buildInfoServer({ buildInfo? }) and surfaceAppServer({ buildInfo?, equals? })
(@kolu/surface-app/surface / /server) implement it.
Service worker stance
- No caching service worker, ever — the ban is on a
fetchhandler that intercepts the network. Definitional for this class of app. - By default the package ships
SW_SOURCE, a self-destructing retirement worker at/sw.js, plusretireServiceWorker()to retire any worker a prior build registered. - The one opt-in is a fetch-less notification worker (
NOTIFICATION_SW_SOURCE), registered viaregisterOrRetireServiceWorker(). An app registers (notify) or retires (none) — never both. - Gate all service-worker logic on
window.isSecureContext, neverlocation.protocol === "https:".