From f1a4d82bfaea16cfec5805e63e0671cd3040e46d Mon Sep 17 00:00:00 2001 From: Michael Sweeney Date: Thu, 24 Oct 2024 16:33:38 -0700 Subject: [PATCH] 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') + }) + }) })