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:
@@ -35,6 +35,12 @@ cfg.resolver.resolveRequest = (context, moduleName, platform) => {
|
|||||||
if (moduleName === '@ipld/dag-cbor') {
|
if (moduleName === '@ipld/dag-cbor') {
|
||||||
return context.resolveRequest(context, '@ipld/dag-cbor/src', platform)
|
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 (process.env.BSKY_PROFILE) {
|
||||||
if (moduleName.endsWith('ReactNativeRenderer-prod')) {
|
if (moduleName.endsWith('ReactNativeRenderer-prod')) {
|
||||||
return context.resolveRequest(
|
return context.resolveRequest(
|
||||||
|
|||||||
@@ -222,6 +222,7 @@
|
|||||||
"react-textarea-autosize": "^8.5.3",
|
"react-textarea-autosize": "^8.5.3",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"sonner-native": "^0.21.0",
|
"sonner-native": "^0.21.0",
|
||||||
|
"superjson": "^2.2.6",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"tlds": "^1.234.0",
|
"tlds": "^1.234.0",
|
||||||
"tldts": "^6.1.46",
|
"tldts": "^6.1.46",
|
||||||
|
|||||||
+2
-1
@@ -146,7 +146,8 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
withTiming(
|
withTiming(
|
||||||
1,
|
1,
|
||||||
{duration: 400, easing: Easing.out(Easing.cubic)},
|
{duration: 400, easing: Easing.out(Easing.cubic)},
|
||||||
async () => {
|
() => {
|
||||||
|
'worklet'
|
||||||
// set these values to check animation at specific point
|
// set these values to check animation at specific point
|
||||||
outroLogo.set(() =>
|
outroLogo.set(() =>
|
||||||
withTiming(
|
withTiming(
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 {useEffect, 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 {
|
||||||
|
type PersistQueryClientOptions,
|
||||||
PersistQueryClientProvider,
|
PersistQueryClientProvider,
|
||||||
type PersistQueryClientProviderProps,
|
type PersistQueryClientProviderProps,
|
||||||
} from '@tanstack/react-query-persist-client'
|
} 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 {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
|
||||||
|
import {PERSISTED_QUERY_ROOT} from '#/state/queries'
|
||||||
|
import * as env from '#/env'
|
||||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
__TANSTACK_QUERY_CLIENT__: import('@tanstack/query-core').QueryClient
|
__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> {
|
async function checkIsOnline(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
@@ -138,7 +137,8 @@ const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehyd
|
|||||||
{
|
{
|
||||||
shouldDehydrateMutation: (_: any) => false,
|
shouldDehydrateMutation: (_: any) => false,
|
||||||
shouldDehydrateQuery: query => {
|
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.
|
// 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(currentDid ?? 'logged-out')
|
||||||
const asyncPersister = createAsyncStoragePersister({
|
const asyncPersister = createAsyncStoragePersister({
|
||||||
storage: AsyncStorage,
|
storage,
|
||||||
key: 'queryClient-' + (currentDid ?? 'logged-out'),
|
key: 'queryClient-' + (currentDid ?? 'logged-out'),
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
persister: asyncPersister,
|
persister: asyncPersister,
|
||||||
dehydrateOptions,
|
dehydrateOptions,
|
||||||
}
|
buster: env.APP_VERSION,
|
||||||
|
} satisfies Omit<PersistQueryClientOptions, 'queryClient'>
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (IS_WEB) {
|
if (IS_WEB) {
|
||||||
|
|||||||
+24
-19
@@ -8,11 +8,11 @@ import {
|
|||||||
moderateFeedGenerator,
|
moderateFeedGenerator,
|
||||||
RichText,
|
RichText,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
import {t} from '@lingui/macro'
|
||||||
import {
|
import {
|
||||||
type InfiniteData,
|
type InfiniteData,
|
||||||
keepPreviousData,
|
keepPreviousData,
|
||||||
type QueryClient,
|
type QueryClient,
|
||||||
type QueryKey,
|
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
@@ -22,7 +22,11 @@ import {
|
|||||||
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
|
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
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 {RQKEY as listQueryKey} from '#/state/queries/list'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
@@ -114,7 +118,7 @@ export function hydrateFeedGenerator(
|
|||||||
avatar: view.avatar,
|
avatar: view.avatar,
|
||||||
displayName: view.displayName
|
displayName: view.displayName
|
||||||
? sanitizeDisplayName(view.displayName)
|
? sanitizeDisplayName(view.displayName)
|
||||||
: `Feed by ${sanitizeHandle(view.creator.handle, '@')}`,
|
: t`Feed by ${sanitizeHandle(view.creator.handle, '@')}`,
|
||||||
description: new RichText({
|
description: new RichText({
|
||||||
text: view.description || '',
|
text: view.description || '',
|
||||||
facets: (view.descriptionFacets || [])?.slice(),
|
facets: (view.descriptionFacets || [])?.slice(),
|
||||||
@@ -155,7 +159,7 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
|
|||||||
creatorHandle: view.creator.handle,
|
creatorHandle: view.creator.handle,
|
||||||
displayName: view.name
|
displayName: view.name
|
||||||
? sanitizeDisplayName(view.name)
|
? sanitizeDisplayName(view.name)
|
||||||
: `User List by ${sanitizeHandle(view.creator.handle, '@')}`,
|
: t`User List by ${sanitizeHandle(view.creator.handle, '@')}`,
|
||||||
contentMode: undefined,
|
contentMode: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,13 +242,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
|
|||||||
)
|
)
|
||||||
const lastPageCountRef = useRef(0)
|
const lastPageCountRef = useRef(0)
|
||||||
|
|
||||||
const query = useInfiniteQuery<
|
const query = useInfiniteQuery({
|
||||||
AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema,
|
|
||||||
Error,
|
|
||||||
InfiniteData<AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema>,
|
|
||||||
QueryKey,
|
|
||||||
string | undefined
|
|
||||||
>({
|
|
||||||
enabled: Boolean(moderationOpts) && options?.enabled !== false,
|
enabled: Boolean(moderationOpts) && options?.enabled !== false,
|
||||||
queryKey: createGetPopularFeedsQueryKey(options),
|
queryKey: createGetPopularFeedsQueryKey(options),
|
||||||
queryFn: async ({pageParam}) => {
|
queryFn: async ({pageParam}) => {
|
||||||
@@ -261,7 +259,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
|
|||||||
|
|
||||||
return res.data
|
return res.data
|
||||||
},
|
},
|
||||||
initialPageParam: undefined,
|
initialPageParam: undefined as string | undefined,
|
||||||
getNextPageParam: lastPage => lastPage.cursor,
|
getNextPageParam: lastPage => lastPage.cursor,
|
||||||
select: useCallback(
|
select: useCallback(
|
||||||
(
|
(
|
||||||
@@ -418,7 +416,10 @@ const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = {
|
|||||||
contentMode: undefined,
|
contentMode: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
|
const createPinnedFeedInfosQueryKeyRoot = (
|
||||||
|
kind: 'pinned' | 'saved',
|
||||||
|
feedUris: string[],
|
||||||
|
) => [PERSISTED_QUERY_ROOT, 'feed-info', kind, feedUris]
|
||||||
|
|
||||||
export function usePinnedFeedsInfos() {
|
export function usePinnedFeedsInfos() {
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
@@ -427,13 +428,13 @@ export function usePinnedFeedsInfos() {
|
|||||||
const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? []
|
const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? []
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
queryKey: createPinnedFeedInfosQueryKeyRoot(
|
||||||
|
'pinned',
|
||||||
|
pinnedItems.map(f => f.value),
|
||||||
|
),
|
||||||
|
gcTime: PERSISTED_QUERY_GCTIME,
|
||||||
staleTime: STALE.INFINITY,
|
staleTime: STALE.INFINITY,
|
||||||
enabled: !isLoadingPrefs,
|
enabled: !isLoadingPrefs,
|
||||||
queryKey: [
|
|
||||||
pinnedFeedInfosQueryKeyRoot,
|
|
||||||
(hasSession ? 'authed:' : 'unauthed:') +
|
|
||||||
pinnedItems.map(f => f.value).join(','),
|
|
||||||
],
|
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!hasSession) {
|
if (!hasSession) {
|
||||||
return [PWI_DISCOVER_FEED_STUB]
|
return [PWI_DISCOVER_FEED_STUB]
|
||||||
@@ -535,9 +536,13 @@ export function useSavedFeeds() {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
queryKey: createPinnedFeedInfosQueryKeyRoot(
|
||||||
|
'saved',
|
||||||
|
savedItems.map(f => f.value),
|
||||||
|
),
|
||||||
|
gcTime: PERSISTED_QUERY_GCTIME,
|
||||||
staleTime: STALE.INFINITY,
|
staleTime: STALE.INFINITY,
|
||||||
enabled: !isLoadingPrefs,
|
enabled: !isLoadingPrefs,
|
||||||
queryKey: [pinnedFeedInfosQueryKeyRoot, ...savedItems],
|
|
||||||
placeholderData: previousData => {
|
placeholderData: previousData => {
|
||||||
return (
|
return (
|
||||||
previousData || {
|
previousData || {
|
||||||
|
|||||||
@@ -18,3 +18,23 @@ export const STALE = {
|
|||||||
},
|
},
|
||||||
INFINITY: Infinity,
|
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
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
|||||||
import {z} from 'zod'
|
import {z} from 'zod'
|
||||||
|
|
||||||
import {MAX_LABELERS} from '#/lib/constants'
|
import {MAX_LABELERS} from '#/lib/constants'
|
||||||
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
|
import {
|
||||||
import {STALE} from '#/state/queries'
|
PERSISTED_QUERY_GCTIME,
|
||||||
|
PERSISTED_QUERY_ROOT,
|
||||||
|
STALE,
|
||||||
|
} from '#/state/queries'
|
||||||
import {
|
import {
|
||||||
preferencesQueryKey,
|
preferencesQueryKey,
|
||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
@@ -23,8 +26,9 @@ export const labelersInfoQueryKey = (dids: string[]) => [
|
|||||||
dids.slice().sort(),
|
dids.slice().sort(),
|
||||||
]
|
]
|
||||||
|
|
||||||
export const labelersDetailedInfoQueryKey = (dids: string[]) => [
|
const persistedLabelersDetailedInfoQueryKey = (dids: string[]) => [
|
||||||
labelersDetailedInfoQueryKeyRoot,
|
PERSISTED_QUERY_ROOT,
|
||||||
|
'labelers-detailed-info',
|
||||||
dids,
|
dids,
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -65,8 +69,8 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
|
|||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: !!dids.length,
|
enabled: !!dids.length,
|
||||||
queryKey: labelersDetailedInfoQueryKey(dids),
|
queryKey: persistedLabelersDetailedInfoQueryKey(dids),
|
||||||
gcTime: 1000 * 60 * 60 * 6, // 6 hours
|
gcTime: PERSISTED_QUERY_GCTIME,
|
||||||
staleTime: STALE.MINUTES.ONE,
|
staleTime: STALE.MINUTES.ONE,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await agent.app.bsky.labeler.getServices({
|
const res = await agent.app.bsky.labeler.getServices({
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
|||||||
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
||||||
import {replaceEqualDeep} from '#/lib/functions'
|
import {replaceEqualDeep} from '#/lib/functions'
|
||||||
import {getAge} from '#/lib/strings/time'
|
import {getAge} from '#/lib/strings/time'
|
||||||
import {STALE} from '#/state/queries'
|
import {
|
||||||
|
PERSISTED_QUERY_GCTIME,
|
||||||
|
PERSISTED_QUERY_ROOT,
|
||||||
|
STALE,
|
||||||
|
} from '#/state/queries'
|
||||||
import {
|
import {
|
||||||
DEFAULT_HOME_FEED_PREFS,
|
DEFAULT_HOME_FEED_PREFS,
|
||||||
DEFAULT_LOGGED_OUT_PREFERENCES,
|
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/moderation'
|
||||||
export * from '#/state/queries/preferences/types'
|
export * from '#/state/queries/preferences/types'
|
||||||
|
|
||||||
const preferencesQueryKeyRoot = 'getPreferences'
|
export const preferencesQueryKey = [PERSISTED_QUERY_ROOT, 'getPreferences']
|
||||||
export const preferencesQueryKey = [preferencesQueryKeyRoot]
|
|
||||||
|
|
||||||
export function usePreferencesQuery() {
|
export function usePreferencesQuery() {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const aa = useAgeAssurance()
|
const aa = useAgeAssurance()
|
||||||
|
|
||||||
return useQuery({
|
const query = useQuery({
|
||||||
staleTime: STALE.SECONDS.FIFTEEN,
|
staleTime: STALE.SECONDS.FIFTEEN,
|
||||||
structuralSharing: replaceEqualDeep,
|
structuralSharing: replaceEqualDeep,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
queryKey: preferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
|
gcTime: PERSISTED_QUERY_GCTIME,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!agent.did) {
|
if (!agent.did) {
|
||||||
return DEFAULT_LOGGED_OUT_PREFERENCES
|
return DEFAULT_LOGGED_OUT_PREFERENCES
|
||||||
@@ -92,6 +96,15 @@ export function usePreferencesQuery() {
|
|||||||
[aa],
|
[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() {
|
export function useClearPreferencesMutation() {
|
||||||
|
|||||||
+43
-28
@@ -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 {type AtpSessionEvent, type BskyAgent} from '@atproto/api'
|
||||||
|
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
@@ -19,6 +28,8 @@ import {type Action, getInitialState, reducer, type State} from './reducer'
|
|||||||
export {isSignupQueued} from './util'
|
export {isSignupQueued} from './util'
|
||||||
import {addSessionDebugLog} from './logging'
|
import {addSessionDebugLog} from './logging'
|
||||||
export type {SessionAccount} from '#/state/session/types'
|
export type {SessionAccount} from '#/state/session/types'
|
||||||
|
|
||||||
|
import {clearPersistedQueryStorage} from '#/lib/persisted-query-storage'
|
||||||
import {
|
import {
|
||||||
type SessionApiContext,
|
type SessionApiContext,
|
||||||
type SessionStateContext,
|
type SessionStateContext,
|
||||||
@@ -29,21 +40,21 @@ import {
|
|||||||
clearAgeAssuranceDataForDid,
|
clearAgeAssuranceDataForDid,
|
||||||
} from '#/ageAssurance/data'
|
} from '#/ageAssurance/data'
|
||||||
|
|
||||||
const StateContext = React.createContext<SessionStateContext>({
|
const StateContext = createContext<SessionStateContext>({
|
||||||
accounts: [],
|
accounts: [],
|
||||||
currentAccount: undefined,
|
currentAccount: undefined,
|
||||||
hasSession: false,
|
hasSession: false,
|
||||||
})
|
})
|
||||||
StateContext.displayName = 'SessionStateContext'
|
StateContext.displayName = 'SessionStateContext'
|
||||||
|
|
||||||
const AgentContext = React.createContext<BskyAgent | null>(null)
|
const AgentContext = createContext<BskyAgent | null>(null)
|
||||||
AgentContext.displayName = 'SessionAgentContext'
|
AgentContext.displayName = 'SessionAgentContext'
|
||||||
|
|
||||||
const ApiContext = React.createContext<SessionApiContext>({
|
const ApiContext = createContext<SessionApiContext>({
|
||||||
createAccount: async () => {},
|
createAccount: async () => {},
|
||||||
login: async () => {},
|
login: async () => {},
|
||||||
logoutCurrentAccount: async () => {},
|
logoutCurrentAccount: () => {},
|
||||||
logoutEveryAccount: async () => {},
|
logoutEveryAccount: () => {},
|
||||||
resumeSession: async () => {},
|
resumeSession: async () => {},
|
||||||
removeAccount: () => {},
|
removeAccount: () => {},
|
||||||
partialRefreshSession: async () => {},
|
partialRefreshSession: async () => {},
|
||||||
@@ -94,11 +105,11 @@ class SessionStore {
|
|||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
const ax = useAnalyticsBase()
|
const ax = useAnalyticsBase()
|
||||||
const cancelPendingTask = useOneTaskAtATime()
|
const cancelPendingTask = useOneTaskAtATime()
|
||||||
const [store] = React.useState(() => new SessionStore())
|
const [store] = useState(() => new SessionStore())
|
||||||
const state = React.useSyncExternalStore(store.subscribe, store.getState)
|
const state = useSyncExternalStore(store.subscribe, store.getState)
|
||||||
const onboardingDispatch = useOnboardingDispatch()
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
|
|
||||||
const onAgentSessionChange = React.useCallback(
|
const onAgentSessionChange = useCallback(
|
||||||
(agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => {
|
(agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => {
|
||||||
const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away.
|
const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away.
|
||||||
if (sessionEvent === 'expired' || sessionEvent === 'create-failed') {
|
if (sessionEvent === 'expired' || sessionEvent === 'create-failed') {
|
||||||
@@ -115,7 +126,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
[store],
|
[store],
|
||||||
)
|
)
|
||||||
|
|
||||||
const createAccount = React.useCallback<SessionApiContext['createAccount']>(
|
const createAccount = useCallback<SessionApiContext['createAccount']>(
|
||||||
async (params, metrics) => {
|
async (params, metrics) => {
|
||||||
addSessionDebugLog({type: 'method:start', method: 'createAccount'})
|
addSessionDebugLog({type: 'method:start', method: 'createAccount'})
|
||||||
const signal = cancelPendingTask()
|
const signal = cancelPendingTask()
|
||||||
@@ -141,7 +152,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
[ax, store, onAgentSessionChange, cancelPendingTask],
|
[ax, store, onAgentSessionChange, cancelPendingTask],
|
||||||
)
|
)
|
||||||
|
|
||||||
const login = React.useCallback<SessionApiContext['login']>(
|
const login = useCallback<SessionApiContext['login']>(
|
||||||
async (params, logContext) => {
|
async (params, logContext) => {
|
||||||
addSessionDebugLog({type: 'method:start', method: 'login'})
|
addSessionDebugLog({type: 'method:start', method: 'login'})
|
||||||
const signal = cancelPendingTask()
|
const signal = cancelPendingTask()
|
||||||
@@ -168,7 +179,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
[ax, store, onAgentSessionChange, cancelPendingTask],
|
[ax, store, onAgentSessionChange, cancelPendingTask],
|
||||||
)
|
)
|
||||||
|
|
||||||
const logoutCurrentAccount = React.useCallback<
|
const logoutCurrentAccount = useCallback<
|
||||||
SessionApiContext['logoutCurrentAccount']
|
SessionApiContext['logoutCurrentAccount']
|
||||||
>(
|
>(
|
||||||
logContext => {
|
logContext => {
|
||||||
@@ -192,6 +203,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
addSessionDebugLog({type: 'method:end', method: 'logout'})
|
addSessionDebugLog({type: 'method:end', method: 'logout'})
|
||||||
if (prevState.currentAgentState.did) {
|
if (prevState.currentAgentState.did) {
|
||||||
clearAgeAssuranceDataForDid({did: prevState.currentAgentState.did})
|
clearAgeAssuranceDataForDid({did: prevState.currentAgentState.did})
|
||||||
|
void clearPersistedQueryStorage(prevState.currentAgentState.did)
|
||||||
}
|
}
|
||||||
// reset onboarding flow on logout
|
// reset onboarding flow on logout
|
||||||
onboardingDispatch({type: 'skip'})
|
onboardingDispatch({type: 'skip'})
|
||||||
@@ -199,7 +211,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
[ax, store, cancelPendingTask, onboardingDispatch],
|
[ax, store, cancelPendingTask, onboardingDispatch],
|
||||||
)
|
)
|
||||||
|
|
||||||
const logoutEveryAccount = React.useCallback<
|
const logoutEveryAccount = useCallback<
|
||||||
SessionApiContext['logoutEveryAccount']
|
SessionApiContext['logoutEveryAccount']
|
||||||
>(
|
>(
|
||||||
logContext => {
|
logContext => {
|
||||||
@@ -222,13 +234,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
)
|
)
|
||||||
addSessionDebugLog({type: 'method:end', method: 'logout'})
|
addSessionDebugLog({type: 'method:end', method: 'logout'})
|
||||||
clearAgeAssuranceData()
|
clearAgeAssuranceData()
|
||||||
|
for (const account of prevState.accounts) {
|
||||||
|
void clearPersistedQueryStorage(account.did)
|
||||||
|
}
|
||||||
// reset onboarding flow on logout
|
// reset onboarding flow on logout
|
||||||
onboardingDispatch({type: 'skip'})
|
onboardingDispatch({type: 'skip'})
|
||||||
},
|
},
|
||||||
[store, cancelPendingTask, onboardingDispatch],
|
[store, cancelPendingTask, onboardingDispatch, ax],
|
||||||
)
|
)
|
||||||
|
|
||||||
const resumeSession = React.useCallback<SessionApiContext['resumeSession']>(
|
const resumeSession = useCallback<SessionApiContext['resumeSession']>(
|
||||||
async (storedAccount, isSwitchingAccounts = false) => {
|
async (storedAccount, isSwitchingAccounts = false) => {
|
||||||
addSessionDebugLog({
|
addSessionDebugLog({
|
||||||
type: 'method:start',
|
type: 'method:start',
|
||||||
@@ -258,7 +273,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
[store, onAgentSessionChange, cancelPendingTask, onboardingDispatch],
|
[store, onAgentSessionChange, cancelPendingTask, onboardingDispatch],
|
||||||
)
|
)
|
||||||
|
|
||||||
const partialRefreshSession = React.useCallback<
|
const partialRefreshSession = useCallback<
|
||||||
SessionApiContext['partialRefreshSession']
|
SessionApiContext['partialRefreshSession']
|
||||||
>(async () => {
|
>(async () => {
|
||||||
const agent = state.currentAgentState.agent as BskyAppAgent
|
const agent = state.currentAgentState.agent as BskyAppAgent
|
||||||
@@ -275,7 +290,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
})
|
})
|
||||||
}, [store, state, cancelPendingTask])
|
}, [store, state, cancelPendingTask])
|
||||||
|
|
||||||
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
|
const removeAccount = useCallback<SessionApiContext['removeAccount']>(
|
||||||
account => {
|
account => {
|
||||||
addSessionDebugLog({
|
addSessionDebugLog({
|
||||||
type: 'method:start',
|
type: 'method:start',
|
||||||
@@ -292,7 +307,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
},
|
},
|
||||||
[store, cancelPendingTask],
|
[store, cancelPendingTask],
|
||||||
)
|
)
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
return persisted.onUpdate('session', nextSession => {
|
return persisted.onUpdate('session', nextSession => {
|
||||||
const synced = nextSession
|
const synced = nextSession
|
||||||
addSessionDebugLog({type: 'persisted:receive', data: synced})
|
addSessionDebugLog({type: 'persisted:receive', data: synced})
|
||||||
@@ -322,7 +337,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
})
|
})
|
||||||
}, [store, state, resumeSession])
|
}, [store, state, resumeSession])
|
||||||
|
|
||||||
const stateContext = React.useMemo(
|
const stateContext = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
accounts: state.accounts,
|
accounts: state.accounts,
|
||||||
currentAccount: state.accounts.find(
|
currentAccount: state.accounts.find(
|
||||||
@@ -333,7 +348,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
[state],
|
[state],
|
||||||
)
|
)
|
||||||
|
|
||||||
const api = React.useMemo(
|
const api = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
createAccount,
|
createAccount,
|
||||||
login,
|
login,
|
||||||
@@ -358,8 +373,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
if (__DEV__ && IS_WEB) window.agent = state.currentAgentState.agent
|
if (__DEV__ && IS_WEB) window.agent = state.currentAgentState.agent
|
||||||
|
|
||||||
const agent = state.currentAgentState.agent as BskyAppAgent
|
const agent = state.currentAgentState.agent as BskyAppAgent
|
||||||
const currentAgentRef = React.useRef(agent)
|
const currentAgentRef = useRef(agent)
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentAgentRef.current !== agent) {
|
if (currentAgentRef.current !== agent) {
|
||||||
// Read the previous value and immediately advance the pointer.
|
// Read the previous value and immediately advance the pointer.
|
||||||
const prevAgent = currentAgentRef.current
|
const prevAgent = currentAgentRef.current
|
||||||
@@ -390,8 +405,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useOneTaskAtATime() {
|
function useOneTaskAtATime() {
|
||||||
const abortController = React.useRef<AbortController | null>(null)
|
const abortController = useRef<AbortController | null>(null)
|
||||||
const cancelPendingTask = React.useCallback(() => {
|
const cancelPendingTask = useCallback(() => {
|
||||||
if (abortController.current) {
|
if (abortController.current) {
|
||||||
abortController.current.abort()
|
abortController.current.abort()
|
||||||
}
|
}
|
||||||
@@ -402,11 +417,11 @@ function useOneTaskAtATime() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSession() {
|
export function useSession() {
|
||||||
return React.useContext(StateContext)
|
return useContext(StateContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSessionApi() {
|
export function useSessionApi() {
|
||||||
return React.useContext(ApiContext)
|
return useContext(ApiContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRequireAuth() {
|
export function useRequireAuth() {
|
||||||
@@ -414,7 +429,7 @@ export function useRequireAuth() {
|
|||||||
const closeAll = useCloseAllActiveElements()
|
const closeAll = useCloseAllActiveElements()
|
||||||
const {signinDialogControl} = useGlobalDialogsControlContext()
|
const {signinDialogControl} = useGlobalDialogsControlContext()
|
||||||
|
|
||||||
return React.useCallback(
|
return useCallback(
|
||||||
(fn: () => void) => {
|
(fn: () => void) => {
|
||||||
if (hasSession) {
|
if (hasSession) {
|
||||||
fn()
|
fn()
|
||||||
@@ -428,7 +443,7 @@ export function useRequireAuth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useAgent(): BskyAgent {
|
export function useAgent(): BskyAgent {
|
||||||
const agent = React.useContext(AgentContext)
|
const agent = useContext(AgentContext)
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
throw Error('useAgent() must be below <SessionProvider>.')
|
throw Error('useAgent() must be below <SessionProvider>.')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ export function create({id}: {id: string}): DB {
|
|||||||
const store = new MMKV({id})
|
const store = new MMKV({id})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async get(key: string): Promise<string | undefined> {
|
get(key: string) {
|
||||||
return store.getString(key) ?? undefined
|
return store.getString(key)
|
||||||
},
|
},
|
||||||
async set(key: string, value: string): Promise<void> {
|
set(key: string, value: string) {
|
||||||
store.set(key, value)
|
return store.set(key, value)
|
||||||
},
|
},
|
||||||
async delete(key: string): Promise<void> {
|
delete(key: string) {
|
||||||
store.delete(key)
|
return store.delete(key)
|
||||||
},
|
},
|
||||||
async clear(): Promise<void> {
|
clear() {
|
||||||
store.clearAll()
|
return store.clearAll()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ export function create({id}: {id: string}): DB {
|
|||||||
const store = createStore(id, id)
|
const store = createStore(id, id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async get(key: string): Promise<string | undefined> {
|
get(key: string) {
|
||||||
return get(key, store) ?? undefined
|
return get(key, store)
|
||||||
},
|
},
|
||||||
async set(key: string, value: string): Promise<void> {
|
set(key: string, value: string) {
|
||||||
await set(key, value, store)
|
return set(key, value, store)
|
||||||
},
|
},
|
||||||
async delete(key: string): Promise<void> {
|
delete(key: string) {
|
||||||
await del(key, store)
|
return del(key, store)
|
||||||
},
|
},
|
||||||
async clear(): Promise<void> {
|
clear() {
|
||||||
await clear(store)
|
return clear(store)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
type MaybePromise<T> = T | Promise<T>
|
||||||
export type DB = {
|
export type DB = {
|
||||||
get(key: string): Promise<string | undefined>
|
get(key: string): MaybePromise<string | undefined>
|
||||||
set(key: string, value: string): Promise<void>
|
set(key: string, value: string): MaybePromise<void>
|
||||||
delete(key: string): Promise<void>
|
delete(key: string): MaybePromise<void>
|
||||||
clear(): Promise<void>
|
clear(): MaybePromise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9722,6 +9722,13 @@ cookie@^0.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
|
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
|
||||||
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
|
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:
|
copy-webpack-plugin@^10.2.0:
|
||||||
version "10.2.4"
|
version "10.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz#6c854be3fdaae22025da34b9112ccf81c63308fe"
|
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"
|
call-bound "^1.0.3"
|
||||||
get-intrinsic "^1.2.6"
|
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:
|
is-wsl@^2.0.0, is-wsl@^2.1.1, is-wsl@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
|
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"
|
tinyglobby "^0.2.11"
|
||||||
ts-interface-checker "^0.1.9"
|
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:
|
supports-color@^5.3.0:
|
||||||
version "5.5.0"
|
version "5.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||||
|
|||||||
Reference in New Issue
Block a user