1- import type { ReactiveOptions } from './types' ;
1+ /* eslint-disable @typescript-eslint/no-explicit-any */
2+ // Using a Proxy-based approach so that any new methods added to the Map
3+ // interface (like getOrInsert, getOrInsertComputed, etc.) are automatically
4+ // supported without needing to manually re-implement each one.
25
36import { consumeTag } from '../tracking' ;
47import { createUpdatableTag , DIRTY_TAG } from '../validators' ;
58
6- class TrackedMap < K = unknown , V = unknown > implements Map < K , V > {
7- #options: ReactiveOptions < V > ;
8- #collection = createUpdatableTag ( ) ;
9- #storages = new Map < K , ReturnType < typeof createUpdatableTag > > ( ) ;
10- #vals: Map < K , V > ;
9+ type Tag = ReturnType < typeof createUpdatableTag > ;
1110
12- #storageFor( key : K ) : ReturnType < typeof createUpdatableTag > {
13- const storages = this . #storages;
11+ export function trackedMap < Key = any , Value = any > (
12+ data ?:
13+ | Map < Key , Value >
14+ | Iterable < readonly [ Key , Value ] >
15+ | readonly ( readonly [ Key , Value ] ) [ ]
16+ | null ,
17+ options ?: { equals ?: ( a : Value , b : Value ) => boolean ; description ?: string }
18+ ) : Map < Key , Value > {
19+ const equals = options ?. equals ?? Object . is ;
20+ // TypeScript doesn't correctly resolve the overloads for calling the `Map`
21+ // constructor for the no-value constructor. This resolves that.
22+ const target : Map < Key , Value > =
23+ data instanceof Map ? new Map ( data . entries ( ) ) : new Map ( data ?? [ ] ) ;
24+ const collection = createUpdatableTag ( ) ;
25+ const storages = new Map < Key , Tag > ( ) ;
26+
27+ function storageFor ( key : Key ) : Tag {
1428 let storage = storages . get ( key ) ;
1529
1630 if ( storage === undefined ) {
@@ -20,181 +34,98 @@ class TrackedMap<K = unknown, V = unknown> implements Map<K, V> {
2034
2135 return storage ;
2236 }
23- #dirtyStorageFor( key : K ) : void {
24- const storage = this . #storages. get ( key ) ;
37+
38+ function dirtyStorageFor ( key : Key ) : void {
39+ const storage = storages . get ( key ) ;
2540
2641 if ( storage ) {
2742 DIRTY_TAG ( storage ) ;
2843 }
2944 }
3045
31- constructor (
32- existing : readonly ( readonly [ K , V ] ) [ ] | Iterable < readonly [ K , V ] > | null | Map < K , V > ,
33- options : ReactiveOptions < V >
34- ) {
35- // TypeScript doesn't correctly resolve the overloads for calling the `Map`
36- // constructor for the no-value constructor. This resolves that.
37- this . #vals = existing instanceof Map ? new Map ( existing . entries ( ) ) : new Map ( existing ) ;
38- this . #options = options ;
39- }
40-
41- get ( key : K ) : V | undefined {
42- consumeTag ( this . #storageFor( key ) ) ;
43-
44- return this . #vals. get ( key ) ;
45- }
46-
47- has ( key : K ) : boolean {
48- consumeTag ( this . #storageFor( key ) ) ;
46+ const proxy : Map < Key , Value > = new Proxy ( target , {
47+ get ( target , prop , receiver ) {
48+ if ( prop === 'set' ) {
49+ return function ( key : Key , value : Value ) : Map < Key , Value > {
50+ const hasExisting = target . has ( key ) ;
4951
50- return this . #vals. has ( key ) ;
51- }
52-
53- // **** ALL GETTERS ****
54- entries ( ) : MapIterator < [ K , V ] > {
55- return this [ Symbol . iterator ] ( ) ;
56- }
52+ if ( hasExisting ) {
53+ const isUnchanged = equals ( target . get ( key ) as Value , value ) ;
5754
58- getOrInsert ( key : K , defaultValue : V ) : V {
59- consumeTag ( this . #storageFor ( key ) ) ;
55+ if ( isUnchanged ) return proxy ;
56+ }
6057
61- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
62- // @ts -ignore -- older versions of TS don't yet have this method
63- // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
64- return this . #vals. getOrInsert ( key , defaultValue ) ;
65- }
66-
67- getOrInsertComputed ( key : K , creator : ( key : K ) => V ) : V {
68- consumeTag ( this . #storageFor( key ) ) ;
69-
70- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
71- // @ts -ignore -- older versions of TS don't yet have this method
72- // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
73- return this . #vals. getOrInsertComputed ( key , creator ) ;
74- }
58+ dirtyStorageFor ( key ) ;
59+ DIRTY_TAG ( collection ) ;
7560
76- keys ( ) {
77- consumeTag ( this . #collection) ;
78-
79- return this . #vals. keys ( ) ;
80- }
81-
82- values ( ) {
83- let iterator = this [ Symbol . iterator ] ( ) ;
84-
85- return {
86- next ( ) {
87- let { value, done } = iterator . next ( ) ;
88- return { value : done ? ( undefined as V ) : value ?. [ 1 ] , done } ;
89- } ,
90- [ Symbol . iterator ] ( ) {
91- return this ;
92- } ,
93- } as MapIterator < V > ;
94- }
95-
96- forEach ( fn : ( value : V , key : K , map : Map < K , V > ) => void ) : void {
97- for ( let [ key , value ] of this ) {
98- fn ( value , key , this ) ;
99- }
100- }
61+ target . set ( key , value ) ;
10162
102- get size ( ) : number {
103- consumeTag ( this . #collection) ;
63+ return proxy ;
64+ } ;
65+ }
10466
105- return this . #vals. size ;
106- }
67+ if ( prop === 'delete' ) {
68+ return function ( key : Key ) : boolean {
69+ if ( ! target . has ( key ) ) return false ;
10770
108- /**
109- * When iterating:
110- * - we entangle with the collection (as we iterate over the whole thing)
111- * via keys() → consumeTag(#collection)
112- * - for each individual item, we entangle with the item as well
113- * via get() → consumeTag(#storageFor(key))
114- */
115- [ Symbol . iterator ] ( ) {
116- let keys = this . keys ( ) ;
117- // eslint-disable-next-line @typescript-eslint/no-this-alias
118- let self = this ;
119-
120- return {
121- next ( ) {
122- let next = keys . next ( ) ;
123- let currentKey = next . value ;
124-
125- if ( next . done ) {
126- return { value : [ undefined , undefined ] , done : true } ;
127- }
128-
129- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
130- return { value : [ currentKey , self . get ( currentKey ! ) ] , done : false } ;
131- } ,
132- [ Symbol . iterator ] ( ) {
133- return this ;
134- } ,
135- } as MapIterator < [ K , V ] > ;
136- }
71+ dirtyStorageFor ( key ) ;
72+ DIRTY_TAG ( collection ) ;
13773
138- get [ Symbol . toStringTag ] ( ) : string {
139- return this . #vals[ Symbol . toStringTag ] ;
140- }
74+ storages . delete ( key ) ;
75+ return target . delete ( key ) ;
76+ } ;
77+ }
14178
142- set ( key : K , value : V ) : this {
143- let hasExisting = this . #vals. has ( key ) ;
79+ if ( prop === 'clear' ) {
80+ return function ( ) : void {
81+ if ( target . size === 0 ) return ;
14482
145- if ( hasExisting ) {
146- let isUnchanged = this . #options . equals ( this . #vals . get ( key ) as V , value ) ;
83+ storages . forEach ( ( s ) => DIRTY_TAG ( s ) ) ;
84+ storages . clear ( ) ;
14785
148- if ( isUnchanged ) {
149- return this ;
86+ DIRTY_TAG ( collection ) ;
87+ target . clear ( ) ;
88+ } ;
15089 }
151- }
15290
153- this . #dirtyStorageFor( key ) ;
91+ if ( prop === 'get' ) {
92+ return function ( key : Key ) : Value | undefined {
93+ consumeTag ( storageFor ( key ) ) ;
15494
155- if ( ! hasExisting ) {
156- DIRTY_TAG ( this . #collection) ;
157- }
158-
159- this . #vals. set ( key , value ) ;
160-
161- return this ;
162- }
95+ return target . get ( key ) ;
96+ } ;
97+ }
16398
164- delete ( key : K ) : boolean {
165- if ( ! this . #vals. has ( key ) ) return false ;
99+ if ( prop === 'has' ) {
100+ return function ( key : Key ) : boolean {
101+ consumeTag ( storageFor ( key ) ) ;
166102
167- this . #dirtyStorageFor( key ) ;
168- DIRTY_TAG ( this . #collection) ;
103+ return target . has ( key ) ;
104+ } ;
105+ }
169106
170- this . #storages. delete ( key ) ;
171- return this . #vals. delete ( key ) ;
172- }
107+ if ( prop === 'size' ) {
108+ consumeTag ( collection ) ;
173109
174- clear ( ) : void {
175- if ( this . #vals . size === 0 ) return ;
110+ return target . size ;
111+ }
176112
177- this . #storages . forEach ( ( s ) => DIRTY_TAG ( s ) ) ;
178- this . #storages . clear ( ) ;
113+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
114+ const value = Reflect . get ( target , prop , receiver ) ;
179115
180- DIRTY_TAG ( this . #collection) ;
181- this . #vals. clear ( ) ;
182- }
183- }
116+ if ( typeof value === 'function' ) {
117+ return function ( this : any , ...args : any [ ] ) {
118+ consumeTag ( collection ) ;
184119
185- // So instanceof works
186- Object . setPrototypeOf ( TrackedMap . prototype , Map . prototype ) ;
120+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
121+ return value . apply ( target , args ) ;
122+ } ;
123+ }
187124
188- export function trackedMap < Key = unknown , Value = unknown > (
189- data ?:
190- | Map < Key , Value >
191- | Iterable < readonly [ Key , Value ] >
192- | readonly ( readonly [ Key , Value ] ) [ ]
193- | null ,
194- options ?: { equals ?: ( a : Value , b : Value ) => boolean ; description ?: string }
195- ) : Map < Key , Value > {
196- return new TrackedMap ( data ?? [ ] , {
197- equals : options ?. equals ?? Object . is ,
198- description : options ?. description ,
125+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
126+ return value ;
127+ } ,
199128 } ) ;
129+
130+ return proxy ;
200131}
0 commit comments