From 2d3714c0cd979f249f8ba355f425d1ce5b2f39c3 Mon Sep 17 00:00:00 2001 From: Michael Sweeney Date: Wed, 23 Oct 2024 14:27:32 -0700 Subject: [PATCH 1/8] fixed issue with .has() when new value is added and then deleted or cleared --- src/vanilla/utils/proxyMap.ts | 6 ++---- src/vanilla/utils/proxySet.ts | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/vanilla/utils/proxyMap.ts b/src/vanilla/utils/proxyMap.ts index 44adf1c8..0964f651 100644 --- a/src/vanilla/utils/proxyMap.ts +++ b/src/vanilla/utils/proxyMap.ts @@ -88,10 +88,8 @@ export function proxyMap(entries?: Iterable<[K, V]> | undefined | null) { has(key: K) { const map = getMapForThis(this) const exists = map.has(key) - if (!exists) { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - this.index // touch property for tracking - } + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + this.data.length // touch property for tracking return exists }, set(key: K, value: V) { diff --git a/src/vanilla/utils/proxySet.ts b/src/vanilla/utils/proxySet.ts index 65cf11d0..298ff81d 100644 --- a/src/vanilla/utils/proxySet.ts +++ b/src/vanilla/utils/proxySet.ts @@ -73,10 +73,8 @@ export function proxySet(initialValues?: Iterable | null) { const map = getMapForThis(this) const v = maybeProxify(value) const exists = map.has(v) - if (!exists) { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - this.index // touch property for tracking - } + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + this.data.length // touch property for tracking return exists }, add(value: T) { From dd50d044bd8087b1859125917b34bc4fcef87d2a Mon Sep 17 00:00:00 2001 From: Michael Sweeney Date: Thu, 24 Oct 2024 05:12:58 -0700 Subject: [PATCH 2/8] removed unneeded variable assignment --- src/vanilla/utils/proxyMap.ts | 3 +-- src/vanilla/utils/proxySet.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vanilla/utils/proxyMap.ts b/src/vanilla/utils/proxyMap.ts index 0964f651..bd1ee5d2 100644 --- a/src/vanilla/utils/proxyMap.ts +++ b/src/vanilla/utils/proxyMap.ts @@ -87,10 +87,9 @@ export function proxyMap(entries?: Iterable<[K, V]> | undefined | null) { }, has(key: K) { const map = getMapForThis(this) - const exists = map.has(key) // eslint-disable-next-line @typescript-eslint/no-unused-expressions this.data.length // touch property for tracking - return exists + return map.has(key) }, set(key: K, value: V) { if (!isProxy(this)) { diff --git a/src/vanilla/utils/proxySet.ts b/src/vanilla/utils/proxySet.ts index 298ff81d..88f6af09 100644 --- a/src/vanilla/utils/proxySet.ts +++ b/src/vanilla/utils/proxySet.ts @@ -72,10 +72,9 @@ export function proxySet(initialValues?: Iterable | null) { has(value: T) { const map = getMapForThis(this) const v = maybeProxify(value) - const exists = map.has(v) // eslint-disable-next-line @typescript-eslint/no-unused-expressions this.data.length // touch property for tracking - return exists + return map.has(v) }, add(value: T) { if (!isProxy(this)) { From e9d75adfd9fbd324f2c96b9090666fa962b9ceb3 Mon Sep 17 00:00:00 2001 From: Michael Sweeney Date: Thu, 24 Oct 2024 05:36:58 -0700 Subject: [PATCH 3/8] added tests for adding and removing data in proxySet and proxyMap to make sure it updates with useSnapshot --- tests/proxyMap.test.tsx | 38 ++++++++++++++++++++++++++++++++++++++ tests/proxySet.test.tsx | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/tests/proxyMap.test.tsx b/tests/proxyMap.test.tsx index b0aa959b..84e18815 100644 --- a/tests/proxyMap.test.tsx +++ b/tests/proxyMap.test.tsx @@ -319,6 +319,7 @@ describe('proxyMap internal', () => { ).toBe(false) }) }) + describe('snapshot', () => { it('should error when trying to mutate a snapshot', () => { const state = proxyMap() @@ -375,3 +376,40 @@ describe('snapshot', () => { expect(snap2.get('key1')).toBe('val1modified') }) }) + +describe('ui updates - useSnapshot', async () => { + it('should update ui when calling has before and after deleting a key', async () => { + const state = proxyMap() + const TestComponent = () => { + const snap = useSnapshot(state) + + return ( + <> +

has key1: {`${snap.has('key')}`}

+ + + + ) + } + + const { getByText } = render( + + + , + ) + + await waitFor(() => { + getByText('has key1: false') + }) + + fireEvent.click(getByText('set key')) + await waitFor(() => { + getByText('has key: true') + }) + + fireEvent.click(getByText('delete key')) + await waitFor(() => { + getByText('has key: false') + }) + }) +}) diff --git a/tests/proxySet.test.tsx b/tests/proxySet.test.tsx index 8ff894a9..8876fac7 100644 --- a/tests/proxySet.test.tsx +++ b/tests/proxySet.test.tsx @@ -365,3 +365,40 @@ describe('snapshot behavior', () => { expect(snap2.has('val2')).toBe(true) }) }) + +describe('ui updates - useSnapshot', async () => { + it('should update ui when calling has before and after deleting a value', async () => { + const state = proxySet() + const TestComponent = () => { + const snap = useSnapshot(state) + + return ( + <> +

has value: {`${snap.has('value')}`}

+ + + + ) + } + + const { getByText } = render( + + + , + ) + + await waitFor(() => { + getByText('has value: false') + }) + + fireEvent.click(getByText('add value')) + await waitFor(() => { + getByText('has value: true') + }) + + fireEvent.click(getByText('delete value')) + await waitFor(() => { + getByText('has value: false') + }) + }) +}) From 660866e7198a2095bd63fecd737c72aff6980379 Mon Sep 17 00:00:00 2001 From: Michael Sweeney Date: Thu, 24 Oct 2024 05:40:46 -0700 Subject: [PATCH 4/8] fixed typo in test --- tests/proxyMap.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/proxyMap.test.tsx b/tests/proxyMap.test.tsx index 84e18815..2b3531c5 100644 --- a/tests/proxyMap.test.tsx +++ b/tests/proxyMap.test.tsx @@ -385,7 +385,7 @@ describe('ui updates - useSnapshot', async () => { return ( <> -

has key1: {`${snap.has('key')}`}

+

has key: {`${snap.has('key')}`}

@@ -399,7 +399,7 @@ describe('ui updates - useSnapshot', async () => { ) await waitFor(() => { - getByText('has key1: false') + getByText('has key: false') }) fireEvent.click(getByText('set key')) From f1a4d82bfaea16cfec5805e63e0671cd3040e46d Mon Sep 17 00:00:00 2001 From: Michael Sweeney Date: Thu, 24 Oct 2024 16:33:38 -0700 Subject: [PATCH 5/8] added epoch to proxySet and proxyMap to keep track of versioning --- src/vanilla/utils/proxyMap.ts | 11 ++++-- src/vanilla/utils/proxySet.ts | 9 ++++- tests/proxyMap.test.tsx | 55 +++++++++++++++++++++++++++++- tests/proxySet.test.tsx | 64 +++++++++++++++++++++++++++++++++-- 4 files changed, 133 insertions(+), 6 deletions(-) diff --git a/src/vanilla/utils/proxyMap.ts b/src/vanilla/utils/proxyMap.ts index bd1ee5d2..6d49d523 100644 --- a/src/vanilla/utils/proxyMap.ts +++ b/src/vanilla/utils/proxyMap.ts @@ -6,6 +6,7 @@ const isProxy = (x: any) => proxyStateMap.has(x) type InternalProxyObject = Map & { data: Array index: number + epoch: number toJSON: () => Map } @@ -68,6 +69,7 @@ export function proxyMap(entries?: Iterable<[K, V]> | undefined | null) { const vObject: InternalProxyObject = { data: initialData, index: initialIndex, + epoch: 0, get size() { if (!isProxy(this)) { registerSnapMap() @@ -80,15 +82,16 @@ export function proxyMap(entries?: Iterable<[K, V]> | undefined | null) { const index = map.get(key) if (index === undefined) { // eslint-disable-next-line @typescript-eslint/no-unused-expressions - this.index // touch property for tracking + this.epoch // touch property for tracking return undefined } return this.data[index] }, has(key: K) { const map = getMapForThis(this) + const exists = map.has(key) // eslint-disable-next-line @typescript-eslint/no-unused-expressions - this.data.length // touch property for tracking + this.epoch return map.has(key) }, set(key: K, value: V) { @@ -102,6 +105,7 @@ export function proxyMap(entries?: Iterable<[K, V]> | undefined | null) { } else { this.data[index] = value } + this.epoch++ return this }, delete(key: K) { @@ -114,6 +118,7 @@ export function proxyMap(entries?: Iterable<[K, V]> | undefined | null) { } delete this.data[index] indexMap.delete(key) + this.epoch++ return true }, clear() { @@ -122,6 +127,7 @@ export function proxyMap(entries?: Iterable<[K, V]> | undefined | null) { } this.data.length = 0 // empty array this.index = 0 + this.epoch++ indexMap.clear() }, forEach(cb: (value: V, key: K, map: Map) => void) { @@ -163,6 +169,7 @@ export function proxyMap(entries?: Iterable<[K, V]> | undefined | null) { Object.defineProperties(proxiedObject, { size: { enumerable: false }, index: { enumerable: false }, + epoch: { enumerable: false }, data: { enumerable: false }, toJSON: { enumerable: false }, }) diff --git a/src/vanilla/utils/proxySet.ts b/src/vanilla/utils/proxySet.ts index 88f6af09..f3535315 100644 --- a/src/vanilla/utils/proxySet.ts +++ b/src/vanilla/utils/proxySet.ts @@ -8,6 +8,7 @@ type InternalProxySet = Set & { data: T[] toJSON: object index: number + epoch: number intersection: (other: Set) => Set isDisjointFrom: (other: Set) => boolean isSubsetOf: (other: Set) => boolean @@ -63,6 +64,7 @@ export function proxySet(initialValues?: Iterable | null) { const vObject: InternalProxySet = { data: initialData, index: initialIndex, + epoch: 0, get size() { if (!isProxy(this)) { registerSnapMap() @@ -73,7 +75,7 @@ export function proxySet(initialValues?: Iterable | null) { const map = getMapForThis(this) const v = maybeProxify(value) // eslint-disable-next-line @typescript-eslint/no-unused-expressions - this.data.length // touch property for tracking + this.epoch // touch property for tracking return map.has(v) }, add(value: T) { @@ -84,6 +86,7 @@ export function proxySet(initialValues?: Iterable | null) { if (!indexMap.has(v)) { indexMap.set(v, this.index) this.data[this.index++] = v + this.epoch++ } return this }, @@ -98,6 +101,7 @@ export function proxySet(initialValues?: Iterable | null) { } delete this.data[index] indexMap.delete(v) + this.epoch++ return true }, clear() { @@ -106,6 +110,7 @@ export function proxySet(initialValues?: Iterable | null) { } this.data.length = 0 // empty array this.index = 0 + this.epoch++ indexMap.clear() }, forEach(cb) { @@ -197,6 +202,8 @@ export function proxySet(initialValues?: Iterable | null) { Object.defineProperties(proxiedObject, { size: { enumerable: false }, data: { enumerable: false }, + index: { enumerable: false }, + epoch: { enumerable: false }, toJSON: { enumerable: false }, }) Object.seal(proxiedObject) diff --git a/tests/proxyMap.test.tsx b/tests/proxyMap.test.tsx index 2b3531c5..32d75ef0 100644 --- a/tests/proxyMap.test.tsx +++ b/tests/proxyMap.test.tsx @@ -378,7 +378,7 @@ describe('snapshot', () => { }) describe('ui updates - useSnapshot', async () => { - it('should update ui when calling has before and after deleting a key', async () => { + it('should update ui when calling has before and after setting and deleting a key', async () => { const state = proxyMap() const TestComponent = () => { const snap = useSnapshot(state) @@ -412,4 +412,57 @@ describe('ui updates - useSnapshot', async () => { getByText('has key: false') }) }) + + it('should update ui when calling has before and after settiing and deleting multiple keys', async () => { + const state = proxyMap() + const TestComponent = () => { + const snap = useSnapshot(state) + + return ( + <> +

has key: {`${snap.has('key')}`}

+

has key2: {`${snap.has('key2')}`}

+ + + + ) + } + + const { getByText } = render( + + + , + ) + + await waitFor(() => { + getByText('has key: false') + getByText('has key2: false') + }) + + fireEvent.click(getByText('set keys')) + await waitFor(() => { + getByText('has key: true') + getByText('has key2: true') + }) + + fireEvent.click(getByText('delete keys')) + await waitFor(() => { + getByText('has key: false') + getByText('has key2: false') + }) + }) }) diff --git a/tests/proxySet.test.tsx b/tests/proxySet.test.tsx index 8876fac7..8ea3eee7 100644 --- a/tests/proxySet.test.tsx +++ b/tests/proxySet.test.tsx @@ -367,7 +367,7 @@ describe('snapshot behavior', () => { }) describe('ui updates - useSnapshot', async () => { - it('should update ui when calling has before and after deleting a value', async () => { + it('should update ui when calling has before and after setting anddeleting a value', async () => { const state = proxySet() const TestComponent = () => { const snap = useSnapshot(state) @@ -375,7 +375,14 @@ describe('ui updates - useSnapshot', async () => { return ( <>

has value: {`${snap.has('value')}`}

- + ) @@ -401,4 +408,57 @@ describe('ui updates - useSnapshot', async () => { getByText('has value: false') }) }) + + it('should update ui when calling has before and after settiing and deleting multiple values', async () => { + const state = proxySet() + const TestComponent = () => { + const snap = useSnapshot(state) + + return ( + <> +

has value: {`${snap.has('value')}`}

+

has value2: {`${snap.has('value2')}`}

+ + + + ) + } + + const { getByText } = render( + + + , + ) + + await waitFor(() => { + getByText('has value: false') + getByText('has value2: false') + }) + + fireEvent.click(getByText('add values')) + await waitFor(() => { + getByText('has value: true') + getByText('has value2: true') + }) + + fireEvent.click(getByText('delete values')) + await waitFor(() => { + getByText('has value: false') + getByText('has value2: false') + }) + }) }) From 77a982c9dc304cc25fd10bd326570c9182e60215 Mon Sep 17 00:00:00 2001 From: Michael Sweeney Date: Thu, 24 Oct 2024 16:39:21 -0700 Subject: [PATCH 6/8] added tests for using .clear on proxyMap and proxySet --- tests/proxyMap.test.tsx | 52 +++++++++++++++++++++++++++++++++++++++++ tests/proxySet.test.tsx | 52 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/tests/proxyMap.test.tsx b/tests/proxyMap.test.tsx index 32d75ef0..b11a7a72 100644 --- a/tests/proxyMap.test.tsx +++ b/tests/proxyMap.test.tsx @@ -465,4 +465,56 @@ describe('ui updates - useSnapshot', async () => { getByText('has key2: false') }) }) + + it('should update ui when clearing the map', async () => { + const state = proxyMap() + const TestComponent = () => { + const snap = useSnapshot(state) + + return ( + <> +

has key: {`${snap.has('key')}`}

+

has key2: {`${snap.has('key2')}`}

+ + + + ) + } + + const { getByText } = render( + + + , + ) + + await waitFor(() => { + getByText('has key: false') + getByText('has key2: false') + }) + + fireEvent.click(getByText('set keys')) + await waitFor(() => { + getByText('has key: true') + getByText('has key2: true') + }) + + fireEvent.click(getByText('clear map')) + await waitFor(() => { + getByText('has key: false') + getByText('has key2: false') + }) + }) }) diff --git a/tests/proxySet.test.tsx b/tests/proxySet.test.tsx index 8ea3eee7..a5c9b07e 100644 --- a/tests/proxySet.test.tsx +++ b/tests/proxySet.test.tsx @@ -461,4 +461,56 @@ describe('ui updates - useSnapshot', async () => { getByText('has value2: false') }) }) + + it('should update ui when clearing the set', async () => { + const state = proxySet() + const TestComponent = () => { + const snap = useSnapshot(state) + + return ( + <> +

has value: {`${snap.has('value')}`}

+

has value2: {`${snap.has('value2')}`}

+ + + + ) + } + + const { getByText } = render( + + + , + ) + + await waitFor(() => { + getByText('has value: false') + getByText('has value2: false') + }) + + fireEvent.click(getByText('add values')) + await waitFor(() => { + getByText('has value: true') + getByText('has value2: true') + }) + + fireEvent.click(getByText('clear set')) + await waitFor(() => { + getByText('has value: false') + getByText('has value2: false') + }) + }) }) From 07e883ca12ff1483e7a771fb556da018e414bd9d Mon Sep 17 00:00:00 2001 From: Michael Sweeney Date: Sat, 26 Oct 2024 00:36:50 -0700 Subject: [PATCH 7/8] added more tests for useSnapshot --- tests/proxyMap.test.tsx | 104 ++++++++++++++++++++++++++++++++++++++++ tests/proxySet.test.tsx | 104 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) diff --git a/tests/proxyMap.test.tsx b/tests/proxyMap.test.tsx index b11a7a72..44ccb453 100644 --- a/tests/proxyMap.test.tsx +++ b/tests/proxyMap.test.tsx @@ -466,6 +466,110 @@ describe('ui updates - useSnapshot', async () => { }) }) + it('should update ui when calling has before and after settiing multile keys and deleting a single one (first item)', async () => { + const state = proxyMap() + const TestComponent = () => { + const snap = useSnapshot(state) + + return ( + <> +

has key: {`${snap.has('key')}`}

+

has key2: {`${snap.has('key2')}`}

+ + + + ) + } + + const { getByText } = render( + + + , + ) + + await waitFor(() => { + getByText('has key: false') + getByText('has key2: false') + }) + + fireEvent.click(getByText('set keys')) + await waitFor(() => { + getByText('has key: true') + getByText('has key2: true') + }) + + fireEvent.click(getByText('delete keys')) + await waitFor(() => { + getByText('has key: false') + getByText('has key2: true') + }) + }) + + it('should update ui when calling has before and after settiing multile keys and deleting a single one (first item)', async () => { + const state = proxyMap() + const TestComponent = () => { + const snap = useSnapshot(state) + + return ( + <> +

has key: {`${snap.has('key')}`}

+

has key2: {`${snap.has('key2')}`}

+ + + + ) + } + + const { getByText } = render( + + + , + ) + + await waitFor(() => { + getByText('has key: false') + getByText('has key2: false') + }) + + fireEvent.click(getByText('set keys')) + await waitFor(() => { + getByText('has key: true') + getByText('has key2: true') + }) + + fireEvent.click(getByText('delete keys')) + await waitFor(() => { + getByText('has key: true') + getByText('has key2: false') + }) + }) + it('should update ui when clearing the map', async () => { const state = proxyMap() const TestComponent = () => { diff --git a/tests/proxySet.test.tsx b/tests/proxySet.test.tsx index a5c9b07e..f9b4cd53 100644 --- a/tests/proxySet.test.tsx +++ b/tests/proxySet.test.tsx @@ -462,6 +462,110 @@ describe('ui updates - useSnapshot', async () => { }) }) + it('should update ui when calling has before and after settiing multiple values and deleting a single one (first item)', async () => { + const state = proxySet() + const TestComponent = () => { + const snap = useSnapshot(state) + + return ( + <> +

has value: {`${snap.has('value')}`}

+

has value2: {`${snap.has('value2')}`}

+ + + + ) + } + + const { getByText } = render( + + + , + ) + + await waitFor(() => { + getByText('has value: false') + getByText('has value2: false') + }) + + fireEvent.click(getByText('add values')) + await waitFor(() => { + getByText('has value: true') + getByText('has value2: true') + }) + + fireEvent.click(getByText('delete values')) + await waitFor(() => { + getByText('has value: false') + getByText('has value2: true') + }) + }) + + it('should update ui when calling has before and after settiing multiple values and deleting a single one (second item)', async () => { + const state = proxySet() + const TestComponent = () => { + const snap = useSnapshot(state) + + return ( + <> +

has value: {`${snap.has('value')}`}

+

has value2: {`${snap.has('value2')}`}

+ + + + ) + } + + const { getByText } = render( + + + , + ) + + await waitFor(() => { + getByText('has value: false') + getByText('has value2: false') + }) + + fireEvent.click(getByText('add values')) + await waitFor(() => { + getByText('has value: true') + getByText('has value2: true') + }) + + fireEvent.click(getByText('delete values')) + await waitFor(() => { + getByText('has value: true') + getByText('has value2: false') + }) + }) + it('should update ui when clearing the set', async () => { const state = proxySet() const TestComponent = () => { From bab9c992a9e806963c7ae661ae498cae1419258b Mon Sep 17 00:00:00 2001 From: Michael Sweeney Date: Sat, 26 Oct 2024 00:39:24 -0700 Subject: [PATCH 8/8] added comment for epoch in proxyMap --- src/vanilla/utils/proxyMap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vanilla/utils/proxyMap.ts b/src/vanilla/utils/proxyMap.ts index 6d49d523..126c67dd 100644 --- a/src/vanilla/utils/proxyMap.ts +++ b/src/vanilla/utils/proxyMap.ts @@ -91,7 +91,7 @@ export function proxyMap(entries?: Iterable<[K, V]> | undefined | null) { const map = getMapForThis(this) const exists = map.has(key) // eslint-disable-next-line @typescript-eslint/no-unused-expressions - this.epoch + this.epoch // touch property for tracking return map.has(key) }, set(key: K, value: V) {