- Added JSDoc comments to `NostrClient` class and methods. - Documented state management (`activeMap`, `tombstones`), caching strategy, and subscription buffering. - Created `docs/nostr-client-overview.md` with a high-level architecture overview. - Verified with `tests/nostr-login-permissions.test.mjs`.
4.6 KiB
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).
Core Responsibilities
- Connection Management: Maintains persistent connections to multiple relays using
SimplePool. - State Management: Tracks video events, deletions (tombstones), and deduplicates content.
- Caching: Implements a "stale-while-revalidate" strategy using IndexedDB (
EventsCacheStore) to load content immediately while fetching updates in the background. - Publishing: Orchestrates the signing and broadcasting of events, including complex multi-event flows (e.g., Video + Mirror + NIP-71).
- Signer Management: Abstracts the differences between NIP-07 (Extension), NIP-46 (Remote/Bunker), and Local (nsec) signers.
State Architecture
The client maintains several in-memory maps to manage state:
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 orpubkey: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.
Caching Strategy
To ensure a snappy UX, the client uses EventsCacheStore to persist allEvents and tombstones to IndexedDB.
- Load: On
init(), the client loads the snapshot from IndexedDB into memory. - Fingerprinting: To minimize write overhead, objects are fingerprinted. Only changed items are written back to disk.
- Persistence: Changes are persisted periodically (debounced) or on critical actions (like a flush after a batch subscription).
Subscription Model: Buffering & Debouncing
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:
- Buffer: Incoming events are pushed into an
eventBufferarray. - Debounce: A flush is scheduled (e.g., 75ms delay).
- Flush: When the timer fires, the entire buffer is processed in a batch:
- Events are converted to
Videoobjects. activeMapis updated (latest-wins logic).onVideocallback is fired once for the batch or per-item, but React updates are batched effectively.- State is persisted to the local cache.
- Events are converted to
Publishing Lifecycle
Publishing a video is a multi-step process to ensure compatibility and discoverability:
- Video Note (Kind 30078): The primary payload containing metadata (title, magnet, etc.).
- 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.
- NIP-71 Metadata (Kind 22): (Optional) If the user adds categories or tags, a separate metadata event is linked to the video.
Code Example
import { NostrClient } from "./js/nostr/client.js";
const client = new NostrClient();
// 1. Initialize (loads cache, connects to relays)
await client.init();
// 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
activeMapensures 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
Videoobject inallEventsis a normalized, app-specific representation.