mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-12 00:17:02 +02:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user