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.
Files changed (206) hide show
  1. package/README.md +60 -35
  2. package/dist/browser.cjs +1333 -1760
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +2 -1392
  5. package/dist/browser.dev.cjs +114 -0
  6. package/dist/browser.dev.cjs.map +1 -0
  7. package/dist/browser.dev.d.ts +2 -0
  8. package/dist/browser.dev.esm.js +5 -0
  9. package/dist/browser.dev.esm.js.map +1 -0
  10. package/dist/browser.esm.js +18 -97
  11. package/dist/browser.esm.js.map +1 -1
  12. package/dist/chunks/{async-browser-lvzLOCgk.cjs → async-browser-Dgr5CreQ.cjs} +16 -95
  13. package/dist/chunks/async-browser-Dgr5CreQ.cjs.map +1 -0
  14. package/dist/chunks/{async-node-C3DeIb0y.cjs → async-node-3PrbVAbB.cjs} +3 -1
  15. package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
  16. package/dist/chunks/index-Sf74wXTV.esm.js +2577 -0
  17. package/dist/chunks/index-Sf74wXTV.esm.js.map +1 -0
  18. package/dist/chunks/node-Bo7WU5S2.esm.js +96 -0
  19. package/dist/chunks/node-Bo7WU5S2.esm.js.map +1 -0
  20. package/dist/chunks/{index-VTO-b2vR.cjs → proxy-Cc79Lrzj.cjs} +2593 -3216
  21. package/dist/chunks/proxy-Cc79Lrzj.cjs.map +1 -0
  22. package/dist/chunks/{index-CtA2AWl3.esm.js → proxy-D2C49sXH.esm.js} +2570 -3174
  23. package/dist/chunks/proxy-D2C49sXH.esm.js.map +1 -0
  24. package/dist/debug.cjs +987 -28
  25. package/dist/debug.cjs.map +1 -1
  26. package/dist/debug.d.ts +37 -38
  27. package/dist/debug.esm.js +963 -1
  28. package/dist/debug.esm.js.map +1 -1
  29. package/dist/devtools/manifest.json +1 -1
  30. package/dist/devtools/panel.html +1 -1
  31. package/dist/devtools/panel.js +107 -94
  32. package/dist/devtools/panel.js.map +1 -1
  33. package/dist/index.d.ts +1322 -1
  34. package/dist/mutts.umd.js +6647 -1
  35. package/dist/mutts.umd.js.map +1 -1
  36. package/dist/mutts.umd.min.js +1 -1
  37. package/dist/mutts.umd.min.js.map +1 -1
  38. package/dist/node.cjs +69 -63
  39. package/dist/node.cjs.map +1 -1
  40. package/dist/node.d.ts +2 -2
  41. package/dist/node.dev.cjs +114 -0
  42. package/dist/node.dev.cjs.map +1 -0
  43. package/dist/node.dev.d.ts +2 -0
  44. package/dist/node.dev.esm.js +6 -0
  45. package/dist/node.dev.esm.js.map +1 -0
  46. package/dist/node.esm.js +4 -97
  47. package/dist/node.esm.js.map +1 -1
  48. package/dist/{types-DaHFfhlN.d.ts → types-Bx2PhORg.d.ts} +134 -88
  49. package/docs/ai/api-reference.md +11 -32
  50. package/docs/ai/manual.md +297 -239
  51. package/docs/reactive/advanced.md +318 -9
  52. package/docs/reactive/attend.md +2 -4
  53. package/docs/reactive/collections.md +22 -187
  54. package/docs/reactive/core.md +223 -131
  55. package/docs/reactive/debugging.md +119 -12
  56. package/docs/reactive/error-handling.md +10 -10
  57. package/docs/reactive/resource.md +125 -0
  58. package/docs/reactive.md +3 -4
  59. package/docs/utils.md +70 -0
  60. package/docs/zone.md +1 -1
  61. package/package.json +76 -38
  62. package/dist/chunks/async-browser-lvzLOCgk.cjs.map +0 -1
  63. package/dist/chunks/async-node-C3DeIb0y.cjs.map +0 -1
  64. package/dist/chunks/index-2vea86wD.esm.js +0 -3011
  65. package/dist/chunks/index-2vea86wD.esm.js.map +0 -1
  66. package/dist/chunks/index-CtA2AWl3.esm.js.map +0 -1
  67. package/dist/chunks/index-VTO-b2vR.cjs.map +0 -1
  68. package/dist/debug/debug.d.ts +0 -122
  69. package/dist/debug/debug.d.ts.map +0 -1
  70. package/dist/debug/index.d.ts +0 -4
  71. package/dist/debug/index.d.ts.map +0 -1
  72. package/dist/debug/lineage-panel.d.ts +0 -5
  73. package/dist/debug/lineage-panel.d.ts.map +0 -1
  74. package/dist/debug/lineage.d.ts +0 -79
  75. package/dist/debug/lineage.d.ts.map +0 -1
  76. package/dist/src/async/browser.d.ts +0 -2
  77. package/dist/src/async/browser.d.ts.map +0 -1
  78. package/dist/src/async/index.d.ts +0 -19
  79. package/dist/src/async/index.d.ts.map +0 -1
  80. package/dist/src/async/node.d.ts +0 -2
  81. package/dist/src/async/node.d.ts.map +0 -1
  82. package/dist/src/decorator.d.ts +0 -106
  83. package/dist/src/decorator.d.ts.map +0 -1
  84. package/dist/src/destroyable.d.ts +0 -87
  85. package/dist/src/destroyable.d.ts.map +0 -1
  86. package/dist/src/entry-browser.d.ts +0 -3
  87. package/dist/src/entry-browser.d.ts.map +0 -1
  88. package/dist/src/entry-node.d.ts +0 -3
  89. package/dist/src/entry-node.d.ts.map +0 -1
  90. package/dist/src/eventful.d.ts +0 -20
  91. package/dist/src/eventful.d.ts.map +0 -1
  92. package/dist/src/flavored.d.ts +0 -33
  93. package/dist/src/flavored.d.ts.map +0 -1
  94. package/dist/src/index.d.ts +0 -14
  95. package/dist/src/index.d.ts.map +0 -1
  96. package/dist/src/indexable.d.ts +0 -243
  97. package/dist/src/indexable.d.ts.map +0 -1
  98. package/dist/src/introspection.d.ts +0 -27
  99. package/dist/src/introspection.d.ts.map +0 -1
  100. package/dist/src/iterableWeak.d.ts +0 -53
  101. package/dist/src/iterableWeak.d.ts.map +0 -1
  102. package/dist/src/mixins.d.ts +0 -25
  103. package/dist/src/mixins.d.ts.map +0 -1
  104. package/dist/src/promiseChain.d.ts +0 -20
  105. package/dist/src/promiseChain.d.ts.map +0 -1
  106. package/dist/src/reactive/array.d.ts +0 -48
  107. package/dist/src/reactive/array.d.ts.map +0 -1
  108. package/dist/src/reactive/buffer.d.ts +0 -120
  109. package/dist/src/reactive/buffer.d.ts.map +0 -1
  110. package/dist/src/reactive/change.d.ts +0 -29
  111. package/dist/src/reactive/change.d.ts.map +0 -1
  112. package/dist/src/reactive/deep-touch.d.ts +0 -28
  113. package/dist/src/reactive/deep-touch.d.ts.map +0 -1
  114. package/dist/src/reactive/deep-watch-state.d.ts +0 -25
  115. package/dist/src/reactive/deep-watch-state.d.ts.map +0 -1
  116. package/dist/src/reactive/deep-watch.d.ts +0 -20
  117. package/dist/src/reactive/deep-watch.d.ts.map +0 -1
  118. package/dist/src/reactive/describe.d.ts +0 -12
  119. package/dist/src/reactive/describe.d.ts.map +0 -1
  120. package/dist/src/reactive/effect-context.d.ts +0 -34
  121. package/dist/src/reactive/effect-context.d.ts.map +0 -1
  122. package/dist/src/reactive/effects.d.ts +0 -164
  123. package/dist/src/reactive/effects.d.ts.map +0 -1
  124. package/dist/src/reactive/index.d.ts +0 -19
  125. package/dist/src/reactive/index.d.ts.map +0 -1
  126. package/dist/src/reactive/map.d.ts +0 -28
  127. package/dist/src/reactive/map.d.ts.map +0 -1
  128. package/dist/src/reactive/memoize.d.ts +0 -28
  129. package/dist/src/reactive/memoize.d.ts.map +0 -1
  130. package/dist/src/reactive/non-reactive-state.d.ts +0 -9
  131. package/dist/src/reactive/non-reactive-state.d.ts.map +0 -1
  132. package/dist/src/reactive/non-reactive.d.ts +0 -11
  133. package/dist/src/reactive/non-reactive.d.ts.map +0 -1
  134. package/dist/src/reactive/project.d.ts +0 -40
  135. package/dist/src/reactive/project.d.ts.map +0 -1
  136. package/dist/src/reactive/proxy-state.d.ts +0 -8
  137. package/dist/src/reactive/proxy-state.d.ts.map +0 -1
  138. package/dist/src/reactive/proxy.d.ts +0 -23
  139. package/dist/src/reactive/proxy.d.ts.map +0 -1
  140. package/dist/src/reactive/record.d.ts +0 -115
  141. package/dist/src/reactive/record.d.ts.map +0 -1
  142. package/dist/src/reactive/register.d.ts +0 -125
  143. package/dist/src/reactive/register.d.ts.map +0 -1
  144. package/dist/src/reactive/registry.d.ts +0 -21
  145. package/dist/src/reactive/registry.d.ts.map +0 -1
  146. package/dist/src/reactive/set.d.ts +0 -26
  147. package/dist/src/reactive/set.d.ts.map +0 -1
  148. package/dist/src/reactive/tracking.d.ts +0 -7
  149. package/dist/src/reactive/tracking.d.ts.map +0 -1
  150. package/dist/src/reactive/types.d.ts +0 -424
  151. package/dist/src/reactive/types.d.ts.map +0 -1
  152. package/dist/src/reactive/watch.d.ts +0 -48
  153. package/dist/src/reactive/watch.d.ts.map +0 -1
  154. package/dist/src/std-decorators.d.ts +0 -45
  155. package/dist/src/std-decorators.d.ts.map +0 -1
  156. package/dist/src/utils.d.ts +0 -49
  157. package/dist/src/utils.d.ts.map +0 -1
  158. package/dist/src/zone.d.ts +0 -40
  159. package/dist/src/zone.d.ts.map +0 -1
  160. package/docs/reactive/describe.md +0 -85
  161. package/docs/reactive/project.md +0 -93
  162. package/docs/reactive/scan.md +0 -293
  163. package/src/async/browser.ts +0 -323
  164. package/src/async/index.ts +0 -27
  165. package/src/async/node.ts +0 -92
  166. package/src/decorator.ts +0 -272
  167. package/src/destroyable.ts +0 -199
  168. package/src/entry-browser.ts +0 -5
  169. package/src/entry-node.ts +0 -5
  170. package/src/eventful.ts +0 -110
  171. package/src/flavored.ts +0 -106
  172. package/src/index.d.ts +0 -12
  173. package/src/index.ts +0 -64
  174. package/src/indexable.ts +0 -526
  175. package/src/introspection.ts +0 -59
  176. package/src/iterableWeak.ts +0 -233
  177. package/src/mixins.ts +0 -123
  178. package/src/promiseChain.ts +0 -110
  179. package/src/reactive/array.ts +0 -500
  180. package/src/reactive/buffer.ts +0 -328
  181. package/src/reactive/change.ts +0 -131
  182. package/src/reactive/deep-touch.ts +0 -273
  183. package/src/reactive/deep-watch-state.ts +0 -82
  184. package/src/reactive/deep-watch.ts +0 -171
  185. package/src/reactive/describe.ts +0 -39
  186. package/src/reactive/effect-context.ts +0 -83
  187. package/src/reactive/effects.ts +0 -1434
  188. package/src/reactive/index.ts +0 -72
  189. package/src/reactive/map.ts +0 -142
  190. package/src/reactive/memoize.ts +0 -186
  191. package/src/reactive/non-reactive-state.ts +0 -49
  192. package/src/reactive/non-reactive.ts +0 -43
  193. package/src/reactive/project.md +0 -107
  194. package/src/reactive/project.ts +0 -430
  195. package/src/reactive/proxy-state.ts +0 -27
  196. package/src/reactive/proxy.ts +0 -282
  197. package/src/reactive/record.ts +0 -181
  198. package/src/reactive/register.ts +0 -538
  199. package/src/reactive/registry.ts +0 -72
  200. package/src/reactive/set.ts +0 -117
  201. package/src/reactive/tracking.ts +0 -41
  202. package/src/reactive/types.ts +0 -520
  203. package/src/reactive/watch.ts +0 -180
  204. package/src/std-decorators.ts +0 -256
  205. package/src/utils.ts +0 -300
  206. 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
- }
@@ -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
- }