mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Extend watch history cache TTL and add tests
This commit is contained in:
1 parent
418239f977
commit
1bcb770556
4 files changed
+145
-43
No files matched your search
@@ -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;
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
Reference in new issue
Block a user