kolu
Docs

@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 fetch handler 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, plus retireServiceWorker() to retire any worker a prior build registered.
  • The one opt-in is a fetch-less notification worker (NOTIFICATION_SW_SOURCE), registered via registerOrRetireServiceWorker(). An app registers (notify) or retires (none) — never both.
  • Gate all service-worker logic on window.isSecureContext, never location.protocol === "https:".