refactor: improve NIP-29 adapter metadata fetching

- Check eventStore first before making network request for kind 39000
- Use extractMetadataFromEvent for sync extraction when event is cached
- Only fetch from relay if not already in eventStore
- Fall back to async resolveGroupMetadata for profile-based groups

This improves performance when opening a chat that was previously
loaded, as the metadata is already in the eventStore cache.

https://claude.ai/code/session_01CCxAcUsRBkWSL6as1wtFoA
This commit is contained in:
Claude
2026-01-30 10:18:27 +00:00
parent 6b747f5aef
commit 9c9e5ab37d

View File

@@ -28,7 +28,10 @@ import {
GroupMessageBlueprint,
ReactionBlueprint,
} from "applesauce-common/blueprints";
import { resolveGroupMetadata } from "@/lib/chat/group-metadata-helpers";
import {
resolveGroupMetadata,
extractMetadataFromEvent,
} from "@/lib/chat/group-metadata-helpers";
/**
* NIP-29 Adapter - Relay-Based Groups
@@ -130,72 +133,90 @@ export class Nip29Adapter extends ChatProtocolAdapter {
}
console.log(
`[NIP-29] Fetching group metadata for ${groupId} from ${relayUrl}`,
`[NIP-29] Resolving group metadata for ${groupId} from ${relayUrl}`,
);
// Fetch group metadata from the specific relay (kind 39000)
const metadataFilter: Filter = {
kinds: [39000],
"#d": [groupId],
limit: 1,
};
// Check eventStore first for cached kind 39000 event
// Use timeline query since we don't know the relay's pubkey
const cachedMetadata = await firstValueFrom(
eventStore
.timeline([{ kinds: [39000], "#d": [groupId], limit: 1 }])
.pipe(first()),
{ defaultValue: [] },
);
let metadataEvent: NostrEvent | undefined = cachedMetadata[0];
// Use pool.subscription to fetch from the relay
const metadataEvents: NostrEvent[] = [];
const metadataObs = pool.subscription([relayUrl], [metadataFilter], {
eventStore, // Automatically add to store
});
// If not in store, fetch from relay
if (!metadataEvent) {
console.log(`[NIP-29] No cached metadata, fetching from relay`);
// Subscribe and wait for EOSE
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
console.log("[NIP-29] Metadata fetch timeout");
resolve();
}, 5000);
const metadataFilter: Filter = {
kinds: [39000],
"#d": [groupId],
limit: 1,
};
const sub = metadataObs.subscribe({
next: (response) => {
if (typeof response === "string") {
// EOSE received
clearTimeout(timeout);
console.log(
`[NIP-29] Got ${metadataEvents.length} metadata events`,
);
sub.unsubscribe();
resolve();
} else {
// Event received
metadataEvents.push(response);
}
},
error: (err) => {
clearTimeout(timeout);
console.error("[NIP-29] Metadata fetch error:", err);
sub.unsubscribe();
reject(err);
},
const metadataEvents: NostrEvent[] = [];
const metadataObs = pool.subscription([relayUrl], [metadataFilter], {
eventStore,
});
});
const metadataEvent = metadataEvents[0];
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
console.log("[NIP-29] Metadata fetch timeout");
resolve();
}, 5000);
// Debug: Log metadata event tags
if (metadataEvent) {
console.log(`[NIP-29] Metadata event tags:`, metadataEvent.tags);
const sub = metadataObs.subscribe({
next: (response) => {
if (typeof response === "string") {
clearTimeout(timeout);
console.log(
`[NIP-29] Got ${metadataEvents.length} metadata events`,
);
sub.unsubscribe();
resolve();
} else {
metadataEvents.push(response);
}
},
error: (err) => {
clearTimeout(timeout);
console.error("[NIP-29] Metadata fetch error:", err);
sub.unsubscribe();
reject(err);
},
});
});
metadataEvent = metadataEvents[0];
} else {
console.log(`[NIP-29] Using cached metadata event`);
}
// Resolve group metadata with profile fallback
const resolved = await resolveGroupMetadata(
groupId,
relayUrl,
metadataEvent,
);
// Extract metadata - use sync extraction if we have the event
let title: string;
let description: string | undefined;
let icon: string | undefined;
let source: string;
const title = resolved.name || groupId;
const description = resolved.description;
const icon = resolved.icon;
if (metadataEvent && metadataEvent.kind === 39000) {
// Fast path: sync extraction from kind 39000 event
const resolved = extractMetadataFromEvent(groupId, metadataEvent);
title = resolved.name || groupId;
description = resolved.description;
icon = resolved.icon;
source = resolved.source;
} else {
// Slow path: async resolution with profile fallback (for pubkey-based groupIds)
const resolved = await resolveGroupMetadata(groupId, relayUrl, undefined);
title = resolved.name || groupId;
description = resolved.description;
icon = resolved.icon;
source = resolved.source;
}
console.log(`[NIP-29] Group title: ${title} (source: ${resolved.source})`);
console.log(`[NIP-29] Group title: ${title} (source: ${source})`);
// Fetch admins (kind 39001) and members (kind 39002) in parallel
// Both use d tag (addressable events signed by relay)