Files
bitvid/js/state/profileCache.js
T
google-labs-jules[bot] 3deb8d08f0 Implement profile-scoped caching and signer invalidation logic
- Updated `profileCache.js` to support clearing runtime memory on signer change.
- Updated `NostrClient` to trigger `clearSignerRuntime` on `setActiveSigner`.
- Updated list services (`subscriptions`, `userBlocks`, `hashtagPreferences`, `watchHistory`) to listen for `runtimeCleared` and reload data.
- Updated `NostrClient` sync metadata methods to default to the active profile pubkey.
- Added `partition-updated` event emission to `profileCache`.
2026-01-17 21:43:35 +00:00

227 lines
7.5 KiB
JavaScript

import { CACHE_POLICIES, STORAGE_TIERS } from "../nostr/cachePolicies.js";
import { userLogger } from "../utils/logger.js";
const PROFILE_CACHE_VERSION = 1;
const STORAGE_KEY_PREFIX = "bitvid:profile:";
class ProfileCache {
constructor() {
this.activePubkey = null;
this.memoryCache = new Map(); // For runtime decrypted data: pubkey:section -> data
this.listeners = new Set();
}
normalizeHexPubkey(pubkey) {
if (typeof pubkey !== "string") {
return null;
}
const trimmed = pubkey.trim();
if (!trimmed) {
return null;
}
// Simple hex check, relying on callers to provide valid hex mostly,
// but robust enough to handle basic whitespace.
// For full validation we'd need nostr-tools or similar regex.
// Assuming standard 64-char hex for now.
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
return trimmed.toLowerCase();
}
return null;
}
getStorageKey(pubkey, section) {
return `${STORAGE_KEY_PREFIX}${pubkey}:${section}:v${PROFILE_CACHE_VERSION}`;
}
getStorageTier(section) {
// We assume section maps to a NOTE_TYPE or we map it manually.
// For now, let's map known sections to cache policies.
// If unknown, default to LOCAL_STORAGE for safety or MEMORY if ephemeral.
// Mapping section names used in services to CACHE_POLICIES keys
// subscriptions -> NOTE_TYPES.SUBSCRIPTION_LIST (30000)
// blocks -> NOTE_TYPES.USER_BLOCK_LIST (30002/10000)
// interests -> NOTE_TYPES.HASHTAG_PREFERENCES (30015/30005)
// watchHistory -> NOTE_TYPES.WATCH_HISTORY (30078)
// relays -> NOTE_TYPES.RELAY_LIST (10002)
// We'll rely on the caller passing the correct section key that matches CACHE_POLICIES logic
// OR we define a map here.
// Let's use simple string keys for sections and map them here.
const policyMap = {
"subscriptions": "subscription_list", // maps to NOTE_TYPES.SUBSCRIPTION_LIST
"blocks": "user_block_list",
"interests": "hashtag_preferences",
"watchHistory": "watch_history",
"relays": "relay_list",
// Add others as needed
};
// Note: CACHE_POLICIES keys are the note types (strings or numbers).
// In `js/nostr/cachePolicies.js`:
// [NOTE_TYPES.SUBSCRIPTION_LIST]: ...
// We need to know what NOTE_TYPES.SUBSCRIPTION_LIST evaluates to.
// However, we can just look up by the policy object directly if we import NOTE_TYPES.
// But to avoid circular deps or heavy imports, we can just check the storage tier directly if provided via config
// or assume LOCAL_STORAGE for these user lists as per requirements.
// For now, hardcode LOCAL_STORAGE for the known persistent types, as per the plan's context.
return STORAGE_TIERS.LOCAL_STORAGE;
}
setActiveProfile(pubkey) {
const normalized = this.normalizeHexPubkey(pubkey);
if (this.activePubkey === normalized) {
return;
}
// Clear memory cache for the old profile to remove sensitive decrypted data
if (this.activePubkey) {
this.clearMemoryCache(this.activePubkey);
}
this.activePubkey = normalized;
// Emit change event
this.emit("profileChanged", { pubkey: this.activePubkey });
}
getActiveProfile() {
return this.activePubkey;
}
get(section) {
if (!this.activePubkey) {
return null;
}
return this.getProfileData(this.activePubkey, section);
}
set(section, data) {
if (!this.activePubkey) {
return;
}
this.setProfileData(this.activePubkey, section, data);
}
getProfileData(pubkey, section) {
// 1. Check memory cache (runtime data)
const memKey = `${pubkey}:${section}`;
if (this.memoryCache.has(memKey)) {
return this.memoryCache.get(memKey);
}
// 2. Load from persistence (canonical/encrypted data)
// Note: 'get' is often used for the *decrypted* runtime state in services.
// But services need to load the *persisted* data to decrypt it.
// The separation of concerns:
// - profileCache stores the *persisted* blob (encrypted or raw).
// - Services decrypt it and store the *result* back in profileCache's memory tier?
// OR services keep their own runtime state?
// The plan says: "Keep decrypted/plaintext and signer-dependent runtime caches **in memory**"
// "Services remain logic + event emitters that read/write through the partition".
// So:
// `profileCache.get(section)` should probably return the *persisted* data.
// Services maintain their own runtime state derived from it, OR we allow storing runtime state in `profileCache` too?
// If `profileCache` is just a storage wrapper, `get` returns what's in localStorage.
// Let's implement `load` from storage.
const storageKey = this.getStorageKey(pubkey, section);
const tier = this.getStorageTier(section);
if (tier === STORAGE_TIERS.LOCAL_STORAGE && typeof localStorage !== "undefined") {
try {
const raw = localStorage.getItem(storageKey);
if (raw) {
return JSON.parse(raw);
}
} catch (error) {
userLogger.warn(`[ProfileCache] Failed to load ${section} for ${pubkey}`, error);
}
}
return null;
}
setProfileData(pubkey, section, data) {
const storageKey = this.getStorageKey(pubkey, section);
const tier = this.getStorageTier(section);
// Save to storage
if (tier === STORAGE_TIERS.LOCAL_STORAGE && typeof localStorage !== "undefined") {
try {
if (data === null || data === undefined) {
localStorage.removeItem(storageKey);
} else {
localStorage.setItem(storageKey, JSON.stringify(data));
}
} catch (error) {
userLogger.warn(`[ProfileCache] Failed to save ${section} for ${pubkey}`, error);
}
}
// Update memory cache?
// If `get` reads from storage, `set` writes to storage.
// We can also cache in memory to avoid parsing JSON every time if we want.
// But if services manage runtime state, `profileCache` acts as the persistence layer.
this.emit("update", { pubkey, section, data });
this.emit("partition-updated", { pubkey, key: section });
}
// Runtime memory cache methods (for decrypted data, if services want to use it)
// Or maybe services just use `get` and `set` for persistence?
// The plan says: "Keep decrypted/plaintext... in memory keyed by pubkey... and clear them on signer change."
// This implies `profileCache` SHOULD manage the memory cache too.
setMemoryData(section, data) {
if (!this.activePubkey) return;
const key = `${this.activePubkey}:${section}`;
this.memoryCache.set(key, data);
}
getMemoryData(section) {
if (!this.activePubkey) return null;
const key = `${this.activePubkey}:${section}`;
return this.memoryCache.get(key);
}
clearMemoryCache(pubkey) {
// Clear all entries starting with pubkey:
const prefix = `${pubkey}:`;
for (const key of this.memoryCache.keys()) {
if (key.startsWith(prefix)) {
this.memoryCache.delete(key);
}
}
}
clearSignerRuntime(pubkey) {
const normalized = this.normalizeHexPubkey(pubkey);
if (!normalized) {
return;
}
this.clearMemoryCache(normalized);
this.emit("runtimeCleared", { pubkey: normalized });
}
subscribe(callback) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
emit(event, detail) {
for (const listener of this.listeners) {
try {
listener(event, detail);
} catch (error) {
console.error("[ProfileCache] Listener error", error);
}
}
}
}
export const profileCache = new ProfileCache();