Extend watch history cache TTL and add tests

This commit is contained in:
thePR0M3TH3AN committed 2025-09-30 14:42:21 -04:00
1 parent 418239f977
commit 1bcb770556
4 files changed
+145 -43

No files matched your search

+10
View File
@@ -103,3 +103,13 @@ export const WATCH_HISTORY_PAYLOAD_MAX_BYTES = 60000;
* so the UI can stitch the full list back together.
*/
export const WATCH_HISTORY_FETCH_EVENT_LIMIT = 12;
/**
* How long clients should cache watch-history snapshots in localStorage.
*
* A 24-hour window keeps recently played videos available across reloads
* without requiring a fresh relay sync on every visit. Increase the value for
* longer-lived caches or decrease it if your deployment needs tighter
* retention guarantees.
*/
export const WATCH_HISTORY_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
+2
View File
@@ -10,6 +10,7 @@ import {
WATCH_HISTORY_BATCH_RESOLVE,
WATCH_HISTORY_PAYLOAD_MAX_BYTES,
WATCH_HISTORY_FETCH_EVENT_LIMIT,
WATCH_HISTORY_CACHE_TTL_MS,
} from "../config/instance-config.js";
export const isDevMode = true; // Set to false for production
@@ -59,3 +60,4 @@ export { WATCH_HISTORY_MAX_ITEMS };
export { WATCH_HISTORY_BATCH_RESOLVE };
export { WATCH_HISTORY_PAYLOAD_MAX_BYTES };
export { WATCH_HISTORY_FETCH_EVENT_LIMIT };
export { WATCH_HISTORY_CACHE_TTL_MS };
+60 -43
View File
@@ -8,6 +8,7 @@ import {
WATCH_HISTORY_BATCH_RESOLVE,
WATCH_HISTORY_PAYLOAD_MAX_BYTES,
WATCH_HISTORY_FETCH_EVENT_LIMIT,
WATCH_HISTORY_CACHE_TTL_MS,
} from "./config.js";
import { ACCEPT_LEGACY_V1 } from "./constants.js";
import { accessControl } from "./accessControl.js";
@@ -32,7 +33,6 @@ const EVENTS_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
const NIP07_LOGIN_TIMEOUT_MS = 15_000; // 15 seconds
const SESSION_ACTOR_STORAGE_KEY = "bitvid:sessionActor:v1";
const WATCH_HISTORY_CACHE_STORAGE_KEY = "bitvid:watchHistoryCache:v1";
const WATCH_HISTORY_CACHE_TTL_MS = EVENTS_CACHE_TTL_MS;
// To limit error spam
let errorLogCount = 0;
@@ -861,6 +861,7 @@ class NostrClient {
this.watchHistoryCache = new Map();
this.watchHistoryStorage = null;
this.watchHistoryRepublishTimers = new Map();
this.watchHistoryCacheTtlMs = WATCH_HISTORY_CACHE_TTL_MS;
}
restoreLocalData() {
@@ -962,60 +963,72 @@ class NostrClient {
return this.allEvents.size > 0;
}
/**
* Resolve the effective TTL for watch-history cache entries.
*/
getWatchHistoryCacheTtlMs() {
const candidate = this.watchHistoryCacheTtlMs;
if (Number.isFinite(candidate) && candidate >= 0) {
return candidate;
}
return WATCH_HISTORY_CACHE_TTL_MS;
}
/**
* Load watch history cache payload from localStorage.
*/
getWatchHistoryStorage() {
if (this.watchHistoryStorage) {
return this.watchHistoryStorage;
}
const base = { version: 1, actors: {} };
if (typeof localStorage === "undefined") {
this.watchHistoryStorage = base;
return this.watchHistoryStorage;
}
if (!this.watchHistoryStorage) {
if (typeof localStorage === "undefined") {
this.watchHistoryStorage = base;
return this.watchHistoryStorage;
}
let parsed = base;
try {
const raw = localStorage.getItem(WATCH_HISTORY_CACHE_STORAGE_KEY);
if (raw) {
const candidate = JSON.parse(raw);
if (
candidate &&
typeof candidate === "object" &&
candidate.version === 1 &&
candidate.actors &&
typeof candidate.actors === "object"
) {
parsed = { version: 1, actors: candidate.actors };
let parsed = base;
try {
const raw = localStorage.getItem(WATCH_HISTORY_CACHE_STORAGE_KEY);
if (raw) {
const candidate = JSON.parse(raw);
if (
candidate &&
typeof candidate === "object" &&
candidate.version === 1 &&
candidate.actors &&
typeof candidate.actors === "object"
) {
parsed = { version: 1, actors: candidate.actors };
}
}
} catch (error) {
if (isDevMode) {
console.warn("[nostr] Failed to parse watch history cache:", error);
}
}
} catch (error) {
if (isDevMode) {
console.warn("[nostr] Failed to parse watch history cache:", error);
}
const actors = { ...(parsed.actors || {}) };
this.watchHistoryStorage = { version: 1, actors };
}
const now = Date.now();
const ttl = this.getWatchHistoryCacheTtlMs();
let mutated = false;
for (const [actor, info] of Object.entries(parsed.actors || {})) {
const actors = this.watchHistoryStorage.actors || {};
for (const actor of Object.keys(actors)) {
const info = actors[actor];
if (
!info ||
typeof info !== "object" ||
typeof info.savedAt !== "number" ||
now - info.savedAt > WATCH_HISTORY_CACHE_TTL_MS
now - info.savedAt > ttl
) {
delete parsed.actors[actor];
delete actors[actor];
mutated = true;
}
}
const actors = { ...(parsed.actors || {}) };
this.watchHistoryStorage = { version: 1, actors };
if (mutated) {
if (mutated && typeof localStorage !== "undefined") {
try {
localStorage.setItem(
WATCH_HISTORY_CACHE_STORAGE_KEY,
@@ -1936,16 +1949,20 @@ class NostrClient {
}
const now = Date.now();
const ttl = this.getWatchHistoryCacheTtlMs();
const cached = this.watchHistoryCache.get(actor);
if (cached && now - cached.savedAt < WATCH_HISTORY_CACHE_TTL_MS) {
return {
pointerEvent: cached.pointerEvent
? cloneEventForCache(cached.pointerEvent)
: null,
items: cached.items
.map((item) => clonePointerItem(item))
.filter(Boolean),
};
if (cached) {
if (now - cached.savedAt < ttl) {
return {
pointerEvent: cached.pointerEvent
? cloneEventForCache(cached.pointerEvent)
: null,
items: cached.items
.map((item) => clonePointerItem(item))
.filter(Boolean),
};
}
this.watchHistoryCache.delete(actor);
}
let storage = null;
@@ -1956,7 +1973,7 @@ class NostrClient {
if (
stored &&
typeof stored.savedAt === "number" &&
now - stored.savedAt < WATCH_HISTORY_CACHE_TTL_MS
now - stored.savedAt < ttl
) {
const entry = this.createWatchHistoryEntry(
stored.pointerEvent,
+73
View File
@@ -158,4 +158,77 @@ assert.deepEqual(
"chunked fetch should preserve pointer order"
);
localStorage.clear();
const originalDateNow = Date.now;
try {
const baseTime = 1_700_000_000_000;
const ttlActor = `${ACTOR}-ttl`;
const pointer = { type: "e", value: "event-ttl", relay: null };
const cachingClient = createDecryptClient(ttlActor);
cachingClient.watchHistoryCacheTtlMs = 24 * 60 * 60 * 1000;
Date.now = () => baseTime;
const entry = cachingClient.createWatchHistoryEntry(null, [pointer], baseTime);
cachingClient.watchHistoryCache.set(ttlActor, entry);
cachingClient.persistWatchHistoryEntry(ttlActor, entry);
const reloadClient = createDecryptClient(ttlActor);
reloadClient.watchHistoryCacheTtlMs = cachingClient.watchHistoryCacheTtlMs;
Date.now = () => baseTime + 60 * 60 * 1000;
const cachedSnapshot = await reloadClient.fetchWatchHistory(ttlActor);
assert.equal(
cachedSnapshot.items.length,
1,
"cached snapshot should survive within extended TTL"
);
assert.equal(
cachedSnapshot.items[0].value,
pointer.value,
"cached snapshot should preserve pointer"
);
localStorage.clear();
const expiryClient = createDecryptClient(ttlActor);
expiryClient.watchHistoryCacheTtlMs = 1_000;
Date.now = () => baseTime;
const expiringEntry = expiryClient.createWatchHistoryEntry(
null,
[pointer],
baseTime
);
expiryClient.watchHistoryCache.set(ttlActor, expiringEntry);
expiryClient.persistWatchHistoryEntry(ttlActor, expiringEntry);
Date.now = () => baseTime + 1_500;
const expiredSnapshot = await expiryClient.fetchWatchHistory(ttlActor);
assert.equal(
expiredSnapshot.items.length,
0,
"expired snapshot should be dropped"
);
const cachedFallback = expiryClient.watchHistoryCache.get(ttlActor);
assert.equal(
cachedFallback?.items?.length ?? 0,
0,
"expired snapshot should not preserve stale items in memory"
);
const storage = expiryClient.getWatchHistoryStorage();
assert.equal(
Object.prototype.hasOwnProperty.call(storage.actors, ttlActor),
false,
"expired snapshot should be removed from localStorage"
);
} finally {
Date.now = originalDateNow;
}
console.log("watch history tests passed");