mutts 1.0.9 → 1.0.11
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 +60 -35
- package/dist/browser.cjs +1333 -1760
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +2 -1392
- package/dist/browser.dev.cjs +114 -0
- package/dist/browser.dev.cjs.map +1 -0
- package/dist/browser.dev.d.ts +2 -0
- package/dist/browser.dev.esm.js +5 -0
- package/dist/browser.dev.esm.js.map +1 -0
- package/dist/browser.esm.js +18 -97
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{async-browser-lvzLOCgk.cjs → async-browser-Dgr5CreQ.cjs} +16 -95
- package/dist/chunks/async-browser-Dgr5CreQ.cjs.map +1 -0
- package/dist/chunks/{async-node-C3DeIb0y.cjs → async-node-3PrbVAbB.cjs} +3 -1
- package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
- package/dist/chunks/index-Sf74wXTV.esm.js +2577 -0
- package/dist/chunks/index-Sf74wXTV.esm.js.map +1 -0
- package/dist/chunks/node-Bo7WU5S2.esm.js +96 -0
- package/dist/chunks/node-Bo7WU5S2.esm.js.map +1 -0
- package/dist/chunks/{index-VTO-b2vR.cjs → proxy-Cc79Lrzj.cjs} +2593 -3216
- package/dist/chunks/proxy-Cc79Lrzj.cjs.map +1 -0
- package/dist/chunks/{index-CtA2AWl3.esm.js → proxy-D2C49sXH.esm.js} +2570 -3174
- package/dist/chunks/proxy-D2C49sXH.esm.js.map +1 -0
- package/dist/debug.cjs +987 -28
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.d.ts +37 -38
- package/dist/debug.esm.js +963 -1
- package/dist/debug.esm.js.map +1 -1
- package/dist/devtools/manifest.json +1 -1
- package/dist/devtools/panel.html +1 -1
- package/dist/devtools/panel.js +107 -94
- package/dist/devtools/panel.js.map +1 -1
- package/dist/index.d.ts +1322 -1
- package/dist/mutts.umd.js +6647 -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/node.cjs +69 -63
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.dev.cjs +114 -0
- package/dist/node.dev.cjs.map +1 -0
- package/dist/node.dev.d.ts +2 -0
- package/dist/node.dev.esm.js +6 -0
- package/dist/node.dev.esm.js.map +1 -0
- package/dist/node.esm.js +4 -97
- package/dist/node.esm.js.map +1 -1
- package/dist/{types-DaHFfhlN.d.ts → types-Bx2PhORg.d.ts} +134 -88
- package/docs/ai/api-reference.md +11 -32
- package/docs/ai/manual.md +297 -239
- package/docs/reactive/advanced.md +318 -9
- package/docs/reactive/attend.md +2 -4
- package/docs/reactive/collections.md +22 -187
- package/docs/reactive/core.md +223 -131
- package/docs/reactive/debugging.md +119 -12
- package/docs/reactive/error-handling.md +10 -10
- package/docs/reactive/resource.md +125 -0
- package/docs/reactive.md +3 -4
- package/docs/utils.md +70 -0
- package/docs/zone.md +1 -1
- package/package.json +76 -38
- package/dist/chunks/async-browser-lvzLOCgk.cjs.map +0 -1
- package/dist/chunks/async-node-C3DeIb0y.cjs.map +0 -1
- package/dist/chunks/index-2vea86wD.esm.js +0 -3011
- package/dist/chunks/index-2vea86wD.esm.js.map +0 -1
- package/dist/chunks/index-CtA2AWl3.esm.js.map +0 -1
- package/dist/chunks/index-VTO-b2vR.cjs.map +0 -1
- package/dist/debug/debug.d.ts +0 -122
- package/dist/debug/debug.d.ts.map +0 -1
- package/dist/debug/index.d.ts +0 -4
- package/dist/debug/index.d.ts.map +0 -1
- package/dist/debug/lineage-panel.d.ts +0 -5
- package/dist/debug/lineage-panel.d.ts.map +0 -1
- package/dist/debug/lineage.d.ts +0 -79
- package/dist/debug/lineage.d.ts.map +0 -1
- package/dist/src/async/browser.d.ts +0 -2
- package/dist/src/async/browser.d.ts.map +0 -1
- package/dist/src/async/index.d.ts +0 -19
- package/dist/src/async/index.d.ts.map +0 -1
- package/dist/src/async/node.d.ts +0 -2
- package/dist/src/async/node.d.ts.map +0 -1
- package/dist/src/decorator.d.ts +0 -106
- package/dist/src/decorator.d.ts.map +0 -1
- package/dist/src/destroyable.d.ts +0 -87
- package/dist/src/destroyable.d.ts.map +0 -1
- package/dist/src/entry-browser.d.ts +0 -3
- package/dist/src/entry-browser.d.ts.map +0 -1
- package/dist/src/entry-node.d.ts +0 -3
- package/dist/src/entry-node.d.ts.map +0 -1
- package/dist/src/eventful.d.ts +0 -20
- package/dist/src/eventful.d.ts.map +0 -1
- package/dist/src/flavored.d.ts +0 -33
- package/dist/src/flavored.d.ts.map +0 -1
- package/dist/src/index.d.ts +0 -14
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/indexable.d.ts +0 -243
- package/dist/src/indexable.d.ts.map +0 -1
- package/dist/src/introspection.d.ts +0 -27
- package/dist/src/introspection.d.ts.map +0 -1
- package/dist/src/iterableWeak.d.ts +0 -53
- package/dist/src/iterableWeak.d.ts.map +0 -1
- package/dist/src/mixins.d.ts +0 -25
- package/dist/src/mixins.d.ts.map +0 -1
- package/dist/src/promiseChain.d.ts +0 -20
- package/dist/src/promiseChain.d.ts.map +0 -1
- package/dist/src/reactive/array.d.ts +0 -48
- package/dist/src/reactive/array.d.ts.map +0 -1
- package/dist/src/reactive/buffer.d.ts +0 -120
- package/dist/src/reactive/buffer.d.ts.map +0 -1
- package/dist/src/reactive/change.d.ts +0 -29
- package/dist/src/reactive/change.d.ts.map +0 -1
- package/dist/src/reactive/deep-touch.d.ts +0 -28
- package/dist/src/reactive/deep-touch.d.ts.map +0 -1
- package/dist/src/reactive/deep-watch-state.d.ts +0 -25
- package/dist/src/reactive/deep-watch-state.d.ts.map +0 -1
- package/dist/src/reactive/deep-watch.d.ts +0 -20
- package/dist/src/reactive/deep-watch.d.ts.map +0 -1
- package/dist/src/reactive/describe.d.ts +0 -12
- package/dist/src/reactive/describe.d.ts.map +0 -1
- package/dist/src/reactive/effect-context.d.ts +0 -34
- package/dist/src/reactive/effect-context.d.ts.map +0 -1
- package/dist/src/reactive/effects.d.ts +0 -164
- package/dist/src/reactive/effects.d.ts.map +0 -1
- package/dist/src/reactive/index.d.ts +0 -19
- package/dist/src/reactive/index.d.ts.map +0 -1
- package/dist/src/reactive/map.d.ts +0 -28
- package/dist/src/reactive/map.d.ts.map +0 -1
- package/dist/src/reactive/memoize.d.ts +0 -28
- package/dist/src/reactive/memoize.d.ts.map +0 -1
- package/dist/src/reactive/non-reactive-state.d.ts +0 -9
- package/dist/src/reactive/non-reactive-state.d.ts.map +0 -1
- package/dist/src/reactive/non-reactive.d.ts +0 -11
- package/dist/src/reactive/non-reactive.d.ts.map +0 -1
- package/dist/src/reactive/project.d.ts +0 -40
- package/dist/src/reactive/project.d.ts.map +0 -1
- package/dist/src/reactive/proxy-state.d.ts +0 -8
- package/dist/src/reactive/proxy-state.d.ts.map +0 -1
- package/dist/src/reactive/proxy.d.ts +0 -23
- package/dist/src/reactive/proxy.d.ts.map +0 -1
- package/dist/src/reactive/record.d.ts +0 -115
- package/dist/src/reactive/record.d.ts.map +0 -1
- package/dist/src/reactive/register.d.ts +0 -125
- package/dist/src/reactive/register.d.ts.map +0 -1
- package/dist/src/reactive/registry.d.ts +0 -21
- package/dist/src/reactive/registry.d.ts.map +0 -1
- package/dist/src/reactive/set.d.ts +0 -26
- package/dist/src/reactive/set.d.ts.map +0 -1
- package/dist/src/reactive/tracking.d.ts +0 -7
- package/dist/src/reactive/tracking.d.ts.map +0 -1
- package/dist/src/reactive/types.d.ts +0 -424
- package/dist/src/reactive/types.d.ts.map +0 -1
- package/dist/src/reactive/watch.d.ts +0 -48
- package/dist/src/reactive/watch.d.ts.map +0 -1
- package/dist/src/std-decorators.d.ts +0 -45
- package/dist/src/std-decorators.d.ts.map +0 -1
- package/dist/src/utils.d.ts +0 -49
- package/dist/src/utils.d.ts.map +0 -1
- package/dist/src/zone.d.ts +0 -40
- package/dist/src/zone.d.ts.map +0 -1
- package/docs/reactive/describe.md +0 -85
- package/docs/reactive/project.md +0 -93
- package/docs/reactive/scan.md +0 -293
- package/src/async/browser.ts +0 -323
- package/src/async/index.ts +0 -27
- package/src/async/node.ts +0 -92
- package/src/decorator.ts +0 -272
- package/src/destroyable.ts +0 -199
- package/src/entry-browser.ts +0 -5
- package/src/entry-node.ts +0 -5
- package/src/eventful.ts +0 -110
- package/src/flavored.ts +0 -106
- package/src/index.d.ts +0 -12
- package/src/index.ts +0 -64
- package/src/indexable.ts +0 -526
- package/src/introspection.ts +0 -59
- package/src/iterableWeak.ts +0 -233
- package/src/mixins.ts +0 -123
- package/src/promiseChain.ts +0 -110
- package/src/reactive/array.ts +0 -500
- package/src/reactive/buffer.ts +0 -328
- package/src/reactive/change.ts +0 -131
- package/src/reactive/deep-touch.ts +0 -273
- package/src/reactive/deep-watch-state.ts +0 -82
- package/src/reactive/deep-watch.ts +0 -171
- package/src/reactive/describe.ts +0 -39
- package/src/reactive/effect-context.ts +0 -83
- package/src/reactive/effects.ts +0 -1434
- package/src/reactive/index.ts +0 -72
- package/src/reactive/map.ts +0 -142
- package/src/reactive/memoize.ts +0 -186
- package/src/reactive/non-reactive-state.ts +0 -49
- package/src/reactive/non-reactive.ts +0 -43
- package/src/reactive/project.md +0 -107
- package/src/reactive/project.ts +0 -430
- package/src/reactive/proxy-state.ts +0 -27
- package/src/reactive/proxy.ts +0 -282
- package/src/reactive/record.ts +0 -181
- package/src/reactive/register.ts +0 -538
- package/src/reactive/registry.ts +0 -72
- package/src/reactive/set.ts +0 -117
- package/src/reactive/tracking.ts +0 -41
- package/src/reactive/types.ts +0 -520
- package/src/reactive/watch.ts +0 -180
- package/src/std-decorators.ts +0 -256
- package/src/utils.ts +0 -300
- package/src/zone.ts +0 -142
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { addState, collectEffects, touched1, touchedOpaque } from './change'
|
|
2
|
-
import { bubbleUpChange, objectsWithDeepWatchers } from './deep-watch-state'
|
|
3
|
-
import { batch } from './effects'
|
|
4
|
-
import { isNonReactive } from './non-reactive-state'
|
|
5
|
-
import { unwrap } from './proxy-state'
|
|
6
|
-
import { getEffectNode, watchers } from './registry'
|
|
7
|
-
import { allProps, type EffectCleanup, type EffectTrigger, type Evolution, optionCall, options } from './types'
|
|
8
|
-
|
|
9
|
-
function isObject(value: any): value is object {
|
|
10
|
-
return typeof value === 'object' && value !== null
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function isObjectLike(value: unknown): value is object {
|
|
14
|
-
return isObject(value)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function getPrototypeToken(value: any): object | null | undefined {
|
|
18
|
-
if (!isObjectLike(value)) return undefined
|
|
19
|
-
if (Array.isArray(value)) return Array.prototype
|
|
20
|
-
try {
|
|
21
|
-
return value.constructor
|
|
22
|
-
} catch {
|
|
23
|
-
return undefined
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function shouldRecurseTouch(oldValue: any, newValue: any): boolean {
|
|
28
|
-
if (oldValue === newValue) return false
|
|
29
|
-
if (!isObjectLike(oldValue) || !isObjectLike(newValue)) return false
|
|
30
|
-
if (isNonReactive(oldValue) || isNonReactive(newValue)) return false
|
|
31
|
-
return getPrototypeToken(oldValue) === getPrototypeToken(newValue)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Centralized function to handle property change notifications with optional recursive touch
|
|
36
|
-
* @param targetObj - The object whose property changed
|
|
37
|
-
* @param prop - The property that changed
|
|
38
|
-
* @param oldValue - The old value (before change)
|
|
39
|
-
* @param newValue - The new value (after change)
|
|
40
|
-
* @param hadProperty - Whether the property existed before (for add vs set)
|
|
41
|
-
*/
|
|
42
|
-
export function notifyPropertyChange(
|
|
43
|
-
targetObj: any,
|
|
44
|
-
prop: any,
|
|
45
|
-
oldValue: any,
|
|
46
|
-
newValue: any,
|
|
47
|
-
hadProperty: boolean
|
|
48
|
-
) {
|
|
49
|
-
const evolution: Evolution = { type: hadProperty ? 'set' : 'add', prop }
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
options.recursiveTouching &&
|
|
53
|
-
oldValue !== undefined &&
|
|
54
|
-
shouldRecurseTouch(oldValue, newValue)
|
|
55
|
-
) {
|
|
56
|
-
const unwrappedObj = unwrap(targetObj)
|
|
57
|
-
const origin = { obj: unwrappedObj, prop }
|
|
58
|
-
// Deep touch: only notify nested property changes with origin filtering
|
|
59
|
-
// Don't notify direct property change - the whole point is to avoid parent effects re-running
|
|
60
|
-
dispatchNotifications(recursiveTouch(oldValue, newValue, new WeakMap(), [], origin))
|
|
61
|
-
|
|
62
|
-
// Notify opaque listeners (like memoize) that always want to know about identity changes
|
|
63
|
-
touchedOpaque(targetObj, evolution, prop)
|
|
64
|
-
} else {
|
|
65
|
-
touched1(targetObj, evolution, prop)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
type VisitedPairs = WeakMap<object, WeakSet<object>>
|
|
70
|
-
type PendingNotification = {
|
|
71
|
-
target: any
|
|
72
|
-
evolution: Evolution
|
|
73
|
-
prop: any
|
|
74
|
-
origin?: { obj: object; prop: PropertyKey } // The property access that triggered this deep touch
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function hasVisitedPair(visited: VisitedPairs, oldObj: object, newObj: object): boolean {
|
|
78
|
-
let mapped = visited.get(oldObj)
|
|
79
|
-
if (!mapped) {
|
|
80
|
-
mapped = new WeakSet<object>()
|
|
81
|
-
visited.set(oldObj, mapped)
|
|
82
|
-
}
|
|
83
|
-
if (mapped.has(newObj)) return true
|
|
84
|
-
mapped.add(newObj)
|
|
85
|
-
return false
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function collectObjectKeys(obj: any): Set<PropertyKey> {
|
|
89
|
-
const keys = new Set<PropertyKey>(Reflect.ownKeys(obj))
|
|
90
|
-
let proto = Object.getPrototypeOf(obj)
|
|
91
|
-
// Continue walking while prototype exists and doesn't have its own constructor
|
|
92
|
-
// This stops at Object.prototype (has own constructor) and class prototypes (have own constructor)
|
|
93
|
-
// but continues for data prototypes (Object.create({}), Object.create(instance), etc.)
|
|
94
|
-
while (proto && !Object.hasOwn(proto, 'constructor')) {
|
|
95
|
-
for (const key of Reflect.ownKeys(proto)) keys.add(key)
|
|
96
|
-
proto = Object.getPrototypeOf(proto)
|
|
97
|
-
}
|
|
98
|
-
return keys
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function recursiveTouch(
|
|
102
|
-
oldValue: any,
|
|
103
|
-
newValue: any,
|
|
104
|
-
visited: VisitedPairs = new WeakMap(),
|
|
105
|
-
notifications: PendingNotification[] = [],
|
|
106
|
-
origin?: { obj: object; prop: PropertyKey }
|
|
107
|
-
): PendingNotification[] {
|
|
108
|
-
if (!shouldRecurseTouch(oldValue, newValue)) return notifications
|
|
109
|
-
if (!isObjectLike(oldValue) || !isObjectLike(newValue)) return notifications
|
|
110
|
-
if (hasVisitedPair(visited, oldValue, newValue)) return notifications
|
|
111
|
-
|
|
112
|
-
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
|
113
|
-
diffArrayElements(oldValue, newValue, visited, notifications, origin)
|
|
114
|
-
return notifications
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
diffObjectProperties(oldValue, newValue, visited, notifications, origin)
|
|
118
|
-
return notifications
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function diffArrayElements(
|
|
122
|
-
oldArray: any[] | readonly any[],
|
|
123
|
-
newArray: any[] | readonly any[],
|
|
124
|
-
_visited: VisitedPairs,
|
|
125
|
-
notifications: PendingNotification[],
|
|
126
|
-
origin?: { obj: object; prop: PropertyKey }
|
|
127
|
-
) {
|
|
128
|
-
const local: PendingNotification[] = []
|
|
129
|
-
const oldLength = oldArray.length
|
|
130
|
-
const newLength = newArray.length
|
|
131
|
-
const max = Math.max(oldLength, newLength)
|
|
132
|
-
|
|
133
|
-
for (let index = 0; index < max; index++) {
|
|
134
|
-
const hasOld = index < oldLength
|
|
135
|
-
const hasNew = index < newLength
|
|
136
|
-
if (hasOld && !hasNew) {
|
|
137
|
-
local.push({ target: oldArray, evolution: { type: 'del', prop: index }, prop: index, origin })
|
|
138
|
-
continue
|
|
139
|
-
}
|
|
140
|
-
if (!hasOld && hasNew) {
|
|
141
|
-
local.push({ target: oldArray, evolution: { type: 'add', prop: index }, prop: index, origin })
|
|
142
|
-
continue
|
|
143
|
-
}
|
|
144
|
-
if (!hasOld || !hasNew) continue
|
|
145
|
-
const oldEntry = unwrap(oldArray[index])
|
|
146
|
-
const newEntry = unwrap(newArray[index])
|
|
147
|
-
if (!Object.is(oldEntry, newEntry)) {
|
|
148
|
-
local.push({ target: oldArray, evolution: { type: 'set', prop: index }, prop: index, origin })
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (oldLength !== newLength)
|
|
153
|
-
local.push({
|
|
154
|
-
target: oldArray,
|
|
155
|
-
evolution: { type: 'set', prop: 'length' },
|
|
156
|
-
prop: 'length',
|
|
157
|
-
origin,
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
notifications.push(...local)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function diffObjectProperties(
|
|
164
|
-
oldObj: any,
|
|
165
|
-
newObj: any,
|
|
166
|
-
visited: VisitedPairs,
|
|
167
|
-
notifications: PendingNotification[],
|
|
168
|
-
origin?: { obj: object; prop: PropertyKey }
|
|
169
|
-
) {
|
|
170
|
-
const oldKeys = collectObjectKeys(oldObj)
|
|
171
|
-
const newKeys = collectObjectKeys(newObj)
|
|
172
|
-
const local: PendingNotification[] = []
|
|
173
|
-
|
|
174
|
-
for (const key of oldKeys)
|
|
175
|
-
if (!newKeys.has(key))
|
|
176
|
-
local.push({ target: oldObj, evolution: { type: 'del', prop: key }, prop: key, origin })
|
|
177
|
-
|
|
178
|
-
for (const key of newKeys)
|
|
179
|
-
if (!oldKeys.has(key))
|
|
180
|
-
local.push({ target: oldObj, evolution: { type: 'add', prop: key }, prop: key, origin })
|
|
181
|
-
|
|
182
|
-
for (const key of newKeys) {
|
|
183
|
-
if (!oldKeys.has(key)) continue
|
|
184
|
-
const oldEntry = unwrap((oldObj as any)[key])
|
|
185
|
-
const newEntry = unwrap((newObj as any)[key])
|
|
186
|
-
if (shouldRecurseTouch(oldEntry, newEntry)) {
|
|
187
|
-
recursiveTouch(oldEntry, newEntry, visited, notifications, origin)
|
|
188
|
-
} else if (!Object.is(oldEntry, newEntry)) {
|
|
189
|
-
local.push({ target: oldObj, evolution: { type: 'set', prop: key }, prop: key, origin })
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
notifications.push(...local)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Checks if an effect or any of its ancestors is in the allowed set
|
|
198
|
-
*/
|
|
199
|
-
function hasAncestorInSet(
|
|
200
|
-
effect: EffectTrigger | EffectCleanup,
|
|
201
|
-
allowedSet: Set<EffectTrigger | EffectCleanup>
|
|
202
|
-
): boolean {
|
|
203
|
-
let current: EffectTrigger | EffectCleanup | undefined = effect
|
|
204
|
-
const visited = new WeakSet<EffectTrigger | EffectCleanup>()
|
|
205
|
-
while (current && !visited.has(current)) {
|
|
206
|
-
visited.add(current)
|
|
207
|
-
if (allowedSet.has(current)) return true
|
|
208
|
-
const node = getEffectNode(current as EffectTrigger)
|
|
209
|
-
current = node.parent
|
|
210
|
-
}
|
|
211
|
-
return false
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function dispatchNotifications(notifications: PendingNotification[]) {
|
|
215
|
-
if (!notifications.length) return
|
|
216
|
-
const combinedEffects = new Set<EffectTrigger>()
|
|
217
|
-
|
|
218
|
-
// Extract origin from first notification (all should have the same origin from a single deep touch)
|
|
219
|
-
const origin = notifications[0]?.origin
|
|
220
|
-
let allowedEffects: Set<EffectTrigger> | undefined
|
|
221
|
-
|
|
222
|
-
// If origin exists, compute allowed effects (those that depend on origin.obj[origin.prop])
|
|
223
|
-
if (origin) {
|
|
224
|
-
allowedEffects = new Set<EffectTrigger>()
|
|
225
|
-
const originWatchers = watchers.get(origin.obj)
|
|
226
|
-
if (originWatchers) {
|
|
227
|
-
const originEffects = new Set<EffectTrigger>()
|
|
228
|
-
collectEffects(
|
|
229
|
-
origin.obj,
|
|
230
|
-
{ type: 'set', prop: origin.prop },
|
|
231
|
-
originEffects,
|
|
232
|
-
originWatchers,
|
|
233
|
-
[allProps],
|
|
234
|
-
[origin.prop]
|
|
235
|
-
)
|
|
236
|
-
for (const effect of originEffects) allowedEffects.add(effect)
|
|
237
|
-
}
|
|
238
|
-
// If no allowed effects, skip all notifications (no one should be notified)
|
|
239
|
-
if (allowedEffects.size === 0) return
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
for (const { target, evolution, prop } of notifications) {
|
|
243
|
-
if (!isObjectLike(target)) continue
|
|
244
|
-
const obj = unwrap(target)
|
|
245
|
-
addState(obj, evolution)
|
|
246
|
-
const objectWatchers = watchers.get(obj)
|
|
247
|
-
let currentEffects: Set<EffectTrigger> | undefined
|
|
248
|
-
const propsArray = [prop]
|
|
249
|
-
if (objectWatchers) {
|
|
250
|
-
// console.log(`[DEBUG] dispatchNotifications: processing ${obj.constructor.name} (has watchers)`)
|
|
251
|
-
currentEffects = new Set<EffectTrigger>()
|
|
252
|
-
collectEffects(obj, evolution, currentEffects, objectWatchers, [allProps], propsArray)
|
|
253
|
-
|
|
254
|
-
// Filter effects by ancestor chain if origin exists
|
|
255
|
-
// Include effects that either directly depend on origin or have an ancestor that does
|
|
256
|
-
if (origin && allowedEffects) {
|
|
257
|
-
const filteredEffects = new Set<EffectTrigger>()
|
|
258
|
-
for (const effect of currentEffects) {
|
|
259
|
-
// Check if effect itself is allowed OR has an ancestor that is allowed
|
|
260
|
-
if (allowedEffects.has(effect) || hasAncestorInSet(effect, allowedEffects)) {
|
|
261
|
-
filteredEffects.add(effect)
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
currentEffects = filteredEffects
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
for (const effect of currentEffects) combinedEffects.add(effect)
|
|
268
|
-
}
|
|
269
|
-
optionCall('touched', obj, evolution, propsArray, currentEffects)
|
|
270
|
-
if (objectsWithDeepWatchers.has(obj)) bubbleUpChange(obj, evolution)
|
|
271
|
-
}
|
|
272
|
-
if (combinedEffects.size) batch([...combinedEffects])
|
|
273
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { batch } from './effects'
|
|
2
|
-
import type { EffectTrigger, Evolution } from './types'
|
|
3
|
-
|
|
4
|
-
// Track which objects contain which other objects (back-references)
|
|
5
|
-
export const objectParents = new WeakMap<object, Set<{ parent: object; prop: PropertyKey }>>()
|
|
6
|
-
|
|
7
|
-
// Track which objects have deep watchers
|
|
8
|
-
export const objectsWithDeepWatchers = new WeakSet<object>()
|
|
9
|
-
|
|
10
|
-
// Track deep watchers per object
|
|
11
|
-
export const deepWatchers = new WeakMap<object, Set<EffectTrigger>>()
|
|
12
|
-
|
|
13
|
-
// Track which effects are doing deep watching
|
|
14
|
-
export const effectToDeepWatchedObjects = new WeakMap<EffectTrigger, Set<object>>()
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Add a back-reference from child to parent
|
|
18
|
-
*/
|
|
19
|
-
export function addBackReference(child: object, parent: object, prop: any) {
|
|
20
|
-
let parents = objectParents.get(child)
|
|
21
|
-
if (!parents) {
|
|
22
|
-
parents = new Set()
|
|
23
|
-
objectParents.set(child, parents)
|
|
24
|
-
}
|
|
25
|
-
parents.add({ parent, prop })
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Remove a back-reference from child to parent
|
|
30
|
-
*/
|
|
31
|
-
export function removeBackReference(child: object, parent: object, prop: any) {
|
|
32
|
-
const parents = objectParents.get(child)
|
|
33
|
-
if (parents) {
|
|
34
|
-
for (const entry of parents) {
|
|
35
|
-
if (entry.parent === parent && entry.prop === prop) {
|
|
36
|
-
parents.delete(entry)
|
|
37
|
-
break
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (parents.size === 0) {
|
|
41
|
-
objectParents.delete(child)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Check if an object needs back-references (has deep watchers or parents with deep watchers)
|
|
48
|
-
*/
|
|
49
|
-
export function needsBackReferences(obj: object): boolean {
|
|
50
|
-
// Fast path: check if object itself has deep watchers
|
|
51
|
-
if (objectsWithDeepWatchers.has(obj)) return true
|
|
52
|
-
// Slow path: check if any parent has deep watchers (recursive)
|
|
53
|
-
return hasParentWithDeepWatchers(obj)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Bubble up changes through the back-reference chain
|
|
58
|
-
*/
|
|
59
|
-
export function bubbleUpChange(changedObject: object, evolution: Evolution) {
|
|
60
|
-
const parents = objectParents.get(changedObject)
|
|
61
|
-
if (!parents) return
|
|
62
|
-
|
|
63
|
-
for (const { parent } of parents) {
|
|
64
|
-
// Trigger deep watchers on parent
|
|
65
|
-
const parentDeepWatchers = deepWatchers.get(parent)
|
|
66
|
-
if (parentDeepWatchers) for (const watcher of parentDeepWatchers) batch(watcher)
|
|
67
|
-
|
|
68
|
-
// Continue bubbling up
|
|
69
|
-
bubbleUpChange(parent, evolution)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function hasParentWithDeepWatchers(obj: object): boolean {
|
|
74
|
-
const parents = objectParents.get(obj)
|
|
75
|
-
if (!parents) return false
|
|
76
|
-
|
|
77
|
-
for (const { parent } of parents) {
|
|
78
|
-
if (objectsWithDeepWatchers.has(parent)) return true
|
|
79
|
-
if (hasParentWithDeepWatchers(parent)) return true
|
|
80
|
-
}
|
|
81
|
-
return false
|
|
82
|
-
}
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
deepWatchers,
|
|
3
|
-
effectToDeepWatchedObjects,
|
|
4
|
-
objectsWithDeepWatchers,
|
|
5
|
-
} from './deep-watch-state'
|
|
6
|
-
import { effect } from './effects'
|
|
7
|
-
import { isNonReactive } from './non-reactive-state'
|
|
8
|
-
import { reactive, unwrap } from './proxy'
|
|
9
|
-
import { markWithRoot } from './registry'
|
|
10
|
-
import { type EffectCleanup, type EffectTrigger, options } from './types'
|
|
11
|
-
|
|
12
|
-
function isObject(value: any): value is object {
|
|
13
|
-
return typeof value === 'object' && value !== null
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export {
|
|
17
|
-
addBackReference,
|
|
18
|
-
bubbleUpChange,
|
|
19
|
-
deepWatchers,
|
|
20
|
-
effectToDeepWatchedObjects,
|
|
21
|
-
needsBackReferences,
|
|
22
|
-
objectParents,
|
|
23
|
-
objectsWithDeepWatchers,
|
|
24
|
-
removeBackReference,
|
|
25
|
-
} from './deep-watch-state'
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Deep watch an object and all its nested properties
|
|
29
|
-
* @param target - The object to watch deeply
|
|
30
|
-
* @param callback - The callback to call when any nested property changes
|
|
31
|
-
* @param options - Options for the deep watch
|
|
32
|
-
* @returns A cleanup function to stop watching
|
|
33
|
-
*/
|
|
34
|
-
/**
|
|
35
|
-
* Sets up deep watching for an object, tracking all nested property changes
|
|
36
|
-
* @param target - The object to watch
|
|
37
|
-
* @param callback - The callback to call when changes occur
|
|
38
|
-
* @param options - Options for deep watching
|
|
39
|
-
* @returns A cleanup function to stop deep watching
|
|
40
|
-
*/
|
|
41
|
-
export function deepWatch<T extends object>(
|
|
42
|
-
target: T,
|
|
43
|
-
callback: (value: T) => void,
|
|
44
|
-
{ immediate = false } = {}
|
|
45
|
-
): EffectCleanup | undefined {
|
|
46
|
-
if (target === null || target === undefined) return undefined
|
|
47
|
-
if (typeof target !== 'object') throw new Error('Target of deep watching must be an object')
|
|
48
|
-
// Create a wrapper callback that matches EffectTrigger signature
|
|
49
|
-
const wrappedCallback: EffectTrigger = markWithRoot(
|
|
50
|
-
(() => callback(target)) as EffectTrigger,
|
|
51
|
-
callback
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
// Use the existing effect system to register dependencies
|
|
55
|
-
return effect(() => {
|
|
56
|
-
// Mark the target object as having deep watchers
|
|
57
|
-
objectsWithDeepWatchers.add(target)
|
|
58
|
-
|
|
59
|
-
// Track which objects this effect is watching for cleanup
|
|
60
|
-
let effectObjects = effectToDeepWatchedObjects.get(wrappedCallback)
|
|
61
|
-
if (!effectObjects) {
|
|
62
|
-
effectObjects = new Set()
|
|
63
|
-
effectToDeepWatchedObjects.set(wrappedCallback, effectObjects)
|
|
64
|
-
}
|
|
65
|
-
effectObjects!.add(target)
|
|
66
|
-
|
|
67
|
-
// Traverse the object graph and register dependencies
|
|
68
|
-
// This will re-run every time the effect runs, ensuring we catch all changes
|
|
69
|
-
const visited = new WeakSet()
|
|
70
|
-
function traverseAndTrack(obj: any, depth = 0) {
|
|
71
|
-
// Prevent infinite recursion and excessive depth
|
|
72
|
-
if (!obj || visited.has(obj) || !isObject(obj) || depth > options.maxDeepWatchDepth) return
|
|
73
|
-
// Do not traverse into unreactive objects
|
|
74
|
-
if (isNonReactive(obj)) return
|
|
75
|
-
visited.add(obj)
|
|
76
|
-
|
|
77
|
-
// Mark this object as having deep watchers
|
|
78
|
-
objectsWithDeepWatchers.add(obj)
|
|
79
|
-
effectObjects!.add(obj)
|
|
80
|
-
|
|
81
|
-
// Traverse all properties to register dependencies
|
|
82
|
-
// unwrap to avoid kicking dependency
|
|
83
|
-
for (const key in unwrap(obj)) {
|
|
84
|
-
if (Object.hasOwn(obj, key)) {
|
|
85
|
-
// Access the property to register dependency
|
|
86
|
-
const value = (obj as any)[key]
|
|
87
|
-
// Make the value reactive if it's an object
|
|
88
|
-
const reactiveValue =
|
|
89
|
-
typeof value === 'object' && value !== null ? reactive(value) : value
|
|
90
|
-
traverseAndTrack(reactiveValue, depth + 1)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Also handle array indices and length
|
|
95
|
-
// biome-ignore lint/suspicious/useIsArray: Check for both native arrays and reactive arrays
|
|
96
|
-
if (Array.isArray(obj) || obj instanceof Array) {
|
|
97
|
-
// Access array length to register dependency on length changes
|
|
98
|
-
const length = obj.length
|
|
99
|
-
|
|
100
|
-
// Access all current array elements to register dependencies
|
|
101
|
-
for (let i = 0; i < length; i++) {
|
|
102
|
-
// Access the array element to register dependency
|
|
103
|
-
const value = obj[i]
|
|
104
|
-
// Make the value reactive if it's an object
|
|
105
|
-
const reactiveValue =
|
|
106
|
-
typeof value === 'object' && value !== null ? reactive(value) : value
|
|
107
|
-
traverseAndTrack(reactiveValue, depth + 1)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// Handle Set values (deep watch values only, not keys since Sets don't have separate keys)
|
|
111
|
-
else if (obj instanceof Set) {
|
|
112
|
-
// Access all Set values to register dependencies
|
|
113
|
-
for (const value of obj) {
|
|
114
|
-
// Make the value reactive if it's an object
|
|
115
|
-
const reactiveValue =
|
|
116
|
-
typeof value === 'object' && value !== null ? reactive(value) : value
|
|
117
|
-
traverseAndTrack(reactiveValue, depth + 1)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// Handle Map values (deep watch values only, not keys)
|
|
121
|
-
else if (obj instanceof Map) {
|
|
122
|
-
// Access all Map values to register dependencies
|
|
123
|
-
for (const [_key, value] of obj) {
|
|
124
|
-
// Make the value reactive if it's an object
|
|
125
|
-
const reactiveValue =
|
|
126
|
-
typeof value === 'object' && value !== null ? reactive(value) : value
|
|
127
|
-
traverseAndTrack(reactiveValue, depth + 1)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// Note: WeakSet and WeakMap cannot be iterated, so we can't deep watch their contents
|
|
131
|
-
// They will only trigger when the collection itself is replaced
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Traverse the target object to register all dependencies
|
|
135
|
-
// This will register dependencies on all current properties and array elements
|
|
136
|
-
traverseAndTrack(target)
|
|
137
|
-
|
|
138
|
-
// Only call the callback if immediate is true or if it's not the first run
|
|
139
|
-
if (immediate) callback(target)
|
|
140
|
-
immediate = true
|
|
141
|
-
|
|
142
|
-
// Return a cleanup function that properly removes deep watcher tracking
|
|
143
|
-
return () => {
|
|
144
|
-
// Get the objects this effect was watching
|
|
145
|
-
const effectObjects = effectToDeepWatchedObjects.get(wrappedCallback)
|
|
146
|
-
if (effectObjects) {
|
|
147
|
-
// Remove deep watcher tracking from all objects this effect was watching
|
|
148
|
-
for (const obj of effectObjects) {
|
|
149
|
-
// Check if this object still has other deep watchers
|
|
150
|
-
const watchers = deepWatchers.get(obj)
|
|
151
|
-
if (watchers) {
|
|
152
|
-
// Remove this effect's callback from the watchers
|
|
153
|
-
watchers.delete(wrappedCallback)
|
|
154
|
-
|
|
155
|
-
// If no more watchers, remove the object from deep watchers tracking
|
|
156
|
-
if (watchers.size === 0) {
|
|
157
|
-
deepWatchers.delete(obj)
|
|
158
|
-
objectsWithDeepWatchers.delete(obj)
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
// No watchers found, remove from deep watchers tracking
|
|
162
|
-
objectsWithDeepWatchers.delete(obj)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Clean up the tracking data
|
|
167
|
-
effectToDeepWatchedObjects.delete(wrappedCallback)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
})
|
|
171
|
-
}
|
package/src/reactive/describe.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { attend } from './buffer'
|
|
2
|
-
import { touched1 } from './change'
|
|
3
|
-
import { cleanedBy } from './effect-context'
|
|
4
|
-
import { reactive } from './proxy'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Reactively defines properties on a target object based on a descriptors record.
|
|
8
|
-
*
|
|
9
|
-
* It tracks the keys of the descriptors record and defines/updates/removes
|
|
10
|
-
* properties on the target object as they change.
|
|
11
|
-
*
|
|
12
|
-
* @param descriptors - A reactive record of property descriptors.
|
|
13
|
-
* @param target - The object to define properties on (defaults to a new object).
|
|
14
|
-
* @returns The target object with reactive property definitions.
|
|
15
|
-
*/
|
|
16
|
-
export function describe<T extends object>(
|
|
17
|
-
descriptors: Record<PropertyKey, PropertyDescriptor>,
|
|
18
|
-
target: T = Object.create(null) as T
|
|
19
|
-
): T {
|
|
20
|
-
descriptors = reactive(descriptors)
|
|
21
|
-
|
|
22
|
-
const stop = attend(
|
|
23
|
-
() => Reflect.ownKeys(descriptors),
|
|
24
|
-
(key) => {
|
|
25
|
-
const desc = (descriptors as any)[key]
|
|
26
|
-
if (desc) {
|
|
27
|
-
Object.defineProperty(target, key, {
|
|
28
|
-
enumerable: true,
|
|
29
|
-
...desc,
|
|
30
|
-
configurable: true,
|
|
31
|
-
})
|
|
32
|
-
touched1(target, { type: 'set', prop: key }, key)
|
|
33
|
-
}
|
|
34
|
-
return () => Reflect.deleteProperty(target, key)
|
|
35
|
-
}
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
return cleanedBy(target, stop)
|
|
39
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { tag } from '../utils'
|
|
2
|
-
import { asyncZone, ZoneAggregator, ZoneHistory } from '../zone'
|
|
3
|
-
import { effect, untracked } from './effects'
|
|
4
|
-
import { getRoot } from './registry'
|
|
5
|
-
import {
|
|
6
|
-
cleanup,
|
|
7
|
-
type EffectAccess,
|
|
8
|
-
type EffectTrigger,
|
|
9
|
-
type ScopedCallback,
|
|
10
|
-
stopped,
|
|
11
|
-
} from './types'
|
|
12
|
-
|
|
13
|
-
export const effectHistory = tag('effectHistory', new ZoneHistory<EffectTrigger>())
|
|
14
|
-
tag('effectHistory.present', effectHistory.present)
|
|
15
|
-
asyncZone.add(effectHistory)
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Aggregator for zones that need to be tracked along effects.
|
|
19
|
-
* ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
|
|
20
|
-
*/
|
|
21
|
-
export const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present))
|
|
22
|
-
|
|
23
|
-
export function isRunning(effect: EffectTrigger): boolean {
|
|
24
|
-
const root = getRoot(effect)
|
|
25
|
-
return effectHistory.some((e) => getRoot(e) === root)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function getActiveEffect() {
|
|
29
|
-
return effectHistory.present.active
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* ADD a cleanup function to an object using the cleanup symbol.
|
|
34
|
-
* The cleanup function will be called when the object needs to be disposed.
|
|
35
|
-
*
|
|
36
|
-
* Note: most of the time, you don't need to use this function directly.
|
|
37
|
-
* The main use if for the cleanup function to be stored with the object, as GC calls the cleanup function when the *function* is garbage collected.
|
|
38
|
-
*
|
|
39
|
-
* @param obj - The object to attach the cleanup function to
|
|
40
|
-
* @param cleanupFn - The cleanup function to attach
|
|
41
|
-
* @returns The object with the cleanup function attached
|
|
42
|
-
*/
|
|
43
|
-
export function cleanedBy<T extends object>(obj: T, cleanupFn: ScopedCallback) {
|
|
44
|
-
const oldCleanup = obj[cleanup]
|
|
45
|
-
return Object.defineProperty(obj, cleanup, {
|
|
46
|
-
value: oldCleanup
|
|
47
|
-
? Object.defineProperties(
|
|
48
|
-
() => {
|
|
49
|
-
oldCleanup()
|
|
50
|
-
cleanupFn()
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
[stopped]: { get: () => oldCleanup[stopped] || cleanupFn[stopped] },
|
|
54
|
-
}
|
|
55
|
-
)
|
|
56
|
-
: cleanupFn,
|
|
57
|
-
writable: false,
|
|
58
|
-
enumerable: false,
|
|
59
|
-
configurable: true,
|
|
60
|
-
}) as T & { [cleanup]: ScopedCallback }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
//#region greedy caching
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Creates a derived value that automatically recomputes when dependencies change
|
|
67
|
-
* @param compute - Function that computes the derived value
|
|
68
|
-
* @returns Object with value and cleanup function
|
|
69
|
-
*/
|
|
70
|
-
export function derived<T>(compute: (dep: EffectAccess) => T): {
|
|
71
|
-
value: T
|
|
72
|
-
[cleanup]: ScopedCallback
|
|
73
|
-
} {
|
|
74
|
-
const rv = { value: undefined as unknown as T }
|
|
75
|
-
return cleanedBy(
|
|
76
|
-
rv,
|
|
77
|
-
untracked(() =>
|
|
78
|
-
effect(function derivedEffect(access) {
|
|
79
|
-
rv.value = compute(access)
|
|
80
|
-
})
|
|
81
|
-
)
|
|
82
|
-
)
|
|
83
|
-
}
|