desktop: one read registry for every read button: single-flight per canonical key, broad reads cover narrow keys, overlapping buttons all busy and disabled (useReadState), 20 s 'read just now' memo, quota pause on the buttons, every read through the scheduler (Check again, Import apps, account Test, Read the stores, Dependencies), writes invalidate only the reads they change; tests/read-registry.test.ts; fixtures ?blocked and window.__fakeCalls

This commit is contained in:
CRTkafa 2026-10-23 11:09:51 +08:00
parent 7345de2f7c
commit c03938f9dc
14 changed files with 861 additions and 164 deletions

View file

@ -180,12 +180,13 @@ export function StoreOnlyTag() {
/** The state of one read beside its card's title: "read 3 min ago" with a small spinner while it refreshes, or
* "couldn't refresh: <reason>" over the last good value, and the per-card refresh icon (no "Read" button first). */
export function ReadControl({ r, label }: { r: { data: unknown; at: string | null; loading: boolean; error: string | null; refresh: () => Promise<void> }; label?: string }) {
const text = r.error ? t('read.couldNotRefresh', { reason: r.error }) : r.at ? t('read.ago', { when: ago(r.at) }) : r.loading ? t('read.waiting') : '';
export function ReadControl({ r, label }: { r: { data: unknown; at: string | null; loading: boolean; error: string | null; justNow?: boolean; pausedUntil?: number | null; refresh: () => Promise<void> }; label?: string }) {
const paused = r.pausedUntil ? t('read.paused', { time: new Date(r.pausedUntil).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }) : '';
const text = r.error ? t('read.couldNotRefresh', { reason: r.error }) : paused && r.loading ? paused : r.justNow ? t('read.justNow') : r.at ? t('read.ago', { when: ago(r.at) }) : r.loading ? t('read.waiting') : '';
return (
<span className="read-ctl" data-read-state={r.loading ? 'loading' : r.error ? 'error' : r.data !== null ? 'ok' : 'none'}>
{text && <span className={`read-stamp ${r.error ? 'read-err' : ''}`} title={r.error ?? undefined}>{text}</span>}
<button className="btn btn-sm btn-icon" onClick={() => { r.refresh(); }} disabled={r.loading} aria-label={label ?? t('read.refresh')} title={label ?? t('read.refresh')}>
<span className="read-ctl" data-read-state={r.loading ? 'loading' : r.error ? 'error' : r.data !== null ? 'ok' : 'none'} data-read-paused={paused ? 'true' : undefined}>
{text && <span className={`read-stamp ${r.error ? 'read-err' : ''}`} title={r.error ?? (paused || undefined)}>{text}</span>}
<button className="btn btn-sm btn-icon" onClick={() => { r.refresh(); }} disabled={r.loading} aria-busy={r.loading} aria-label={label ?? t('read.refresh')} title={label ?? t('read.refresh')}>
<Icon name="refresh" className={r.loading ? 'spin' : undefined} />
</button>
</span>