feat: added caching using browsers built in index db #96

Merged
b merged 9 commits from issue-69 into staging 2024-05-31 11:38:20 +00:00
7 changed files with 206 additions and 70 deletions
Showing only changes of commit 2b9617232e - Show all commits

View File

@ -14,71 +14,130 @@ import { NostrController } from '.'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { queryNip05 } from '../utils' import { queryNip05 } from '../utils'
import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk' import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
import { EventEmitter } from 'tseep'
import { localCache } from '../services'
export class MetadataController { export class MetadataController extends EventEmitter {
private nostrController: NostrController private nostrController: NostrController
private specialMetadataRelay = 'wss://purplepag.es' private specialMetadataRelay = 'wss://purplepag.es'
constructor() { constructor() {
super()
this.nostrController = NostrController.getInstance() this.nostrController = NostrController.getInstance()
} }
public getEmptyMetadataEvent = (): Event => { /**
return { * Asynchronously checks for more recent metadata events authored by a specific key.
content: '', * If a more recent metadata event is found, it is handled and returned.
created_at: new Date().valueOf(), * If no more recent event is found, the current event is returned.
id: '', * @param hexKey The hexadecimal key of the author to filter metadata events.
kind: 0, * @param currentEvent The current metadata event, if any, to compare with newer events.
pubkey: '', * @returns A promise resolving to the most recent metadata event found, or null if none is found.
sig: '', */
tags: [] private async checkForMoreRecentMetadata(
} hexKey: string,
} currentEvent: Event | null
): Promise<Event | null> {
public findMetadata = async (hexKey: string) => { // Define the event filter to only include metadata events authored by the given key
const eventFilter: Filter = { const eventFilter: Filter = {
kinds: [kinds.Metadata], kinds: [kinds.Metadata], // Only metadata events
authors: [hexKey] authors: [hexKey] // Authored by the specified key
} }
const pool = new SimplePool() const pool = new SimplePool()
// Try to get the metadata event from a special relay (wss://purplepag.es)
const metadataEvent = await pool const metadataEvent = await pool
.get([this.specialMetadataRelay], eventFilter) .get([this.specialMetadataRelay], eventFilter)
.catch((err) => { .catch((err) => {
console.error(err) console.error(err) // Log any errors
return null return null // Return null if an error occurs
}) })
// If a valid metadata event is found from the special relay
if ( if (
metadataEvent && metadataEvent &&
validateEvent(metadataEvent) && validateEvent(metadataEvent) && // Validate the event
verifyEvent(metadataEvent) verifyEvent(metadataEvent) // Verify the event's authenticity
) { ) {
return metadataEvent // If there's no current event or the new metadata event is more recent
if (!currentEvent || metadataEvent.created_at > currentEvent.created_at) {
// Handle the new metadata event
this.handleNewMetadataEvent(metadataEvent)
return metadataEvent
}
} }
// If no valid metadata event is found from the special relay, get the most popular relays
const mostPopularRelays = await this.nostrController.getMostPopularRelays() const mostPopularRelays = await this.nostrController.getMostPopularRelays()
// Query the most popular relays for metadata events
const events = await pool const events = await pool
.querySync(mostPopularRelays, eventFilter) .querySync(mostPopularRelays, eventFilter)
.catch((err) => { .catch((err) => {
console.error(err) console.error(err) // Log any errors
return null // Return null if an error occurs
return null
}) })
// If events are found from the popular relays
if (events && events.length) { if (events && events.length) {
events.sort((a, b) => b.created_at - a.created_at) events.sort((a, b) => b.created_at - a.created_at) // Sort events by creation date (descending)
// Iterate through the events
for (const event of events) { for (const event of events) {
if (validateEvent(event) && verifyEvent(event)) { // If the event is valid, authentic, and more recent than the current event
if (
validateEvent(event) &&
verifyEvent(event) &&
(!currentEvent || event.created_at > currentEvent.created_at)
) {
// Handle the new metadata event
this.handleNewMetadataEvent(event)
return event return event
} }
} }
} }
throw new Error('Mo metadata found.') return currentEvent // Return the current event if no newer event is found
}
/**
* Handle new metadata events and emit them to subscribers
*/
private async handleNewMetadataEvent(event: VerifiedEvent) {
// update the event in local cache
localCache.addUserMetadata(event)
// Emit the event to subscribers.
this.emit(event.pubkey, event.kind, event)
}
/**
* Finds metadata for a given hexadecimal key.
*
* @param hexKey - The hexadecimal key to search for metadata.
* @returns A promise that resolves to the metadata event.
*/
public findMetadata = async (hexKey: string): Promise<Event | null> => {
// Attempt to retrieve the metadata event from the local cache
const cachedMetadataEvent = await localCache.getUserMetadata(hexKey)
// If cached metadata is found, check its validity
if (cachedMetadataEvent) {
const oneDayInMS = 24 * 60 * 60 * 1000 // Number of milliseconds in one day
// Check if the cached metadata is older than one day
if (Date.now() - cachedMetadataEvent.cachedAt > oneDayInMS) {
// If older than one day, find the metadata from relays in background
this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event)
}
// Return the cached metadata event
return cachedMetadataEvent.event
}
// If no cached metadata is found, retrieve it from relays
return this.checkForMoreRecentMetadata(hexKey, null)
} }
public findRelayListMetadata = async (hexKey: string) => { public findRelayListMetadata = async (hexKey: string) => {
@ -142,7 +201,7 @@ export class MetadataController {
throw new Error('No relay list metadata found.') throw new Error('No relay list metadata found.')
} }
public extractProfileMetadataContent = (event: VerifiedEvent) => { public extractProfileMetadataContent = (event: Event) => {
try { try {
if (!event.content) return {} if (!event.content) return {}
return JSON.parse(event.content) as ProfileMetadata return JSON.parse(event.content) as ProfileMetadata
@ -175,6 +234,7 @@ export class MetadataController {
.publishEvent(signedMetadataEvent, [this.specialMetadataRelay]) .publishEvent(signedMetadataEvent, [this.specialMetadataRelay])
.then((relays) => { .then((relays) => {
toast.success(`Metadata event published on: ${relays.join('\n')}`) toast.success(`Metadata event published on: ${relays.join('\n')}`)
this.handleNewMetadataEvent(signedMetadataEvent as VerifiedEvent)
}) })
.catch((err) => { .catch((err) => {
toast.error(err.message) toast.error(err.message)
@ -193,6 +253,8 @@ export class MetadataController {
userRelays.push(...relaySet.write) userRelays.push(...relaySet.write)
} else { } else {
const metadata = await this.findMetadata(hexKey) const metadata = await this.findMetadata(hexKey)
if (!metadata) return null
const metadataContent = this.extractProfileMetadataContent(metadata) const metadataContent = this.extractProfileMetadataContent(metadata)
if (metadataContent?.nip05) { if (metadataContent?.nip05) {
@ -306,4 +368,16 @@ export class MetadataController {
} }
public validate = (event: Event) => validateEvent(event) && verifyEvent(event) public validate = (event: Event) => validateEvent(event) && verifyEvent(event)
public getEmptyMetadataEvent = (): Event => {
return {
content: '',
created_at: new Date().valueOf(),
id: '',
kind: 0,
pubkey: '',
sig: '',
tags: []
}
}
} }

View File

@ -18,8 +18,7 @@ import { MetadataController, NostrController } from '../controllers'
import { LoginMethods } from '../store/auth/types' import { LoginMethods } from '../store/auth/types'
import { setUserRobotImage } from '../store/userRobotImage/action' import { setUserRobotImage } from '../store/userRobotImage/action'
import { State } from '../store/rootReducer' import { State } from '../store/rootReducer'
import { Event, kinds } from 'nostr-tools'
const metadataController = new MetadataController()
export const MainLayout = () => { export const MainLayout = () => {
const dispatch: Dispatch = useDispatch() const dispatch: Dispatch = useDispatch()
@ -27,6 +26,8 @@ export const MainLayout = () => {
const authState = useSelector((state: State) => state.auth) const authState = useSelector((state: State) => state.auth)
useEffect(() => { useEffect(() => {
const metadataController = new MetadataController()
const logout = () => { const logout = () => {
dispatch( dispatch(
setAuthState({ setAuthState({
@ -68,6 +69,20 @@ export const MainLayout = () => {
nostrController.createNsecBunkerSigner(usersPubkey) nostrController.createNsecBunkerSigner(usersPubkey)
}) })
} }
const handleMetadataEvent = (event: Event) => {
dispatch(setMetadataEvent(event))
}
metadataController.on(usersPubkey, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(event)
}
})
metadataController.findMetadata(usersPubkey).then((metadataEvent) => {
if (metadataEvent) handleMetadataEvent(metadataEvent)
})
} }
} }

View File

@ -49,6 +49,7 @@ import type { Identifier, XYCoord } from 'dnd-core'
import { useDrag, useDrop } from 'react-dnd' import { useDrag, useDrop } from 'react-dnd'
import saveAs from 'file-saver' import saveAs from 'file-saver'
import CopyModal from '../../components/copyModal' import CopyModal from '../../components/copyModal'
import { Event, kinds } from 'nostr-tools'
export const CreatePage = () => { export const CreatePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -517,16 +518,26 @@ const DisplayUser = ({
if (!(user.pubkey in metadata)) { if (!(user.pubkey in metadata)) {
const metadataController = new MetadataController() const metadataController = new MetadataController()
const handleMetadataEvent = (event: Event) => {
const metadataContent =
metadataController.extractProfileMetadataContent(event)
if (metadataContent)
setMetadata((prev) => ({
...prev,
[user.pubkey]: metadataContent
}))
}
metadataController.on(user.pubkey, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(event)
}
})
metadataController metadataController
.findMetadata(user.pubkey) .findMetadata(user.pubkey)
.then((metadataEvent) => { .then((metadataEvent) => {
const metadataContent = if (metadataEvent) handleMetadataEvent(metadataEvent)
metadataController.extractProfileMetadataContent(metadataEvent)
if (metadataContent)
setMetadata((prev) => ({
...prev,
[user.pubkey]: metadataContent
}))
}) })
.catch((err) => { .catch((err) => {
console.error( console.error(

View File

@ -2,7 +2,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import EditIcon from '@mui/icons-material/Edit' import EditIcon from '@mui/icons-material/Edit'
import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material' import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material'
import { truncate } from 'lodash' import { truncate } from 'lodash'
import { VerifiedEvent, nip19 } from 'nostr-tools' import { Event, VerifiedEvent, kinds, nip19 } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { Link, useNavigate, useParams } from 'react-router-dom' import { Link, useNavigate, useParams } from 'react-router-dom'
@ -75,6 +75,20 @@ export const ProfilePage = () => {
if (pubkey) { if (pubkey) {
const getMetadata = async (pubkey: string) => { const getMetadata = async (pubkey: string) => {
const handleMetadataEvent = (event: Event) => {
const metadataContent =
metadataController.extractProfileMetadataContent(event)
if (metadataContent) {
setProfileMetadata(metadataContent)
}
}
metadataController.on(pubkey, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(event)
}
})
const metadataEvent = await metadataController const metadataEvent = await metadataController
.findMetadata(pubkey) .findMetadata(pubkey)
.catch((err) => { .catch((err) => {
@ -82,13 +96,7 @@ export const ProfilePage = () => {
return null return null
}) })
if (metadataEvent) { if (metadataEvent) handleMetadataEvent(metadataEvent)
const metadataContent =
metadataController.extractProfileMetadataContent(metadataEvent)
if (metadataContent) {
setProfileMetadata(metadataContent)
}
}
setIsLoading(false) setIsLoading(false)
} }

View File

@ -11,7 +11,7 @@ import {
Typography, Typography,
useTheme useTheme
} from '@mui/material' } from '@mui/material'
import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' import { UnsignedEvent, nip19, kinds, VerifiedEvent, Event } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { Link, useParams } from 'react-router-dom' import { Link, useParams } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
@ -95,6 +95,20 @@ export const ProfileSettingsPage = () => {
if (pubkey) { if (pubkey) {
const getMetadata = async (pubkey: string) => { const getMetadata = async (pubkey: string) => {
const handleMetadataEvent = (event: Event) => {
const metadataContent =
metadataController.extractProfileMetadataContent(event)
if (metadataContent) {
setProfileMetadata(metadataContent)
}
}
metadataController.on(pubkey, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(event)
}
})
const metadataEvent = await metadataController const metadataEvent = await metadataController
.findMetadata(pubkey) .findMetadata(pubkey)
.catch((err) => { .catch((err) => {
@ -102,13 +116,7 @@ export const ProfileSettingsPage = () => {
return null return null
}) })
if (metadataEvent) { if (metadataEvent) handleMetadataEvent(metadataEvent)
const metadataContent =
metadataController.extractProfileMetadataContent(metadataEvent)
if (metadataContent) {
setProfileMetadata(metadataContent)
}
}
setIsLoading(false) setIsLoading(false)
} }

View File

@ -20,7 +20,7 @@ import saveAs from 'file-saver'
import JSZip from 'jszip' import JSZip from 'jszip'
import _ from 'lodash' import _ from 'lodash'
import { MuiFileInput } from 'mui-file-input' import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools' import { Event, kinds, verifyEvent } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useNavigate, useSearchParams } from 'react-router-dom' import { useNavigate, useSearchParams } from 'react-router-dom'
@ -846,17 +846,27 @@ const DisplayMeta = ({
hexKeys.forEach((key) => { hexKeys.forEach((key) => {
if (!(key in metadata)) { if (!(key in metadata)) {
const handleMetadataEvent = (event: Event) => {
const metadataContent =
metadataController.extractProfileMetadataContent(event)
if (metadataContent)
setMetadata((prev) => ({
...prev,
[key]: metadataContent
}))
}
metadataController.on(key, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(event)
}
})
metadataController metadataController
.findMetadata(key) .findMetadata(key)
.then((metadataEvent) => { .then((metadataEvent) => {
const metadataContent = if (metadataEvent) handleMetadataEvent(metadataEvent)
metadataController.extractProfileMetadataContent(metadataEvent)
if (metadataContent)
setMetadata((prev) => ({
...prev,
[key]: metadataContent
}))
}) })
.catch((err) => { .catch((err) => {
console.error(`error occurred in finding metadata for: ${key}`, err) console.error(`error occurred in finding metadata for: ${key}`, err)

View File

@ -10,7 +10,7 @@ import {
} from '@mui/material' } from '@mui/material'
import JSZip from 'jszip' import JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input' import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools' import { Event, kinds, verifyEvent } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
@ -107,16 +107,26 @@ export const VerifyPage = () => {
const pubkey = npubToHex(user)! const pubkey = npubToHex(user)!
if (!(pubkey in metadata)) { if (!(pubkey in metadata)) {
const handleMetadataEvent = (event: Event) => {
const metadataContent =
metadataController.extractProfileMetadataContent(event)
if (metadataContent)
setMetadata((prev) => ({
...prev,
[pubkey]: metadataContent
}))
}
metadataController.on(pubkey, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(event)
}
})
metadataController metadataController
.findMetadata(pubkey) .findMetadata(pubkey)
.then((metadataEvent) => { .then((metadataEvent) => {
const metadataContent = if (metadataEvent) handleMetadataEvent(metadataEvent)
metadataController.extractProfileMetadataContent(metadataEvent)
if (metadataContent)
setMetadata((prev) => ({
...prev,
[pubkey]: metadataContent
}))
}) })
.catch((err) => { .catch((err) => {
console.error( console.error(