mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 10:22:25 +00:00
Merge pull request #183 from PR0M3TH3AN/codex/update-classifywatchhistorypointer-logic
Fix watch history chunk classification for address pointers
This commit is contained in:
3 files changed
+269
-46
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;
|
||||
|
||||
+151
-46
@@ -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
|
||||
@@ -2579,19 +2639,46 @@ class NostrClient {
|
||||
? Math.max(1, normalizedBatchSize)
|
||||
: 1;
|
||||
|
||||
const classifyWatchHistoryPointer = (pointer) => {
|
||||
const classifyWatchHistoryPointer = (pointer, ownerPubkey) => {
|
||||
if (!pointer || pointer.type !== "a") {
|
||||
return "none";
|
||||
}
|
||||
const [kindStr, pubkey, identifier] = pointer.value.split(":");
|
||||
const value =
|
||||
typeof pointer.value === "string" ? pointer.value.trim() : "";
|
||||
if (!value) {
|
||||
return "none";
|
||||
}
|
||||
const parts = value.split(":");
|
||||
if (parts.length < 3) {
|
||||
return "none";
|
||||
}
|
||||
const [kindStr, pubkeyRaw, ...identifierParts] = parts;
|
||||
const kind = Number.parseInt(kindStr, 10);
|
||||
if (!Number.isFinite(kind) || kind !== WATCH_HISTORY_KIND) {
|
||||
return "none";
|
||||
}
|
||||
if (identifier === WATCH_HISTORY_LIST_IDENTIFIER) {
|
||||
const identifier = identifierParts.join(":");
|
||||
const normalizedIdentifier =
|
||||
typeof identifier === "string" ? identifier.trim() : "";
|
||||
if (!normalizedIdentifier) {
|
||||
return "none";
|
||||
}
|
||||
const normalizedIdentifierLower = normalizedIdentifier.toLowerCase();
|
||||
if (normalizedIdentifierLower === WATCH_HISTORY_LIST_IDENTIFIER) {
|
||||
return "head";
|
||||
}
|
||||
if (typeof identifier === "string" && identifier) {
|
||||
if (!normalizedIdentifierLower.startsWith("watch-history:")) {
|
||||
return "none";
|
||||
}
|
||||
const normalizedOwner =
|
||||
typeof ownerPubkey === "string" && ownerPubkey.trim()
|
||||
? ownerPubkey.trim().toLowerCase()
|
||||
: "";
|
||||
const normalizedPubkey =
|
||||
typeof pubkeyRaw === "string" && pubkeyRaw.trim()
|
||||
? pubkeyRaw.trim().toLowerCase()
|
||||
: "";
|
||||
if (!normalizedOwner || !normalizedPubkey || normalizedOwner === normalizedPubkey) {
|
||||
return "chunk";
|
||||
}
|
||||
return "none";
|
||||
@@ -2628,7 +2715,7 @@ class NostrClient {
|
||||
if (!pointer) {
|
||||
continue;
|
||||
}
|
||||
const pointerType = classifyWatchHistoryPointer(pointer);
|
||||
const pointerType = classifyWatchHistoryPointer(pointer, actor);
|
||||
if (pointerType === "head") {
|
||||
addPointerToMap(pointer, headPointerMap);
|
||||
continue;
|
||||
@@ -2664,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;
|
||||
});
|
||||
|
||||
@@ -2700,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 || []) {
|
||||
@@ -2776,7 +2866,7 @@ class NostrClient {
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
const pointerType = classifyWatchHistoryPointer(pointer);
|
||||
const pointerType = classifyWatchHistoryPointer(pointer, actor);
|
||||
const event = pointerMatches.get(key);
|
||||
if (pointerType === "chunk" || pointerType === "head") {
|
||||
if (event) {
|
||||
@@ -2803,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) {
|
||||
@@ -2815,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 = {}) {
|
||||
|
||||
@@ -398,6 +398,120 @@ try {
|
||||
|
||||
localStorage.clear();
|
||||
|
||||
const VIDEO_POINTER_AUTHOR = `${ACTOR}-video-author`;
|
||||
const VIDEO_POINTER_IDENTIFIER = "pointer-video-identifier";
|
||||
const videoPointer = {
|
||||
type: "a",
|
||||
value: `${WATCH_HISTORY_KIND}:${VIDEO_POINTER_AUTHOR}:${VIDEO_POINTER_IDENTIFIER}`,
|
||||
relay: null,
|
||||
};
|
||||
|
||||
const pointerClient = createDecryptClient(ACTOR);
|
||||
const pointerEntry = pointerClient.createWatchHistoryEntry(
|
||||
null,
|
||||
[videoPointer],
|
||||
Date.now()
|
||||
);
|
||||
pointerClient.watchHistoryCache.set(ACTOR, pointerEntry);
|
||||
pointerClient.fetchWatchHistory = async () => {};
|
||||
|
||||
const observedResolveFilters = [];
|
||||
const videoEvent = {
|
||||
id: "video-pointer-event",
|
||||
kind: WATCH_HISTORY_KIND,
|
||||
pubkey: VIDEO_POINTER_AUTHOR,
|
||||
created_at: 1_700_555_000,
|
||||
tags: [["d", VIDEO_POINTER_IDENTIFIER]],
|
||||
content: JSON.stringify({
|
||||
version: 3,
|
||||
title: "Pointer Video",
|
||||
url: "https://cdn.example.com/video.mp4",
|
||||
}),
|
||||
};
|
||||
|
||||
pointerClient.pool = {
|
||||
list: async (_relays, filters) => {
|
||||
observedResolveFilters.push(filters);
|
||||
return [videoEvent];
|
||||
},
|
||||
};
|
||||
|
||||
const resolvedFromPointer = await pointerClient.resolveWatchHistory(1);
|
||||
|
||||
assert.equal(
|
||||
resolvedFromPointer.length,
|
||||
1,
|
||||
"resolveWatchHistory should return results for 'a' style video pointers"
|
||||
);
|
||||
assert.equal(
|
||||
resolvedFromPointer[0].id,
|
||||
videoEvent.id,
|
||||
"resolved video should match the pointer event id"
|
||||
);
|
||||
assert.equal(
|
||||
resolvedFromPointer[0].title,
|
||||
"Pointer Video",
|
||||
"resolved video should parse title from the event payload"
|
||||
);
|
||||
|
||||
const pointerKey = `a:${videoPointer.value.trim().toLowerCase()}`;
|
||||
const cachedEntry = pointerClient.watchHistoryCache.get(ACTOR);
|
||||
assert(
|
||||
cachedEntry.resolvedVideos.has(pointerKey),
|
||||
"resolved cache should retain the video"
|
||||
);
|
||||
assert.equal(
|
||||
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) =>
|
||||
filters.some((filter) =>
|
||||
Array.isArray(filter?.["#d"]) &&
|
||||
filter["#d"].includes(VIDEO_POINTER_IDENTIFIER)
|
||||
)
|
||||
),
|
||||
"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