// 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 } 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() const session = useSessionStore() function request(key: string, fetcher: () => Promise): Promise { const entry = entries.get(key) if (entry?.inflight) return entry.inflight as Promise const p: Promise = 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(key: string, fetcher: () => Promise, opts: FetchOptions = {}): Promise { 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(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 } })