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 {
buildAdminListEvent,
ADMIN_LIST_IDENTIFIERS,
NOTE_TYPES,
} from "./nostrEventSchemas.js";
import { CACHE_POLICIES, STORAGE_TIERS } from "./nostr/cachePolicies.js";
import { publishEventToRelay } from "./nostrPublish.js";
const ADMIN_STATE_CACHE_VERSION = 1;
@@ -231,6 +233,11 @@ function hasAnyEntries(state = {}) {
}
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") {
return null;
}
@@ -263,6 +270,11 @@ export const readCachedAdminState = () => {
};
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") {
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,
NOTE_TYPES,
} from "../nostrEventSchemas.js";
import { CACHE_POLICIES } from "./cachePolicies.js";
import {
listVideoViewEvents as listVideoViewEventsForClient,
subscribeVideoViewEvents as subscribeVideoViewEventsForClient,
@@ -314,7 +315,9 @@ function shouldRequestExtensionPermissions(signer) {
}
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_VERSION = 1;
const EVENTS_CACHE_PERSIST_DELAY_MS = 450;
+10 -2
View File
@@ -11,7 +11,9 @@ import {
} from "../config.js";
import {
buildWatchHistoryEvent,
NOTE_TYPES,
} from "../nostrEventSchemas.js";
import { CACHE_POLICIES } from "./cachePolicies.js";
import { publishEventToRelays } from "../nostrPublish.js";
import {
RELAY_URLS,
@@ -960,11 +962,17 @@ class WatchHistoryManager {
if (Number.isFinite(this.cacheTtlMs) && this.cacheTtlMs > 0) {
return this.cacheTtlMs;
}
const policyTtl = CACHE_POLICIES[NOTE_TYPES.WATCH_HISTORY]?.ttl;
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
? Math.floor(configured)
: 24 * 60 * 60 * 1000;
: Number.isFinite(policyTtl) && policyTtl > 0
? policyTtl
: 24 * 60 * 60 * 1000;
this.cacheTtlMs = resolved;
return resolved;
}
+45 -2
View File
@@ -5,7 +5,8 @@ import {
requestDefaultExtensionPermissions,
} from "./nostrClientFacade.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 {
publishEventToRelays,
@@ -17,6 +18,7 @@ const MODE_SEQUENCE = ["both", "read", "write"];
const FAST_RELAY_FETCH_LIMIT = 3;
const FAST_RELAY_TIMEOUT_MS = 2500;
const BACKGROUND_RELAY_TIMEOUT_MS = 6000;
const RELAY_PREFS_STORAGE_KEY = "bitvid:relay-preferences:v1";
function normalizeHexPubkey(pubkey) {
if (typeof pubkey !== "string") {
@@ -191,7 +193,47 @@ class RelayPreferencesManager {
const normalized = normalizeRelayUrl(url) || url;
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() {
@@ -279,6 +321,7 @@ class RelayPreferencesManager {
this.syncClient();
}
this.saveToStorage();
return this.getEntries();
}
+9
View File
@@ -18,6 +18,7 @@ import {
getNostrEventSchema,
NOTE_TYPES,
} from "./nostrEventSchemas.js";
import { CACHE_POLICIES, STORAGE_TIERS } from "./nostr/cachePolicies.js";
import { getSidebarLoadingMarkup } from "./sidebarLoading.js";
import {
publishEventToRelays,
@@ -277,6 +278,10 @@ class SubscriptionsManager {
}
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;
const key = this.getCacheKey(userPubkey);
if (!key) return null;
@@ -294,6 +299,10 @@ class SubscriptionsManager {
}
saveToCache(userPubkey) {
const policy = CACHE_POLICIES[NOTE_TYPES.SUBSCRIPTION_LIST];
if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) {
return;
}
if (typeof localStorage === "undefined") return;
const key = this.getCacheKey(userPubkey);
if (!key) return;
+10 -1
View File
@@ -4,7 +4,8 @@ import {
requestDefaultExtensionPermissions,
} from "./nostrClientFacade.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 {
publishEventToRelays,
@@ -1448,10 +1449,18 @@ class UserBlockListManager {
}
_loadLocal(actorHex) {
const policy = CACHE_POLICIES[NOTE_TYPES.USER_BLOCK_LIST];
if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) {
return null;
}
return readLocalBlocks(actorHex);
}
_saveLocal(actorHex) {
const policy = CACHE_POLICIES[NOTE_TYPES.USER_BLOCK_LIST];
if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) {
return;
}
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);
});