diff --git a/benchmark-array.jpg b/benchmark-array.jpg index e0a138ad..8547585e 100644 Binary files a/benchmark-array.jpg and b/benchmark-array.jpg differ diff --git a/benchmark-object.jpg b/benchmark-object.jpg index 64a8478d..463aed56 100644 Binary files a/benchmark-object.jpg and b/benchmark-object.jpg differ diff --git a/benchmark.jpg b/benchmark.jpg index 4786468f..573e6db2 100644 Binary files a/benchmark.jpg and b/benchmark.jpg differ diff --git a/src/apply.ts b/src/apply.ts index 6cbbf8ea..d05aed89 100644 --- a/src/apply.ts +++ b/src/apply.ts @@ -52,7 +52,10 @@ export function apply( let base: any = draft; for (let index = 0; index < path.length - 1; index += 1) { const parentType = getType(base); - const key = String(path[index]); + let key = path[index]; + if (typeof key !== 'string' && typeof key !== 'number') { + key = String(key); + } if ( ((parentType === DraftType.Object || parentType === DraftType.Array) && diff --git a/src/draft.ts b/src/draft.ts index 2dd1ab4e..057be616 100644 --- a/src/draft.ts +++ b/src/draft.ts @@ -244,7 +244,7 @@ export function createDraft(createDraftOptions: { target.finalities.draft.push((patches, inversePatches) => { const oldProxyDraft = getProxyDraft(proxy)!; // if target is a Set draft, `setMap` is the real Set copies proxy mapping. - const copy = target.type === DraftType.Set ? target.setMap : target.copy; + let copy = target.type === DraftType.Set ? target.setMap : target.copy; const draft = get(copy, key!); const proxyDraft = getProxyDraft(draft); if (proxyDraft) { @@ -260,6 +260,22 @@ export function createDraft(createDraftOptions: { target.options.updatedValues ?? new WeakMap(); target.options.updatedValues.set(updatedValue, proxyDraft.original); } + if ( + proxyDraft.parent!.key !== undefined && + proxyDraft.parent!.parent!.proxy + ) { + const parent = + proxyDraft.parent!.parent!.proxy[proxyDraft.parent!.key]; + const parentProxyDraft = getProxyDraft(parent); + if (parentProxyDraft === undefined) { + // !case: handle assigning a non-draft with the same key + copy = parent; + const current = get(copy, key!); + if (!getProxyDraft(current)) { + updatedValue = current; + } + } + } // final update value set(copy, key!, updatedValue); } diff --git a/src/utils/draft.ts b/src/utils/draft.ts index 3e8ed3ad..1a16b3fe 100644 --- a/src/utils/draft.ts +++ b/src/utils/draft.ts @@ -67,7 +67,8 @@ export function get(target: any, key: PropertyKey) { } export function set(target: any, key: PropertyKey, value: any) { - if (getType(target) === DraftType.Map) { + const type = getType(target); + if (type === DraftType.Map) { target.set(key, value); } else { target[key] = value; diff --git a/src/utils/finalize.ts b/src/utils/finalize.ts index 52d0e727..2b094486 100644 --- a/src/utils/finalize.ts +++ b/src/utils/finalize.ts @@ -27,9 +27,11 @@ export function handleValue(target: any, handledSet: WeakSet) { if (isDraft(value)) { const proxyDraft = getProxyDraft(value)!; ensureShallowCopy(proxyDraft); - const updatedValue = proxyDraft.assignedMap?.size - ? proxyDraft.copy - : proxyDraft.original; + // A draft where a child node has been changed, or assigned a value + const updatedValue = + proxyDraft.assignedMap?.size || proxyDraft.operated + ? proxyDraft.copy + : proxyDraft.original; // final update value set(isSet ? setMap! : target, key, updatedValue); } else { diff --git a/test/immer-non-support.test.ts b/test/immer-non-support.test.ts index c888d171..975ef03f 100644 --- a/test/immer-non-support.test.ts +++ b/test/immer-non-support.test.ts @@ -2,8 +2,16 @@ /* eslint-disable prefer-destructuring */ /* eslint-disable no-param-reassign */ /* eslint-disable no-lone-blocks */ -import { produce, enableMapSet, setAutoFreeze, Immutable } from 'immer'; -import { create } from '../src'; +import { + produce, + enableMapSet, + setAutoFreeze, + Immutable, + produceWithPatches, + enablePatches, + applyPatches, +} from 'immer'; +import { create, apply } from '../src'; enableMapSet(); @@ -303,3 +311,65 @@ test('circular reference', () => { ); } }); + +test('#18 - set: assigning a non-draft with the same key', () => { + const baseState = { + array: [ + { + one: { + two: { + three: 3, + }, + }, + }, + ], + }; + + const created = create( + baseState, + (draft) => { + draft.array[0].one.two.three = 2; + const two = draft.array[0].one.two; + const one = new Set(); + // @ts-ignore + draft.array = [{ one }]; + // @ts-ignore + one.add(two); + // @ts-ignore + expect(Array.from(draft.array[0].one)[0].three).toBe(2); + }, + { + enablePatches: true, + } + ); + // @ts-ignore + expect(Array.from(created[0].array[0].one)[0].three).toBe(2); + expect(apply(baseState, created[1])).toEqual(created[0]); + // expect(apply(created[0], created[2])).toEqual(baseState); + + enablePatches(); + // @ts-ignore + const produced = produceWithPatches(baseState, (draft: any) => { + draft.array[0].one.two.three = 2; + const two = draft.array[0].one.two; + const one = new Set(); + // @ts-ignore + draft.array = [{ one }]; + // @ts-ignore + one.add(two); + // @ts-ignore + expect(Array.from(draft.array[0].one)[0].three).toBe(2); + }); + + // @ts-ignore + expect(() => { + // @ts-ignore + // eslint-disable-next-line no-unused-expressions + Array.from(produced[0].array[0].one)[0].three; + }).toThrowError(); + + // @ts-ignore + // expect(applyPatches(baseState, produced[1])).toEqual(produced[0]); + // @ts-ignore + // expect(applyPatches(produced[0], produced[2])).toEqual(baseState); +}); diff --git a/test/index.test.ts b/test/index.test.ts index 772210a9..81014398 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -2750,3 +2750,550 @@ test('can return an object that references itself', () => { create(res, (draft) => res.self, { enableAutoFreeze: true }); }).toThrowErrorMatchingInlineSnapshot(`"Maximum call stack size exceeded"`); }); + +test('#18 - array: assigning a non-draft with the same key', () => { + const baseState = { + array: [ + { + one: { + two: 3, + }, + }, + ], + }; + + const created = create(baseState, (draft) => { + draft.array[0].one.two = 2; + + draft.array = [draft.array[0]]; + }); + + expect(created.array[0].one.two).toBe(2); +}); + +test('#18 - object: assigning a non-draft with the same key', () => { + const baseState = { + object: { + zero: { + one: { + two: 3, + }, + }, + }, + }; + + const created = create(baseState, (draft) => { + draft.object.zero.one.two = 2; + + draft.object = { zero: draft.object.zero }; + }); + + expect(created.object.zero.one.two).toBe(2); +}); + +test('#18 - array: assigning a non-draft with the same key - deep1', () => { + const baseState = { + array: [ + { + one: { + two: { + three: 3, + }, + }, + }, + ], + }; + + const created = create(baseState, (draft) => { + draft.array[0].one.two.three = 2; + draft.array = [{ one: draft.array[0].one }]; + }); + + expect(created.array[0].one.two.three).toBe(2); +}); + +test('#18 - array: assigning a non-draft with the same key - deep2', () => { + const baseState = { + array: [ + { + one: { + two: { + three: 3, + }, + }, + }, + ], + }; + + const created = create(baseState, (draft) => { + draft.array[0].one.two.three = 2; + draft.array = [{ one: { two: draft.array[0].one.two } }]; + }); + + expect(created.array[0].one.two.three).toBe(2); +}); + +test('#18 - array: assigning a non-draft with the same key - deep3', () => { + const baseState = { + array: [ + { + one: { + two: { + three: 3, + }, + }, + }, + ], + }; + + const created = create(baseState, (draft) => { + draft.array[0].one.two.three = 2; + const tow = draft.array[0].one.two; + // @ts-ignore + draft.array = [{ one: {} }]; + draft.array[0].one.two = tow; + expect(draft.array[0].one.two.three).toBe(2); + }); + + expect(created.array[0].one.two.three).toBe(2); +}); + +test('#18 - array: assigning a non-draft with the same key - deep5', () => { + const baseState = { + array: [ + { + one: { + two: { + three: 3, + }, + }, + }, + ], + }; + + const created = create( + baseState, + (draft) => { + draft.array[0].one.two.three = 2; + const two = draft.array[0].one.two; + // @ts-ignore + const one = []; + // @ts-ignore + draft.array1 = [{ one }]; + // @ts-ignore + one.push(two); + }, + { + enablePatches: true, + } + ); + + // @ts-ignore + expect(Array.from(created[0].array1[0].one)[0].three).toBe(2); + + // @ts-ignore + expect(apply(baseState, created[1])).toEqual(created[0]); + // @ts-ignore + expect(apply(created[0], created[2])).toEqual(baseState); +}); + +test('#18 - object: assigning a non-draft with the same key - deep5', () => { + const baseState = { + array: [ + { + one: { + two: { + three: 3, + }, + }, + }, + ], + }; + + const created = create( + baseState, + (draft) => { + draft.array[0].one.two.three = 2; + const two = draft.array[0].one.two; + // @ts-ignore + const one = {}; + // @ts-ignore + draft.array1 = [{ one }]; + // @ts-ignore + one.x = two; + }, + { + enablePatches: true, + } + ); + + // @ts-ignore + expect(created[0].array1[0].one.x.three).toBe(2); + + // @ts-ignore + expect(apply(baseState, created[1])).toEqual(created[0]); + // @ts-ignore + expect(apply(created[0], created[2])).toEqual(baseState); +}); + +test('#18 - set: assigning a non-draft with the same key - deep5', () => { + const baseState = { + array: [ + { + one: { + two: { + three: 3, + }, + }, + }, + ], + }; + + const created = create( + baseState, + (draft) => { + draft.array[0].one.two.three = 2; + const two = draft.array[0].one.two; + // @ts-ignore + const one = new Set(); + // @ts-ignore + draft.array1 = [{ one }]; + // @ts-ignore + one.add(two); + }, + { + enablePatches: true, + } + ); + + // @ts-ignore + expect(Array.from(created[0].array1[0].one)[0].three).toBe(2); + + // @ts-ignore + expect(apply(baseState, created[1])).toEqual(created[0]); + // @ts-ignore + expect(apply(created[0], created[2])).toEqual(baseState); +}); + +test('#18 - map: assigning a non-draft with the same key - deep5', () => { + const baseState = { + array: [ + { + one: { + two: { + three: 3, + }, + }, + }, + ], + }; + + const created = create( + baseState, + (draft) => { + draft.array[0].one.two.three = 2; + const two = draft.array[0].one.two; + // @ts-ignore + const one = new Map(); + // @ts-ignore + draft.array1 = [{ one }]; + // @ts-ignore + one.set(0, two); + }, + { + enablePatches: true, + } + ); + + // @ts-ignore + expect(Array.from(created[0].array1[0].one.values())[0].three).toBe(2); + + // @ts-ignore + expect(apply(baseState, created[1])).toEqual(created[0]); + // @ts-ignore + expect(apply(created[0], created[2])).toEqual(baseState); +}); + +test('#18 - set: assigning a non-draft with the same key - deep6', () => { + const baseState = { + array: [ + { + one: { + two: { + three: 3, + }, + }, + }, + ], + }; + + const created = create( + baseState, + (draft) => { + draft.array[0].one.two.three = 2; + const two = draft.array[0].one.two; + const one = new Set(); + // @ts-ignore + draft.array = [{ one }]; + // @ts-ignore + one.add(two); + // @ts-ignore + expect(Array.from(draft.array[0].one.values())[0].three).toBe(2); + }, + { + enablePatches: true, + } + ); + + // @ts-ignore + expect(Array.from(created[0].array[0].one.values())[0].three).toBe(2); + + // @ts-ignore + expect(apply(baseState, created[1])).toEqual(created[0]); + // @ts-ignore + // expect(apply(created[0], created[2])).toEqual(baseState); +}); + +test('#18 - set: assigning a non-draft with the same key - deep3', () => { + const baseState = { + array: [ + { + one: { + two: { + three: 3, + }, + }, + }, + ], + }; + + const created = create(baseState, (draft) => { + draft.array[0].one.two.three = 2; + const two = draft.array[0].one.two; + const one = new Set(); + // @ts-ignore + draft.array = [{ one }]; + // @ts-ignore + one.add(two); + // @ts-ignore + expect(Array.from(draft.array[0].one)[0].three).toBe(2); + }); + + // @ts-ignore + expect(Array.from(created.array[0].one)[0].three).toBe(2); +}); + +test('#18 - set: assigning a non-draft with the same key - deep7', () => { + const baseState = { + array: [ + { + one: { + two: { + three: 3, + }, + }, + }, + ], + }; + + const created = create(baseState, (draft) => { + draft.array[0].one.two.three = 2; + const two = draft.array[0].one.two; + const one = new Set(); + // @ts-ignore + draft.array = [0, { one }]; + // @ts-ignore + one.add(two); + // @ts-ignore + expect(Array.from(draft.array[1].one)[0].three).toBe(2); + }); + + // @ts-ignore + expect(Array.from(created.array[1].one)[0].three).toBe(2); +}); + +test('#18 - array: assigning a non-draft with the same key - deep4', () => { + const baseState = { + array: [ + { + one: { + two: { + three: { + four: 3, + }, + }, + }, + }, + ], + }; + + const created = create(baseState, (draft) => { + draft.array[0].one.two.three.four = 2; + draft.array = [{ one: { two: { three: draft.array[0].one.two.three } } }]; + }); + + expect(created.array[0].one.two.three.four).toBe(2); +}); + +test('#18 - map: assigning a non-draft with the same key', () => { + const baseState: any = { + map: new Map([ + [ + 0, + { + one: { + two: 3, + }, + }, + ], + ]), + }; + + const created = create(baseState, (draft) => { + draft.map.get(0).one.two = 2; + draft.map = new Map([[0, draft.map.get(0)]]); + }); + expect(created.map.get(0).one.two).toBe(2); +}); + +test('#18 - map: assigning a non-draft with the same key - enablePatches', () => { + const baseState: any = { + map: new Map([ + [ + 0, + { + one: { + two: 3, + }, + }, + ], + ]), + }; + + const created = create( + baseState, + (draft) => { + draft.map.get(0).one.two = 2; + draft.map = new Map([[0, draft.map.get(0)]]); + }, + { + enablePatches: true, + } + ); + expect(created[0].map.get(0).one.two).toBe(2); + + expect(apply(baseState, created[1])).toEqual(created[0]); + expect(apply(created[0], created[2])).toEqual(baseState); +}); + +test('#18 - set: assigning a non-draft with the same key', () => { + const baseState: any = { + set: new Set([ + { + one: { + two: 3, + }, + }, + ]), + }; + + const created = create(baseState, (draft) => { + draft.set.values().next().value.one.two = 2; + draft.set = new Set([Array.from(draft.set)[0]]); + }); + expect(created.set.values().next().value.one.two).toBe(2); +}); + +test('#18 - set: assigning a non-draft with the same key - enablePatches', () => { + const baseState: any = { + set: new Set([ + { + one: { + two: 3, + }, + }, + ]), + }; + + const created = create( + baseState, + (draft) => { + draft.set.values().next().value.one.two = 2; + draft.set = new Set([Array.from(draft.set)[0]]); + }, + { + enablePatches: true, + } + ); + expect(created[0].set.values().next().value.one.two).toBe(2); + + expect(apply(baseState, created[1])).toEqual(created[0]); + expect(apply(created[0], created[2])).toEqual(baseState); +}); + +test('#18 - array: assigning a non-draft with the same key - enablePatches', () => { + const baseState = { + array: [ + { + one: { + two: 3, + }, + }, + ], + }; + + const created = create( + baseState, + (draft) => { + draft.array[0].one.two = 2; + + draft.array = [draft.array[0]]; + }, + { + enablePatches: true, + } + ); + expect(created[0].array[0].one.two).toBe(2); + + expect(apply(baseState, created[1])).toEqual(created[0]); + expect(apply(created[0], created[2])).toEqual(baseState); +}); + +test('#18: assigning a non-draft with the different key - enablePatches', () => { + const baseState = { + array: [ + { + one: { + two: 3, + }, + }, + ], + }; + + const created = create( + baseState, + (draft) => { + draft.array[0].one.two = 2; + // @ts-ignore + draft.array1 = [0, { c: draft.array[0] }]; + // @ts-ignore + draft.map = [0, new Map([[0, draft.array[0]]])]; + // @ts-ignore + draft.set = [0, new Set([draft.array[0]])]; + }, + { + enablePatches: true, + } + ); + // @ts-ignore + expect(created[0].array[0].one.two).toBe(2); + // @ts-ignore + expect(created[0].array1[1].c.one.two).toBe(2); + // @ts-ignore + expect(created[0].map[1].get(0).one.two).toBe(2); + // @ts-ignore + expect(Array.from(created[0].set[1])[0].one.two).toBe(2); + + expect(apply(baseState, created[1])).toEqual(created[0]); + expect(apply(created[0], created[2])).toEqual(baseState); +});