4.2 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)]
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.