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({
|
const persister = createAsyncStoragePersister({
|
||||||
storage: createPersistedQueryStorage('age_assurance'),
|
storage: createPersistedQueryStorage('age_assurance_cache'),
|
||||||
key: 'age-assurance-query-client',
|
key: 'age-assurance-query-client',
|
||||||
})
|
})
|
||||||
const [, cacheHydrationPromise] = persistQueryClient({
|
const [, cacheHydrationPromise] = persistQueryClient({
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
/**
|
import {create as createArchiveDB} from '#/storage/archive/db'
|
||||||
* 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'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for async storage compatible with @tanstack/query-async-storage-persister
|
* 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)
|
* @param id - Unique identifier for this storage instance (used as MMKV store id)
|
||||||
*/
|
*/
|
||||||
export function createPersistedQueryStorage(id: string): PersistedQueryStorage {
|
export function createPersistedQueryStorage(id: string): PersistedQueryStorage {
|
||||||
const mmkv = new MMKV({id})
|
const store = createArchiveDB({id})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getItem: async (key: string): Promise<string | null> => {
|
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> => {
|
setItem: async (key: string, value: string): Promise<void> => {
|
||||||
mmkv.set(key, value)
|
await store.set(key, value)
|
||||||
},
|
},
|
||||||
removeItem: async (key: string): Promise<void> => {
|
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,
|
PersistQueryClientProvider,
|
||||||
type PersistQueryClientProviderProps,
|
type PersistQueryClientProviderProps,
|
||||||
} from '@tanstack/react-query-persist-client'
|
} from '@tanstack/react-query-persist-client'
|
||||||
import {SuperJSON} from 'superjson'
|
|
||||||
|
|
||||||
import {createPersistedQueryStorage} from '#/lib/persisted-query-storage'
|
import {createPersistedQueryStorage} from '#/lib/persisted-query-storage'
|
||||||
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
|
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
|
||||||
|
import {PERSISTED_QUERY_ROOT} from '#/state/queries'
|
||||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
|
|
||||||
declare global {
|
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> {
|
async function checkIsOnline(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
@@ -146,7 +134,8 @@ const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehyd
|
|||||||
{
|
{
|
||||||
shouldDehydrateMutation: (_: any) => false,
|
shouldDehydrateMutation: (_: any) => false,
|
||||||
shouldDehydrateQuery: query => {
|
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.
|
// Do not move the query client creation outside of this component.
|
||||||
const [queryClient, _setQueryClient] = useState(() => createQueryClient())
|
const [queryClient, _setQueryClient] = useState(() => createQueryClient())
|
||||||
const [persistOptions, _setPersistOptions] = useState(() => {
|
const [persistOptions, _setPersistOptions] = useState(() => {
|
||||||
const storage = createPersistedQueryStorage('persisted_queries')
|
const storage = createPersistedQueryStorage('react-query-cache')
|
||||||
const asyncPersister = createAsyncStoragePersister({
|
const asyncPersister = createAsyncStoragePersister({
|
||||||
storage,
|
storage,
|
||||||
key:
|
key:
|
||||||
'queryClient-' + (currentDid ?? 'logged-out') + `-v${PERSIST_VERSION}`,
|
'queryClient-' + (currentDid ?? 'logged-out') + `-v${PERSIST_VERSION}`,
|
||||||
serialize: SuperJSON.stringify,
|
|
||||||
deserialize: SuperJSON.parse,
|
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
persister: asyncPersister,
|
persister: asyncPersister,
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ import {
|
|||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
|
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
|
||||||
import {pinnedFeedInfosQueryKeyRoot} from '#/lib/react-query'
|
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
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 {RQKEY as listQueryKey} from '#/state/queries/list'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
@@ -419,6 +418,12 @@ const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = {
|
|||||||
contentMode: undefined,
|
contentMode: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createPinnedFeedInfosQueryKeyRoot = (...args: any[]) => [
|
||||||
|
PERSISTED_QUERY_ROOT,
|
||||||
|
'feed-info',
|
||||||
|
...args,
|
||||||
|
]
|
||||||
|
|
||||||
export function usePinnedFeedsInfos() {
|
export function usePinnedFeedsInfos() {
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
@@ -428,11 +433,10 @@ export function usePinnedFeedsInfos() {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
staleTime: STALE.INFINITY,
|
staleTime: STALE.INFINITY,
|
||||||
enabled: !isLoadingPrefs,
|
enabled: !isLoadingPrefs,
|
||||||
queryKey: [
|
queryKey: createPinnedFeedInfosQueryKeyRoot(
|
||||||
pinnedFeedInfosQueryKeyRoot,
|
'pinned',
|
||||||
(hasSession ? 'authed:' : 'unauthed:') +
|
...pinnedItems.map(f => f.value),
|
||||||
pinnedItems.map(f => f.value).join(','),
|
),
|
||||||
],
|
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!hasSession) {
|
if (!hasSession) {
|
||||||
return [PWI_DISCOVER_FEED_STUB]
|
return [PWI_DISCOVER_FEED_STUB]
|
||||||
@@ -536,7 +540,10 @@ export function useSavedFeeds() {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
staleTime: STALE.INFINITY,
|
staleTime: STALE.INFINITY,
|
||||||
enabled: !isLoadingPrefs,
|
enabled: !isLoadingPrefs,
|
||||||
queryKey: [pinnedFeedInfosQueryKeyRoot, ...savedItems],
|
queryKey: createPinnedFeedInfosQueryKeyRoot(
|
||||||
|
'saved',
|
||||||
|
...savedItems.map(f => f.value),
|
||||||
|
),
|
||||||
placeholderData: previousData => {
|
placeholderData: previousData => {
|
||||||
return (
|
return (
|
||||||
previousData || {
|
previousData || {
|
||||||
|
|||||||
@@ -18,3 +18,19 @@ export const STALE = {
|
|||||||
},
|
},
|
||||||
INFINITY: Infinity,
|
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 {z} from 'zod'
|
||||||
|
|
||||||
import {MAX_LABELERS} from '#/lib/constants'
|
import {MAX_LABELERS} from '#/lib/constants'
|
||||||
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
|
import {PERSISTED_QUERY_ROOT, STALE} from '#/state/queries'
|
||||||
import {STALE} from '#/state/queries'
|
|
||||||
import {
|
import {
|
||||||
preferencesQueryKey,
|
preferencesQueryKey,
|
||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
@@ -23,8 +22,9 @@ export const labelersInfoQueryKey = (dids: string[]) => [
|
|||||||
dids.slice().sort(),
|
dids.slice().sort(),
|
||||||
]
|
]
|
||||||
|
|
||||||
export const labelersDetailedInfoQueryKey = (dids: string[]) => [
|
export const persistedLabelersDetailedInfoQueryKey = (dids: string[]) => [
|
||||||
labelersDetailedInfoQueryKeyRoot,
|
PERSISTED_QUERY_ROOT,
|
||||||
|
'labelers-detailed-info',
|
||||||
dids,
|
dids,
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
|
|||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: !!dids.length,
|
enabled: !!dids.length,
|
||||||
queryKey: labelersDetailedInfoQueryKey(dids),
|
queryKey: persistedLabelersDetailedInfoQueryKey(dids),
|
||||||
gcTime: 1000 * 60 * 60 * 6, // 6 hours
|
gcTime: 1000 * 60 * 60 * 6, // 6 hours
|
||||||
staleTime: STALE.MINUTES.ONE,
|
staleTime: STALE.MINUTES.ONE,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
||||||
import {replaceEqualDeep} from '#/lib/functions'
|
import {replaceEqualDeep} from '#/lib/functions'
|
||||||
import {preferencesQueryKeyRoot} from '#/lib/react-query'
|
|
||||||
import {getAge} from '#/lib/strings/time'
|
import {getAge} from '#/lib/strings/time'
|
||||||
import {STALE} from '#/state/queries'
|
import {PERSISTED_QUERY_ROOT, STALE} from '#/state/queries'
|
||||||
import {
|
import {
|
||||||
DEFAULT_HOME_FEED_PREFS,
|
DEFAULT_HOME_FEED_PREFS,
|
||||||
DEFAULT_LOGGED_OUT_PREFERENCES,
|
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/moderation'
|
||||||
export * from '#/state/queries/preferences/types'
|
export * from '#/state/queries/preferences/types'
|
||||||
|
|
||||||
export const preferencesQueryKey = [preferencesQueryKeyRoot]
|
export const preferencesQueryKey = [PERSISTED_QUERY_ROOT, 'getPreferences']
|
||||||
|
|
||||||
export function usePreferencesQuery() {
|
export function usePreferencesQuery() {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const aa = useAgeAssurance()
|
const aa = useAgeAssurance()
|
||||||
|
|
||||||
return useQuery({
|
const query = useQuery({
|
||||||
staleTime: STALE.SECONDS.FIFTEEN,
|
staleTime: STALE.SECONDS.FIFTEEN,
|
||||||
structuralSharing: replaceEqualDeep,
|
structuralSharing: replaceEqualDeep,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
@@ -92,6 +91,15 @@ export function usePreferencesQuery() {
|
|||||||
[aa],
|
[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() {
|
export function useClearPreferencesMutation() {
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ export function create({id}: {id: string}): DB {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
async get(key: string): Promise<string | undefined> {
|
async get(key: string): Promise<string | undefined> {
|
||||||
return store.getString(key) ?? undefined
|
return store.getString(key)
|
||||||
},
|
},
|
||||||
async set(key: string, value: string): Promise<void> {
|
async set(key: string, value: string): Promise<void> {
|
||||||
store.set(key, value)
|
return store.set(key, value)
|
||||||
},
|
},
|
||||||
async delete(key: string): Promise<void> {
|
async delete(key: string): Promise<void> {
|
||||||
store.delete(key)
|
return store.delete(key)
|
||||||
},
|
},
|
||||||
async clear(): Promise<void> {
|
async clear(): Promise<void> {
|
||||||
store.clearAll()
|
return store.clearAll()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ export function create({id}: {id: string}): DB {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
async get(key: string): Promise<string | undefined> {
|
async get(key: string): Promise<string | undefined> {
|
||||||
return get(key, store) ?? undefined
|
return get(key, store)
|
||||||
},
|
},
|
||||||
async set(key: string, value: string): Promise<void> {
|
async set(key: string, value: string): Promise<void> {
|
||||||
await set(key, value, store)
|
return set(key, value, store)
|
||||||
},
|
},
|
||||||
async delete(key: string): Promise<void> {
|
async delete(key: string): Promise<void> {
|
||||||
await del(key, store)
|
return del(key, store)
|
||||||
},
|
},
|
||||||
async clear(): Promise<void> {
|
async clear(): Promise<void> {
|
||||||
await clear(store)
|
return clear(store)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user