mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
chore(ai): doc js/nostr/client.js and videoEventBuffer.js
- Add JSDoc to exported methods in `js/nostr/client.js` - Add JSDoc to `js/nostr/videoEventBuffer.js` - Add in-code comments for complex algorithms (history hydration, soft/hard delete, buffering) - Update `docs/nostr-client-overview.md` with invariants and pitfalls - No functional changes Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
da3942bace
commit
a997120f42
3 files changed
+122
-28
No files matched your search
@@ -119,3 +119,26 @@ The client abstracts the signing mechanism, allowing the user to switch between
|
||||
3. **Local (NIP-01)**: Uses a local private key (nsec) stored in `sessionStorage` (encrypted).
|
||||
|
||||
The `activeSigner` object is normalized to expose a standard interface (`signEvent`, `nip04Encrypt`, `nip44Encrypt`), regardless of the underlying implementation.
|
||||
|
||||
## Implicit Invariants
|
||||
|
||||
Maintaining these invariants is critical for the stability of the application:
|
||||
|
||||
1. **Active Map Consistency**: `activeMap` must *only* contain the single latest version of a video series.
|
||||
* *Violation*: If two versions of the same video exist in `activeMap`, the UI will duplicate the card or flicker.
|
||||
2. **Tombstone Authority**: Any event, regardless of its source (relay or cache), MUST be discarded if its `created_at` is older than the timestamp in `tombstones.get(activeKey)`.
|
||||
* *Violation*: Deleted videos will reappear ("zombie events").
|
||||
3. **History Integrity**: All versions in a history chain must share the same `videoRootId` (V3) or `d` tag (V2).
|
||||
* *Violation*: The history log will be fragmented or incomplete.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Direct State Mutation**:
|
||||
* *Don't*: `client.activeMap.set(key, video)` manually.
|
||||
* *Do*: Use `_processAndCacheEvent(evt)` or let `VideoEventBuffer` handle it. The client needs to run deduplication and tombstone checks.
|
||||
2. **Stable ID Assumption**:
|
||||
* *Don't*: Rely on `event.id` as a permanent reference to a video.
|
||||
* *Do*: Use `videoRootId` or the `d` tag. `event.id` changes every time the user edits the title/thumbnail.
|
||||
3. **Sync vs Async State**:
|
||||
* *Don't*: Expect `client.allEvents` to be populated immediately after `init()`.
|
||||
* *Do*: Use `restoreLocalData()` (awaited in `init`) which loads the cache, but network updates arrive asynchronously via subscriptions.
|
||||
+66
-28
@@ -755,6 +755,9 @@ export class NostrClient {
|
||||
* Restores application state from the best available local cache.
|
||||
* Delegates to PersistenceManager.
|
||||
*
|
||||
* This method ensures the UI can render immediately (Stale-While-Revalidate)
|
||||
* by loading `activeMap`, `allEvents`, and `tombstones` from IndexedDB.
|
||||
*
|
||||
* @returns {Promise<boolean>} True if data was successfully restored.
|
||||
*/
|
||||
async restoreLocalData() {
|
||||
@@ -1065,6 +1068,11 @@ export class NostrClient {
|
||||
return this.connectionManager.ensurePool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to `ConnectionManager` to connect to all configured relays.
|
||||
*
|
||||
* @returns {Promise<Array<{url: string, success: boolean}>>} Connection results for each relay.
|
||||
*/
|
||||
async connectToRelays() {
|
||||
return this.connectionManager.connectToRelays();
|
||||
}
|
||||
@@ -1105,17 +1113,6 @@ export class NostrClient {
|
||||
return this.connectionManager.getHealthyRelays(candidates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out the current user and clears all session state.
|
||||
*
|
||||
* **Cleanup:**
|
||||
* - Clears `this.pubkey` and notifies the global signer registry.
|
||||
* - Wipes session actor (ephemeral keys).
|
||||
* - Disconnects NIP-46 remote signer (if active).
|
||||
* - Clears Watch History cache.
|
||||
* - Resets permissions cache.
|
||||
*/
|
||||
|
||||
getDmDecryptCacheLimit() {
|
||||
return DM_DECRYPT_CACHE_LIMIT;
|
||||
}
|
||||
@@ -2300,6 +2297,8 @@ export class NostrClient {
|
||||
const signerCapabilities = resolveSignerCapabilities(signer);
|
||||
const useNip17 = Boolean(resolvedOptions.useNip17);
|
||||
|
||||
// Heuristic: If attachments are present, we MUST use NIP-17 because
|
||||
// NIP-04 does not support rich metadata or multiple recipients securely.
|
||||
if (hasAttachments && !useNip17) {
|
||||
return { ok: false, error: "attachments-unsupported" };
|
||||
}
|
||||
@@ -2331,6 +2330,8 @@ export class NostrClient {
|
||||
};
|
||||
|
||||
if (useNip17) {
|
||||
// Protocol: NIP-17 (Sealed Rumor)
|
||||
// Requires NIP-44 encryption and NIP-65 relay discovery.
|
||||
if (!signerCapabilities.nip44 || typeof signer.nip44Encrypt !== "function") {
|
||||
return { ok: false, error: "nip44-unsupported" };
|
||||
}
|
||||
@@ -2369,6 +2370,8 @@ export class NostrClient {
|
||||
});
|
||||
}
|
||||
|
||||
// Protocol: NIP-04 (Legacy)
|
||||
// Fallback for older clients. Simple ECDH encryption.
|
||||
return this._sendNip04DirectMessage({
|
||||
signer,
|
||||
signingAdapter,
|
||||
@@ -2638,6 +2641,16 @@ export class NostrClient {
|
||||
return this.signerManager.clearStoredSessionActor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out the current user and clears all session state.
|
||||
*
|
||||
* **Cleanup:**
|
||||
* - Clears `this.pubkey` and notifies the global signer registry.
|
||||
* - Wipes session actor (ephemeral keys).
|
||||
* - Disconnects NIP-46 remote signer (if active).
|
||||
* - Clears Watch History cache.
|
||||
* - Resets permissions cache.
|
||||
*/
|
||||
logout() {
|
||||
return this.signerManager.logout();
|
||||
}
|
||||
@@ -2853,6 +2866,16 @@ export class NostrClient {
|
||||
return { matchingEvents, inferredRoot };
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to perform a soft delete (revert) on a set of video events.
|
||||
* Publishes a new version with `deleted: true` for each unique d-tag found.
|
||||
*
|
||||
* @param {Map} matchingEvents - The events to delete.
|
||||
* @param {string} inferredRoot - The root ID of the series.
|
||||
* @param {object} targetVideo - The main video object being deleted.
|
||||
* @param {string} pubkey - The active user's pubkey.
|
||||
* @returns {Promise<{revertSummaries: object[], revertEvents: object[]}>}
|
||||
*/
|
||||
async _softDeleteVersions(matchingEvents, inferredRoot, targetVideo, pubkey) {
|
||||
const revertSummaries = [];
|
||||
const revertEvents = [];
|
||||
@@ -2935,6 +2958,17 @@ export class NostrClient {
|
||||
return { revertSummaries, revertEvents };
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to perform a hard delete (Kind 5) on a set of video events.
|
||||
* Collects all event IDs (`e` tags) and addresses (`a` tags) and publishes a deletion request.
|
||||
*
|
||||
* @param {Map} matchingEvents - The original events to delete.
|
||||
* @param {object[]} revertEvents - The soft-delete events created in the previous step (also deleted).
|
||||
* @param {object} targetVideo - The main video object.
|
||||
* @param {string} inferredRoot - The root ID.
|
||||
* @param {string} pubkey - The active user's pubkey.
|
||||
* @returns {Promise<object[]>} Summary of deletion results.
|
||||
*/
|
||||
async _hardDeleteVersions(matchingEvents, revertEvents, targetVideo, inferredRoot, pubkey) {
|
||||
const eventIdSet = new Set();
|
||||
const addressPointerSet = new Set();
|
||||
@@ -3246,12 +3280,6 @@ export class NostrClient {
|
||||
* @returns {import("nostr-tools").Sub} The subscription object. Call `unsub()` to stop listening.
|
||||
*/
|
||||
subscribeVideos(onVideo, options = {}) {
|
||||
// Explanation:
|
||||
// This method handles the primary video feed. It uses a buffering strategy to
|
||||
// prevent UI thrashing when thousands of events arrive at once (e.g. initial load).
|
||||
// Incoming events are pushed to `eventBuffer` and processed in batches
|
||||
// via `flushEventBuffer` which is debounced.
|
||||
|
||||
const { since, until, limit } = options;
|
||||
const latestCachedCreatedAt = this.getLatestCachedCreatedAt();
|
||||
|
||||
@@ -3318,6 +3346,12 @@ export class NostrClient {
|
||||
return sub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to `processNip71EventsHelper` to parse and cache NIP-71 metadata from events.
|
||||
*
|
||||
* @param {import("nostr-tools").Event[]} events - The raw events to process.
|
||||
* @param {Map} [pointerMap] - Optional optimization map.
|
||||
*/
|
||||
processNip71Events(events, pointerMap = null) {
|
||||
processNip71EventsHelper(events, {
|
||||
nip71Cache: this.nip71Cache,
|
||||
@@ -3325,12 +3359,24 @@ export class NostrClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to `mergeNip71MetadataIntoVideoHelper` to apply cached NIP-71 metadata to a video.
|
||||
*
|
||||
* @param {object} video - The video object to mutate.
|
||||
* @returns {object} The mutated video object.
|
||||
*/
|
||||
mergeNip71MetadataIntoVideo(video) {
|
||||
return mergeNip71MetadataIntoVideoHelper(video, {
|
||||
nip71Cache: this.nip71Cache,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to `populateNip71MetadataForVideosHelper` to fetch missing NIP-71 metadata.
|
||||
*
|
||||
* @param {object[]} videos - The list of video objects to hydrate.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async populateNip71MetadataForVideos(videos = []) {
|
||||
if (!Array.isArray(videos) || !videos.length) {
|
||||
return;
|
||||
@@ -3804,17 +3850,6 @@ export class NostrClient {
|
||||
* @returns {Promise<object[]>} A promise resolving to an array of Video objects, sorted from newest to oldest.
|
||||
*/
|
||||
async hydrateVideoHistory(video) {
|
||||
// Explanation:
|
||||
// This method reconstructs the edit history of a video series.
|
||||
// Since Nostr events are immutable, "editing" creates a new event.
|
||||
// We link these events together using:
|
||||
// 1. `videoRootId` (V3 canonical ID) - The primary key for the series.
|
||||
// 2. `d` tag (NIP-33 addressability) - The secondary key for lookups.
|
||||
// 3. Fallback: Matching ID for legacy V1/V2 posts (where ID was the root).
|
||||
//
|
||||
// The method first checks local cache (`allEvents`), and if data is sparse,
|
||||
// queries relays for the specific `d` tag to find missing links.
|
||||
|
||||
if (!video || typeof video !== "object") {
|
||||
return [];
|
||||
}
|
||||
@@ -3827,6 +3862,7 @@ export class NostrClient {
|
||||
|
||||
let localMatches = this.getLocalVideoHistory(video);
|
||||
|
||||
// Ensure we have the "Root" event (genesis block) even if we didn't start with it.
|
||||
const ensureRootPresence = async () => {
|
||||
const normalizedRoot = targetRoot && typeof targetRoot === "string"
|
||||
? targetRoot
|
||||
@@ -3872,6 +3908,8 @@ export class NostrClient {
|
||||
|
||||
await ensureRootPresence();
|
||||
|
||||
// Heuristic: If we only have 1 version (the current one) and it's addressable (d-tag),
|
||||
// we likely missed history. Query relays for all events with the same d-tag to fill gaps.
|
||||
const shouldFetchFromRelays =
|
||||
localMatches.filter((entry) => !entry.deleted).length <= 1 && targetDTag;
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ import { devLogger, userLogger } from "../utils/logger.js";
|
||||
import { convertEventToVideo } from "./nip71.js";
|
||||
|
||||
export class VideoEventBuffer {
|
||||
/**
|
||||
* @param {import("./client.js").NostrClient} client - The parent client instance (for state access).
|
||||
* @param {function(object[]): void} onVideo - The UI callback to fire with batched updates.
|
||||
*/
|
||||
constructor(client, onVideo) {
|
||||
this.client = client;
|
||||
this.onVideo = onVideo;
|
||||
@@ -19,11 +23,21 @@ export class VideoEventBuffer {
|
||||
this.FLUSH_DEBOUNCE_MS = 75;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a raw event into the buffer and schedules a debounced flush.
|
||||
* @param {import("nostr-tools").Event} event - The incoming Nostr event.
|
||||
*/
|
||||
push(event) {
|
||||
this.buffer.push(event);
|
||||
this.scheduleFlush(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules processing of the buffer.
|
||||
* Uses a debounce timer to wait for the stream to pause (pressure valve).
|
||||
*
|
||||
* @param {boolean} [immediate=false] - If true, flushes immediately (e.g. on EOSE).
|
||||
*/
|
||||
scheduleFlush(immediate = false) {
|
||||
if (this.flushTimerId) {
|
||||
if (!immediate) {
|
||||
@@ -44,6 +58,17 @@ export class VideoEventBuffer {
|
||||
}, this.FLUSH_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes all buffered events in a single batch.
|
||||
*
|
||||
* **Steps:**
|
||||
* 1. Validates and converts raw events to Video objects.
|
||||
* 2. Checks for tombstones (deleted videos).
|
||||
* 3. Updates the client's `activeMap` (Latest Wins).
|
||||
* 4. Triggers the UI callback *once*.
|
||||
* 5. Triggers background hydration of NIP-71 metadata.
|
||||
* 6. Persists state to IndexedDB.
|
||||
*/
|
||||
flush() {
|
||||
if (!this.buffer.length) {
|
||||
return;
|
||||
@@ -137,6 +162,10 @@ export class VideoEventBuffer {
|
||||
this.client.saveLocalData("subscribeVideos:flush");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the subscription reaches End-of-Stored-Events (EOSE).
|
||||
* Triggers an immediate flush to ensure the UI is fully up-to-date.
|
||||
*/
|
||||
handleEose() {
|
||||
if (isDevMode && this.invalidEvents.length > 0) {
|
||||
userLogger.warn(
|
||||
@@ -150,6 +179,10 @@ export class VideoEventBuffer {
|
||||
this.scheduleFlush(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up timers and flushes any remaining events.
|
||||
* Call when unsubscribing.
|
||||
*/
|
||||
cleanup() {
|
||||
if (this.flushTimerId) {
|
||||
clearTimeout(this.flushTimerId);
|
||||
|
||||
Reference in new issue
Block a user