mirror of
https://github.com/hoornet/vega.git
synced 2026-06-13 08:23:31 -07:00
Add SQLite note and profile cache (roadmap #3)
- Rust: rusqlite (bundled) with WAL mode; wrystr.db in app data dir - db_save_notes: upsert batch of raw event JSON, prune to 500 kind-1 notes - db_load_feed: return N most-recent kind-1 raws for instant startup display - db_save_profile / db_load_profile: cache NDKUserProfile JSON by pubkey - Falls back to in-memory SQLite if the on-disk open fails - src/lib/db.ts: typed invoke wrappers; all errors silenced (cache is best-effort) - feed store: loadCachedFeed() populates notes before relay connects; loadFeed() merges fresh+cached (so relay returning fewer notes doesn't erase cached ones), then saves fresh notes to SQLite - useProfile: reads SQLite cache to show avatar/name instantly while relay request is in-flight; saves result to SQLite after relay responds - Feed: calls loadCachedFeed() first → notes visible before relay connects Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
type FeedTab = "global" | "following";
|
||||
|
||||
export function Feed() {
|
||||
const { notes, loading, connected, error, connect, loadFeed } = useFeedStore();
|
||||
const { notes, loading, connected, error, connect, loadCachedFeed, loadFeed } = useFeedStore();
|
||||
const { loggedIn, follows } = useUserStore();
|
||||
|
||||
const [tab, setTab] = useState<FeedTab>("global");
|
||||
@@ -17,6 +17,8 @@ export function Feed() {
|
||||
const [followLoading, setFollowLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Show cached notes immediately, then fetch fresh ones once connected
|
||||
loadCachedFeed();
|
||||
connect().then(() => loadFeed());
|
||||
}, []);
|
||||
|
||||
|
||||
+28
-9
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetchProfile } from "../lib/nostr";
|
||||
import { dbLoadProfile, dbSaveProfile } from "../lib/db";
|
||||
|
||||
const profileCache = new Map<string, any>();
|
||||
const pendingRequests = new Map<string, Promise<any>>();
|
||||
@@ -18,20 +19,38 @@ export function useProfile(pubkey: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduplicate requests for the same pubkey
|
||||
// Kick off relay fetch (deduplicated across simultaneous callers)
|
||||
if (!pendingRequests.has(pubkey)) {
|
||||
const request = fetchProfile(pubkey).then((p) => {
|
||||
profileCache.set(pubkey, p ?? null);
|
||||
pendingRequests.delete(pubkey);
|
||||
return p;
|
||||
}).catch(() => {
|
||||
pendingRequests.delete(pubkey);
|
||||
return null;
|
||||
});
|
||||
const request = fetchProfile(pubkey)
|
||||
.then((p) => {
|
||||
const result = p ?? null;
|
||||
profileCache.set(pubkey, result);
|
||||
pendingRequests.delete(pubkey);
|
||||
if (result) dbSaveProfile(pubkey, JSON.stringify(result));
|
||||
return result;
|
||||
})
|
||||
.catch(() => {
|
||||
pendingRequests.delete(pubkey);
|
||||
return null;
|
||||
});
|
||||
pendingRequests.set(pubkey, request);
|
||||
}
|
||||
|
||||
// Show SQLite cached profile immediately while the relay request is in-flight.
|
||||
// `settled` prevents the stale cached value from overwriting a fresh relay result.
|
||||
let settled = false;
|
||||
dbLoadProfile(pubkey).then((cached) => {
|
||||
if (!settled && cached && !profileCache.has(pubkey)) {
|
||||
try {
|
||||
setProfile(JSON.parse(cached));
|
||||
} catch {
|
||||
// Corrupt cache entry — ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pendingRequests.get(pubkey)!.then((p) => {
|
||||
settled = true;
|
||||
setProfile(p ?? null);
|
||||
});
|
||||
}, [pubkey]);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
/** Upsert a batch of raw Nostr event JSON strings into the SQLite note cache. */
|
||||
export function dbSaveNotes(notes: string[]): void {
|
||||
if (notes.length === 0) return;
|
||||
invoke("db_save_notes", { notes }).catch(() => {});
|
||||
}
|
||||
|
||||
/** Load up to `limit` recent kind-1 note JSONs from cache (newest first). */
|
||||
export async function dbLoadFeed(limit = 200): Promise<string[]> {
|
||||
return invoke<string[]>("db_load_feed", { limit }).catch(() => []);
|
||||
}
|
||||
|
||||
/** Cache a profile object (NDKUserProfile) for `pubkey`. Fire-and-forget. */
|
||||
export function dbSaveProfile(pubkey: string, content: string): void {
|
||||
invoke("db_save_profile", { pubkey, content }).catch(() => {});
|
||||
}
|
||||
|
||||
/** Load a cached profile JSON for `pubkey`. Returns null if not cached. */
|
||||
export async function dbLoadProfile(pubkey: string): Promise<string | null> {
|
||||
return invoke<string | null>("db_load_profile", { pubkey }).catch(() => null);
|
||||
}
|
||||
+29
-3
@@ -1,6 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { connectToRelays, fetchGlobalFeed } from "../lib/nostr";
|
||||
import { connectToRelays, fetchGlobalFeed, getNDK } from "../lib/nostr";
|
||||
import { dbLoadFeed, dbSaveNotes } from "../lib/db";
|
||||
|
||||
interface FeedState {
|
||||
notes: NDKEvent[];
|
||||
@@ -8,6 +9,7 @@ interface FeedState {
|
||||
connected: boolean;
|
||||
error: string | null;
|
||||
connect: () => Promise<void>;
|
||||
loadCachedFeed: () => Promise<void>;
|
||||
loadFeed: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -27,12 +29,36 @@ export const useFeedStore = create<FeedState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
loadCachedFeed: async () => {
|
||||
try {
|
||||
const rawNotes = await dbLoadFeed(200);
|
||||
if (rawNotes.length === 0) return;
|
||||
const ndk = getNDK();
|
||||
const events = rawNotes.map((raw) => new NDKEvent(ndk, JSON.parse(raw)));
|
||||
set({ notes: events });
|
||||
} catch {
|
||||
// Cache read failure is non-critical
|
||||
}
|
||||
},
|
||||
|
||||
loadFeed: async () => {
|
||||
if (get().loading) return;
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const notes = await fetchGlobalFeed(80);
|
||||
set({ notes, loading: false });
|
||||
const fresh = await fetchGlobalFeed(80);
|
||||
|
||||
// Merge with currently displayed notes so cached notes aren't lost
|
||||
// if the relay returns fewer results than the cache had.
|
||||
const freshIds = new Set(fresh.map((n) => n.id));
|
||||
const kept = get().notes.filter((n) => !freshIds.has(n.id));
|
||||
const merged = [...fresh, ...kept]
|
||||
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))
|
||||
.slice(0, 200);
|
||||
|
||||
set({ notes: merged, loading: false });
|
||||
|
||||
// Persist fresh notes to SQLite (fire-and-forget)
|
||||
dbSaveNotes(fresh.map((e) => JSON.stringify(e.rawEvent())));
|
||||
} catch (err) {
|
||||
set({ error: `Feed failed: ${err}`, loading: false });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user