# smesh frontend architecture Five execution contexts on the page side, plus the relay binary and remote nostr relays. Single-concern per context; cross-context comms are postMessage-shaped. ``` +--------------------------+ | page (app.wasm) | | - DOM, routing, state | | - 2 MessagePorts + SW | +-+--------+-------------+-+ | | | MessageChannel MessageChannel navigator.serviceWorker | | .controller.postMessage | | | +---------v--+ +--v---------+ +-v--------------+ | HTTP-proxy | | Relay-proxy| | Service Worker | | worker | | worker | | (legacy / | | | | | | cache layer) | | fetch queue| | WS pool | | Cache API | | /proxy/... | | IDB writes | | fetch intercept| +-----+------+ +-+--------+-+ +----------------+ | | | v v v smesh relay remote IDB (events, dms) /proxy/X relays wss://... ``` ## Contexts | Context | Source | Built artifact | Purpose | |---|---|---|---| | Page main thread | `web/static/wasm-host.mjs` (loader), `web/static/index.html` | served as-is | Supervisor. Constructs all workers, holds DOM, brokers signer extension and DOM bridge calls. | | App | `web/wasm/app/` | `app.wasm` + `wasm-app-worker-host.mjs` | The smesh app: routing, rendering, state. Runs in a Web Worker with a thin DOM-proxy bridge to the supervisor. | | HTTP-proxy worker | `web/wasm/http-proxy/` | `http-proxy.wasm` + `http-proxy-wasm-host.mjs` | Receives `FETCH` requests, hits the relay's `/proxy/` endpoint, returns Blob URLs. Concurrency-limited fetch queue with LRU. | | Relay-proxy worker | `web/wasm/relay-proxy/` | `relay-proxy.wasm` + `relay-proxy-wasm-host.mjs` | WS pool to remote nostr relays. Handles REQ/CLOSE/EVENT/PUBLISH_TO/PROXY/SET_PUBKEY/SET_WRITE_RELAYS/ENC_KEY/DM_LIST/DM_HISTORY. Owns IDB events store. Reconnect via 5s timer. | | Service Worker | `web/legacy/sw/` (production), `web/wasm/sw/` (in-progress) | `/$sw/$entry.mjs` | Handles install/activate/fetch lifecycle and legacy MLS handlers (dead in current production - kept only because the MLS path is unused, not because anyone is calling them). Will shrink to cache-only when the in-progress wasm SW takes over. | ## Message routing (page <-> SW <-> relay-proxy worker) App-side outbound dispatch lives in `routeMsg` (web/wasm/app/main.mx). It examines the JSON-array message type and dispatches: - **Relay-proxy worker** via `relayproxy.Send`: REQ, CLOSE, EVENT, PUBLISH_TO, PROXY, SET_PUBKEY, SET_WRITE_RELAYS, ENC_KEY, CRYPTO_RESULT. - **MLS worker** via `mlsw.Send`: MLS_INIT, MLS_SEND_DM, MLS_SEND_GROUP, MLS_RATCHET_DM, MLS_DM_LIST, MLS_DM_HISTORY. - **Service Worker** via `dom.PostToSW`: `activate-update` only. - **Unrouted messages** log to console as errors. No silent fallthrough. App-side inbound (`onSWMessage`) handles both sources uniformly. Both workers register the same callback: ``` dom.OnSWMessage(onSWMessage) relayproxy.OnMessage(onSWMessage) ``` Message type prefixes are unique per source so there is no collision (worker emits `EVENT`/`EOSE`/`OK`, SW emits `update-available`/`reload`/`SW_LOG`/MLS responses). ## Bridge packages Bridge packages live in `web/common/jsbridge//` for smesh-specific shims, and upstream in `../moxie/jsruntime//` for generic Web APIs (per the moxie CLAUDE.md "Browser Target: WASM Only" rule). | Package | Used by | Function set | |---|---|---| | `dom` | app | DOM ops, localStorage proxy, websocket proxy, signer proxy. Most calls SAB-sync via the supervisor. | | `httpproxy` | app (consumer) + http-proxy worker (internal) | One package, two contexts; DCE strips unused imports per binary. | | `relayproxy` | app (consumer) + relay-proxy worker (internal) | Same pattern. | | `idb` | relay-proxy worker, SW | Events + DM store. Each context has its own connection to the shared `smesh` IDB database. | | `ws` | relay-proxy worker (and any context needing raw WS) | Native browser WebSocket. | | `signer` | app | Wraps the smesh signer extension protocol. | ## State ownership Single-writer-per-store discipline: | Store | Owner | Reason | |---|---|---| | IDB `events` | Store worker (writes via relay-proxy), various readers | Worker is in the WS event flow, primary write path. Idempotent by event ID. | | IDB `dms` | Store worker (writes via MLS/relay-proxy) | Same pattern. | | IDB `kv_profiles`, `kv_settings`, `kv_cache` | Store worker (writes via supervisor) | App reads/writes routed through supervisor to store worker. Single IDB authority. | | Cache API | Service Worker | Owns fetch interception. | | WS connections | Relay-proxy worker | Pool of `relay.Conn` instances; `flushSubs` re-emits REQs on reconnect via `ScheduleReconnect` callback. | | Signing keys | Browser extension (canonical) | Per Q7 Option B: signer extension never shares keys; page mediates between worker and extension. | ## Worker boot order Each worker boots WASM async; the supervisor waits for explicit READY before forwarding requests. 1. `wasm-host.mjs` calls `startHttpProxyWorker()`, `startRelayProxyWorker()`, `startWorker()` (app). 2. Each worker shim listens for `{type: 'init', mode: 'root', wasmUrl: ...}`, fetches the wasm, instantiates. 3. Each worker's `main()` finishes setup and calls `WorkerPost("[\"READY\"]")` (or equivalent). 4. Supervisor sees READY, sets `window.__hpReady` / `window.__rpReady` flags (used by tests; benign in production). 5. Pending messages queued on the supervisor while workers booted are drained. ## Build ```sh make build # all targets make build-relay # native relay binary make build-app-wasm # app.wasm make build-http-proxy-wasm make build-relay-proxy-wasm make build-sw # legacy moxiejs SW (current production) make build-ext # signer extension XPI + Chrome ZIP ``` Each `*.wasm` artifact has a paired `*-wasm-host.mjs` shim in `web/static/`. Bridges that any moxie web app could reasonably want live upstream in `../moxie/jsruntime/`; smesh-specific protocol shims live in `web/common/jsbridge/`. ## Tests Selenium + Firefox via the pytest harness in `test/conftest.py`. The relay fixture boots `smesh` on `127.0.0.1:23334` with `web/static/` as the static root. Phase 1 + 2 worker tests live in `test/test_http_proxy_worker.py`. Each phase ships with its own end-to-end test on a fresh build before being declared done; that is the project's "Reporting Completion" rule. ```sh python3 -m pytest test/test_http_proxy_worker.py -v ``` ## /proxy/ caching (live) The legacy SW (`web/legacy/sw/`) intercepts `/proxy/` requests via `sw.RespondWithProxyCache`. The cache key is the *decoded* upstream URL (so two different base64url encodings of the same upstream share one entry), wrapped in `/__proxy/`. Each cached response carries an `X-Smesh-Cached-At` header. Cache hits younger than 30 days are returned immediately; older or missing entries fall through to network and are re-cached on success. `onActivate` runs `sw.PruneProxyCache(30days)` to delete stale entries (best-effort). The runtime functions are upstream in `../moxie/jsruntime/sw.mjs`. The bridge declarations are in `web/common/jsbridge/sw/`. ## Deferred items - **MLS migration.** ~500 lines of MLS state machine in `web/legacy/sw/mls.mx`. The MLS path is unused in current production, so the legacy handlers stay dead in the SW rather than being migrated. If MLS comes back into scope, the migration follows the same Option B pattern as DM (CRYPTO_REQ to the page for sign/encrypt/decrypt). - **Wasm SW production cutover.** `web/wasm/sw/` exists as a stub; the production SW is still legacy moxiejs in `web/legacy/sw/`. After the wasm SW migration, the wasm SW host shim needs `sw_respond_with_proxy_cache` and `sw_prune_proxy_cache` bridges wired (the moxie-side wasmimports already exist in `web/common/jsbridge/sw/sw_wasm.mx`). - **Server-side async response generation.** `pkg/relay/server/server.mx` has a single-threaded epoll loop; `mediaproxy.Fetch` blocks it during proxy fetches. The refactor needs `routeHTTP` to return a "pending" sentinel and an event mechanism that integrates Moxie spawn channels into the epoll loop (likely via eventfd). Substantial, separate ticket. Acceptable at personal scale, needs work for production scale.