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.
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
|||||||
AtpAgent,
|
AtpAgent,
|
||||||
getAgeAssuranceRegionConfig,
|
getAgeAssuranceRegionConfig,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
||||||
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
|
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
|
||||||
import {focusManager, QueryClient, useQuery} from '@tanstack/react-query'
|
import {focusManager, QueryClient, useQuery} from '@tanstack/react-query'
|
||||||
import {persistQueryClient} from '@tanstack/react-query-persist-client'
|
import {persistQueryClient} from '@tanstack/react-query-persist-client'
|
||||||
@@ -14,6 +13,7 @@ import debounce from 'lodash.debounce'
|
|||||||
|
|
||||||
import {networkRetry} from '#/lib/async/retry'
|
import {networkRetry} from '#/lib/async/retry'
|
||||||
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
|
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
|
||||||
|
import {createPersistedQueryStorage} from '#/lib/persisted-query-storage'
|
||||||
import {getAge} from '#/lib/strings/time'
|
import {getAge} from '#/lib/strings/time'
|
||||||
import {
|
import {
|
||||||
hasSnoozedBirthdateUpdateForDid,
|
hasSnoozedBirthdateUpdateForDid,
|
||||||
@@ -45,7 +45,7 @@ const qc = new QueryClient({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const persister = createAsyncStoragePersister({
|
const persister = createAsyncStoragePersister({
|
||||||
storage: AsyncStorage,
|
storage: createPersistedQueryStorage('age_assurance'),
|
||||||
key: 'age-assurance-query-client',
|
key: 'age-assurance-query-client',
|
||||||
})
|
})
|
||||||
const [, cacheHydrationPromise] = persistQueryClient({
|
const [, cacheHydrationPromise] = persistQueryClient({
|
||||||
|
|||||||
@@ -22,31 +22,44 @@ jest.mock('@bsky.app/react-native-mmkv', () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import {persistedQueryStorage} from '../persisted-query-storage'
|
import {createPersistedQueryStorage} from '../persisted-query-storage'
|
||||||
|
|
||||||
describe('persistedQueryStorage', () => {
|
describe('createPersistedQueryStorage', () => {
|
||||||
beforeEach(async () => {
|
it('should create isolated storage instances', async () => {
|
||||||
// Clear storage between tests
|
const storage1 = createPersistedQueryStorage('store1')
|
||||||
await persistedQueryStorage.removeItem('test-key')
|
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 () => {
|
it('should return null for non-existent keys', async () => {
|
||||||
const result = await persistedQueryStorage.getItem('non-existent-key')
|
const result = await storage.getItem('non-existent-key')
|
||||||
expect(result).toBeNull()
|
expect(result).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should store and retrieve a value', async () => {
|
it('should store and retrieve a value', async () => {
|
||||||
const testValue = JSON.stringify({data: 'test'})
|
const testValue = JSON.stringify({data: 'test'})
|
||||||
await persistedQueryStorage.setItem('test-key', testValue)
|
await storage.setItem('test-key', testValue)
|
||||||
const result = await persistedQueryStorage.getItem('test-key')
|
const result = await storage.getItem('test-key')
|
||||||
expect(result).toBe(testValue)
|
expect(result).toBe(testValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should remove a value', async () => {
|
it('should remove a value', async () => {
|
||||||
const testValue = JSON.stringify({data: 'test'})
|
const testValue = JSON.stringify({data: 'test'})
|
||||||
await persistedQueryStorage.setItem('test-key', testValue)
|
await storage.setItem('test-key', testValue)
|
||||||
await persistedQueryStorage.removeItem('test-key')
|
await storage.removeItem('test-key')
|
||||||
const result = await persistedQueryStorage.getItem('test-key')
|
const result = await storage.getItem('test-key')
|
||||||
expect(result).toBeNull()
|
expect(result).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -58,16 +71,17 @@ describe('persistedQueryStorage', () => {
|
|||||||
],
|
],
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
})
|
})
|
||||||
await persistedQueryStorage.setItem('complex-key', complexData)
|
await storage.setItem('complex-key', complexData)
|
||||||
const result = await persistedQueryStorage.getItem('complex-key')
|
const result = await storage.getItem('complex-key')
|
||||||
expect(result).toBe(complexData)
|
expect(result).toBe(complexData)
|
||||||
expect(JSON.parse(result!)).toEqual(JSON.parse(complexData))
|
expect(JSON.parse(result!)).toEqual(JSON.parse(complexData))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should overwrite existing values', async () => {
|
it('should overwrite existing values', async () => {
|
||||||
await persistedQueryStorage.setItem('test-key', 'value1')
|
await storage.setItem('test-key', 'value1')
|
||||||
await persistedQueryStorage.setItem('test-key', 'value2')
|
await storage.setItem('test-key', 'value2')
|
||||||
const result = await persistedQueryStorage.getItem('test-key')
|
const result = await storage.getItem('test-key')
|
||||||
expect(result).toBe('value2')
|
expect(result).toBe('value2')
|
||||||
})
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,13 +15,17 @@ export interface PersistedQueryStorage {
|
|||||||
removeItem: (key: string) => Promise<void>
|
removeItem: (key: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const mmkv = new MMKV({id: 'bsky_persisted_queries'})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MMKV-based storage adapter for persisting react-query cache on native platforms.
|
* 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.
|
* 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 const persistedQueryStorage: PersistedQueryStorage = {
|
export function createPersistedQueryStorage(id: string): PersistedQueryStorage {
|
||||||
|
const mmkv = new MMKV({id})
|
||||||
|
|
||||||
|
return {
|
||||||
getItem: async (key: string): Promise<string | null> => {
|
getItem: async (key: string): Promise<string | null> => {
|
||||||
return mmkv.getString(key) ?? null
|
return mmkv.getString(key) ?? null
|
||||||
},
|
},
|
||||||
@@ -31,4 +35,5 @@ export const persistedQueryStorage: PersistedQueryStorage = {
|
|||||||
removeItem: async (key: string): Promise<void> => {
|
removeItem: async (key: string): Promise<void> => {
|
||||||
mmkv.delete(key)
|
mmkv.delete(key)
|
||||||
},
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,32 +22,39 @@ interface PersistedQueryDB extends DBSchema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DB_NAME = 'bsky_persisted_queries'
|
|
||||||
const DB_VERSION = 1
|
const DB_VERSION = 1
|
||||||
const STORE_NAME = 'queries'
|
const STORE_NAME = 'queries'
|
||||||
|
|
||||||
let dbPromise: Promise<IDBPDatabase<PersistedQueryDB>> | null = null
|
const dbCache = new Map<string, Promise<IDBPDatabase<PersistedQueryDB>>>()
|
||||||
|
|
||||||
function getDB(): Promise<IDBPDatabase<PersistedQueryDB>> {
|
function getDB(dbName: string): Promise<IDBPDatabase<PersistedQueryDB>> {
|
||||||
|
let dbPromise = dbCache.get(dbName)
|
||||||
if (!dbPromise) {
|
if (!dbPromise) {
|
||||||
dbPromise = openDB<PersistedQueryDB>(DB_NAME, DB_VERSION, {
|
dbPromise = openDB<PersistedQueryDB>(dbName, DB_VERSION, {
|
||||||
upgrade(db) {
|
upgrade(db) {
|
||||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
db.createObjectStore(STORE_NAME)
|
db.createObjectStore(STORE_NAME)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
dbCache.set(dbName, dbPromise)
|
||||||
}
|
}
|
||||||
return dbPromise
|
return dbPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IndexedDB-based storage adapter for persisting react-query cache on web platforms.
|
* 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 const persistedQueryStorage: PersistedQueryStorage = {
|
export function createPersistedQueryStorage(id: string): PersistedQueryStorage {
|
||||||
|
const dbName = `bsky_${id}`
|
||||||
|
|
||||||
|
return {
|
||||||
getItem: async (key: string): Promise<string | null> => {
|
getItem: async (key: string): Promise<string | null> => {
|
||||||
try {
|
try {
|
||||||
const db = await getDB()
|
const db = await getDB(dbName)
|
||||||
const value = await db.get(STORE_NAME, key)
|
const value = await db.get(STORE_NAME, key)
|
||||||
return value ?? null
|
return value ?? null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -57,7 +64,7 @@ export const persistedQueryStorage: PersistedQueryStorage = {
|
|||||||
},
|
},
|
||||||
setItem: async (key: string, value: string): Promise<void> => {
|
setItem: async (key: string, value: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const db = await getDB()
|
const db = await getDB(dbName)
|
||||||
await db.put(STORE_NAME, value, key)
|
await db.put(STORE_NAME, value, key)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to set item in IndexedDB:', e)
|
console.error('Failed to set item in IndexedDB:', e)
|
||||||
@@ -65,10 +72,11 @@ export const persistedQueryStorage: PersistedQueryStorage = {
|
|||||||
},
|
},
|
||||||
removeItem: async (key: string): Promise<void> => {
|
removeItem: async (key: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const db = await getDB()
|
const db = await getDB(dbName)
|
||||||
await db.delete(STORE_NAME, key)
|
await db.delete(STORE_NAME, key)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to remove item from IndexedDB:', e)
|
console.error('Failed to remove item from IndexedDB:', e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@tanstack/react-query-persist-client'
|
} from '@tanstack/react-query-persist-client'
|
||||||
import {SuperJSON} from 'superjson'
|
import {SuperJSON} from 'superjson'
|
||||||
|
|
||||||
import {persistedQueryStorage} from '#/lib/persisted-query-storage'
|
import {createPersistedQueryStorage} from '#/lib/persisted-query-storage'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
|
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
|
||||||
|
|
||||||
@@ -181,8 +181,9 @@ 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 asyncPersister = createAsyncStoragePersister({
|
const asyncPersister = createAsyncStoragePersister({
|
||||||
storage: persistedQueryStorage,
|
storage,
|
||||||
key:
|
key:
|
||||||
'queryClient-' + (currentDid ?? 'logged-out') + `-v${PERSIST_VERSION}`,
|
'queryClient-' + (currentDid ?? 'logged-out') + `-v${PERSIST_VERSION}`,
|
||||||
serialize: SuperJSON.stringify,
|
serialize: SuperJSON.stringify,
|
||||||
|
|||||||
Reference in New Issue
Block a user