refactor: persist queries individually
6 unmodified lines789101112131415161718192021222316 unmodified lines6 unmodified linesimport { LOCALE } from '#/locale/intl/locale';
import App from '#/App';import { overwriteGetLocale } from '#/paraglide/runtime';import { prepareQueryCache } from '#/storage/query-cache';
// the UI locale is fixed for the page session (changing it reloads), so point paraglide at the// resolved LOCALE and stamp the document language once, up front.overwriteGetLocale(() => LOCALE);document.documentElement.lang = LOCALE;
initAppearance();prepareQueryCache();
const rootEl = document.getElementById('root');if (!rootEl) { throw new Error('Missing #root element');16 unmodified lines56 unmodified lines575859606161636263646544 unmodified lines56 unmodified lines "@oomfware/lightbox": "^0.1.9", "@oomfware/scion": "^0.1.5", "@oomfware/stacker": "^0.2.3", "@oomfware/tug": "^0.2.0", "@tanstack/query-async-storage-persister": "^5.102.7", "@tanstack/react-query": "^5.102.7", "@tanstack/react-query-persist-client": "^5.102.7", "@vanilla-extract/css": "^1.21.2", "@vanilla-extract/dynamic": "^2.1.5", "clsx": "^2.1.1", "dequal": "^2.0.3",44 unmodified lines131 unmodified linesExpand all132133134135136137138136137138142143144139140141142759 unmodified linesExpand all902903904905912913914906907908909513 unmodified linesExpand all142314241425142614361437143814271428142914421443144414451446144714481449145014301431143214331289 unmodified linesExpand all2723272427252726274827492727272827292730250 unmodified linesExpand all2981298229832984300830093010301130122985298630153016301730183019302030213022302330242987298829892990654 unmodified linesExpand all131 unmodified linesExpand all version: 0.2.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@oomfware/tug': specifier: ^0.2.0 version: 0.2.0 '@tanstack/query-async-storage-persister': specifier: ^5.102.7 version: 5.102.7 '@tanstack/react-query': specifier: ^5.102.7 version: 5.102.7(react@19.2.8) '@tanstack/react-query-persist-client': specifier: ^5.102.7 version: 5.102.7(@tanstack/react-query@5.102.7(react@19.2.8))(react@19.2.8) '@vanilla-extract/css': specifier: ^1.21.2 version: 1.21.2(patch_hash=db5f4b15486aaa4226da06516bfb01460b8d79a52973e5b8695b3a3bbc9a4a98) '@vanilla-extract/dynamic':759 unmodified linesExpand all
'@oxc-project/types@0.146.0': resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==}
'@oxc-project/types@0.147.0': resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==}
'@oxc-transform-react/binding-android-arm-eabi@0.147.0': resolution: {integrity: sha512-VRKQm1E8GA0yqii9t9f7EAOn3Y7wikAPjSdUDVQlfZx+p29MRG72oaVnS8vRjug2/a6wy8wrjlDiYi0fgvks3g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm]513 unmodified linesExpand all
'@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@tanstack/query-async-storage-persister@5.102.7': resolution: {integrity: sha512-c8COrQOEUG5fhK9XJ4ufZoWzxef2qrpz8lbNCxBXJ5VH8XUEZawe3ROoS0r2+1dIw9yh5mD/uBDzFV3Jpe2N6w==}
'@tanstack/query-core@5.102.7': resolution: {integrity: sha512-4TH8KQrCLoNs9Zvl1LW1iaZ9cZx3dGTgfZ1BnLrcRxMNnX/toe/dGbfvF/DkvbzRyfLfpMKZkembz3o0rnugtw==}
'@tanstack/query-persist-client-core@5.102.7': resolution: {integrity: sha512-nropT9sI7fOhoZEMUi2IYJnOj1OtCNmmThqaZlvIplQmPy9swhlr0cPW58FzyJbC1Z8X8SdzziAnB63fAH30Jg==}
'@tanstack/react-query-persist-client@5.102.7': resolution: {integrity: sha512-rLCR9zeKDv4f1eN1MSrHO2zJoCUpTQJfxcGygN4JaudEy2sJMVBqEvTgJz72ILvc0kHVkqONeAZb0gZ3fqfU1w==} peerDependencies: '@tanstack/react-query': ^5.102.7 react: ^18 || ^19
'@tanstack/react-query@5.102.7': resolution: {integrity: sha512-bbd8T4jDIj9aPbNn11SsjNH8apKY4zspkrw63JRsNDXjpW5SRF7X0ipi7yrGd8DCs8QhYfhQJKqEj1YLru1DbQ==} peerDependencies: react: ^18 || ^191289 unmodified linesExpand all - rolldown
'@oxc-project/types@0.146.0': {}
'@oxc-project/types@0.147.0': {}
'@oxc-transform-react/binding-android-arm-eabi@0.147.0': optional: true
'@oxc-transform-react/binding-android-arm64@0.147.0':250 unmodified linesExpand all '@sqlite.org/sqlite-wasm@3.48.0-build4': {}
'@standard-schema/spec@1.1.0': {}
'@tanstack/query-async-storage-persister@5.102.7': dependencies: '@tanstack/query-core': 5.102.7 '@tanstack/query-persist-client-core': 5.102.7
'@tanstack/query-core@5.102.7': {}
'@tanstack/query-persist-client-core@5.102.7': dependencies: '@tanstack/query-core': 5.102.7
'@tanstack/react-query-persist-client@5.102.7(@tanstack/react-query@5.102.7(react@19.2.8))(react@19.2.8)': dependencies: '@tanstack/query-persist-client-core': 5.102.7 '@tanstack/react-query': 5.102.7(react@19.2.8) react: 19.2.8
'@tanstack/react-query@5.102.7(react@19.2.8)': dependencies: '@tanstack/query-core': 5.102.7 react: 19.2.8654 unmodified linesExpand all12345678910111213141516171819202122232425262728293031323334353637import { 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 a localStorage-backed adapter for persisting react-query cache. * * @param id unique identifier for this storage instance. */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();}1234556689101178910111213141516151718191920212223104 unmodified linesExpand all1281291301311321331341351361371381321331341351361441371381391401414 unmodified lines146147148149157158159160161162163150151152153154155156157171172173174175176177178179180181182183184185186187158159import type { ReactNode } from 'react';
import type { AnyProfileView } from '@atcute/bluesky';import type { Did } from '@atcute/lexicons';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';import { focusManager, onlineManager, type Query, QueryCache, QueryClient } from '@tanstack/react-query';import { type PersistQueryClientOptions, PersistQueryClientProvider, type PersistQueryClientProviderProps,} from '@tanstack/react-query-persist-client'; focusManager, onlineManager, type Query, QueryCache, QueryClient, QueryClientProvider,} from '@tanstack/react-query';
import { isDocumentVisible } from '#/lib/browser/visibility';import { useConstant } from '#/lib/hooks/use-constant';import { createPersistedQueryStorage } from '#/lib/persisted-query-storage';
import { networkConfirmed, networkLost } from '#/state/events';import { PROFILE_RQKEY_ROOT } from '#/state/queries/profile-key';import { isQueryPersisted } from '#/state/queries/util';import { updateAccountProfile } from '#/state/session';
declare global { interface Window {104 unmodified linesExpand all }, }, });
const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] = { shouldDehydrateMutation: () => false, shouldDehydrateQuery: (query) => { return query.state.status === 'success' && isQueryPersisted(query.queryKey); },};
export function QueryProvider({ children, currentDid,}: { children: ReactNode; currentDid: string | undefined; currentDid: Did | undefined;}) { return ( <QueryProviderInner // key the provider by account so caches are not shared.4 unmodified lines </QueryProviderInner> );}
function QueryProviderInner({ children, currentDid,}: { children: ReactNode; currentDid: string | undefined;}) {function QueryProviderInner({ children, currentDid }: { children: ReactNode; currentDid: Did | undefined }) { // keep a mount-time snapshot for the account invariant. const initialDid = useConstant(() => currentDid); if (currentDid !== initialDid) { throw Error('Something is very wrong. Expected did to be stable due to key above.'); } // create the client inside the account-keyed subtree. const queryClient = useConstant(() => createQueryClient()); const persistOptions = useConstant(() => { const storage = createPersistedQueryStorage(currentDid ?? 'logged-out'); const asyncPersister = createAsyncStoragePersister({ storage, key: 'queryClient-' + (currentDid ?? 'logged-out'), }); return { persister: asyncPersister, dehydrateOptions, buster: import.meta.env.PUBLIC_GIT_COMMIT_HASH || 'dev', } satisfies Omit<PersistQueryClientOptions, 'queryClient'>; }); return ( <PersistQueryClientProvider client={queryClient} persistOptions={persistOptions}> {children} </PersistQueryClientProvider> ); return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364import { notifyManager, type Query, type QueryFunction, type QueryFunctionContext, type QueryKey,} from '@tanstack/react-query';
import { getCurrentDid } from '#/state/session/store';
import { persistedQueryCache } from '#/storage';import type { PersistedQueryEntry, QueryCacheScope } from '#/storage/schema';
/** * creates a query persister. * * @param options.version format version * @returns the persister */export const createQueryPersister = ({ version }: { version: number }) => { return async <T, TQueryKey extends QueryKey>( queryFn: QueryFunction<T, TQueryKey>, context: QueryFunctionContext<TQueryKey>, query: Query, ): Promise<T> => { const scope: QueryCacheScope = getCurrentDid() ?? 'logged-out';
if (query.state.data === undefined) { const stored = persistedQueryCache.get([scope, query.queryHash]); if (stored?.version === version) { notifyManager.schedule(() => { // preserve the cached fetch time for stale checks. query.setState({ dataUpdatedAt: stored.dataUpdatedAt }); if (query.isStale()) { void query.fetch(); } });
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- hash and version identify the query type return stored.data as T; }
if (stored !== undefined) { persistedQueryCache.remove([scope, query.queryHash]); } }
const data = await queryFn(context); notifyManager.schedule(() => { if (query.state.status !== 'success') { return; }
persistedQueryCache.set([scope, query.queryHash], { // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- query data must be JSON-serializable data: query.state.data as PersistedQueryEntry['data'], dataUpdatedAt: query.state.dataUpdatedAt, version, }); });
return data; };};1123456 unmodified lines1213141516171819202122232425262728import type { Account, Auth, Device } from '#/storage/schema';import type { Account, Auth, Device, QueryCache, QueryCacheScope } from '#/storage/schema';import { Storage } from '#/storage/storage';
export * from '#/storage/schema';export { useStorageValue } from '#/storage/use-storage';6 unmodified lines
/** OAuth-backed account list and active account pointer for this device. */export const auth = new Storage<[], Auth>({ id: 'bsky_auth' });
export const persistedQueryCache = new Storage<[QueryCacheScope], QueryCache>({ id: 'bsky_query_cache',});
if (import.meta.env.DEV && typeof window !== 'undefined') { // @ts-expect-error - dev global window.bsky_storage = { account, auth, device, persistedQueryCache, };}1234567891011121314151617181920212223242526import type { Did } from '@atcute/lexicons';
import { device, persistedQueryCache } from '#/storage';
const BUSTER = import.meta.env.PUBLIC_GIT_COMMIT_HASH || 'dev';
/** * invalidates persisted queries between builds; call before cache reads. */export const prepareQueryCache = (): void => { if (device.get(['queryCacheBuster']) === BUSTER) { return; }
persistedQueryCache.removeAll(); device.set(['queryCacheBuster'], BUSTER);};
/** * clears an account's persisted queries. * * @param did account did */export const clearPersistedQueryCache = (did: Did): void => { persistedQueryCache.removeScope([did]);};12345678933 unmodified lines43444546474849505144 unmodified lines96979899100101102103104105106107108109110111import type { AppBskyActorDefs } from '@atcute/bluesky';import type { Did, GenericUri, Handle } from '@atcute/lexicons';
import type { JsonValue } from 'type-fest';
import type { Gif } from '#/lib/gif';
import type { Locale } from '#/paraglide/runtime';
33 unmodified lines /** Previously used {@link Device.postLanguage} values, used to pre-populate the composer selector. */ postLanguageHistory?: string[]; /** BCP-47 2-letter language code to translate posts into. */ primaryLanguage?: string; queryCacheBuster?: string; /** recently picked emoji ids, most recent first. */ recentEmojis?: string[]; requireAltTextEnabled?: boolean; subtitlesEnabled?: boolean;44 unmodified lines displayName?: string; /** statuses used by verification badges. */ verification?: Pick<AppBskyActorDefs.VerificationState, 'trustedVerifierStatus' | 'verifiedStatus'>;};
export type QueryCacheScope = Did | 'logged-out';
export type QueryCache = { [queryHash: string]: PersistedQueryEntry;};
export type PersistedQueryEntry = { data: JsonValue; dataUpdatedAt: number; version: number;};118 unmodified linesExpand all11912012112212312412512612312412512612712812913013113212913013113213313413513313413513613713813914014114214014314414514614714814915015115215315415515615715815916016116216316416516616713 unmodified lines118 unmodified linesExpand all this.remove([...scopes, key]); } }
/** removes all stored keys belonging to this storage instance. */ removeAll() { if (!store) { return; protected removeByPrefix(prefix: string) { const keys = new Set<string>();
// clear memory even without backing storage. for (const key of this.cache.keys()) { if (key.startsWith(prefix)) { keys.add(key); } }
const prefix = `${this.id}${SEP}`; const keys: string[] = [];
for (let idx = 0, len = store.length; idx < len; idx++) { const key = store.key(idx); if (key?.startsWith(prefix)) { keys.push(key); if (store) { for (let idx = 0, len = store.length; idx < len; idx++) { const key = store.key(idx); if (key?.startsWith(prefix)) { keys.add(key); } } }
for (const key of keys) { store.removeItem(key); store?.removeItem(key); this.cache.delete(key); this.emit(key); } }
/** * removes all values in a scope. * * @param scopes scope path */ removeScope(scopes: [...Scopes]) { this.removeByPrefix(`${this.scopedKey(scopes)}${SEP}`); }
/** removes all stored keys belonging to this storage instance. */ removeAll() { this.removeByPrefix(`${this.id}${SEP}`); }
/** * attaches a listener for changes to a given scope path. * * @param scopes array of scope identifiers leading to the key * @param callback function invoked when the value changes13 unmodified lines36 unmodified lines37383940414142434445377 unmodified linesExpand all42342442542642742842842943043143243343443542943043143243343443543643743843644044144244344444544644743743843944044144244344844945044444544644743 unmodified lines491492493494495496497498499151 unmodified linesExpand all65165265365465565665765865924 unmodified lines36 unmodified lines
import { GCTIME, STALE } from '#/state/queries';import { RQKEY as listQueryKey } from '#/state/queries/list';import { usePreferencesQuery } from '#/state/queries/preferences';import { createQueryKey } from '#/state/queries/util';import { createQueryPersister } from '#/state/query-persister';import { getClients, useSession } from '#/state/session';
import { m } from '#/paraglide/messages';import type { RouteTarget } from '#/router';377 unmodified linesExpand all contentMode: undefined,});
export const FEED_INFO_RQKEY_ROOT = 'feed-info';const feedInfoQueryPersister = createQueryPersister({ version: 1 });
const createPinnedFeedInfoQueryKey = (uri: string) => createQueryKey( FEED_INFO_RQKEY_ROOT, { kind: 'pinned', uri, }, {const createPinnedFeedInfoQueryKey = (uri: string) => [ FEED_INFO_RQKEY_ROOT, { kind: 'pinned', uri, },]; persistedVersion: 1, }, );
const createSavedFeedInfosQueryKey = (feedUris: string[]) => createQueryKey( FEED_INFO_RQKEY_ROOT, { kind: 'saved', feedUris, }, {const createSavedFeedInfosQueryKey = (feedUris: string[]) => [ FEED_INFO_RQKEY_ROOT, { kind: 'saved', feedUris, },]; persistedVersion: 1, }, );
const fetchFeedGenerator = createBatchedFetch<ResourceUri, AppBskyFeedDefs.GeneratorView>({ limit: 50, timeout: 0,43 unmodified lines gcTime: GCTIME.INFINITY, queryFn({ signal }) { return fetchPinnedFeedInfo(item, signal); }, persister: feedInfoQueryPersister, })), combine(results: UseQueryResult<FeedSourceInfo | null>[]) { return { resolved: mapDefined(results, (result) => result.data ?? undefined),151 unmodified linesExpand all count: result.length, feeds: result, }; }, persister: feedInfoQueryPersister, placeholderData: (previousData) => { return ( previousData || { count: savedItems.length,24 unmodified lines1 unmodified line234566789101111121314151617181919202122202122232425262727282930313219 unmodified lines5253545555565758596048 unmodified lines10911011111211311411511611714 unmodified lines13213313413513413613713813914014114214341 unmodified lines1 unmodified lineimport { type Client, ok } from '@atcute/client';
import { type QueryClient, type QueryKey, useQuery, useQueryClient } from '@tanstack/react-query';
import { createQueryKey } from '#/state/queries/util';import { createQueryPersister } from '#/state/query-persister';import { getClients } from '#/state/session';
import { STALE } from '.';
const joinLinkPreviewQueryKeyRoot = 'join-link-preview';const RQKEY_ROOT = 'join-link-preview';const joinLinkPreviewQueryPersister = createQueryPersister({ version: 1 });
/** * an entry from a join link preview response. can be a usable preview or a disabled/invalid placeholder. * narrow on `$type === 'chat.bsky.group.defs#joinLinkPreviewView'` before reading details. */export type JoinLinkPreview = ChatBskyGroupGetJoinLinkPreviews.$output['joinLinkPreviews'][number];
export const createJoinLinkPreviewQueryKey = (args: { codes: string[]; hasSession: boolean }) => createQueryKey(joinLinkPreviewQueryKeyRoot, args, { persistedVersion: 1, });export const createJoinLinkPreviewQueryKey = (args: { codes: string[]; hasSession: boolean }) => [ RQKEY_ROOT, args,];
/** matches a join link preview query key whose `codes` argument contains `code`. */const joinLinkPreviewKeyHasCode = (queryKey: QueryKey, code: string) => { const [root, args] = queryKey; if (root !== joinLinkPreviewQueryKeyRoot) { if (root !== RQKEY_ROOT) { return false; } if (typeof args !== 'object' || args === null || !('codes' in args)) { return false;19 unmodified linesexport function invalidateJoinLinkPreviewsForConvo(queryClient: QueryClient, convoId: string) { return queryClient.invalidateQueries({ predicate: (query) => { const [root] = query.queryKey; if (root !== joinLinkPreviewQueryKeyRoot) { if (root !== RQKEY_ROOT) { return false; } // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the root check above pins the cache entry const data = query.state.data as ChatBskyGroupGetJoinLinkPreviews.$output | undefined;48 unmodified lines throw new Error('No invite code'); } return fetchJoinLinkPreviews({ chat, codes, signal }); }, persister: joinLinkPreviewQueryPersister, initialData, });}
14 unmodified lines code: string; hasSession: boolean; }): Promise<JoinLinkPreview | undefined> => { try { const data = await queryClient.fetchQuery({ const data = await queryClient.fetchQuery<ChatBskyGroupGetJoinLinkPreviews.$output>({ queryKey: createJoinLinkPreviewQueryKey({ codes: [code], hasSession }), queryFn: ({ signal }) => fetchJoinLinkPreviews({ chat, codes: [code], signal }), persister: joinLinkPreviewQueryPersister, staleTime: STALE.SECONDS.FIFTEEN, }); return data.joinLinkPreviews[0]; } catch (error) {41 unmodified lines7 unmodified lines89101112121314151615161718191819202122232438 unmodified lines63646566676869707168 unmodified lines7 unmodified lines
import { GCTIME, STALE } from '#/state/queries';import { preferencesQueryKey, usePreferencesQuery } from '#/state/queries/preferences';import { addLabeler, removeLabeler } from '#/state/queries/preferences/agent';import { createQueryKey } from '#/state/queries/util';import { createQueryPersister } from '#/state/query-persister';import { getClients } from '#/state/session';
const labelerInfoQueryKeyRoot = 'labeler-info';const labelerInfoQueryKey = (did: string) => [labelerInfoQueryKeyRoot, did];const LABELER_INFO_RQKEY_ROOT = 'labeler-info';const labelerInfoQueryKey = (did: string) => [LABELER_INFO_RQKEY_ROOT, did];
const createLabelersDetailedInfoQueryKey = (dids: string[]) => createQueryKey('labelers-detailed-info', { dids }, { persistedVersion: 1 });const LABELERS_DETAILED_INFO_RQKEY_ROOT = 'labelers-detailed-info';const createLabelersDetailedInfoQueryKey = (dids: string[]) => [LABELERS_DETAILED_INFO_RQKEY_ROOT, { dids }];const labelersDetailedInfoQueryPersister = createQueryPersister({ version: 1 });
/** * queries the labeler service record published by an account. *38 unmodified lines ); // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- `detailed: true` makes the service views detailed return data.views as AppBskyLabelerDefs.LabelerViewDetailed[]; }, persister: labelersDetailedInfoQueryPersister, });}
export function useRemoveLabelersMutation() {68 unmodified lines