mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-11 15:22:34 +00:00
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:
3 files changed
+113
-59
No files matched your search
@@ -1,80 +1,68 @@
|
|||||||
# NostrClient Overview
|
# 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`.
|
1. **Connection Management**: Maintains a pool of connections to Nostr relays (`SimplePool`).
|
||||||
2. **State Management**: Tracks video events, deletions (tombstones), and deduplicates content.
|
2. **State Management**:
|
||||||
3. **Caching**: Implements a "stale-while-revalidate" strategy using IndexedDB (`EventsCacheStore`) to load content immediately while fetching updates in the background.
|
* `allEvents`: A raw map of all fetched events, useful for history.
|
||||||
4. **Publishing**: Orchestrates the signing and broadcasting of events, including complex multi-event flows (e.g., Video + Mirror + NIP-71).
|
* `activeMap`: A "materialized view" containing only the latest version of each video (deduplicated by `videoRootId` or `d` tag).
|
||||||
5. **Signer Management**: Abstracts the differences between NIP-07 (Extension), NIP-46 (Remote/Bunker), and Local (nsec) signers.
|
* `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.
|
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.
|
||||||
* **`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.
|
|
||||||
|
|
||||||
## 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.
|
To prevent the UI from freezing when thousands of events arrive during the initial connection:
|
||||||
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).
|
|
||||||
|
|
||||||
## 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.
|
The client attempts to restore state immediately on load ("Stale-While-Revalidate"):
|
||||||
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
|
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.).
|
### 1. Publishing a Video
|
||||||
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.
|
`publishVideo(videoPayload, pubkey)`
|
||||||
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
|
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
|
### 2. Loading the Feed
|
||||||
import { NostrClient } from "./js/nostr/client.js";
|
`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)
|
## When to Modify
|
||||||
await client.init();
|
|
||||||
|
|
||||||
// 2. Subscribe to videos
|
* **New Event Kinds**: If adding support for a new NIP or event type, add handlers here.
|
||||||
const sub = client.subscribeVideos((video) => {
|
* **Performance Tuning**: Adjust buffering timeouts or cache policies in `EventsCacheStore` if UI responsiveness issues arise.
|
||||||
console.log("New video received:", video.title);
|
* **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.
|
||||||
});
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
* NostrClient
|
* NostrClient
|
||||||
*
|
*
|
||||||
* The central controller for all Nostr network interactions and state management.
|
* The central controller for all Nostr network interactions and state management.
|
||||||
|
* For a high-level overview, see `docs/nostr-client-overview.md`.
|
||||||
*
|
*
|
||||||
* Responsibilities:
|
* Responsibilities:
|
||||||
* - Connection Management: Maintains the connection pool to Relays.
|
* - Connection Management: Maintains the connection pool to Relays.
|
||||||
|
|||||||
@@ -290,6 +290,22 @@ function buildParticipantTag(participant) {
|
|||||||
return values;
|
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) {
|
export function buildNip71MetadataTags(metadata) {
|
||||||
if (!metadata || typeof metadata !== "object") {
|
if (!metadata || typeof metadata !== "object") {
|
||||||
return [];
|
return [];
|
||||||
@@ -571,6 +587,16 @@ function parseHashtagTag(tag) {
|
|||||||
return value || null;
|
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) {
|
export function extractNip71MetadataFromTags(event) {
|
||||||
if (!event || typeof event !== "object") {
|
if (!event || typeof event !== "object") {
|
||||||
return null;
|
return null;
|
||||||
@@ -840,6 +866,21 @@ export function buildNip71PointerTags({
|
|||||||
return pointerTags;
|
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({
|
export function buildNip71VideoEvent({
|
||||||
metadata,
|
metadata,
|
||||||
pubkey = "",
|
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 } = {}) {
|
export function processNip71Events(events, { nip71Cache, pointerMap = null } = {}) {
|
||||||
if (!Array.isArray(events) || !events.length || !(nip71Cache instanceof Map)) {
|
if (!Array.isArray(events) || !events.length || !(nip71Cache instanceof Map)) {
|
||||||
return;
|
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 } = {}) {
|
export function mergeNip71MetadataIntoVideo(video, { nip71Cache } = {}) {
|
||||||
if (!video || typeof video !== "object") {
|
if (!video || typeof video !== "object") {
|
||||||
return video;
|
return video;
|
||||||
|
|||||||
Reference in new issue
Block a user