Files
bitvid/docs/nostr-client-overview.md
T
google-labs-jules[bot]andPR0M3TH3AN a997120f42 chore(ai): doc js/nostr/client.js and videoEventBuffer.js
- 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]>
2026-02-12 15:28:54 +00:00

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

  1. Connection Management: Maintains a persistent connection pool (SimplePool) to multiple relays.
  2. State Management: Tracks the global state of video events, including history, active versions, and deletions.
  3. Event Ingestion: Handles high-volume event streams with buffering and deduplication.
  4. Caching: Implements a persistent IndexedDB cache to support "Stale-While-Revalidate" loading (instant UI render).
  5. 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 with ROOT:) for versioned edits, or pubkey:dTag for 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 activeKey of 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:

  1. Subscription: Opens a subscription to all connected relays.
  2. Buffering: Incoming events are pushed to an eventBuffer array immediately.
  3. Debouncing: A timer flushes the buffer every ~75ms (or when full).
  4. 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_at is compared against the existing entry in activeMap.
    • Update: If the new event is newer, activeMap is updated, and the UI callback is triggered.
  5. 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:

  1. Preparation: The client normalizes the input (Title, Magnet, URL) and generates a unique d tag (NIP-33) and videoRootId (V3 Schema).
  2. Signing: The payload is signed by the active signer (Extension, NIP-46, or Local).
  3. Primary Publish: The main Kind 30078 event is broadcast to relays.
  4. 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.
  5. 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):

  1. Hydration: Fetches the full history to identify every Event ID (e tags) and Address (a tags).
  2. Tombstone: Publishes a "Soft Delete" event to ensure immediate hiding.
  3. Pruning: Publishes a Kind 5 (Deletion) event listing all historical IDs. Relays respecting NIP-09 will delete the data.
  4. 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:

  1. NIP-07 (Extension): Delegates signing to a browser extension (e.g., Alby, nos2x).
  2. NIP-46 (Remote/Bunker): Uses a partial NostrClient instance to communicate with a remote signer via a relay.
  3. 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:

  1. Active Map Consistency: activeMap must 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.
  2. Tombstone Authority: Any event, regardless of its source (relay or cache), MUST be discarded if its created_at is older than the timestamp in tombstones.get(activeKey).
    • Violation: Deleted videos will reappear ("zombie events").
  3. History Integrity: All versions in a history chain must share the same videoRootId (V3) or d tag (V2).
    • Violation: The history log will be fragmented or incomplete.

Common Pitfalls

  1. Direct State Mutation:
    • Don't: client.activeMap.set(key, video) manually.
    • Do: Use _processAndCacheEvent(evt) or let VideoEventBuffer handle it. The client needs to run deduplication and tombstone checks.
  2. Stable ID Assumption:
    • Don't: Rely on event.id as a permanent reference to a video.
    • Do: Use videoRootId or the d tag. event.id changes every time the user edits the title/thumbnail.
  3. Sync vs Async State:
    • Don't: Expect client.allEvents to be populated immediately after init().
    • Do: Use restoreLocalData() (awaited in init) which loads the cache, but network updates arrive asynchronously via subscriptions.