add ndk cache tauri

This commit is contained in:
reya 2023-10-29 11:07:05 +07:00
parent ace58ecdd5
commit 0b25a4a04b
12 changed files with 236 additions and 97 deletions

View File

@ -0,0 +1,27 @@
-- Add migration script here
CREATE TABLE
ndk_users (
pubkey TEXT NOT NULL PRIMARY KEY,
profile TEXT,
createdAt NUMBER
);
CREATE TABLE
ndk_events (
id TEXT NOT NULL PRIMARY KEY,
pubkey TEXT,
content TEXT,
kind NUMBER,
createdAt NUMBER,
relay TEXT,
event TEXT
);
CREATE TABLE
ndk_eventtags (
id TEXT NOT NULL PRIMARY KEY,
eventId TEXT,
tag TEXT,
value TEXT,
tagValue TEXT
);

View File

@ -128,12 +128,20 @@ fn main() {
tauri_plugin_sql::Builder::default() tauri_plugin_sql::Builder::default()
.add_migrations( .add_migrations(
"sqlite:lume_v2.db", "sqlite:lume_v2.db",
vec![Migration { vec![
version: 20230418013219, Migration {
description: "initial data", version: 20230418013219,
sql: include_str!("../migrations/20230418013219_initial_data.sql"), description: "initial data",
kind: MigrationKind::Up, sql: include_str!("../migrations/20230418013219_initial_data.sql"),
}], kind: MigrationKind::Up,
},
Migration {
version: 20231028083224,
description: "add ndk cache table",
sql: include_str!("../migrations/20231028083224_add_ndk_cache_table.sql"),
kind: MigrationKind::Up,
},
],
) )
.build(), .build(),
) )

View File

@ -5,60 +5,30 @@ import type {
NDKFilter, NDKFilter,
NDKSubscription, NDKSubscription,
NDKUserProfile, NDKUserProfile,
NostrEvent,
} from '@nostr-dev-kit/ndk'; } from '@nostr-dev-kit/ndk';
import _debug from 'debug'; import { LRUCache } from 'lru-cache';
import { matchFilter } from 'nostr-tools'; import { matchFilter } from 'nostr-tools';
import { LRUCache } from 'typescript-lru-cache';
import { createDatabase, db } from './db'; import { LumeStorage } from '@libs/storage/instance';
export { db } from './db'; export default class NDKCacheAdapterTauri implements NDKCacheAdapter {
public db: LumeStorage;
interface NDKCacheAdapterDexieOptions {
/**
* The name of the database to use
*/
dbName?: string;
/**
* Debug instance to use for logging
*/
debug?: debug.IDebugger;
/**
* The number of seconds to store events in Dexie (IndexedDB) before they expire
* Defaults to 3600 seconds (1 hour)
*/
expirationTime?: number;
/**
* Number of profiles to keep in an LRU cache
*/
profileCacheSize?: number | 'disabled';
}
export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
public debug: debug.Debugger;
private expirationTime;
readonly locking;
public profiles?: LRUCache<Hexpubkey, NDKUserProfile>; public profiles?: LRUCache<Hexpubkey, NDKUserProfile>;
private dirtyProfiles: Set<Hexpubkey> = new Set(); private dirtyProfiles: Set<Hexpubkey> = new Set();
readonly locking: boolean;
constructor(opts: NDKCacheAdapterDexieOptions = {}) { constructor(db: LumeStorage) {
createDatabase(opts.dbName || 'ndk'); this.db = db;
this.debug = opts.debug || _debug('ndk:dexie-adapter');
this.locking = true; this.locking = true;
this.expirationTime = opts.expirationTime || 3600;
if (opts.profileCacheSize !== 'disabled') { this.profiles = new LRUCache({
this.profiles = new LRUCache({ max: 100000,
maxSize: opts.profileCacheSize || 100000, });
});
setInterval(() => { setInterval(() => {
this.dumpProfiles(); this.dumpProfiles();
}, 1000 * 10); }, 1000 * 10);
}
} }
public async query(subscription: NDKSubscription): Promise<void> { public async query(subscription: NDKSubscription): Promise<void> {
@ -73,9 +43,9 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
let profile = this.profiles.get(pubkey); let profile = this.profiles.get(pubkey);
if (!profile) { if (!profile) {
const user = await db.users.get({ pubkey }); const user = await this.db.getCacheUser(pubkey);
if (user) { if (user) {
profile = user.profile; profile = user.profile as NDKUserProfile;
this.profiles.set(pubkey, profile); this.profiles.set(pubkey, profile);
} }
} }
@ -126,7 +96,7 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
if (event.isParamReplaceable()) { if (event.isParamReplaceable()) {
const replaceableId = `${event.kind}:${event.pubkey}:${event.tagId()}`; const replaceableId = `${event.kind}:${event.pubkey}:${event.tagId()}`;
const existingEvent = await db.events.where({ id: replaceableId }).first(); const existingEvent = await this.db.getCacheEvent(replaceableId);
if ( if (
existingEvent && existingEvent &&
event.created_at && event.created_at &&
@ -137,7 +107,7 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
} }
if (addEvent) { if (addEvent) {
db.events.put({ this.db.setCacheEvent({
id: event.tagId(), id: event.tagId(),
pubkey: event.pubkey, pubkey: event.pubkey,
content: event.content, content: event.content,
@ -153,7 +123,7 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
event.tags.forEach((tag) => { event.tags.forEach((tag) => {
if (tag[0].length !== 1) return; if (tag[0].length !== 1) return;
db.eventTags.put({ this.db.setCacheEventTag({
id: `${event.id}:${tag[0]}:${tag[1]}`, id: `${event.id}:${tag[0]}:${tag[1]}`,
eventId: event.id, eventId: event.id,
tag: tag[0], tag: tag[0],
@ -182,9 +152,9 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
if (hasAllKeys && filter.authors) { if (hasAllKeys && filter.authors) {
for (const pubkey of filter.authors) { for (const pubkey of filter.authors) {
const events = await db.events.where({ pubkey }).toArray(); const events = await this.db.getCacheEventsByPubkey(pubkey);
for (const event of events) { for (const event of events) {
let rawEvent; let rawEvent: NostrEvent;
try { try {
rawEvent = JSON.parse(event.event); rawEvent = JSON.parse(event.event);
} catch (e) { } catch (e) {
@ -218,9 +188,9 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
if (hasAllKeys && filter.kinds) { if (hasAllKeys && filter.kinds) {
for (const kind of filter.kinds) { for (const kind of filter.kinds) {
const events = await db.events.where({ kind }).toArray(); const events = await this.db.getCacheEventsByKind(kind);
for (const event of events) { for (const event of events) {
let rawEvent; let rawEvent: NostrEvent;
try { try {
rawEvent = JSON.parse(event.event); rawEvent = JSON.parse(event.event);
} catch (e) { } catch (e) {
@ -252,10 +222,10 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
if (hasAllKeys && filter.ids) { if (hasAllKeys && filter.ids) {
for (const id of filter.ids) { for (const id of filter.ids) {
const event = await db.events.where({ id }).first(); const event = await this.db.getCacheEvent(id);
if (!event) continue; if (!event) continue;
let rawEvent; let rawEvent: NostrEvent;
try { try {
rawEvent = JSON.parse(event.event); rawEvent = JSON.parse(event.event);
} catch (e) { } catch (e) {
@ -295,10 +265,10 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
for (const author of filter.authors) { for (const author of filter.authors) {
for (const dTag of filter['#d']) { for (const dTag of filter['#d']) {
const replaceableId = `${kind}:${author}:${dTag}`; const replaceableId = `${kind}:${author}:${dTag}`;
const event = await db.events.where({ id: replaceableId }).first(); const event = await this.db.getCacheEvent(replaceableId);
if (!event) continue; if (!event) continue;
let rawEvent; let rawEvent: NostrEvent;
try { try {
rawEvent = JSON.parse(event.event); rawEvent = JSON.parse(event.event);
} catch (e) { } catch (e) {
@ -335,10 +305,10 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
if (filter.kinds && filter.authors) { if (filter.kinds && filter.authors) {
for (const kind of filter.kinds) { for (const kind of filter.kinds) {
for (const author of filter.authors) { for (const author of filter.authors) {
const events = await db.events.where({ kind, pubkey: author }).toArray(); const events = await this.db.getCacheEventsByKindAndAuthor(kind, author);
for (const event of events) { for (const event of events) {
let rawEvent; let rawEvent: NostrEvent;
try { try {
rawEvent = JSON.parse(event.event); rawEvent = JSON.parse(event.event);
} catch (e) { } catch (e) {
@ -400,12 +370,12 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
} }
for (const value of values) { for (const value of values) {
const eventTags = await db.eventTags.where({ tagValue: tag + value }).toArray(); const eventTags = await this.db.getCacheEventTagsByTagValue(tag + value);
if (!eventTags.length) continue; if (!eventTags.length) continue;
const eventIds = eventTags.map((t) => t.eventId); const eventIds = eventTags.map((t) => t.eventId);
const events = await db.events.where('id').anyOf(eventIds).toArray(); const events = await this.db.getCacheEvents(eventIds);
for (const event of events) { for (const event of events) {
let rawEvent; let rawEvent;
try { try {
@ -441,13 +411,13 @@ export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
profiles.push({ profiles.push({
pubkey, pubkey,
profile, profile: JSON.stringify(profile),
createdAt: Date.now(), createdAt: Date.now(),
}); });
} }
if (profiles.length) { if (profiles.length) {
await db.users.bulkPut(profiles); await this.db.setCacheProfiles(profiles);
} }
this.dirtyProfiles.clear(); this.dirtyProfiles.clear();

View File

@ -1,11 +1,11 @@
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie';
import { ndkAdapter } from '@nostr-fetch/adapter-ndk'; import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
import { message } from '@tauri-apps/plugin-dialog'; import { message } from '@tauri-apps/plugin-dialog';
import { fetch } from '@tauri-apps/plugin-http'; import { fetch } from '@tauri-apps/plugin-http';
import { NostrFetcher } from 'nostr-fetch'; import { NostrFetcher } from 'nostr-fetch';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import NDKCacheAdapterTauri from '@libs/ndk/cache';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
export const NDKInstance = () => { export const NDKInstance = () => {
@ -77,7 +77,7 @@ export const NDKInstance = () => {
const outboxSetting = await db.getSettingValue('outbox'); const outboxSetting = await db.getSettingValue('outbox');
const explicitRelayUrls = await getExplicitRelays(); const explicitRelayUrls = await getExplicitRelays();
const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'lume_ndkcache' }); const dexieAdapter = new NDKCacheAdapterTauri(db);
const instance = new NDK({ const instance = new NDK({
explicitRelayUrls, explicitRelayUrls,
cacheAdapter: dexieAdapter, cacheAdapter: dexieAdapter,

View File

@ -6,7 +6,15 @@ import Database from '@tauri-apps/plugin-sql';
import { FULL_RELAYS } from '@stores/constants'; import { FULL_RELAYS } from '@stores/constants';
import { rawEvent } from '@utils/transform'; import { rawEvent } from '@utils/transform';
import { Account, DBEvent, Relays, Widget } from '@utils/types'; import type {
Account,
DBEvent,
NDKCacheEvent,
NDKCacheEventTag,
NDKCacheUser,
Relays,
Widget,
} from '@utils/types';
export class LumeStorage { export class LumeStorage {
public db: Database; public db: Database;
@ -37,6 +45,115 @@ export class LumeStorage {
return await invoke('secure_remove', { key }); return await invoke('secure_remove', { key });
} }
public async getCacheUser(pubkey: string) {
const results: Array<NDKCacheUser> = await this.db.select(
'SELECT * FROM ndk_users WHERE pubkey = $1 ORDER BY pubkey DESC LIMIT 1;',
[pubkey]
);
if (results.length < 1) return null;
if (typeof results[0].profile === 'string')
results[0].profile = JSON.parse(results[0].profile);
return results[0];
}
public async getCacheEvent(id: string) {
const results: Array<NDKCacheEvent> = await this.db.select(
'SELECT * FROM ndk_events WHERE id = $1 ORDER BY id DESC LIMIT 1;',
[id]
);
if (results.length < 1) return null;
return results[0];
}
public async getCacheEvents(ids: string[]) {
const idsArr = `'${ids.join("','")}'`;
const results: Array<NDKCacheEvent> = await this.db.select(
`SELECT * FROM ndk_events WHERE id IN (${idsArr}) ORDER BY id;`
);
if (results.length < 1) return [];
return results;
}
public async getCacheEventsByPubkey(pubkey: string) {
const results: Array<NDKCacheEvent> = await this.db.select(
'SELECT * FROM ndk_events WHERE pubkey = $1 ORDER BY id;',
[pubkey]
);
if (results.length < 1) return [];
return results;
}
public async getCacheEventsByKind(kind: number) {
const results: Array<NDKCacheEvent> = await this.db.select(
'SELECT * FROM ndk_events WHERE kind = $1 ORDER BY id;',
[kind]
);
if (results.length < 1) return [];
return results;
}
public async getCacheEventsByKindAndAuthor(kind: number, pubkey: string) {
const results: Array<NDKCacheEvent> = await this.db.select(
'SELECT * FROM ndk_events WHERE kind = $1 AND pubkey = $2 ORDER BY id;',
[kind, pubkey]
);
if (results.length < 1) return [];
return results;
}
public async getCacheEventTagsByTagValue(tagValue: string) {
const results: Array<NDKCacheEventTag> = await this.db.select(
'SELECT * FROM ndk_eventtags WHERE tagValue = $1 ORDER BY id;',
[tagValue]
);
if (results.length < 1) return [];
return results;
}
public async setCacheEvent({
id,
pubkey,
content,
kind,
createdAt,
relay,
event,
}: NDKCacheEvent) {
return await this.db.execute(
'INSERT OR IGNORE INTO ndk_events (id, pubkey, content, kind, createdAt, relay, event) VALUES ($1, $2, $3, $4, $5, $6, $7);',
[id, pubkey, content, kind, createdAt, relay, event]
);
}
public async setCacheEventTag({ id, eventId, tag, value, tagValue }: NDKCacheEventTag) {
return await this.db.execute(
'INSERT OR IGNORE INTO ndk_eventtags (id, eventId, tag, value, tagValue) VALUES ($1, $2, $3, $4, $5);',
[id, eventId, tag, value, tagValue]
);
}
public async setCacheProfiles(profiles: Array<NDKCacheUser>) {
return await Promise.all(
profiles.map(
async (profile) =>
await this.db.execute(
'INSERT OR IGNORE INTO ndk_users (pubkey, profile, createdAt) VALUES ($1, $2, $3);',
[profile.pubkey, profile.profile, profile.createdAt]
)
)
);
}
public async checkAccount() { public async checkAccount() {
const result: Array<{ total: string }> = await this.db.select( const result: Array<{ total: string }> = await this.db.select(
'SELECT COUNT(*) AS "total" FROM accounts;' 'SELECT COUNT(*) AS "total" FROM accounts;'

View File

@ -189,7 +189,7 @@ export const User = memo(function User({
return ( return (
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div className="h-11 w-11 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" /> <div className="h-11 w-11 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
<div className="flex w-full flex-col items-start"> <div className="flex w-full flex-col items-start gap-1">
<div className="h-4 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" /> <div className="h-4 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" /> <div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
</div> </div>

View File

@ -19,19 +19,18 @@ export function EventLoader({ firstTime }: { firstTime: boolean }) {
useEffect(() => { useEffect(() => {
async function getEvents() { async function getEvents() {
const events = await getAllEventsSinceLastLogin(); const events = await getAllEventsSinceLastLogin();
console.log('total new events has found: ', events.data.length); console.log('total new events has found: ', events.length);
const promises = await Promise.all( if (events) {
events.data.map(async (event) => await db.createEvent(event))
);
if (promises) {
setProgress(100); setProgress(100);
setIsFetched(); setIsFetched();
// invalidate queries // invalidate queries
queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ['local-network-widget'] queryKey: ['local-network-widget'],
}); });
// update last login time, use for next fetch
await db.updateLastLogin(); await db.updateLastLogin();
} }
} }

View File

@ -10,6 +10,7 @@ import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets'; import { WidgetWrapper } from '@shared/widgets';
import { useActivities } from '@stores/activities'; import { useActivities } from '@stores/activities';
import { useWidgets } from '@stores/widgets';
import { useNostr } from '@utils/hooks/useNostr'; import { useNostr } from '@utils/hooks/useNostr';
import { Widget } from '@utils/types'; import { Widget } from '@utils/types';
@ -23,6 +24,8 @@ export function LocalNotificationWidget({ params }: { params: Widget }) {
state.setActivities, state.setActivities,
]); ]);
const isFetched = useWidgets((state) => state.isFetched);
const renderEvent = useCallback( const renderEvent = useCallback(
(event: NDKEvent) => { (event: NDKEvent) => {
if (event.pubkey === db.account.pubkey) return null; if (event.pubkey === db.account.pubkey) return null;
@ -37,8 +40,8 @@ export function LocalNotificationWidget({ params }: { params: Widget }) {
setActivities(events); setActivities(events);
} }
getActivities(); if (isFetched) getActivities();
}, []); }, [isFetched]);
return ( return (
<WidgetWrapper> <WidgetWrapper>

View File

@ -3,14 +3,12 @@ import { useQuery } from '@tanstack/react-query';
import { AddressPointer } from 'nostr-tools/lib/types/nip19'; import { AddressPointer } from 'nostr-tools/lib/types/nip19';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
export function useEvent( export function useEvent(
id: undefined | string, id: undefined | string,
naddr?: undefined | AddressPointer, naddr?: undefined | AddressPointer,
embed?: undefined | string embed?: undefined | string
) { ) {
const { db } = useStorage();
const { ndk } = useNDK(); const { ndk } = useNDK();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['event', id], queryKey: ['event', id],
@ -33,20 +31,16 @@ export function useEvent(
return event; return event;
} }
// get event from db // get event from relay
const dbEvent = await db.getEventByID(id);
if (dbEvent) return dbEvent;
// get event from relay if event in db not present
const event = await ndk.fetchEvent(id); const event = await ndk.fetchEvent(id);
if (!event) return Promise.reject(new Error('event not found')); if (!event) return Promise.reject(new Error('event not found'));
await db.createEvent(event);
return event; return event;
}, },
enabled: !!ndk,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Infinity,
}); });
return { status, data }; return { status, data };

View File

@ -186,10 +186,9 @@ export function useNostr() {
{ since: since } { since: since }
)) as unknown as NDKEvent[]; )) as unknown as NDKEvent[];
return { status: 'ok', message: 'fetch completed', data: events }; return events;
} catch (e) { } catch (e) {
console.error('prefetch events failed, error: ', e); console.error('prefetch events failed, error: ', e);
return { status: 'failed', message: e };
} }
}; };

View File

@ -22,12 +22,10 @@ export function useProfile(pubkey: string, embed?: string) {
return await user.fetchProfile(); return await user.fetchProfile();
}, },
enabled: !!ndk,
staleTime: Infinity, staleTime: Infinity,
refetchOnMount: false, refetchOnMount: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnReconnect: false, refetchOnReconnect: false,
retry: 2,
}); });
return { status, user, error }; return { status, user, error };

24
src/utils/types.d.ts vendored
View File

@ -115,3 +115,27 @@ export interface Resources {
title: string; title: string;
data: Array<Resource>; data: Array<Resource>;
} }
export interface NDKCacheUser {
pubkey: string;
profile: string | NDKUserProfile;
createdAt: number;
}
export interface NDKCacheEvent {
id: string;
pubkey: string;
content: string;
kind: number;
createdAt: number;
relay: string;
event: string;
}
export interface NDKCacheEventTag {
id: string;
eventId: string;
tag: string;
value: string;
tagValue: string;
}