Merge pull request #1465 from PR0M3TH3AN/ai/doc-nostr-client-20231027-549968251283674277

chore(ai): document js/nostr/client.js and nip71.js
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-21 12:01:39 -05:00
commit b7b691d9cc
3 files changed
+113 -59

No files matched your search

+47 -59
View File
@@ -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<string, Video>`): A raw collection of all fetched video events, keyed by Event ID. This includes history (older versions) and is used for hydration.
* **`activeMap`** (`Map<string, Video>`): 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<string, Event>`): 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<string, number>`): 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.
+1
View File
@@ -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.
+65
View File
@@ -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;