- Add JSDoc to exported methods in `js/nostr/client.js` - Add JSDoc to `js/nostr/videoEventBuffer.js` - Add in-code comments for complex algorithms (history hydration, soft/hard delete, buffering) - Update `docs/nostr-client-overview.md` with invariants and pitfalls - No functional changes Co-authored-by: PR0M3TH3AN <[email protected]>
8.4 KiB
NostrClient Architecture Overview
The NostrClient (js/nostr/client.js) is the central controller for all Nostr network interactions in the application. It acts as a singleton facade over the nostr-tools library, managing relay connections, event subscriptions, state synchronization, and cryptographic signing.
Core Responsibilities
- Connection Management: Maintains a persistent connection pool (
SimplePool) to multiple relays. - State Management: Tracks the global state of video events, including history, active versions, and deletions.
- Event Ingestion: Handles high-volume event streams with buffering and deduplication.
- Caching: Implements a persistent IndexedDB cache to support "Stale-While-Revalidate" loading (instant UI render).
- Signer Orchestration: Manages NIP-07 (Browser Extension), NIP-46 (Remote Signer), and Local (nsec) signers seamlessly.
State Management
The client maintains three primary state maps to ensure data consistency:
-
allEvents(Map<string, VideoObject>):- Stores every valid video event fetched from relays, indexed by Event ID.
- Used for history preservation and resolving direct links to specific versions.
-
activeMap(Map<string, VideoObject>):- The "materialized view" used by the UI.
- Keys are
videoRootId(prefixed withROOT:) for versioned edits, orpubkey:dTagfor addressable events. - Values are the latest valid version of the video.
- Invariant: Only one entry exists per "video entity", ensuring the UI displays the most recent edit.
-
tombstones(Map<string, timestamp>):- Tracks deletions (Kind 5).
- Keys are the
activeKeyof the deleted item. - Values are the timestamp of the deletion.
- Invariant: Events older than the tombstone are strictly ignored, preventing "zombie" events from reappearing.
Data Ingestion Flow
The subscribeVideos method implements a buffered ingestion pipeline to handle high-throughput streams without freezing the UI:
- Subscription: Opens a subscription to all connected relays.
- Buffering: Incoming events are pushed to an
eventBufferarray immediately. - Debouncing: A timer flushes the buffer every ~75ms (or when full).
- Processing (Flush):
- Events are parsed and validated.
- Tombstone Check: If an event predates a known deletion, it is discarded.
- Latest-Wins Resolution: The new event's
created_atis compared against the existing entry inactiveMap. - Update: If the new event is newer,
activeMapis updated, and the UI callback is triggered.
- Persistence: The updated state is asynchronously persisted to IndexedDB.
graph TD
Relay[Relays] -->|Event Stream| Buffer[Event Buffer]
Buffer -->|Debounce Timer| Processor[Processor]
Processor -->|Check| Tombstones{Is Deleted?}
Tombstones -->|Yes| Discard[Discard]
Tombstones -->|No| Compare{Is Newer?}
Compare -->|No| Discard
Compare -->|Yes| ActiveMap[Update ActiveMap]
ActiveMap --> UI[Update UI]
ActiveMap --> IDB[(IndexedDB Cache)]
Publishing Lifecycle
Creating and editing content involves a coordinated sequence of events to satisfy multiple NIPs:
- Preparation: The client normalizes the input (Title, Magnet, URL) and generates a unique
dtag (NIP-33) andvideoRootId(V3 Schema). - Signing: The payload is signed by the active signer (Extension, NIP-46, or Local).
- Primary Publish: The main Kind 30078 event is broadcast to relays.
- Mirroring (NIP-94): If a hosted URL is present, a Kind 1063 "File Header" event is also published. This ensures compatibility with generic file-sharing clients.
- Tagging (NIP-71): If categories/tags are used, a Kind 22 event is published referencing the video. This keeps the main event payload lightweight.
Deletion & Versioning
The client implements a robust deletion strategy to handle the immutable nature of Nostr events:
Versioning
Edits are new events that share the same d tag and videoRootId. The client's hydrateVideoHistory method reconstructs the timeline by fetching all events with the matching d tag and sorting by created_at.
Soft Delete (Revert)
The revertVideo method publishes a new version of the video where the content is replaced with a tombstone marker (deleted: true). This hides the video from the feed while preserving the history chain.
Hard Delete (Nuclear Option)
The deleteAllVersions method attempts to physically remove data from relays (NIP-09):
- Hydration: Fetches the full history to identify every Event ID (
etags) and Address (atags). - Tombstone: Publishes a "Soft Delete" event to ensure immediate hiding.
- Pruning: Publishes a Kind 5 (Deletion) event listing all historical IDs. Relays respecting NIP-09 will delete the data.
- Local Guard: A local tombstone is recorded to prevent the client from re-displaying cached versions.
Direct Messages
The client supports a dual-stack DM system to ensure compatibility and privacy:
-
NIP-04 (Legacy):
- Uses simple ECDH encryption (Kind 4).
- Pros: Supported by almost all clients.
- Cons: Leaks metadata (who is talking to whom).
- Usage: Default fallback for text messages.
-
NIP-17 (Private / Gift Wrap):
- Uses a "Russian Doll" onion encryption scheme (Kind 1059 -> Kind 13 -> Kind 14).
- Pros: Hides sender, receiver, and content from relays.
- Cons: Higher complexity and bandwidth.
- Usage: Required for attachments (images/videos) and strictly private conversations.
The sendDirectMessage method automatically selects the appropriate protocol based on the content (e.g., attachments force NIP-17).
Caching Strategy (EventsCacheStore)
To provide an "app-like" experience, the client persists its state to IndexedDB (bitvid-events-cache).
- Fingerprinting: To minimize I/O, the store tracks JSON fingerprints (hashes) of all persisted events.
- Incremental Persistence: Only changed events (where fingerprint differs) are written to IDB during a snapshot.
- Stale-While-Revalidate: On startup (
init()), the client immediately loads data from IDB to render the UI, then connects to relays to fetch updates.
Signer Integration
The client abstracts the signing mechanism, allowing the user to switch between methods without changing application logic:
- NIP-07 (Extension): Delegates signing to a browser extension (e.g., Alby, nos2x).
- NIP-46 (Remote/Bunker): Uses a partial
NostrClientinstance to communicate with a remote signer via a relay. - Local (NIP-01): Uses a local private key (nsec) stored in
sessionStorage(encrypted).
The activeSigner object is normalized to expose a standard interface (signEvent, nip04Encrypt, nip44Encrypt), regardless of the underlying implementation.
Implicit Invariants
Maintaining these invariants is critical for the stability of the application:
- Active Map Consistency:
activeMapmust only contain the single latest version of a video series.- Violation: If two versions of the same video exist in
activeMap, the UI will duplicate the card or flicker.
- Violation: If two versions of the same video exist in
- Tombstone Authority: Any event, regardless of its source (relay or cache), MUST be discarded if its
created_atis older than the timestamp intombstones.get(activeKey).- Violation: Deleted videos will reappear ("zombie events").
- History Integrity: All versions in a history chain must share the same
videoRootId(V3) ordtag (V2).- Violation: The history log will be fragmented or incomplete.
Common Pitfalls
- Direct State Mutation:
- Don't:
client.activeMap.set(key, video)manually. - Do: Use
_processAndCacheEvent(evt)or letVideoEventBufferhandle it. The client needs to run deduplication and tombstone checks.
- Don't:
- Stable ID Assumption:
- Don't: Rely on
event.idas a permanent reference to a video. - Do: Use
videoRootIdor thedtag.event.idchanges every time the user edits the title/thumbnail.
- Don't: Rely on
- Sync vs Async State:
- Don't: Expect
client.allEventsto be populated immediately afterinit(). - Do: Use
restoreLocalData()(awaited ininit) which loads the cache, but network updates arrive asynchronously via subscriptions.
- Don't: Expect