mutts 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -6
- package/dist/chunks/_tslib-BgjropY9.js +81 -0
- package/dist/chunks/_tslib-BgjropY9.js.map +1 -0
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js +75 -0
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +1 -0
- package/dist/chunks/{decorator-8qjFb7dw.js → decorator-DLvrD0UF.js} +103 -14
- package/dist/chunks/decorator-DLvrD0UF.js.map +1 -0
- package/dist/chunks/{decorator-AbRkXM5O.esm.js → decorator-DqiszP7i.esm.js} +100 -15
- package/dist/chunks/decorator-DqiszP7i.esm.js.map +1 -0
- package/dist/chunks/index-DzUDtFc7.esm.js +4841 -0
- package/dist/chunks/index-DzUDtFc7.esm.js.map +1 -0
- package/dist/chunks/index-HNVqPzjz.js +4891 -0
- package/dist/chunks/index-HNVqPzjz.js.map +1 -0
- package/dist/decorator.d.ts +57 -0
- package/dist/decorator.esm.js +1 -1
- package/dist/decorator.js +1 -1
- package/dist/destroyable.d.ts +43 -1
- package/dist/destroyable.esm.js +19 -1
- package/dist/destroyable.esm.js.map +1 -1
- package/dist/destroyable.js +19 -1
- package/dist/destroyable.js.map +1 -1
- package/dist/devtools/devtools.html +9 -0
- package/dist/devtools/devtools.js +5 -0
- package/dist/devtools/devtools.js.map +1 -0
- package/dist/devtools/manifest.json +8 -0
- package/dist/devtools/panel.css +72 -0
- package/dist/devtools/panel.html +31 -0
- package/dist/devtools/panel.js +13048 -0
- package/dist/devtools/panel.js.map +1 -0
- package/dist/eventful.d.ts +10 -1
- package/dist/eventful.esm.js +5 -27
- package/dist/eventful.esm.js.map +1 -1
- package/dist/eventful.js +15 -37
- package/dist/eventful.js.map +1 -1
- package/dist/index.d.ts +18 -14
- package/dist/index.esm.js +4 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +44 -5
- package/dist/index.js.map +1 -1
- package/dist/indexable.d.ts +213 -1
- package/dist/indexable.esm.js +203 -3
- package/dist/indexable.esm.js.map +1 -1
- package/dist/indexable.js +204 -2
- package/dist/indexable.js.map +1 -1
- package/dist/mutts.umd.js +1 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/promiseChain.d.ts +10 -0
- package/dist/promiseChain.esm.js +6 -0
- package/dist/promiseChain.esm.js.map +1 -1
- package/dist/promiseChain.js +6 -0
- package/dist/promiseChain.js.map +1 -1
- package/dist/reactive.d.ts +774 -33
- package/dist/reactive.esm.js +4 -1458
- package/dist/reactive.esm.js.map +1 -1
- package/dist/reactive.js +53 -1474
- package/dist/reactive.js.map +1 -1
- package/dist/std-decorators.d.ts +35 -0
- package/dist/std-decorators.esm.js +36 -1
- package/dist/std-decorators.esm.js.map +1 -1
- package/dist/std-decorators.js +36 -1
- package/dist/std-decorators.js.map +1 -1
- package/docs/ai/api-reference.md +133 -0
- package/docs/ai/manual.md +105 -0
- package/docs/iterableWeak.md +646 -0
- package/docs/mixin.md +229 -0
- package/docs/reactive/advanced.md +1280 -0
- package/docs/reactive/collections.md +767 -0
- package/docs/reactive/core.md +973 -0
- package/docs/reactive.md +21 -2688
- package/package.json +18 -5
- package/src/decorator.ts +266 -0
- package/src/destroyable.ts +199 -0
- package/src/eventful.ts +77 -0
- package/src/index.d.ts +9 -0
- package/src/index.ts +9 -0
- package/src/indexable.ts +484 -0
- package/src/introspection.ts +59 -0
- package/src/iterableWeak.ts +233 -0
- package/src/mixins.ts +123 -0
- package/src/promiseChain.ts +110 -0
- package/src/reactive/array.ts +414 -0
- package/src/reactive/change.ts +134 -0
- package/src/reactive/debug.ts +517 -0
- package/src/reactive/deep-touch.ts +268 -0
- package/src/reactive/deep-watch-state.ts +82 -0
- package/src/reactive/deep-watch.ts +168 -0
- package/src/reactive/effect-context.ts +94 -0
- package/src/reactive/effects.ts +1333 -0
- package/src/reactive/index.ts +75 -0
- package/src/reactive/interface.ts +223 -0
- package/src/reactive/map.ts +171 -0
- package/src/reactive/mapped.ts +130 -0
- package/src/reactive/memoize.ts +107 -0
- package/src/reactive/non-reactive-state.ts +49 -0
- package/src/reactive/non-reactive.ts +43 -0
- package/src/reactive/project.project.md +93 -0
- package/src/reactive/project.ts +335 -0
- package/src/reactive/proxy-state.ts +27 -0
- package/src/reactive/proxy.ts +285 -0
- package/src/reactive/record.ts +196 -0
- package/src/reactive/register.ts +421 -0
- package/src/reactive/set.ts +144 -0
- package/src/reactive/tracking.ts +101 -0
- package/src/reactive/types.ts +358 -0
- package/src/reactive/zone.ts +208 -0
- package/src/std-decorators.ts +217 -0
- package/src/utils.ts +117 -0
- package/dist/chunks/decorator-8qjFb7dw.js.map +0 -1
- package/dist/chunks/decorator-AbRkXM5O.esm.js.map +0 -1
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { ArrayReadForward, forwardArray, getAt, Indexable, setAt } from '../indexable'
|
|
2
|
+
import { effect } from './effects'
|
|
3
|
+
import { unreactive } from './interface'
|
|
4
|
+
import { reactive } from './proxy'
|
|
5
|
+
import { type DependencyFunction, prototypeForwarding, type ScopedCallback } from './types'
|
|
6
|
+
|
|
7
|
+
// TODO: use register in a real-world crud situation, have "events" for add, delete, update
|
|
8
|
+
|
|
9
|
+
type KeyFunction<T, K extends PropertyKey> = (item: T) => K
|
|
10
|
+
|
|
11
|
+
// Helper to work around TypeScript limitation: base class expressions cannot reference class type parameters
|
|
12
|
+
function getRegisterBase<T>() {
|
|
13
|
+
class RegisterBase extends Indexable(ArrayReadForward, {
|
|
14
|
+
get(this: any, index: number) {
|
|
15
|
+
return this[getAt](index)
|
|
16
|
+
},
|
|
17
|
+
set(this: any, index: number, value: T) {
|
|
18
|
+
this[setAt](index, value)
|
|
19
|
+
},
|
|
20
|
+
getLength(this: any) {
|
|
21
|
+
return this.length
|
|
22
|
+
},
|
|
23
|
+
setLength(this: any, value: number) {
|
|
24
|
+
this.length = value
|
|
25
|
+
},
|
|
26
|
+
}) {
|
|
27
|
+
toArray(): T[] {
|
|
28
|
+
return Array.from(this)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return RegisterBase as new () => ArrayReadForward<T> & {
|
|
32
|
+
[x: number]: T
|
|
33
|
+
toArray(): T[]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
interface RegisterInstance<T> extends ArrayReadForward<T> {
|
|
37
|
+
[index: number]: T
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@unreactive
|
|
41
|
+
class RegisterClass<T, K extends PropertyKey = PropertyKey>
|
|
42
|
+
extends getRegisterBase<any>()
|
|
43
|
+
implements RegisterInstance<T>
|
|
44
|
+
{
|
|
45
|
+
protected get [forwardArray](): readonly T[] {
|
|
46
|
+
return this.toArray()
|
|
47
|
+
}
|
|
48
|
+
readonly #keyFn: KeyFunction<T, K>
|
|
49
|
+
readonly #keys: K[]
|
|
50
|
+
readonly #values: Map<K, T>
|
|
51
|
+
readonly #usage = new Map<K, number>()
|
|
52
|
+
readonly #valueInfo = new Map<T, { key: K; stop?: ScopedCallback }>()
|
|
53
|
+
readonly #keyEffects = new Set<ScopedCallback>()
|
|
54
|
+
readonly #ascend: DependencyFunction
|
|
55
|
+
|
|
56
|
+
constructor(keyFn: KeyFunction<T, K>, initial?: Iterable<T>) {
|
|
57
|
+
super()
|
|
58
|
+
/* Moved below initialization */
|
|
59
|
+
let ascendGet: DependencyFunction | undefined
|
|
60
|
+
effect(({ ascend }) => {
|
|
61
|
+
ascendGet = ascend
|
|
62
|
+
})
|
|
63
|
+
this.#ascend = ascendGet!
|
|
64
|
+
if (typeof keyFn !== 'function') throw new Error('Register requires a key function')
|
|
65
|
+
this.#keyFn = keyFn
|
|
66
|
+
this.#keys = reactive([] as K[])
|
|
67
|
+
this.#values = reactive(new Map<K, T>())
|
|
68
|
+
Object.defineProperties(this, {
|
|
69
|
+
[prototypeForwarding]: { value: this.#keys },
|
|
70
|
+
})
|
|
71
|
+
if (initial) this.push(...initial)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private ensureKey(value: T): K {
|
|
75
|
+
let info = this.#valueInfo.get(value)
|
|
76
|
+
if (info) return info.key
|
|
77
|
+
info = { key: undefined as unknown as K }
|
|
78
|
+
this.#valueInfo.set(value, info)
|
|
79
|
+
this.#ascend(() => {
|
|
80
|
+
const stop = effect(({ reaction }) => {
|
|
81
|
+
const nextKey = this.#keyFn(value)
|
|
82
|
+
this.assertValidKey(nextKey)
|
|
83
|
+
const previousKey = info!.key
|
|
84
|
+
if (reaction && previousKey !== undefined && !Object.is(nextKey, previousKey))
|
|
85
|
+
this.#rekeyValue(value, previousKey, nextKey)
|
|
86
|
+
info!.key = nextKey
|
|
87
|
+
})
|
|
88
|
+
info!.stop = stop
|
|
89
|
+
this.#keyEffects.add(stop)
|
|
90
|
+
})
|
|
91
|
+
if (info.key === undefined) throw new Error('Register key function must return a property key')
|
|
92
|
+
return info.key
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private assertValidKey(key: unknown): asserts key is K {
|
|
96
|
+
const type = typeof key
|
|
97
|
+
if (type !== 'string' && type !== 'number' && type !== 'symbol')
|
|
98
|
+
throw new Error('Register key function must return a property key')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private setKeyValue(key: K, value: T) {
|
|
102
|
+
const existing = this.#values.get(key)
|
|
103
|
+
if (existing !== undefined && existing !== value) this.cleanupValue(existing)
|
|
104
|
+
this.#values.set(key, value)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private cleanupValue(value: T) {
|
|
108
|
+
const info = this.#valueInfo.get(value)
|
|
109
|
+
if (!info) return
|
|
110
|
+
const stop = info.stop
|
|
111
|
+
if (stop) {
|
|
112
|
+
info.stop = undefined
|
|
113
|
+
this.#keyEffects.delete(stop)
|
|
114
|
+
stop()
|
|
115
|
+
}
|
|
116
|
+
this.#valueInfo.delete(value)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private disposeKeyEffects() {
|
|
120
|
+
for (const value of Array.from(this.#valueInfo.keys())) this.cleanupValue(value)
|
|
121
|
+
this.#keyEffects.clear()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#rekeyValue(value: T, oldKey: K, newKey: K) {
|
|
125
|
+
if (Object.is(oldKey, newKey)) return
|
|
126
|
+
const existingValue = this.#values.get(newKey)
|
|
127
|
+
if (existingValue !== undefined && existingValue !== value)
|
|
128
|
+
throw new Error(`Register key collision for key ${String(newKey)}`)
|
|
129
|
+
const count = this.#usage.get(oldKey)
|
|
130
|
+
if (!count) return
|
|
131
|
+
const existingCount = this.#usage.get(newKey) ?? 0
|
|
132
|
+
this.setKeyValue(newKey, value)
|
|
133
|
+
for (let i = 0; i < this.#keys.length; i++)
|
|
134
|
+
if (Object.is(this.#keys[i], oldKey)) this.#keys[i] = newKey
|
|
135
|
+
this.#usage.set(newKey, existingCount + count)
|
|
136
|
+
this.#usage.delete(oldKey)
|
|
137
|
+
this.#values.delete(oldKey)
|
|
138
|
+
const updatedInfo = this.#valueInfo.get(value)
|
|
139
|
+
if (updatedInfo) updatedInfo.key = newKey
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private incrementUsage(key: K) {
|
|
143
|
+
const count = this.#usage.get(key) ?? 0
|
|
144
|
+
this.#usage.set(key, count + 1)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private decrementUsage(key: K) {
|
|
148
|
+
const count = this.#usage.get(key)
|
|
149
|
+
if (!count) return
|
|
150
|
+
if (count <= 1) {
|
|
151
|
+
const value = this.#values.get(key)
|
|
152
|
+
this.#usage.delete(key)
|
|
153
|
+
this.#values.delete(key)
|
|
154
|
+
if (value !== undefined) this.cleanupValue(value)
|
|
155
|
+
} else {
|
|
156
|
+
this.#usage.set(key, count - 1)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private normalizeIndex(index: number, allowEnd = false): number {
|
|
161
|
+
const length = this.length
|
|
162
|
+
let resolved = index
|
|
163
|
+
if (resolved < 0) resolved = Math.max(length + resolved, 0)
|
|
164
|
+
if (resolved > length) {
|
|
165
|
+
if (allowEnd) resolved = length
|
|
166
|
+
else throw new RangeError('Index out of bounds')
|
|
167
|
+
}
|
|
168
|
+
if (!allowEnd && resolved === length) throw new RangeError('Index out of bounds')
|
|
169
|
+
return resolved
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private assignAt(index: number, key: K, value: T) {
|
|
173
|
+
const oldKey = this.#keys[index]
|
|
174
|
+
if (oldKey !== undefined && Object.is(oldKey, key)) {
|
|
175
|
+
this.setKeyValue(key, value)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
if (oldKey !== undefined) this.decrementUsage(oldKey as K)
|
|
179
|
+
this.#keys[index] = key
|
|
180
|
+
this.incrementUsage(key)
|
|
181
|
+
this.setKeyValue(key, value)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private insertKeyValue(index: number, key: K, value: T) {
|
|
185
|
+
this.#keys.splice(index, 0, key)
|
|
186
|
+
this.incrementUsage(key)
|
|
187
|
+
this.setKeyValue(key, value)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private rebuildFrom(values: T[]) {
|
|
191
|
+
this.disposeKeyEffects()
|
|
192
|
+
this.#keys.splice(0, this.#keys.length)
|
|
193
|
+
this.#usage.clear()
|
|
194
|
+
this.#values.clear()
|
|
195
|
+
for (const value of values) {
|
|
196
|
+
const key = this.ensureKey(value)
|
|
197
|
+
this.#keys.push(key)
|
|
198
|
+
this.incrementUsage(key)
|
|
199
|
+
this.#values.set(key, value)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
get length(): number {
|
|
204
|
+
return this.#keys.length
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
[getAt](index: number): T | undefined {
|
|
208
|
+
const key = this.#keys[index]
|
|
209
|
+
return key === undefined ? undefined : this.#values.get(key)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
[setAt](index: number, value: T): void {
|
|
213
|
+
const key = this.ensureKey(value)
|
|
214
|
+
if (index === this.length) {
|
|
215
|
+
this.insertKeyValue(index, key, value)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
const normalized = this.normalizeIndex(index)
|
|
219
|
+
this.assignAt(normalized, key, value)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
push(...items: T[]): number {
|
|
223
|
+
for (const item of items) {
|
|
224
|
+
const key = this.ensureKey(item)
|
|
225
|
+
this.insertKeyValue(this.length, key, item)
|
|
226
|
+
}
|
|
227
|
+
return this.length
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
pop(): T | undefined {
|
|
231
|
+
if (!this.length) return undefined
|
|
232
|
+
return this.removeAt(this.length - 1)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
shift(): T | undefined {
|
|
236
|
+
if (!this.length) return undefined
|
|
237
|
+
return this.removeAt(0)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
unshift(...items: T[]): number {
|
|
241
|
+
let index = 0
|
|
242
|
+
for (const item of items) {
|
|
243
|
+
const key = this.ensureKey(item)
|
|
244
|
+
this.insertKeyValue(index++, key, item)
|
|
245
|
+
}
|
|
246
|
+
return this.length
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
splice(start: number, deleteCount?: number, ...items: T[]): T[] {
|
|
250
|
+
const normalizedStart = this.normalizeIndex(start, true)
|
|
251
|
+
const maxDeletions = this.length - normalizedStart
|
|
252
|
+
const actualDelete = Math.min(
|
|
253
|
+
deleteCount === undefined ? maxDeletions : Math.max(deleteCount, 0),
|
|
254
|
+
maxDeletions
|
|
255
|
+
)
|
|
256
|
+
const keysToInsert: K[] = []
|
|
257
|
+
for (const item of items) keysToInsert.push(this.ensureKey(item))
|
|
258
|
+
const removedKeys = this.#keys.splice(normalizedStart, actualDelete, ...keysToInsert)
|
|
259
|
+
const removedValues: T[] = []
|
|
260
|
+
for (const key of removedKeys) {
|
|
261
|
+
if (key === undefined) continue
|
|
262
|
+
const value = this.#values.get(key as K)
|
|
263
|
+
this.decrementUsage(key as K)
|
|
264
|
+
removedValues.push(value as T)
|
|
265
|
+
}
|
|
266
|
+
for (let i = 0; i < keysToInsert.length; i++) {
|
|
267
|
+
const key = keysToInsert[i]
|
|
268
|
+
const value = items[i]
|
|
269
|
+
this.incrementUsage(key)
|
|
270
|
+
this.setKeyValue(key, value)
|
|
271
|
+
}
|
|
272
|
+
return removedValues
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
clear(): void {
|
|
276
|
+
this.#keys.length = 0
|
|
277
|
+
this.#usage.clear()
|
|
278
|
+
this.#values.clear()
|
|
279
|
+
this.disposeKeyEffects()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
get(key: K): T | undefined {
|
|
283
|
+
return this.#values.get(key)
|
|
284
|
+
}
|
|
285
|
+
set(key: K, value: T): void {
|
|
286
|
+
if (this.#values.has(key)) this.setKeyValue(key, value)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
remove(key: K) {
|
|
290
|
+
let index: number = this.indexOfKey(key)
|
|
291
|
+
while (index !== -1) {
|
|
292
|
+
this.removeAt(index)
|
|
293
|
+
index = this.indexOfKey(key)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
public removeAt(index: number): T | undefined {
|
|
298
|
+
const [key] = this.#keys.splice(index, 1)
|
|
299
|
+
if (key === undefined) return undefined
|
|
300
|
+
const value = this.#values.get(key as K)
|
|
301
|
+
this.decrementUsage(key as K)
|
|
302
|
+
return value
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Keep only the items for which the predicate returns true.
|
|
307
|
+
* Items for which the predicate returns false are removed.
|
|
308
|
+
*
|
|
309
|
+
* The predicate is evaluated once per distinct key; duplicate keys
|
|
310
|
+
* will follow the same keep/remove decision.
|
|
311
|
+
*/
|
|
312
|
+
public keep(predicate: (value: T) => boolean): void {
|
|
313
|
+
const decisions = new Map<K, boolean>()
|
|
314
|
+
for (const [index, key] of this.#keys.entries()) {
|
|
315
|
+
if (decisions.has(key)) {
|
|
316
|
+
if (!decisions.get(key)) this.removeAt(index)
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
const value = this.#values.get(key)
|
|
320
|
+
const shouldKeep = predicate(value as T)
|
|
321
|
+
decisions.set(key, shouldKeep)
|
|
322
|
+
if (!shouldKeep) this.removeAt(index)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
hasKey(key: K): boolean {
|
|
327
|
+
return this.#usage.has(key)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
indexOfKey(key: K): number {
|
|
331
|
+
return this.#keys.indexOf(key)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
mapKeys(): IterableIterator<K> {
|
|
335
|
+
return this.#values.keys()
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
update(...values: T[]): void {
|
|
339
|
+
for (const value of values) {
|
|
340
|
+
const key = this.ensureKey(value)
|
|
341
|
+
if (this.#values.has(key)) this.setKeyValue(key, value)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
upsert(insert: (value: T) => void, ...values: T[]): void {
|
|
346
|
+
for (const value of values) {
|
|
347
|
+
const key = this.ensureKey(value)
|
|
348
|
+
if (this.#values.has(key)) this.setKeyValue(key, value)
|
|
349
|
+
else insert(value)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
entries(): IterableIterator<[number, T]> {
|
|
354
|
+
const self = this
|
|
355
|
+
function* iterator(): IterableIterator<[number, T]> {
|
|
356
|
+
for (let i = 0; i < self.#keys.length; i++) {
|
|
357
|
+
const val = self.#values.get(self.#keys[i])
|
|
358
|
+
if (val !== undefined) yield [i, val]
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return iterator()
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
[Symbol.iterator](): IterableIterator<T> {
|
|
365
|
+
const self = this
|
|
366
|
+
function* iterator(): IterableIterator<T> {
|
|
367
|
+
for (const key of self.#keys) {
|
|
368
|
+
const value = self.#values.get(key)
|
|
369
|
+
if (value !== undefined) yield value
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return iterator()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
toString(): string {
|
|
376
|
+
return `[Register length=${this.length}]`
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
at(index: number): T | undefined {
|
|
380
|
+
const resolved = index < 0 ? this.length + index : index
|
|
381
|
+
if (resolved < 0 || resolved >= this.length) return undefined
|
|
382
|
+
return this[getAt](resolved)
|
|
383
|
+
}
|
|
384
|
+
reverse(): this {
|
|
385
|
+
this.#keys.reverse()
|
|
386
|
+
return this
|
|
387
|
+
}
|
|
388
|
+
sort(compareFn?: ((a: T, b: T) => number) | undefined): this {
|
|
389
|
+
const fwdCompareFn = compareFn
|
|
390
|
+
? (a: K, b: K) => compareFn(this.#values.get(a) as T, this.#values.get(b) as T)
|
|
391
|
+
: undefined
|
|
392
|
+
this.#keys.sort(fwdCompareFn)
|
|
393
|
+
return this
|
|
394
|
+
}
|
|
395
|
+
fill(value: T, start = 0, end = this.length): this {
|
|
396
|
+
const values = this.toArray()
|
|
397
|
+
values.fill(value, start, end)
|
|
398
|
+
this.rebuildFrom(values)
|
|
399
|
+
return this
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
copyWithin(target: number, start: number, end?: number): this {
|
|
403
|
+
const values = this.toArray()
|
|
404
|
+
values.copyWithin(target, start, end)
|
|
405
|
+
this.rebuildFrom(values)
|
|
406
|
+
return this
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export type Register<T, K extends PropertyKey = PropertyKey> = RegisterClass<T, K> & T[]
|
|
411
|
+
export const Register = RegisterClass as new <T, K extends PropertyKey = PropertyKey>(
|
|
412
|
+
keyFn: KeyFunction<T, K>,
|
|
413
|
+
initial?: Iterable<T>
|
|
414
|
+
) => Register<T, K>
|
|
415
|
+
|
|
416
|
+
export function register<T, K extends PropertyKey = PropertyKey>(
|
|
417
|
+
keyFn: KeyFunction<T, K>,
|
|
418
|
+
initial?: Iterable<T>
|
|
419
|
+
): Register<T, K> {
|
|
420
|
+
return new RegisterClass(keyFn, initial) as Register<T, K>
|
|
421
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { touched, touched1 } from './change'
|
|
2
|
+
import { makeReactiveEntriesIterator, makeReactiveIterator } from './non-reactive'
|
|
3
|
+
import { reactive } from './proxy'
|
|
4
|
+
import { dependant } from './tracking'
|
|
5
|
+
import { prototypeForwarding } from './types'
|
|
6
|
+
|
|
7
|
+
const native = Symbol('native')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reactive wrapper around JavaScript's WeakSet class
|
|
11
|
+
* Only tracks individual value operations, no size tracking (WeakSet limitation)
|
|
12
|
+
*/
|
|
13
|
+
export class ReactiveWeakSet<T extends object> {
|
|
14
|
+
declare readonly [native]: WeakSet<T>
|
|
15
|
+
declare readonly content: symbol
|
|
16
|
+
|
|
17
|
+
constructor(original: WeakSet<T>) {
|
|
18
|
+
Object.defineProperties(this, {
|
|
19
|
+
[native]: { value: original },
|
|
20
|
+
[prototypeForwarding]: { value: original },
|
|
21
|
+
content: { value: Symbol('content') },
|
|
22
|
+
[Symbol.toStringTag]: { value: 'ReactiveWeakSet' },
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
add(value: T): this {
|
|
27
|
+
const had = this[native].has(value)
|
|
28
|
+
this[native].add(value)
|
|
29
|
+
if (!had) {
|
|
30
|
+
// touch the specific value and the collection view
|
|
31
|
+
touched1(this.content, { type: 'add', prop: value }, value)
|
|
32
|
+
// no size/allProps for WeakSet
|
|
33
|
+
}
|
|
34
|
+
return this
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
delete(value: T): boolean {
|
|
38
|
+
const had = this[native].has(value)
|
|
39
|
+
const res = this[native].delete(value)
|
|
40
|
+
if (had) touched1(this.content, { type: 'del', prop: value }, value)
|
|
41
|
+
return res
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
has(value: T): boolean {
|
|
45
|
+
dependant(this.content, value)
|
|
46
|
+
return this[native].has(value)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reactive wrapper around JavaScript's Set class
|
|
52
|
+
* Tracks size changes, individual value operations, and collection-wide operations
|
|
53
|
+
*/
|
|
54
|
+
export class ReactiveSet<T> {
|
|
55
|
+
declare readonly [native]: Set<T>
|
|
56
|
+
declare readonly content: symbol
|
|
57
|
+
constructor(original: Set<T>) {
|
|
58
|
+
Object.defineProperties(this, {
|
|
59
|
+
[native]: { value: original },
|
|
60
|
+
[prototypeForwarding]: { value: original },
|
|
61
|
+
content: { value: Symbol('content') },
|
|
62
|
+
[Symbol.toStringTag]: { value: 'ReactiveSet' },
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get size(): number {
|
|
67
|
+
// size depends on the wrapper instance, like Map counterpart
|
|
68
|
+
dependant(this, 'size')
|
|
69
|
+
return this[native].size
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
add(value: T): this {
|
|
73
|
+
const had = this[native].has(value)
|
|
74
|
+
const reactiveValue = reactive(value)
|
|
75
|
+
this[native].add(reactiveValue)
|
|
76
|
+
if (!had) {
|
|
77
|
+
const evolution = { type: 'add', prop: reactiveValue } as const
|
|
78
|
+
// touch for value-specific and aggregate dependencies
|
|
79
|
+
touched1(this.content, evolution, reactiveValue)
|
|
80
|
+
touched1(this, evolution, 'size')
|
|
81
|
+
}
|
|
82
|
+
return this
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
clear(): void {
|
|
86
|
+
const hadEntries = this[native].size > 0
|
|
87
|
+
this[native].clear()
|
|
88
|
+
if (hadEntries) {
|
|
89
|
+
const evolution = { type: 'bunch', method: 'clear' } as const
|
|
90
|
+
touched1(this, evolution, 'size')
|
|
91
|
+
touched(this.content, evolution)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
delete(value: T): boolean {
|
|
96
|
+
const had = this[native].has(value)
|
|
97
|
+
const res = this[native].delete(value)
|
|
98
|
+
if (had) {
|
|
99
|
+
const evolution = { type: 'del', prop: value } as const
|
|
100
|
+
touched1(this.content, evolution, value)
|
|
101
|
+
touched1(this, evolution, 'size')
|
|
102
|
+
}
|
|
103
|
+
return res
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
has(value: T): boolean {
|
|
107
|
+
dependant(this.content, value)
|
|
108
|
+
return this[native].has(value)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
entries(): Generator<[T, T]> {
|
|
112
|
+
dependant(this.content)
|
|
113
|
+
return makeReactiveEntriesIterator(this[native].entries())
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void {
|
|
117
|
+
dependant(this.content)
|
|
118
|
+
this[native].forEach(callbackfn, thisArg)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
keys(): Generator<T> {
|
|
122
|
+
dependant(this.content)
|
|
123
|
+
return makeReactiveIterator(this[native].keys())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
values(): Generator<T> {
|
|
127
|
+
dependant(this.content)
|
|
128
|
+
return makeReactiveIterator(this[native].values())
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
[Symbol.iterator](): Iterator<T> {
|
|
132
|
+
dependant(this.content)
|
|
133
|
+
const nativeIterator = this[native][Symbol.iterator]()
|
|
134
|
+
return {
|
|
135
|
+
next() {
|
|
136
|
+
const result = nativeIterator.next()
|
|
137
|
+
if (result.done) {
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
return { value: reactive(result.value), done: false }
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { getActiveEffect } from './effect-context'
|
|
2
|
+
import { unwrap } from './proxy-state'
|
|
3
|
+
import { allProps, rootFunction, type ScopedCallback } from './types'
|
|
4
|
+
|
|
5
|
+
// Track which effects are watching which reactive objects for cleanup
|
|
6
|
+
export const effectToReactiveObjects = new WeakMap<ScopedCallback, Set<object>>()
|
|
7
|
+
|
|
8
|
+
// Track effects per reactive object and property
|
|
9
|
+
export const watchers = new WeakMap<object, Map<any, Set<ScopedCallback>>>()
|
|
10
|
+
|
|
11
|
+
// runEffect -> set<stop>
|
|
12
|
+
export const effectChildren = new WeakMap<ScopedCallback, Set<ScopedCallback>>()
|
|
13
|
+
|
|
14
|
+
// Track parent effect relationships for hierarchy traversal (used in deep touch filtering)
|
|
15
|
+
export const effectParent = new WeakMap<ScopedCallback, ScopedCallback | undefined>()
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Marks a function with its root function for effect tracking
|
|
19
|
+
* @param fn - The function to mark
|
|
20
|
+
* @param root - The root function
|
|
21
|
+
* @returns The marked function
|
|
22
|
+
*/
|
|
23
|
+
export function markWithRoot<T extends Function>(fn: T, root: Function): T {
|
|
24
|
+
// Mark fn with the new root
|
|
25
|
+
return Object.defineProperty(fn, rootFunction, {
|
|
26
|
+
value: getRoot(root),
|
|
27
|
+
writable: false,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Gets the root function of a function for effect tracking
|
|
33
|
+
* @param fn - The function to get the root of
|
|
34
|
+
* @returns The root function
|
|
35
|
+
*/
|
|
36
|
+
export function getRoot<T extends Function | undefined>(fn: T): T {
|
|
37
|
+
return (fn as any)?.[rootFunction] || fn
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Flag to disable dependency tracking for the current active effect (not globally)
|
|
41
|
+
const trackingDisabledEffects = new WeakSet<ScopedCallback>()
|
|
42
|
+
let globalTrackingDisabled = false
|
|
43
|
+
|
|
44
|
+
export function getTrackingDisabled(): boolean {
|
|
45
|
+
const active = getActiveEffect()
|
|
46
|
+
if (!active) return globalTrackingDisabled
|
|
47
|
+
return trackingDisabledEffects.has(getRoot(active))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function setTrackingDisabled(value: boolean): void {
|
|
51
|
+
const active = getActiveEffect()
|
|
52
|
+
if (!active) {
|
|
53
|
+
globalTrackingDisabled = value
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
const root = getRoot(active)
|
|
57
|
+
if (value) trackingDisabledEffects.add(root)
|
|
58
|
+
else trackingDisabledEffects.delete(root)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Marks a property as a dependency of the current effect
|
|
63
|
+
* @param obj - The object containing the property
|
|
64
|
+
* @param prop - The property name (defaults to allProps)
|
|
65
|
+
*/
|
|
66
|
+
export function dependant(obj: any, prop: any = allProps) {
|
|
67
|
+
obj = unwrap(obj)
|
|
68
|
+
const currentActiveEffect = getActiveEffect()
|
|
69
|
+
|
|
70
|
+
// Early return if no active effect, tracking disabled, or invalid prop
|
|
71
|
+
if (
|
|
72
|
+
!currentActiveEffect ||
|
|
73
|
+
getTrackingDisabled() ||
|
|
74
|
+
(typeof prop === 'symbol' && prop !== allProps)
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
registerDependency(obj, prop, currentActiveEffect)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function registerDependency(obj: any, prop: any, currentActiveEffect: ScopedCallback) {
|
|
82
|
+
let objectWatchers = watchers.get(obj)
|
|
83
|
+
if (!objectWatchers) {
|
|
84
|
+
objectWatchers = new Map<PropertyKey, Set<ScopedCallback>>()
|
|
85
|
+
watchers.set(obj, objectWatchers)
|
|
86
|
+
}
|
|
87
|
+
let deps = objectWatchers.get(prop)
|
|
88
|
+
if (!deps) {
|
|
89
|
+
deps = new Set<ScopedCallback>()
|
|
90
|
+
objectWatchers.set(prop, deps)
|
|
91
|
+
}
|
|
92
|
+
deps.add(currentActiveEffect)
|
|
93
|
+
|
|
94
|
+
// Track which reactive objects this effect is watching
|
|
95
|
+
const effectObjects = effectToReactiveObjects.get(currentActiveEffect)
|
|
96
|
+
if (effectObjects) {
|
|
97
|
+
effectObjects.add(obj)
|
|
98
|
+
} else {
|
|
99
|
+
effectToReactiveObjects.set(currentActiveEffect, new Set([obj]))
|
|
100
|
+
}
|
|
101
|
+
}
|