mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 15:07:10 +02:00
Merge pull request #13 from purrgrammer/claude/useprofile-race-fix-EeWQZ
fix: prevent race conditions in useProfile hook
This commit is contained in:
@@ -1,19 +1,38 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { profileLoader } from "@/services/loaders";
|
||||
import { ProfileContent, getProfileContent } from "applesauce-core/helpers";
|
||||
import { kinds } from "nostr-tools";
|
||||
import db from "@/services/db";
|
||||
|
||||
/**
|
||||
* Hook to fetch and cache user profile metadata
|
||||
*
|
||||
* Uses AbortController to prevent race conditions when:
|
||||
* - Component unmounts during async operations
|
||||
* - Pubkey changes while a fetch is in progress
|
||||
*
|
||||
* @param pubkey - The user's public key (hex)
|
||||
* @returns ProfileContent or undefined if loading/not found
|
||||
*/
|
||||
export function useProfile(pubkey?: string): ProfileContent | undefined {
|
||||
const [profile, setProfile] = useState<ProfileContent | undefined>();
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
if (!pubkey) return;
|
||||
if (!pubkey) {
|
||||
setProfile(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load from IndexedDB first
|
||||
// Abort any in-flight requests from previous effect runs
|
||||
abortControllerRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current = controller;
|
||||
|
||||
// Load from IndexedDB first (fast path)
|
||||
db.profiles.get(pubkey).then((cachedProfile) => {
|
||||
if (mounted && cachedProfile) {
|
||||
if (controller.signal.aborted) return;
|
||||
if (cachedProfile) {
|
||||
setProfile(cachedProfile);
|
||||
}
|
||||
});
|
||||
@@ -21,6 +40,7 @@ export function useProfile(pubkey?: string): ProfileContent | undefined {
|
||||
// Fetch from network
|
||||
const sub = profileLoader({ kind: kinds.Metadata, pubkey }).subscribe({
|
||||
next: async (fetchedEvent) => {
|
||||
if (controller.signal.aborted) return;
|
||||
if (!fetchedEvent || !fetchedEvent.content) return;
|
||||
|
||||
// Use applesauce helper for safe profile parsing
|
||||
@@ -30,24 +50,31 @@ export function useProfile(pubkey?: string): ProfileContent | undefined {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to IndexedDB
|
||||
await db.profiles.put({
|
||||
...profileData,
|
||||
pubkey,
|
||||
created_at: fetchedEvent.created_at,
|
||||
});
|
||||
// Only update state and cache if not aborted
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
if (mounted) {
|
||||
setProfile(profileData);
|
||||
setProfile(profileData);
|
||||
|
||||
// Save to IndexedDB after state update to avoid blocking UI
|
||||
try {
|
||||
await db.profiles.put({
|
||||
...profileData,
|
||||
pubkey,
|
||||
created_at: fetchedEvent.created_at,
|
||||
});
|
||||
} catch (err) {
|
||||
// Log but don't throw - cache failure shouldn't break the UI
|
||||
console.error("[useProfile] Failed to cache profile:", err);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
if (controller.signal.aborted) return;
|
||||
console.error("[useProfile] Error fetching profile:", err);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
controller.abort();
|
||||
sub.unsubscribe();
|
||||
};
|
||||
}, [pubkey]);
|
||||
|
||||
Reference in New Issue
Block a user