diff --git a/metro.config.js b/metro.config.js index dcdead3f5d..b81d790f75 100644 --- a/metro.config.js +++ b/metro.config.js @@ -35,6 +35,12 @@ cfg.resolver.resolveRequest = (context, moduleName, platform) => { if (moduleName === '@ipld/dag-cbor') { return context.resolveRequest(context, '@ipld/dag-cbor/src', platform) } + if (moduleName === 'copy-anything') { + return context.resolveRequest(context, 'copy-anything/dist', platform) + } + if (moduleName === 'is-what') { + return context.resolveRequest(context, 'is-what/dist', platform) + } if (process.env.BSKY_PROFILE) { if (moduleName.endsWith('ReactNativeRenderer-prod')) { return context.resolveRequest( diff --git a/package.json b/package.json index 6a1b9e89cf..029c7af884 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,7 @@ "react-textarea-autosize": "^8.5.3", "sonner": "^2.0.7", "sonner-native": "^0.21.0", + "superjson": "^2.2.6", "tippy.js": "^6.3.7", "tlds": "^1.234.0", "tldts": "^6.1.46", diff --git a/src/Splash.tsx b/src/Splash.tsx index 7a99d36891..5d46ba617e 100644 --- a/src/Splash.tsx +++ b/src/Splash.tsx @@ -146,7 +146,8 @@ export function Splash(props: React.PropsWithChildren) { withTiming( 1, {duration: 400, easing: Easing.out(Easing.cubic)}, - async () => { + () => { + 'worklet' // set these values to check animation at specific point outroLogo.set(() => withTiming( diff --git a/src/ageAssurance/data.tsx b/src/ageAssurance/data.tsx index f6353a899b..d2abfb5f88 100644 --- a/src/ageAssurance/data.tsx +++ b/src/ageAssurance/data.tsx @@ -6,7 +6,6 @@ import { AtpAgent, getAgeAssuranceRegionConfig, } from '@atproto/api' -import AsyncStorage from '@react-native-async-storage/async-storage' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' import {focusManager, QueryClient, useQuery} from '@tanstack/react-query' import {persistQueryClient} from '@tanstack/react-query-persist-client' @@ -14,6 +13,7 @@ import debounce from 'lodash.debounce' import {networkRetry} from '#/lib/async/retry' import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' +import {createPersistedQueryStorage} from '#/lib/persisted-query-storage' import {getAge} from '#/lib/strings/time' import { hasSnoozedBirthdateUpdateForDid, @@ -45,7 +45,7 @@ const qc = new QueryClient({ }, }) const persister = createAsyncStoragePersister({ - storage: AsyncStorage, + storage: createPersistedQueryStorage('age-assurance'), key: 'age-assurance-query-client', }) const [, cacheHydrationPromise] = persistQueryClient({ diff --git a/src/lib/__tests__/persisted-query-storage.test.ts b/src/lib/__tests__/persisted-query-storage.test.ts new file mode 100644 index 0000000000..fe6a61884b --- /dev/null +++ b/src/lib/__tests__/persisted-query-storage.test.ts @@ -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() + + 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 + + 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') + }) + }) +}) diff --git a/src/lib/persisted-query-storage.ts b/src/lib/persisted-query-storage.ts new file mode 100644 index 0000000000..48818dd3fe --- /dev/null +++ b/src/lib/persisted-query-storage.ts @@ -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 + setItem: (key: string, value: string) => Promise + removeItem: (key: string) => Promise +} + +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 => { + return (await store.get(key)) ?? null + }, + setItem: async (key: string, value: string): Promise => { + await store.set(key, value) + }, + removeItem: async (key: string): Promise => { + await store.delete(key) + }, + } +} + +export async function clearPersistedQueryStorage(id: string) { + const store = createArchiveDB({id: createId(id)}) + await store.clear() +} diff --git a/src/lib/react-query.tsx b/src/lib/react-query.tsx index 47b3846f60..ec657b6c41 100644 --- a/src/lib/react-query.tsx +++ b/src/lib/react-query.tsx @@ -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 { 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 }) useEffect(() => { if (IS_WEB) { diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index de1e92533a..8a08dc8f0b 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -8,11 +8,11 @@ import { moderateFeedGenerator, RichText, } from '@atproto/api' +import {t} from '@lingui/macro' import { type InfiniteData, keepPreviousData, type QueryClient, - type QueryKey, useInfiniteQuery, useMutation, useQuery, @@ -22,7 +22,11 @@ import { import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' -import {STALE} from '#/state/queries' +import { + PERSISTED_QUERY_GCTIME, + PERSISTED_QUERY_ROOT, + STALE, +} from '#/state/queries' import {RQKEY as listQueryKey} from '#/state/queries/list' import {usePreferencesQuery} from '#/state/queries/preferences' import {useAgent, useSession} from '#/state/session' @@ -114,7 +118,7 @@ export function hydrateFeedGenerator( avatar: view.avatar, displayName: view.displayName ? sanitizeDisplayName(view.displayName) - : `Feed by ${sanitizeHandle(view.creator.handle, '@')}`, + : t`Feed by ${sanitizeHandle(view.creator.handle, '@')}`, description: new RichText({ text: view.description || '', facets: (view.descriptionFacets || [])?.slice(), @@ -155,7 +159,7 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo { creatorHandle: view.creator.handle, displayName: view.name ? sanitizeDisplayName(view.name) - : `User List by ${sanitizeHandle(view.creator.handle, '@')}`, + : t`User List by ${sanitizeHandle(view.creator.handle, '@')}`, contentMode: undefined, } } @@ -238,13 +242,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { ) const lastPageCountRef = useRef(0) - const query = useInfiniteQuery< - AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema, - Error, - InfiniteData, - QueryKey, - string | undefined - >({ + const query = useInfiniteQuery({ enabled: Boolean(moderationOpts) && options?.enabled !== false, queryKey: createGetPopularFeedsQueryKey(options), queryFn: async ({pageParam}) => { @@ -261,7 +259,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { return res.data }, - initialPageParam: undefined, + initialPageParam: undefined as string | undefined, getNextPageParam: lastPage => lastPage.cursor, select: useCallback( ( @@ -418,7 +416,10 @@ const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = { contentMode: undefined, } -const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos' +const createPinnedFeedInfosQueryKeyRoot = ( + kind: 'pinned' | 'saved', + feedUris: string[], +) => [PERSISTED_QUERY_ROOT, 'feed-info', kind, feedUris] export function usePinnedFeedsInfos() { const {hasSession} = useSession() @@ -427,13 +428,13 @@ export function usePinnedFeedsInfos() { const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? [] return useQuery({ + queryKey: createPinnedFeedInfosQueryKeyRoot( + 'pinned', + pinnedItems.map(f => f.value), + ), + gcTime: PERSISTED_QUERY_GCTIME, staleTime: STALE.INFINITY, enabled: !isLoadingPrefs, - queryKey: [ - pinnedFeedInfosQueryKeyRoot, - (hasSession ? 'authed:' : 'unauthed:') + - pinnedItems.map(f => f.value).join(','), - ], queryFn: async () => { if (!hasSession) { return [PWI_DISCOVER_FEED_STUB] @@ -535,9 +536,13 @@ export function useSavedFeeds() { const queryClient = useQueryClient() return useQuery({ + queryKey: createPinnedFeedInfosQueryKeyRoot( + 'saved', + savedItems.map(f => f.value), + ), + gcTime: PERSISTED_QUERY_GCTIME, staleTime: STALE.INFINITY, enabled: !isLoadingPrefs, - queryKey: [pinnedFeedInfosQueryKeyRoot, ...savedItems], placeholderData: previousData => { return ( previousData || { diff --git a/src/state/queries/index.ts b/src/state/queries/index.ts index cdc47bf99c..bac6931fd5 100644 --- a/src/state/queries/index.ts +++ b/src/state/queries/index.ts @@ -18,3 +18,23 @@ export const STALE = { }, 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). + * + * Note that for queries that are persisted, it is recommended to extend + * the `gcTime` to a longer duration, otherwise it'll get busted + */ +export const PERSISTED_QUERY_ROOT = 'PERSISTED' +export const PERSISTED_QUERY_GCTIME = Infinity diff --git a/src/state/queries/labeler.ts b/src/state/queries/labeler.ts index 4eddb27f4e..465a2b57a3 100644 --- a/src/state/queries/labeler.ts +++ b/src/state/queries/labeler.ts @@ -3,8 +3,11 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {z} from 'zod' import {MAX_LABELERS} from '#/lib/constants' -import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query' -import {STALE} from '#/state/queries' +import { + PERSISTED_QUERY_GCTIME, + PERSISTED_QUERY_ROOT, + STALE, +} from '#/state/queries' import { preferencesQueryKey, usePreferencesQuery, @@ -23,8 +26,9 @@ export const labelersInfoQueryKey = (dids: string[]) => [ dids.slice().sort(), ] -export const labelersDetailedInfoQueryKey = (dids: string[]) => [ - labelersDetailedInfoQueryKeyRoot, +const persistedLabelersDetailedInfoQueryKey = (dids: string[]) => [ + PERSISTED_QUERY_ROOT, + 'labelers-detailed-info', dids, ] @@ -65,8 +69,8 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) { const agent = useAgent() return useQuery({ enabled: !!dids.length, - queryKey: labelersDetailedInfoQueryKey(dids), - gcTime: 1000 * 60 * 60 * 6, // 6 hours + queryKey: persistedLabelersDetailedInfoQueryKey(dids), + gcTime: PERSISTED_QUERY_GCTIME, staleTime: STALE.MINUTES.ONE, queryFn: async () => { const res = await agent.app.bsky.labeler.getServices({ diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index c1eb590136..58df94fcc3 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -9,7 +9,11 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {PROD_DEFAULT_FEED} from '#/lib/constants' import {replaceEqualDeep} from '#/lib/functions' import {getAge} from '#/lib/strings/time' -import {STALE} from '#/state/queries' +import { + PERSISTED_QUERY_GCTIME, + PERSISTED_QUERY_ROOT, + STALE, +} from '#/state/queries' import { DEFAULT_HOME_FEED_PREFS, DEFAULT_LOGGED_OUT_PREFERENCES, @@ -29,18 +33,18 @@ export * from '#/state/queries/preferences/const' export * from '#/state/queries/preferences/moderation' export * from '#/state/queries/preferences/types' -const preferencesQueryKeyRoot = 'getPreferences' -export const preferencesQueryKey = [preferencesQueryKeyRoot] +export const preferencesQueryKey = [PERSISTED_QUERY_ROOT, 'getPreferences'] export function usePreferencesQuery() { const agent = useAgent() const aa = useAgeAssurance() - return useQuery({ + const query = useQuery({ staleTime: STALE.SECONDS.FIFTEEN, structuralSharing: replaceEqualDeep, refetchOnWindowFocus: true, queryKey: preferencesQueryKey, + gcTime: PERSISTED_QUERY_GCTIME, queryFn: async () => { if (!agent.did) { return DEFAULT_LOGGED_OUT_PREFERENCES @@ -92,6 +96,15 @@ export function usePreferencesQuery() { [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() { diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 5c667c39c0..4a70fc4c09 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -1,4 +1,13 @@ -import React from 'react' +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + useSyncExternalStore, +} from 'react' import {type AtpSessionEvent, type BskyAgent} from '@atproto/api' import * as persisted from '#/state/persisted' @@ -19,6 +28,8 @@ import {type Action, getInitialState, reducer, type State} from './reducer' export {isSignupQueued} from './util' import {addSessionDebugLog} from './logging' export type {SessionAccount} from '#/state/session/types' + +import {clearPersistedQueryStorage} from '#/lib/persisted-query-storage' import { type SessionApiContext, type SessionStateContext, @@ -29,21 +40,21 @@ import { clearAgeAssuranceDataForDid, } from '#/ageAssurance/data' -const StateContext = React.createContext({ +const StateContext = createContext({ accounts: [], currentAccount: undefined, hasSession: false, }) StateContext.displayName = 'SessionStateContext' -const AgentContext = React.createContext(null) +const AgentContext = createContext(null) AgentContext.displayName = 'SessionAgentContext' -const ApiContext = React.createContext({ +const ApiContext = createContext({ createAccount: async () => {}, login: async () => {}, - logoutCurrentAccount: async () => {}, - logoutEveryAccount: async () => {}, + logoutCurrentAccount: () => {}, + logoutEveryAccount: () => {}, resumeSession: async () => {}, removeAccount: () => {}, partialRefreshSession: async () => {}, @@ -94,11 +105,11 @@ class SessionStore { export function Provider({children}: React.PropsWithChildren<{}>) { const ax = useAnalyticsBase() const cancelPendingTask = useOneTaskAtATime() - const [store] = React.useState(() => new SessionStore()) - const state = React.useSyncExternalStore(store.subscribe, store.getState) + const [store] = useState(() => new SessionStore()) + const state = useSyncExternalStore(store.subscribe, store.getState) const onboardingDispatch = useOnboardingDispatch() - const onAgentSessionChange = React.useCallback( + const onAgentSessionChange = useCallback( (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away. if (sessionEvent === 'expired' || sessionEvent === 'create-failed') { @@ -115,7 +126,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [store], ) - const createAccount = React.useCallback( + const createAccount = useCallback( async (params, metrics) => { addSessionDebugLog({type: 'method:start', method: 'createAccount'}) const signal = cancelPendingTask() @@ -141,7 +152,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [ax, store, onAgentSessionChange, cancelPendingTask], ) - const login = React.useCallback( + const login = useCallback( async (params, logContext) => { addSessionDebugLog({type: 'method:start', method: 'login'}) const signal = cancelPendingTask() @@ -168,7 +179,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [ax, store, onAgentSessionChange, cancelPendingTask], ) - const logoutCurrentAccount = React.useCallback< + const logoutCurrentAccount = useCallback< SessionApiContext['logoutCurrentAccount'] >( logContext => { @@ -192,6 +203,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { addSessionDebugLog({type: 'method:end', method: 'logout'}) if (prevState.currentAgentState.did) { clearAgeAssuranceDataForDid({did: prevState.currentAgentState.did}) + void clearPersistedQueryStorage(prevState.currentAgentState.did) } // reset onboarding flow on logout onboardingDispatch({type: 'skip'}) @@ -199,7 +211,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [ax, store, cancelPendingTask, onboardingDispatch], ) - const logoutEveryAccount = React.useCallback< + const logoutEveryAccount = useCallback< SessionApiContext['logoutEveryAccount'] >( logContext => { @@ -222,13 +234,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) addSessionDebugLog({type: 'method:end', method: 'logout'}) clearAgeAssuranceData() + for (const account of prevState.accounts) { + void clearPersistedQueryStorage(account.did) + } // reset onboarding flow on logout onboardingDispatch({type: 'skip'}) }, - [store, cancelPendingTask, onboardingDispatch], + [store, cancelPendingTask, onboardingDispatch, ax], ) - const resumeSession = React.useCallback( + const resumeSession = useCallback( async (storedAccount, isSwitchingAccounts = false) => { addSessionDebugLog({ type: 'method:start', @@ -258,7 +273,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [store, onAgentSessionChange, cancelPendingTask, onboardingDispatch], ) - const partialRefreshSession = React.useCallback< + const partialRefreshSession = useCallback< SessionApiContext['partialRefreshSession'] >(async () => { const agent = state.currentAgentState.agent as BskyAppAgent @@ -275,7 +290,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }) }, [store, state, cancelPendingTask]) - const removeAccount = React.useCallback( + const removeAccount = useCallback( account => { addSessionDebugLog({ type: 'method:start', @@ -292,7 +307,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }, [store, cancelPendingTask], ) - React.useEffect(() => { + useEffect(() => { return persisted.onUpdate('session', nextSession => { const synced = nextSession addSessionDebugLog({type: 'persisted:receive', data: synced}) @@ -322,7 +337,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }) }, [store, state, resumeSession]) - const stateContext = React.useMemo( + const stateContext = useMemo( () => ({ accounts: state.accounts, currentAccount: state.accounts.find( @@ -333,7 +348,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [state], ) - const api = React.useMemo( + const api = useMemo( () => ({ createAccount, login, @@ -358,8 +373,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { if (__DEV__ && IS_WEB) window.agent = state.currentAgentState.agent const agent = state.currentAgentState.agent as BskyAppAgent - const currentAgentRef = React.useRef(agent) - React.useEffect(() => { + const currentAgentRef = useRef(agent) + useEffect(() => { if (currentAgentRef.current !== agent) { // Read the previous value and immediately advance the pointer. const prevAgent = currentAgentRef.current @@ -390,8 +405,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } function useOneTaskAtATime() { - const abortController = React.useRef(null) - const cancelPendingTask = React.useCallback(() => { + const abortController = useRef(null) + const cancelPendingTask = useCallback(() => { if (abortController.current) { abortController.current.abort() } @@ -402,11 +417,11 @@ function useOneTaskAtATime() { } export function useSession() { - return React.useContext(StateContext) + return useContext(StateContext) } export function useSessionApi() { - return React.useContext(ApiContext) + return useContext(ApiContext) } export function useRequireAuth() { @@ -414,7 +429,7 @@ export function useRequireAuth() { const closeAll = useCloseAllActiveElements() const {signinDialogControl} = useGlobalDialogsControlContext() - return React.useCallback( + return useCallback( (fn: () => void) => { if (hasSession) { fn() @@ -428,7 +443,7 @@ export function useRequireAuth() { } export function useAgent(): BskyAgent { - const agent = React.useContext(AgentContext) + const agent = useContext(AgentContext) if (!agent) { throw Error('useAgent() must be below .') } diff --git a/src/storage/archive/db/index.ts b/src/storage/archive/db/index.ts index 02202a8d95..f554d5f5c0 100644 --- a/src/storage/archive/db/index.ts +++ b/src/storage/archive/db/index.ts @@ -6,17 +6,17 @@ export function create({id}: {id: string}): DB { const store = new MMKV({id}) return { - async get(key: string): Promise { - return store.getString(key) ?? undefined + get(key: string) { + return store.getString(key) }, - async set(key: string, value: string): Promise { - store.set(key, value) + set(key: string, value: string) { + return store.set(key, value) }, - async delete(key: string): Promise { - store.delete(key) + delete(key: string) { + return store.delete(key) }, - async clear(): Promise { - store.clearAll() + clear() { + return store.clearAll() }, } } diff --git a/src/storage/archive/db/index.web.ts b/src/storage/archive/db/index.web.ts index f0f75633b9..d36d36eec6 100644 --- a/src/storage/archive/db/index.web.ts +++ b/src/storage/archive/db/index.web.ts @@ -6,17 +6,17 @@ export function create({id}: {id: string}): DB { const store = createStore(id, id) return { - async get(key: string): Promise { - return get(key, store) ?? undefined + get(key: string) { + return get(key, store) }, - async set(key: string, value: string): Promise { - await set(key, value, store) + set(key: string, value: string) { + return set(key, value, store) }, - async delete(key: string): Promise { - await del(key, store) + delete(key: string) { + return del(key, store) }, - async clear(): Promise { - await clear(store) + clear() { + return clear(store) }, } } diff --git a/src/storage/archive/db/types.ts b/src/storage/archive/db/types.ts index 4582dfab61..388b2b8035 100644 --- a/src/storage/archive/db/types.ts +++ b/src/storage/archive/db/types.ts @@ -1,6 +1,7 @@ +type MaybePromise = T | Promise export type DB = { - get(key: string): Promise - set(key: string, value: string): Promise - delete(key: string): Promise - clear(): Promise + get(key: string): MaybePromise + set(key: string, value: string): MaybePromise + delete(key: string): MaybePromise + clear(): MaybePromise } diff --git a/yarn.lock b/yarn.lock index f50ea1d5c0..9351de69fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9722,6 +9722,13 @@ cookie@^0.7.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +copy-anything@^4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-4.0.5.tgz#16cabafd1ea4bb327a540b750f2b4df522825aea" + integrity sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA== + dependencies: + is-what "^5.2.0" + copy-webpack-plugin@^10.2.0: version "10.2.4" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz#6c854be3fdaae22025da34b9112ccf81c63308fe" @@ -13525,6 +13532,11 @@ is-weakset@^2.0.3: call-bound "^1.0.3" get-intrinsic "^1.2.6" +is-what@^5.2.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-5.5.0.tgz#a3031815757cfe1f03fed990bf6355a2d3f628c4" + integrity sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw== + is-wsl@^2.0.0, is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" @@ -18913,6 +18925,13 @@ sucrase@~3.35.1: tinyglobby "^0.2.11" ts-interface-checker "^0.1.9" +superjson@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.6.tgz#a223a3a988172a5f9656e2063fe5f733af40d099" + integrity sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA== + dependencies: + copy-anything "^4" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"