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.
This commit is contained in:
@@ -169,6 +169,7 @@
|
|||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"hls.js": "^1.6.2",
|
"hls.js": "^1.6.2",
|
||||||
|
"idb": "^8.0.3",
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lande": "^1.0.10",
|
"lande": "^1.0.10",
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
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 {persistedQueryStorage} from '../persisted-query-storage'
|
||||||
|
|
||||||
|
describe('persistedQueryStorage', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Clear storage between tests
|
||||||
|
await persistedQueryStorage.removeItem('test-key')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null for non-existent keys', async () => {
|
||||||
|
const result = await persistedQueryStorage.getItem('non-existent-key')
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should store and retrieve a value', async () => {
|
||||||
|
const testValue = JSON.stringify({data: 'test'})
|
||||||
|
await persistedQueryStorage.setItem('test-key', testValue)
|
||||||
|
const result = await persistedQueryStorage.getItem('test-key')
|
||||||
|
expect(result).toBe(testValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove a value', async () => {
|
||||||
|
const testValue = JSON.stringify({data: 'test'})
|
||||||
|
await persistedQueryStorage.setItem('test-key', testValue)
|
||||||
|
await persistedQueryStorage.removeItem('test-key')
|
||||||
|
const result = await persistedQueryStorage.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 persistedQueryStorage.setItem('complex-key', complexData)
|
||||||
|
const result = await persistedQueryStorage.getItem('complex-key')
|
||||||
|
expect(result).toBe(complexData)
|
||||||
|
expect(JSON.parse(result!)).toEqual(JSON.parse(complexData))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should overwrite existing values', async () => {
|
||||||
|
await persistedQueryStorage.setItem('test-key', 'value1')
|
||||||
|
await persistedQueryStorage.setItem('test-key', 'value2')
|
||||||
|
const result = await persistedQueryStorage.getItem('test-key')
|
||||||
|
expect(result).toBe('value2')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
export interface PersistedQueryStorage {
|
||||||
|
getItem: (key: string) => Promise<string | null>
|
||||||
|
setItem: (key: string, value: 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.
|
||||||
|
* MMKV provides synchronous access but we wrap it in Promises for API compatibility.
|
||||||
|
*/
|
||||||
|
export const persistedQueryStorage: PersistedQueryStorage = {
|
||||||
|
getItem: async (key: string): Promise<string | null> => {
|
||||||
|
return mmkv.getString(key) ?? null
|
||||||
|
},
|
||||||
|
setItem: async (key: string, value: string): Promise<void> => {
|
||||||
|
mmkv.set(key, value)
|
||||||
|
},
|
||||||
|
removeItem: async (key: string): Promise<void> => {
|
||||||
|
mmkv.delete(key)
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 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_NAME = 'bsky_persisted_queries'
|
||||||
|
const DB_VERSION = 1
|
||||||
|
const STORE_NAME = 'queries'
|
||||||
|
|
||||||
|
let dbPromise: Promise<IDBPDatabase<PersistedQueryDB>> | null = null
|
||||||
|
|
||||||
|
function getDB(): Promise<IDBPDatabase<PersistedQueryDB>> {
|
||||||
|
if (!dbPromise) {
|
||||||
|
dbPromise = openDB<PersistedQueryDB>(DB_NAME, DB_VERSION, {
|
||||||
|
upgrade(db) {
|
||||||
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
db.createObjectStore(STORE_NAME)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dbPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IndexedDB-based storage adapter for persisting react-query cache on web platforms.
|
||||||
|
*/
|
||||||
|
export const persistedQueryStorage: PersistedQueryStorage = {
|
||||||
|
getItem: async (key: string): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
const db = await getDB()
|
||||||
|
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()
|
||||||
|
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()
|
||||||
|
await db.delete(STORE_NAME, key)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to remove item from IndexedDB:', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import {useRef, useState} from 'react'
|
import {useRef, useState} from 'react'
|
||||||
import {AppState, type AppStateStatus} from 'react-native'
|
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 {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
|
||||||
import {focusManager, onlineManager, QueryClient} from '@tanstack/react-query'
|
import {focusManager, onlineManager, QueryClient} from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
@@ -9,10 +8,11 @@ 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 {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
|
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
|
||||||
|
|
||||||
// any query keys in this array will be persisted to AsyncStorage.
|
// any query keys in this array will be persisted to storage.
|
||||||
// keys are here for require cycle reasons ¯\_(ツ)_/¯
|
// keys are here for require cycle reasons ¯\_(ツ)_/¯
|
||||||
export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info'
|
export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info'
|
||||||
export const preferencesQueryKeyRoot = 'get-preferences'
|
export const preferencesQueryKeyRoot = 'get-preferences'
|
||||||
@@ -182,7 +182,7 @@ function QueryProviderInner({
|
|||||||
const [queryClient, _setQueryClient] = useState(() => createQueryClient())
|
const [queryClient, _setQueryClient] = useState(() => createQueryClient())
|
||||||
const [persistOptions, _setPersistOptions] = useState(() => {
|
const [persistOptions, _setPersistOptions] = useState(() => {
|
||||||
const asyncPersister = createAsyncStoragePersister({
|
const asyncPersister = createAsyncStoragePersister({
|
||||||
storage: AsyncStorage,
|
storage: persistedQueryStorage,
|
||||||
key:
|
key:
|
||||||
'queryClient-' + (currentDid ?? 'logged-out') + `-v${PERSIST_VERSION}`,
|
'queryClient-' + (currentDid ?? 'logged-out') + `-v${PERSIST_VERSION}`,
|
||||||
serialize: SuperJSON.stringify,
|
serialize: SuperJSON.stringify,
|
||||||
|
|||||||
@@ -12605,6 +12605,11 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
||||||
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
|
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
|
||||||
|
|
||||||
|
idb@^8.0.3:
|
||||||
|
version "8.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.3.tgz#c91e558f15a8d53f1d7f53a094d226fc3ad71fd9"
|
||||||
|
integrity sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==
|
||||||
|
|
||||||
ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1:
|
ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||||
|
|||||||
Reference in New Issue
Block a user