netx/web/src/features/cutover/lib/cutoverDataCache.ts
oliver 0d894efe5e Refactor web UI: feature-first structure, tokenized styles, modern blue theme.
Structure
- pages/ -> features/<domain>/{Page, components, hooks, lib}; 95 files moved,
  333 imports rewritten, verified with tsc.
- services/api.ts -> services/api/<domain>.ts (250 exports preserved),
  types.ts -> types/<domain>.ts, i18n zh/en -> locales/<lang>/<namespace>.ts
  (2696 messages deep-equal before/after).
- ESLint rule: shared layers must not import features/.

Styles
- index.css (16k lines) split losslessly into styles/{base,layout,components,
  pages,legacy,theme}; compiled CSS was byte-identical before tokenizing.
- ~930 hard-coded colors replaced by --nx-* tokens (theme/tokens.css); WebCRT
  off-palette colors unified; fixed dark-on-dark text introduced by mapping.
- theme/theme.css: modern blue design language (rounded shapes, gradients,
  unified buttons/fields/tables/modals), form-grid alignment fix, flat nav list
  without tree indent, soft selected state instead of left accent bar.

Docs: WEB.md documents code structure and style rules.
Not touched: the large Biz*/Webcrt/Topology page components (to be split later).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 03:17:51 +08:00

70 lines
2 KiB
TypeScript

/**
* Short-lived cache for cutover list APIs.
* Switching biz-state / compare / migration / templates remounts each page;
* without cache every click re-waits on Promise.all network round-trips.
*/
type Entry = { at: number; data: unknown };
const store = new Map<string, Entry>();
const inFlight = new Map<string, Promise<unknown>>();
const DEFAULT_TTL_MS = 45_000;
export async function cutoverCachedGet<T>(
key: string,
fetcher: () => Promise<T>,
opts?: { force?: boolean; ttlMs?: number },
): Promise<T> {
const ttl = opts?.ttlMs ?? DEFAULT_TTL_MS;
if (!opts?.force) {
const hit = store.get(key);
if (hit && Date.now() - hit.at < ttl) {
return hit.data as T;
}
}
const existing = inFlight.get(key);
if (!opts?.force && existing) return existing as Promise<T>;
const request = Promise.resolve().then(fetcher).then((data) => {
// An invalidation or forced refresh may supersede this request.
if (inFlight.get(key) === request) store.set(key, { at: Date.now(), data });
return data;
}).finally(() => {
if (inFlight.get(key) === request) inFlight.delete(key);
});
inFlight.set(key, request);
return request;
}
/** Return stale immediately (if any), refresh in background and notify. */
export async function cutoverCachedGetSWR<T>(
key: string,
fetcher: () => Promise<T>,
onFresh?: (data: T) => void,
): Promise<T> {
const hit = store.get(key);
if (hit) {
void cutoverCachedGet(key, fetcher, { ttlMs: 0 })
.then((data) => {
if (store.get(key)?.data === data) onFresh?.(data);
})
.catch(() => {
/* keep stale */
});
return hit.data as T;
}
return cutoverCachedGet(key, fetcher);
}
export function invalidateCutoverCache(prefix = ""): void {
if (!prefix) {
store.clear();
inFlight.clear();
return;
}
for (const k of [...store.keys()]) {
if (k.startsWith(prefix)) store.delete(k);
}
for (const k of [...inFlight.keys()]) {
if (k.startsWith(prefix)) inFlight.delete(k);
}
}