mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 08:22:24 +00:00
- 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`.
227 lines
7.5 KiB
JavaScript
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();
|