Files
tehran_tala/apps/user-app/src/stores/queryCache.ts
T

124 lines
4.7 KiB
TypeScript
Raw Normal View History

// Shared read cache for API responses.
//
// Every screen used to fetch on mount, so each tab switch repeated the same
// requests — the whole catalogue alone was fetched from ten places. Reads now go
// through `fetch(key, …)`:
//
// - cached entry → returned at once, no request
// - request in flight → the same promise is shared, never a second request
// - `force` → always asks the server (pull-to-refresh, the
// profile tab's balance, reaching the end of a list)
//
// Entries never expire on a timer, deliberately (owner ruling 2026-10-12: «هربار
// با رفتن به اون صفحه اون مقادیر مورد نیاز رو درخواست بزنه … هم سربار برای سرور
// نداره، هم درخواست الکی نمیخوره، هم دیتاها به‌روز میمونه»). A TTL guesses how
// long data stays true; entering a screen is the moment it actually matters.
//
// So the policy is:
//
// - a screen that shows server-owned data re-reads it with `force` when it is
// opened (`onMounted`, plus `onActivated` for the kept-alive views), while
// `peek` paints the previous copy instantly so the refresh is invisible;
// - the cached copy exists for that first paint and to collapse concurrent
// callers into one request, NOT to avoid asking again;
// - the catalogue is the exception: it is the whole product list in one
// response, changes when an admin edits it, and is already invalidated by
// pull-to-refresh and by placing an order.
//
// `force` is what makes this work for anything changed OUTSIDE the app — an
// order's status, a ledger entry, the gold price. Nothing here can know those
// changed, so not asking means never seeing them.
//
// Entries live in a plain Map, not reactive state: the catalogue is large and
// making it deeply reactive would cost more than the request it saves.
//
// Keys under `me:` hold one customer's data. They are dropped whenever the
// session token changes — or the act-as target does, which changes whose data
// they hold without changing the token — so one user never sees another's.
import { defineStore } from 'pinia'
import { watch } from 'vue'
import { useSessionStore } from './session'
export const KEYS = {
catalog: 'catalog',
goldPrice: 'goldPrice',
homeSlider: 'homeSlider',
profile: 'me:profile',
orders: 'me:orders',
balance: 'me:balance',
ledger: 'me:ledger',
} as const
const USER_PREFIX = 'me:'
interface Entry {
data?: unknown
/** False until the first successful response. */
has: boolean
inflight?: Promise<unknown>
}
export interface FetchOptions {
/** Skip the cached value. An identical request already in flight is still shared. */
force?: boolean
}
export const useQueryCache = defineStore('queryCache', () => {
const entries = new Map<string, Entry>()
const session = useSessionStore()
function request<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const entry = entries.get(key)
if (entry?.inflight) return entry.inflight as Promise<T>
const p: Promise<T> = fetcher()
.then((data) => {
// Invalidated (or the user changed) while this was in flight: the
// response may predate the change, so it is handed back but not kept.
if (entries.get(key)?.inflight === p) entries.set(key, { data, has: true })
return data
})
.finally(() => {
const current = entries.get(key)
if (current?.inflight === p) current.inflight = undefined
})
entries.set(key, { ...(entry ?? { has: false }), inflight: p })
return p
}
async function fetch<T>(key: string, fetcher: () => Promise<T>, opts: FetchOptions = {}): Promise<T> {
const entry = entries.get(key)
if (!opts.force && entry?.has) return entry.data as T
return request(key, fetcher)
}
/** The cached value, without fetching. */
function peek<T>(key: string): T | undefined {
const entry = entries.get(key)
return entry?.has ? (entry.data as T) : undefined
}
function invalidate(...keys: string[]) {
for (const key of keys) entries.delete(key)
}
function clearUserScoped() {
for (const key of [...entries.keys()]) {
if (key.startsWith(USER_PREFIX)) entries.delete(key)
}
}
// The token changes when the user changes — and, with «ورود به حساب مشتری»,
// WHOSE data `me:` keys hold changes without the token changing at all. Both
// have to drop the cache, or a sales expert entering a customer's account
// would be shown their own profile, orders, balance and ledger from before
// the switch.
watch(
() => [session.token, session.actingAs?.id ?? null],
() => clearUserScoped(),
)
return { fetch, peek, invalidate, clearUserScoped }
})