Skip to content

Commit d40cc62

Browse files
committed
fix: preserve fragment data identity across snapshot updates
`reconcile({ key: "__id", merge: true })` only preserves identity by walking the existing tree in place when the current store value is wrappable. solid-js/store's `reconcile` (modifiers.ts) early-returns the new value as-is when it isn't, so reconciling against `undefined` silently produces a fresh top-level reference and the merge contract goes away. The result observer was pre-clearing `data` to `undefined` inside the same batch that subsequently applied the reconciled snapshot, so that contract was being defeated on every Relay update. Identity-sensitive consumers — most prominently `<Show keyed when={data()}>` — re-mounted their subtree on every snapshot tick, including pure field updates such as a reaction toggle or a polled count delta. In a real fediverse timeline (hackers.pub `/feed`, ~25 cards × multiple Kobalte primitives per card), this dominated CPU and heap during interaction (~+10 MB/s sustained vs. ~0 MB/s when idle on the same route). Restructure the observer so each branch owns its full state transition and no shared pre-clears defeat the reconcile: - "ok" — clear `error`, set `pending` to false, reconcile `data`. - "error" — set `data` to `undefined`, set `error`, set `pending`. - "loading" — no case. Leave `data` / `error` / `pending` as-is for stale-while-revalidate behaviour, matching `createLazyLoadQuery`'s observer in the same package. Behaviour is otherwise unchanged. Observers never saw the intermediate pre-clear state — every mutation is inside the same `batch()` — so the fix is invisible to anything except identity comparisons across snapshot boundaries, which the reconcile contract is exactly there to keep stable. Assisted-by: Claude Code:claude-opus-4-7
1 parent 1bfb1a9 commit d40cc62

1 file changed

Lines changed: 18 additions & 3 deletions

File tree

src/primitives/createFragment.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,24 @@ export function createFragmentInternal<
132132
next(res) {
133133
queueMicrotask(() => {
134134
batch(() => {
135-
setResult("data", undefined);
136-
setResult("error", undefined);
137-
135+
// Each branch owns its full state transition; we do not
136+
// pre-clear `data` to `undefined` before reconciling.
137+
//
138+
// `reconcile({ key: "__id", merge: true })` only walks the
139+
// existing tree in place when the current store value is
140+
// wrappable — solid-js/store's modifiers.ts early-returns
141+
// the new value as-is when it isn't. Reconciling against
142+
// `undefined` therefore always produces a fresh top-level
143+
// reference, defeating the merge contract. Identity-
144+
// sensitive consumers (e.g. `<Show keyed when={data()}>`)
145+
// would re-mount on every snapshot tick, even on pure
146+
// field updates.
147+
//
148+
// The "loading" state intentionally has no case: leaving
149+
// `data` / `error` / `pending` as-is gives consumers
150+
// stale-while-revalidate behaviour rather than flashing an
151+
// empty UI on every transient missing-data snapshot, and
152+
// matches `createLazyLoadQuery`'s observer.
138153
switch (res.state) {
139154
case "ok":
140155
setResult("error", undefined);

0 commit comments

Comments
 (0)