Merge pull request #1442 from PR0M3TH3AN/ai/doc-client-20231026-7119633933241753888

chore(ai): document js/nostr/client.js
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-20 11:19:08 -05:00
commit 5726997475
2 files changed
+96 -67

No files matched your search

+64 -63
View File
@@ -1,79 +1,80 @@
# NostrClient Overview
`NostrClient` (`js/nostr/client.js`) is the central controller for all Nostr network interactions and state management in the application. It acts as a facade over `nostr-tools`, managing connections to multiple relays, signing events, and maintaining the local application state.
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).
## Responsibilities
## Core Responsibilities
1. **Connection Management**: Maintains a pool of connections to configured Nostr relays. It handles initialization, reconnection, and error logging.
2. **Event Publishing**: Handles the creation, signing, and broadcasting of various Nostr event kinds (Video Post: 30078, Reaction: 7, Comment: 1, etc.). It supports NIP-07 (Extension), NIP-46 (Remote/Bunker), and NIP-01 (Local nsec) signers.
3. **State Management**:
* `allEvents`: A raw map of all fetched video events (Kind 30078), preserving history.
* `activeMap`: A derived map containing only the *latest* version of each video, deduplicated by `videoRootId` or `d` tag. This is the source of truth for the UI.
* `rawEvents`: A cache of the exact raw JSON events from relays, used for signature verification and republishing.
* `tombstones`: Tracks deletion timestamps to prevent deleted events from reappearing.
4. **Caching**: Implements a dual-layer cache (`IndexedDB` + `localStorage`) to persist `activeMap`, `allEvents`, and `tombstones`. This allows the app to restore its state instantly on load ("stale-while-revalidate").
5. **Video Lifecycle**: Manages the full lifecycle of a video post: Publish -> Edit (new event with same `d` tag) -> Revert (new event, same `d`, deleted flag) -> Delete (Kind 5).
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.
## Key Concepts
## State Architecture
### Active Video Logic
Nostr is an append-only protocol. Edits are new events. `NostrClient` implements "latest-wins" logic to present a coherent view:
- **Identifier**: Videos are identified by a `videoRootId` (in the content) or a NIP-33 `d` tag.
- **Resolution**: When a new event arrives, the client checks if it's newer than the current active version in `activeMap`. If so, it replaces the entry.
- **Deletions**: If an event has `deleted: true` or is a Kind 5 deletion, it is removed from `activeMap`, and a tombstone is recorded.
The client maintains several in-memory maps to manage state:
### Buffering & Debouncing
To handle high volumes of events (e.g., during initial relay dump), `subscribeVideos` uses a buffering strategy:
1. Incoming events are pushed to `eventBuffer`.
2. A flush is scheduled with a short debounce (e.g., 75ms).
3. On flush, the batch is processed: validation, state updates, and UI notification.
This prevents UI thrashing.
* **`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.
### Tombstones
Because relays are eventually consistent, a deleted event might arrive *after* a deletion command.
- `tombstones` map `activeKey` -> `timestamp`.
- If an incoming event is older than the tombstone for its key, it is ignored (`applyTombstoneGuard`).
## Caching Strategy
## Example Flow: `subscribeVideos`
To ensure a snappy UX, the client uses `EventsCacheStore` to persist `allEvents` and `tombstones` to IndexedDB.
```mermaid
sequenceDiagram
participant UI
participant Client as NostrClient
participant Buffer as EventBuffer
participant State as activeMap
participant Relays
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).
UI->>Client: subscribeVideos(onVideo)
Client->>Relays: REQ Kind 30078
Relays-->>Client: EVENT (Video A, v1)
Client->>Buffer: Push Event
Client->>Buffer: Schedule Flush (75ms)
Relays-->>Client: EVENT (Video B, v1)
Client->>Buffer: Push Event
## Subscription Model: Buffering & Debouncing
Note over Client, Buffer: 75ms passes...
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`:
Client->>Buffer: Flush()
loop For each event
Buffer->>State: Is newer?
alt Yes
State->>State: Update Entry
State->>UI: onVideo(Video)
else No
State->>State: Ignore
end
end
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.
## Publishing Lifecycle
Publishing a video is a multi-step process to ensure compatibility and discoverability:
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.
## Code Example
```javascript
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);
```
## When to Modify
## Why it works this way
- **Adding a new Event Kind**: If the app needs to support a new type of interaction (e.g., playlists), add the handling logic here and in `js/nostrEventSchemas.js`.
- **Changing Caching Strategy**: Modify `EventsCacheStore` or `persistLocalData` if the caching requirements change.
- **Updating Signing Logic**: Changes to NIP-07 or NIP-46 integration should be carefully reviewed here.
## Related Files
- `js/nostrEventSchemas.js`: Definitions of event structures.
- `js/nostr/nip71.js`: Helper logic for video categorization (NIP-71).
- `js/nostr/watchHistory.js`: Manages user watch history state.
* **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.
+32 -4
View File
@@ -1030,8 +1030,18 @@ export class NostrClient {
/**
* Initializes the NostrClient.
* Sets up connection pools, state maps, and default relays.
*
* The client manages the full lifecycle of Nostr interactions:
* - Connection pooling via `SimplePool`.
* - State tracking (allEvents, activeMap).
* - Signer negotiation (NIP-07, NIP-46, local).
* - Background caching with IndexedDB.
*/
constructor() {
/**
* @type {import("nostr-tools").SimplePool}
* The underlying pool of relay connections.
*/
this.pool = null;
this.poolPromise = null;
this.pubkey = null;
@@ -1043,13 +1053,15 @@ export class NostrClient {
* @type {Map<string, object>}
* Maps event ID to the converted Video object.
* Stores ALL fetched versions to ensure old links resolve.
* @public
*/
this.allEvents = new Map();
/**
* @type {Map<string, object>}
* @type {Map<string, import("nostr-tools").Event>}
* Maps event ID to the raw Nostr event object.
* Kept for signature verification and republishing (e.g. NIP-94 mirror).
* @public
*/
this.rawEvents = new Map();
@@ -1057,6 +1069,8 @@ export class NostrClient {
* @type {Map<string, object>}
* Maps `videoRootId` (or `pubkey:dTag`) to the latest valid Video object.
* This is the "materialized view" used by the UI.
* It automatically deduplicates versions, keeping only the most recent one.
* @public
*/
this.activeMap = new Map();
@@ -1064,6 +1078,8 @@ export class NostrClient {
* @type {Map<string, number>}
* Maps `activeKey` to the timestamp of its latest deletion.
* Prevents older events from reappearing after a delete.
* Critical for "Eventual Consistency" handling.
* @public
*/
this.tombstones = new Map();
@@ -3688,6 +3704,7 @@ export class NostrClient {
async init() {
devLogger.log("Connecting to relays...");
// 1. Restore cache for immediate UI render (Stale-While-Revalidate)
await this.restoreLocalData();
try {
@@ -5125,7 +5142,9 @@ export class NostrClient {
* - Core metadata (title, description, thumbnail).
* - `magnet`: The Magnet URI (XT=InfoHash, WS=WebSeed, XS=TorrentMetadata).
* - `url`: The direct HTTP URL (WebSeed).
* - NIP-94 Mirror: Optionally publishes a Kind 1063 file header event if supported.
*
* Side Effects:
* - NIP-94 Mirror: Optionally publishes a Kind 1063 file header event if a hosted URL is provided.
* - NIP-71: Optionally publishes a Kind 22 (Video Wrapper) for categorization if metadata is provided.
*
* @param {object} videoPayload - The video metadata and form data.
@@ -6397,7 +6416,9 @@ export class NostrClient {
* - Persist to local cache.
* - Notify the UI (`onVideo`).
*
* @param {Function} onVideo - Callback fired when new valid videos are processed. Receives the `Video` object.
* This ensures O(1) renders per batch instead of O(N) renders per event.
*
* @param {function(object): void} onVideo - Callback fired when new valid videos are processed. Receives the `Video` object.
* @param {{ since?: number, until?: number, limit?: number }} [options] - Filter options.
* @returns {import("nostr-tools").Sub} The subscription object. Call `unsub()` to stop.
*/
@@ -6667,7 +6688,14 @@ export class NostrClient {
}
/**
* fetchVideos => old approach
* Fetches videos using a standard Request/Response model (legacy).
*
* Unlike `subscribeVideos` (which is streaming), this waits for all relays to EOSE
* before returning. It is useful for one-off fetches but less efficient for the main feed.
*
* @param {object} options - Filter options (limit, etc.).
* @returns {Promise<object[]>} A promise resolving to the list of active videos.
* @deprecated Use `subscribeVideos` for the main feed to support buffering and streaming.
*/
async fetchVideos(options = {}) {
const requestedLimit = Number(options?.limit);