Repository navigation
fix(data-objectstack): a console page load reads sys_user_preference once, shared by the three preference adapters (objectui#12187) - #12241
Merged
objectstack-fleet[bot] merged 4 commits intoOct 12, 2026
Conversation
…s_user_preference read per load createObjectStackUserStateAdapter's load() now joins an open read for its (data source, resource, user). Every load of that scope issued before the next microtask adds its key; the read is then sent once and each adapter takes its own row. One key keeps the per-key query; several go as one `key in (...)` query with `$top` equal to the key count. Nothing is kept past the send, and save() still finds and writes its own row. Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude <noreply@anthropic.com>
…r console boot consoleUserPreferenceBudget now boots the real ConsoleShell and ConnectedShell over a fake framework that answers preference reads by user_id and key (= or in): one read per boot asking for the three keys, every consumer gets its own row, each adapter writes its own row, and a user switch reads again for the new user. The 11797 wire pin's first case moves to the shared read; userState.sharedRead-12187 pins the share's edges (user and data-source scope, nothing kept past the send, untagged rows, a failed read, writes unchanged). Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude <noreply@anthropic.com>
…erence read Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude <noreply@anthropic.com>
… options member, not a new any Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ 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
|
objectstack-fleet
Bot
deleted the
claude/issue-12187-user-preference-one-read
branch
October 12, 2026 12:21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12187
Clause-②: no
This is item 3 of objectui#12187, and the last one: one
sys_user_preferenceread per console load. Items 1 (54159d3) and 2 (6eca5b5) landed earlier, and this PR does not touch their code. Claim6119197321, amended for this item by6122942302.What changes
The change is to
createObjectStackUserStateAdapterin@object-ui/data-objectstack(userState.ts). Itsload()now joins an open read for its data source, resource and user. Every load in that scope that starts before the next microtask adds its key to that read. The read is then sent once, and each adapter takes its own row from the answer.user_id = U and key = K,$top: 1).user_id = U and key in (K1, K2, K3), with$topset to the number of keys.load()asks the server again, and a different user or data source never joins another one's read.save()is unchanged. A write still finds and writes its own row: a per-key find, then an update or an insert.ConsoleShell.tsxdoes not change. ItsUserStateBridgealready builds the three adapters in one effect, so the providers' loads land in one read. No reader makes a read of its own, so no reader file changes.Published surface:
index.tsis unchanged.userState.tshas the sameexportlines as at the base (diff exit 0).ObjectStackUserStateAdapterOptionsis the same as at the base (diff exit 0).SharedReadinterface are module-private.dist/index.d.tsis one new paragraph of text in the module's header comment, which tsup copies into it.Hence
Clause-②: noand apatchchangeset for@object-ui/data-objectstack.@object-ui/app-shellonly changes a test.H1: the reads per load, before and after
How it was measured: a jsdom boot of the real
ConsoleShellproviders and the realConnectedShellover a counting globalfetch. The shell includes the realAdapterProvider,ObjectStackAdapter, client andUserStateBridge. Two entries were booted:/apps/demo/page/welcome, and/with a metadata-gated redirect to that page. Only the session is stood in. The call sites come from a sync stack read atObjectStackAdapter.find. The probe was a scratch file and is not committed. The committed pin isconsoleUserPreferenceBudget.test.tsx(see Tests).Before: at base
adfbe3b(main, which contains6eca5b5). Each entry made 3 GETs, each filtered by one key, not by the user's whole set:GET /api/v1/data/sys_user_preference?top=1&filter=["and",["user_id","=","u1"],["key","=","ui.flow.palette.recents"]]FlowPaletteRecentsProviderhydrate effect, thenadapter.load(),findExisting,ObjectStackAdapter.find["key","=","ui.recent"]RecentItemsProviderhydrate effect, same chain["key","=","ui.favorites"]FavoritesProviderhydrate effect, same chainAfter: at this head, each entry made 1 GET:
GET /api/v1/data/sys_user_preference?top=3&filter=["and",["user_id","=","u1"],["key","in",["ui.flow.palette.recents","ui.recent","ui.favorites"]]]The same three provider effects issue it, now joined into one read.
The
/entry does not remountConnectedShellon its redirect: the same element type sits at the same position. So it also reads once, before and after.Browser reading: NOT MEASURED. Reason: this container has no runnable backend for a signed-in console. The jsdom boot is the instrument. The staging reading (cloud's pin carrying this merge) is the card's acceptance 3, and it stays with the seat.
H2: the shape chosen, and why
Chosen: the share lives inside
userState.ts. A module-scopeWeakMapholds the open reads, keyed by the data-source object and then by (resource, user). This adds no export, option or type.Not chosen: one loader built in
ConsoleShelland passed to the three adapters. That needs a new option on the factory, which widens the published surface.Why
key inthe three keys, and not the user's whole set:user_idpredicate.user_id,key), per organization, allows at most one row per key. So a$topequal to the key count drops none.in: it is anAST_OPERATOR_MAPkey behindVALID_AST_OPERATORSin@objectstack/spec'sdata/filter.zod.ts, and driver-sql's operator switch has aninarm. I read these sources at objectstack2e11d9006. No live server was run here.$inreads throughfindwith$topequal to the list length (AccessExplainPanel).A row that carries no
key: in an answer for several keys it cannot be assigned to one. When that happens, each adapter reads its own key as before. The pina shared answer whose rows name no key is not attributedcovers this.H3: refresh and invalidation
When each adapter reads:
FavoritesProvider,RecentItemsProviderandFlowPaletteRecentsProviderre-run only when the attached adapter or the user id changes.MetadataProvider key={language}on a language switch.localStorageevents only.Readings (same jsdom boot; the "before" column was taken under the ablation below):
H4
Neither
GET /api/v1/runtime/confignorGET /api/v1/auth/me/localizationappears in this boot reading. Their initiators (index.htmlapplyEarlyBrandingand app-shellinitRuntimeConfig, called frommain.tsx) are outside the mounted tree, so whether they double is not measured here.This reverses an earlier reading
objectui#5544 and objectui#11797 recorded three reads per load as the correct budget ("three different questions"), and their pins held that. objectui#12187 rules the budget at one.
consoleUserPreferenceBudget.test.tsx(the objectui#5544 file) is rewritten.userState.inflightShare-11797.test.ts: the first case moves to the shared read. The other two cases (one key loaded twice at once is one request; a load after a save reaches the server) are unchanged.Tests (at
55be3d7, through the verify lock)turbo run build --filter='@object-ui/data-objectstack^...' --filter='@object-ui/app-shell^...' --concurrency=2: 28 successful.@object-ui/data-objectstackwas rebuilt at head.@object-ui/data-objectstacktype-check(tsc --noEmit) and@object-ui/app-shelltype-check(tsc --noEmit && tsc -p tsconfig.test.json). Both pass, and both scripts were echoed.--listFilesOnlyshows that both programs compile the edited tests.createObjectStackUserStateAdapter,sys_user_preferenceorUserStateAdapters, including the new pins:Test Files 14 passed (14),Tests 161 passed (161).consoleUserPreferenceBudget.test.tsx, five cases: one read per boot asking for the three keys; every consumer gets its own row; each adapter writes only its own row; an account switch reads again for the new user; one key loaded twice at once is one request.userState.sharedRead-12187.test.ts, six cases: user scope; nothing kept past the send; data-source scope; untagged rows; a failed read; writes unchanged.scripts/ablation-replace.mjs:const row = await loadExisting();toconst row = await findExisting();, which turns the share off. Anchor 1 to 0, blobcc27d4dtobc3fb25.cc27d4d), andgit diff HEADis empty.dist/is on their resolution path.check:eager-closure(Console eager closure is 3126.9 KB gzipped across 70 of 2253 chunks (budget: 3204.6 KB, headroom: 77.6 KB), membership exact, on a consolevite buildat head),check-changeset-presence,check:changeset-claims,check:pending-changeset-literals,check:control-bytes,check:new-line-citations(0 new),check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots,changeset:check.eslint --no-inline-config --format jsonover the 4 changed.ts/.tsxfiles.DataSourcetyped withanyon the options member; the base file has the same 1.**/*.{ts,tsx}block ofeslint.config.jscovers all four files.projectService|typeChecked|tsconfigRootDir|parserOptionsexits 1), and noeslint-rules/*.jsreads the filesystem. So the diff cannot change the verdict on an untouched file.pnpm lintbelongs to CI.Acceptance notes
Session:
https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sKGenerated by Claude Code