perf(watch-history): decrypt only recent months on load (cap cold cost)

Root cause of the ~20s watch-history load: history is bucketed by month (one
encrypted snapshot event per month) and the fetch decrypted EVERY month via the
serialized nip-07 extension — ~1 slow call per month, so accounts with years of
history paid dozens of back-to-back extension decrypts on every load.

Cap the load to the most recent RECENT_MONTHS_DECRYPT_LIMIT (6) months by default
— enough for the feed's watched-video suppression and the initial view. Callers
that need the full history (the explicit History view) pass recentMonthsLimit: 0,
threaded loadLatest -> scheduleWatchHistoryRefresh -> resolveWatchHistory ->
resolve -> fetch.

Harness (fresh-context wh-repro): 600 items / 18 months goes from 18 nip-07
decrypt calls to 6. Watch-history tests green. Bumps the watchHistory.js
file-size baseline for the added logic.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-12 21:11:40 -04:00
1 parent 2c657c961e
commit b5ad582b6c
4 files changed
+24 -2

No files matched your search

+3
View File
@@ -1521,9 +1521,12 @@ export function createWatchHistoryRenderer(config = {}) {
);
}
}
// The History view shows the user's FULL history (all months), unlike the
// feed's automatic load which only decrypts recent months for speed.
const items = await watchHistoryService.loadLatest(actorInput, {
allowStale: !forceRefresh,
forceRefresh,
recentMonthsLimit: 0,
});
const normalized = normalizeHistoryItems(items);
return { items: normalized, metadata: { engine: "service-fallback" } };
+16
View File
@@ -41,6 +41,10 @@ import {
export { normalizePointerInput, pointerKey };
// Recent monthly watch-history snapshots to decrypt on load (1 nip-07 call each).
// Override per-call with recentMonthsLimit (0 = all, e.g. the History view).
const RECENT_MONTHS_DECRYPT_LIMIT = 6;
/**
* Domain utilities for watch-history interactions. This module owns pointer
* normalization/serialization, chunking, fingerprint hashing, persistence,
@@ -1989,6 +1993,17 @@ class WatchHistoryManager {
return await loadFromStorage();
}
// Decrypt only the most recent months by default (see constant above).
const recentMonthsLimit = Number.isFinite(options?.recentMonthsLimit)
? Math.floor(options.recentMonthsLimit)
: RECENT_MONTHS_DECRYPT_LIMIT;
if (recentMonthsLimit > 0 && pointerEvents.length > recentMonthsLimit) {
pointerEvents = [...pointerEvents]
.sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0))
.slice(0, recentMonthsLimit);
eventToProcess = pointerEvents;
}
// Determine snapshotId from the latest event (prioritizing monthly)
const latestEvent = selectNewestListEvent(eventToProcess);
@@ -2217,6 +2232,7 @@ class WatchHistoryManager {
const fetchResult = await this.fetch(resolvedActor, {
forceRefresh: options.forceRefresh || false,
recentMonthsLimit: options.recentMonthsLimit,
});
const merged = mergeWatchHistoryItemsWithFallback(
{
+4 -1
View File
@@ -1015,7 +1015,10 @@ function scheduleWatchHistoryRefresh(actorKey, cacheEntry = {}, options = {}) {
if (!permissionResult.ok) {
throw permissionResult.error;
}
return nostrClient.resolveWatchHistory(actorKey, { forceRefresh: true });
return nostrClient.resolveWatchHistory(actorKey, {
forceRefresh: true,
recentMonthsLimit: options?.recentMonthsLimit,
});
})()
.then((resolvedItems) => updateFingerprintCache(actorKey, resolvedItems))
.then(() => {
+1 -1
View File
@@ -39,7 +39,7 @@ const GRANDFATHERED = {
"js/subscriptions.js": 2426,
"js/userBlocks.js": 2353,
"js/services/nostrService.js": 2227,
"js/nostr/watchHistory.js": 2335,
"js/nostr/watchHistory.js": 2360,
"js/nostr/nip46Client.js": 2102,
"js/state/cache.js": 1827,
"js/app/feedCoordinator.js": 1767,