From f8783f9472c6fe964e410b99bc6b528f2c824e27 Mon Sep 17 00:00:00 2001 From: Kim Juho Date: Wed, 5 Nov 2025 22:56:42 +0900 Subject: [PATCH 1/7] fix(reactivity): add tests for set composition methods --- .../__tests__/collections/Set.spec.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/packages/reactivity/__tests__/collections/Set.spec.ts b/packages/reactivity/__tests__/collections/Set.spec.ts index 2aeef198256..88229705c49 100644 --- a/packages/reactivity/__tests__/collections/Set.spec.ts +++ b/packages/reactivity/__tests__/collections/Set.spec.ts @@ -459,5 +459,107 @@ describe('reactivity/collections', () => { const result = set.add('a') expect(result).toBe(set) }) + + it('should suppoort Set.difference call', () => { + const setA = reactive(new Set([1, 2, 3, 4])) + const setB = new Set([3, 4, 5, 6]) + + if ('difference' in Set.prototype) { + // @ts-expect-error + const result = setA.difference(setB) + + expect(result).toBeInstanceOf(Set) + expect(result.size).toBe(2) + expect(result.has(1)).toBe(true) + expect(result.has(2)).toBe(true) + } + }) + + it('should suppoort Set.intersection call', () => { + const setA = reactive(new Set([1, 2, 3, 4])) + const setB = new Set([3, 4, 5, 6]) + + if ('intersection' in Set.prototype) { + // @ts-expect-error + const result = setA.intersection(setB) + + expect(result).toBeInstanceOf(Set) + expect(result.size).toBe(2) + expect(result.has(3)).toBe(true) + expect(result.has(4)).toBe(true) + } + }) + + it('should suppoort Set.symmetricDifference call', () => { + const setA = reactive(new Set([1, 2, 3, 4])) + const setB = new Set([3, 4, 5, 6]) + + if ('symmetricDifference' in Set.prototype) { + // @ts-expect-error + const result = setA.symmetricDifference(setB) + + expect(result).toBeInstanceOf(Set) + expect(result.size).toBe(4) + expect(result.has(1)).toBe(true) + expect(result.has(2)).toBe(true) + expect(result.has(5)).toBe(true) + expect(result.has(6)).toBe(true) + } + }) + + it('should suppoort Set.union call', () => { + const setA = reactive(new Set([1, 2, 3, 4])) + const setB = new Set([3, 4, 5, 6]) + + if ('union' in Set.prototype) { + // @ts-expect-error + const result = setA.union(setB) + + expect(result).toBeInstanceOf(Set) + expect(result.size).toBe(6) + expect(result.has(1)).toBe(true) + expect(result.has(2)).toBe(true) + expect(result.has(3)).toBe(true) + expect(result.has(4)).toBe(true) + expect(result.has(5)).toBe(true) + expect(result.has(6)).toBe(true) + } + }) + + it('should suppoort Set.isDisjointFrom call', () => { + const setA = reactive(new Set([1, 2, 3, 4])) + const setB = new Set([3, 4, 5, 6]) + + if ('isDisjointFrom' in Set.prototype) { + // @ts-expect-error + const result = setA.isDisjointFrom(setB) + + expect(result).toBe(false) + } + }) + + it('should suppoort Set.isSubsetOf call', () => { + const setA = reactive(new Set([1, 2, 3, 4])) + const setB = new Set([3, 4, 5, 6]) + + if ('isSubsetOf' in Set.prototype) { + // @ts-expect-error + const result = setA.isSubsetOf(setB) + + expect(result).toBe(false) + } + }) + + it('should suppoort Set.isSupersetOf call', () => { + const setA = reactive(new Set([1, 2, 3, 4])) + const setB = new Set([3, 4, 5, 6]) + + if ('isSupersetOf' in Set.prototype) { + // @ts-expect-error + const result = setA.isSupersetOf(setB) + + expect(result).toBe(false) + } + }) }) }) From 79537670886000e0e5e548b2f280c4bdbb6506b1 Mon Sep 17 00:00:00 2001 From: Kim Juho Date: Wed, 5 Nov 2025 23:01:29 +0900 Subject: [PATCH 2/7] fix(reactivity): extends instrumentations to support set composition methods --- packages/reactivity/src/collectionHandlers.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index ffc3289f2ed..af9e13c9689 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -24,6 +24,8 @@ type IterableCollections = (Map | Set) & Target type WeakCollections = (WeakMap | WeakSet) & Target type MapTypes = (Map | WeakMap) & Target type SetTypes = (Set | WeakSet) & Target +type SetType = Set & Target +type SetLikeType = Pick, 'size' | 'has' | 'keys'> const toShallow = (value: T): T => value @@ -246,6 +248,39 @@ function createInstrumentations( }, ) + extend(instrumentations, { + difference(this: SetType, other: SetLikeType): SetType { + // @ts-expect-error + return toRaw(this).difference(other) + }, + intersection(this: SetType, other: SetLikeType): SetType { + // @ts-expect-error + return toRaw(this).intersection(other) + }, + symmetricDifference(this: SetType, other: SetLikeType): SetType { + // @ts-expect-error + return toRaw(this).symmetricDifference(other) + }, + union(this: SetType, other: SetLikeType): SetType { + // @ts-expect-error + return toRaw(this).union(other) + }, + + // Set composition methods return boolean + isDisjointFrom(this: SetType, other: SetLikeType): boolean { + // @ts-expect-error + return toRaw(this).isDisjointFrom(other) + }, + isSubsetOf(this: SetType, other: SetLikeType): boolean { + // @ts-expect-error + return toRaw(this).isSubsetOf(other) + }, + isSupersetOf(this: SetType, other: SetLikeType): boolean { + // @ts-expect-error + return toRaw(this).isSupersetOf(other) + }, + }) + const iteratorMethods = [ 'keys', 'values', From ad292c71e0348a7790b3b079adb2243d2651ee1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A3=BC=ED=98=B8?= Date: Wed, 5 Nov 2025 23:29:42 +0900 Subject: [PATCH 3/7] fix(reactivity): add track Set changes Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/reactivity/src/collectionHandlers.ts | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index af9e13c9689..8170c57c5fd 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -250,34 +250,62 @@ function createInstrumentations( extend(instrumentations, { difference(this: SetType, other: SetLikeType): SetType { + const raw = toRaw(this) + if (!readonly) { + track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) + } // @ts-expect-error - return toRaw(this).difference(other) + return raw.difference(other) }, intersection(this: SetType, other: SetLikeType): SetType { + const raw = toRaw(this) + if (!readonly) { + track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) + } // @ts-expect-error - return toRaw(this).intersection(other) + return raw.intersection(other) }, symmetricDifference(this: SetType, other: SetLikeType): SetType { + const raw = toRaw(this) + if (!readonly) { + track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) + } // @ts-expect-error - return toRaw(this).symmetricDifference(other) + return raw.symmetricDifference(other) }, union(this: SetType, other: SetLikeType): SetType { + const raw = toRaw(this) + if (!readonly) { + track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) + } // @ts-expect-error - return toRaw(this).union(other) + return raw.union(other) }, // Set composition methods return boolean isDisjointFrom(this: SetType, other: SetLikeType): boolean { + const raw = toRaw(this) + if (!readonly) { + track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) + } // @ts-expect-error - return toRaw(this).isDisjointFrom(other) + return raw.isDisjointFrom(other) }, isSubsetOf(this: SetType, other: SetLikeType): boolean { + const raw = toRaw(this) + if (!readonly) { + track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) + } // @ts-expect-error - return toRaw(this).isSubsetOf(other) + return raw.isSubsetOf(other) }, isSupersetOf(this: SetType, other: SetLikeType): boolean { + const raw = toRaw(this) + if (!readonly) { + track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) + } // @ts-expect-error - return toRaw(this).isSupersetOf(other) + return raw.isSupersetOf(other) }, }) From 2a0581e083dd66e5e6effcf3adec2782f5458f92 Mon Sep 17 00:00:00 2001 From: Kim Juho Date: Wed, 5 Nov 2025 23:50:28 +0900 Subject: [PATCH 4/7] fix(reactivity): follow conventions on size property code --- packages/reactivity/src/collectionHandlers.ts | 64 ++++++------------- 1 file changed, 21 insertions(+), 43 deletions(-) diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 8170c57c5fd..94f93389d52 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -250,62 +250,40 @@ function createInstrumentations( extend(instrumentations, { difference(this: SetType, other: SetLikeType): SetType { - const raw = toRaw(this) - if (!readonly) { - track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) - } - // @ts-expect-error - return raw.difference(other) + const target = this[ReactiveFlags.RAW] + !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) + return target.difference(other) }, intersection(this: SetType, other: SetLikeType): SetType { - const raw = toRaw(this) - if (!readonly) { - track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) - } - // @ts-expect-error - return raw.intersection(other) + const target = this[ReactiveFlags.RAW] + !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) + return target.intersection(other) }, symmetricDifference(this: SetType, other: SetLikeType): SetType { - const raw = toRaw(this) - if (!readonly) { - track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) - } - // @ts-expect-error - return raw.symmetricDifference(other) + const target = this[ReactiveFlags.RAW] + !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) + return target.symmetricDifference(other) }, union(this: SetType, other: SetLikeType): SetType { - const raw = toRaw(this) - if (!readonly) { - track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) - } - // @ts-expect-error - return raw.union(other) + const target = this[ReactiveFlags.RAW] + !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) + return target.union(other) }, - // Set composition methods return boolean isDisjointFrom(this: SetType, other: SetLikeType): boolean { - const raw = toRaw(this) - if (!readonly) { - track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) - } - // @ts-expect-error - return raw.isDisjointFrom(other) + const target = this[ReactiveFlags.RAW] + !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) + return target.isDisjointFrom(other) }, isSubsetOf(this: SetType, other: SetLikeType): boolean { - const raw = toRaw(this) - if (!readonly) { - track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) - } - // @ts-expect-error - return raw.isSubsetOf(other) + const target = this[ReactiveFlags.RAW] + !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) + return target.isSubsetOf(other) }, isSupersetOf(this: SetType, other: SetLikeType): boolean { - const raw = toRaw(this) - if (!readonly) { - track(raw, TrackOpTypes.ITERATE, ITERATE_KEY) - } - // @ts-expect-error - return raw.isSupersetOf(other) + const target = this[ReactiveFlags.RAW] + !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) + return target.isSupersetOf(other) }, }) From d15e10d06334d5974b237d911f4d50f07617d1d5 Mon Sep 17 00:00:00 2001 From: Kim Juho Date: Thu, 6 Nov 2025 00:57:28 +0900 Subject: [PATCH 5/7] fix(reactivity): add test for tracking this set or other setlike --- .../__tests__/collections/Set.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/reactivity/__tests__/collections/Set.spec.ts b/packages/reactivity/__tests__/collections/Set.spec.ts index 88229705c49..dc5764030a4 100644 --- a/packages/reactivity/__tests__/collections/Set.spec.ts +++ b/packages/reactivity/__tests__/collections/Set.spec.ts @@ -561,5 +561,35 @@ describe('reactivity/collections', () => { expect(result).toBe(false) } }) + + it('should observe this set or other setlike when calling composition method', () => { + let dummy: any + const e1 = { val: 1 } + const e2 = { val: 2 } + const e3 = { val: 3 } + const e4 = { val: 4 } + + const setA = reactive(new Set([e1, e2])) + const setB = reactive(new Set([e3, e4])) + + effect(() => { + // @ts-expect-error + dummy = setA.intersection(setB) + }) + + expect(dummy.size).toBe(0) + + setB.add(e1) + expect(dummy.size).toBe(1) + + setB.add(e2) + expect(dummy.size).toBe(2) + + setA.delete(e1) + expect(dummy.size).toBe(1) + + setA.clear() + expect(dummy.size).toBe(0) + }) }) }) From 07198fac6b407fa5a582d569077645d8d36e772e Mon Sep 17 00:00:00 2001 From: Kim Juho Date: Thu, 6 Nov 2025 00:59:39 +0900 Subject: [PATCH 6/7] fix(reactivity): refactor codes (reference #11399) --- packages/reactivity/src/collectionHandlers.ts | 57 +++++++------------ 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 94f93389d52..40fbeb9ea96 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -93,6 +93,18 @@ function createReadonlyMethod(type: TriggerOpTypes): Function { } } +function createSetCompositionMethod(method: string, readonly: boolean) { + return function (this: SetType, other: SetLikeType) { + const target = this[ReactiveFlags.RAW] + const rawTarget = toRaw(target) + + if (!readonly) { + track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY) + } + return rawTarget[method](other) + } +} + type Instrumentations = Record function createInstrumentations( @@ -249,42 +261,17 @@ function createInstrumentations( ) extend(instrumentations, { - difference(this: SetType, other: SetLikeType): SetType { - const target = this[ReactiveFlags.RAW] - !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) - return target.difference(other) - }, - intersection(this: SetType, other: SetLikeType): SetType { - const target = this[ReactiveFlags.RAW] - !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) - return target.intersection(other) - }, - symmetricDifference(this: SetType, other: SetLikeType): SetType { - const target = this[ReactiveFlags.RAW] - !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) - return target.symmetricDifference(other) - }, - union(this: SetType, other: SetLikeType): SetType { - const target = this[ReactiveFlags.RAW] - !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) - return target.union(other) - }, + difference: createSetCompositionMethod('difference', readonly), + intersection: createSetCompositionMethod('intersection', readonly), + symmetricDifference: createSetCompositionMethod( + 'symmetricDifference', + readonly, + ), + union: createSetCompositionMethod('union', readonly), - isDisjointFrom(this: SetType, other: SetLikeType): boolean { - const target = this[ReactiveFlags.RAW] - !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) - return target.isDisjointFrom(other) - }, - isSubsetOf(this: SetType, other: SetLikeType): boolean { - const target = this[ReactiveFlags.RAW] - !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) - return target.isSubsetOf(other) - }, - isSupersetOf(this: SetType, other: SetLikeType): boolean { - const target = this[ReactiveFlags.RAW] - !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) - return target.isSupersetOf(other) - }, + isDisjointFrom: createSetCompositionMethod('isDisjointFrom', readonly), + isSubsetOf: createSetCompositionMethod('isSubsetOf', readonly), + isSupersetOf: createSetCompositionMethod('isSupersetOf', readonly), }) const iteratorMethods = [ From 70bdf363b8500c5a478736c7c01be601d6b6c9d1 Mon Sep 17 00:00:00 2001 From: Kim Juho Date: Thu, 6 Nov 2025 01:10:35 +0900 Subject: [PATCH 7/7] fix(reactivity): check composition method is supported in test code --- .../__tests__/collections/Set.spec.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/reactivity/__tests__/collections/Set.spec.ts b/packages/reactivity/__tests__/collections/Set.spec.ts index dc5764030a4..e4333e3db38 100644 --- a/packages/reactivity/__tests__/collections/Set.spec.ts +++ b/packages/reactivity/__tests__/collections/Set.spec.ts @@ -572,24 +572,26 @@ describe('reactivity/collections', () => { const setA = reactive(new Set([e1, e2])) const setB = reactive(new Set([e3, e4])) - effect(() => { - // @ts-expect-error - dummy = setA.intersection(setB) - }) + if ('intersection' in Set.prototype) { + effect(() => { + // @ts-expect-error + dummy = setA.intersection(setB) + }) - expect(dummy.size).toBe(0) + expect(dummy.size).toBe(0) - setB.add(e1) - expect(dummy.size).toBe(1) + setB.add(e1) + expect(dummy.size).toBe(1) - setB.add(e2) - expect(dummy.size).toBe(2) + setB.add(e2) + expect(dummy.size).toBe(2) - setA.delete(e1) - expect(dummy.size).toBe(1) + setA.delete(e1) + expect(dummy.size).toBe(1) - setA.clear() - expect(dummy.size).toBe(0) + setA.clear() + expect(dummy.size).toBe(0) + } }) }) })