mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 08:52:25 +00:00
Improve watch history caching and reuse
This commit is contained in:
1 parent
5e81bc9aef
commit
6039ad2fd7
3 files changed
+163
-42
No files matched your search
@@ -950,6 +950,10 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
state.actor = beforeResult.actor.trim();
|
||||
}
|
||||
|
||||
if (state.actor) {
|
||||
nostrClient.resetWatchHistoryProgress(state.actor);
|
||||
}
|
||||
|
||||
let fingerprint;
|
||||
if (typeof fingerprintOverride !== "undefined") {
|
||||
fingerprint = fingerprintOverride;
|
||||
|
||||
+118
-40
@@ -1102,9 +1102,15 @@ class NostrClient {
|
||||
resolved: previousEntry?.resolved
|
||||
? new Map(previousEntry.resolved)
|
||||
: new Map(),
|
||||
resolvedVideos: previousEntry?.resolvedVideos
|
||||
? new Map(previousEntry.resolvedVideos)
|
||||
: new Map(),
|
||||
resolving: previousEntry?.resolving
|
||||
? new Set(previousEntry.resolving)
|
||||
: new Set(),
|
||||
delivered: previousEntry?.delivered instanceof Set
|
||||
? new Set(previousEntry.delivered)
|
||||
: new Set(),
|
||||
};
|
||||
|
||||
const validKeys = new Set(entry.items.map((item) => pointerKey(item)));
|
||||
@@ -1113,11 +1119,21 @@ class NostrClient {
|
||||
entry.resolved.delete(key);
|
||||
}
|
||||
}
|
||||
for (const key of entry.resolvedVideos.keys()) {
|
||||
if (!validKeys.has(key)) {
|
||||
entry.resolvedVideos.delete(key);
|
||||
}
|
||||
}
|
||||
for (const key of [...entry.resolving]) {
|
||||
if (!validKeys.has(key)) {
|
||||
entry.resolving.delete(key);
|
||||
}
|
||||
}
|
||||
for (const key of [...entry.delivered]) {
|
||||
if (!validKeys.has(key)) {
|
||||
entry.delivered.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
const previousChunkPointers = previousEntry?.chunkPointers;
|
||||
const headPointer = previousChunkPointers?.head
|
||||
@@ -1214,6 +1230,40 @@ class NostrClient {
|
||||
return `${pointerId}:${pointerCreated}:${itemKeys}`;
|
||||
}
|
||||
|
||||
resetWatchHistoryProgress(pubkeyOrSession = null) {
|
||||
let actor =
|
||||
typeof pubkeyOrSession === "string" && pubkeyOrSession.trim()
|
||||
? pubkeyOrSession.trim()
|
||||
: "";
|
||||
|
||||
if (!actor) {
|
||||
if (this.sessionActor?.pubkey) {
|
||||
actor = this.sessionActor.pubkey.trim();
|
||||
} else if (typeof this.pubkey === "string" && this.pubkey.trim()) {
|
||||
actor = this.pubkey.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (!actor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = this.watchHistoryCache.get(actor);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(entry.delivered instanceof Set)) {
|
||||
entry.delivered = new Set();
|
||||
} else {
|
||||
entry.delivered.clear();
|
||||
}
|
||||
|
||||
if (entry.resolving instanceof Set) {
|
||||
entry.resolving.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async encryptWatchHistoryPayload(actorPubkey, payload) {
|
||||
const normalizedActor =
|
||||
typeof actorPubkey === "string" && actorPubkey.trim()
|
||||
@@ -2518,7 +2568,10 @@ class NostrClient {
|
||||
}
|
||||
|
||||
for (const key of Array.from(entry.resolved.keys())) {
|
||||
if (!key || !key.startsWith("a:")) {
|
||||
if (
|
||||
!key ||
|
||||
!key.startsWith(`a:${WATCH_HISTORY_KIND}:`)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!chunkPointerKeys.has(key)) {
|
||||
@@ -2570,6 +2623,13 @@ class NostrClient {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!(entry.resolvedVideos instanceof Map)) {
|
||||
entry.resolvedVideos = new Map();
|
||||
}
|
||||
if (!(entry.delivered instanceof Set)) {
|
||||
entry.delivered = new Set();
|
||||
}
|
||||
|
||||
const parsedBatchSize = Number(batchSize);
|
||||
const normalizedBatchSize =
|
||||
Number.isFinite(parsedBatchSize) && parsedBatchSize > 0
|
||||
@@ -2691,12 +2751,18 @@ class NostrClient {
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
if (entry.resolved.has(key)) {
|
||||
if (entry.delivered.has(key)) {
|
||||
return false;
|
||||
}
|
||||
if (entry.resolving.has(key)) {
|
||||
return false;
|
||||
}
|
||||
if (entry.resolvedVideos.has(key)) {
|
||||
const cachedVideo = entry.resolvedVideos.get(key);
|
||||
if (cachedVideo && typeof cachedVideo === "object" && cachedVideo.id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -2727,56 +2793,53 @@ class NostrClient {
|
||||
|
||||
const videoBatch = available.slice(0, effectiveBatchSize);
|
||||
const batch = [...videoBatch, ...chunkAvailable];
|
||||
if (!batch.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
batch.forEach((item) => {
|
||||
const key = pointerKey(item);
|
||||
if (key) {
|
||||
entry.resolving.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
const relaySet = new Set(
|
||||
Array.isArray(this.relays) && this.relays.length ? this.relays : RELAY_URLS
|
||||
);
|
||||
const filters = [];
|
||||
|
||||
for (const pointer of batch) {
|
||||
if (pointer.relay) {
|
||||
relaySet.add(pointer.relay);
|
||||
}
|
||||
if (pointer.type === "e") {
|
||||
filters.push({ ids: [pointer.value] });
|
||||
} else if (pointer.type === "a") {
|
||||
const [kindStr, pubkey, identifier] = pointer.value.split(":");
|
||||
const kind = Number(kindStr);
|
||||
if (Number.isFinite(kind) && pubkey) {
|
||||
const filter = { kinds: [kind], authors: [pubkey] };
|
||||
if (identifier) {
|
||||
filter["#d"] = [identifier];
|
||||
if (batch.length) {
|
||||
batch.forEach((item) => {
|
||||
const key = pointerKey(item);
|
||||
if (key) {
|
||||
entry.resolving.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
for (const pointer of batch) {
|
||||
if (pointer.relay) {
|
||||
relaySet.add(pointer.relay);
|
||||
}
|
||||
if (pointer.type === "e") {
|
||||
filters.push({ ids: [pointer.value] });
|
||||
} else if (pointer.type === "a") {
|
||||
const [kindStr, pubkey, identifier] = pointer.value.split(":");
|
||||
const kind = Number(kindStr);
|
||||
if (Number.isFinite(kind) && pubkey) {
|
||||
const filter = { kinds: [kind], authors: [pubkey] };
|
||||
if (identifier) {
|
||||
filter["#d"] = [identifier];
|
||||
}
|
||||
filters.push(filter);
|
||||
}
|
||||
filters.push(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!filters.length) {
|
||||
batch.forEach((item) => entry.resolving.delete(pointerKey(item)));
|
||||
return [];
|
||||
}
|
||||
|
||||
let events = [];
|
||||
try {
|
||||
events = await this.pool.list(Array.from(relaySet), filters);
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn("[nostr] Failed to resolve watch history batch:", error);
|
||||
if (filters.length) {
|
||||
try {
|
||||
events = await this.pool.list(Array.from(relaySet), filters);
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn("[nostr] Failed to resolve watch history batch:", error);
|
||||
}
|
||||
}
|
||||
} else if (batch.length) {
|
||||
batch.forEach((item) => entry.resolving.delete(pointerKey(item)));
|
||||
}
|
||||
|
||||
const resolvedVideos = [];
|
||||
const pointerMatches = new Map();
|
||||
|
||||
for (const evt of events || []) {
|
||||
@@ -2830,8 +2893,7 @@ class NostrClient {
|
||||
this.activeMap.set(activeKey, video);
|
||||
}
|
||||
}
|
||||
entry.resolved.set(key, video);
|
||||
resolvedVideos.push(video);
|
||||
entry.resolvedVideos.set(key, video);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDevMode) {
|
||||
@@ -2842,7 +2904,23 @@ class NostrClient {
|
||||
entry.resolving.delete(key);
|
||||
}
|
||||
|
||||
return resolvedVideos;
|
||||
const deliverableVideos = [];
|
||||
for (const pointer of videoCandidates) {
|
||||
if (deliverableVideos.length >= effectiveBatchSize) {
|
||||
break;
|
||||
}
|
||||
const key = pointerKey(pointer);
|
||||
if (!key || entry.delivered.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const cachedVideo = entry.resolvedVideos.get(key);
|
||||
if (cachedVideo && typeof cachedVideo === "object" && cachedVideo.id) {
|
||||
deliverableVideos.push(cachedVideo);
|
||||
entry.delivered.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return deliverableVideos;
|
||||
}
|
||||
|
||||
async recordVideoView(videoPointer, options = {}) {
|
||||
|
||||
@@ -456,12 +456,19 @@ assert.equal(
|
||||
|
||||
const pointerKey = `a:${videoPointer.value.trim().toLowerCase()}`;
|
||||
const cachedEntry = pointerClient.watchHistoryCache.get(ACTOR);
|
||||
assert(cachedEntry.resolved.has(pointerKey), "resolved cache should retain the video");
|
||||
assert(
|
||||
cachedEntry.resolvedVideos.has(pointerKey),
|
||||
"resolved cache should retain the video"
|
||||
);
|
||||
assert.equal(
|
||||
cachedEntry.resolved.get(pointerKey)?.id,
|
||||
cachedEntry.resolvedVideos.get(pointerKey)?.id,
|
||||
videoEvent.id,
|
||||
"resolved cache should store the converted video result"
|
||||
);
|
||||
assert(
|
||||
cachedEntry.delivered.has(pointerKey),
|
||||
"delivery set should mark the pointer as delivered"
|
||||
);
|
||||
|
||||
assert(
|
||||
observedResolveFilters.some((filters) =>
|
||||
@@ -473,6 +480,38 @@ assert(
|
||||
"resolveWatchHistory should request the video identifier instead of treating it as a chunk"
|
||||
);
|
||||
|
||||
const secondResolve = await pointerClient.resolveWatchHistory(1);
|
||||
assert.equal(
|
||||
secondResolve.length,
|
||||
0,
|
||||
"subsequent resolve calls should avoid refetching already delivered videos"
|
||||
);
|
||||
|
||||
pointerClient.resetWatchHistoryProgress(ACTOR);
|
||||
assert.equal(
|
||||
cachedEntry.delivered.size,
|
||||
0,
|
||||
"resetWatchHistoryProgress should clear delivered pointers"
|
||||
);
|
||||
|
||||
pointerClient.pool = {
|
||||
list: async () => {
|
||||
throw new Error("cached resolve should not trigger network fetch");
|
||||
},
|
||||
};
|
||||
|
||||
const cachedResolve = await pointerClient.resolveWatchHistory(1);
|
||||
assert.equal(
|
||||
cachedResolve.length,
|
||||
1,
|
||||
"reset should allow cached videos to be delivered again"
|
||||
);
|
||||
assert.equal(
|
||||
cachedResolve[0].id,
|
||||
videoEvent.id,
|
||||
"cached resolve should reuse the stored video payload"
|
||||
);
|
||||
|
||||
const originalDateNow = Date.now;
|
||||
try {
|
||||
const baseTime = 1_700_000_000_000;
|
||||
|
||||
Reference in new issue
Block a user