Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

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>

authored by samuel.fm

Claude
Eric Bailey
and committed by
GitHub
4f1d4821 bfd5ed48

+304 -89
+6
metro.config.js
··· 35 35 if (moduleName === '@ipld/dag-cbor') { 36 36 return context.resolveRequest(context, '@ipld/dag-cbor/src', platform) 37 37 } 38 + if (moduleName === 'copy-anything') { 39 + return context.resolveRequest(context, 'copy-anything/dist', platform) 40 + } 41 + if (moduleName === 'is-what') { 42 + return context.resolveRequest(context, 'is-what/dist', platform) 43 + } 38 44 if (process.env.BSKY_PROFILE) { 39 45 if (moduleName.endsWith('ReactNativeRenderer-prod')) { 40 46 return context.resolveRequest(
+1
package.json
··· 222 222 "react-textarea-autosize": "^8.5.3", 223 223 "sonner": "^2.0.7", 224 224 "sonner-native": "^0.21.0", 225 + "superjson": "^2.2.6", 225 226 "tippy.js": "^6.3.7", 226 227 "tlds": "^1.234.0", 227 228 "tldts": "^6.1.46",
+2 -1
src/Splash.tsx
··· 146 146 withTiming( 147 147 1, 148 148 {duration: 400, easing: Easing.out(Easing.cubic)}, 149 - async () => { 149 + () => { 150 + 'worklet' 150 151 // set these values to check animation at specific point 151 152 outroLogo.set(() => 152 153 withTiming(
+2 -2
src/ageAssurance/data.tsx
··· 6 6 AtpAgent, 7 7 getAgeAssuranceRegionConfig, 8 8 } from '@atproto/api' 9 - import AsyncStorage from '@react-native-async-storage/async-storage' 10 9 import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' 11 10 import {focusManager, QueryClient, useQuery} from '@tanstack/react-query' 12 11 import {persistQueryClient} from '@tanstack/react-query-persist-client' ··· 14 13 15 14 import {networkRetry} from '#/lib/async/retry' 16 15 import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' 16 + import {createPersistedQueryStorage} from '#/lib/persisted-query-storage' 17 17 import {getAge} from '#/lib/strings/time' 18 18 import { 19 19 hasSnoozedBirthdateUpdateForDid, ··· 45 45 }, 46 46 }) 47 47 const persister = createAsyncStoragePersister({ 48 - storage: AsyncStorage, 48 + storage: createPersistedQueryStorage('age-assurance'), 49 49 key: 'age-assurance-query-client', 50 50 }) 51 51 const [, cacheHydrationPromise] = persistQueryClient({
+87
src/lib/__tests__/persisted-query-storage.test.ts
··· 1 + import {beforeEach, describe, expect, it, jest} from '@jest/globals' 2 + 3 + jest.mock('@bsky.app/react-native-mmkv', () => ({ 4 + MMKV: class MMKVMock { 5 + _store = new Map<string, string>() 6 + 7 + getString(key: string) { 8 + return this._store.get(key) 9 + } 10 + 11 + set(key: string, value: string) { 12 + this._store.set(key, value) 13 + } 14 + 15 + delete(key: string) { 16 + this._store.delete(key) 17 + } 18 + 19 + clearAll() { 20 + this._store.clear() 21 + } 22 + }, 23 + })) 24 + 25 + import {createPersistedQueryStorage} from '../persisted-query-storage' 26 + 27 + describe('createPersistedQueryStorage', () => { 28 + it('should create isolated storage instances', async () => { 29 + const storage1 = createPersistedQueryStorage('store1') 30 + const storage2 = createPersistedQueryStorage('store2') 31 + 32 + await storage1.setItem('key', 'value1') 33 + await storage2.setItem('key', 'value2') 34 + 35 + expect(await storage1.getItem('key')).toBe('value1') 36 + expect(await storage2.getItem('key')).toBe('value2') 37 + }) 38 + 39 + describe('storage operations', () => { 40 + let storage: ReturnType<typeof createPersistedQueryStorage> 41 + 42 + beforeEach(() => { 43 + storage = createPersistedQueryStorage('test_store') 44 + }) 45 + 46 + it('should return null for non-existent keys', async () => { 47 + const result = await storage.getItem('non-existent-key') 48 + expect(result).toBeNull() 49 + }) 50 + 51 + it('should store and retrieve a value', async () => { 52 + const testValue = JSON.stringify({data: 'test'}) 53 + await storage.setItem('test-key', testValue) 54 + const result = await storage.getItem('test-key') 55 + expect(result).toBe(testValue) 56 + }) 57 + 58 + it('should remove a value', async () => { 59 + const testValue = JSON.stringify({data: 'test'}) 60 + await storage.setItem('test-key', testValue) 61 + await storage.removeItem('test-key') 62 + const result = await storage.getItem('test-key') 63 + expect(result).toBeNull() 64 + }) 65 + 66 + it('should handle complex JSON data', async () => { 67 + const complexData = JSON.stringify({ 68 + queries: [ 69 + {key: 'query1', data: {nested: {value: 123}}}, 70 + {key: 'query2', data: {array: [1, 2, 3]}}, 71 + ], 72 + timestamp: Date.now(), 73 + }) 74 + await storage.setItem('complex-key', complexData) 75 + const result = await storage.getItem('complex-key') 76 + expect(result).toBe(complexData) 77 + expect(JSON.parse(result!)).toEqual(JSON.parse(complexData)) 78 + }) 79 + 80 + it('should overwrite existing values', async () => { 81 + await storage.setItem('test-key', 'value1') 82 + await storage.setItem('test-key', 'value2') 83 + const result = await storage.getItem('test-key') 84 + expect(result).toBe('value2') 85 + }) 86 + }) 87 + })
+41
src/lib/persisted-query-storage.ts
··· 1 + import {create as createArchiveDB} from '#/storage/archive/db' 2 + 3 + /** 4 + * Interface for async storage compatible with @tanstack/query-async-storage-persister 5 + */ 6 + export interface PersistedQueryStorage { 7 + getItem: (key: string) => Promise<string | null> 8 + setItem: (key: string, value: string) => Promise<void> 9 + removeItem: (key: string) => Promise<void> 10 + } 11 + 12 + function createId(id: string) { 13 + return `react-query-cache-${id}` 14 + } 15 + 16 + /** 17 + * Creates an MMKV-based storage adapter for persisting react-query cache on native platforms. 18 + * Each storage instance uses a separate MMKV store identified by the provided id. 19 + * MMKV provides synchronous access but we wrap it in Promises for API compatibility. 20 + * 21 + * @param id - Unique identifier for this storage instance (used as MMKV store id) 22 + */ 23 + export function createPersistedQueryStorage(id: string): PersistedQueryStorage { 24 + const store = createArchiveDB({id: createId(id)}) 25 + return { 26 + getItem: async (key: string): Promise<string | null> => { 27 + return (await store.get(key)) ?? null 28 + }, 29 + setItem: async (key: string, value: string): Promise<void> => { 30 + await store.set(key, value) 31 + }, 32 + removeItem: async (key: string): Promise<void> => { 33 + await store.delete(key) 34 + }, 35 + } 36 + } 37 + 38 + export async function clearPersistedQueryStorage(id: string) { 39 + const store = createArchiveDB({id: createId(id)}) 40 + await store.clear() 41 + }
+11 -9
src/lib/react-query.tsx
··· 1 1 import {useEffect, useRef, useState} from 'react' 2 2 import {AppState, type AppStateStatus} from 'react-native' 3 - import AsyncStorage from '@react-native-async-storage/async-storage' 4 3 import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' 5 4 import {focusManager, onlineManager, QueryClient} from '@tanstack/react-query' 6 5 import { 6 + type PersistQueryClientOptions, 7 7 PersistQueryClientProvider, 8 8 type PersistQueryClientProviderProps, 9 9 } from '@tanstack/react-query-persist-client' 10 - import type React from 'react' 11 10 11 + import {createPersistedQueryStorage} from '#/lib/persisted-query-storage' 12 12 import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' 13 + import {PERSISTED_QUERY_ROOT} from '#/state/queries' 14 + import * as env from '#/env' 13 15 import {IS_NATIVE, IS_WEB} from '#/env' 14 16 15 17 declare global { 16 18 interface Window { 19 + // eslint-disable-next-line @typescript-eslint/consistent-type-imports 17 20 __TANSTACK_QUERY_CLIENT__: import('@tanstack/query-core').QueryClient 18 21 } 19 22 } 20 - 21 - // any query keys in this array will be persisted to AsyncStorage 22 - export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info' 23 - const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot] 24 23 25 24 async function checkIsOnline(): Promise<boolean> { 26 25 try { ··· 138 137 { 139 138 shouldDehydrateMutation: (_: any) => false, 140 139 shouldDehydrateQuery: query => { 141 - return STORED_CACHE_QUERY_KEY_ROOTS.includes(String(query.queryKey[0])) 140 + const root = String(query.queryKey[0]) 141 + return root === PERSISTED_QUERY_ROOT 142 142 }, 143 143 } 144 144 ··· 177 177 // Do not move the query client creation outside of this component. 178 178 const [queryClient, _setQueryClient] = useState(() => createQueryClient()) 179 179 const [persistOptions, _setPersistOptions] = useState(() => { 180 + const storage = createPersistedQueryStorage(currentDid ?? 'logged-out') 180 181 const asyncPersister = createAsyncStoragePersister({ 181 - storage: AsyncStorage, 182 + storage, 182 183 key: 'queryClient-' + (currentDid ?? 'logged-out'), 183 184 }) 184 185 return { 185 186 persister: asyncPersister, 186 187 dehydrateOptions, 187 - } 188 + buster: env.APP_VERSION, 189 + } satisfies Omit<PersistQueryClientOptions, 'queryClient'> 188 190 }) 189 191 useEffect(() => { 190 192 if (IS_WEB) {
+24 -19
src/state/queries/feed.ts
··· 8 8 moderateFeedGenerator, 9 9 RichText, 10 10 } from '@atproto/api' 11 + import {t} from '@lingui/macro' 11 12 import { 12 13 type InfiniteData, 13 14 keepPreviousData, 14 15 type QueryClient, 15 - type QueryKey, 16 16 useInfiniteQuery, 17 17 useMutation, 18 18 useQuery, ··· 22 22 import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants' 23 23 import {sanitizeDisplayName} from '#/lib/strings/display-names' 24 24 import {sanitizeHandle} from '#/lib/strings/handles' 25 - import {STALE} from '#/state/queries' 25 + import { 26 + PERSISTED_QUERY_GCTIME, 27 + PERSISTED_QUERY_ROOT, 28 + STALE, 29 + } from '#/state/queries' 26 30 import {RQKEY as listQueryKey} from '#/state/queries/list' 27 31 import {usePreferencesQuery} from '#/state/queries/preferences' 28 32 import {useAgent, useSession} from '#/state/session' ··· 114 118 avatar: view.avatar, 115 119 displayName: view.displayName 116 120 ? sanitizeDisplayName(view.displayName) 117 - : `Feed by ${sanitizeHandle(view.creator.handle, '@')}`, 121 + : t`Feed by ${sanitizeHandle(view.creator.handle, '@')}`, 118 122 description: new RichText({ 119 123 text: view.description || '', 120 124 facets: (view.descriptionFacets || [])?.slice(), ··· 155 159 creatorHandle: view.creator.handle, 156 160 displayName: view.name 157 161 ? sanitizeDisplayName(view.name) 158 - : `User List by ${sanitizeHandle(view.creator.handle, '@')}`, 162 + : t`User List by ${sanitizeHandle(view.creator.handle, '@')}`, 159 163 contentMode: undefined, 160 164 } 161 165 } ··· 238 242 ) 239 243 const lastPageCountRef = useRef(0) 240 244 241 - const query = useInfiniteQuery< 242 - AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema, 243 - Error, 244 - InfiniteData<AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema>, 245 - QueryKey, 246 - string | undefined 247 - >({ 245 + const query = useInfiniteQuery({ 248 246 enabled: Boolean(moderationOpts) && options?.enabled !== false, 249 247 queryKey: createGetPopularFeedsQueryKey(options), 250 248 queryFn: async ({pageParam}) => { ··· 261 259 262 260 return res.data 263 261 }, 264 - initialPageParam: undefined, 262 + initialPageParam: undefined as string | undefined, 265 263 getNextPageParam: lastPage => lastPage.cursor, 266 264 select: useCallback( 267 265 ( ··· 418 416 contentMode: undefined, 419 417 } 420 418 421 - const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos' 419 + const createPinnedFeedInfosQueryKeyRoot = ( 420 + kind: 'pinned' | 'saved', 421 + feedUris: string[], 422 + ) => [PERSISTED_QUERY_ROOT, 'feed-info', kind, feedUris] 422 423 423 424 export function usePinnedFeedsInfos() { 424 425 const {hasSession} = useSession() ··· 427 428 const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? [] 428 429 429 430 return useQuery({ 431 + queryKey: createPinnedFeedInfosQueryKeyRoot( 432 + 'pinned', 433 + pinnedItems.map(f => f.value), 434 + ), 435 + gcTime: PERSISTED_QUERY_GCTIME, 430 436 staleTime: STALE.INFINITY, 431 437 enabled: !isLoadingPrefs, 432 - queryKey: [ 433 - pinnedFeedInfosQueryKeyRoot, 434 - (hasSession ? 'authed:' : 'unauthed:') + 435 - pinnedItems.map(f => f.value).join(','), 436 - ], 437 438 queryFn: async () => { 438 439 if (!hasSession) { 439 440 return [PWI_DISCOVER_FEED_STUB] ··· 535 536 const queryClient = useQueryClient() 536 537 537 538 return useQuery({ 539 + queryKey: createPinnedFeedInfosQueryKeyRoot( 540 + 'saved', 541 + savedItems.map(f => f.value), 542 + ), 543 + gcTime: PERSISTED_QUERY_GCTIME, 538 544 staleTime: STALE.INFINITY, 539 545 enabled: !isLoadingPrefs, 540 - queryKey: [pinnedFeedInfosQueryKeyRoot, ...savedItems], 541 546 placeholderData: previousData => { 542 547 return ( 543 548 previousData || {
+20
src/state/queries/index.ts
··· 18 18 }, 19 19 INFINITY: Infinity, 20 20 } 21 + 22 + /** 23 + * Root key for persisted queries. 24 + * 25 + * If the `querykey` of your query uses this at index 0, it will be 26 + * persisted automatically by the `PersistQueryClientProvider` in 27 + * `#/lib/react-query.tsx`. 28 + * 29 + * Be careful when using this, since it will change the query key and may 30 + * break any cases where we call `invalidateQueries` or `refetchQueries` 31 + * with the old key. 32 + * 33 + * Also, only use this for queries that are safe to persist between 34 + * app launches (like user preferences). 35 + * 36 + * Note that for queries that are persisted, it is recommended to extend 37 + * the `gcTime` to a longer duration, otherwise it'll get busted 38 + */ 39 + export const PERSISTED_QUERY_ROOT = 'PERSISTED' 40 + export const PERSISTED_QUERY_GCTIME = Infinity
+10 -6
src/state/queries/labeler.ts
··· 3 3 import {z} from 'zod' 4 4 5 5 import {MAX_LABELERS} from '#/lib/constants' 6 - import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query' 7 - import {STALE} from '#/state/queries' 6 + import { 7 + PERSISTED_QUERY_GCTIME, 8 + PERSISTED_QUERY_ROOT, 9 + STALE, 10 + } from '#/state/queries' 8 11 import { 9 12 preferencesQueryKey, 10 13 usePreferencesQuery, ··· 23 26 dids.slice().sort(), 24 27 ] 25 28 26 - export const labelersDetailedInfoQueryKey = (dids: string[]) => [ 27 - labelersDetailedInfoQueryKeyRoot, 29 + const persistedLabelersDetailedInfoQueryKey = (dids: string[]) => [ 30 + PERSISTED_QUERY_ROOT, 31 + 'labelers-detailed-info', 28 32 dids, 29 33 ] 30 34 ··· 65 69 const agent = useAgent() 66 70 return useQuery({ 67 71 enabled: !!dids.length, 68 - queryKey: labelersDetailedInfoQueryKey(dids), 69 - gcTime: 1000 * 60 * 60 * 6, // 6 hours 72 + queryKey: persistedLabelersDetailedInfoQueryKey(dids), 73 + gcTime: PERSISTED_QUERY_GCTIME, 70 74 staleTime: STALE.MINUTES.ONE, 71 75 queryFn: async () => { 72 76 const res = await agent.app.bsky.labeler.getServices({
+17 -4
src/state/queries/preferences/index.ts
··· 9 9 import {PROD_DEFAULT_FEED} from '#/lib/constants' 10 10 import {replaceEqualDeep} from '#/lib/functions' 11 11 import {getAge} from '#/lib/strings/time' 12 - import {STALE} from '#/state/queries' 12 + import { 13 + PERSISTED_QUERY_GCTIME, 14 + PERSISTED_QUERY_ROOT, 15 + STALE, 16 + } from '#/state/queries' 13 17 import { 14 18 DEFAULT_HOME_FEED_PREFS, 15 19 DEFAULT_LOGGED_OUT_PREFERENCES, ··· 29 33 export * from '#/state/queries/preferences/moderation' 30 34 export * from '#/state/queries/preferences/types' 31 35 32 - const preferencesQueryKeyRoot = 'getPreferences' 33 - export const preferencesQueryKey = [preferencesQueryKeyRoot] 36 + export const preferencesQueryKey = [PERSISTED_QUERY_ROOT, 'getPreferences'] 34 37 35 38 export function usePreferencesQuery() { 36 39 const agent = useAgent() 37 40 const aa = useAgeAssurance() 38 41 39 - return useQuery({ 42 + const query = useQuery({ 40 43 staleTime: STALE.SECONDS.FIFTEEN, 41 44 structuralSharing: replaceEqualDeep, 42 45 refetchOnWindowFocus: true, 43 46 queryKey: preferencesQueryKey, 47 + gcTime: PERSISTED_QUERY_GCTIME, 44 48 queryFn: async () => { 45 49 if (!agent.did) { 46 50 return DEFAULT_LOGGED_OUT_PREFERENCES ··· 92 96 [aa], 93 97 ), 94 98 }) 99 + 100 + if (query.data?.birthDate) { 101 + /** 102 + * The persisted query cache stores dates as strings, but our code expects a `Date`. 103 + */ 104 + query.data.birthDate = new Date(query.data.birthDate) 105 + } 106 + 107 + return query 95 108 } 96 109 97 110 export function useClearPreferencesMutation() {
+43 -28
src/state/session/index.tsx
··· 1 - import React from 'react' 1 + import { 2 + createContext, 3 + useCallback, 4 + useContext, 5 + useEffect, 6 + useMemo, 7 + useRef, 8 + useState, 9 + useSyncExternalStore, 10 + } from 'react' 2 11 import {type AtpSessionEvent, type BskyAgent} from '@atproto/api' 3 12 4 13 import * as persisted from '#/state/persisted' ··· 19 28 export {isSignupQueued} from './util' 20 29 import {addSessionDebugLog} from './logging' 21 30 export type {SessionAccount} from '#/state/session/types' 31 + 32 + import {clearPersistedQueryStorage} from '#/lib/persisted-query-storage' 22 33 import { 23 34 type SessionApiContext, 24 35 type SessionStateContext, ··· 29 40 clearAgeAssuranceDataForDid, 30 41 } from '#/ageAssurance/data' 31 42 32 - const StateContext = React.createContext<SessionStateContext>({ 43 + const StateContext = createContext<SessionStateContext>({ 33 44 accounts: [], 34 45 currentAccount: undefined, 35 46 hasSession: false, 36 47 }) 37 48 StateContext.displayName = 'SessionStateContext' 38 49 39 - const AgentContext = React.createContext<BskyAgent | null>(null) 50 + const AgentContext = createContext<BskyAgent | null>(null) 40 51 AgentContext.displayName = 'SessionAgentContext' 41 52 42 - const ApiContext = React.createContext<SessionApiContext>({ 53 + const ApiContext = createContext<SessionApiContext>({ 43 54 createAccount: async () => {}, 44 55 login: async () => {}, 45 - logoutCurrentAccount: async () => {}, 46 - logoutEveryAccount: async () => {}, 56 + logoutCurrentAccount: () => {}, 57 + logoutEveryAccount: () => {}, 47 58 resumeSession: async () => {}, 48 59 removeAccount: () => {}, 49 60 partialRefreshSession: async () => {}, ··· 94 105 export function Provider({children}: React.PropsWithChildren<{}>) { 95 106 const ax = useAnalyticsBase() 96 107 const cancelPendingTask = useOneTaskAtATime() 97 - const [store] = React.useState(() => new SessionStore()) 98 - const state = React.useSyncExternalStore(store.subscribe, store.getState) 108 + const [store] = useState(() => new SessionStore()) 109 + const state = useSyncExternalStore(store.subscribe, store.getState) 99 110 const onboardingDispatch = useOnboardingDispatch() 100 111 101 - const onAgentSessionChange = React.useCallback( 112 + const onAgentSessionChange = useCallback( 102 113 (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { 103 114 const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away. 104 115 if (sessionEvent === 'expired' || sessionEvent === 'create-failed') { ··· 115 126 [store], 116 127 ) 117 128 118 - const createAccount = React.useCallback<SessionApiContext['createAccount']>( 129 + const createAccount = useCallback<SessionApiContext['createAccount']>( 119 130 async (params, metrics) => { 120 131 addSessionDebugLog({type: 'method:start', method: 'createAccount'}) 121 132 const signal = cancelPendingTask() ··· 141 152 [ax, store, onAgentSessionChange, cancelPendingTask], 142 153 ) 143 154 144 - const login = React.useCallback<SessionApiContext['login']>( 155 + const login = useCallback<SessionApiContext['login']>( 145 156 async (params, logContext) => { 146 157 addSessionDebugLog({type: 'method:start', method: 'login'}) 147 158 const signal = cancelPendingTask() ··· 168 179 [ax, store, onAgentSessionChange, cancelPendingTask], 169 180 ) 170 181 171 - const logoutCurrentAccount = React.useCallback< 182 + const logoutCurrentAccount = useCallback< 172 183 SessionApiContext['logoutCurrentAccount'] 173 184 >( 174 185 logContext => { ··· 192 203 addSessionDebugLog({type: 'method:end', method: 'logout'}) 193 204 if (prevState.currentAgentState.did) { 194 205 clearAgeAssuranceDataForDid({did: prevState.currentAgentState.did}) 206 + void clearPersistedQueryStorage(prevState.currentAgentState.did) 195 207 } 196 208 // reset onboarding flow on logout 197 209 onboardingDispatch({type: 'skip'}) ··· 199 211 [ax, store, cancelPendingTask, onboardingDispatch], 200 212 ) 201 213 202 - const logoutEveryAccount = React.useCallback< 214 + const logoutEveryAccount = useCallback< 203 215 SessionApiContext['logoutEveryAccount'] 204 216 >( 205 217 logContext => { ··· 222 234 ) 223 235 addSessionDebugLog({type: 'method:end', method: 'logout'}) 224 236 clearAgeAssuranceData() 237 + for (const account of prevState.accounts) { 238 + void clearPersistedQueryStorage(account.did) 239 + } 225 240 // reset onboarding flow on logout 226 241 onboardingDispatch({type: 'skip'}) 227 242 }, 228 - [store, cancelPendingTask, onboardingDispatch], 243 + [store, cancelPendingTask, onboardingDispatch, ax], 229 244 ) 230 245 231 - const resumeSession = React.useCallback<SessionApiContext['resumeSession']>( 246 + const resumeSession = useCallback<SessionApiContext['resumeSession']>( 232 247 async (storedAccount, isSwitchingAccounts = false) => { 233 248 addSessionDebugLog({ 234 249 type: 'method:start', ··· 258 273 [store, onAgentSessionChange, cancelPendingTask, onboardingDispatch], 259 274 ) 260 275 261 - const partialRefreshSession = React.useCallback< 276 + const partialRefreshSession = useCallback< 262 277 SessionApiContext['partialRefreshSession'] 263 278 >(async () => { 264 279 const agent = state.currentAgentState.agent as BskyAppAgent ··· 275 290 }) 276 291 }, [store, state, cancelPendingTask]) 277 292 278 - const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( 293 + const removeAccount = useCallback<SessionApiContext['removeAccount']>( 279 294 account => { 280 295 addSessionDebugLog({ 281 296 type: 'method:start', ··· 292 307 }, 293 308 [store, cancelPendingTask], 294 309 ) 295 - React.useEffect(() => { 310 + useEffect(() => { 296 311 return persisted.onUpdate('session', nextSession => { 297 312 const synced = nextSession 298 313 addSessionDebugLog({type: 'persisted:receive', data: synced}) ··· 322 337 }) 323 338 }, [store, state, resumeSession]) 324 339 325 - const stateContext = React.useMemo( 340 + const stateContext = useMemo( 326 341 () => ({ 327 342 accounts: state.accounts, 328 343 currentAccount: state.accounts.find( ··· 333 348 [state], 334 349 ) 335 350 336 - const api = React.useMemo( 351 + const api = useMemo( 337 352 () => ({ 338 353 createAccount, 339 354 login, ··· 358 373 if (__DEV__ && IS_WEB) window.agent = state.currentAgentState.agent 359 374 360 375 const agent = state.currentAgentState.agent as BskyAppAgent 361 - const currentAgentRef = React.useRef(agent) 362 - React.useEffect(() => { 376 + const currentAgentRef = useRef(agent) 377 + useEffect(() => { 363 378 if (currentAgentRef.current !== agent) { 364 379 // Read the previous value and immediately advance the pointer. 365 380 const prevAgent = currentAgentRef.current ··· 390 405 } 391 406 392 407 function useOneTaskAtATime() { 393 - const abortController = React.useRef<AbortController | null>(null) 394 - const cancelPendingTask = React.useCallback(() => { 408 + const abortController = useRef<AbortController | null>(null) 409 + const cancelPendingTask = useCallback(() => { 395 410 if (abortController.current) { 396 411 abortController.current.abort() 397 412 } ··· 402 417 } 403 418 404 419 export function useSession() { 405 - return React.useContext(StateContext) 420 + return useContext(StateContext) 406 421 } 407 422 408 423 export function useSessionApi() { 409 - return React.useContext(ApiContext) 424 + return useContext(ApiContext) 410 425 } 411 426 412 427 export function useRequireAuth() { ··· 414 429 const closeAll = useCloseAllActiveElements() 415 430 const {signinDialogControl} = useGlobalDialogsControlContext() 416 431 417 - return React.useCallback( 432 + return useCallback( 418 433 (fn: () => void) => { 419 434 if (hasSession) { 420 435 fn() ··· 428 443 } 429 444 430 445 export function useAgent(): BskyAgent { 431 - const agent = React.useContext(AgentContext) 446 + const agent = useContext(AgentContext) 432 447 if (!agent) { 433 448 throw Error('useAgent() must be below <SessionProvider>.') 434 449 }
+8 -8
src/storage/archive/db/index.ts
··· 6 6 const store = new MMKV({id}) 7 7 8 8 return { 9 - async get(key: string): Promise<string | undefined> { 10 - return store.getString(key) ?? undefined 9 + get(key: string) { 10 + return store.getString(key) 11 11 }, 12 - async set(key: string, value: string): Promise<void> { 13 - store.set(key, value) 12 + set(key: string, value: string) { 13 + return store.set(key, value) 14 14 }, 15 - async delete(key: string): Promise<void> { 16 - store.delete(key) 15 + delete(key: string) { 16 + return store.delete(key) 17 17 }, 18 - async clear(): Promise<void> { 19 - store.clearAll() 18 + clear() { 19 + return store.clearAll() 20 20 }, 21 21 } 22 22 }
+8 -8
src/storage/archive/db/index.web.ts
··· 6 6 const store = createStore(id, id) 7 7 8 8 return { 9 - async get(key: string): Promise<string | undefined> { 10 - return get(key, store) ?? undefined 9 + get(key: string) { 10 + return get(key, store) 11 11 }, 12 - async set(key: string, value: string): Promise<void> { 13 - await set(key, value, store) 12 + set(key: string, value: string) { 13 + return set(key, value, store) 14 14 }, 15 - async delete(key: string): Promise<void> { 16 - await del(key, store) 15 + delete(key: string) { 16 + return del(key, store) 17 17 }, 18 - async clear(): Promise<void> { 19 - await clear(store) 18 + clear() { 19 + return clear(store) 20 20 }, 21 21 } 22 22 }
+5 -4
src/storage/archive/db/types.ts
··· 1 + type MaybePromise<T> = T | Promise<T> 1 2 export type DB = { 2 - get(key: string): Promise<string | undefined> 3 - set(key: string, value: string): Promise<void> 4 - delete(key: string): Promise<void> 5 - clear(): Promise<void> 3 + get(key: string): MaybePromise<string | undefined> 4 + set(key: string, value: string): MaybePromise<void> 5 + delete(key: string): MaybePromise<void> 6 + clear(): MaybePromise<void> 6 7 }
+19
yarn.lock
··· 9722 9722 resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" 9723 9723 integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== 9724 9724 9725 + copy-anything@^4: 9726 + version "4.0.5" 9727 + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-4.0.5.tgz#16cabafd1ea4bb327a540b750f2b4df522825aea" 9728 + integrity sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA== 9729 + dependencies: 9730 + is-what "^5.2.0" 9731 + 9725 9732 copy-webpack-plugin@^10.2.0: 9726 9733 version "10.2.4" 9727 9734 resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz#6c854be3fdaae22025da34b9112ccf81c63308fe" ··· 13524 13531 dependencies: 13525 13532 call-bound "^1.0.3" 13526 13533 get-intrinsic "^1.2.6" 13534 + 13535 + is-what@^5.2.0: 13536 + version "5.5.0" 13537 + resolved "https://registry.yarnpkg.com/is-what/-/is-what-5.5.0.tgz#a3031815757cfe1f03fed990bf6355a2d3f628c4" 13538 + integrity sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw== 13527 13539 13528 13540 is-wsl@^2.0.0, is-wsl@^2.1.1, is-wsl@^2.2.0: 13529 13541 version "2.2.0" ··· 18912 18924 pirates "^4.0.1" 18913 18925 tinyglobby "^0.2.11" 18914 18926 ts-interface-checker "^0.1.9" 18927 + 18928 + superjson@^2.2.6: 18929 + version "2.2.6" 18930 + resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.6.tgz#a223a3a988172a5f9656e2063fe5f733af40d099" 18931 + integrity sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA== 18932 + dependencies: 18933 + copy-anything "^4" 18915 18934 18916 18935 supports-color@^5.3.0: 18917 18936 version "5.5.0"