From d40cc62209fb0c909e87b7fb1daacc41be7c00f6 Mon Sep 17 00:00:00 2001 From: nyanrus <68762426+nyanrus@users.noreply.github.com> Date: Wed, 27 May 2026 14:30:18 +0900 Subject: [PATCH 1/4] fix: preserve fragment data identity across snapshot updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 `` — 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 --- src/primitives/createFragment.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/primitives/createFragment.ts b/src/primitives/createFragment.ts index c1acda8..af26743 100644 --- a/src/primitives/createFragment.ts +++ b/src/primitives/createFragment.ts @@ -132,9 +132,24 @@ export function createFragmentInternal< next(res) { queueMicrotask(() => { batch(() => { - setResult("data", undefined); - setResult("error", undefined); - + // Each branch owns its full state transition; we do not + // pre-clear `data` to `undefined` before reconciling. + // + // `reconcile({ key: "__id", merge: true })` only walks the + // existing tree in place when the current store value is + // wrappable — solid-js/store's modifiers.ts early-returns + // the new value as-is when it isn't. Reconciling against + // `undefined` therefore always produces a fresh top-level + // reference, defeating the merge contract. Identity- + // sensitive consumers (e.g. ``) + // would re-mount on every snapshot tick, even on pure + // field updates. + // + // The "loading" state intentionally has no case: leaving + // `data` / `error` / `pending` as-is gives consumers + // stale-while-revalidate behaviour rather than flashing an + // empty UI on every transient missing-data snapshot, and + // matches `createLazyLoadQuery`'s observer. switch (res.state) { case "ok": setResult("error", undefined); From d31a930a159ce620f99efb0e01917da8bfc5a8f0 Mon Sep 17 00:00:00 2001 From: Iha Shin Date: Wed, 27 May 2026 14:59:27 +0900 Subject: [PATCH 2/4] chore: clean up comments --- src/primitives/createFragment.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/primitives/createFragment.ts b/src/primitives/createFragment.ts index af26743..0e590b7 100644 --- a/src/primitives/createFragment.ts +++ b/src/primitives/createFragment.ts @@ -132,24 +132,6 @@ export function createFragmentInternal< next(res) { queueMicrotask(() => { batch(() => { - // Each branch owns its full state transition; we do not - // pre-clear `data` to `undefined` before reconciling. - // - // `reconcile({ key: "__id", merge: true })` only walks the - // existing tree in place when the current store value is - // wrappable — solid-js/store's modifiers.ts early-returns - // the new value as-is when it isn't. Reconciling against - // `undefined` therefore always produces a fresh top-level - // reference, defeating the merge contract. Identity- - // sensitive consumers (e.g. ``) - // would re-mount on every snapshot tick, even on pure - // field updates. - // - // The "loading" state intentionally has no case: leaving - // `data` / `error` / `pending` as-is gives consumers - // stale-while-revalidate behaviour rather than flashing an - // empty UI on every transient missing-data snapshot, and - // matches `createLazyLoadQuery`'s observer. switch (res.state) { case "ok": setResult("error", undefined); From bdf67806f9dfe5d360910dbdc27c8f0f52594147 Mon Sep 17 00:00:00 2001 From: Iha Shin Date: Wed, 27 May 2026 15:02:42 +0900 Subject: [PATCH 3/4] fix: handle loading fragment state properly --- src/primitives/createFragment.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/primitives/createFragment.ts b/src/primitives/createFragment.ts index 0e590b7..9ead7ed 100644 --- a/src/primitives/createFragment.ts +++ b/src/primitives/createFragment.ts @@ -143,6 +143,11 @@ export function createFragmentInternal< setResult("error", res.error); setResult("pending", false); break; + case "loading": + setResult("data", undefined); + setResult("error", undefined); + setResult("pending", true); + break; } }); }); From ac5b2560c4b2725e8afba8d31a6f4514bac9bd48 Mon Sep 17 00:00:00 2001 From: Iha Shin Date: Wed, 27 May 2026 15:03:10 +0900 Subject: [PATCH 4/4] chore: add changeset --- .changeset/common-clocks-grab.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/common-clocks-grab.md diff --git a/.changeset/common-clocks-grab.md b/.changeset/common-clocks-grab.md new file mode 100644 index 0000000..5efde6f --- /dev/null +++ b/.changeset/common-clocks-grab.md @@ -0,0 +1,5 @@ +--- +"solid-relay": patch +--- + +fix: preserve fragment data identity across snapshot updates