diff --git a/src/current.ts b/src/current.ts index be027225..f6d464e6 100644 --- a/src/current.ts +++ b/src/current.ts @@ -64,38 +64,23 @@ function getCurrent(target: any) { if (!isDraftable(target, proxyDraft?.options)) return target; const type = getType(target); if (proxyDraft && !proxyDraft.operated) return proxyDraft.original; - let currentValue: any; - function ensureShallowCopy() { - if (!currentValue || currentValue === target) + if (proxyDraft) proxyDraft.finalized = true; + let currentValue = target; + if (proxyDraft) { + try { currentValue = type === DraftType.Map ? new Map(target) : type === DraftType.Set - ? Array.from(proxyDraft!.setMap!.values()!) - : shallowCopy(target, proxyDraft?.options); - } - - if (proxyDraft) { - // It's a proxy draft, let's create a shallow copy eagerly - proxyDraft.finalized = true; - try { - ensureShallowCopy(); + ? Array.from(proxyDraft.setMap!.values()!) + : shallowCopy(target, proxyDraft.options); } finally { proxyDraft.finalized = false; } - } else { - // It's not a proxy draft, let's use the target directly and let's see - // lazily if we need to create a shallow copy - currentValue = target; } - forEach(currentValue, (key, value) => { if (proxyDraft && isEqual(get(proxyDraft.original, key), value)) return; - const newValue = getCurrent(value); - if (newValue !== value) { - ensureShallowCopy(); - set(currentValue, key, newValue); - } + set(currentValue, key, getCurrent(value)); }); return type === DraftType.Set ? new Set(currentValue) : currentValue; } diff --git a/src/utils/copy.ts b/src/utils/copy.ts index f53aa257..1af6d89f 100644 --- a/src/utils/copy.ts +++ b/src/utils/copy.ts @@ -29,7 +29,7 @@ function strictCopy(target: any) { const propIsEnum = Object.prototype.propertyIsEnumerable; -export function shallowCopy(original: any, options?: Options) { +export function shallowCopy(original: any, options: Options) { let markResult: any; if (Array.isArray(original)) { return Array.prototype.concat.call(original); @@ -38,7 +38,7 @@ export function shallowCopy(original: any, options?: Options) { } else if (original instanceof Map) { return new Map(original); } else if ( - options?.mark && + options.mark && ((markResult = options.mark(original, dataTypes)), markResult !== undefined) && markResult !== dataTypes.mutable diff --git a/test/current.test.ts b/test/current.test.ts index 631d4476..038998cf 100644 --- a/test/current.test.ts +++ b/test/current.test.ts @@ -223,3 +223,83 @@ test('nested draft', () => { expect(current(draft.j) === current(draft.j)).toBeTruthy(); }); }); + +test('#47 current creates new copies of the objects where unnecessary', () => { + const obj = { k: 42 }; + const original = { x: { y: { z: [obj] } } }; + const yReplace = { z: [obj] }; + + // with create + const withCreate = create(original, (draft) => { + draft.x.y = yReplace; + }); + expect(withCreate.x.y === yReplace).toBe(true); + expect(withCreate.x.y.z[0] === obj).toBe(true); + // with draft + current + const [draft] = create(original); + draft.x.y = yReplace; + const withDraft = current(draft); + expect(withDraft.x.y === yReplace).toBe(true); + expect(withDraft.x.y.z[0] === obj).toBe(true); +}); + +test('current() for changed Set/Map draft', () => { + const obj = { k: 42 }; + const base = { x: { y: { z: obj } }, a: new Set([1]), b: new Map([[1, 2]]) }; + create(base, (draft) => { + draft.a.add(2); + draft.b.delete(1); + draft.x.y.z = { k: 43 }; + const c = current(draft); + expect(c.a.has(2)).toBeTruthy(); + expect(c.a.has(1)).toBeTruthy(); + expect(c.b.has(1)).toBeFalsy(); + expect(c.x.y.z.k).toBe(43); + expect(JSON.stringify(c)).toMatchInlineSnapshot( + `"{"x":{"y":{"z":{"k":43}}},"a":{},"b":{}}"` + ); + }); +}); + +test('Avoid deep copies', () => { + const obj = { k: 42 }; + const base = { x: { y: { z: obj } }, a: { c: 1 } }; + create(base, (draft) => { + // @ts-ignore + const a = draft.a; + a.c = 2; + // @ts-ignore + delete draft.a; + // @ts-ignore + draft.x1 = { y1: { z1: obj }, a }; + const c = current(draft); + // @ts-ignore + expect(c.x1.y1.z1).toBe(obj); + expect(JSON.stringify(c)).toMatchInlineSnapshot( + `"{"x":{"y":{"z":{"k":42}}},"x1":{"y1":{"z1":{"k":42}},"a":{"c":2}}}"` + ); + }); +}); + +test('nested create() - Avoid deep copies', () => { + const obj = { k: 42 }; + const base = { x: { y: { z: obj } }, a: { c: 1 } }; + const base0 = { x: { y: { z: obj } }, a: { c: 1 } }; + create(base0, (draft0) => { + // @ts-ignore + const a = draft0.a; + a.c = 2; + // @ts-ignore + delete draft0.a; + create(base, (draft) => { + // @ts-ignore + draft.x1 = { y1: { z1: obj }, a }; + const c = current(draft); + // @ts-ignore + expect(c.x1.y1.z1).toBe(obj); + expect(JSON.stringify(c)).toMatchInlineSnapshot( + `"{"x":{"y":{"z":{"k":42}}},"a":{"c":1},"x1":{"y1":{"z1":{"k":42}},"a":{"c":2}}}"` + ); + }); + }); +}); diff --git a/test/immer-non-support.test.ts b/test/immer-non-support.test.ts index 0f724e0f..8a083767 100644 --- a/test/immer-non-support.test.ts +++ b/test/immer-non-support.test.ts @@ -14,8 +14,9 @@ import { enablePatches, applyPatches, setUseStrictShallowCopy, + current as immerCurrent, } from 'immer'; -import { create, apply } from '../src'; +import { create, apply, current } from '../src'; enableMapSet(); @@ -566,3 +567,25 @@ test(`Object values of a Map are not frozen anymore #1119`, () => { expect(Object.isFrozen(product)).toBeTruthy(); } }); + +test('#47 Avoid deep copies', () => { + { + const obj = { k: 42 }; + const base = { x: { y: { z: obj } } }; + produce(base, (draft) => { + draft.x = { y: { z: obj } }; + const c = immerCurrent(draft); + // ! it should be equal + expect(c.x.y.z).not.toBe(obj); + }); + } + { + const obj = { k: 42 }; + const base = { x: { y: { z: obj } } }; + create(base, (draft) => { + draft.x = { y: { z: obj } }; + const c = current(draft); + expect(c.x.y.z).toBe(obj); + }); + } +});