diff --git a/src/ageAssurance/data.tsx b/src/ageAssurance/data.tsx index ad431e6c9b..f2d934db17 100644 --- a/src/ageAssurance/data.tsx +++ b/src/ageAssurance/data.tsx @@ -45,7 +45,7 @@ const qc = new QueryClient({ }, }) const persister = createAsyncStoragePersister({ - storage: createPersistedQueryStorage('age_assurance'), + storage: createPersistedQueryStorage('age_assurance_cache'), key: 'age-assurance-query-client', }) const [, cacheHydrationPromise] = persistQueryClient({ diff --git a/src/lib/persisted-query-storage.ts b/src/lib/persisted-query-storage.ts index 916f6d17df..4177d11454 100644 --- a/src/lib/persisted-query-storage.ts +++ b/src/lib/persisted-query-storage.ts @@ -1,10 +1,4 @@ -/** - * Persisted Query Storage - Native Implementation (MMKV) - * - * This module provides a storage abstraction for persisting react-query cache. - * On native platforms, it uses MMKV for high-performance synchronous storage. - */ -import {MMKV} from '@bsky.app/react-native-mmkv' +import {create as createArchiveDB} from '#/storage/archive/db' /** * Interface for async storage compatible with @tanstack/query-async-storage-persister @@ -23,17 +17,16 @@ export interface PersistedQueryStorage { * @param id - Unique identifier for this storage instance (used as MMKV store id) */ export function createPersistedQueryStorage(id: string): PersistedQueryStorage { - const mmkv = new MMKV({id}) - + const store = createArchiveDB({id}) return { getItem: async (key: string): Promise => { - return mmkv.getString(key) ?? null + return (await store.get(key)) ?? null }, setItem: async (key: string, value: string): Promise => { - mmkv.set(key, value) + await store.set(key, value) }, removeItem: async (key: string): Promise => { - mmkv.delete(key) + await store.delete(key) }, } } diff --git a/src/lib/persisted-query-storage.web.ts b/src/lib/persisted-query-storage.web.ts deleted file mode 100644 index 520d867167..0000000000 --- a/src/lib/persisted-query-storage.web.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Persisted Query Storage - Web Implementation (IndexedDB) - * - * This module provides a storage abstraction for persisting react-query cache. - * On web platforms, it uses IndexedDB for efficient async storage. - */ -import {type DBSchema, type IDBPDatabase, openDB} from 'idb' - -/** - * Interface for async storage compatible with @tanstack/query-async-storage-persister - */ -export interface PersistedQueryStorage { - getItem: (key: string) => Promise - setItem: (key: string, value: string) => Promise - removeItem: (key: string) => Promise -} - -interface PersistedQueryDB extends DBSchema { - queries: { - key: string - value: string - } -} - -const DB_VERSION = 1 -const STORE_NAME = 'queries' - -const dbCache = new Map>>() - -function getDB(dbName: string): Promise> { - let dbPromise = dbCache.get(dbName) - if (!dbPromise) { - dbPromise = openDB(dbName, DB_VERSION, { - upgrade(db) { - if (!db.objectStoreNames.contains(STORE_NAME)) { - db.createObjectStore(STORE_NAME) - } - }, - }) - dbCache.set(dbName, dbPromise) - } - return dbPromise -} - -/** - * Creates an IndexedDB-based storage adapter for persisting react-query cache on web platforms. - * Each storage instance uses a separate IndexedDB database identified by the provided id. - * - * @param id - Unique identifier for this storage instance (used as database name) - */ -export function createPersistedQueryStorage(id: string): PersistedQueryStorage { - const dbName = `bsky_${id}` - - return { - getItem: async (key: string): Promise => { - try { - const db = await getDB(dbName) - const value = await db.get(STORE_NAME, key) - return value ?? null - } catch (e) { - console.error('Failed to get item from IndexedDB:', e) - return null - } - }, - setItem: async (key: string, value: string): Promise => { - try { - const db = await getDB(dbName) - await db.put(STORE_NAME, value, key) - } catch (e) { - console.error('Failed to set item in IndexedDB:', e) - } - }, - removeItem: async (key: string): Promise => { - try { - const db = await getDB(dbName) - await db.delete(STORE_NAME, key) - } catch (e) { - console.error('Failed to remove item from IndexedDB:', e) - } - }, - } -} diff --git a/src/lib/react-query.tsx b/src/lib/react-query.tsx index 1f04d81ba0..9753147ccc 100644 --- a/src/lib/react-query.tsx +++ b/src/lib/react-query.tsx @@ -6,10 +6,10 @@ import { PersistQueryClientProvider, type PersistQueryClientProviderProps, } from '@tanstack/react-query-persist-client' -import {SuperJSON} from 'superjson' import {createPersistedQueryStorage} from '#/lib/persisted-query-storage' import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' +import {PERSISTED_QUERY_ROOT} from '#/state/queries' import {IS_NATIVE, IS_WEB} from '#/env' declare global { @@ -18,18 +18,6 @@ declare global { } } -// any query keys in this array will be persisted to storage. -// keys are here for require cycle reasons ¯\_(ツ)_/¯ -export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info' -export const preferencesQueryKeyRoot = 'get-preferences' -export const pinnedFeedInfosQueryKeyRoot = 'pinned-feeds-infos' - -const STORED_CACHE_QUERY_KEY_ROOTS = [ - labelersDetailedInfoQueryKeyRoot, - preferencesQueryKeyRoot, - pinnedFeedInfosQueryKeyRoot, -] - async function checkIsOnline(): Promise { try { const controller = new AbortController() @@ -146,7 +134,8 @@ const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehyd { shouldDehydrateMutation: (_: any) => false, shouldDehydrateQuery: query => { - return STORED_CACHE_QUERY_KEY_ROOTS.includes(String(query.queryKey[0])) + const root = String(query.queryKey[0]) + return root === PERSISTED_QUERY_ROOT }, } @@ -187,13 +176,11 @@ function QueryProviderInner({ // Do not move the query client creation outside of this component. const [queryClient, _setQueryClient] = useState(() => createQueryClient()) const [persistOptions, _setPersistOptions] = useState(() => { - const storage = createPersistedQueryStorage('persisted_queries') + const storage = createPersistedQueryStorage('react-query-cache') const asyncPersister = createAsyncStoragePersister({ storage, key: 'queryClient-' + (currentDid ?? 'logged-out') + `-v${PERSIST_VERSION}`, - serialize: SuperJSON.stringify, - deserialize: SuperJSON.parse, }) return { persister: asyncPersister, diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 33e0dcca46..9af49361e0 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -20,10 +20,9 @@ import { } from '@tanstack/react-query' import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants' -import {pinnedFeedInfosQueryKeyRoot} from '#/lib/react-query' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' -import {STALE} from '#/state/queries' +import {PERSISTED_QUERY_ROOT, STALE} from '#/state/queries' import {RQKEY as listQueryKey} from '#/state/queries/list' import {usePreferencesQuery} from '#/state/queries/preferences' import {useAgent, useSession} from '#/state/session' @@ -419,6 +418,12 @@ const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = { contentMode: undefined, } +const createPinnedFeedInfosQueryKeyRoot = (...args: any[]) => [ + PERSISTED_QUERY_ROOT, + 'feed-info', + ...args, +] + export function usePinnedFeedsInfos() { const {hasSession} = useSession() const agent = useAgent() @@ -428,11 +433,10 @@ export function usePinnedFeedsInfos() { return useQuery({ staleTime: STALE.INFINITY, enabled: !isLoadingPrefs, - queryKey: [ - pinnedFeedInfosQueryKeyRoot, - (hasSession ? 'authed:' : 'unauthed:') + - pinnedItems.map(f => f.value).join(','), - ], + queryKey: createPinnedFeedInfosQueryKeyRoot( + 'pinned', + ...pinnedItems.map(f => f.value), + ), queryFn: async () => { if (!hasSession) { return [PWI_DISCOVER_FEED_STUB] @@ -536,7 +540,10 @@ export function useSavedFeeds() { return useQuery({ staleTime: STALE.INFINITY, enabled: !isLoadingPrefs, - queryKey: [pinnedFeedInfosQueryKeyRoot, ...savedItems], + queryKey: createPinnedFeedInfosQueryKeyRoot( + 'saved', + ...savedItems.map(f => f.value), + ), placeholderData: previousData => { return ( previousData || { diff --git a/src/state/queries/index.ts b/src/state/queries/index.ts index cdc47bf99c..666e07a453 100644 --- a/src/state/queries/index.ts +++ b/src/state/queries/index.ts @@ -18,3 +18,19 @@ export const STALE = { }, INFINITY: Infinity, } + +/** + * Root key for persisted queries. + * + * If the `querykey` of your query uses this at index 0, it will be + * persisted automatically by the `PersistQueryClientProvider` in + * `#/lib/react-query.tsx`. + * + * Be careful when using this, since it will change the query key and may + * break any cases where we call `invalidateQueries` or `refetchQueries` + * with the old key. + * + * Also, only use this for queries that are safe to persist between + * app launches (like user preferences). + */ +export const PERSISTED_QUERY_ROOT = 'PERSISTED' diff --git a/src/state/queries/labeler.ts b/src/state/queries/labeler.ts index 4eddb27f4e..d40f077b15 100644 --- a/src/state/queries/labeler.ts +++ b/src/state/queries/labeler.ts @@ -3,8 +3,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {z} from 'zod' import {MAX_LABELERS} from '#/lib/constants' -import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query' -import {STALE} from '#/state/queries' +import {PERSISTED_QUERY_ROOT, STALE} from '#/state/queries' import { preferencesQueryKey, usePreferencesQuery, @@ -23,8 +22,9 @@ export const labelersInfoQueryKey = (dids: string[]) => [ dids.slice().sort(), ] -export const labelersDetailedInfoQueryKey = (dids: string[]) => [ - labelersDetailedInfoQueryKeyRoot, +export const persistedLabelersDetailedInfoQueryKey = (dids: string[]) => [ + PERSISTED_QUERY_ROOT, + 'labelers-detailed-info', dids, ] @@ -65,7 +65,7 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) { const agent = useAgent() return useQuery({ enabled: !!dids.length, - queryKey: labelersDetailedInfoQueryKey(dids), + queryKey: persistedLabelersDetailedInfoQueryKey(dids), gcTime: 1000 * 60 * 60 * 6, // 6 hours staleTime: STALE.MINUTES.ONE, queryFn: async () => { diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 5cafc51286..5d30e2fb41 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -8,9 +8,8 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {PROD_DEFAULT_FEED} from '#/lib/constants' import {replaceEqualDeep} from '#/lib/functions' -import {preferencesQueryKeyRoot} from '#/lib/react-query' import {getAge} from '#/lib/strings/time' -import {STALE} from '#/state/queries' +import {PERSISTED_QUERY_ROOT, STALE} from '#/state/queries' import { DEFAULT_HOME_FEED_PREFS, DEFAULT_LOGGED_OUT_PREFERENCES, @@ -30,13 +29,13 @@ export * from '#/state/queries/preferences/const' export * from '#/state/queries/preferences/moderation' export * from '#/state/queries/preferences/types' -export const preferencesQueryKey = [preferencesQueryKeyRoot] +export const preferencesQueryKey = [PERSISTED_QUERY_ROOT, 'getPreferences'] export function usePreferencesQuery() { const agent = useAgent() const aa = useAgeAssurance() - return useQuery({ + const query = useQuery({ staleTime: STALE.SECONDS.FIFTEEN, structuralSharing: replaceEqualDeep, refetchOnWindowFocus: true, @@ -92,6 +91,15 @@ export function usePreferencesQuery() { [aa], ), }) + + if (query.data?.birthDate) { + /** + * The persisted query cache stores dates as strings, but our code expects a `Date`. + */ + query.data.birthDate = new Date(query.data.birthDate) + } + + return query } export function useClearPreferencesMutation() { diff --git a/src/storage/archive/db/index.ts b/src/storage/archive/db/index.ts index 02202a8d95..ee24d32b7b 100644 --- a/src/storage/archive/db/index.ts +++ b/src/storage/archive/db/index.ts @@ -7,16 +7,16 @@ export function create({id}: {id: string}): DB { return { async get(key: string): Promise { - return store.getString(key) ?? undefined + return store.getString(key) }, async set(key: string, value: string): Promise { - store.set(key, value) + return store.set(key, value) }, async delete(key: string): Promise { - store.delete(key) + return store.delete(key) }, async clear(): Promise { - store.clearAll() + return store.clearAll() }, } } diff --git a/src/storage/archive/db/index.web.ts b/src/storage/archive/db/index.web.ts index f0f75633b9..c8d5095b1c 100644 --- a/src/storage/archive/db/index.web.ts +++ b/src/storage/archive/db/index.web.ts @@ -7,16 +7,16 @@ export function create({id}: {id: string}): DB { return { async get(key: string): Promise { - return get(key, store) ?? undefined + return get(key, store) }, async set(key: string, value: string): Promise { - await set(key, value, store) + return set(key, value, store) }, async delete(key: string): Promise { - await del(key, store) + return del(key, store) }, async clear(): Promise { - await clear(store) + return clear(store) }, } }