Optimize channel profile event lookup with author index

- Replaced `NostrClient.rawEvents` standard Map with `EventsMap`
- Implemented `EventsMap` to maintain an author-based index
- Updated `js/channelProfile.js` to use `getEventsByAuthor` for efficient lookup
- Added unit tests for `EventsMap`
- Added benchmark script verifying ~15x performance improvement (637ms -> 41ms for 1M events)

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-08 00:38:42 +00:00
1 parent 566c2dbf89
commit 67aa8ad2aa
5 files changed
+282 -5

No files matched your search

+17 -4
View File
@@ -4366,10 +4366,23 @@ function getCachedChannelVideoEvents(pubkey) {
};
const rawMatches = collectMatches(nostrClient?.rawEvents, (callback) => {
if (
nostrClient?.rawEvents &&
typeof nostrClient.rawEvents.forEach === "function"
) {
if (!nostrClient?.rawEvents) {
return;
}
if (typeof nostrClient.rawEvents.getEventsByAuthor === "function") {
const candidates = nostrClient.rawEvents.getEventsByAuthor(normalized);
if (Array.isArray(candidates)) {
candidates.forEach((event) => {
if (event?.kind === 30078) {
callback(event);
}
});
return;
}
}
if (typeof nostrClient.rawEvents.forEach === "function") {
nostrClient.rawEvents.forEach((event) => {
if (event?.kind === 30078) {
callback(event);
+2 -1
View File
@@ -203,6 +203,7 @@ import { createNip07Adapter } from "./adapters/nip07Adapter.js";
import { createNsecAdapter } from "./adapters/nsecAdapter.js";
import { createNip46Adapter } from "./adapters/nip46Adapter.js";
import { queueSignEvent } from "./signRequestQueue.js";
import { EventsMap } from "./eventsMap.js";
function normalizeProfileFromEvent(event) {
if (!event || !event.content) return null;
@@ -1271,7 +1272,7 @@ export class NostrClient {
* Kept for signature verification and republishing (e.g. NIP-94 mirror).
* @public
*/
this.rawEvents = new Map();
this.rawEvents = new EventsMap();
/**
* @type {Map<string, object>}
+90
View File
@@ -0,0 +1,90 @@
/**
* A Map extension that maintains an author-based index for fast lookups.
* Used for `rawEvents` in NostrClient.
*/
export class EventsMap extends Map {
constructor(entries) {
super(entries);
this.authorIndex = new Map(); // pubkey -> Set<Event>
// If entries provided, index them
if (entries) {
for (const [key, value] of this.entries()) {
this._addToIndex(value);
}
}
}
_normalizePubkey(pubkey) {
if (typeof pubkey !== "string") return "";
return pubkey.trim().toLowerCase();
}
_addToIndex(event) {
if (!event || typeof event !== "object") return;
const pubkey = this._normalizePubkey(event.pubkey);
if (!pubkey) return;
let set = this.authorIndex.get(pubkey);
if (!set) {
set = new Set();
this.authorIndex.set(pubkey, set);
}
set.add(event);
}
_removeFromIndex(event) {
if (!event || typeof event !== "object") return;
const pubkey = this._normalizePubkey(event.pubkey);
if (!pubkey) return;
const set = this.authorIndex.get(pubkey);
if (set) {
set.delete(event);
if (set.size === 0) {
this.authorIndex.delete(pubkey);
}
}
}
set(key, value) {
const existing = this.get(key);
// If the value is identical, we still set it in the map,
// but the index doesn't strictly need updating.
// However, if the key was mapped to a DIFFERENT object with the SAME pubkey,
// we must update the Set (remove old object, add new object).
if (existing) {
if (existing === value) {
// No index change needed
return super.set(key, value);
}
this._removeFromIndex(existing);
}
super.set(key, value);
this._addToIndex(value);
return this;
}
delete(key) {
const value = this.get(key);
const result = super.delete(key);
if (result && value) {
this._removeFromIndex(value);
}
return result;
}
clear() {
super.clear();
this.authorIndex.clear();
}
getEventsByAuthor(pubkey) {
const normalized = this._normalizePubkey(pubkey);
if (!normalized) return [];
const set = this.authorIndex.get(normalized);
// Return array to allow iteration
return set ? Array.from(set) : [];
}
}