mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 01:12:39 +00:00
Removes the metadata argument from `WatchHistoryService.publishView` and cleans up the associated caching and resolution logic in `watchHistory.js` and `watchHistoryFeed.js`. Deletes `js/watchHistoryMetadata.js` as it is no longer used. This simplification ensures watch history strictly manages pointers and timestamps, leaving metadata resolution to higher-level consumers. It also reduces the payload size and complexity of persisted history records. Tests have been updated to reflect the API changes.
225 lines
5.9 KiB
JavaScript
225 lines
5.9 KiB
JavaScript
import "../test-helpers/setup-localstorage.mjs";
|
|
import assert from "node:assert/strict";
|
|
|
|
import WatchHistoryTelemetry from "../../js/services/watchHistoryTelemetry.js";
|
|
|
|
const hexPubkey = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
|
|
|
|
function createTelemetry({
|
|
serviceOverrides = {},
|
|
controllerOverrides = {},
|
|
options = {},
|
|
} = {}) {
|
|
const watchHistoryService = {
|
|
isEnabled: () => true,
|
|
getSettings: () => ({ metadata: { storeLocally: true } }),
|
|
setLocalMetadata: () => {},
|
|
removeLocalMetadata: () => {},
|
|
clearLocalMetadata: () => {},
|
|
publishView: async () => ({ ok: true }),
|
|
subscribe: () => () => {},
|
|
...serviceOverrides,
|
|
};
|
|
|
|
const watchHistoryController = {
|
|
handleWatchHistoryRemoval: async () => ({}),
|
|
flush: async () => {},
|
|
...controllerOverrides,
|
|
};
|
|
|
|
const telemetry = new WatchHistoryTelemetry({
|
|
watchHistoryService,
|
|
watchHistoryController,
|
|
nostrClient: options.nostrClient || { pubkey: hexPubkey },
|
|
log: options.log || (() => {}),
|
|
normalizeHexPubkey:
|
|
options.normalizeHexPubkey || ((value) => (typeof value === "string" ? value.toLowerCase() : null)),
|
|
getActiveUserPubkey: options.getActiveUserPubkey || (() => hexPubkey),
|
|
ingestLocalViewEvent: options.ingestLocalViewEvent || (() => {}),
|
|
viewThresholdSeconds: options.viewThresholdSeconds,
|
|
});
|
|
|
|
return { telemetry, watchHistoryService, watchHistoryController };
|
|
}
|
|
|
|
class FakeVideoElement {
|
|
constructor() {
|
|
this.listeners = new Map();
|
|
this.paused = true;
|
|
this.currentTime = 0;
|
|
}
|
|
|
|
addEventListener(eventName, handler) {
|
|
if (!this.listeners.has(eventName)) {
|
|
this.listeners.set(eventName, new Set());
|
|
}
|
|
this.listeners.get(eventName).add(handler);
|
|
}
|
|
|
|
removeEventListener(eventName, handler) {
|
|
const set = this.listeners.get(eventName);
|
|
if (!set) {
|
|
return;
|
|
}
|
|
set.delete(handler);
|
|
if (set.size === 0) {
|
|
this.listeners.delete(eventName);
|
|
}
|
|
}
|
|
|
|
emit(eventName) {
|
|
const set = this.listeners.get(eventName);
|
|
if (!set) {
|
|
return;
|
|
}
|
|
for (const handler of set) {
|
|
handler({ type: eventName });
|
|
}
|
|
}
|
|
}
|
|
|
|
async function flushAsyncOperations() {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
}
|
|
|
|
async function testMetadataPreferenceSync() {
|
|
const { telemetry } = createTelemetry();
|
|
const unsubscribe = await telemetry.initPreferenceSync();
|
|
assert.equal(unsubscribe, null, "initPreferenceSync should no-op");
|
|
assert.equal(telemetry.isMetadataPreferenceEnabled(), false);
|
|
}
|
|
|
|
async function testPreparePlaybackLoggingPublishesOnce() {
|
|
const ingestEvents = [];
|
|
const publishCalls = [];
|
|
|
|
const pointer = { type: "e", value: "event-id" };
|
|
const pointerKey = "event-id::relay";
|
|
|
|
const { telemetry } = createTelemetry({
|
|
serviceOverrides: {
|
|
publishView: async (ptr, _relay) => {
|
|
publishCalls.push({ ptr });
|
|
return {
|
|
ok: true,
|
|
event: { pubkey: hexPubkey },
|
|
};
|
|
},
|
|
},
|
|
options: {
|
|
ingestLocalViewEvent: (payload) => ingestEvents.push(payload),
|
|
},
|
|
});
|
|
|
|
const videoA = new FakeVideoElement();
|
|
videoA.paused = false;
|
|
videoA.currentTime = telemetry.viewThresholdSeconds;
|
|
|
|
telemetry.preparePlaybackLogging({
|
|
videoElement: videoA,
|
|
pointer,
|
|
pointerKey,
|
|
});
|
|
await flushAsyncOperations();
|
|
|
|
assert.equal(publishCalls.length, 1, "publishView should be called once");
|
|
assert.equal(ingestEvents.length, 1, "ingestLocalViewEvent should run once");
|
|
assert.deepEqual(publishCalls[0].ptr, pointer);
|
|
assert.deepEqual(ingestEvents[0].pointer, pointer);
|
|
|
|
const videoB = new FakeVideoElement();
|
|
videoB.paused = false;
|
|
videoB.currentTime = telemetry.viewThresholdSeconds;
|
|
|
|
telemetry.preparePlaybackLogging({
|
|
videoElement: videoB,
|
|
pointer,
|
|
pointerKey,
|
|
});
|
|
await flushAsyncOperations();
|
|
|
|
assert.equal(
|
|
publishCalls.length,
|
|
1,
|
|
"Repeated preparePlaybackLogging should reuse cooldown cache"
|
|
);
|
|
}
|
|
|
|
async function testPreparePlaybackLoggingIgnoresMetadata() {
|
|
const publishCalls = [];
|
|
|
|
const pointer = { type: "e", value: "event-id" };
|
|
const pointerKey = "event-id";
|
|
|
|
const { telemetry } = createTelemetry({
|
|
serviceOverrides: {
|
|
publishView: async (ptr, _relay) => {
|
|
publishCalls.push({ ptr });
|
|
return {
|
|
ok: true,
|
|
event: { pubkey: hexPubkey },
|
|
};
|
|
},
|
|
},
|
|
});
|
|
|
|
const videoElement = new FakeVideoElement();
|
|
videoElement.paused = false;
|
|
videoElement.currentTime = telemetry.viewThresholdSeconds;
|
|
|
|
const videoMetadata = {
|
|
id: "evt1",
|
|
title: "Example Video",
|
|
thumbnail: "https://example.com/thumb.jpg",
|
|
pubkey: hexPubkey,
|
|
rootCreatedAt: 1_700_000_000,
|
|
url: "https://example.com/video.mp4",
|
|
};
|
|
|
|
telemetry.preparePlaybackLogging({
|
|
videoElement,
|
|
pointer,
|
|
pointerKey,
|
|
video: videoMetadata,
|
|
});
|
|
|
|
await flushAsyncOperations();
|
|
|
|
assert.equal(publishCalls.length, 1, "publishView should be invoked once");
|
|
assert.equal(publishCalls[0].ptr, pointer);
|
|
assert.equal(publishCalls[0].metadata, undefined, "metadata should NOT be passed");
|
|
}
|
|
|
|
async function testCancelPlaybackLoggingFlushes() {
|
|
let flushArgs = null;
|
|
const video = new FakeVideoElement();
|
|
const { telemetry } = createTelemetry({
|
|
controllerOverrides: {
|
|
flush: async (reason, context) => {
|
|
flushArgs = { reason, context };
|
|
},
|
|
},
|
|
});
|
|
|
|
video.paused = true;
|
|
video.currentTime = 0;
|
|
|
|
telemetry.preparePlaybackLogging({
|
|
videoElement: video,
|
|
pointer: { type: "e", value: "event" },
|
|
pointerKey: "event",
|
|
});
|
|
|
|
telemetry.cancelPlaybackLogging({ reason: "unit", context: "test" });
|
|
await flushAsyncOperations();
|
|
|
|
assert.deepEqual(flushArgs, { reason: "unit", context: "test" });
|
|
}
|
|
|
|
await testMetadataPreferenceSync();
|
|
await testPreparePlaybackLoggingPublishesOnce();
|
|
await testPreparePlaybackLoggingIgnoresMetadata();
|
|
await testCancelPlaybackLoggingFlushes();
|
|
|
|
console.log("watch-history-telemetry tests passed");
|