Speed up startup by persisting some queries (#9594)
* persist startup queries * Use IDB for query storage (#9687) * Add storage abstraction for persisted query data Introduce a platform-specific storage abstraction layer for react-query persistence: - Native: Uses MMKV for high-performance synchronous storage - Web: Uses IndexedDB via the `idb` library for efficient async storage This replaces the previous AsyncStorage implementation with more performant platform-native solutions. The abstraction maintains API compatibility with @tanstack/query-async-storage-persister. * Refactor storage abstraction to use factory pattern Change createPersistedQueryStorage to a factory function that accepts a storage ID, allowing multiple isolated storage instances: - Native: Each instance gets its own MMKV store - Web: Each instance gets its own IndexedDB database Adopt the factory pattern in: - react-query.tsx: Uses 'persisted_queries' storage - ageAssurance/data.tsx: Uses 'age_assurance' storage This provides better separation between different query client caches and allows each to be managed independently. --------- Co-authored-by: Claude <noreply@anthropic.com> * Refactor to use archival storage (cherry picked from commit a773b40e41c96f821cd32260919ce1437c0fc3ab) * Improve archive db types (cherry picked from commit 80e4959ba2aa00c984c26aed2f7dfae1095720b0) * rm idb * clear on logout, bust on app version * create abstraction for persisting queries, make gcTime infinite * Rm abstraction --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import {beforeEach, describe, expect, it, jest} from '@jest/globals'
|
||||
|
||||
jest.mock('@bsky.app/react-native-mmkv', () => ({
|
||||
MMKV: class MMKVMock {
|
||||
_store = new Map<string, string>()
|
||||
|
||||
getString(key: string) {
|
||||
return this._store.get(key)
|
||||
}
|
||||
|
||||
set(key: string, value: string) {
|
||||
this._store.set(key, value)
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
this._store.delete(key)
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
this._store.clear()
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
import {createPersistedQueryStorage} from '../persisted-query-storage'
|
||||
|
||||
describe('createPersistedQueryStorage', () => {
|
||||
it('should create isolated storage instances', async () => {
|
||||
const storage1 = createPersistedQueryStorage('store1')
|
||||
const storage2 = createPersistedQueryStorage('store2')
|
||||
|
||||
await storage1.setItem('key', 'value1')
|
||||
await storage2.setItem('key', 'value2')
|
||||
|
||||
expect(await storage1.getItem('key')).toBe('value1')
|
||||
expect(await storage2.getItem('key')).toBe('value2')
|
||||
})
|
||||
|
||||
describe('storage operations', () => {
|
||||
let storage: ReturnType<typeof createPersistedQueryStorage>
|
||||
|
||||
beforeEach(() => {
|
||||
storage = createPersistedQueryStorage('test_store')
|
||||
})
|
||||
|
||||
it('should return null for non-existent keys', async () => {
|
||||
const result = await storage.getItem('non-existent-key')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should store and retrieve a value', async () => {
|
||||
const testValue = JSON.stringify({data: 'test'})
|
||||
await storage.setItem('test-key', testValue)
|
||||
const result = await storage.getItem('test-key')
|
||||
expect(result).toBe(testValue)
|
||||
})
|
||||
|
||||
it('should remove a value', async () => {
|
||||
const testValue = JSON.stringify({data: 'test'})
|
||||
await storage.setItem('test-key', testValue)
|
||||
await storage.removeItem('test-key')
|
||||
const result = await storage.getItem('test-key')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle complex JSON data', async () => {
|
||||
const complexData = JSON.stringify({
|
||||
queries: [
|
||||
{key: 'query1', data: {nested: {value: 123}}},
|
||||
{key: 'query2', data: {array: [1, 2, 3]}},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
await storage.setItem('complex-key', complexData)
|
||||
const result = await storage.getItem('complex-key')
|
||||
expect(result).toBe(complexData)
|
||||
expect(JSON.parse(result!)).toEqual(JSON.parse(complexData))
|
||||
})
|
||||
|
||||
it('should overwrite existing values', async () => {
|
||||
await storage.setItem('test-key', 'value1')
|
||||
await storage.setItem('test-key', 'value2')
|
||||
const result = await storage.getItem('test-key')
|
||||
expect(result).toBe('value2')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import {create as createArchiveDB} from '#/storage/archive/db'
|
||||
|
||||
/**
|
||||
* 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>
|
||||
}
|
||||
|
||||
function createId(id: string) {
|
||||
return `react-query-cache-${id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an MMKV-based storage adapter for persisting react-query cache on native platforms.
|
||||
* Each storage instance uses a separate MMKV store identified by the provided id.
|
||||
* MMKV provides synchronous access but we wrap it in Promises for API compatibility.
|
||||
*
|
||||
* @param id - Unique identifier for this storage instance (used as MMKV store id)
|
||||
*/
|
||||
export function createPersistedQueryStorage(id: string): PersistedQueryStorage {
|
||||
const store = createArchiveDB({id: createId(id)})
|
||||
return {
|
||||
getItem: async (key: string): Promise<string | null> => {
|
||||
return (await store.get(key)) ?? null
|
||||
},
|
||||
setItem: async (key: string, value: string): Promise<void> => {
|
||||
await store.set(key, value)
|
||||
},
|
||||
removeItem: async (key: string): Promise<void> => {
|
||||
await store.delete(key)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearPersistedQueryStorage(id: string) {
|
||||
const store = createArchiveDB({id: createId(id)})
|
||||
await store.clear()
|
||||
}
|
||||
+11
-9
@@ -1,27 +1,26 @@
|
||||
import {useEffect, useRef, useState} from 'react'
|
||||
import {AppState, type AppStateStatus} from 'react-native'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
|
||||
import {focusManager, onlineManager, QueryClient} from '@tanstack/react-query'
|
||||
import {
|
||||
type PersistQueryClientOptions,
|
||||
PersistQueryClientProvider,
|
||||
type PersistQueryClientProviderProps,
|
||||
} from '@tanstack/react-query-persist-client'
|
||||
import type React from 'react'
|
||||
|
||||
import {createPersistedQueryStorage} from '#/lib/persisted-query-storage'
|
||||
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
|
||||
import {PERSISTED_QUERY_ROOT} from '#/state/queries'
|
||||
import * as env from '#/env'
|
||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
__TANSTACK_QUERY_CLIENT__: import('@tanstack/query-core').QueryClient
|
||||
}
|
||||
}
|
||||
|
||||
// any query keys in this array will be persisted to AsyncStorage
|
||||
export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info'
|
||||
const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot]
|
||||
|
||||
async function checkIsOnline(): Promise<boolean> {
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
@@ -138,7 +137,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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -177,14 +177,16 @@ 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(currentDid ?? 'logged-out')
|
||||
const asyncPersister = createAsyncStoragePersister({
|
||||
storage: AsyncStorage,
|
||||
storage,
|
||||
key: 'queryClient-' + (currentDid ?? 'logged-out'),
|
||||
})
|
||||
return {
|
||||
persister: asyncPersister,
|
||||
dehydrateOptions,
|
||||
}
|
||||
buster: env.APP_VERSION,
|
||||
} satisfies Omit<PersistQueryClientOptions, 'queryClient'>
|
||||
})
|
||||
useEffect(() => {
|
||||
if (IS_WEB) {
|
||||
|
||||
Reference in New Issue
Block a user