Files
client/plans/worker-mute-list.md
2026-04-17 16:52:51 -04:00

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:

  1. Every page must independently import and call isEventMuted() — easy to forget
  2. Muted events still get cached in IndexedDB by the worker
  3. Muted events still consume bandwidth and processing across all tabs
  4. 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 via messageSigner.requestFromPage
  • Modify wireSubscriptionHandlers() — filter events through isWorkerEventMuted() before broadcast()
  • Add syncMuteList message handler in the switch statement
  • Call handleLoadMuteList() during handleInit() after relay connection
  • Broadcast muteListUpdated event to all pages when mute list changes

www/js/init-ndk.mjs

  • Add syncWorkerMuteList() export that sends syncMuteList message to worker
  • Modify bootstrapMuteList() to also trigger worker-side load
  • Add handler for muteListUpdated worker 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 muteListUpdated event path

Pages (minimal changes)

  • www/vj.html — can remove the isEventMuted check in handleMusicNdkEvent() since worker pre-filters
  • www/feed.html, www/post.html, www/notifications.html, www/skills-edit.html — subscription events will arrive pre-filtered; existing isEventMuted() 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 badge
  • www/block.html — no subscription events; manages the mute list directly

Implementation Steps

  1. Add mute state and parsing to worker — port the parseTags() and isEventMuted() logic into ndk-worker.js as standalone functions
  2. Add handleLoadMuteList() to worker — fetch kind 10000, parse public tags, decrypt private content using messageSigner.requestFromPage('nip44Decrypt', ...)
  3. Filter in wireSubscriptionHandlers() — before broadcast(), check isWorkerEventMuted(event) and skip if true
  4. Call during init — add handleLoadMuteList(currentPubkey) call in handleInit() after relay setup
  5. Add syncMuteList message type — so pages can trigger a reload after publishing mute changes
  6. Broadcast muteListUpdated — after worker loads/reloads mute list, broadcast the parsed public mute data to all pages
  7. Wire init-ndk.mjs — handle muteListUpdated message, call syncWorkerMuteList() after addMute/removeMute
  8. 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 sends syncMuteList to 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.