5.7 KiB
Plan: Move Mute List Filtering into SharedWorker
Problem
The mute list (kind 10000) is currently loaded and evaluated page-side only in www/js/mute-list.mjs. The SharedWorker (www/ndk-worker.js) broadcasts all subscription events unfiltered. This means:
- Every page must independently import and call
isEventMuted()— easy to forget - Muted events still get cached in IndexedDB by the worker
- Muted events still consume bandwidth and processing across all tabs
- Pages that don't implement mute checks show muted content
Current Architecture
flowchart LR
R[Relays] -->|all events| W[SharedWorker]
W -->|broadcast unfiltered| P1[Page 1]
W -->|broadcast unfiltered| P2[Page 2]
P1 -->|page-side check| ML1[mute-list.mjs]
P2 -->|page-side check| ML2[mute-list.mjs]
style W fill:#553,color:#fff
style ML1 fill:#335,color:#fff
style ML2 fill:#335,color:#fff
Target Architecture
flowchart LR
R[Relays] -->|all events| W[SharedWorker]
W -->|filter via mute sets| F[isEventMuted check]
F -->|clean events only| P1[Page 1]
F -->|clean events only| P2[Page 2]
W -->|fetch kind 10000| R
P1 -->|updateMuteList msg| W
style W fill:#553,color:#fff
style F fill:#533,color:#fff
Design Decisions
Public mutes only in worker
The worker will only filter on public mute entries (the tags array of the kind 10000 event). Private/encrypted mute entries require NIP-44 decryption which needs the signer — the worker already has messageSigner for this, so we can decrypt private entries too via the existing requestFromPage pattern.
Worker fetches its own mute list
During handleInit(), after relay connection, the worker will fetch kind 10000 for the authenticated pubkey and parse it. This parallels how it already fetches kind 3 (follows), kind 10002 (relay list), etc.
Pages can still add/remove mutes
The page-side addMute()/removeMute() flow publishes a new kind 10000 event. After publishing, the page sends a syncMuteList message to the worker so it reloads.
Backward compatibility
Keep isEventMuted() exported from init-ndk.mjs so pages that need fine-grained control (like event-management showing muted items with a badge) can still check. But the worker pre-filters subscription broadcasts so most pages don't need to.
Files to Modify
www/ndk-worker.js
- Add mute list state variables (public/private mute sets)
- Add
parseMuteListTags()helper - Add
isWorkerEventMuted()check function - Add
handleLoadMuteList()— fetches kind 10000, parses public tags, decrypts private content viamessageSigner.requestFromPage - Modify
wireSubscriptionHandlers()— filter events throughisWorkerEventMuted()beforebroadcast() - Add
syncMuteListmessage handler in the switch statement - Call
handleLoadMuteList()duringhandleInit()after relay connection - Broadcast
muteListUpdatedevent to all pages when mute list changes
www/js/init-ndk.mjs
- Add
syncWorkerMuteList()export that sendssyncMuteListmessage to worker - Modify
bootstrapMuteList()to also trigger worker-side load - Add handler for
muteListUpdatedworker message to keep page-side mute-list.mjs in sync - Keep all existing exports for backward compatibility
www/js/mute-list.mjs
- No changes needed — it continues to work page-side for pages that need it
- Worker sync will update it via the
muteListUpdatedevent path
Pages (minimal changes)
www/vj.html— can remove theisEventMutedcheck inhandleMusicNdkEvent()since worker pre-filterswww/feed.html,www/post.html,www/notifications.html,www/skills-edit.html— subscription events will arrive pre-filtered; existingisEventMuted()calls become redundant but harmless (double-check is fine)www/event-management.html— keep page-side check since it shows muted items with a visual badgewww/block.html— no subscription events; manages the mute list directly
Implementation Steps
- Add mute state and parsing to worker — port the
parseTags()andisEventMuted()logic intondk-worker.jsas standalone functions - Add
handleLoadMuteList()to worker — fetch kind 10000, parse public tags, decrypt private content usingmessageSigner.requestFromPage('nip44Decrypt', ...) - Filter in
wireSubscriptionHandlers()— beforebroadcast(), checkisWorkerEventMuted(event)and skip if true - Call during init — add
handleLoadMuteList(currentPubkey)call inhandleInit()after relay setup - Add
syncMuteListmessage type — so pages can trigger a reload after publishing mute changes - Broadcast
muteListUpdated— after worker loads/reloads mute list, broadcast the parsed public mute data to all pages - Wire
init-ndk.mjs— handlemuteListUpdatedmessage, callsyncWorkerMuteList()afteraddMute/removeMute - Clean up page-side checks — remove redundant
isEventMuted()calls from subscription handlers in pages where the worker now pre-filters
Edge Cases
- Worker init before mute list loads: Events arriving before the mute list is fetched will pass through unfiltered. This is acceptable — the mute list loads quickly and the window is small.
- Multiple tabs with different mute states: The worker is shared, so all tabs see the same mute filtering. This is correct behavior.
- Mute list publish race: After
addMute()publishes, the page sendssyncMuteListto the worker. The worker re-fetches from cache/relays. There's a brief window where the old mute list is active. Acceptable. - Anonymous/unauthenticated mode: No mute list to load. Worker skips mute filtering. No change from current behavior.