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:
+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 * 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<SessionStateContext>({
|
||||
const StateContext = createContext<SessionStateContext>({
|
||||
accounts: [],
|
||||
currentAccount: undefined,
|
||||
hasSession: false,
|
||||
})
|
||||
StateContext.displayName = 'SessionStateContext'
|
||||
|
||||
const AgentContext = React.createContext<BskyAgent | null>(null)
|
||||
const AgentContext = createContext<BskyAgent | null>(null)
|
||||
AgentContext.displayName = 'SessionAgentContext'
|
||||
|
||||
const ApiContext = React.createContext<SessionApiContext>({
|
||||
const ApiContext = createContext<SessionApiContext>({
|
||||
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<SessionApiContext['createAccount']>(
|
||||
const createAccount = useCallback<SessionApiContext['createAccount']>(
|
||||
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<SessionApiContext['login']>(
|
||||
const login = useCallback<SessionApiContext['login']>(
|
||||
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<SessionApiContext['resumeSession']>(
|
||||
const resumeSession = useCallback<SessionApiContext['resumeSession']>(
|
||||
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<SessionApiContext['removeAccount']>(
|
||||
const removeAccount = useCallback<SessionApiContext['removeAccount']>(
|
||||
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<AbortController | null>(null)
|
||||
const cancelPendingTask = React.useCallback(() => {
|
||||
const abortController = useRef<AbortController | null>(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 <SessionProvider>.')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user