Repository navigation
fix(plugin-dashboard): the auto-refresh interval reads its handler through a ref, not a memoised identity (objectui#11004) - #11059
Conversation
…rough a ref useDashboardAutoRefresh keyed its interval effect on the handleRefresh useCallback identity and on the host's onRefresh identity (AGENTS.md #10). Either identity changing at an equal period cleared and re-armed the interval, restarting its phase. The effect now depends on the period and on whether a handler is wired; the handler is read through a ref at run time, so the current onRefresh is still the one called. The timer pin adds one case per surface that forces a memo discard mid-period (module-level react proxy, as in the permissions pins) and then swaps the host handler mid-period, asserting the phase holds both times. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card #11004 (body, triage ① Derived judgments
② Semver level
Clause-②: no — RIGHT. No accept set, schema, export or contract moves; an interval's re-arm condition moves inside one hook, and the PR body's own ③ Boundary flagsDev flags (the os-dev-report's
Out-of-scope observation (the console's Check-runs on the head ( Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #11004
Clause-②: no
What changed
useDashboardAutoRefresh(the one timer behindDashboardGridLayoutandDashboardRenderer) armed its interval in an effect keyed on[seconds, onRefresh, handleRefresh], andhandleRefreshis auseCallbackresult. That is the shape AGENTS.md commandment #10 bans (objectui#8640): if React discards the memo, the new function identity clears the interval and sets it again, and the phase restarts. The card's premise holds onorigin/mainat30f912a0d: the dependency list and its own warning comment were still there.onRefreshRef), updated in a dependency-free effect. This is the house shape, the same one used bysurfaceDeepLinkChannel.tsand the studio-design panels.runRefresh(handler, setRefreshing)holds the run: call the handler, then show the 600ms indicator. The manualhandleRefresh(auseCallbackwith an empty list, kept only for cost) and the interval callback both call it, and both read the handler through the ref.[seconds, wired], wherewired = Boolean(onRefresh).Zone 2, item 2, was decided from commandment #10:
onRefreshleaves the list too. It is a caller-supplied identity, which is the cross-boundary instance of the same class the commandment names ("a prop, a hook return, a context"). The ref still meets the handler-swap case, because the CURRENT handler is the one called. The one behaviour change is the intended one: when the host passes a new handler identity at the same period, the phase is kept instead of restarted. No in-tree host passes a changingonRefreshtoday.DashboardWithConfigonly passes its prop through, and the console'sDashboardViewwires none.The new case
The new case is
keeps its phase when a handler identity changes at an equal period (objectui#11004), inside the existing per-surfacedescribe.each, so it runs once per surface. It runs as two steps on one 30s schedule:reactproxy, the same technique asproviderCtxIdentity.discarded.test.tsxinpackages/permissions, and is inert unless a case arms it. It then asserts the run still lands at 30s. A control asserts that the discard really reached the hook:handleRefreshcomes back as a different function. Without that control, a proxy that patched nothing would read green.All 22 earlier cases are untouched and green, the equal-period phase case and the handler-swap case among them.
Evidence, all at HEAD
8fa76e0edpnpm exec vitest run packages/plugin-dashboard/src/__tests__/dashboardAutoRefreshTimer.test.tsx: base treeTests 22 passed (22), this branchTests 24 passed (24).30f912a0d, restored withgit checkout HEAD, blob equal to HEAD andgit diff HEADempty):Tests 2 failed | 22 passed (24). The two failures are the new case, one per surface, ata discarded memo re-armed the interval. The controls passed, so the discard did reach the hook.ablation-replace.mjs(anchor hit 1 to 0, blob changed, then restored to the HEAD blob withgit diff HEADempty):onRefreshadded back to the interval list:2 failed | 22 passed (24)ata new host handler identity re-armed the interval. Step 1 passed, so step 2 is load-bearing on its own.handleRefreshadded back (even as a ref-reading, empty-list callback):2 failed | 22 passed (24)ata discarded memo re-armed the interval.pnpm exec vitest run packages/plugin-dashboard/:Test Files 151 passed (151),Tests 1379 passed | 6 skipped (1385).pnpm --filter @object-ui/plugin-dashboard type-check: exit 0, after building the package's dependency closure (--filter '@object-ui/plugin-dashboard^...' run build, exit 0).tsc -p tsconfig.test.json --listFilesincludes the changed test file.pnpm exec eslint --format jsonon the two changed source files: 2 files, 0 errors, 0 warnings.react-hooks/exhaustive-depsaccepts[seconds, wired]. This is a declared narrowing: the config enables no type-aware linting (noparserOptions.projectorprojectService), and no local rule ineslint-rules/reads another file, so this diff cannot move a verdict on an untouched file. The package-wideeslint .belongs to CI.node scripts/check-*.mjs, each exit 0: changeset-presence, control-bytes, vi-mock-specifiers, vi-mock-inherit, vi-mock-override-shape, new-cross-file-line-citations (0 new citation(s)), changeset-no-major, changeset-claims, pending-changeset-literals, test-path-roots.scripts/,eslint-rules/or.github/enforces commandment [WIP] Enhance every detail of the designer #10. A grep for it returned zero hits, with a positive control that did hit, which matches the commandment's own "Nothing enforces this rule".Changeset:
.changeset/11004-autorefresh-handler-ref.md,'@object-ui/plugin-dashboard': patch.Acceptance notes
DashboardViewmountsDashboardRendererwith noonRefresh, so an authoredrefreshIntervalSecondsnever starts a timer in the console. The Studio metadata form still offers the field (its zh label is "自动刷新"). This was read from the JSX, not measured in a running console. It is handed to the seat in the report.Generated by Claude Code