fix: expire persisted query cache entries
25 unmodified lines2627282930303132333419 unmodified lines5455565758596061623 unmodified lines25 unmodified lines const scope: QueryCacheScope = getCurrentDid() ?? 'logged-out';
if (query.state.data === undefined) { const stored = persistedQueryCache.get([scope, query.queryHash]); if (stored?.version === version) { if (stored?.version === version && stored.expiresAt > Date.now()) { notifyManager.schedule(() => { // preserve the cached fetch time for stale checks. query.setState({ dataUpdatedAt: stored.dataUpdatedAt }); if (query.isStale()) {19 unmodified lines 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, expiresAt: query.state.dataUpdatedAt + query.gcTime, version, }); });
3 unmodified lines2 unmodified lines345678978119101112131415161516171819206 unmodified lines2 unmodified linesimport { device, persistedQueryCache } from '#/storage';
const BUSTER = import.meta.env.PUBLIC_GIT_COMMIT_HASH || 'dev';
/** * invalidates persisted queries between builds; call before cache reads. *//** invalidates persisted queries between builds and prunes expired entries; call before cache reads. */export const prepareQueryCache = (): void => { if (device.get(['queryCacheBuster']) === BUSTER) { if (device.get(['queryCacheBuster']) !== BUSTER) { persistedQueryCache.removeAll(); device.set(['queryCacheBuster'], BUSTER); return; }
persistedQueryCache.removeAll(); device.set(['queryCacheBuster'], BUSTER); const now = Date.now(); persistedQueryCache.removeWhere((entry) => entry.expiresAt <= now);};
/** * clears an account's persisted queries.6 unmodified lines105 unmodified linesExpand all106107108109110111112105 unmodified linesExpand all
export type PersistedQueryEntry = { data: JsonValue; dataUpdatedAt: number; expiresAt: number; version: number;};123345672 unmodified lines10111213141415161718100 unmodified lines11912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017154 unmodified linesimport { SimpleEventEmitter } from '@mary-ext/simple-event-emitter';
import type { JsonObject, JsonValue } from 'type-fest';import type { JsonObject } from 'type-fest';
const SEP = ':';
let store: globalThis.Storage | undefined;2 unmodified lines store = localStorage;} catch {}
export class Storage<Scopes extends unknown[], Schema extends JsonObject> { protected cache = new Map<string, JsonValue | undefined>(); protected cache = new Map<string, Schema[keyof Schema] | undefined>(); protected emitters = new Map<string, SimpleEventEmitter<[]>>(); protected id: string;
constructor({ id }: { id: string }) {100 unmodified lines this.remove([...scopes, key]); } }
/** * removes stored entries matching a predicate. * * @param predicate selects values to remove */ removeWhere(predicate: (data: Schema[keyof Schema]) => boolean) { const prefix = `${this.id}${SEP}`; const keys = new Set<string>();
for (const key of this.cache.keys()) { if (key.startsWith(prefix)) { keys.add(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) { let value = this.cache.get(key); if (!this.cache.has(key)) { try { const raw = store?.getItem(key) || '{}'; // oxlint-disable-next-line typescript/no-unsafe-type-assertion const parsed = JSON.parse(raw) as { data: Schema[keyof Schema] | undefined }; value = parsed.data; } catch { // malformed value } }
if (value !== undefined && predicate(value)) { store?.removeItem(key); this.cache.delete(key); this.emit(key); } } }
protected removeByPrefix(prefix: string) { const keys = new Set<string>();
// clear memory even without backing storage.54 unmodified lines486 unmodified linesExpand all48748848949049149149249349449575 unmodified lines571572573574575575576577578579104 unmodified linesExpand all486 unmodified linesExpand all const { resolved, error, isLoading, isPending, isError } = useQueries({ queries: fetchedItems.map((item) => ({ queryKey: createPinnedFeedInfoQueryKey(item.value), staleTime: STALE.MINUTES.FIFTEEN, gcTime: GCTIME.INFINITY, gcTime: GCTIME.DAYS.SEVEN, queryFn({ signal }) { return fetchPinnedFeedInfo(item, signal); }, persister: feedInfoQueryPersister,75 unmodified lines return useQuery({ queryKey: createSavedFeedInfosQueryKey(savedItems.map((f) => f.value)), enabled: !isLoadingPrefs, staleTime: STALE.INFINITY, gcTime: GCTIME.INFINITY, gcTime: GCTIME.DAYS.SEVEN, queryFn: async ({ signal }) => { const resolvedFeeds = new Map<string, AppBskyFeedDefs.GeneratorView>(); const resolvedLists = new Map<string, AppBskyGraphDefs.ListView>();
104 unmodified linesExpand all1234567815 unmodified lines242526272728293031const SECOND = 1e3;const MINUTE = SECOND * 60;const HOUR = MINUTE * 60;const DAY = HOUR * 24;
export const STALE = { SECONDS: { FIFTEEN: 15 * SECOND,15 unmodified linesexport const GCTIME = { MINUTES: { FIVE: 5 * MINUTE, }, INFINITY: Infinity, DAYS: { SEVEN: 7 * DAY, },};51 unmodified lines5253545556565758596079 unmodified lines51 unmodified lines return useQuery({ queryKey: createLabelersDetailedInfoQueryKey(dids), enabled: !!dids.length, staleTime: STALE.MINUTES.ONE, gcTime: GCTIME.INFINITY, gcTime: GCTIME.DAYS.SEVEN, queryFn: async ({ signal }) => { const data = await ok( appview.get('app.bsky.labeler.getServices', { signal,79 unmodified lines17 unmodified lines181920212222232425264 unmodified lines17 unmodified lines const queryClient = useQueryClient();
return async (did: Did) => { const { pds } = await queryClient.fetchQuery({ gcTime: GCTIME.INFINITY, gcTime: GCTIME.MINUTES.FIVE, staleTime: STALE.HOURS.ONE, queryKey: fetchPdsQueryKey(did), queryFn: ({ signal }) => resolveMiniDoc(did, signal), });4 unmodified lines13 unmodified lines141516171818192021225 unmodified lines13 unmodified lines
return useQuery({ queryKey: chatActorStatusQueryKey(), staleTime: STALE.SECONDS.FIFTEEN, gcTime: GCTIME.INFINITY, gcTime: GCTIME.DAYS.SEVEN, queryFn: async ({ signal }) => { if (!chat) { throw new Error('Not signed in'); }5 unmodified lines53 unmodified lines54555657585859606162332 unmodified linesExpand all53 unmodified lines
const query = useQuery({ queryKey: preferencesQueryKey, staleTime: STALE.MINUTES.FIVE, gcTime: GCTIME.INFINITY, gcTime: GCTIME.DAYS.SEVEN, refetchOnWindowFocus: true, queryFn: async ({ signal }) => { if (!pds || !currentAccount) { return DEFAULT_LOGGED_OUT_PREFERENCES;332 unmodified linesExpand all