Files
bitvid/docs/nostr-client-overview.md
T

5.4 KiB

NostrClient Overview

js/nostr/client.js exports the NostrClient class, which serves as the central controller for all Nostr network interactions and state management within the application. It acts as a facade over nostr-tools, managing connections, subscriptions, caching, and signing.

Key Responsibilities

  1. Connection Management: Maintains a pool of connections to multiple relays (SimplePool).
  2. State Management: Tracks video events, deduplicates them, and manages deletions via tombstones.
  3. Caching: Implements a "stale-while-revalidate" strategy using IndexedDB (with localStorage fallback) to restore UI state immediately upon load.
  4. Event Publishing: Orchestrates the signing and broadcasting of complex event chains (Video Note -> NIP-94 Mirror -> NIP-71 Metadata).
  5. Signer Abstraction: Unifies NIP-07 (Extension), NIP-46 (Remote/Bunker), and Local (nsec) signing under a single interface.
  6. Video History: Reconstructs the edit history of videos using videoRootId and d tags.

Architecture & State

The client maintains several internal maps to manage state:

  • allEvents: A raw map of all fetched video events (key: event ID).
  • activeMap: A derived map containing only the latest version of each video, keyed by a stable identifier (videoRootId or addressable pointer).
  • rawEvents: A cache of the exact raw JSON events from relays, used for signature verification and re-publishing.
  • tombstones: Tracks timestamps of deletion events to prevent "zombie" events (deleted events reappearing from slow relays) from resurfacing.

Caching Strategy (EventsCacheStore)

To ensure instant load times, the client persists its state to IndexedDB (bitvid-events-cache).

  • Snapshot: On a debounced timer (or idle callback), the current state (allEvents, tombstones) is serialized.
  • Fingerprinting: Events are fingerprinted to avoid writing unchanged data to the DB.
  • Restore: On init(), the client loads the latest snapshot into memory, allowing the UI to render before any network requests complete.

Key Flows

1. Initialization (init)

graph TD
    A[init()] --> B[restoreLocalData]
    B --> C{IndexedDB Available?}
    C -- Yes --> D[Load Snapshot]
    C -- No --> E[Load localStorage]
    D & E --> F[Render UI with Cached Data]
    F --> G[ensurePool]
    G --> H[connectToRelays]
    H --> I[Ready for Subscriptions]

2. Video Subscription (subscribeVideos)

To prevent UI thrashing during high-volume event ingress (e.g., initial flood from relays), the client uses a buffering and debouncing strategy.

  1. Buffer: Incoming events are pushed to eventBuffer instead of being processed immediately.
  2. Debounce: A flush timer is scheduled (75ms).
  3. Flush: When the timer fires, the entire buffer is processed:
    • Events are validated and converted.
    • activeMap is updated (latest-wins logic).
    • onVideo callback is fired once with the batch updates.
    • Cache persistence is scheduled.

3. Publishing a Video (publishVideo)

Publishing is a multi-step process ensuring compatibility and discoverability.

  1. Video Note (Kind 30078): The core content (magnet, infohash, metadata) is signed and published. A unique videoRootId is generated for new videos.
  2. NIP-94 Mirror (Kind 1063): If a direct HTTP URL is provided, a file header event is published to point to the hosted file.
  3. NIP-71 Metadata (Kind 22/21): Addressable metadata tags are published to support categorization/grouping without modifying the immutable video note.

4. Remote Signer Handshake (connectRemoteSigner)

For NIP-46 (Nostr Connect), the client implements a full handshake flow:

  1. Ephemeral Key: Generates a local key pair for the session.
  2. Handshake: Publishes a Kind 24133 (NIP46_RPC_KIND) event to the relay.
  3. Wait: Listens for an acknowledgment or auth_url challenge from the signer.
  4. Session: Once connected, stores the session metadata to allow auto-reconnection.

Why it works this way

  • Buffering: essential for performance. React rendering cycles are expensive; updating state for every single event in a 5000-event relay dump would freeze the browser.
  • Tombstones: essential for eventual consistency. Relays are distributed and can be out of sync. If a user deletes a video, we must remember that deletion timestamp so a slow relay doesn't "undelete" it by serving the old event later.
  • Dual-Layer Cache: IndexedDB is asynchronous and large-capacity; localStorage is synchronous but small. We use IndexedDB for the bulk data but keep a fallback for environments where IDB might be flaky or slow to initialize.

When to change

  • Refactor EventsCacheStore: If moving to a more robust state management library (like RxDB or dedicated worker), this logic should be extracted.
  • New Event Kinds: If the video data model changes (e.g., V4 schema), convertEventToVideo and publishVideo will need updates.
  • Signer upgrades: As NIP-46 evolves (e.g., Bunker standards), the handshake and connection logic in nip46Client.js (and its orchestration here) will need adjustment.
  • js/nostrEventSchemas.js: Defines the structure of the events being built/parsed.
  • js/nostr/nip46Client.js: Handles the low-level RPC for remote signers.
  • js/state/profileCache.js: Handles user profile metadata (Kind 0), which is separate from video data.