mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-02 02:42:28 +00:00
Improve watch history loading responsiveness
This commit is contained in:
1 parent
9d7efce44e
commit
10751a8fb9
4 files changed
+263
-35
No files matched your search
@@ -654,6 +654,12 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
featureEnabled: syncEnabled || localSupported,
|
||||
};
|
||||
|
||||
let rendererRef = null;
|
||||
let fingerprintRefreshQueued = false;
|
||||
const scheduleTask =
|
||||
typeof queueMicrotask === "function"
|
||||
? queueMicrotask
|
||||
: (callback) => Promise.resolve().then(callback);
|
||||
let elements = {
|
||||
view: null,
|
||||
grid: null,
|
||||
@@ -733,6 +739,7 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
}
|
||||
}
|
||||
subscriptions.clear();
|
||||
fingerprintRefreshQueued = false;
|
||||
}
|
||||
|
||||
function clearToasts() {
|
||||
@@ -907,6 +914,50 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function queueFingerprintRefresh() {
|
||||
if (!fingerprintRefreshQueued) {
|
||||
return;
|
||||
}
|
||||
if (state.isLoading) {
|
||||
return;
|
||||
}
|
||||
if (!rendererRef) {
|
||||
return;
|
||||
}
|
||||
fingerprintRefreshQueued = false;
|
||||
void rendererRef.refresh({ force: false });
|
||||
}
|
||||
|
||||
function subscribeToFingerprintUpdates() {
|
||||
if (typeof watchHistoryService.subscribe !== "function") {
|
||||
return;
|
||||
}
|
||||
const unsubscribe = watchHistoryService.subscribe(
|
||||
"fingerprint",
|
||||
(payload) => {
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
payload.actor &&
|
||||
state.actor &&
|
||||
payload.actor !== state.actor
|
||||
) {
|
||||
return;
|
||||
}
|
||||
fingerprintRefreshQueued = true;
|
||||
if (!state.isLoading) {
|
||||
scheduleTask(() => {
|
||||
queueFingerprintRefresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
if (typeof unsubscribe === "function") {
|
||||
subscriptions.add(unsubscribe);
|
||||
}
|
||||
}
|
||||
|
||||
function setLoadingState(loading) {
|
||||
state.isLoading = loading;
|
||||
if (elements.loading) {
|
||||
@@ -1546,6 +1597,11 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
result = null;
|
||||
}
|
||||
setLoadingState(false);
|
||||
if (fingerprintRefreshQueued) {
|
||||
scheduleTask(() => {
|
||||
queueFingerprintRefresh();
|
||||
});
|
||||
}
|
||||
const feedItems = Array.isArray(result?.items) ? result.items : [];
|
||||
const normalized = [];
|
||||
state.metadataCache.clear();
|
||||
@@ -1644,6 +1700,7 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
bindMetadataToggle();
|
||||
subscribeToMetadataPreference();
|
||||
subscribeToRepublishEvents();
|
||||
subscribeToFingerprintUpdates();
|
||||
updateInfoCallout();
|
||||
state.metadataStorageEnabled =
|
||||
typeof watchHistoryService.shouldStoreMetadata === "function"
|
||||
@@ -1787,6 +1844,8 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
state.cursor = 0;
|
||||
state.hasMore = false;
|
||||
state.metadataCache.clear();
|
||||
rendererRef = null;
|
||||
fingerprintRefreshQueued = false;
|
||||
},
|
||||
async handleRemove(pointerKeyValue) {
|
||||
if (!pointerKeyValue) {
|
||||
@@ -1884,6 +1943,7 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
},
|
||||
};
|
||||
|
||||
rendererRef = renderer;
|
||||
return renderer;
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +277,10 @@ export default class WatchHistoryController {
|
||||
|
||||
if (!normalizedItems) {
|
||||
try {
|
||||
const latest = await this.watchHistoryService.loadLatest(actorCandidate);
|
||||
const latest = await this.watchHistoryService.loadLatest(
|
||||
actorCandidate,
|
||||
{ allowStale: false },
|
||||
);
|
||||
normalizedItems = Array.isArray(latest)
|
||||
? latest.map(normalizeEntry).filter(Boolean)
|
||||
: [];
|
||||
|
||||
+73
-33
@@ -1200,12 +1200,58 @@ async function snapshot(items, options = {}) {
|
||||
return run;
|
||||
}
|
||||
|
||||
async function loadLatest(actorInput) {
|
||||
function scheduleWatchHistoryRefresh(actorKey, cacheEntry = {}) {
|
||||
if (!actorKey) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
if (cacheEntry && typeof cacheEntry === "object" && cacheEntry.promise) {
|
||||
return cacheEntry.promise;
|
||||
}
|
||||
|
||||
const promise = nostrClient
|
||||
.resolveWatchHistory(actorKey, { forceRefresh: true })
|
||||
.then((resolvedItems) => updateFingerprintCache(actorKey, resolvedItems))
|
||||
.then(() => {
|
||||
const latest = state.fingerprintCache.get(actorKey);
|
||||
return latest?.items || [];
|
||||
})
|
||||
.catch((error) => {
|
||||
if (isDevMode) {
|
||||
console.warn("[watchHistoryService] Failed to load watch history:", error);
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
const entry = state.fingerprintCache.get(actorKey);
|
||||
if (entry) {
|
||||
delete entry.promise;
|
||||
}
|
||||
});
|
||||
|
||||
console.info("[watchHistoryService] Triggered nostr watch list refresh.", {
|
||||
actor: actorKey,
|
||||
});
|
||||
|
||||
const baseEntry = cacheEntry && typeof cacheEntry === "object" ? cacheEntry : {};
|
||||
state.fingerprintCache.set(actorKey, {
|
||||
...baseEntry,
|
||||
promise,
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function loadLatest(actorInput, options = {}) {
|
||||
const actorKey = resolveActorKey(actorInput);
|
||||
if (!actorKey) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizedOptions =
|
||||
options && typeof options === "object" ? options : {};
|
||||
const allowStale = normalizedOptions.allowStale !== false;
|
||||
|
||||
if (!isFeatureEnabled(actorKey)) {
|
||||
if (!state.restored) {
|
||||
restoreQueueState();
|
||||
@@ -1231,12 +1277,13 @@ async function loadLatest(actorInput) {
|
||||
cacheExpiresAt: cacheEntry?.expiresAt || null,
|
||||
}
|
||||
);
|
||||
if (
|
||||
cacheEntry?.items &&
|
||||
cacheEntry.expiresAt &&
|
||||
cacheEntry.expiresAt > now &&
|
||||
Array.isArray(cacheEntry.items)
|
||||
) {
|
||||
const hasCachedItems = Array.isArray(cacheEntry?.items);
|
||||
const cacheIsFresh =
|
||||
hasCachedItems &&
|
||||
cacheEntry?.expiresAt &&
|
||||
cacheEntry.expiresAt > now;
|
||||
|
||||
if (cacheIsFresh) {
|
||||
console.info(
|
||||
"[watchHistoryService] Returning cached watch list items.",
|
||||
{
|
||||
@@ -1254,42 +1301,35 @@ async function loadLatest(actorInput) {
|
||||
actor: actorKey,
|
||||
}
|
||||
);
|
||||
if (allowStale && hasCachedItems) {
|
||||
console.info(
|
||||
"[watchHistoryService] Returning stale watch list items while refresh completes.",
|
||||
{
|
||||
actor: actorKey,
|
||||
itemCount: cacheEntry.items.length,
|
||||
}
|
||||
);
|
||||
return cacheEntry.items;
|
||||
}
|
||||
return cacheEntry.promise;
|
||||
}
|
||||
|
||||
const promise = nostrClient
|
||||
.resolveWatchHistory(actorKey, { forceRefresh: true })
|
||||
.then((resolvedItems) => updateFingerprintCache(actorKey, resolvedItems))
|
||||
.then(() => {
|
||||
const latest = state.fingerprintCache.get(actorKey);
|
||||
return latest?.items || [];
|
||||
})
|
||||
.catch((error) => {
|
||||
if (isDevMode) {
|
||||
console.warn("[watchHistoryService] Failed to load watch history:", error);
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
const entry = state.fingerprintCache.get(actorKey);
|
||||
if (entry) {
|
||||
delete entry.promise;
|
||||
}
|
||||
});
|
||||
if (!allowStale || !hasCachedItems) {
|
||||
return scheduleWatchHistoryRefresh(actorKey, cacheEntry);
|
||||
}
|
||||
|
||||
const refreshPromise = scheduleWatchHistoryRefresh(actorKey, cacheEntry);
|
||||
refreshPromise.catch(() => {});
|
||||
|
||||
console.info(
|
||||
"[watchHistoryService] Triggered nostr watch list refresh.",
|
||||
"[watchHistoryService] Returning stale watch list items while refresh is pending.",
|
||||
{
|
||||
actor: actorKey,
|
||||
itemCount: cacheEntry?.items?.length || 0,
|
||||
}
|
||||
);
|
||||
|
||||
state.fingerprintCache.set(actorKey, {
|
||||
...(cacheEntry || {}),
|
||||
promise,
|
||||
});
|
||||
|
||||
return promise;
|
||||
return cacheEntry.items || [];
|
||||
}
|
||||
|
||||
async function getFingerprint(actorInput) {
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import "./test-helpers/setup-localstorage.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { WATCH_HISTORY_PAYLOAD_MAX_BYTES, WATCH_HISTORY_MAX_ITEMS } =
|
||||
const {
|
||||
WATCH_HISTORY_PAYLOAD_MAX_BYTES,
|
||||
WATCH_HISTORY_MAX_ITEMS,
|
||||
WATCH_HISTORY_CACHE_TTL_MS,
|
||||
} =
|
||||
await import("../js/config.js");
|
||||
const {
|
||||
getWatchHistoryV2Enabled,
|
||||
@@ -986,6 +990,126 @@ async function testWatchHistoryServiceIntegration() {
|
||||
}
|
||||
}
|
||||
|
||||
async function testWatchHistoryStaleCacheRefresh() {
|
||||
console.log("Running watch history stale cache refresh test...");
|
||||
|
||||
const actor = "stale-cache-actor";
|
||||
const originalResolve = nostrClient.resolveWatchHistory;
|
||||
const originalFingerprint = nostrClient.getWatchHistoryFingerprint;
|
||||
const originalDateNow = Date.now;
|
||||
|
||||
try {
|
||||
watchHistoryService.resetProgress(actor);
|
||||
|
||||
nostrClient.getWatchHistoryFingerprint = async (_actor, items) => {
|
||||
const values = (Array.isArray(items) ? items : []).map((entry) => {
|
||||
if (!entry) {
|
||||
return "";
|
||||
}
|
||||
if (typeof entry.value === "string") {
|
||||
return entry.value;
|
||||
}
|
||||
if (
|
||||
entry.pointer &&
|
||||
typeof entry.pointer.value === "string"
|
||||
) {
|
||||
return entry.pointer.value;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
return `fingerprint:${values.join("|")}`;
|
||||
};
|
||||
|
||||
const firstItems = [
|
||||
{ type: "e", value: "watch-history-old", watchedAt: 1_700_000_000 },
|
||||
];
|
||||
|
||||
let resolveCallCount = 0;
|
||||
nostrClient.resolveWatchHistory = async (requestedActor) => {
|
||||
resolveCallCount += 1;
|
||||
assert.equal(
|
||||
requestedActor,
|
||||
actor,
|
||||
"initial load should request the expected actor",
|
||||
);
|
||||
return firstItems;
|
||||
};
|
||||
|
||||
await watchHistoryService.loadLatest(actor, { allowStale: false });
|
||||
|
||||
const baseNow = originalDateNow();
|
||||
Date.now = () => baseNow + WATCH_HISTORY_CACHE_TTL_MS + 1;
|
||||
|
||||
const refreshedItems = [
|
||||
{ type: "e", value: "watch-history-new", watchedAt: 1_700_000_600 },
|
||||
];
|
||||
|
||||
let refreshResolve;
|
||||
const refreshGate = new Promise((resolve) => {
|
||||
refreshResolve = resolve;
|
||||
});
|
||||
|
||||
resolveCallCount = 0;
|
||||
nostrClient.resolveWatchHistory = async (requestedActor) => {
|
||||
resolveCallCount += 1;
|
||||
assert.equal(
|
||||
requestedActor,
|
||||
actor,
|
||||
"refresh should continue targeting the same actor",
|
||||
);
|
||||
await refreshGate;
|
||||
return refreshedItems;
|
||||
};
|
||||
|
||||
const staleResult = await watchHistoryService.loadLatest(actor);
|
||||
assert.equal(
|
||||
resolveCallCount,
|
||||
1,
|
||||
"stale load should trigger a single background refresh",
|
||||
);
|
||||
assert.equal(
|
||||
staleResult.length,
|
||||
firstItems.length,
|
||||
"stale load should return cached entries immediately",
|
||||
);
|
||||
const staleValue =
|
||||
staleResult[0]?.value || staleResult[0]?.pointer?.value || "";
|
||||
assert.equal(
|
||||
staleValue,
|
||||
"watch-history-old",
|
||||
"stale load should surface the cached pointer value",
|
||||
);
|
||||
|
||||
refreshResolve();
|
||||
await refreshGate;
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
nostrClient.resolveWatchHistory = async () => {
|
||||
throw new Error("loadLatest should rely on refreshed cache");
|
||||
};
|
||||
|
||||
const freshResult = await watchHistoryService.loadLatest(actor);
|
||||
assert.equal(
|
||||
freshResult.length,
|
||||
refreshedItems.length,
|
||||
"fresh load should surface refreshed watch history entries",
|
||||
);
|
||||
const freshValue =
|
||||
freshResult[0]?.value || freshResult[0]?.pointer?.value || "";
|
||||
assert.equal(
|
||||
freshValue,
|
||||
"watch-history-new",
|
||||
"fresh load should include the updated pointer",
|
||||
);
|
||||
} finally {
|
||||
Date.now = originalDateNow;
|
||||
nostrClient.resolveWatchHistory = originalResolve;
|
||||
nostrClient.getWatchHistoryFingerprint = originalFingerprint;
|
||||
watchHistoryService.resetProgress(actor);
|
||||
}
|
||||
}
|
||||
|
||||
async function testWatchHistoryLocalFallbackWhenDisabled() {
|
||||
console.log("Running watch history local fallback test...");
|
||||
|
||||
@@ -1199,6 +1323,7 @@ await testPublishSnapshotFailureRetry();
|
||||
await testWatchHistoryPartialRelayRetry();
|
||||
await testResolveWatchHistoryBatchingWindow();
|
||||
await testWatchHistoryServiceIntegration();
|
||||
await testWatchHistoryStaleCacheRefresh();
|
||||
await testWatchHistoryLocalFallbackWhenDisabled();
|
||||
await testWatchHistorySyncEnabledForLoggedInUsers();
|
||||
await testWatchHistoryAppLoginFallback();
|
||||
|
||||
Reference in new issue
Block a user