From f7d104f4a80e5d2ff2f9c92b280503d88d7e260c Mon Sep 17 00:00:00 2001 From: Liu Jie Date: Tue, 13 Oct 2026 14:48:27 +1300 Subject: [PATCH 1/2] fix: preserve Map state when reassigning NaN --- __tests__/base.js | 63 +++++++++++++++++++++++++++++++++++++++---- src/plugins/mapset.ts | 11 +++++--- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/__tests__/base.js b/__tests__/base.js index 208a1149..4fc1be9a 100644 --- a/__tests__/base.js +++ b/__tests__/base.js @@ -60,7 +60,7 @@ function runBaseTest( useListener, useArrayMethods = false ) { - const listener = useListener ? function() {} : undefined + const listener = useListener ? function () {} : undefined const {produce, produceWithPatches} = createPatchedImmer({ autoFreeze, @@ -73,7 +73,7 @@ function runBaseTest( const immer = new Immer(options) const {produce} = immer - immer.produce = function(...args) { + immer.produce = function (...args) { return typeof args[1] === "function" && args.length < 3 ? produce(...args, listener) : produce(...args) @@ -2348,6 +2348,59 @@ function runBaseTest( expect(nextState.aMap).toBe(baseState.aMap) }) + it("keeps a Map when NaN is assigned to an existing NaN", () => { + const base = new Map([["value", NaN]]) + const result = produce(base, draft => { + expect(draft.set("value", NaN)).toBe(draft) + }) + expect(result).toBe(base) + }) + + it("keeps the parent when NaN is assigned to a nested Map's existing NaN", () => { + const base = {map: new Map([["value", NaN]])} + const result = produce(base, draft => { + draft.map.set("value", NaN) + }) + expect(result.map).toBe(base.map) + expect(result).toBe(base) + }) + + it.each([ + [NaN, 1], + [1, NaN] + ])("changes a Map value from %s to %s", (before, after) => { + const base = new Map([["value", before]]) + const result = produce(base, draft => { + draft.set("value", after) + }) + expect(result).not.toBe(base) + expect(base.get("value")).toBe(before) + expect(result.get("value")).toBe(after) + }) + + it("adds a missing Map key with a NaN value", () => { + const base = new Map() + const result = produce(base, draft => { + draft.set("value", NaN) + }) + expect(result).not.toBe(base) + expect(base.has("value")).toBe(false) + expect(result.has("value")).toBe(true) + expect(result.get("value")).toBeNaN() + }) + + it.each([ + [0, -0], + [-0, 0] + ])("keeps a Map when %s is replaced with %s", (before, after) => { + const base = new Map([["value", before]]) + const result = produce(base, draft => { + draft.set("value", after) + }) + expect(result).toBe(base) + expect(result.get("value")).toBe(before) + }) + it("returns 'size'", () => { const nextState = produce(baseState, s => { s.aMap.set("newKey", true) @@ -3415,13 +3468,13 @@ function runBaseTest( it("'this' should not be bound anymore - 1", () => { const base = {x: 3} - const next1 = produce(base, function() { + const next1 = produce(base, function () { expect(this).toBe(undefined) }) }) it("'this' should not be bound anymore - 2", () => { - const incrementor = produce(function() { + const incrementor = produce(function () { expect(this).toBe(undefined) }) incrementor() @@ -3430,7 +3483,7 @@ function runBaseTest( it("should be possible to use dynamic bound this", () => { const world = { counter: {count: 1}, - inc: produce(function(draft) { + inc: produce(function (draft) { expect(this).toBe(world) draft.counter.count = this.counter.count + 1 }) diff --git a/src/plugins/mapset.ts b/src/plugins/mapset.ts index c9b21cbe..3113f537 100644 --- a/src/plugins/mapset.ts +++ b/src/plugins/mapset.ts @@ -27,9 +27,8 @@ import { // while keeping the runtime feature detect accurate. (#1273) const _globalIterator = (globalThis as any).Iterator as | undefined - | { from: (iterable: Iterator) => IterableIterator } -const hasIteratorFrom = - typeof _globalIterator?.from === "function" + | {from: (iterable: Iterator) => IterableIterator} +const hasIteratorFrom = typeof _globalIterator?.from === "function" export function enableMapSet() { class DraftMap extends Map { @@ -68,7 +67,11 @@ export function enableMapSet() { set(key: any, value: any) { const state: MapState = this[DRAFT_STATE] assertUnrevoked(state) - if (!latest(state).has(key) || latest(state).get(key) !== value) { + const current = latest(state).get(key) + if ( + !latest(state).has(key) || + (current !== value && !(Number.isNaN(current) && Number.isNaN(value))) + ) { prepareMapCopy(state) markChanged(state) state.assigned_!.set(key, true) From 82cacce4f0e15aa60b9a9c80c78761ce8ca30dfc Mon Sep 17 00:00:00 2001 From: Liu Jie Date: Tue, 13 Oct 2026 14:49:13 +1300 Subject: [PATCH 2/2] chore: preserve existing formatting outside the fix --- __tests__/base.js | 10 +++++----- src/plugins/mapset.ts | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/__tests__/base.js b/__tests__/base.js index 4fc1be9a..50d718f9 100644 --- a/__tests__/base.js +++ b/__tests__/base.js @@ -60,7 +60,7 @@ function runBaseTest( useListener, useArrayMethods = false ) { - const listener = useListener ? function () {} : undefined + const listener = useListener ? function() {} : undefined const {produce, produceWithPatches} = createPatchedImmer({ autoFreeze, @@ -73,7 +73,7 @@ function runBaseTest( const immer = new Immer(options) const {produce} = immer - immer.produce = function (...args) { + immer.produce = function(...args) { return typeof args[1] === "function" && args.length < 3 ? produce(...args, listener) : produce(...args) @@ -3468,13 +3468,13 @@ function runBaseTest( it("'this' should not be bound anymore - 1", () => { const base = {x: 3} - const next1 = produce(base, function () { + const next1 = produce(base, function() { expect(this).toBe(undefined) }) }) it("'this' should not be bound anymore - 2", () => { - const incrementor = produce(function () { + const incrementor = produce(function() { expect(this).toBe(undefined) }) incrementor() @@ -3483,7 +3483,7 @@ function runBaseTest( it("should be possible to use dynamic bound this", () => { const world = { counter: {count: 1}, - inc: produce(function (draft) { + inc: produce(function(draft) { expect(this).toBe(world) draft.counter.count = this.counter.count + 1 }) diff --git a/src/plugins/mapset.ts b/src/plugins/mapset.ts index 3113f537..1fb9cfcf 100644 --- a/src/plugins/mapset.ts +++ b/src/plugins/mapset.ts @@ -27,8 +27,9 @@ import { // while keeping the runtime feature detect accurate. (#1273) const _globalIterator = (globalThis as any).Iterator as | undefined - | {from: (iterable: Iterator) => IterableIterator} -const hasIteratorFrom = typeof _globalIterator?.from === "function" + | { from: (iterable: Iterator) => IterableIterator } +const hasIteratorFrom = + typeof _globalIterator?.from === "function" export function enableMapSet() { class DraftMap extends Map {