mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 12:52:26 +00:00
perf(dm): coalesce concurrent decrypts of the same DM event
Multiple subscribers (history list + notifications) decrypt the same DM event at once; before the in-memory cache fills, each hit the serialized nip-07 extension, multiplying round-trips per message. Cache the in-flight decrypt promise by event id so concurrent callers share one decrypt. Harness: cold-load extension decrypt calls drop from ~50-60 to exactly 20 for 20 DMs (1 per message); warm reload stays 0 via the persisted cache. Combined with the earlier ordering/sequential and persist-cache fixes, legacy DM decryption went from ~241 extension calls (20 DMs) to 20 cold / 0 warm. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
8171fcb347
commit
39aa81fb23
1 file changed
+46
-23
+46
-23
@@ -577,6 +577,8 @@ export class NostrClient {
|
||||
},
|
||||
});
|
||||
this.dmDecryptCache = new LRUCache({ maxSize: DM_DECRYPT_CACHE_LIMIT });
|
||||
// event id -> in-flight decrypt promise (coalesces concurrent decrypts).
|
||||
this.dmDecryptInFlight = new Map();
|
||||
this.dmDecryptor = null;
|
||||
this.dmDecryptorPromise = null;
|
||||
this.isInitialized = false;
|
||||
@@ -1373,32 +1375,53 @@ export class NostrClient {
|
||||
// fall through to a fresh decrypt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const decryptDM = await this.ensureDmDecryptor();
|
||||
const context = await this.buildDmDecryptContext(actorPubkey);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await decryptDM(event, context);
|
||||
} catch (error) {
|
||||
devLogger.warn("[nostr] DM decryptor threw unexpectedly.", {
|
||||
error: sanitizeDecryptError(error),
|
||||
event: summarizeDmEventForLog(event),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (result?.ok && eventId) {
|
||||
this.dmDecryptCache.set(eventId, result);
|
||||
try {
|
||||
dmPlaintextCache.set(eventId, JSON.stringify(result));
|
||||
} catch (_) {
|
||||
// result not serializable — skip persisting
|
||||
// Coalesce concurrent decrypts of the SAME event. Multiple subscribers
|
||||
// (history list, notifications) often process an event at once; without
|
||||
// this they each hit the (serialized, slow) extension before the cache
|
||||
// fills, multiplying extension round-trips per message.
|
||||
const inFlight = this.dmDecryptInFlight.get(eventId);
|
||||
if (inFlight) {
|
||||
return inFlight;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
const decryptPromise = (async () => {
|
||||
const decryptDM = await this.ensureDmDecryptor();
|
||||
const context = await this.buildDmDecryptContext(actorPubkey);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await decryptDM(event, context);
|
||||
} catch (error) {
|
||||
devLogger.warn("[nostr] DM decryptor threw unexpectedly.", {
|
||||
error: sanitizeDecryptError(error),
|
||||
event: summarizeDmEventForLog(event),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (result?.ok && eventId) {
|
||||
this.dmDecryptCache.set(eventId, result);
|
||||
try {
|
||||
dmPlaintextCache.set(eventId, JSON.stringify(result));
|
||||
} catch (_) {
|
||||
// result not serializable — skip persisting
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
})();
|
||||
|
||||
if (eventId) {
|
||||
this.dmDecryptInFlight.set(eventId, decryptPromise);
|
||||
}
|
||||
try {
|
||||
return await decryptPromise;
|
||||
} finally {
|
||||
if (eventId) {
|
||||
this.dmDecryptInFlight.delete(eventId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user