Scope graph-view and schema-view layouts to the browser tab#1894
Open
kmcginnes wants to merge 5 commits into
Open
Scope graph-view and schema-view layouts to the browser tab#1894kmcginnes wants to merge 5 commits into
kmcginnes wants to merge 5 commits into
Conversation
db6f162 to
b182877
Compare
b182877 to
8347655
Compare
The two view-layout atoms shared one localForage value across tabs, so opening the app in a second tab clobbered the first tab's sidebar and toggle state (last-writer-wins). Extract the per-tab sessionStorage + shared localForage breadcrumb trick that createActiveConfigurationAtom() used into a generic createSessionScopedAtom<T>, and refactor active-config onto it so the subtle seed logic lives in one named place. Point both layout atoms at it with zod-validated codecs co-located with each model: schema-view is plain JSON; graph-view serializes its activeToggles Set as an array. parseSessionJson() rejects a stale or hand-edited per-tab value with the wrong shape, falling through to the breadcrumb. The breadcrumb key/shape are unchanged, so existing stored layouts are reused with no migration.
Add an ADR recording the per-tab session-scoped storage primitive: the three named cross-tab scopes (per-tab / shared-reconciled / shared-blind-write), why layout is per-tab, and how it relates to the active-connection and per-key-diff-merge ADRs and spike #1876. Note in the per-key-diff-merge ADR that layout has since moved from a shared scalar to per-tab session scope, so it is no longer a standing example of a shared blind-write scalar. Add a test exercising createSessionScopedAtom with the real graphViewLayoutCodec over the cold-start claim path: a breadcrumb holding a native activeToggles Set is seeded as a Set and claimed into sessionStorage as its array-serialized form. Previously the generic helper and the Set-bearing codec were only tested in isolation.
parseSessionJson swallowed both JSON and schema-validation errors into an indistinguishable null, hiding corruption and conflating it with a legitimate absent value. Separate detecting corruption from deciding what to do about it: deserialize now returns null only for an absent value and throws (SyntaxError or ZodError) on a present-but-invalid one. createSessionScopedAtom owns seeding policy, so it is the seam that catches: a corrupt per-tab value (or a sessionStorage read that throws a SecurityError when DOM storage is blocked) is logged and treated as a miss, falling through to the breadcrumb then the default rather than crashing app startup. Update the per-tab-storage ADR to describe the throw-and-recover flow.
- Move the graphViewLayout activeToggles Set rebuild from an object-level transform onto the activeToggles field, so the transform sits on the field it converts and the object schema infers the runtime shape. - Correct the SessionValueCodec.deserialize contract doc: it returns null only for an absent value and throws on a corrupt one, which the seam catches. Do not swallow errors in the codec. - Drop the unused session() accessor from the test tab harness. - Add cross-tab coverage for both layout codecs through the real codecs (graph view proves the activeToggles Set survives the array round-trip across a write-then-cold-start sequence; schema view covers isolation and cold-start seeding), matching the active-connection assurances.
8347655 to
b2f6ab7
Compare
The global test setup already gives every test a fresh IndexedDB backend (dropInstance + new IDBFactory per test), so the per-describe beforeEach(localForage.clear()) blocks added no isolation.
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.
Description
The graph-view and schema-view layout state (active sidebar tab, sidebar width, view toggles, details-auto-open preference) was persisted as a single localForage value shared across all browser tabs. Opening the app in a second tab clobbered the first tab's layout — last-writer-wins over the whole value.
This scopes both layouts to the browser tab using the same per-tab mechanism the active connection already uses: the live value lives in
sessionStorage(survives this tab's reload, never leaks to other tabs), with a shared localForage breadcrumb that seeds a freshly-opened tab from the last-used layout on cold start.Core change: the per-tab + breadcrumb "trick" that was inlined in
activeConnectionStorage.tsis extracted into a genericcreateSessionScopedAtom<T>.createActiveConfigurationAtomis refactored onto it (so the subtle seed-and-claim logic lives in one place), and both layout atoms now route through it with zod-validated codecs.Notable details:
sessionStorageholds strings, so each layout has a codec.deserializereturnsnullfor an absent value but throws on a corrupt/wrong-shape one; the seam (createSessionScopedAtom) catches, logs, and falls back to the breadcrumb then the default — so a stale or hand-edited value can never crash the top-level-await startup. Detecting corruption is kept separate from deciding what to do about it.Setserialization. Graph-view'sactiveTogglesis a runtimeSet; its codec serializes it as an array for JSON and rebuilds theSeton read. Schema-view is plain JSON.The new per-tab scope is recorded in an ADR alongside the two existing shared scopes (reconciled maps, blind-write scalars).
Validation
pnpm checkspasses (types, lint, format).pnpm testpasses (full suite, 2086 tests), including new coverage insessionScopedStorage.test.ts(seed order, corrupt-value +SecurityErrorrecovery, theSet-bearing cold-start claim, and cross-tab isolation/seeding through the real graph-view and schema-view codecs).activeTogglesSetround-trips across a write-then-cold-start sequence.How to read
parseSessionJsonand the corrupt-value recovery seamactiveTogglesSet↔ array serializationRelated Issues
Check List
pnpm checkspasses with no errors.pnpm testpasses with no failures.