Merge pull request #1281 from PR0M3TH3AN/cache-policies-wiring-2547401626759645404

Centralized Cache Policy Registry
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-17 14:15:40 -05:00
commit 7a7ecde97f
8 files changed
+216 -6

No files matched your search

+12
View File
@@ -17,7 +17,9 @@ import { devLogger, userLogger } from "./utils/logger.js";
import { import {
buildAdminListEvent, buildAdminListEvent,
ADMIN_LIST_IDENTIFIERS, ADMIN_LIST_IDENTIFIERS,
NOTE_TYPES,
} from "./nostrEventSchemas.js"; } from "./nostrEventSchemas.js";
import { CACHE_POLICIES, STORAGE_TIERS } from "./nostr/cachePolicies.js";
import { publishEventToRelay } from "./nostrPublish.js"; import { publishEventToRelay } from "./nostrPublish.js";
const ADMIN_STATE_CACHE_VERSION = 1; const ADMIN_STATE_CACHE_VERSION = 1;
@@ -231,6 +233,11 @@ function hasAnyEntries(state = {}) {
} }
export const readCachedAdminState = () => { export const readCachedAdminState = () => {
const policy = CACHE_POLICIES[NOTE_TYPES.ADMIN_MODERATION_LIST];
if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) {
return null;
}
if (typeof localStorage === "undefined") { if (typeof localStorage === "undefined") {
return null; return null;
} }
@@ -263,6 +270,11 @@ export const readCachedAdminState = () => {
}; };
export const writeCachedAdminState = (state) => { export const writeCachedAdminState = (state) => {
const policy = CACHE_POLICIES[NOTE_TYPES.ADMIN_MODERATION_LIST];
if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) {
return;
}
if (typeof localStorage === "undefined") { if (typeof localStorage === "undefined") {
return; return;
} }
+87
View File
@@ -0,0 +1,87 @@
import { NOTE_TYPES } from "../nostrEventSchemas.js";
export const STORAGE_TIERS = Object.freeze({
MEMORY: "memory",
INDEXED_DB: "indexedDB",
LOCAL_STORAGE: "localStorage",
});
export const MERGE_STRATEGIES = Object.freeze({
REPLACEABLE: "replaceable",
APPEND_ONLY: "append-only",
});
export const CACHE_POLICIES = Object.freeze({
[NOTE_TYPES.VIDEO_POST]: {
storage: STORAGE_TIERS.INDEXED_DB,
ttl: 10 * 60 * 1000, // 10 minutes
addressing: "kind:pubkey:d",
merge: MERGE_STRATEGIES.REPLACEABLE,
},
[NOTE_TYPES.WATCH_HISTORY]: {
storage: STORAGE_TIERS.LOCAL_STORAGE,
ttl: 24 * 60 * 60 * 1000, // 24 hours
addressing: "kind:pubkey:d",
merge: MERGE_STRATEGIES.APPEND_ONLY, // Bucketed append
},
[NOTE_TYPES.SUBSCRIPTION_LIST]: {
storage: STORAGE_TIERS.LOCAL_STORAGE,
ttl: Infinity, // Manual management
addressing: "kind:pubkey:d",
merge: MERGE_STRATEGIES.REPLACEABLE,
},
[NOTE_TYPES.USER_BLOCK_LIST]: {
storage: STORAGE_TIERS.LOCAL_STORAGE,
ttl: Infinity, // Manual management
addressing: "kind:pubkey:d",
merge: MERGE_STRATEGIES.REPLACEABLE,
},
[NOTE_TYPES.RELAY_LIST]: {
storage: STORAGE_TIERS.LOCAL_STORAGE,
ttl: Infinity, // Manual management
addressing: "kind:pubkey",
merge: MERGE_STRATEGIES.REPLACEABLE,
},
[NOTE_TYPES.ADMIN_MODERATION_LIST]: {
storage: STORAGE_TIERS.LOCAL_STORAGE,
ttl: Infinity,
addressing: "kind:pubkey:d",
merge: MERGE_STRATEGIES.REPLACEABLE,
},
[NOTE_TYPES.ADMIN_BLACKLIST]: {
storage: STORAGE_TIERS.LOCAL_STORAGE,
ttl: Infinity,
addressing: "kind:pubkey:d",
merge: MERGE_STRATEGIES.REPLACEABLE,
},
[NOTE_TYPES.ADMIN_WHITELIST]: {
storage: STORAGE_TIERS.LOCAL_STORAGE,
ttl: Infinity,
addressing: "kind:pubkey:d",
merge: MERGE_STRATEGIES.REPLACEABLE,
},
[NOTE_TYPES.VIDEO_COMMENT]: {
storage: STORAGE_TIERS.MEMORY,
ttl: 0, // Session only
addressing: "id",
merge: MERGE_STRATEGIES.APPEND_ONLY,
},
[NOTE_TYPES.VIDEO_REACTION]: {
storage: STORAGE_TIERS.MEMORY,
ttl: 0,
addressing: "id",
merge: MERGE_STRATEGIES.APPEND_ONLY,
},
[NOTE_TYPES.VIEW_EVENT]: {
storage: STORAGE_TIERS.MEMORY,
ttl: 0,
addressing: "id",
merge: MERGE_STRATEGIES.APPEND_ONLY,
},
[NOTE_TYPES.HASHTAG_PREFERENCES]: {
storage: STORAGE_TIERS.LOCAL_STORAGE,
ttl: Infinity,
addressing: "kind:pubkey:d",
merge: MERGE_STRATEGIES.REPLACEABLE,
},
});
+4 -1
View File
@@ -60,6 +60,7 @@ import {
getNostrEventSchema, getNostrEventSchema,
NOTE_TYPES, NOTE_TYPES,
} from "../nostrEventSchemas.js"; } from "../nostrEventSchemas.js";
import { CACHE_POLICIES } from "./cachePolicies.js";
import { import {
listVideoViewEvents as listVideoViewEventsForClient, listVideoViewEvents as listVideoViewEventsForClient,
subscribeVideoViewEvents as subscribeVideoViewEventsForClient, subscribeVideoViewEvents as subscribeVideoViewEventsForClient,
@@ -314,7 +315,9 @@ function shouldRequestExtensionPermissions(signer) {
} }
const EVENTS_CACHE_STORAGE_KEY = "bitvid:eventsCache:v1"; const EVENTS_CACHE_STORAGE_KEY = "bitvid:eventsCache:v1";
const EVENTS_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes // We use the policy TTL, but currently the storage backend is hardcoded to IDB (with localStorage fallback).
// Future refactors should make EventsCacheStore dynamic based on CACHE_POLICIES[NOTE_TYPES.VIDEO_POST].storage.
const EVENTS_CACHE_TTL_MS = CACHE_POLICIES[NOTE_TYPES.VIDEO_POST]?.ttl ?? (10 * 60 * 1000);
const EVENTS_CACHE_DB_NAME = "bitvid-events-cache"; const EVENTS_CACHE_DB_NAME = "bitvid-events-cache";
const EVENTS_CACHE_DB_VERSION = 1; const EVENTS_CACHE_DB_VERSION = 1;
const EVENTS_CACHE_PERSIST_DELAY_MS = 450; const EVENTS_CACHE_PERSIST_DELAY_MS = 450;
+10 -2
View File
@@ -11,7 +11,9 @@ import {
} from "../config.js"; } from "../config.js";
import { import {
buildWatchHistoryEvent, buildWatchHistoryEvent,
NOTE_TYPES,
} from "../nostrEventSchemas.js"; } from "../nostrEventSchemas.js";
import { CACHE_POLICIES } from "./cachePolicies.js";
import { publishEventToRelays } from "../nostrPublish.js"; import { publishEventToRelays } from "../nostrPublish.js";
import { import {
RELAY_URLS, RELAY_URLS,
@@ -960,11 +962,17 @@ class WatchHistoryManager {
if (Number.isFinite(this.cacheTtlMs) && this.cacheTtlMs > 0) { if (Number.isFinite(this.cacheTtlMs) && this.cacheTtlMs > 0) {
return this.cacheTtlMs; return this.cacheTtlMs;
} }
const policyTtl = CACHE_POLICIES[NOTE_TYPES.WATCH_HISTORY]?.ttl;
const configured = Number(WATCH_HISTORY_CACHE_TTL_MS); const configured = Number(WATCH_HISTORY_CACHE_TTL_MS);
const resolved =
// Prefer config if set (for backward compat or overrides), otherwise policy
let resolved =
Number.isFinite(configured) && configured > 0 Number.isFinite(configured) && configured > 0
? Math.floor(configured) ? Math.floor(configured)
: 24 * 60 * 60 * 1000; : Number.isFinite(policyTtl) && policyTtl > 0
? policyTtl
: 24 * 60 * 60 * 1000;
this.cacheTtlMs = resolved; this.cacheTtlMs = resolved;
return resolved; return resolved;
} }
+45 -2
View File
@@ -5,7 +5,8 @@ import {
requestDefaultExtensionPermissions, requestDefaultExtensionPermissions,
} from "./nostrClientFacade.js"; } from "./nostrClientFacade.js";
import { getActiveSigner } from "./nostr/index.js"; import { getActiveSigner } from "./nostr/index.js";
import { buildRelayListEvent } from "./nostrEventSchemas.js"; import { buildRelayListEvent, NOTE_TYPES } from "./nostrEventSchemas.js";
import { CACHE_POLICIES, STORAGE_TIERS } from "./nostr/cachePolicies.js";
import { devLogger, userLogger } from "./utils/logger.js"; import { devLogger, userLogger } from "./utils/logger.js";
import { import {
publishEventToRelays, publishEventToRelays,
@@ -17,6 +18,7 @@ const MODE_SEQUENCE = ["both", "read", "write"];
const FAST_RELAY_FETCH_LIMIT = 3; const FAST_RELAY_FETCH_LIMIT = 3;
const FAST_RELAY_TIMEOUT_MS = 2500; const FAST_RELAY_TIMEOUT_MS = 2500;
const BACKGROUND_RELAY_TIMEOUT_MS = 6000; const BACKGROUND_RELAY_TIMEOUT_MS = 6000;
const RELAY_PREFS_STORAGE_KEY = "bitvid:relay-preferences:v1";
function normalizeHexPubkey(pubkey) { function normalizeHexPubkey(pubkey) {
if (typeof pubkey !== "string") { if (typeof pubkey !== "string") {
@@ -191,7 +193,47 @@ class RelayPreferencesManager {
const normalized = normalizeRelayUrl(url) || url; const normalized = normalizeRelayUrl(url) || url;
return createEntry(normalized, "both"); return createEntry(normalized, "both");
}); });
this.setEntries(this.defaultEntries, { allowEmpty: false, updateClient: true });
// Attempt to load from storage if policy allows
const loaded = this.loadFromStorage();
if (loaded && loaded.length) {
this.setEntries(loaded, { allowEmpty: false, updateClient: true });
} else {
this.setEntries(this.defaultEntries, { allowEmpty: false, updateClient: true });
}
}
loadFromStorage() {
const policy = CACHE_POLICIES[NOTE_TYPES.RELAY_LIST];
if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) {
return null;
}
if (typeof localStorage === "undefined") return null;
try {
const raw = localStorage.getItem(RELAY_PREFS_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
return parsed;
}
}
} catch (err) {
devLogger.warn("[RelayPreferencesManager] Failed to load from storage", err);
}
return null;
}
saveToStorage() {
const policy = CACHE_POLICIES[NOTE_TYPES.RELAY_LIST];
if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) {
return;
}
if (typeof localStorage === "undefined") return;
try {
localStorage.setItem(RELAY_PREFS_STORAGE_KEY, serializeEntries(this.entries));
} catch (err) {
devLogger.warn("[RelayPreferencesManager] Failed to save to storage", err);
}
} }
snapshot() { snapshot() {
@@ -279,6 +321,7 @@ class RelayPreferencesManager {
this.syncClient(); this.syncClient();
} }
this.saveToStorage();
return this.getEntries(); return this.getEntries();
} }
+9
View File
@@ -18,6 +18,7 @@ import {
getNostrEventSchema, getNostrEventSchema,
NOTE_TYPES, NOTE_TYPES,
} from "./nostrEventSchemas.js"; } from "./nostrEventSchemas.js";
import { CACHE_POLICIES, STORAGE_TIERS } from "./nostr/cachePolicies.js";
import { getSidebarLoadingMarkup } from "./sidebarLoading.js"; import { getSidebarLoadingMarkup } from "./sidebarLoading.js";
import { import {
publishEventToRelays, publishEventToRelays,
@@ -277,6 +278,10 @@ class SubscriptionsManager {
} }
loadFromCache(userPubkey) { loadFromCache(userPubkey) {
const policy = CACHE_POLICIES[NOTE_TYPES.SUBSCRIPTION_LIST];
if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) {
return null;
}
if (typeof localStorage === "undefined") return null; if (typeof localStorage === "undefined") return null;
const key = this.getCacheKey(userPubkey); const key = this.getCacheKey(userPubkey);
if (!key) return null; if (!key) return null;
@@ -294,6 +299,10 @@ class SubscriptionsManager {
} }
saveToCache(userPubkey) { saveToCache(userPubkey) {
const policy = CACHE_POLICIES[NOTE_TYPES.SUBSCRIPTION_LIST];
if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) {
return;
}
if (typeof localStorage === "undefined") return; if (typeof localStorage === "undefined") return;
const key = this.getCacheKey(userPubkey); const key = this.getCacheKey(userPubkey);
if (!key) return; if (!key) return;
+10 -1
View File
@@ -4,7 +4,8 @@ import {
requestDefaultExtensionPermissions, requestDefaultExtensionPermissions,
} from "./nostrClientFacade.js"; } from "./nostrClientFacade.js";
import { getActiveSigner } from "./nostr/index.js"; import { getActiveSigner } from "./nostr/index.js";
import { buildBlockListEvent, BLOCK_LIST_IDENTIFIER } from "./nostrEventSchemas.js"; import { buildBlockListEvent, BLOCK_LIST_IDENTIFIER, NOTE_TYPES } from "./nostrEventSchemas.js";
import { CACHE_POLICIES, STORAGE_TIERS } from "./nostr/cachePolicies.js";
import { devLogger, userLogger } from "./utils/logger.js"; import { devLogger, userLogger } from "./utils/logger.js";
import { import {
publishEventToRelays, publishEventToRelays,
@@ -1448,10 +1449,18 @@ class UserBlockListManager {
} }
_loadLocal(actorHex) { _loadLocal(actorHex) {
const policy = CACHE_POLICIES[NOTE_TYPES.USER_BLOCK_LIST];
if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) {
return null;
}
return readLocalBlocks(actorHex); return readLocalBlocks(actorHex);
} }
_saveLocal(actorHex) { _saveLocal(actorHex) {
const policy = CACHE_POLICIES[NOTE_TYPES.USER_BLOCK_LIST];
if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) {
return;
}
writeLocalBlocks(actorHex, this.blockedPubkeys); writeLocalBlocks(actorHex, this.blockedPubkeys);
} }
+39
View File
@@ -0,0 +1,39 @@
import { test } from 'node:test';
import assert from 'node:assert';
import { CACHE_POLICIES, STORAGE_TIERS, MERGE_STRATEGIES } from '../../js/nostr/cachePolicies.js';
import { NOTE_TYPES } from '../../js/nostrEventSchemas.js';
test('CACHE_POLICIES structure', () => {
assert.ok(CACHE_POLICIES, 'CACHE_POLICIES should be defined');
assert.ok(STORAGE_TIERS, 'STORAGE_TIERS should be defined');
assert.ok(MERGE_STRATEGIES, 'MERGE_STRATEGIES should be defined');
});
test('VIDEO_POST policy', () => {
const policy = CACHE_POLICIES[NOTE_TYPES.VIDEO_POST];
assert.ok(policy, 'VIDEO_POST policy should exist');
assert.strictEqual(policy.storage, STORAGE_TIERS.INDEXED_DB);
assert.strictEqual(policy.ttl, 10 * 60 * 1000);
assert.strictEqual(policy.merge, MERGE_STRATEGIES.REPLACEABLE);
});
test('WATCH_HISTORY policy', () => {
const policy = CACHE_POLICIES[NOTE_TYPES.WATCH_HISTORY];
assert.ok(policy, 'WATCH_HISTORY policy should exist');
assert.strictEqual(policy.storage, STORAGE_TIERS.LOCAL_STORAGE);
assert.strictEqual(policy.ttl, 24 * 60 * 60 * 1000);
assert.strictEqual(policy.merge, MERGE_STRATEGIES.APPEND_ONLY);
});
test('SUBSCRIPTION_LIST policy', () => {
const policy = CACHE_POLICIES[NOTE_TYPES.SUBSCRIPTION_LIST];
assert.ok(policy, 'SUBSCRIPTION_LIST policy should exist');
assert.strictEqual(policy.storage, STORAGE_TIERS.LOCAL_STORAGE);
assert.strictEqual(policy.ttl, Infinity);
});
test('VIDEO_COMMENT policy', () => {
const policy = CACHE_POLICIES[NOTE_TYPES.VIDEO_COMMENT];
assert.ok(policy, 'VIDEO_COMMENT policy should exist');
assert.strictEqual(policy.storage, STORAGE_TIERS.MEMORY);
});