refactor: simplify group name fix by removing IndexedDB caching

The eventStore already caches kind 39000 events, so the additional
IndexedDB layer adds complexity without significant benefit.

Simplified approach:
1. Sync extraction: Extract names directly from groupMetadataMap
   when kind 39000 events are already in eventStore (instant)
2. Async only when needed: Profile fallback runs only for groups
   where groupId is a pubkey but no NIP-29 metadata exists

Removed:
- CachedGroupMetadata interface and DB version 18
- loadCachedGroupMetadata, cacheGroupMetadata, cacheGroupMetadataBatch
- cacheLoaded state and cache loading effect

https://claude.ai/code/session_01CCxAcUsRBkWSL6as1wtFoA
This commit is contained in:
Claude
2026-01-30 09:24:33 +00:00
parent 9739b0f2c9
commit 6b747f5aef
3 changed files with 7 additions and 165 deletions

View File

@@ -18,8 +18,6 @@ import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import {
resolveGroupMetadata,
extractMetadataFromEvent,
loadCachedGroupMetadata,
cacheGroupMetadataBatch,
getGroupCacheKey,
isValidPubkey,
type ResolvedGroupMetadata,
@@ -346,42 +344,16 @@ export function GroupListViewer({ identifier }: GroupListViewerProps) {
);
}, [groups]);
// Resolve metadata with a multi-tier approach to prevent flicker:
// 1. Load from IndexedDB cache immediately (fastest, prevents flicker on subsequent visits)
// 2. Use sync extraction from kind 39000 events (fast, no async)
// 3. Only async-resolve for groups needing profile fallback (slow, only when needed)
// Resolve metadata with sync extraction + async-only-when-needed:
// 1. Sync extraction from kind 39000 events (fast, no async)
// 2. Only async-resolve for groups needing profile fallback (groupId is pubkey)
const [resolvedMetadataMap, setResolvedMetadataMap] = useState<
Map<string, ResolvedGroupMetadata>
>(new Map());
// Track if cache has been loaded to avoid flashing groupId before cache loads
const [cacheLoaded, setCacheLoaded] = useState(false);
// Step 1: Load from IndexedDB cache immediately on mount (fast path for returning users)
// Sync extraction from kind 39000 + async profile fallback only when needed
useEffect(() => {
if (groups.length === 0) {
setCacheLoaded(true);
return;
}
loadCachedGroupMetadata(groups).then((cached) => {
if (cached.size > 0) {
setResolvedMetadataMap((prev) => {
// Merge cached with existing (cached is baseline, existing may have fresher data)
const merged = new Map(cached);
for (const [key, value] of prev) {
merged.set(key, value);
}
return merged;
});
}
setCacheLoaded(true);
});
}, [groups]);
// Step 2 & 3: Sync extraction from kind 39000 + async profile fallback
useEffect(() => {
if (groups.length === 0 || !cacheLoaded) return;
if (groups.length === 0) return;
// Determine which groups need async resolution (no NIP-29 metadata, groupId is pubkey)
const needsAsyncResolution: Array<{ groupId: string; relayUrl: string }> =
@@ -417,7 +389,7 @@ export function GroupListViewer({ identifier }: GroupListViewerProps) {
// No NIP-29 metadata, not a pubkey - use groupId as name
updated.set(key, { name: group.groupId, source: "fallback" });
}
// For pubkey groups without metadata, keep existing cached value (don't overwrite)
// For pubkey groups without metadata, keep existing value (don't overwrite)
}
return updated;
@@ -456,39 +428,12 @@ export function GroupListViewer({ identifier }: GroupListViewerProps) {
}
return updated;
});
// Cache the resolved metadata for future visits
cacheGroupMetadataBatch(resolved);
}
};
resolveProfileMetadata();
}
// Cache sync-resolved NIP-29 metadata
const toCache: Array<{
relayUrl: string;
groupId: string;
metadata: ResolvedGroupMetadata;
}> = [];
for (const group of groups) {
if (group.groupId === "_") continue;
const metadataEvent = groupMetadataMap?.get(group.groupId);
if (metadataEvent && metadataEvent.kind === 39000) {
toCache.push({
relayUrl: group.relayUrl,
groupId: group.groupId,
metadata: extractMetadataFromEvent(group.groupId, metadataEvent),
});
}
}
if (toCache.length > 0) {
cacheGroupMetadataBatch(toCache);
}
}, [groups, groupMetadataMap, cacheLoaded]);
}, [groups, groupMetadataMap]);
// Subscribe to latest messages (kind 9) for all groups to get recency
// NOTE: Separate filters needed to ensure we get 1 message per group (not N total across all groups)

View File

@@ -2,7 +2,6 @@ import { firstValueFrom } from "rxjs";
import { kinds } from "nostr-tools";
import { profileLoader } from "@/services/loaders";
import { getProfileContent } from "applesauce-core/helpers";
import db, { type CachedGroupMetadata } from "@/services/db";
import type { NostrEvent } from "@/types/nostr";
/**
@@ -49,78 +48,6 @@ export function extractMetadataFromEvent(
};
}
/**
* Load cached group metadata from IndexedDB
* Returns a Map of cache key -> metadata for fast lookups
*/
export async function loadCachedGroupMetadata(
groups: Array<{ groupId: string; relayUrl: string }>,
): Promise<Map<string, ResolvedGroupMetadata>> {
const keys = groups.map((g) => getGroupCacheKey(g.relayUrl, g.groupId));
const cached = await db.groupMetadata.bulkGet(keys);
const map = new Map<string, ResolvedGroupMetadata>();
for (const entry of cached) {
if (entry) {
map.set(entry.key, {
name: entry.name,
description: entry.description,
icon: entry.icon,
source: entry.source,
});
}
}
return map;
}
/**
* Save resolved group metadata to IndexedDB cache
*/
export async function cacheGroupMetadata(
relayUrl: string,
groupId: string,
metadata: ResolvedGroupMetadata,
): Promise<void> {
const key = getGroupCacheKey(relayUrl, groupId);
const entry: CachedGroupMetadata = {
key,
groupId,
relayUrl,
name: metadata.name,
description: metadata.description,
icon: metadata.icon,
source: metadata.source,
updatedAt: Date.now(),
};
await db.groupMetadata.put(entry);
}
/**
* Batch save multiple group metadata entries
*/
export async function cacheGroupMetadataBatch(
entries: Array<{
relayUrl: string;
groupId: string;
metadata: ResolvedGroupMetadata;
}>,
): Promise<void> {
const now = Date.now();
const records: CachedGroupMetadata[] = entries.map(
({ relayUrl, groupId, metadata }) => ({
key: getGroupCacheKey(relayUrl, groupId),
groupId,
relayUrl,
name: metadata.name,
description: metadata.description,
icon: metadata.icon,
source: metadata.source,
updatedAt: now,
}),
);
await db.groupMetadata.bulkPut(records);
}
/**
* Resolve group metadata with profile fallback
*

View File

@@ -108,17 +108,6 @@ export interface GrimoireZap {
comment?: string; // Optional zap comment/message
}
export interface CachedGroupMetadata {
key: string; // Primary key: "relayUrl'groupId"
groupId: string;
relayUrl: string;
name?: string;
description?: string;
icon?: string;
source: "nip29" | "profile" | "fallback";
updatedAt: number;
}
class GrimoireDb extends Dexie {
profiles!: Table<Profile>;
nip05!: Table<Nip05>;
@@ -132,7 +121,6 @@ class GrimoireDb extends Dexie {
spellbooks!: Table<LocalSpellbook>;
lnurlCache!: Table<LnurlCache>;
grimoireZaps!: Table<GrimoireZap>;
groupMetadata!: Table<CachedGroupMetadata>;
constructor(name: string) {
super(name);
@@ -400,24 +388,6 @@ class GrimoireDb extends Dexie {
grimoireZaps:
"&eventId, senderPubkey, timestamp, [senderPubkey+timestamp]",
});
// Version 18: Add group metadata caching to prevent name flicker
this.version(18).stores({
profiles: "&pubkey",
nip05: "&nip05",
nips: "&id",
relayInfo: "&url",
relayAuthPreferences: "&url",
relayLists: "&pubkey, updatedAt",
relayLiveness: "&url",
blossomServers: "&pubkey, updatedAt",
spells: "&id, alias, createdAt, isPublished, deletedAt",
spellbooks: "&id, slug, title, createdAt, isPublished, deletedAt",
lnurlCache: "&address, fetchedAt",
grimoireZaps:
"&eventId, senderPubkey, timestamp, [senderPubkey+timestamp]",
groupMetadata: "&key, groupId, relayUrl, updatedAt",
});
}
}