mutts 1.0.0 → 1.0.2
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 +24 -2
- package/dist/chunks/_tslib-C-cuVLvZ.js +73 -0
- package/dist/chunks/_tslib-C-cuVLvZ.js.map +1 -0
- package/dist/chunks/_tslib-CMEnd0VE.esm.js +68 -0
- package/dist/chunks/_tslib-CMEnd0VE.esm.js.map +1 -0
- package/dist/chunks/{decorator-BXsign4Z.js → decorator-D4DU97Zg.js} +70 -4
- package/dist/chunks/decorator-D4DU97Zg.js.map +1 -0
- package/dist/chunks/{decorator-CPbZNnsX.esm.js → decorator-GnHw1Az7.esm.js} +67 -5
- package/dist/chunks/decorator-GnHw1Az7.esm.js.map +1 -0
- package/dist/chunks/index-DBScoeCX.esm.js +1960 -0
- package/dist/chunks/index-DBScoeCX.esm.js.map +1 -0
- package/dist/chunks/index-DOTmXL89.js +1983 -0
- package/dist/chunks/index-DOTmXL89.js.map +1 -0
- package/dist/decorator.d.ts +58 -1
- package/dist/decorator.esm.js +1 -1
- package/dist/decorator.js +1 -1
- package/dist/destroyable.d.ts +42 -0
- 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/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 +52 -3
- package/dist/index.esm.js +3 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +18 -3
- package/dist/index.js.map +1 -1
- package/dist/indexable.d.ts +26 -0
- package/dist/indexable.esm.js +6 -0
- package/dist/indexable.esm.js.map +1 -1
- package/dist/indexable.js +6 -0
- 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 +258 -20
- package/dist/reactive.esm.js +4 -1454
- package/dist/reactive.esm.js.map +1 -1
- package/dist/reactive.js +29 -1466
- 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/mixin.md +229 -0
- package/docs/reactive.md +7931 -458
- package/package.json +1 -2
- package/dist/chunks/decorator-BXsign4Z.js.map +0 -1
- package/dist/chunks/decorator-CPbZNnsX.esm.js.map +0 -1
- package/src/decorator.test.ts +0 -495
- package/src/decorator.ts +0 -205
- package/src/destroyable.test.ts +0 -155
- package/src/destroyable.ts +0 -158
- package/src/eventful.test.ts +0 -380
- package/src/eventful.ts +0 -69
- package/src/index.ts +0 -7
- package/src/indexable.test.ts +0 -388
- package/src/indexable.ts +0 -124
- package/src/promiseChain.test.ts +0 -201
- package/src/promiseChain.ts +0 -99
- package/src/reactive/array.test.ts +0 -923
- package/src/reactive/array.ts +0 -352
- package/src/reactive/core.test.ts +0 -1663
- package/src/reactive/core.ts +0 -866
- package/src/reactive/index.ts +0 -28
- package/src/reactive/interface.test.ts +0 -1477
- package/src/reactive/interface.ts +0 -231
- package/src/reactive/map.test.ts +0 -866
- package/src/reactive/map.ts +0 -162
- package/src/reactive/set.test.ts +0 -289
- package/src/reactive/set.ts +0 -142
- package/src/std-decorators.test.ts +0 -679
- package/src/std-decorators.ts +0 -182
- package/src/utils.ts +0 -52
package/src/reactive/core.ts
DELETED
|
@@ -1,866 +0,0 @@
|
|
|
1
|
-
// biome-ignore-all lint/suspicious/noConfusingVoidType: Type 'void' is not assignable to type 'ScopedCallback | undefined'.
|
|
2
|
-
// Argument of type '() => void' is not assignable to parameter of type '(dep: DependencyFunction) => ScopedCallback | undefined'.
|
|
3
|
-
|
|
4
|
-
import { decorator } from '../decorator'
|
|
5
|
-
|
|
6
|
-
export type DependencyFunction = <T>(cb: () => T) => T
|
|
7
|
-
// TODO: proper async management, read when fn returns a promise and let the effect as "running",
|
|
8
|
-
// either to cancel the running one or to avoid running 2 in "parallel" and debounce the second one
|
|
9
|
-
|
|
10
|
-
// TODO: generic "batch" forcing even if not in an effect (perhaps when calling a reactive' function ?)
|
|
11
|
-
// example: storage will make 2 modifications (add slot, modify count), they could raise 2 effects
|
|
12
|
-
export type ScopedCallback = () => void
|
|
13
|
-
|
|
14
|
-
export type PropEvolution = {
|
|
15
|
-
type: 'set' | 'del' | 'add'
|
|
16
|
-
prop: any
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type BunchEvolution = {
|
|
20
|
-
type: 'bunch'
|
|
21
|
-
method: string
|
|
22
|
-
}
|
|
23
|
-
type Evolution = PropEvolution | BunchEvolution
|
|
24
|
-
|
|
25
|
-
type State =
|
|
26
|
-
| {
|
|
27
|
-
evolution: Evolution
|
|
28
|
-
next: State
|
|
29
|
-
}
|
|
30
|
-
| {}
|
|
31
|
-
// Track which effects are watching which reactive objects for cleanup
|
|
32
|
-
const effectToReactiveObjects = new WeakMap<ScopedCallback, Set<object>>()
|
|
33
|
-
|
|
34
|
-
// Track object -> proxy and proxy -> object relationships
|
|
35
|
-
const objectToProxy = new WeakMap<object, object>()
|
|
36
|
-
const proxyToObject = new WeakMap<object, object>()
|
|
37
|
-
// Deep watching data structures
|
|
38
|
-
// Track which objects contain which other objects (back-references)
|
|
39
|
-
const objectParents = new WeakMap<object, Set<{ parent: object; prop: PropertyKey }>>()
|
|
40
|
-
|
|
41
|
-
// Track which objects have deep watchers
|
|
42
|
-
const objectsWithDeepWatchers = new WeakSet<object>()
|
|
43
|
-
|
|
44
|
-
// Track deep watchers per object
|
|
45
|
-
const deepWatchers = new WeakMap<object, Set<ScopedCallback>>()
|
|
46
|
-
|
|
47
|
-
// Track which effects are doing deep watching
|
|
48
|
-
const effectToDeepWatchedObjects = new WeakMap<ScopedCallback, Set<object>>()
|
|
49
|
-
|
|
50
|
-
// Track objects that should never be reactive and cannot be modified
|
|
51
|
-
export const nonReactiveObjects = new WeakSet<object>()
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Converts an iterator to a generator that yields reactive values
|
|
55
|
-
*/
|
|
56
|
-
export function* makeReactiveIterator<T>(iterator: Iterator<T>): Generator<T> {
|
|
57
|
-
let result = iterator.next()
|
|
58
|
-
while (!result.done) {
|
|
59
|
-
yield reactive(result.value)
|
|
60
|
-
result = iterator.next()
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Converts an iterator of key-value pairs to a generator that yields reactive key-value pairs
|
|
66
|
-
*/
|
|
67
|
-
export function* makeReactiveEntriesIterator<K, V>(iterator: Iterator<[K, V]>): Generator<[K, V]> {
|
|
68
|
-
let result = iterator.next()
|
|
69
|
-
while (!result.done) {
|
|
70
|
-
const [key, value] = result.value
|
|
71
|
-
yield [reactive(key), reactive(value)]
|
|
72
|
-
result = iterator.next()
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Track effects per reactive object and property
|
|
77
|
-
const watchers = new WeakMap<object, Map<any, Set<ScopedCallback>>>()
|
|
78
|
-
|
|
79
|
-
export const profileInfo: any = {
|
|
80
|
-
objectToProxy,
|
|
81
|
-
proxyToObject,
|
|
82
|
-
effectToReactiveObjects,
|
|
83
|
-
watchers,
|
|
84
|
-
objectParents,
|
|
85
|
-
objectsWithDeepWatchers,
|
|
86
|
-
deepWatchers,
|
|
87
|
-
effectToDeepWatchedObjects,
|
|
88
|
-
nonReactiveObjects,
|
|
89
|
-
}
|
|
90
|
-
// Track native reactivity
|
|
91
|
-
const nativeReactive = Symbol('native-reactive')
|
|
92
|
-
|
|
93
|
-
// Symbol to mark individual objects as non-reactive
|
|
94
|
-
export const nonReactiveMark = Symbol('non-reactive')
|
|
95
|
-
// Symbol to mark class properties as non-reactive
|
|
96
|
-
export const unreactiveProperties = Symbol('unreactive-properties')
|
|
97
|
-
export const prototypeForwarding: unique symbol = Symbol('prototype-forwarding')
|
|
98
|
-
|
|
99
|
-
export const allProps = Symbol('all-props')
|
|
100
|
-
|
|
101
|
-
// Symbol to mark functions with their root function
|
|
102
|
-
const rootFunction = Symbol('root-function')
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Mark a function with its root function. If the function already has a root,
|
|
106
|
-
* the root becomes the root of the new root (transitive root tracking).
|
|
107
|
-
* @param fn - The function to mark
|
|
108
|
-
* @param root - The root function to associate with fn
|
|
109
|
-
*/
|
|
110
|
-
export function markWithRoot<T extends Function>(fn: T, root: Function): T {
|
|
111
|
-
// Mark fn with the new root
|
|
112
|
-
return Object.defineProperty(fn, rootFunction, {
|
|
113
|
-
value: getRoot(root),
|
|
114
|
-
writable: false,
|
|
115
|
-
})
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Retrieve the root function from a callback. Returns the function itself if it has no root.
|
|
120
|
-
* @param fn - The function to get the root from
|
|
121
|
-
* @returns The root function, or the function itself if no root exists
|
|
122
|
-
*/
|
|
123
|
-
export function getRoot<T extends Function | undefined>(fn: T): T {
|
|
124
|
-
return (fn as any)?.[rootFunction] || fn
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export class ReactiveError extends Error {
|
|
128
|
-
constructor(message: string) {
|
|
129
|
-
super(message)
|
|
130
|
-
this.name = 'ReactiveError'
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
|
|
135
|
-
/**
|
|
136
|
-
* Options for the reactive system, can be configured at runtime
|
|
137
|
-
*/
|
|
138
|
-
export const options = {
|
|
139
|
-
/**
|
|
140
|
-
* Debug purpose: called when an effect is entered
|
|
141
|
-
* @param effect - The effect that is entered
|
|
142
|
-
*/
|
|
143
|
-
enter: (effect: Function) => {},
|
|
144
|
-
/**
|
|
145
|
-
* Debug purpose: called when an effect is left
|
|
146
|
-
* @param effect - The effect that is left
|
|
147
|
-
*/
|
|
148
|
-
leave: (effect: Function) => {},
|
|
149
|
-
/**
|
|
150
|
-
* Debug purpose: called when an effect is chained
|
|
151
|
-
* @param target - The effect that is being triggered
|
|
152
|
-
* @param caller - The effect that is calling the target
|
|
153
|
-
*/
|
|
154
|
-
chain: (target: Function, caller?: Function) => {},
|
|
155
|
-
/**
|
|
156
|
-
* Debug purpose: maximum effect chain (like call stack max depth)
|
|
157
|
-
* Used to prevent infinite loops
|
|
158
|
-
* @default 100
|
|
159
|
-
*/
|
|
160
|
-
maxEffectChain: 100,
|
|
161
|
-
/**
|
|
162
|
-
* Maximum depth for deep watching traversal
|
|
163
|
-
* Used to prevent infinite recursion in circular references
|
|
164
|
-
* @default 100
|
|
165
|
-
*/
|
|
166
|
-
maxDeepWatchDepth: 100,
|
|
167
|
-
/**
|
|
168
|
-
* Only react on instance members modification (not inherited properties)
|
|
169
|
-
* For instance, do not track class methods
|
|
170
|
-
* @default true
|
|
171
|
-
*/
|
|
172
|
-
instanceMembers: true,
|
|
173
|
-
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
174
|
-
warn: (...args: any[]) => console.warn(...args),
|
|
175
|
-
}
|
|
176
|
-
// biome-ignore-end lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
|
|
177
|
-
|
|
178
|
-
//#region evolution
|
|
179
|
-
|
|
180
|
-
function raiseDeps(objectWatchers: Map<any, Set<ScopedCallback>>, ...keyChains: Iterable<any>[]) {
|
|
181
|
-
for (const keys of keyChains)
|
|
182
|
-
for (const key of keys) {
|
|
183
|
-
const deps = objectWatchers.get(key)
|
|
184
|
-
if (deps) for (const effect of Array.from(deps)) hasEffect(effect)
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function touched1(obj: any, evolution: Evolution, prop: any) {
|
|
189
|
-
touched(obj, evolution, [prop])
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function touched(obj: any, evolution: Evolution, props?: Iterable<any>) {
|
|
193
|
-
obj = unwrap(obj)
|
|
194
|
-
addState(obj, evolution)
|
|
195
|
-
const objectWatchers = watchers.get(obj)
|
|
196
|
-
if (objectWatchers) {
|
|
197
|
-
if (props) raiseDeps(objectWatchers, [allProps], props)
|
|
198
|
-
else raiseDeps(objectWatchers, objectWatchers.keys())
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Bubble up changes if this object has deep watchers
|
|
202
|
-
if (objectsWithDeepWatchers.has(obj)) {
|
|
203
|
-
bubbleUpChange(obj, evolution)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const states = new WeakMap<object, State>()
|
|
208
|
-
|
|
209
|
-
function addState(obj: any, evolution: Evolution) {
|
|
210
|
-
obj = unwrap(obj)
|
|
211
|
-
const next = {}
|
|
212
|
-
const state = getState(obj)
|
|
213
|
-
if (state) Object.assign(state, { evolution, next })
|
|
214
|
-
states.set(obj, next)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export function getState(obj: any) {
|
|
218
|
-
obj = unwrap(obj)
|
|
219
|
-
let state = states.get(obj)
|
|
220
|
-
if (!state) {
|
|
221
|
-
state = {}
|
|
222
|
-
states.set(obj, state)
|
|
223
|
-
}
|
|
224
|
-
return state
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function dependant(obj: any, prop: any = allProps) {
|
|
228
|
-
// TODO: avoid depending on property get?
|
|
229
|
-
obj = unwrap(obj)
|
|
230
|
-
if (activeEffect && (typeof prop !== 'symbol' || prop === allProps)) {
|
|
231
|
-
let objectWatchers = watchers.get(obj)
|
|
232
|
-
if (!objectWatchers) {
|
|
233
|
-
objectWatchers = new Map<PropertyKey, Set<ScopedCallback>>()
|
|
234
|
-
watchers.set(obj, objectWatchers)
|
|
235
|
-
}
|
|
236
|
-
let deps = objectWatchers.get(prop)
|
|
237
|
-
if (!deps) {
|
|
238
|
-
deps = new Set<ScopedCallback>()
|
|
239
|
-
objectWatchers.set(prop, deps)
|
|
240
|
-
}
|
|
241
|
-
deps.add(activeEffect)
|
|
242
|
-
|
|
243
|
-
// Track which reactive objects this effect is watching
|
|
244
|
-
let effectObjects = effectToReactiveObjects.get(activeEffect)
|
|
245
|
-
if (!effectObjects) {
|
|
246
|
-
effectObjects = new Set<object>()
|
|
247
|
-
effectToReactiveObjects.set(activeEffect, effectObjects)
|
|
248
|
-
}
|
|
249
|
-
effectObjects.add(obj)
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Stack of active effects to handle nested effects
|
|
254
|
-
let activeEffect: ScopedCallback | undefined
|
|
255
|
-
// Parent effect used for lifecycle/cleanup relationships (can diverge later)
|
|
256
|
-
let parentEffect: ScopedCallback | undefined
|
|
257
|
-
|
|
258
|
-
// Track currently executing effects to prevent re-execution
|
|
259
|
-
// These are all the effects triggered under `activeEffect`
|
|
260
|
-
let batchedEffects: Map<Function, ScopedCallback> | undefined
|
|
261
|
-
|
|
262
|
-
// Track which sub-effects have been executed to prevent infinite loops
|
|
263
|
-
// These are all the effects triggered under `activeEffect` and all their sub-effects
|
|
264
|
-
function hasEffect(effect: ScopedCallback) {
|
|
265
|
-
const root = getRoot(effect)
|
|
266
|
-
|
|
267
|
-
options?.chain(getRoot(effect), getRoot(activeEffect))
|
|
268
|
-
if (batchedEffects) batchedEffects.set(root, effect)
|
|
269
|
-
else {
|
|
270
|
-
const runEffects: any[] = []
|
|
271
|
-
batchedEffects = new Map<Function, ScopedCallback>([[root, effect]])
|
|
272
|
-
try {
|
|
273
|
-
while (batchedEffects.size) {
|
|
274
|
-
if (runEffects.length > options.maxEffectChain)
|
|
275
|
-
throw new ReactiveError('[reactive] Max effect chain reached')
|
|
276
|
-
const [root, effect] = batchedEffects.entries().next().value!
|
|
277
|
-
runEffects.push(root)
|
|
278
|
-
effect()
|
|
279
|
-
batchedEffects.delete(root)
|
|
280
|
-
}
|
|
281
|
-
} finally {
|
|
282
|
-
batchedEffects = undefined
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export function withEffect<T>(
|
|
288
|
-
effect: ScopedCallback | undefined,
|
|
289
|
-
fn: () => T,
|
|
290
|
-
keepParent?: true
|
|
291
|
-
): T {
|
|
292
|
-
if (getRoot(effect) === getRoot(activeEffect)) return fn()
|
|
293
|
-
const oldActiveEffect = activeEffect
|
|
294
|
-
const oldParentEffect = parentEffect
|
|
295
|
-
activeEffect = effect
|
|
296
|
-
if (!keepParent) parentEffect = effect
|
|
297
|
-
try {
|
|
298
|
-
return fn()
|
|
299
|
-
} finally {
|
|
300
|
-
activeEffect = oldActiveEffect
|
|
301
|
-
parentEffect = oldParentEffect
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
//#endregion
|
|
306
|
-
|
|
307
|
-
//#region deep watching
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Add a back-reference from child to parent
|
|
311
|
-
*/
|
|
312
|
-
function addBackReference(child: object, parent: object, prop: any) {
|
|
313
|
-
let parents = objectParents.get(child)
|
|
314
|
-
if (!parents) {
|
|
315
|
-
parents = new Set()
|
|
316
|
-
objectParents.set(child, parents)
|
|
317
|
-
}
|
|
318
|
-
parents.add({ parent, prop })
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Remove a back-reference from child to parent
|
|
323
|
-
*/
|
|
324
|
-
function removeBackReference(child: object, parent: object, prop: any) {
|
|
325
|
-
const parents = objectParents.get(child)
|
|
326
|
-
if (parents) {
|
|
327
|
-
parents.delete({ parent, prop })
|
|
328
|
-
if (parents.size === 0) {
|
|
329
|
-
objectParents.delete(child)
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Check if an object needs back-references (has deep watchers or parents with deep watchers)
|
|
336
|
-
*/
|
|
337
|
-
function needsBackReferences(obj: object): boolean {
|
|
338
|
-
return objectsWithDeepWatchers.has(obj) || hasParentWithDeepWatchers(obj)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Check if an object has any parent with deep watchers
|
|
343
|
-
*/
|
|
344
|
-
function hasParentWithDeepWatchers(obj: object): boolean {
|
|
345
|
-
const parents = objectParents.get(obj)
|
|
346
|
-
if (!parents) return false
|
|
347
|
-
|
|
348
|
-
for (const { parent } of parents) {
|
|
349
|
-
if (objectsWithDeepWatchers.has(parent)) return true
|
|
350
|
-
if (hasParentWithDeepWatchers(parent)) return true
|
|
351
|
-
}
|
|
352
|
-
return false
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Bubble up changes through the back-reference chain
|
|
357
|
-
*/
|
|
358
|
-
function bubbleUpChange(changedObject: object, evolution: Evolution) {
|
|
359
|
-
const parents = objectParents.get(changedObject)
|
|
360
|
-
if (!parents) return
|
|
361
|
-
|
|
362
|
-
for (const { parent } of parents) {
|
|
363
|
-
// Trigger deep watchers on parent
|
|
364
|
-
const parentDeepWatchers = deepWatchers.get(parent)
|
|
365
|
-
if (parentDeepWatchers) for (const watcher of parentDeepWatchers) hasEffect(watcher)
|
|
366
|
-
|
|
367
|
-
// Continue bubbling up
|
|
368
|
-
bubbleUpChange(parent, evolution)
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export function track1(obj: object, prop: any, oldVal: any, newValue: any) {
|
|
373
|
-
// Manage back-references if this object has deep watchers
|
|
374
|
-
if (objectsWithDeepWatchers.has(obj)) {
|
|
375
|
-
// Remove old back-references
|
|
376
|
-
if (typeof oldVal === 'object' && oldVal !== null) {
|
|
377
|
-
removeBackReference(oldVal, obj, prop)
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Add new back-references
|
|
381
|
-
if (typeof newValue === 'object' && newValue !== null) {
|
|
382
|
-
const reactiveValue = reactive(newValue)
|
|
383
|
-
addBackReference(reactiveValue, obj, prop)
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return newValue
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
//#endregion
|
|
390
|
-
|
|
391
|
-
const reactiveHandlers = {
|
|
392
|
-
[Symbol.toStringTag]: 'MutTs Reactive',
|
|
393
|
-
get(obj: any, prop: PropertyKey, receiver: any) {
|
|
394
|
-
if (prop === nonReactiveMark) return false
|
|
395
|
-
// Check if this property is marked as unreactive
|
|
396
|
-
if (obj[unreactiveProperties]?.has(prop) || typeof prop === 'symbol')
|
|
397
|
-
return Reflect.get(obj, prop, receiver)
|
|
398
|
-
const absent = !(prop in obj)
|
|
399
|
-
// Depend if...
|
|
400
|
-
if (!options.instanceMembers || Object.hasOwn(receiver, prop) || absent) dependant(obj, prop)
|
|
401
|
-
|
|
402
|
-
const value = Reflect.get(obj, prop, receiver)
|
|
403
|
-
if (typeof value === 'object' && value !== null) {
|
|
404
|
-
const reactiveValue = reactive(value)
|
|
405
|
-
|
|
406
|
-
// Only create back-references if this object needs them
|
|
407
|
-
if (needsBackReferences(obj)) {
|
|
408
|
-
addBackReference(reactiveValue, obj, prop)
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return reactiveValue
|
|
412
|
-
}
|
|
413
|
-
return value
|
|
414
|
-
},
|
|
415
|
-
set(obj: any, prop: PropertyKey, value: any, receiver: any): boolean {
|
|
416
|
-
// Check if this property is marked as unreactive
|
|
417
|
-
if (obj[unreactiveProperties]?.has(prop)) return Reflect.set(obj, prop, value, receiver)
|
|
418
|
-
// Really specific case for when Array is forwarder, in order to let it manage the reactivity
|
|
419
|
-
const isArrayCase =
|
|
420
|
-
prototypeForwarding in obj &&
|
|
421
|
-
// biome-ignore lint/suspicious/useIsArray: This is the whole point here
|
|
422
|
-
obj[prototypeForwarding] instanceof Array &&
|
|
423
|
-
(!Number.isNaN(Number(prop)) || prop === 'length')
|
|
424
|
-
const newValue = unwrap(value)
|
|
425
|
-
|
|
426
|
-
if (isArrayCase) {
|
|
427
|
-
;(obj as any)[prop] = newValue
|
|
428
|
-
return true
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const oldVal = (obj as any)[prop]
|
|
432
|
-
const oldPresent = prop in obj
|
|
433
|
-
track1(obj, prop, oldVal, newValue)
|
|
434
|
-
|
|
435
|
-
if (oldVal !== newValue) {
|
|
436
|
-
Reflect.set(obj, prop, newValue, receiver)
|
|
437
|
-
// try to find a "generic" way to express that
|
|
438
|
-
touched1(obj, { type: oldPresent ? 'set' : 'add', prop }, prop)
|
|
439
|
-
}
|
|
440
|
-
return true
|
|
441
|
-
},
|
|
442
|
-
deleteProperty(obj: any, prop: PropertyKey): boolean {
|
|
443
|
-
if (!Object.hasOwn(obj, prop)) return false
|
|
444
|
-
|
|
445
|
-
const oldVal = (obj as any)[prop]
|
|
446
|
-
|
|
447
|
-
// Remove back-references if this object has deep watchers
|
|
448
|
-
if (objectsWithDeepWatchers.has(obj) && typeof oldVal === 'object' && oldVal !== null) {
|
|
449
|
-
removeBackReference(oldVal, obj, prop)
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
delete (obj as any)[prop]
|
|
453
|
-
touched1(obj, { type: 'del', prop }, prop)
|
|
454
|
-
|
|
455
|
-
// Bubble up changes if this object has deep watchers
|
|
456
|
-
if (objectsWithDeepWatchers.has(obj)) {
|
|
457
|
-
bubbleUpChange(obj, { type: 'del', prop })
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
return true
|
|
461
|
-
},
|
|
462
|
-
getPrototypeOf(obj: any): object | null {
|
|
463
|
-
if (prototypeForwarding in obj) return obj[prototypeForwarding]
|
|
464
|
-
return Object.getPrototypeOf(obj)
|
|
465
|
-
},
|
|
466
|
-
setPrototypeOf(obj: any, proto: object | null): boolean {
|
|
467
|
-
if (prototypeForwarding in obj) return false
|
|
468
|
-
Object.setPrototypeOf(obj, proto)
|
|
469
|
-
return true
|
|
470
|
-
},
|
|
471
|
-
ownKeys(obj: any): (string | symbol)[] {
|
|
472
|
-
dependant(obj, allProps)
|
|
473
|
-
return Reflect.ownKeys(obj)
|
|
474
|
-
},
|
|
475
|
-
} as const
|
|
476
|
-
|
|
477
|
-
const reactiveClasses = new WeakSet<Function>()
|
|
478
|
-
export class ReactiveBase {
|
|
479
|
-
constructor() {
|
|
480
|
-
// biome-ignore lint/correctness/noConstructorReturn: This is the whole point here
|
|
481
|
-
return reactiveClasses.has(new.target) ? reactive(this) : this
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function reactiveObject<T>(anyTarget: T): T {
|
|
486
|
-
if (!anyTarget || typeof anyTarget !== 'object') return anyTarget
|
|
487
|
-
const target = anyTarget as any
|
|
488
|
-
// If target is already a proxy, return it
|
|
489
|
-
if (proxyToObject.has(target) || isNonReactive(target)) return target as T
|
|
490
|
-
|
|
491
|
-
// If we already have a proxy for this object, return it
|
|
492
|
-
if (objectToProxy.has(target)) return objectToProxy.get(target) as T
|
|
493
|
-
|
|
494
|
-
const proxied =
|
|
495
|
-
nativeReactive in target && !(target instanceof target[nativeReactive])
|
|
496
|
-
? new target[nativeReactive](target)
|
|
497
|
-
: target
|
|
498
|
-
if (proxied !== target) proxyToObject.set(proxied, target)
|
|
499
|
-
const proxy = new Proxy(proxied, reactiveHandlers)
|
|
500
|
-
|
|
501
|
-
// Store the relationships
|
|
502
|
-
objectToProxy.set(target, proxy)
|
|
503
|
-
proxyToObject.set(proxy, target)
|
|
504
|
-
return proxy as T
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
export const reactive = decorator({
|
|
508
|
-
class(original) {
|
|
509
|
-
if (original.prototype instanceof ReactiveBase) {
|
|
510
|
-
reactiveClasses.add(original)
|
|
511
|
-
return original
|
|
512
|
-
}
|
|
513
|
-
class Reactive extends original {
|
|
514
|
-
constructor(...args: any[]) {
|
|
515
|
-
super(...args)
|
|
516
|
-
if (new.target !== Reactive && !reactiveClasses.has(new.target))
|
|
517
|
-
options.warn(
|
|
518
|
-
`${(original as any).name} has been inherited by ${this.constructor.name} that is not reactive.
|
|
519
|
-
@reactive decorator must be applied to the leaf class OR classes have to extend ReactiveBase.`
|
|
520
|
-
)
|
|
521
|
-
// biome-ignore lint/correctness/noConstructorReturn: This is the whole point here
|
|
522
|
-
return reactive(this)
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
Object.defineProperty(Reactive, 'name', {
|
|
526
|
-
value: `Reactive<${original.name}>`,
|
|
527
|
-
})
|
|
528
|
-
return Reactive as any
|
|
529
|
-
},
|
|
530
|
-
get(original) {
|
|
531
|
-
return reactiveObject(original)
|
|
532
|
-
},
|
|
533
|
-
default: reactiveObject,
|
|
534
|
-
})
|
|
535
|
-
|
|
536
|
-
export function unwrap<T>(proxy: T): T {
|
|
537
|
-
// Return the original object
|
|
538
|
-
return (proxyToObject.get(proxy as any) as T) ?? proxy
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
export function isReactive(obj: any): boolean {
|
|
542
|
-
return proxyToObject.has(obj)
|
|
543
|
-
}
|
|
544
|
-
export function untracked(fn: () => ScopedCallback | undefined | void) {
|
|
545
|
-
withEffect(undefined, fn, true)
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// runEffect -> set<cleanup>
|
|
549
|
-
const effectChildren = new WeakMap<ScopedCallback, Set<ScopedCallback>>()
|
|
550
|
-
const fr = new FinalizationRegistry<() => void>((f) => f())
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* @param fn - The effect function to run - provides the cleaner
|
|
554
|
-
* @returns The cleanup function
|
|
555
|
-
*/
|
|
556
|
-
export function effect<Args extends any[]>(
|
|
557
|
-
fn: (dep: DependencyFunction, ...args: Args) => ScopedCallback | undefined | void,
|
|
558
|
-
...args: Args
|
|
559
|
-
): ScopedCallback {
|
|
560
|
-
let cleanup: (() => void) | null = null
|
|
561
|
-
const dep = markWithRoot(<T>(cb: () => T) => withEffect(runEffect, cb), fn)
|
|
562
|
-
let effectStopped = false
|
|
563
|
-
|
|
564
|
-
function runEffect() {
|
|
565
|
-
// Clear previous dependencies
|
|
566
|
-
cleanup?.()
|
|
567
|
-
|
|
568
|
-
options.enter(fn)
|
|
569
|
-
const reactionCleanup = withEffect(effectStopped ? undefined : runEffect, () =>
|
|
570
|
-
fn(dep, ...args)
|
|
571
|
-
) as undefined | ScopedCallback
|
|
572
|
-
options.leave(fn)
|
|
573
|
-
|
|
574
|
-
// Create cleanup function for next run
|
|
575
|
-
cleanup = () => {
|
|
576
|
-
cleanup = null
|
|
577
|
-
reactionCleanup?.()
|
|
578
|
-
// Remove this effect from all reactive objects it's watching
|
|
579
|
-
const effectObjects = effectToReactiveObjects.get(runEffect)
|
|
580
|
-
if (effectObjects) {
|
|
581
|
-
for (const reactiveObj of effectObjects) {
|
|
582
|
-
const objectWatchers = watchers.get(reactiveObj)
|
|
583
|
-
if (objectWatchers) {
|
|
584
|
-
for (const [prop, deps] of objectWatchers.entries()) {
|
|
585
|
-
deps.delete(runEffect)
|
|
586
|
-
if (deps.size === 0) {
|
|
587
|
-
objectWatchers.delete(prop)
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
if (objectWatchers.size === 0) {
|
|
591
|
-
watchers.delete(reactiveObj)
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
effectToReactiveObjects.delete(runEffect)
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
// Mark the runEffect callback with the original function as its root
|
|
600
|
-
markWithRoot(runEffect, fn)
|
|
601
|
-
|
|
602
|
-
// Run the effect immediately
|
|
603
|
-
if (!batchedEffects) {
|
|
604
|
-
hasEffect(runEffect)
|
|
605
|
-
} else {
|
|
606
|
-
const oldBatchedEffects = batchedEffects
|
|
607
|
-
try {
|
|
608
|
-
// Simulate a hasEffect who batches, but do not execute the batch, give it back to the parent batch,
|
|
609
|
-
// Only the immediate effect has to be executed, the sub-effects will be executed by the parent batch
|
|
610
|
-
batchedEffects = new Map([[fn, runEffect]])
|
|
611
|
-
runEffect()
|
|
612
|
-
batchedEffects.delete(fn)
|
|
613
|
-
for (const [root, effect] of batchedEffects!) oldBatchedEffects.set(root, effect)
|
|
614
|
-
} finally {
|
|
615
|
-
batchedEffects = oldBatchedEffects
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const mainCleanup = (): void => {
|
|
620
|
-
if (effectStopped) return
|
|
621
|
-
effectStopped = true
|
|
622
|
-
cleanup?.()
|
|
623
|
-
// Invoke all child cleanups (recursive via subEffectCleanup calling its own mainCleanup)
|
|
624
|
-
const children = effectChildren.get(runEffect)
|
|
625
|
-
if (children) {
|
|
626
|
-
for (const childCleanup of children) childCleanup()
|
|
627
|
-
effectChildren.delete(runEffect)
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
fr.unregister(mainCleanup)
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Only ROOT effects are registered for GC cleanup
|
|
634
|
-
if (!parentEffect) {
|
|
635
|
-
const callIfCollected = () => mainCleanup()
|
|
636
|
-
fr.register(callIfCollected, mainCleanup, callIfCollected)
|
|
637
|
-
return callIfCollected
|
|
638
|
-
}
|
|
639
|
-
// TODO: parentEffect = last non-undefined activeEffect
|
|
640
|
-
// Register this effect to be cleaned up with the parent effect
|
|
641
|
-
let children = effectChildren.get(parentEffect)
|
|
642
|
-
if (!children) {
|
|
643
|
-
children = new Set()
|
|
644
|
-
effectChildren.set(parentEffect, children)
|
|
645
|
-
}
|
|
646
|
-
const parent = parentEffect
|
|
647
|
-
const subEffectCleanup = (): void => {
|
|
648
|
-
children.delete(subEffectCleanup)
|
|
649
|
-
if (children.size === 0) {
|
|
650
|
-
effectChildren.delete(parent)
|
|
651
|
-
}
|
|
652
|
-
// Execute this child effect cleanup (which triggers its own mainCleanup)
|
|
653
|
-
mainCleanup()
|
|
654
|
-
}
|
|
655
|
-
children.add(subEffectCleanup)
|
|
656
|
-
return subEffectCleanup
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* Mark an object as non-reactive. This object and all its properties will never be made reactive.
|
|
661
|
-
* @param obj - The object to mark as non-reactive
|
|
662
|
-
*/
|
|
663
|
-
function nonReactive<T extends object[]>(...obj: T): T[0] {
|
|
664
|
-
for (const o of obj) {
|
|
665
|
-
try {
|
|
666
|
-
Object.defineProperty(o, nonReactiveMark, {
|
|
667
|
-
value: true,
|
|
668
|
-
writable: false,
|
|
669
|
-
enumerable: false,
|
|
670
|
-
configurable: false,
|
|
671
|
-
})
|
|
672
|
-
} catch {}
|
|
673
|
-
if (!(nonReactiveMark in (o as object))) nonReactiveObjects.add(o as object)
|
|
674
|
-
}
|
|
675
|
-
return obj[0]
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* Set of functions to test if an object is immutable
|
|
680
|
-
*/
|
|
681
|
-
export const immutables = new Set<(tested: any) => boolean>()
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* Check if an object is marked as non-reactive (for testing purposes)
|
|
685
|
-
* @param obj - The object to check
|
|
686
|
-
* @returns true if the object is marked as non-reactive
|
|
687
|
-
*/
|
|
688
|
-
export function isNonReactive(obj: any): boolean {
|
|
689
|
-
// Don't make primitives reactive
|
|
690
|
-
if (obj === null || typeof obj !== 'object') return true
|
|
691
|
-
|
|
692
|
-
// Check if the object itself is marked as non-reactive
|
|
693
|
-
if (nonReactiveObjects.has(obj)) return true
|
|
694
|
-
|
|
695
|
-
// Check if the object has the non-reactive symbol
|
|
696
|
-
if (obj[nonReactiveMark]) return true
|
|
697
|
-
|
|
698
|
-
// Check if the object is immutable
|
|
699
|
-
if (Array.from(immutables).some((fn) => fn(obj))) return true
|
|
700
|
-
|
|
701
|
-
return false
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
/**
|
|
705
|
-
* Mark a class as non-reactive. All instances of this class will automatically be non-reactive.
|
|
706
|
-
* @param cls - The class constructor to mark as non-reactive
|
|
707
|
-
*/
|
|
708
|
-
export function nonReactiveClass<T extends (new (...args: any[]) => any)[]>(...cls: T): T[0] {
|
|
709
|
-
for (const c of cls) if (c) (c.prototype as any)[nonReactiveMark] = true
|
|
710
|
-
return cls[0]
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
nonReactiveClass(Date, RegExp, Error, Promise, Function)
|
|
714
|
-
if (typeof window !== 'undefined') nonReactive(window, document)
|
|
715
|
-
if (typeof Element !== 'undefined') nonReactiveClass(Element, Node)
|
|
716
|
-
|
|
717
|
-
export function registerNativeReactivity(
|
|
718
|
-
originalClass: new (...args: any[]) => any,
|
|
719
|
-
reactiveClass: new (...args: any[]) => any
|
|
720
|
-
) {
|
|
721
|
-
originalClass.prototype[nativeReactive] = reactiveClass
|
|
722
|
-
nonReactiveClass(reactiveClass)
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/**
|
|
726
|
-
* Deep watch an object and all its nested properties
|
|
727
|
-
* @param target - The object to watch deeply
|
|
728
|
-
* @param callback - The callback to call when any nested property changes
|
|
729
|
-
* @param options - Options for the deep watch
|
|
730
|
-
* @returns A cleanup function to stop watching
|
|
731
|
-
*/
|
|
732
|
-
export function deepWatch<T extends object>(
|
|
733
|
-
target: T,
|
|
734
|
-
callback: (value: T) => void,
|
|
735
|
-
{ immediate = false } = {}
|
|
736
|
-
): (() => void) | undefined {
|
|
737
|
-
if (target === null || target === undefined) return undefined
|
|
738
|
-
if (typeof target !== 'object') throw new Error('Target of deep watching must be an object')
|
|
739
|
-
// Create a wrapper callback that matches ScopedCallback signature
|
|
740
|
-
const wrappedCallback: ScopedCallback = markWithRoot(() => callback(target), callback)
|
|
741
|
-
|
|
742
|
-
// Use the existing effect system to register dependencies
|
|
743
|
-
return effect(() => {
|
|
744
|
-
// Mark the target object as having deep watchers
|
|
745
|
-
objectsWithDeepWatchers.add(target)
|
|
746
|
-
|
|
747
|
-
// Track which objects this effect is watching for cleanup
|
|
748
|
-
let effectObjects = effectToDeepWatchedObjects.get(wrappedCallback)
|
|
749
|
-
if (!effectObjects) {
|
|
750
|
-
effectObjects = new Set()
|
|
751
|
-
effectToDeepWatchedObjects.set(wrappedCallback, effectObjects)
|
|
752
|
-
}
|
|
753
|
-
effectObjects!.add(target)
|
|
754
|
-
|
|
755
|
-
// Traverse the object graph and register dependencies
|
|
756
|
-
// This will re-run every time the effect runs, ensuring we catch all changes
|
|
757
|
-
const visited = new WeakSet()
|
|
758
|
-
function traverseAndTrack(obj: any, depth = 0) {
|
|
759
|
-
// Prevent infinite recursion and excessive depth
|
|
760
|
-
if (visited.has(obj) || !isObject(obj) || depth > options.maxDeepWatchDepth) return
|
|
761
|
-
// Do not traverse into unreactive objects
|
|
762
|
-
if (isNonReactive(obj)) return
|
|
763
|
-
visited.add(obj)
|
|
764
|
-
|
|
765
|
-
// Mark this object as having deep watchers
|
|
766
|
-
objectsWithDeepWatchers.add(obj)
|
|
767
|
-
effectObjects!.add(obj)
|
|
768
|
-
|
|
769
|
-
// Traverse all properties to register dependencies
|
|
770
|
-
// unwrap to avoid kicking dependency
|
|
771
|
-
for (const key in unwrap(obj)) {
|
|
772
|
-
if (Object.hasOwn(obj, key)) {
|
|
773
|
-
// Access the property to register dependency
|
|
774
|
-
const value = (obj as any)[key]
|
|
775
|
-
// Make the value reactive if it's an object
|
|
776
|
-
const reactiveValue =
|
|
777
|
-
typeof value === 'object' && value !== null ? reactive(value) : value
|
|
778
|
-
traverseAndTrack(reactiveValue, depth + 1)
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// Also handle array indices and length
|
|
783
|
-
// biome-ignore lint/suspicious/useIsArray: Check for both native arrays and reactive arrays
|
|
784
|
-
if (Array.isArray(obj) || obj instanceof Array) {
|
|
785
|
-
// Access array length to register dependency on length changes
|
|
786
|
-
const length = obj.length
|
|
787
|
-
|
|
788
|
-
// Access all current array elements to register dependencies
|
|
789
|
-
for (let i = 0; i < length; i++) {
|
|
790
|
-
// Access the array element to register dependency
|
|
791
|
-
const value = obj[i]
|
|
792
|
-
// Make the value reactive if it's an object
|
|
793
|
-
const reactiveValue =
|
|
794
|
-
typeof value === 'object' && value !== null ? reactive(value) : value
|
|
795
|
-
traverseAndTrack(reactiveValue, depth + 1)
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
// Handle Set values (deep watch values only, not keys since Sets don't have separate keys)
|
|
799
|
-
else if (obj instanceof Set) {
|
|
800
|
-
// Access all Set values to register dependencies
|
|
801
|
-
for (const value of obj) {
|
|
802
|
-
// Make the value reactive if it's an object
|
|
803
|
-
const reactiveValue =
|
|
804
|
-
typeof value === 'object' && value !== null ? reactive(value) : value
|
|
805
|
-
traverseAndTrack(reactiveValue, depth + 1)
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
// Handle Map values (deep watch values only, not keys)
|
|
809
|
-
else if (obj instanceof Map) {
|
|
810
|
-
// Access all Map values to register dependencies
|
|
811
|
-
for (const [_key, value] of obj) {
|
|
812
|
-
// Make the value reactive if it's an object
|
|
813
|
-
const reactiveValue =
|
|
814
|
-
typeof value === 'object' && value !== null ? reactive(value) : value
|
|
815
|
-
traverseAndTrack(reactiveValue, depth + 1)
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
// Note: WeakSet and WeakMap cannot be iterated, so we can't deep watch their contents
|
|
819
|
-
// They will only trigger when the collection itself is replaced
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// Traverse the target object to register all dependencies
|
|
823
|
-
// This will register dependencies on all current properties and array elements
|
|
824
|
-
traverseAndTrack(target)
|
|
825
|
-
|
|
826
|
-
// Only call the callback if immediate is true or if it's not the first run
|
|
827
|
-
if (immediate) callback(target)
|
|
828
|
-
immediate = true
|
|
829
|
-
|
|
830
|
-
// Return a cleanup function that properly removes deep watcher tracking
|
|
831
|
-
return () => {
|
|
832
|
-
// Get the objects this effect was watching
|
|
833
|
-
const effectObjects = effectToDeepWatchedObjects.get(wrappedCallback)
|
|
834
|
-
if (effectObjects) {
|
|
835
|
-
// Remove deep watcher tracking from all objects this effect was watching
|
|
836
|
-
for (const obj of effectObjects) {
|
|
837
|
-
// Check if this object still has other deep watchers
|
|
838
|
-
const watchers = deepWatchers.get(obj)
|
|
839
|
-
if (watchers) {
|
|
840
|
-
// Remove this effect's callback from the watchers
|
|
841
|
-
watchers.delete(wrappedCallback)
|
|
842
|
-
|
|
843
|
-
// If no more watchers, remove the object from deep watchers tracking
|
|
844
|
-
if (watchers.size === 0) {
|
|
845
|
-
deepWatchers.delete(obj)
|
|
846
|
-
objectsWithDeepWatchers.delete(obj)
|
|
847
|
-
}
|
|
848
|
-
} else {
|
|
849
|
-
// No watchers found, remove from deep watchers tracking
|
|
850
|
-
objectsWithDeepWatchers.delete(obj)
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// Clean up the tracking data
|
|
855
|
-
effectToDeepWatchedObjects.delete(wrappedCallback)
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
})
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
/**
|
|
862
|
-
* Check if an object is an object (not null, not primitive)
|
|
863
|
-
*/
|
|
864
|
-
function isObject(obj: any): obj is object {
|
|
865
|
-
return obj !== null && typeof obj === 'object'
|
|
866
|
-
}
|