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:
Jure
2026-03-10 17:53:00 +01:00
parent a8627b7305
commit e3ba3dbcee
7 changed files with 283 additions and 14 deletions

View File

@@ -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]);