From 8003d39aa1c815dac58a3e28c6f86696d2d933e0 Mon Sep 17 00:00:00 2001 From: Carlo Sala Date: Tue, 27 May 2025 12:38:32 +0200 Subject: [PATCH] refactor: migrate metadata storage to idb (#51) --- package.json | 1 + pnpm-lock.yaml | 3 ++ src/state/chains/chain.state.ts | 92 +++++++++++++++++++-------------- 3 files changed, 58 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index be307a5..88f4d66 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.1.1", + "idb-keyval": "^6.2.2", "lucide-react": "^0.511.0", "polkadot-api": "^1.12.0", "react": "^19.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c560969..6a8a5bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,6 +106,9 @@ importers: cmdk: specifier: 1.1.1 version: 1.1.1(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + idb-keyval: + specifier: ^6.2.2 + version: 6.2.2 lucide-react: specifier: ^0.511.0 version: 0.511.0(react@19.1.0) diff --git a/src/state/chains/chain.state.ts b/src/state/chains/chain.state.ts index 62f20ae..254f723 100644 --- a/src/state/chains/chain.state.ts +++ b/src/state/chains/chain.state.ts @@ -1,3 +1,4 @@ +import { get, update } from "idb-keyval" import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" import { getObservableClient } from "@polkadot-api/observable-client" import { @@ -16,6 +17,7 @@ import { filter, finalize, firstValueFrom, + from, map, NEVER, of, @@ -141,41 +143,54 @@ export const selectedChain$ = state( const selectedSource$ = selectedChain$.pipe(switchMap(getChainSource)) -const getMetadataCache = () => { - const cached = localStorage.getItem(`metadata-cache`) - return new Map( - cached ? JSON.parse(cached) : [], - ) -} -const setMetadataCache = ( - cache: Map, -) => { - localStorage.setItem("metadata-cache", JSON.stringify([...cache.entries()])) -} +// TODO: 2025-05-27 +// remove old localStorage clear after a while +localStorage.removeItem("metadata-cache") -const getMetadata = (codeHash: string) => { - const cache = getMetadataCache() - const metadata = cache.get(codeHash) - if (!metadata) return of(null) - // update usage time - metadata.time = Date.now() - setMetadataCache(cache) - return of(fromHex(metadata.data)) -} -const setMetadataFactory = - (id: string) => (codeHash: string, data: Uint8Array) => { - const cached = getMetadataCache() - const old = [...cached.entries()].find(([, v]) => v.id === id) - // remove if there has been a runtime upgrade +type MetadataCache = Map +const IDB_KEY = "metadata-cache" +const MAX_CACHE_ENTRIES = 3 + +const addEntryToCache = ( + codeHash: string, + entry: { id: string; time: number; data: HexString }, +) => + update(IDB_KEY, (cached) => { + cached ??= new Map() + const old = [...cached.entries()].find(([, v]) => v.id === entry.id) if (old) cached.delete(old[0]) - cached.set(codeHash, { id, time: Date.now(), data: toHex(data) }) - if (cached.size > 3) { - const oldest = [...cached.entries()].reduce((a, b) => - a[1].time < b[1].time ? a : b, - )[0] - cached.delete(oldest) - } - setMetadataCache(cached) + cached.set(codeHash, entry) + ;[...cached.entries()] + .sort(([, a], [, b]) => b.time - a.time) + .slice(MAX_CACHE_ENTRIES) + .forEach(([k]) => { + cached.delete(k) + }) + return cached + }) + +// TODO: ATM chopsticks hash is not implemented +// avoid cache in this situation +// remove `| null` when it is +const getMetadata = (codeHash: string | null) => + codeHash + ? from(get(IDB_KEY)).pipe( + map((cache) => { + const entry = cache?.get(codeHash) + if (!entry) return null + addEntryToCache(codeHash, { ...entry, time: Date.now() }) + return fromHex(entry.data) + }), + ) + : of(null) + +// TODO: ATM chopsticks hash is not implemented +// avoid cache in this situation +// remove `| null` when it is +const setMetadataFactory = + (id: string) => (codeHash: string | null, data: Uint8Array) => { + if (codeHash) + addEntryToCache(codeHash, { id, time: Date.now(), data: toHex(data) }) } export const chainClient$ = state( @@ -220,11 +235,12 @@ const uncachedRuntimeCtx$ = chainClient$.pipeState( ) export const runtimeCtx$ = chainClient$.pipeState( - switchMap(({ id }) => { - const cached = [...getMetadataCache().entries()].find( - ([, v]) => v.id === id, - ) - + switchMap(({ id }) => + get(IDB_KEY).then((cache) => + cache ? [...cache.entries()].find(([, v]) => v.id === id) : undefined, + ), + ), + switchMap((cached) => { if (cached) { const metadata = unifyMetadata(decAnyMetadata(cached[1].data)) const lookup = getLookupFn(metadata)