Refactor to use archival storage
(cherry picked from commit a773b40e41c96f821cd32260919ce1437c0fc3ab)
This commit is contained in:
committed by
Samuel Newman
parent
38177bc9a6
commit
ab8d14a1f0
@@ -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({
|
||||
|
||||
@@ -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<string | null> => {
|
||||
return mmkv.getString(key) ?? null
|
||||
return (await store.get(key)) ?? null
|
||||
},
|
||||
setItem: async (key: string, value: string): Promise<void> => {
|
||||
mmkv.set(key, value)
|
||||
await store.set(key, value)
|
||||
},
|
||||
removeItem: async (key: string): Promise<void> => {
|
||||
mmkv.delete(key)
|
||||
await store.delete(key)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string | null>
|
||||
setItem: (key: string, value: string) => Promise<void>
|
||||
removeItem: (key: string) => Promise<void>
|
||||
}
|
||||
|
||||
interface PersistedQueryDB extends DBSchema {
|
||||
queries: {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
}
|
||||
|
||||
const DB_VERSION = 1
|
||||
const STORE_NAME = 'queries'
|
||||
|
||||
const dbCache = new Map<string, Promise<IDBPDatabase<PersistedQueryDB>>>()
|
||||
|
||||
function getDB(dbName: string): Promise<IDBPDatabase<PersistedQueryDB>> {
|
||||
let dbPromise = dbCache.get(dbName)
|
||||
if (!dbPromise) {
|
||||
dbPromise = openDB<PersistedQueryDB>(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<string | null> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
try {
|
||||
const db = await getDB(dbName)
|
||||
await db.delete(STORE_NAME, key)
|
||||
} catch (e) {
|
||||
console.error('Failed to remove item from IndexedDB:', e)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
+4
-17
@@ -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<boolean> {
|
||||
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,
|
||||
|
||||
@@ -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 || {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -7,16 +7,16 @@ export function create({id}: {id: string}): DB {
|
||||
|
||||
return {
|
||||
async get(key: string): Promise<string | undefined> {
|
||||
return store.getString(key) ?? undefined
|
||||
return store.getString(key)
|
||||
},
|
||||
async set(key: string, value: string): Promise<void> {
|
||||
store.set(key, value)
|
||||
return store.set(key, value)
|
||||
},
|
||||
async delete(key: string): Promise<void> {
|
||||
store.delete(key)
|
||||
return store.delete(key)
|
||||
},
|
||||
async clear(): Promise<void> {
|
||||
store.clearAll()
|
||||
return store.clearAll()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@ export function create({id}: {id: string}): DB {
|
||||
|
||||
return {
|
||||
async get(key: string): Promise<string | undefined> {
|
||||
return get(key, store) ?? undefined
|
||||
return get(key, store)
|
||||
},
|
||||
async set(key: string, value: string): Promise<void> {
|
||||
await set(key, value, store)
|
||||
return set(key, value, store)
|
||||
},
|
||||
async delete(key: string): Promise<void> {
|
||||
await del(key, store)
|
||||
return del(key, store)
|
||||
},
|
||||
async clear(): Promise<void> {
|
||||
await clear(store)
|
||||
return clear(store)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user