# NostrClient Overview `js/nostr/client.js` exports the `NostrClient` class, which serves as the central controller for all Nostr protocol interactions in the application. It acts as a bridge between the UI components, the local state (React/Redux/Listeners), and the decentralized Nostr network. ## Responsibilities 1. **Connection Management**: * Maintains a persistent connection pool (`SimplePool` from `nostr-tools`) to multiple relays. * Handles relay discovery, connection retries, and error reporting. * Supports "Read" vs. "Write" relay splitting. 2. **Event Publishing (Write Path)**: * Constructs spec-compliant Nostr events (Kind 1, 30078, etc.). * Handles **signing**: * NIP-07 (Browser Extensions like Alby/nos2x). * NIP-46 (Remote Signers / Bunker). * Local Private Keys (nsec). * Broadcasts events to write relays and verifies acceptance. * Implements **NIP-94** (File Metadata) and **NIP-71** (Video Categorization) mirroring. 3. **Data Fetching & Subscription (Read Path)**: * **Live Feeds**: Uses persistent subscriptions (`sub`) to stream new events. * **Buffering**: Implements a debounced buffer strategy (`subscribeVideos`) to handle relay floods without freezing the UI. * **Caching**: Stores events in `allEvents` (memory) and persists to IndexedDB/LocalStorage for instant load ("Stale-While-Revalidate"). 4. **State Management**: * **`allEvents`**: Map of all raw video events fetched, keyed by Event ID. * **`activeMap`**: A "materialized view" of the latest video state. It deduplicates multiple versions of the same video (edits) based on `videoRootId` or `d` tag, ensuring the UI only shows the most recent valid version. * **`tombstones`**: Tracks deleted events to prevent "zombie" events from reappearing during partial syncs. ## Key Architectures ### 1. The "Active Map" & Deduplication Nostr is immutable; "editing" a video means publishing a NEW event with the same identifier (`d` tag). The client listens to *all* versions but only keeps the latest one in `activeMap`. ```javascript // Simplified Logic if (incomingEvent.createdAt > currentActiveEvent.createdAt) { activeMap.set(videoRootId, incomingEvent); notifyUI(); } ``` ### 2. Buffering Strategy When the app loads, relays might send 1000+ events in a second. Rendering React components 1000 times is too slow. `subscribeVideos` pushes events to a buffer and flushes them every 75ms. ```javascript sub.on("event", (evt) => { buffer.push(evt); debounce(flushBuffer, 75); }); ``` ### 3. Tombstoning (Deletes) Since relays are eventually consistent, a "delete" event (Kind 5) might arrive *before* the event it deletes, or *after*. The client tracks `tombstones` (map of ID -> deletedAt timestamp). Any incoming event older than its tombstone is immediately discarded. ## Main Entry Points * `init()`: Bootstraps the client, restores local cache, connects to relays. * `subscribeVideos(callback)`: Main feed subscription. * `publishVideo(payload)`: Complex flow to upload -> sign -> publish -> mirror. * `connectRemoteSigner(uri)`: Handshake protocol for NIP-46. ## Why it works this way ### 1. Immutability & "Active Map" Nostr events are immutable signed messages. To support "edits", we must allow multiple events to represent the same "entity" (Video). - **The Trade-off**: We accept higher memory usage (storing multiple versions in `allEvents`) to gain strict cryptographic verifiability. - **The Solution**: `activeMap` acts as a "Latest-State Projection" (Materialized View) that the UI consumes. This decouples the network reality (append-only log) from the user expectation (mutable state). ### 2. Buffering vs. Streaming Directly piping relay events to React State (`setState`) triggers a Re-Render Cycle for every message. - **The Bottleneck**: Initial loads often fetch 500-2000 events in <2 seconds. - **The Solution**: A 75ms debounce buffer collects bursts and processes them in one microtask. This turns 1000 re-renders into ~10-15, keeping the UI thread responsive (60fps). ### 3. Dual-Layer Caching (IDB + Memory) Relays are slow and sometimes fail. - **Offline-First**: We restore from IndexedDB immediately on boot (`init`). - **Fingerprinting**: We hash events before writing to IDB. If `JSON.stringify(video)` hasn't changed, we skip the write syscall. This reduces I/O freezing on mobile devices. ## Advanced Topics ### Version History Reconstruction (`hydrateVideoHistory`) Since edits create new events, a video might have 10+ versions scattered across relays. `hydrateVideoHistory` stitches them together by: 1. **Scanning Local Cache**: Finds all events matching the `videoRootId`. 2. **Relay Fallback**: If local history is sparse, queries relays for `kind: 30078` + `#d` tag. 3. **Root Recovery**: Explicitly fetches the original "Root" event if missing, to establish the timeline start. ### Optimized List Fetching (`fetchListIncrementally`) Used for things like Mute Lists (Kind 10000) or Pin Lists. - **Last-Seen Check**: Tracks the `created_at` of the last item seen from *each* relay. - **Delta Sync**: Asks for `since: lastSeen + 1`. - **Fallback**: If the delta sync fails, performs a full fetch to ensure consistency. - **Concurrency**: Batches requests to 4 relays at a time to prevent browser network saturation. ### Video Deletion Flow (`deleteAllVersions`) Nostr's immutability makes "deleting" a video series complex, as multiple edit versions might exist across relays. `deleteAllVersions` handles this via a multi-stage process: 1. **Hydration**: Calls `hydrateVideoHistory` to find *every* event related to the video (scanning by `videoRootId` and `d` tag). 2. **Soft Delete (Revert)**: For each unique version identifier (Root + D-Tag combination), it publishes a new event with `deleted: true` and cleared content. This ensures clients resolving the "latest" version see the deletion immediately. 3. **Hard Delete (NIP-09)**: Publishes a Kind 5 Deletion Request referencing the Event IDs of *all* versions found. Compliant relays will physically remove the data. 4. **Tombstoning**: Updates the local `tombstones` map with the deletion timestamp to preventing caching mechanisms from resurrecting old versions. ```mermaid sequenceDiagram participant UI as VideoEditModal participant Client as NostrClient participant Relay UI->>Client: deleteAllVersions(rootId) activate Client Note right of Client: 1. Find all versions Client->>Client: hydrateVideoHistory(video) Client->>Relay: Query (Kind 30078 + #d) Relay-->>Client: Events [v1, v2, v3] Note right of Client: 2. Soft Delete (Revert) loop For each unique branch Client->>Relay: Publish Kind 30078 (deleted=true) end Note right of Client: 3. Hard Delete (NIP-09) Client->>Relay: Publish Kind 5 (referencing v1, v2, v3 ids) Note right of Client: 4. Local Cleanup Client->>Client: recordTombstone(rootId) Client->>Client: saveLocalData() Client-->>UI: Success deactivate Client ``` ## When to change - **Update `EVENTS_CACHE_VERSION`**: If you change the internal `Video` object structure (e.g., rename fields in `convertEventToVideo`), bump the DB version in `EventsCacheStore` to force a wipe/rebuild on client devices. - **Modify `subscribeVideos`**: If adding new filters (e.g., "Kids Only Mode"), update the subscription filter here. Ensure the buffer logic remains to protect performance. - **Refactor Signers**: If NIP-46 changes or a new signer standard (NIP-XX) emerges, modify `signerHelpers.js` and `client.js` methods (`signAndPublishEvent`). ## Sequence Example: Publish Video Publishing is not atomic; it's a multi-step orchestration to ensure compatibility across different Nostr clients (WebSeed vs. File Metadata vs. Categorization). ```mermaid sequenceDiagram participant UI as VideoUploadModal participant Client as NostrClient participant Signer as NIP-07/46 participant Relay UI->>Client: publishVideo(payload) activate Client Note right of Client: 1. Construct Main Video Note Client->>Client: buildVideoPostEvent (Kind 30078) Client->>Signer: signEvent(Kind 30078) Signer-->>Client: signedEvent Client->>Relay: Publish Kind 30078 Note right of Client: 2. NIP-94 Mirror (Optional) opt has hosted URL Client->>Client: buildMirrorEvent (Kind 1063) Client->>Signer: signEvent(Kind 1063) Signer-->>Client: signedMirror Client->>Relay: Publish Kind 1063 end Note right of Client: 3. NIP-71 Metadata (Optional) opt has tags/categories Client->>Client: buildNip71Event (Kind 22) Client->>Signer: signEvent(Kind 22) Signer-->>Client: signedWrapper Client->>Relay: Publish Kind 22 end Client-->>UI: Success (returns Main Event) deactivate Client ```