feat: catalog read cache (api + user apps) and clearer Zarrin accounting errors
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
// 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: data changes when the user asks for it or
|
||||
// when a write here makes it wrong, and those writes call `invalidate`.
|
||||
//
|
||||
// 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, so the next user never sees the previous one's data.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => session.token,
|
||||
() => clearUserScoped(),
|
||||
)
|
||||
|
||||
return { fetch, peek, invalidate, clearUserScoped }
|
||||
})
|
||||
Reference in New Issue
Block a user