From 9abd71d3bdcdc1ff4c5d64d5969eb7e495ee0bad Mon Sep 17 00:00:00 2001 From: Junaid Khan Date: Thu, 5 Nov 2026 17:29:12 +0530 Subject: [PATCH] fix(signals): skip IMMUTABLE_UPDATE_IN_STORE for shallow/raw leaves --- .changeset/shallow-immutable-update-4686.md | 5 ++++ packages/signals/src/store/store.ts | 7 ++++++ .../attribution-immutable-update.test.ts | 23 ++++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 .changeset/shallow-immutable-update-4686.md diff --git a/.changeset/shallow-immutable-update-4686.md b/.changeset/shallow-immutable-update-4686.md new file mode 100644 index 0000000000..a78908e974 --- /dev/null +++ b/.changeset/shallow-immutable-update-4686.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +`IMMUTABLE_UPDATE_IN_STORE` no longer reports wholesale replacements on a shallow store, or of a `markRaw` / unwrappable leaf (#4686). Those values are invalidated as a unit, so leaf draft assignment does not narrow who re-runs — the diagnostic's advice does not apply. diff --git a/packages/signals/src/store/store.ts b/packages/signals/src/store/store.ts index 2d865f3b79..29bef12d46 100644 --- a/packages/signals/src/store/store.ts +++ b/packages/signals/src/store/store.ts @@ -1071,6 +1071,10 @@ function reportReplacedContainers( pb: Record, writtenKeys: Iterable | null ): void { + // Shallow stores (and raw / unwrappable leaves) invalidate the whole + // replaced value as one unit — leaf draft assignment does not narrow + // who re-runs (#4686). The spread-copy diagnostic only helps deep stores. + if (t.s) return; const keys = writtenKeys ?? Reflect.ownKeys(pb); const isArray = Array.isArray(pb); const owner = storeRootOwner(t); @@ -1091,6 +1095,9 @@ function reportReplacedContainers( targetsEqual(ov, nv) ) continue; + // A raw / unwrappable previous value is itself a leaf: replacing it is + // the write, not a container copy the store could have mutated in place. + if (isRawValue(ov) || !isWrappable(ov)) continue; if (isArray) { if (oldArrayItems === null) { oldArrayItems = new Set(); diff --git a/packages/signals/tests/attribution-immutable-update.test.ts b/packages/signals/tests/attribution-immutable-update.test.ts index 8cd1685888..9424d1e4b7 100644 --- a/packages/signals/tests/attribution-immutable-update.test.ts +++ b/packages/signals/tests/attribution-immutable-update.test.ts @@ -6,7 +6,7 @@ */ import { afterEach, describe, expect, it, vi } from "vitest"; import { attribution } from "../src/attribution.js"; -import { createStore, flush, reconcile, OBSERVE } from "../src/index.js"; +import { createStore, flush, markRaw, reconcile, OBSERVE } from "../src/index.js"; import type { DiagnosticEvent } from "../src/core/dev.js"; afterEach(() => { @@ -174,4 +174,25 @@ describe("IMMUTABLE_UPDATE_IN_STORE", () => { expect(hits).toHaveLength(1); expect(hits[0].data!.path).toBe("store.a.b"); }); + + it("does not report replacing a value in a shallow store (#4686)", () => { + const hits = arm(); + const [, setStore] = createStore([{ a: 1, b: 2, c: 3 }], { shallow: true }); + setStore(s => { + s[0] = { a: 1, b: 2, c: 4 }; + }); + flush(); + expect(hits).toEqual([]); + }); + + it("does not report replacing a markRaw leaf (#4686)", () => { + const hits = arm(); + const leaf = markRaw({ a: 1, b: 2, c: 3 }); + const [, setStore] = createStore({ item: leaf }); + setStore(s => { + s.item = { a: 1, b: 2, c: 4 }; + }); + flush(); + expect(hits).toEqual([]); + }); });