mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Simplify watch history pipeline
This commit is contained in:
1 parent
bd5d5e9a87
commit
891226697d
7 files changed
+26
-106
No files matched your search
@@ -293,15 +293,12 @@ export const THEME_ACCENT_OVERRIDES = Object.freeze({
|
||||
/**
|
||||
* Nostr kind used when persisting watch history events.
|
||||
*
|
||||
* bitvid’s roadmap standardizes on kind 30079 so that watch events, view logs,
|
||||
* and media metadata stay in the same family of documents. During the rollout
|
||||
* from the legacy 30078 payloads, clients query both kinds so historical data
|
||||
* keeps syncing; remove the compatibility fetch once all writers emit the new
|
||||
* kind. Operators that want to experiment with a separate list kind (for
|
||||
* example, a NIP-51 collection) can flip this number so long as their relays
|
||||
* accept the chosen kind.
|
||||
* bitvid standardizes on kind 30079 so that watch events, view logs, and media
|
||||
* metadata stay in the same family of documents. Operators that want to
|
||||
* experiment with a separate list kind (for example, a NIP-51 collection) can
|
||||
* flip this number so long as their relays accept the chosen kind.
|
||||
*/
|
||||
export const WATCH_HISTORY_KIND = 30003;
|
||||
export const WATCH_HISTORY_KIND = 30079;
|
||||
|
||||
/**
|
||||
* Identifier applied to the watch-history list when storing it on relays.
|
||||
@@ -313,17 +310,6 @@ export const WATCH_HISTORY_KIND = 30003;
|
||||
*/
|
||||
export const WATCH_HISTORY_LIST_IDENTIFIER = "watch-history";
|
||||
|
||||
/**
|
||||
* Whether to enable the V2 encrypted watch-history service.
|
||||
*
|
||||
* The runtime flag that controls this feature defaults to `false` so that new
|
||||
* deployments stick with the analytics-only view flow until operators opt in.
|
||||
* To enable V2, set `window.__BITVID_RUNTIME_FLAGS__.FEATURE_WATCH_HISTORY_V2 = true`
|
||||
* in a bootstrap script (or override the value before the app loads). When the
|
||||
* flag stays off, bitvid still emits legacy view events and will honor
|
||||
* existing watch-history reads per plan §12, but the sync UI will surface a
|
||||
* disabled banner instead of querying relays.
|
||||
*/
|
||||
/**
|
||||
* Maximum number of watch-history entries to retain per user.
|
||||
*
|
||||
|
||||
@@ -18,7 +18,8 @@ Playback telemetry runs even when a viewer has not connected a Nostr account. Th
|
||||
|
||||
When a player crosses the 12-second watch threshold, bitvid invokes [`publishViewEvent`](../js/nostr/viewEvents.js#L662-L838) through the view-only [`recordVideoView`](../js/nostr/viewEvents.js#L854-L879) contract. The helper considers the publish successful as soon as any configured relay acknowledges the event, which in practice keeps latency under a second on public relays. The UI optimistically increments totals by feeding the signed event into [`ingestLocalViewEvent`](../js/viewCounter.js#L619-L633), so cards and modals reflect the new view immediately even while other relays finish syncing.
|
||||
|
||||
The watch history pipeline has been retired; `recordVideoView` now focuses solely on analytics publishing.
|
||||
After a view is recorded, the watch history service updates the actor's queue
|
||||
and publishes snapshots as needed, keeping analytics and history in sync.
|
||||
|
||||
To avoid double-counting the same session, the playback stack respects the cooldown keys generated in [`js/app.js`](../js/app.js#L6825-L6893). The combination of pointer identity and viewer fingerprint prevents re-logging until the dedupe window expires or the user switches accounts.
|
||||
|
||||
|
||||
@@ -144,8 +144,8 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca
|
||||
| NIP-71 video (`NOTE_TYPES.NIP71_VIDEO`) | `21` | `['title', <title>]`, optional `['published_at', <unix seconds>]`, optional `['alt', <text>]`, repeated `['imeta', ...]` entries describing NIP-92 media variants, optional `['duration', <seconds>]`, repeated `['text-track', <url>, <kind>, <language>]`, optional `['content-warning', <reason>]`, repeated `['segment', <start>, <end>, <title>, <thumbnail>]`, repeated hashtags `['t', <tag>]`, repeated participants `['p', <pubkey>, <relay?>]`, repeated references `['r', <url>]` | Plain text summary carried in the content field. Publishing is gated by the `FEATURE_PUBLISH_NIP71` runtime flag while the rollout stabilizes. |
|
||||
| NIP-71 short video (`NOTE_TYPES.NIP71_SHORT_VIDEO`) | `22` | Same as `NOTE_TYPES.NIP71_VIDEO`; the kind differentiates short-form presentations. | Plain text summary; gated by `FEATURE_PUBLISH_NIP71`. |
|
||||
| Relay list (`NOTE_TYPES.RELAY_LIST`) | `10002` | Repeating `['r', <relay url>]` tags, optionally with a marker of `'read'` or `'write'` to scope the relay; marker omitted for read/write relays | Empty content |
|
||||
| View counter (`NOTE_TYPES.VIEW_EVENT`) | `WATCH_HISTORY_KIND` (default `30079`, clients also read legacy `30078`) | Canonical tag set: `['t','view']`, a pointer tag (`['e', <eventId>]` or `['a', <address>]`), and a stable dedupe tag `['d', <view identifier>]`, with optional `['session','true']` when a session actor signs; schema overrides may append extra tags. `['video', ...]` is supported for legacy overrides only. | Optional plaintext message |
|
||||
| Watch history month (`NOTE_TYPES.WATCH_HISTORY_INDEX`) | `WATCH_HISTORY_KIND` (default `30079`, clients also read legacy `30078`) | Replaceable list tag `['d', `${WATCH_HISTORY_LIST_IDENTIFIER}:<YYYY-MM>`]` with optional `['month', <YYYY-MM>]` marker plus schema append tags; no chunk pointers required. | JSON payload `{ version, month: 'YYYY-MM', items: [{ id: <eventId\|address>, watched_at?: <unix seconds> }] }` |
|
||||
| View counter (`NOTE_TYPES.VIEW_EVENT`) | `WATCH_HISTORY_KIND` (default `30079`) | Canonical tag set: `['t','view']`, a pointer tag (`['e', <eventId>]` or `['a', <address>]`), and a stable dedupe tag `['d', <view identifier>]`, with optional `['session','true']` when a session actor signs; schema overrides may append extra tags. `['video', ...]` is supported for legacy overrides only. | Optional plaintext message |
|
||||
| Watch history month (`NOTE_TYPES.WATCH_HISTORY_INDEX`) | `WATCH_HISTORY_KIND` (default `30079`) | Replaceable list tag `['d', `${WATCH_HISTORY_LIST_IDENTIFIER}:<YYYY-MM>`]` with optional `['month', <YYYY-MM>]` marker plus schema append tags; no chunk pointers required. | JSON payload `{ version, month: 'YYYY-MM', items: [{ id: <eventId\|address>, watched_at?: <unix seconds> }] }` |
|
||||
| Subscription list (`NOTE_TYPES.SUBSCRIPTION_LIST`) | `30000` | `['d', 'subscriptions']` | NIP-04/NIP-44 encrypted JSON array of NIP-51 follow-set tuples (e.g., `[['p', <hex>], …]`) |
|
||||
| User block list (`NOTE_TYPES.USER_BLOCK_LIST`) | `10000` | `['d', 'user-blocks']` | NIP-04/NIP-44 encrypted JSON `{ blockedPubkeys: string[] }` |
|
||||
| Hashtag preferences (`NOTE_TYPES.HASHTAG_PREFERENCES`) | `30015` | `['d', 'bitvid:tag-preferences']` plus schema-appended `['encrypted','nip44_v2']` | NIP-44 encrypted JSON `{ version, interests: string[], disinterests: string[] }` |
|
||||
@@ -243,18 +243,12 @@ experimentation.
|
||||
|
||||
### Watch history identifiers
|
||||
|
||||
The watch history pipeline is gated by the `FEATURE_WATCH_HISTORY_V2` runtime
|
||||
flag. When the flag is disabled, clients continue emitting view events but skip
|
||||
publishing monthly records; the UI still resolves legacy
|
||||
`watch-history:v2:index` lists so operators can stage the rollout per
|
||||
deployment.【F:config/instance-config.js†L69-L94】【F:js/watchHistoryService.js†L82-L140】【F:js/watchHistoryService.js†L948-L985】
|
||||
|
||||
Active identifiers include the default `WATCH_HISTORY_LIST_IDENTIFIER`
|
||||
(`"watch-history"`) and the legacy aliases enumerated in
|
||||
`WATCH_HISTORY_LEGACY_LIST_IDENTIFIERS`. Each month publishes as a replaceable
|
||||
record keyed by `['d', `${identifier}:${YYYY-MM}`]`, optionally echoed in a
|
||||
`['month', <YYYY-MM>]` tag for readability. The content carries a compact JSON
|
||||
body such as:
|
||||
The watch history pipeline is always enabled and publishes encrypted monthly
|
||||
records to the canonical `WATCH_HISTORY_KIND` stream. Clients rely on the
|
||||
default `WATCH_HISTORY_LIST_IDENTIFIER` (`"watch-history"`) and store each
|
||||
month as a replaceable record keyed by `['d', `${identifier}:${YYYY-MM}`]`,
|
||||
optionally echoed in a `['month', <YYYY-MM>]` tag for readability. The content
|
||||
contains a compact JSON body such as:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -38,11 +38,11 @@ renderers like `historyView`.【F:config/instance-config.js†L101-L123】【F:j
|
||||
|
||||
## Snapshot cadence
|
||||
|
||||
`WatchHistoryService.publishView` queues pointer entries while the feature flag
|
||||
is enabled, merging repeats with a one-minute throttle and persisting the queue
|
||||
in `sessionStorage`. Snapshots run when `watchHistoryService.snapshot()` is
|
||||
invoked (e.g., on `beforeunload`, `visibilitychange`, or manual sync actions in
|
||||
the history UI) and clear the queue on success.【F:js/watchHistoryService.js†L872-L917】【F:js/app.js†L6041-L6081】【F:js/app.js†L7467-L7484】
|
||||
`WatchHistoryService.publishView` queues pointer entries, merging repeats with a
|
||||
one-minute throttle and persisting the queue in `sessionStorage`. Snapshots run
|
||||
when `watchHistoryService.snapshot()` is invoked (e.g., on `beforeunload`,
|
||||
`visibilitychange`, or manual sync actions in the history UI) and clear the
|
||||
queue on success.【F:js/watchHistoryService.js†L872-L917】【F:js/app.js†L6041-L6081】【F:js/app.js†L7467-L7484】
|
||||
If no items are pending, the snapshot resolves with an empty result so UI calls
|
||||
remain idempotent.【F:js/watchHistoryService.js†L932-L944】
|
||||
|
||||
@@ -54,14 +54,6 @@ Nostr client to retry with exponential backoff. Retries start after two seconds,
|
||||
double per attempt with 25% jitter, cap at five minutes, and stop after eight
|
||||
attempts unless a publish succeeds sooner.【F:js/watchHistoryService.js†L948-L985】【F:js/watchHistoryService.js†L723-L777】【F:js/nostr/watchHistory.js†L929-L999】
|
||||
|
||||
## Legacy payload handling
|
||||
|
||||
Readers hydrate history by combining the pointer event, any encrypted chunks,
|
||||
and fallback metadata embedded in legacy watch-history lists. The resolver still
|
||||
accepts the `watch-history:v2:index` identifier, merges pointer tags from the
|
||||
index event, and falls back to plaintext content when decryption fails so older
|
||||
relays remain compatible.【F:config/instance-config.js†L60-L78】【F:js/nostr/watchHistory.js†L2120-L2170】【F:js/nostr/watchHistory.js†L660-L689】
|
||||
|
||||
## Local metadata toggles
|
||||
|
||||
By default the service only persists pointer data to relays; richer metadata is
|
||||
|
||||
@@ -13,11 +13,6 @@ import {
|
||||
WATCH_HISTORY_CACHE_TTL_MS,
|
||||
WATCH_HISTORY_MAX_ITEMS,
|
||||
} from "./config.js";
|
||||
import {
|
||||
FEATURE_WATCH_HISTORY_V2,
|
||||
getWatchHistoryV2Enabled,
|
||||
} from "./constants.js";
|
||||
import { isDevMode } from "./config.js";
|
||||
import { getApplication } from "./applicationContext.js";
|
||||
import { devLogger, userLogger } from "./utils/logger.js";
|
||||
|
||||
@@ -32,20 +27,6 @@ const state = {
|
||||
listeners: new Map(),
|
||||
};
|
||||
|
||||
function resolveFlagEnabled() {
|
||||
try {
|
||||
if (typeof getWatchHistoryV2Enabled === "function") {
|
||||
return getWatchHistoryV2Enabled() === true;
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[watchHistoryService] Failed to read FEATURE_WATCH_HISTORY_V2 flag:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
return FEATURE_WATCH_HISTORY_V2 === true;
|
||||
}
|
||||
|
||||
function getLoggedInActorKey() {
|
||||
const direct = normalizeActorKey(nostrClient?.pubkey);
|
||||
if (direct) {
|
||||
@@ -162,29 +143,12 @@ function resolveEffectiveActorKey(actorInput) {
|
||||
}
|
||||
|
||||
function isFeatureEnabled(actorInput) {
|
||||
const baseEnabled = resolveFlagEnabled();
|
||||
const actorKey = resolveEffectiveActorKey(actorInput);
|
||||
const logged = getLoggedInActorKey();
|
||||
const session = getSessionActorKey();
|
||||
|
||||
if (actorKey && logged && actorKey === logged) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (actorKey && session && actorKey === session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!actorKey) {
|
||||
if (logged) {
|
||||
return true;
|
||||
}
|
||||
if (session) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return baseEnabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
function isLocalOnly(actorInput) {
|
||||
@@ -881,14 +845,14 @@ async function snapshot(items, options = {}) {
|
||||
|
||||
if (!isFeatureEnabled(actorKey)) {
|
||||
devLogger.debug(
|
||||
"[watchHistoryService] Snapshot skipped because watch history sync is unavailable for this actor.",
|
||||
"[watchHistoryService] Snapshot skipped because this actor is limited to local watch history.",
|
||||
{
|
||||
actor: actorKey,
|
||||
requestedItems: Array.isArray(items) ? items.length : 0,
|
||||
reason,
|
||||
}
|
||||
);
|
||||
return { ok: true, skipped: true, reason: "feature-disabled" };
|
||||
return { ok: true, skipped: true, reason: "local-only" };
|
||||
}
|
||||
|
||||
if (state.inflightSnapshots.has(actorKey)) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "../../js/nostr/watchHistory.js";
|
||||
import { WATCH_HISTORY_KIND } from "../../js/config.js";
|
||||
|
||||
const WATCH_HISTORY_STORAGE_KEY = "bitvid:watch-history:v2";
|
||||
const WATCH_HISTORY_STORAGE_KEY = "bitvid:watch-history:v4";
|
||||
|
||||
function ensureLocalStorageCleared() {
|
||||
if (typeof localStorage?.clear === "function") {
|
||||
|
||||
@@ -13,10 +13,6 @@ const {
|
||||
WATCH_HISTORY_KIND,
|
||||
} =
|
||||
await import("../js/config.js");
|
||||
const {
|
||||
getWatchHistoryV2Enabled,
|
||||
setWatchHistoryV2Enabled,
|
||||
} = await import("../js/constants.js");
|
||||
const [{ nostrClient }, { setActiveSigner, getActiveSigner, clearActiveSigner }] =
|
||||
await Promise.all([
|
||||
import("../js/nostrClientFacade.js"),
|
||||
@@ -43,9 +39,6 @@ if (!window.crypto || !window.crypto.subtle) {
|
||||
window.crypto = webcrypto;
|
||||
}
|
||||
|
||||
const originalFlag = getWatchHistoryV2Enabled();
|
||||
setWatchHistoryV2Enabled(true);
|
||||
|
||||
const originalWindowNostr = window.nostr;
|
||||
const originalNostrTools = window.NostrTools || {};
|
||||
const originalWatchHistoryEnsureTools =
|
||||
@@ -2323,7 +2316,6 @@ async function testWatchHistoryLocalFallbackWhenDisabled() {
|
||||
const originalSession = nostrClient.sessionActor;
|
||||
|
||||
try {
|
||||
setWatchHistoryV2Enabled(false);
|
||||
localStorage.clear();
|
||||
watchHistoryService.resetProgress();
|
||||
nostrClient.pubkey = "";
|
||||
@@ -2358,7 +2350,7 @@ async function testWatchHistoryLocalFallbackWhenDisabled() {
|
||||
assert.equal(
|
||||
enabled,
|
||||
false,
|
||||
"sync should be disabled for session actors when feature flag is off",
|
||||
"sync should be disabled for session actors",
|
||||
);
|
||||
|
||||
const createdAt = 1_700_500_000;
|
||||
@@ -2387,7 +2379,6 @@ async function testWatchHistoryLocalFallbackWhenDisabled() {
|
||||
"local fallback entries should carry watchedAt timestamps",
|
||||
);
|
||||
} finally {
|
||||
setWatchHistoryV2Enabled(true);
|
||||
watchHistoryService.resetProgress();
|
||||
nostrClient.recordVideoView = originalRecordView;
|
||||
nostrClient.pubkey = originalPub;
|
||||
@@ -2405,7 +2396,6 @@ async function testWatchHistorySyncEnabledForLoggedInUsers() {
|
||||
const originalFingerprint = nostrClient.getWatchHistoryFingerprint;
|
||||
|
||||
try {
|
||||
setWatchHistoryV2Enabled(false);
|
||||
localStorage.clear();
|
||||
watchHistoryService.resetProgress();
|
||||
nostrClient.pubkey = actor;
|
||||
@@ -2452,7 +2442,7 @@ async function testWatchHistorySyncEnabledForLoggedInUsers() {
|
||||
assert.equal(
|
||||
enabled,
|
||||
true,
|
||||
"sync should remain enabled for logged-in actors even when the flag is disabled",
|
||||
"sync should remain enabled for logged-in actors",
|
||||
);
|
||||
|
||||
const items = await watchHistoryService.loadLatest(actor);
|
||||
@@ -2469,7 +2459,6 @@ async function testWatchHistorySyncEnabledForLoggedInUsers() {
|
||||
"fingerprint cache should update for logged-in actors",
|
||||
);
|
||||
} finally {
|
||||
setWatchHistoryV2Enabled(true);
|
||||
watchHistoryService.resetProgress();
|
||||
nostrClient.resolveWatchHistory = originalResolve;
|
||||
nostrClient.getWatchHistoryFingerprint = originalFingerprint;
|
||||
@@ -2487,7 +2476,6 @@ async function testWatchHistoryAppLoginFallback() {
|
||||
const originalApp = getApplication();
|
||||
|
||||
try {
|
||||
setWatchHistoryV2Enabled(false);
|
||||
localStorage.clear();
|
||||
watchHistoryService.resetProgress();
|
||||
nostrClient.pubkey = "";
|
||||
@@ -2512,7 +2500,6 @@ async function testWatchHistoryAppLoginFallback() {
|
||||
"sync should be enabled when the app reports a logged-in pubkey",
|
||||
);
|
||||
} finally {
|
||||
setWatchHistoryV2Enabled(true);
|
||||
watchHistoryService.resetProgress();
|
||||
nostrClient.pubkey = originalPub;
|
||||
nostrClient.sessionActor = originalSession;
|
||||
@@ -2682,7 +2669,3 @@ if (originalExtensionPermissionSnapshot.length) {
|
||||
} else {
|
||||
clearStoredExtensionPermissions();
|
||||
}
|
||||
|
||||
if (!originalFlag) {
|
||||
setWatchHistoryV2Enabled(false);
|
||||
}
|
||||
Reference in new issue
Block a user