From 891226697dc95779b00f35e4163d4d208e0880e4 Mon Sep 17 00:00:00 2001
From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com>
Date: Tue, 6 Jan 2026 18:53:27 -0500
Subject: [PATCH] Simplify watch history pipeline
---
config/instance-config.js | 24 ++++--------------
docs/nostr-analytics.md | 3 ++-
docs/nostr-event-schemas.md | 22 ++++++-----------
docs/watch-history-logging.md | 18 ++++----------
js/watchHistoryService.js | 42 +++-----------------------------
tests/nostr/watchHistory.test.js | 2 +-
tests/watch-history.test.mjs | 21 ++--------------
7 files changed, 26 insertions(+), 106 deletions(-)
diff --git a/config/instance-config.js b/config/instance-config.js
index 8c81c133..15f519ed 100644
--- a/config/instance-config.js
+++ b/config/instance-config.js
@@ -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.
*
diff --git a/docs/nostr-analytics.md b/docs/nostr-analytics.md
index d10a0847..1fb41edd 100644
--- a/docs/nostr-analytics.md
+++ b/docs/nostr-analytics.md
@@ -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.
diff --git a/docs/nostr-event-schemas.md b/docs/nostr-event-schemas.md
index 9bb91cdf..05c66dc1 100644
--- a/docs/nostr-event-schemas.md
+++ b/docs/nostr-event-schemas.md
@@ -144,8 +144,8 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca
| NIP-71 video (`NOTE_TYPES.NIP71_VIDEO`) | `21` | `['title',
]`, optional `['published_at', ]`, optional `['alt', ]`, repeated `['imeta', ...]` entries describing NIP-92 media variants, optional `['duration', ]`, repeated `['text-track', , , ]`, optional `['content-warning', ]`, repeated `['segment', , , , ]`, repeated hashtags `['t', ]`, repeated participants `['p', , ]`, repeated references `['r', ]` | 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', ]` 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', ]` or `['a', ]`), and a stable dedupe tag `['d', ]`, 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}:`]` with optional `['month', ]` marker plus schema append tags; no chunk pointers required. | JSON payload `{ version, month: 'YYYY-MM', items: [{ id: , watched_at?: }] }` |
+| View counter (`NOTE_TYPES.VIEW_EVENT`) | `WATCH_HISTORY_KIND` (default `30079`) | Canonical tag set: `['t','view']`, a pointer tag (`['e', ]` or `['a', ]`), and a stable dedupe tag `['d', ]`, 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}:`]` with optional `['month', ]` marker plus schema append tags; no chunk pointers required. | JSON payload `{ version, month: 'YYYY-MM', items: [{ id: , watched_at?: }] }` |
| 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', ], …]`) |
| 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', ]` 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', ]` tag for readability. The content
+contains a compact JSON body such as:
```json
{
diff --git a/docs/watch-history-logging.md b/docs/watch-history-logging.md
index c0272e3e..a216956e 100644
--- a/docs/watch-history-logging.md
+++ b/docs/watch-history-logging.md
@@ -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
diff --git a/js/watchHistoryService.js b/js/watchHistoryService.js
index e8f1a1ed..f2c27a51 100644
--- a/js/watchHistoryService.js
+++ b/js/watchHistoryService.js
@@ -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)) {
diff --git a/tests/nostr/watchHistory.test.js b/tests/nostr/watchHistory.test.js
index 8fe7d92d..7be1c096 100644
--- a/tests/nostr/watchHistory.test.js
+++ b/tests/nostr/watchHistory.test.js
@@ -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") {
diff --git a/tests/watch-history.test.mjs b/tests/watch-history.test.mjs
index 2dd5f9a6..9ab7a6eb 100644
--- a/tests/watch-history.test.mjs
+++ b/tests/watch-history.test.mjs
@@ -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);
-}