Skip to content

spicysapling/compunism

Repository files navigation

compunism

seizing the means of shared worker production

A POC demonstrating multi-tab communication entirely through browser SharedWorker instances — no server-side state, no WebSocket, no BroadcastChannel. Pages discover each other through a shared "register" worker and exchange messages through per-session "inbox" workers.

Stack

  • Vite + React 19 + TypeScript for the client
  • Tailwind CSS v4 + shadcn/ui (Base UI primitives via the base-nova style)
  • Bun as the dev runtime and static file server

Run

Live deploy: https://compunism.fly.dev/

Dev (HMR):

bun install
bun run dev

Production preview (build + serve via Bun):

bun run serve

Both serve at http://localhost:4137. Open additional tabs at the same URL to spawn more sessions; use the pop out button to launch a window that shares the original tab's session id (carried via the URL hash, #sess=<id>).

Deploy

Containerized with Dockerfile, deployed to Fly:

fly deploy --remote-only

The server reads PORT from the env (defaults to 4137 locally; 8080 in the container, which Fly's proxy fronts on 443). Browser auto-open is skipped when FLY_APP_NAME or CI is set.

Architecture

Two SharedWorker scripts, instantiated by name:

  • register-worker.js (single instance, name "register"): the discovery hub. Every page connects to it, announces its sessionId, and receives the current list of live sessions. The register pushes the updated list to every connected page whenever a session joins or leaves — no polling.
  • session-worker.js (one instance per session, name "sess-<id>"): the per-session inbox. The session owner connects to receive incoming messages; senders connect to the same name to deliver. Sibling windows of the same session (via pop-out) all connect to the same instance.

Sessions vs. windows

  • sessionId — addressable identity. Shared across pop-out windows opened from the same origin page.
  • windowId — unique per page load. Used by the session worker to know which port to skip when broadcasting, so an outgoing message doesn't echo back to its originator but does reach sibling windows.

Cleanup

The register tracks each session's ports and only drops the session when its last port unregisters — this prevents a closing pop-out from removing a session that the parent window still owns. Two cleanup signals:

  1. beforeunload sends an explicit unregister (covers normal tab close).
  2. Heartbeat (every 5s) + stale prune (15s) covers crashes and force-kills where beforeunload doesn't fire.

Message flow

Sending text from (session A, window α) to peer session B:

  1. Post {from: A, windowId: α, text} to sess-B. sess-B broadcasts to all of its ports except the one tagged with windowId α — every window of session B (including pop-outs) renders it as incoming.
  2. Render me → B: text locally for immediate feedback.
  3. Post {from: A, windowId: α, text, to: B} to sess-A. Sibling windows of session A receive it and render me (α) → B: text, keeping every window of a shared session in sync.

Files

File Role
server.ts Bun static file server (serves dist/) + browser launcher
vite.config.ts Vite + Tailwind v4 + path alias config
index.html Vite entry HTML
src/main.tsx React entry
src/App.tsx UI composed from shadcn (Base UI) components
src/hooks/useSession.ts Session/window id, SharedWorker wiring, send/popout
src/components/ui/* shadcn Base UI components (base-nova style)
public/register-worker.js Discovery hub with refcounted sessions
public/session-worker.js Per-session inbox; windowId-based routing

About

seizing the means of shared worker production

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors