# smesh2 cleanup plan ## Phase 0: Channel primitive A ~30-line module that replaces callback maps, timeout cleanup, and stale closures with linear async flow. ### chan.js ```js // broadcast channel: multiple listeners, each gets every message function chan() { const waiters = [] return { send(val) { for (const w of waiters.splice(0)) w(val) }, recv() { return new Promise(resolve => waiters.push(resolve)) } } } // multiplexed channel: keyed by request ID, one-shot per key function mux() { const pending = new Map() return { send(id, val) { const resolve = pending.get(id) if (resolve) { pending.delete(id); resolve(val) } }, recv(id, timeoutMs = 15000) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { pending.delete(id); reject(new Error('timeout')) }, timeoutMs) pending.set(id, (val) => { clearTimeout(timer); resolve(val) }) }) } } } export { chan, mux } ``` Used in both SW and UI: SW side (dm.js): ```js const cryptoMux = mux() // requestCrypto becomes: async function requestCrypto(type, pubkey, text) { const id = ++cryptoRequestId broadcastToClients([type, id, pubkey, text]) return cryptoMux.recv(id) } // CRYPTO_RESULT handler becomes: cryptoMux.send(requestId, { result, error }) ``` UI side (app.js): ```js const signMux = mux() const swChan = chan() // broadcast: every SW message goes here // signViaSW becomes: async function signViaSW(event) { const id = ++signCounter send(['SIGN', id, event]) return signMux.recv(id) } // message handler — one line, no switch statement for routing navigator.serviceWorker.addEventListener('message', e => { const [type, ...args] = e.data swChan.send({ type, args }) // broadcast to all listeners if (type === 'SIGNED') signMux.send(args[0], args[1]) // etc for other muxed channels }) // DM listener — reads state at consumption time, never stale async function listenDMs() { while (true) { const msg = await swChan.recv() if (msg.type !== 'DM_RECEIVED') continue const dm = msg.args[0] dispatch({ type: 'ADD_CONVERSATION', conversation: { peer: dm.peer, lastMessage: dm.content.slice(0, 80), lastTs: dm.created_at, from: dm.from } }) dispatch({ type: 'ADD_DM_MESSAGE', message: dm }) } } ``` The broadcast channel (`chan`) replaces useEffect message handlers. The multiplexed channel (`mux`) replaces callback maps with timeouts. Both are tiny, zero-dependency, and eliminate the stale closure disease at the root. ## Phase 1: Split sw.js into modules Create 5 new files, keeping sw.js as the orchestrator. ### crypto.js (~170 lines) - signEvent, signEventWithKey, randomizeTimestamp - nip04SharedKey, nip04EncryptRaw, nip04DecryptRaw - NIP44_SALT, nip44ConversationKey, nip44MessageKeys - nip44CalcPadding, nip44Pad, nip44Unpad, nip44EncryptRaw, nip44DecryptRaw - giftWrap (needs crypto + signing) - toBase64 chunked encoder (no spread overflow) - FIX #1: replace `btoa(String.fromCharCode(...payload))` with chunked encoder Exports: all functions above. Imports noble-curves/hashes/ciphers. ### db.js (~140 lines) - DB_NAME, DB_VERSION, openDB, getDB, patchDBClose - saveEvent, queryEvents - saveDM, queryDMs, getConversationList, dmDedupId Exports: getDB, saveEvent, queryEvents, saveDM, queryDMs, getConversationList, dmDedupId. ### pool.js (~220 lines) - MAX_CONNECTIONS, pool map, getConnection - sendToRelay, sanitizeFilter, HEX_FILTER_KEYS - handleRelayMessage (EVENT, EOSE, OK, NOTICE, AUTH) - proxySubs, handleProxy, cleanupProxy - handleEvent, handleRelayInfo, relayInfoCache - reconnect handler registry - FIX #2: on WebSocket open after reconnect, call registered handlers (dm.js re-sends subs) - FIX #3: handleEvent publishes to writeRelays only, not all pool connections - FIX #7: sanitizeFilter drops entire filter object if required array field becomes empty after sanitization - FIX #11: handle AUTH challenge — sign with secretKey, send AUTH event back pool.js exports `onReconnect(url, fn)`. dm.js registers its re-sub handler. ### dm.js (~180 lines) - Uses mux() from chan.js for crypto request/response - decryptNip04, encryptNip04, decryptNip44, encryptNip44 wrappers - processIncomingDM, processNip04DM, processNip17DM - sendDM, sendNip04DM, sendNip17DM - handleDMSub, dmSubIds - FIX #8: processNip17DM logs errors via console.warn instead of empty catch - Registers reconnect handler with pool.js to re-send DM subs ### sw.js (~100 lines, down from 1139) - imports from ./chan.js, ./crypto.js, ./db.js, ./pool.js, ./dm.js - install/activate/fetch lifecycle handlers - message dispatch switch (thin — delegates to modules) - broadcastIdentity - module-level state: secretKey, secretKeyHex, myPubkey, writeRelays - subs map, handleReq, handleClose, pushToMatchingSubs, matchesFilter ## Phase 2: Remove Preact, go vanilla JS Replace Preact+hooks+htm with plain DOM rendering + channel-driven message handling. ### Architecture - Global `state` object (same shape as current useState initial value) - `dispatch(action)` calls reducer, updates state, calls `render()` - `render()` reads `state.activeTab` and calls the matching view function - View functions build DOM via innerHTML or document.createElement - Event delegation on `#app` for clicks, inputs, submits, keydowns - SW messages flow through chan/mux — read state at consumption time, never captured ### style.css (~190 lines) - Extract the `