Merge pull request #1070 from PR0M3TH3AN/refactor-watch-history-payload-v2-2612877448360263100

Refactor watch history to use monthly payload schema
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-04 18:54:18 -05:00
commit 4e8364764e
2 files changed
+58 -14

No files matched your search

+56 -12
View File
@@ -316,11 +316,16 @@ function normalizePointersFromPayload(payload) {
return normalized;
}
const WATCH_HISTORY_MONTH_PATTERN = /^(\d{4})-(0[1-9]|1[0-2])$/;
function parseWatchHistoryPayload(plaintext) {
if (typeof plaintext !== "string") {
return {
version: 0,
month: "",
items: [],
events: [],
watchedAt: {},
snapshot: "",
};
}
@@ -329,24 +334,39 @@ function parseWatchHistoryPayload(plaintext) {
if (!parsed || typeof parsed !== "object") {
return {
version: 0,
month: "",
items: [],
events: [],
watchedAt: {},
snapshot: "",
};
}
const version = Number.isFinite(parsed.version) ? parsed.version : 0;
const month =
typeof parsed.month === "string" && WATCH_HISTORY_MONTH_PATTERN.test(parsed.month)
? parsed.month
: "";
const items = normalizePointersFromPayload(parsed);
const snapshot = typeof parsed.snapshot === "string" ? parsed.snapshot : "";
return { version, items, snapshot };
const events = Array.isArray(parsed.events)
? parsed.events.filter((id) => typeof id === "string" && id.trim())
: [];
const watchedAt =
parsed.watchedAt && typeof parsed.watchedAt === "object" ? parsed.watchedAt : {};
return { version, month, items, events, watchedAt, snapshot };
} catch (error) {
devLogger.warn("[nostr] Failed to parse watch history payload:", error);
return {
version: 0,
month: "",
items: [],
events: [],
watchedAt: {},
snapshot: "",
};
}
}
const WATCH_HISTORY_MONTH_PATTERN = /^(\d{4})-(0[1-9]|1[0-2])$/;
function normalizeWatchHistoryPayloadItem(entry, watchedAtMap) {
const pointer = normalizePointerInput(entry);
@@ -385,7 +405,7 @@ export function buildWatchHistoryPayload(monthString, events, watchedAtMap, maxB
const payload = {
version: 2,
month: normalizedMonth,
items: [],
events: [],
watchedAt: {},
};
@@ -406,7 +426,7 @@ export function buildWatchHistoryPayload(monthString, events, watchedAtMap, maxB
continue;
}
const nextItems = [...payload.items, normalized.id];
const nextEvents = [...payload.events, normalized.id];
const nextWatchedAt = { ...payload.watchedAt };
if (Number.isFinite(normalized.watchedAt)) {
nextWatchedAt[normalized.id] = normalized.watchedAt;
@@ -414,7 +434,7 @@ export function buildWatchHistoryPayload(monthString, events, watchedAtMap, maxB
const candidate = {
version: payload.version,
month: payload.month,
items: nextItems,
events: nextEvents,
watchedAt: nextWatchedAt,
};
@@ -428,7 +448,7 @@ export function buildWatchHistoryPayload(monthString, events, watchedAtMap, maxB
continue;
}
payload.items = nextItems;
payload.events = nextEvents;
payload.watchedAt = nextWatchedAt;
included.push(normalized.pointer);
}
@@ -877,10 +897,38 @@ function mergeWatchHistoryItemsWithFallback(parsed, fallbackItems) {
if (!parsed || typeof parsed !== "object") {
return {
version: 0,
month: "",
items: fallbackItems,
snapshot: "",
chunkIndex: 0,
totalChunks: 1,
};
}
if (Array.isArray(parsed.events) && parsed.events.length) {
const fallbackMap = new Map();
for (const item of Array.isArray(fallbackItems) ? fallbackItems : []) {
const key = pointerKey(item);
if (key) {
fallbackMap.set(item.value, item);
}
}
const merged = [];
const watchedAtMap = parsed.watchedAt || {};
for (const id of parsed.events) {
const fallback = fallbackMap.get(id);
const pointer = fallback ? clonePointerItem(fallback) : normalizePointerInput(id);
if (!pointer) {
continue;
}
const watchedAt = watchedAtMap[id];
if (Number.isFinite(watchedAt)) {
pointer.watchedAt = Math.max(0, Math.floor(watchedAt));
}
merged.push(pointer);
}
return {
version: parsed.version,
month: parsed.month,
items: merged,
snapshot: parsed.snapshot,
};
}
if (Array.isArray(parsed.items) && parsed.items.length) {
@@ -2533,10 +2581,6 @@ class WatchHistoryManager {
version: 2,
items: fetchResult.items || [],
snapshot: fetchResult.snapshotId || "",
chunkIndex: 0,
totalChunks: Array.isArray(fetchResult.items)
? fetchResult.items.length
: 0,
},
fallbackItems,
);
+2 -2
View File
@@ -1001,7 +1001,7 @@ async function testPublishSnapshotCanonicalizationAndChunking() {
);
const payload = JSON.parse(plaintext);
assert.equal(payload.snapshot, "session-snapshot");
assert(Array.isArray(payload.items), "payload should include items");
assert(Array.isArray(payload.events), "payload should include events");
const serializedLength = plaintext.length;
assert(
serializedLength <= WATCH_HISTORY_PAYLOAD_MAX_BYTES,
@@ -1106,7 +1106,7 @@ async function testPublishSnapshotUsesExtensionCrypto() {
const payload = JSON.parse(decrypted);
assert.equal(payload.snapshot, "extension");
assert.equal(
payload.items.length,
payload.events.length,
result.items.length,
"decrypted payload should match canonical item count",
);