mirror of
https://github.com/hansjone/netx.git
synced 2026-10-14 16:00:51 +08:00
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>
70 lines
2 KiB
TypeScript
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);
|
|
}
|
|
}
|