From fb95d5c9141eafe19de7d33a56b884176164d365 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:10:38 +0000 Subject: [PATCH] chore(ai): doc js/nostr/client.js and nip71.js (agent) --- docs/nostr-client-overview.md | 106 +++++++++++++++------------------- js/nostr/client.js | 1 + js/nostr/nip71.js | 65 +++++++++++++++++++++ 3 files changed, 113 insertions(+), 59 deletions(-) diff --git a/docs/nostr-client-overview.md b/docs/nostr-client-overview.md index 9e4bc198..9a4d67c9 100644 --- a/docs/nostr-client-overview.md +++ b/docs/nostr-client-overview.md @@ -1,80 +1,68 @@ # NostrClient Overview -The `NostrClient` (`js/nostr/client.js`) is the central controller for all Nostr network interactions, state management, and cryptographic operations in the application. It acts as a facade over `nostr-tools` and manages the complexity of connecting to relays, syncing data, and handling various NIPs (Nostr Implementation Possibilities). +The `NostrClient` class in `js/nostr/client.js` is the central controller for all Nostr protocol interactions in the application. It handles connection management, event publishing, state synchronization, and caching. -## Core Responsibilities +## Key Responsibilities -1. **Connection Management**: Maintains persistent connections to multiple relays using `SimplePool`. -2. **State Management**: Tracks video events, deletions (tombstones), and deduplicates content. -3. **Caching**: Implements a "stale-while-revalidate" strategy using IndexedDB (`EventsCacheStore`) to load content immediately while fetching updates in the background. -4. **Publishing**: Orchestrates the signing and broadcasting of events, including complex multi-event flows (e.g., Video + Mirror + NIP-71). -5. **Signer Management**: Abstracts the differences between NIP-07 (Extension), NIP-46 (Remote/Bunker), and Local (nsec) signers. +1. **Connection Management**: Maintains a pool of connections to Nostr relays (`SimplePool`). +2. **State Management**: + * `allEvents`: A raw map of all fetched events, useful for history. + * `activeMap`: A "materialized view" containing only the latest version of each video (deduplicated by `videoRootId` or `d` tag). + * `tombstones`: Tracks deleted events to prevent them from reappearing if an old relay broadcasts them. +3. **Event Publishing**: Handles signing and broadcasting events (Kind 30078 video posts, Kind 1 metadata, etc.). +4. **Buffering**: Implements a buffering strategy for incoming events to optimize UI rendering performance. +5. **Caching**: Uses `EventsCacheStore` (IndexedDB + localStorage) to persist state across sessions for instant load times. +6. **Signer Abstraction**: Manages different signing methods (NIP-07 extension, NIP-46 remote signer, local keys). -## State Architecture +## Core Concepts -The client maintains several in-memory maps to manage state: +### Eventual Consistency & `activeMap` -* **`allEvents`** (`Map`): A raw collection of all fetched video events, keyed by Event ID. This includes history (older versions) and is used for hydration. -* **`activeMap`** (`Map`): The "materialized view" used by the UI. It maps a unique key (Video Root ID or `pubkey:dTag`) to the *latest* valid version of a video. It automatically handles deduplication and versioning. -* **`rawEvents`** (`Map`): caches the exact raw JSON events received from relays. This is crucial for signature verification and republishing (e.g., NIP-94 mirrors) without altering the original signature. -* **`tombstones`** (`Map`): Tracks the timestamp of deletions. This prevents older versions of a video from "resurrecting" if a relay sends them after a user has deleted the video. +Nostr is a decentralized protocol where events can arrive out of order or be duplicated across relays. The `activeMap` is critical for presenting a consistent view to the user. -## Caching Strategy +* **Logic**: When a new event arrives, `activeMap` only updates if the new event has a newer `created_at` timestamp than the existing entry for that `videoRootId` (or `d` tag). +* **Deletions**: If an event is deleted (Kind 5 or explicit `deleted: true` flag), a "tombstone" is recorded. Future events with a timestamp older than the tombstone are ignored. -To ensure a snappy UX, the client uses `EventsCacheStore` to persist `allEvents` and `tombstones` to IndexedDB. +### Buffering Strategy (`subscribeVideos`) -1. **Load**: On `init()`, the client loads the snapshot from IndexedDB into memory. -2. **Fingerprinting**: To minimize write overhead, objects are fingerprinted. Only changed items are written back to disk. -3. **Persistence**: Changes are persisted periodically (debounced) or on critical actions (like a flush after a batch subscription). +To prevent the UI from freezing when thousands of events arrive during the initial connection: -## Subscription Model: Buffering & Debouncing +1. Incoming events are pushed into an `eventBuffer`. +2. A debounced flush function processes the buffer every ~75ms. +3. Events are processed in batches: validation, deduplication, and state updates happen in bulk. +4. The UI is notified only once per batch. -Relays often send events in bursts (e.g., thousands of events on initial load). Processing each event individually would freeze the UI. `NostrClient` implements a **Buffering & Debouncing** strategy in `subscribeVideos`: +### Caching (`EventsCacheStore`) -1. **Buffer**: Incoming events are pushed into an `eventBuffer` array. -2. **Debounce**: A flush is scheduled (e.g., 75ms delay). -3. **Flush**: When the timer fires, the entire buffer is processed in a batch: - * Events are converted to `Video` objects. - * `activeMap` is updated (latest-wins logic). - * `onVideo` callback is fired once for the batch or per-item, but React updates are batched effectively. - * State is persisted to the local cache. +The client attempts to restore state immediately on load ("Stale-While-Revalidate"): -## Publishing Lifecycle +1. **Restore**: `restoreLocalData` reads from IndexedDB (or localStorage fallback). +2. **Display**: The UI renders cached content immediately. +3. **Fetch**: The client connects to relays and fetches new events, updating the UI as they arrive. +4. **Persist**: The `EventsCacheStore` periodically saves the current state to disk, using fingerprints to avoid unnecessary writes. -Publishing a video is a multi-step process to ensure compatibility and discoverability: +## Common Workflows -1. **Video Note (Kind 30078)**: The primary payload containing metadata (title, magnet, etc.). -2. **NIP-94 Mirror (Kind 1063)**: (Optional) If a HTTP URL is provided, a "File Header" event is published to support clients that only rely on NIP-94. -3. **NIP-71 Metadata (Kind 22)**: (Optional) If the user adds categories or tags, a separate metadata event is linked to the video. +### 1. Publishing a Video +`publishVideo(videoPayload, pubkey)` -## Code Example +1. Constructs the video metadata (Kind 30078). +2. Signs the event using the active signer. +3. Publishes to configured relays. +4. **Side Effects**: + * If a hosted URL is present, publishes a NIP-94 mirror event (Kind 1063). + * If NIP-71 categorization tags are present, publishes a wrapper event (Kind 22). -```javascript -import { NostrClient } from "./js/nostr/client.js"; +### 2. Loading the Feed +`subscribeVideos(onVideo, options)` -const client = new NostrClient(); +1. Checks cache for the latest timestamp. +2. Subscribes to relays with `since` filter (if cached) or full history. +3. Buffers incoming events. +4. Updates `activeMap` and calls `onVideo` for valid updates. -// 1. Initialize (loads cache, connects to relays) -await client.init(); +## When to Modify -// 2. Subscribe to videos -const sub = client.subscribeVideos((video) => { - console.log("New video received:", video.title); -}); - -// 3. Login (NIP-07) -await client.login(); - -// 4. Publish a video -await client.publishVideo({ - title: "My Video", - magnet: "magnet:?xt=urn:btih:...", - // ... -}, client.pubkey); -``` - -## Why it works this way - -* **Active Map**: The `activeMap` ensures that if a user edits a video (publishing a new version), the UI automatically shows the new one and hides the old one, even if relays send them out of order. -* **Tombstones**: Distributed systems are eventually consistent. A relay might send a "deleted" video event *after* we've processed the deletion locally. Tombstones ensure we ignore it. -* **Raw Events**: We never modify the original event object for verification purposes. The `Video` object in `allEvents` is a normalized, app-specific representation. +* **New Event Kinds**: If adding support for a new NIP or event type, add handlers here. +* **Performance Tuning**: Adjust buffering timeouts or cache policies in `EventsCacheStore` if UI responsiveness issues arise. +* **Signer Logic**: If modifying how keys are handled (e.g., adding a new wallet adapter), update `signerHelpers.js` or `nip46Client.js`, but ensure `NostrClient` integrates it correctly. diff --git a/js/nostr/client.js b/js/nostr/client.js index 7f048343..c4ff9a45 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -4,6 +4,7 @@ * NostrClient * * The central controller for all Nostr network interactions and state management. + * For a high-level overview, see `docs/nostr-client-overview.md`. * * Responsibilities: * - Connection Management: Maintains the connection pool to Relays. diff --git a/js/nostr/nip71.js b/js/nostr/nip71.js index 9935642e..3f528c6f 100644 --- a/js/nostr/nip71.js +++ b/js/nostr/nip71.js @@ -290,6 +290,22 @@ function buildParticipantTag(participant) { return values; } +/** + * Converts a NIP-71 metadata object into Nostr tags. + * + * Mapping: + * - `title` -> `['title', ...]` + * - `publishedAt` -> `['published_at', ...]` + * - `imeta` -> `['imeta', 'url', ..., 'dim', ...]` (Video variants) + * - `textTracks` -> `['text-track', ...]` + * - `segments` -> `['segment', ...]` + * - `hashtags` -> `['t', ...]` + * - `participants` -> `['p', ...]` + * - `references` -> `['r', ...]` + * + * @param {object} metadata - The metadata object (parsed structure). + * @returns {string[][]} Array of tags ready for the event. + */ export function buildNip71MetadataTags(metadata) { if (!metadata || typeof metadata !== "object") { return []; @@ -571,6 +587,16 @@ function parseHashtagTag(tag) { return value || null; } +/** + * Parses a NIP-71 event (Kind 22 or 21) into a structured metadata object. + * + * Extracts: + * - Standard NIP-71 tags (title, imeta, segments, etc.) + * - Pointer references (a, e, d tags) used to link this metadata to the video. + * + * @param {import("nostr-tools").Event} event - The raw Nostr event. + * @returns {{metadata: object, pointers: object, source: object}|null} The parsed structure or null if invalid. + */ export function extractNip71MetadataFromTags(event) { if (!event || typeof event !== "object") { return null; @@ -840,6 +866,21 @@ export function buildNip71PointerTags({ return pointerTags; } +/** + * Constructs a Kind 22 (Video Wrapper) or Kind 21 (Video Segment) event. + * + * This event holds the NIP-71 categorization and metadata, separate from the + * playable video note (Kind 30078). It links to the video via `a` or `e` tags. + * + * @param {object} params + * @param {object} params.metadata - The NIP-71 metadata object. + * @param {string} params.pubkey - The publisher's public key. + * @param {string} params.title - The title (required by NIP-71). + * @param {string} [params.summaryFallback] - Fallback content if summary is missing. + * @param {number} [params.createdAt] - Timestamp. + * @param {object} [params.pointerIdentifiers] - Pointers to the target video ({videoRootId, eventId, dTag}). + * @returns {import("nostr-tools").Event|null} The unsigned event object. + */ export function buildNip71VideoEvent({ metadata, pubkey = "", @@ -1011,6 +1052,19 @@ function storeNip71RecordForRoot(nip71Cache, videoRootId, parsedRecord) { } } +/** + * Processes a batch of NIP-71 events and updates the cache. + * + * Logic: + * - Parses each event. + * - Identifies which video root(s) it belongs to (via `a` tag or cache lookup). + * - Updates `nip71Cache` with the most recent valid metadata for that root. + * + * @param {import("nostr-tools").Event[]} events - List of fetched events. + * @param {object} context + * @param {Map} context.nip71Cache - The shared metadata cache. + * @param {Map} [context.pointerMap] - Map of pointer values to video info (optimization). + */ export function processNip71Events(events, { nip71Cache, pointerMap = null } = {}) { if (!Array.isArray(events) || !events.length || !(nip71Cache instanceof Map)) { return; @@ -1045,6 +1099,17 @@ export function processNip71Events(events, { nip71Cache, pointerMap = null } = { }); } +/** + * Hydrates a video object with cached NIP-71 metadata. + * + * If metadata exists for the video's `videoRootId`, `eventId`, or `d` tag, + * it is merged into `video.nip71` and `video.nip71Source`. + * + * @param {object} video - The video object to mutate. + * @param {object} context + * @param {Map} context.nip71Cache - The metadata cache. + * @returns {object} The mutated video object. + */ export function mergeNip71MetadataIntoVideo(video, { nip71Cache } = {}) { if (!video || typeof video !== "object") { return video;