Merge pull request #184 from v0l/feed-cache

feed cache
This commit is contained in:
2023-02-02 13:55:31 +00:00
committed by GitHub
21 changed files with 372 additions and 291 deletions

View File

@ -1,23 +1,35 @@
import Dexie, { Table } from "dexie"; import Dexie, { Table } from "dexie";
import { TaggedRawEvent, u256 } from "Nostr";
import { MetadataCache } from "State/Users"; import { MetadataCache } from "State/Users";
import { hexToBech32 } from "Util"; import { hexToBech32 } from "Util";
export const NAME = 'snortDB' export const NAME = 'snortDB'
export const VERSION = 2 export const VERSION = 3
export interface SubCache {
id: string,
ids: u256[],
until?: number,
since?: number,
}
const STORES = { const STORES = {
users: '++pubkey, name, display_name, picture, nip05, npub' users: '++pubkey, name, display_name, picture, nip05, npub',
events: '++id, pubkey, created_at',
feeds: '++id'
} }
export class SnortDB extends Dexie { export class SnortDB extends Dexie {
users!: Table<MetadataCache>; users!: Table<MetadataCache>;
events!: Table<TaggedRawEvent>;
feeds!: Table<SubCache>;
constructor() { constructor() {
super(NAME); super(NAME);
this.version(VERSION).stores(STORES).upgrade(tx => { this.version(VERSION).stores(STORES).upgrade(async tx => {
return tx.table("users").toCollection().modify(user => { await tx.table("users").toCollection().modify(user => {
user.npub = hexToBech32("npub", user.pubkey) user.npub = hexToBech32("npub", user.pubkey)
}) });
}); });
} }
} }

View File

@ -7,7 +7,7 @@ import useSubscription from "Feed/Subscription";
export default function useFollowersFeed(pubkey: HexKey) { export default function useFollowersFeed(pubkey: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
let x = new Subscriptions(); let x = new Subscriptions();
x.Id = "followers"; x.Id = `followers:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ContactList]); x.Kinds = new Set([EventKind.ContactList]);
x.PTags = new Set([pubkey]); x.PTags = new Set([pubkey]);

View File

@ -7,7 +7,7 @@ import useSubscription, { NoteStore } from "Feed/Subscription";
export default function useFollowsFeed(pubkey: HexKey) { export default function useFollowsFeed(pubkey: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
let x = new Subscriptions(); let x = new Subscriptions();
x.Id = "follows"; x.Id = `follows:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ContactList]); x.Kinds = new Set([EventKind.ContactList]);
x.Authors = new Set([pubkey]); x.Authors = new Set([pubkey]);

View File

@ -9,12 +9,10 @@ import { Subscriptions } from "Nostr/Subscriptions";
import { addDirectMessage, setFollows, setRelays, setMuted, setBlocked, sendNotification } from "State/Login"; import { addDirectMessage, setFollows, setRelays, setMuted, setBlocked, sendNotification } from "State/Login";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
import { mapEventToProfile, MetadataCache } from "State/Users"; import { mapEventToProfile, MetadataCache } from "State/Users";
import { getDb } from "State/Users/Db"; import { useDb } from "State/Users/Db";
import useSubscription from "Feed/Subscription"; import useSubscription from "Feed/Subscription";
import { getDisplayName } from "Element/ProfileImage";
import { barierNip07 } from "Feed/EventPublisher"; import { barierNip07 } from "Feed/EventPublisher";
import { getMutedKeys, getNewest } from "Feed/MuteList"; import { getMutedKeys, getNewest } from "Feed/MuteList";
import { MentionRegex } from "Const";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
/** /**
@ -24,6 +22,7 @@ export default function useLoginFeed() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { publicKey: pubKey, privateKey: privKey } = useSelector((s: RootState) => s.login); const { publicKey: pubKey, privateKey: privKey } = useSelector((s: RootState) => s.login);
const { isMuted } = useModeration(); const { isMuted } = useModeration();
const db = useDb();
const subMetadata = useMemo(() => { const subMetadata = useMemo(() => {
if (!pubKey) return null; if (!pubKey) return null;
@ -77,10 +76,10 @@ export default function useLoginFeed() {
return dms; return dms;
}, [pubKey]); }, [pubKey]);
const metadataFeed = useSubscription(subMetadata, { leaveOpen: true }); const metadataFeed = useSubscription(subMetadata, { leaveOpen: true, cache: true });
const notificationFeed = useSubscription(subNotification, { leaveOpen: true }); const notificationFeed = useSubscription(subNotification, { leaveOpen: true, cache: true });
const dmsFeed = useSubscription(subDms, { leaveOpen: true }); const dmsFeed = useSubscription(subDms, { leaveOpen: true, cache: true });
const mutedFeed = useSubscription(subMuted, { leaveOpen: true }); const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true });
useEffect(() => { useEffect(() => {
let contactList = metadataFeed.store.notes.filter(a => a.kind === EventKind.ContactList); let contactList = metadataFeed.store.notes.filter(a => a.kind === EventKind.ContactList);
@ -107,26 +106,25 @@ export default function useLoginFeed() {
return acc; return acc;
}, { created: 0, profile: null as MetadataCache | null }); }, { created: 0, profile: null as MetadataCache | null });
if (maxProfile.profile) { if (maxProfile.profile) {
const db = getDb()
let existing = await db.find(maxProfile.profile.pubkey); let existing = await db.find(maxProfile.profile.pubkey);
if ((existing?.created ?? 0) < maxProfile.created) { if ((existing?.created ?? 0) < maxProfile.created) {
await db.put(maxProfile.profile); await db.put(maxProfile.profile);
} }
} }
})().catch(console.warn); })().catch(console.warn);
}, [dispatch, metadataFeed.store]); }, [dispatch, metadataFeed.store, db]);
useEffect(() => { useEffect(() => {
const replies = notificationFeed.store.notes.filter(a => a.kind === EventKind.TextNote && !isMuted(a.pubkey)) const replies = notificationFeed.store.notes.filter(a => a.kind === EventKind.TextNote && !isMuted(a.pubkey))
replies.forEach(nx => { replies.forEach(nx => {
makeNotification(nx).then(notification => { makeNotification(db, nx).then(notification => {
if (notification) { if (notification) {
// @ts-ignore // @ts-ignore
dispatch(sendNotification(notification)) dispatch(sendNotification(notification))
} }
}) })
}) })
}, [dispatch, notificationFeed.store]); }, [dispatch, notificationFeed.store, db]);
useEffect(() => { useEffect(() => {
const muted = getMutedKeys(mutedFeed.store.notes) const muted = getMutedKeys(mutedFeed.store.notes)

View File

@ -1,6 +1,4 @@
import { useLiveQuery } from "dexie-react-hooks"; import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { RootState } from "State/Store";
import { MetadataCache } from "State/Users"; import { MetadataCache } from "State/Users";
import { useKey, useKeys } from "State/Users/Hooks"; import { useKey, useKeys } from "State/Users/Hooks";
import { HexKey } from "Nostr"; import { HexKey } from "Nostr";

View File

@ -3,6 +3,7 @@ import { System } from "Nostr/System";
import { TaggedRawEvent } from "Nostr"; import { TaggedRawEvent } from "Nostr";
import { Subscriptions } from "Nostr/Subscriptions"; import { Subscriptions } from "Nostr/Subscriptions";
import { debounce } from "Util"; import { debounce } from "Util";
import { db } from "Db";
export type NoteStore = { export type NoteStore = {
notes: Array<TaggedRawEvent>, notes: Array<TaggedRawEvent>,
@ -10,7 +11,8 @@ export type NoteStore = {
}; };
export type UseSubscriptionOptions = { export type UseSubscriptionOptions = {
leaveOpen: boolean leaveOpen: boolean,
cache: boolean
} }
interface ReducerArg { interface ReducerArg {
@ -77,33 +79,49 @@ export default function useSubscription(sub: Subscriptions | null, options?: Use
const [state, dispatch] = useReducer(notesReducer, initStore); const [state, dispatch] = useReducer(notesReducer, initStore);
const [debounceOutput, setDebounceOutput] = useState<number>(0); const [debounceOutput, setDebounceOutput] = useState<number>(0);
const [subDebounce, setSubDebounced] = useState<Subscriptions>(); const [subDebounce, setSubDebounced] = useState<Subscriptions>();
const useCache = useMemo(() => options?.cache === true, [options]);
useEffect(() => { useEffect(() => {
if (sub) { if (sub) {
return debounce(DebounceMs, () => { return debounce(DebounceMs, () => {
dispatch({
type: "END",
end: false
});
setSubDebounced(sub); setSubDebounced(sub);
}); });
} }
}, [sub, options]); }, [sub, options]);
useEffect(() => { useEffect(() => {
if (sub) { if (subDebounce) {
sub.OnEvent = (e) => { dispatch({
type: "END",
end: false
});
if (useCache) {
// preload notes from db
PreloadNotes(subDebounce.Id)
.then(ev => {
dispatch({
type: "EVENT",
ev: ev
});
})
.catch(console.warn);
}
subDebounce.OnEvent = (e) => {
dispatch({ dispatch({
type: "EVENT", type: "EVENT",
ev: e ev: e
}); });
if (useCache) {
db.events.put(e);
}
}; };
sub.OnEnd = (c) => { subDebounce.OnEnd = (c) => {
if (!(options?.leaveOpen ?? false)) { if (!(options?.leaveOpen ?? false)) {
c.RemoveSubscription(sub.Id); c.RemoveSubscription(subDebounce.Id);
if (sub.IsFinished()) { if (subDebounce.IsFinished()) {
System.RemoveSubscription(sub.Id); System.RemoveSubscription(subDebounce.Id);
} }
} }
dispatch({ dispatch({
@ -112,14 +130,23 @@ export default function useSubscription(sub: Subscriptions | null, options?: Use
}); });
}; };
console.debug("Adding sub: ", sub.ToObject()); console.debug("Adding sub: ", subDebounce.ToObject());
System.AddSubscription(sub); System.AddSubscription(subDebounce);
return () => { return () => {
console.debug("Removing sub: ", sub.ToObject()); console.debug("Removing sub: ", subDebounce.ToObject());
System.RemoveSubscription(sub.Id); System.RemoveSubscription(subDebounce.Id);
}; };
} }
}, [subDebounce]); }, [subDebounce, useCache]);
useEffect(() => {
if (subDebounce && useCache) {
return debounce(500, () => {
TrackNotesInFeed(subDebounce.Id, state.notes)
.catch(console.warn);
});
}
}, [state, useCache]);
useEffect(() => { useEffect(() => {
return debounce(DebounceMs, () => { return debounce(DebounceMs, () => {
@ -141,3 +168,23 @@ export default function useSubscription(sub: Subscriptions | null, options?: Use
} }
} }
} }
/**
* Lookup cached copy of feed
*/
const PreloadNotes = async (id: string): Promise<TaggedRawEvent[]> => {
const feed = await db.feeds.get(id);
if (feed) {
const events = await db.events.bulkGet(feed.ids);
return events.filter(a => a !== undefined).map(a => a!);
}
return [];
}
const TrackNotesInFeed = async (id: string, notes: TaggedRawEvent[]) => {
const existing = await db.feeds.get(id);
const ids = Array.from(new Set([...(existing?.ids || []), ...notes.map(a => a.id)]));
const since = notes.reduce((acc, v) => acc > v.created_at ? v.created_at : acc, +Infinity);
const until = notes.reduce((acc, v) => acc < v.created_at ? v.created_at : acc, -Infinity);
await db.feeds.put({ id, ids, since, until });
}

View File

@ -38,7 +38,7 @@ export default function useThreadFeed(id: u256) {
return thisSub; return thisSub;
}, [trackingEvents, pref, id]); }, [trackingEvents, pref, id]);
const main = useSubscription(sub, { leaveOpen: true }); const main = useSubscription(sub, { leaveOpen: true, cache: true });
useEffect(() => { useEffect(() => {
if (main.store) { if (main.store) {

View File

@ -14,6 +14,7 @@ export interface TimelineFeedOptions {
export interface TimelineSubject { export interface TimelineSubject {
type: "pubkey" | "hashtag" | "global" | "ptag" | "keyword", type: "pubkey" | "hashtag" | "global" | "ptag" | "keyword",
discriminator: string,
items: string[] items: string[]
} }
@ -32,7 +33,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
} }
let sub = new Subscriptions(); let sub = new Subscriptions();
sub.Id = `timeline:${subject.type}`; sub.Id = `timeline:${subject.type}:${subject.discriminator}`;
sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]); sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]);
switch (subject.type) { switch (subject.type) {
case "pubkey": { case "pubkey": {
@ -54,7 +55,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
} }
} }
return sub; return sub;
}, [subject.type, subject.items]); }, [subject.type, subject.items, subject.discriminator]);
const sub = useMemo(() => { const sub = useMemo(() => {
let sub = createSub(); let sub = createSub();
@ -86,7 +87,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
return sub; return sub;
}, [until, since, options.method, pref, createSub]); }, [until, since, options.method, pref, createSub]);
const main = useSubscription(sub, { leaveOpen: true }); const main = useSubscription(sub, { leaveOpen: true, cache: true });
const subRealtime = useMemo(() => { const subRealtime = useMemo(() => {
let subLatest = createSub(); let subLatest = createSub();
@ -98,7 +99,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
return subLatest; return subLatest;
}, [pref, createSub]); }, [pref, createSub]);
const latest = useSubscription(subRealtime, { leaveOpen: true }); const latest = useSubscription(subRealtime, { leaveOpen: true, cache: false });
const subNext = useMemo(() => { const subNext = useMemo(() => {
let sub: Subscriptions | undefined; let sub: Subscriptions | undefined;
@ -111,7 +112,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
return sub ?? null; return sub ?? null;
}, [trackingEvents, pref, subject.type]); }, [trackingEvents, pref, subject.type]);
const others = useSubscription(subNext, { leaveOpen: true }); const others = useSubscription(subNext, { leaveOpen: true, cache: true });
const subParents = useMemo(() => { const subParents = useMemo(() => {
if (trackingParentEvents.length > 0) { if (trackingParentEvents.length > 0) {

View File

@ -1,7 +1,6 @@
import { HexKey, TaggedRawEvent } from "Nostr"; import { HexKey, TaggedRawEvent } from "Nostr";
import { getDb } from "State/Users/Db";
import { ProfileCacheExpire } from "Const"; import { ProfileCacheExpire } from "Const";
import { mapEventToProfile, MetadataCache } from "State/Users"; import { mapEventToProfile, MetadataCache, UsersDb } from "State/Users";
import Connection, { RelaySettings } from "Nostr/Connection"; import Connection, { RelaySettings } from "Nostr/Connection";
import Event from "Nostr/Event"; import Event from "Nostr/Event";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
@ -31,6 +30,11 @@ export class NostrSystem {
*/ */
WantsMetadata: Set<HexKey>; WantsMetadata: Set<HexKey>;
/**
* User db store
*/
UserDb?: UsersDb;
constructor() { constructor() {
this.Sockets = new Map(); this.Sockets = new Map();
this.Subscriptions = new Map(); this.Subscriptions = new Map();
@ -166,9 +170,9 @@ export class NostrSystem {
} }
async _FetchMetadata() { async _FetchMetadata() {
if (this.UserDb) {
let missing = new Set<HexKey>(); let missing = new Set<HexKey>();
const db = getDb() let meta = await this.UserDb.bulkGet(Array.from(this.WantsMetadata));
let meta = await db.bulkGet(Array.from(this.WantsMetadata));
let expire = new Date().getTime() - ProfileCacheExpire; let expire = new Date().getTime() - ProfileCacheExpire;
for (let pk of this.WantsMetadata) { for (let pk of this.WantsMetadata) {
let m = meta.find(a => a?.pubkey === pk); let m = meta.find(a => a?.pubkey === pk);
@ -191,25 +195,25 @@ export class NostrSystem {
sub.OnEvent = async (e) => { sub.OnEvent = async (e) => {
let profile = mapEventToProfile(e); let profile = mapEventToProfile(e);
if (profile) { if (profile) {
let existing = await db.find(profile.pubkey); let existing = await this.UserDb!.find(profile.pubkey);
if ((existing?.created ?? 0) < profile.created) { if ((existing?.created ?? 0) < profile.created) {
await db.put(profile); await this.UserDb!.put(profile);
} else if (existing) { } else if (existing) {
await db.update(profile.pubkey, { loaded: new Date().getTime() }); await this.UserDb!.update(profile.pubkey, { loaded: new Date().getTime() });
} }
} }
} }
let results = await this.RequestSubscription(sub); let results = await this.RequestSubscription(sub);
let couldNotFetch = Array.from(missing).filter(a => !results.some(b => b.pubkey === a)); let couldNotFetch = Array.from(missing).filter(a => !results.some(b => b.pubkey === a));
console.debug("No profiles: ", couldNotFetch); console.debug("No profiles: ", couldNotFetch);
await db.bulkPut(couldNotFetch.map(a => { await this.UserDb!.bulkPut(couldNotFetch.map(a => {
return { return {
pubkey: a, pubkey: a,
loaded: new Date().getTime() loaded: new Date().getTime()
} as MetadataCache; } as MetadataCache;
})); }));
} }
}
setTimeout(() => this._FetchMetadata(), 500); setTimeout(() => this._FetchMetadata(), 500);
} }

View File

@ -3,13 +3,11 @@ import Nostrich from "nostrich.webp";
import { TaggedRawEvent } from "Nostr"; import { TaggedRawEvent } from "Nostr";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import type { NotificationRequest } from "State/Login"; import type { NotificationRequest } from "State/Login";
import { MetadataCache } from "State/Users"; import { MetadataCache, UsersDb } from "State/Users";
import { getDb } from "State/Users/Db";
import { getDisplayName } from "Element/ProfileImage"; import { getDisplayName } from "Element/ProfileImage";
import { MentionRegex } from "Const"; import { MentionRegex } from "Const";
export async function makeNotification(ev: TaggedRawEvent): Promise<NotificationRequest | null> { export async function makeNotification(db: UsersDb, ev: TaggedRawEvent): Promise<NotificationRequest | null> {
const db = getDb()
switch (ev.kind) { switch (ev.kind) {
case EventKind.TextNote: { case EventKind.TextNote: {
const pubkeys = new Set([ev.pubkey, ...ev.tags.filter(a => a[0] === "p").map(a => a[1]!)]); const pubkeys = new Set([ev.pubkey, ...ev.tags.filter(a => a[0] === "p").map(a => a[1]!)]);

View File

@ -8,7 +8,7 @@ const HashTagsPage = () => {
return ( return (
<> <>
<h2>#{tag}</h2> <h2>#{tag}</h2>
<Timeline key={tag} subject={{ type: "hashtag", items: [tag] }} postsOnly={false} method={"TIME_RANGE"} /> <Timeline key={tag} subject={{ type: "hashtag", items: [tag], discriminator: tag }} postsOnly={false} method={"TIME_RANGE"} />
</> </>
) )
} }

View File

@ -17,6 +17,8 @@ import { totalUnread } from "Pages/MessagesPage";
import { SearchRelays } from 'Const'; import { SearchRelays } from 'Const';
import useEventPublisher from "Feed/EventPublisher"; import useEventPublisher from "Feed/EventPublisher";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import { IndexedUDB, useDb } from "State/Users/Db";
import { db } from "Db";
export default function Layout() { export default function Layout() {
@ -31,6 +33,7 @@ export default function Layout() {
const { isMuted } = useModeration(); const { isMuted } = useModeration();
const filteredDms = dms.filter(a => !isMuted(a.pubkey)) const filteredDms = dms.filter(a => !isMuted(a.pubkey))
const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences); const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences);
const usingDb = useDb();
const pub = useEventPublisher(); const pub = useEventPublisher();
useLoginFeed(); useLoginFeed();
@ -38,6 +41,10 @@ export default function Layout() {
System.nip42Auth = pub.nip42Auth System.nip42Auth = pub.nip42Auth
}, [pub]) }, [pub])
useEffect(() => {
System.UserDb = usingDb;
}, [usingDb])
useEffect(() => { useEffect(() => {
if (relays) { if (relays) {
for (let [k, v] of Object.entries(relays)) { for (let [k, v] of Object.entries(relays)) {
@ -73,7 +80,28 @@ export default function Layout() {
}, [prefs.theme]); }, [prefs.theme]);
useEffect(() => { useEffect(() => {
dispatch(init()); // check DB support then init
IndexedUDB.isAvailable()
.then(async a => {
const dbType = a ? "indexdDb" : "redux";
// cleanup on load
if (dbType === "indexdDb") {
await db.feeds.clear();
const now = Math.floor(new Date().getTime() / 1000);
const cleanupEvents = await db.events
.where("created_at")
.above(now - (60 * 60))
.primaryKeys();
console.debug(`Cleanup ${cleanupEvents.length} events`);
await db.events.bulkDelete(cleanupEvents)
}
console.debug(`Using db: ${dbType}`);
dispatch(init(dbType));
})
}, []); }, []);
async function goToNotifications(e: any) { async function goToNotifications(e: any) {

View File

@ -17,7 +17,7 @@ export default function NotificationsPage() {
return ( return (
<> <>
{pubkey ? {pubkey ?
<Timeline subject={{ type: "ptag", items: [pubkey!] }} postsOnly={false} method={"TIME_RANGE"} /> <Timeline subject={{ type: "ptag", items: [pubkey!], discriminator: pubkey!.slice(0, 12) }} postsOnly={false} method={"TIME_RANGE"} />
: null} : null}
</> </>
) )

View File

@ -105,7 +105,7 @@ export default function ProfilePage() {
function tabContent() { function tabContent() {
switch (tab) { switch (tab) {
case ProfileTab.Notes: case ProfileTab.Notes:
return <Timeline key={id} subject={{ type: "pubkey", items: [id] }} postsOnly={false} method={"LIMIT_UNTIL"} ignoreModeration={true} />; return <Timeline key={id} subject={{ type: "pubkey", items: [id], discriminator: id.slice(0, 12) }} postsOnly={false} method={"LIMIT_UNTIL"} ignoreModeration={true} />;
case ProfileTab.Follows: { case ProfileTab.Follows: {
if (isMe) { if (isMe) {
return ( return (

View File

@ -29,7 +29,7 @@ export default function RootPage() {
} }
const isGlobal = loggedOut || tab === RootTab.Global; const isGlobal = loggedOut || tab === RootTab.Global;
const timelineSubect: TimelineSubject = isGlobal ? { type: "global", items: [] } : { type: "pubkey", items: follows }; const timelineSubect: TimelineSubject = isGlobal ? { type: "global", items: [], discriminator: "all" } : { type: "pubkey", items: follows, discriminator: "follows" };
return ( return (
<> <>
{pubKey ? <> {pubKey ? <>

View File

@ -43,7 +43,7 @@ const SearchPage = () => {
<div className="flex mb10"> <div className="flex mb10">
<input type="text" className="f-grow mr10" placeholder="Search.." value={search} onChange={e => setSearch(e.target.value)} /> <input type="text" className="f-grow mr10" placeholder="Search.." value={search} onChange={e => setSearch(e.target.value)} />
</div> </div>
{keyword && <Timeline key={keyword} subject={{ type: "keyword", items: [keyword] }} postsOnly={false} method={"LIMIT_UNTIL"} />} {keyword && <Timeline key={keyword} subject={{ type: "keyword", items: [keyword], discriminator: keyword }} postsOnly={false} method={"LIMIT_UNTIL"} />}
</> </>
) )
} }

View File

@ -61,8 +61,14 @@ export interface UserPreferences {
*/ */
imgProxyConfig: ImgProxySettings | null imgProxyConfig: ImgProxySettings | null
} }
export type DbType = "indexdDb" | "redux";
export interface LoginStore { export interface LoginStore {
/**
* Which db we will use to cache data
*/
useDb: DbType,
/** /**
* If there is no login * If there is no login
*/ */
@ -146,6 +152,7 @@ const DefaultImgProxy = {
}; };
export const InitState = { export const InitState = {
useDb: "redux",
loggedOut: undefined, loggedOut: undefined,
publicKey: undefined, publicKey: undefined,
privateKey: undefined, privateKey: undefined,
@ -186,7 +193,8 @@ const LoginSlice = createSlice({
name: "Login", name: "Login",
initialState: InitState, initialState: InitState,
reducers: { reducers: {
init: (state) => { init: (state, action: PayloadAction<DbType>) => {
state.useDb = action.payload;
state.privateKey = window.localStorage.getItem(PrivateKeyItem) ?? undefined; state.privateKey = window.localStorage.getItem(PrivateKeyItem) ?? undefined;
if (state.privateKey) { if (state.privateKey) {
window.localStorage.removeItem(PublicKeyItem); // reset nip07 if using private key window.localStorage.removeItem(PublicKeyItem); // reset nip07 if using private key

View File

@ -2,9 +2,12 @@ import { HexKey } from "Nostr";
import { db as idb } from "Db"; import { db as idb } from "Db";
import { UsersDb, MetadataCache, setUsers } from "State/Users"; import { UsersDb, MetadataCache, setUsers } from "State/Users";
import store from "State/Store"; import store, { RootState } from "State/Store";
import { useSelector } from "react-redux";
class IndexedUsersDb implements UsersDb {
ready: boolean = false;
class IndexedDb implements UsersDb {
isAvailable() { isAvailable() {
if ("indexedDB" in window) { if ("indexedDB" in window) {
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
@ -130,20 +133,13 @@ class ReduxUsersDb implements UsersDb {
} }
} }
export const IndexedUDB = new IndexedUsersDb();
export const ReduxUDB = new ReduxUsersDb();
const indexedDb = new IndexedDb() export function useDb(): UsersDb {
export const inMemoryDb = new ReduxUsersDb() const db = useSelector((s: RootState) => s.login.useDb);
switch (db) {
let db: UsersDb = inMemoryDb case "indexdDb": return IndexedUDB
indexedDb.isAvailable().then((available) => { default: return ReduxUDB
if (available) {
console.debug('Using Indexed DB')
db = indexedDb;
} else {
console.debug('Using in-memory DB')
} }
})
export function getDb() {
return db
} }

View File

@ -1,27 +1,17 @@
import { useSelector } from "react-redux" import { useSelector } from "react-redux"
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import { MetadataCache } from "State/Users"; import { MetadataCache } from "State/Users";
import { getDb, inMemoryDb } from "State/Users/Db";
import type { RootState } from "State/Store" import type { RootState } from "State/Store"
import { HexKey } from "Nostr"; import { HexKey } from "Nostr";
import { useDb } from "./Db";
export function useQuery(query: string, limit: number = 5) { export function useQuery(query: string, limit: number = 5) {
const db = getDb() const db = useDb()
return useLiveQuery(async () => db.query(query), [query],)
const allUsers = useLiveQuery(
() => db.query(query)
.catch((err) => {
console.error(err)
return inMemoryDb.query(query)
}),
[query],
)
return allUsers
} }
export function useKey(pubKey: HexKey) { export function useKey(pubKey: HexKey) {
const db = getDb() const db = useDb()
const { users } = useSelector((state: RootState) => state.users) const { users } = useSelector((state: RootState) => state.users)
const defaultUser = users[pubKey] const defaultUser = users[pubKey]
@ -40,7 +30,9 @@ export function useKey(pubKey: HexKey) {
} }
export function useKeys(pubKeys: HexKey[]): Map<HexKey, MetadataCache> { export function useKeys(pubKeys: HexKey[]): Map<HexKey, MetadataCache> {
const db = getDb() const db = useDb()
const { users } = useSelector((state: RootState) => state.users)
const dbUsers = useLiveQuery(async () => { const dbUsers = useLiveQuery(async () => {
if (pubKeys) { if (pubKeys) {
try { try {
@ -48,12 +40,11 @@ export function useKeys(pubKeys: HexKey[]): Map<HexKey, MetadataCache> {
return new Map(ret.map(a => [a.pubkey, a])) return new Map(ret.map(a => [a.pubkey, a]))
} catch (error) { } catch (error) {
console.error(error) console.error(error)
const ret = await inMemoryDb.bulkGet(pubKeys); return new Map(pubKeys.map(a => [a, users[a]]))
return new Map(ret.map(a => [a.pubkey, a]))
} }
} }
return new Map() return new Map()
}, [pubKeys]); }, [pubKeys, users]);
return dbUsers! return dbUsers!
} }