Fix watch history snapshot merging

This commit is contained in:
thePR0M3TH3AN committed 2025-10-20 22:36:19 -04:00
1 parent 397b38d5b8
commit 7c9db2eeaf
2 files changed
+156 -12

No files matched your search

+53 -11
View File
@@ -843,6 +843,42 @@ function collectQueueItems(actorKey) {
return items.slice(0, WATCH_HISTORY_MAX_ITEMS);
}
function getCachedSnapshotItems(actorKey) {
if (!actorKey) {
return [];
}
const fingerprintEntry = state.fingerprintCache.get(actorKey);
if (Array.isArray(fingerprintEntry?.items) && fingerprintEntry.items.length) {
return Array.from(fingerprintEntry.items);
}
const clientCache =
nostrClient &&
nostrClient.watchHistoryCache instanceof Map &&
nostrClient.watchHistoryCache.get(actorKey);
if (Array.isArray(clientCache?.items) && clientCache.items.length) {
return Array.from(clientCache.items);
}
if (typeof nostrClient?.getWatchHistoryStorage === "function") {
try {
const storage = nostrClient.getWatchHistoryStorage();
const storedItems = storage?.actors?.[actorKey]?.items;
if (Array.isArray(storedItems) && storedItems.length) {
return Array.from(storedItems);
}
} catch (error) {
devLogger.warn(
"[watchHistoryService] Failed to read cached watch history items:",
error,
);
}
}
return [];
}
function notifyQueueChange(actorKey) {
emit("queue-changed", {
actor: actorKey,
@@ -1215,10 +1251,17 @@ async function snapshot(items, options = {}) {
const queue = ensureQueue(actorKey);
if (!items) {
const cachedItems = getCachedSnapshotItems(actorKey);
if (cachedItems.length) {
payloadItems = [...payloadItems, ...cachedItems];
}
}
devLogger.info(
"[watchHistoryService] Initiating watch list snapshot publish.",
{
actor: actorKey,
actor: actorKey,
reason,
itemCount: payloadItems.length,
queued: queue?.items?.size ?? 0,
@@ -1303,16 +1346,15 @@ function scheduleWatchHistoryRefresh(actorKey, cacheEntry = {}) {
return cacheEntry.promise;
}
const promise = Promise.resolve()
.then(async () => {
const permissionResult = await ensureWatchHistoryExtensionPermissions(
actorKey,
);
if (!permissionResult.ok) {
throw permissionResult.error;
}
return nostrClient.resolveWatchHistory(actorKey, { forceRefresh: true });
})
const promise = (async () => {
const permissionResult = await ensureWatchHistoryExtensionPermissions(
actorKey,
);
if (!permissionResult.ok) {
throw permissionResult.error;
}
return nostrClient.resolveWatchHistory(actorKey, { forceRefresh: true });
})()
.then((resolvedItems) => updateFingerprintCache(actorKey, resolvedItems))
.then(() => {
const latest = state.fingerprintCache.get(actorKey);
+103 -1
View File
@@ -1282,7 +1282,7 @@ async function testWatchHistoryPartialRelayRetry() {
nostrClient.sessionActor = originalSession;
nostrClient.pubkey = originalPub;
poolHarness.setResolver(() => ({ ok: true }));
watchHistoryService.resetProgress(actor);
watchHistoryService.resetProgress();
restoreCrypto.restore();
}
}
@@ -1533,6 +1533,107 @@ async function testWatchHistoryServiceIntegration() {
}
}
async function testWatchHistorySnapshotMergesQueuedWithCachedItems() {
console.log("Running watch history snapshot merge test...");
const actor = "npub-snapshot-merge";
const originalPublishSnapshot = nostrClient.publishWatchHistorySnapshot;
const originalRecordView = nostrClient.recordVideoView;
const originalEnsure = nostrClient.ensureSessionActor;
const originalSession = nostrClient.sessionActor;
const originalPub = nostrClient.pubkey;
const originalFingerprint = nostrClient.getWatchHistoryFingerprint;
try {
nostrClient.pubkey = actor;
nostrClient.sessionActor = { pubkey: actor, privateKey: "merge-priv" };
nostrClient.ensureSessionActor = async () => actor;
nostrClient.recordVideoView = async (_pointer, options = {}) => ({
ok: true,
event: {
id: `view-${options.created_at || Date.now()}`,
pubkey: actor,
created_at: options.created_at || Math.floor(Date.now() / 1000),
},
});
const publishedPayloads = [];
nostrClient.publishWatchHistorySnapshot = async (items, options = {}) => {
const clonedItems = Array.isArray(items)
? items.map((entry) => ({ ...entry }))
: [];
publishedPayloads.push({
items: clonedItems,
options: { ...options },
});
return {
ok: true,
snapshotId: `snapshot-${publishedPayloads.length}`,
items: clonedItems,
publishResults: {},
};
};
nostrClient.getWatchHistoryFingerprint = async (_actor, items = []) =>
`fingerprint-${Array.isArray(items) ? items.length : 0}-${Date.now()}`;
watchHistoryService.resetProgress(actor);
const seedItems = [
{ type: "e", value: "seed-one", watchedAt: 1_700_000_000 },
{ type: "e", value: "seed-two", watchedAt: 1_700_000_100 },
];
await watchHistoryService.snapshot(seedItems, {
actor,
reason: "seed-history",
});
assert.equal(
publishedPayloads.length,
1,
"initial snapshot should publish seed items",
);
await watchHistoryService.publishView(
{ type: "e", value: "fresh-entry" },
1_700_000_200,
{ actor },
);
const mergeResult = await watchHistoryService.snapshot(null, {
actor,
reason: "merge-queued-items",
});
assert.ok(mergeResult.ok, "merge snapshot should succeed");
assert.equal(
publishedPayloads.length,
2,
"merge snapshot should trigger a second publish",
);
const mergedCall = publishedPayloads[1];
const mergedKeys = mergedCall.items.map(
(entry) => `${entry?.type}:${entry?.value}`,
);
mergedKeys.sort();
assert.deepEqual(
mergedKeys,
["e:fresh-entry", "e:seed-one", "e:seed-two"],
"merged snapshot should include seed and queued pointers",
);
} finally {
nostrClient.publishWatchHistorySnapshot = originalPublishSnapshot;
nostrClient.recordVideoView = originalRecordView;
nostrClient.ensureSessionActor = originalEnsure;
nostrClient.sessionActor = originalSession;
nostrClient.pubkey = originalPub;
nostrClient.getWatchHistoryFingerprint = originalFingerprint;
watchHistoryService.resetProgress();
}
}
async function testHistoryCardsUseDecryptedPlaybackMetadata() {
console.log("Running watch history card playback metadata test...");
@@ -2047,6 +2148,7 @@ await testPublishSnapshotFailureRetry();
await testWatchHistoryPartialRelayRetry();
await testResolveWatchHistoryBatchingWindow();
await testWatchHistoryServiceIntegration();
await testWatchHistorySnapshotMergesQueuedWithCachedItems();
await testHistoryCardsUseDecryptedPlaybackMetadata();
await testWatchHistoryStaleCacheRefresh();
await testWatchHistoryLocalFallbackWhenDisabled();