diff --git a/benchmarks/watch_history_publish_bench.js b/benchmarks/watch_history_publish_bench.js new file mode 100644 index 00000000..ab562bb2 --- /dev/null +++ b/benchmarks/watch_history_publish_bench.js @@ -0,0 +1,69 @@ + +import { createWatchHistoryManager } from "../js/nostr/watchHistory.js"; + +async function main() { + console.log("Starting WatchHistoryManager benchmark..."); + + // Mock dependencies + const mockSigner = { + signEvent: async (event) => { + // Simulate signing latency + await new Promise(resolve => setTimeout(resolve, 10)); + return { ...event, sig: "mock-sig" }; + }, + nip04Encrypt: async (pubkey, plaintext) => { + await new Promise(resolve => setTimeout(resolve, 5)); + return "mock-ciphertext"; + } + }; + + const mockPool = { + publish: (relays, event) => { + return { + on: (status, cb) => { + if (status === 'ok') { + // Simulate network latency + setTimeout(cb, 50); + } + } + } + } + }; + + const deps = { + ensureNostrTools: async () => ({ + nip04: { encrypt: async () => "encrypted" }, + utils: { hexToBytes: () => new Uint8Array(32) } + }), + getCachedNostrTools: () => ({ + nip04: { encrypt: async () => "encrypted" }, + utils: { hexToBytes: () => new Uint8Array(32) } + }), + getActivePubkey: () => "00".repeat(32), + getSessionActor: () => null, + resolveActiveSigner: () => mockSigner, + getPool: () => mockPool, + signEventWithPrivateKey: async () => { + await new Promise(resolve => setTimeout(resolve, 10)); + return { sig: "mock-sig" }; + }, + shouldRequestExtensionPermissions: () => false, + }; + + const manager = createWatchHistoryManager(deps); + + // Create records for 12 months + const records = {}; + for (let i = 1; i <= 12; i++) { + const month = `2023-${String(i).padStart(2, '0')}`; + records[month] = [{ type: 'e', value: `item-${i}`, watchedAt: 1672531200 + i * 100 }]; + } + + const start = Date.now(); + await manager.publishRecords(records, { actorPubkey: "00".repeat(32) }); + const duration = Date.now() - start; + + console.log(`Published 12 months in ${duration}ms`); +} + +main().catch(console.error); diff --git a/js/nostr/watchHistory.js b/js/nostr/watchHistory.js index 38926ddb..cbae8899 100644 --- a/js/nostr/watchHistory.js +++ b/js/nostr/watchHistory.js @@ -1908,30 +1908,32 @@ class WatchHistoryManager { const chunkEvents = Array.isArray(results) ? results.flat().filter((event) => event && typeof event === "object") : []; - const decryptionPromises = chunkEvents.map(async (event) => { - const ciphertext = typeof event.content === "string" ? event.content : ""; - if (!ciphertext) { - return []; - } - let plaintext = ""; - if (typeof decryptSigner.nip04Decrypt === "function") { - try { - plaintext = await decryptSigner.nip04Decrypt(actorKey, ciphertext); - } catch (error) { - devLogger.warn("[nostr] Failed to decrypt watch history chunk:", error); + const chunkResults = await pMap( + chunkEvents, + async (event) => { + const ciphertext = typeof event.content === "string" ? event.content : ""; + if (!ciphertext) { + return []; + } + let plaintext = ""; + if (typeof decryptSigner.nip04Decrypt === "function") { + try { + plaintext = await decryptSigner.nip04Decrypt(actorKey, ciphertext); + } catch (error) { + devLogger.warn("[nostr] Failed to decrypt watch history chunk:", error); + } + } + if (!plaintext) { + return []; + } + const parsed = parseWatchHistoryPayload(plaintext); + if (Array.isArray(parsed.items) && parsed.items.length) { + return parsed.items; } - } - if (!plaintext) { return []; - } - const parsed = parseWatchHistoryPayload(plaintext); - if (Array.isArray(parsed.items) && parsed.items.length) { - return parsed.items; - } - return []; - }); - - const chunkResults = await Promise.all(decryptionPromises); + }, + { concurrency: 5 }, + ); decryptedItems.push(...chunkResults.flat()); } catch (error) { devLogger.warn("[nostr] Failed to fetch watch history chunks:", error); diff --git a/js/state/profileCache.js b/js/state/profileCache.js index b046332d..4e0bc561 100644 --- a/js/state/profileCache.js +++ b/js/state/profileCache.js @@ -367,8 +367,12 @@ class ProfileCache { return userCache ? userCache.get(section) : undefined; } + /** + * Clears all cached data for a specific public key from memory. + * This operation is O(1) thanks to the nested map structure. + * @param {string} pubkey - The hex public key to clear. + */ clearMemoryCache(pubkey) { - // Optimized: O(1) deletion due to nested map structure this.memoryCache.delete(pubkey); }