mutts 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +14 -6
  2. package/dist/chunks/{_tslib-C-cuVLvZ.js → _tslib-BgjropY9.js} +9 -1
  3. package/dist/chunks/_tslib-BgjropY9.js.map +1 -0
  4. package/dist/chunks/{_tslib-CMEnd0VE.esm.js → _tslib-Mzh1rNsX.esm.js} +9 -2
  5. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +1 -0
  6. package/dist/chunks/{decorator-D4DU97Zg.js → decorator-DLvrD0UF.js} +42 -19
  7. package/dist/chunks/decorator-DLvrD0UF.js.map +1 -0
  8. package/dist/chunks/{decorator-GnHw1Az7.esm.js → decorator-DqiszP7i.esm.js} +42 -19
  9. package/dist/chunks/decorator-DqiszP7i.esm.js.map +1 -0
  10. package/dist/chunks/index-DzUDtFc7.esm.js +4841 -0
  11. package/dist/chunks/index-DzUDtFc7.esm.js.map +1 -0
  12. package/dist/chunks/index-HNVqPzjz.js +4891 -0
  13. package/dist/chunks/index-HNVqPzjz.js.map +1 -0
  14. package/dist/decorator.esm.js +1 -1
  15. package/dist/decorator.js +1 -1
  16. package/dist/destroyable.d.ts +1 -1
  17. package/dist/destroyable.esm.js +1 -1
  18. package/dist/destroyable.esm.js.map +1 -1
  19. package/dist/destroyable.js +1 -1
  20. package/dist/destroyable.js.map +1 -1
  21. package/dist/devtools/devtools.html +9 -0
  22. package/dist/devtools/devtools.js +5 -0
  23. package/dist/devtools/devtools.js.map +1 -0
  24. package/dist/devtools/manifest.json +8 -0
  25. package/dist/devtools/panel.css +72 -0
  26. package/dist/devtools/panel.html +31 -0
  27. package/dist/devtools/panel.js +13048 -0
  28. package/dist/devtools/panel.js.map +1 -0
  29. package/dist/eventful.esm.js +1 -1
  30. package/dist/eventful.js +1 -1
  31. package/dist/index.d.ts +18 -63
  32. package/dist/index.esm.js +4 -4
  33. package/dist/index.js +36 -11
  34. package/dist/index.js.map +1 -1
  35. package/dist/indexable.d.ts +187 -1
  36. package/dist/indexable.esm.js +197 -3
  37. package/dist/indexable.esm.js.map +1 -1
  38. package/dist/indexable.js +198 -2
  39. package/dist/indexable.js.map +1 -1
  40. package/dist/mutts.umd.js +1 -1
  41. package/dist/mutts.umd.js.map +1 -1
  42. package/dist/mutts.umd.min.js +1 -1
  43. package/dist/mutts.umd.min.js.map +1 -1
  44. package/dist/promiseChain.esm.js.map +1 -1
  45. package/dist/promiseChain.js.map +1 -1
  46. package/dist/reactive.d.ts +601 -97
  47. package/dist/reactive.esm.js +3 -3
  48. package/dist/reactive.js +31 -10
  49. package/dist/reactive.js.map +1 -1
  50. package/dist/std-decorators.esm.js +1 -1
  51. package/dist/std-decorators.js +1 -1
  52. package/docs/ai/api-reference.md +133 -0
  53. package/docs/ai/manual.md +105 -0
  54. package/docs/iterableWeak.md +646 -0
  55. package/docs/reactive/advanced.md +1280 -0
  56. package/docs/reactive/collections.md +767 -0
  57. package/docs/reactive/core.md +973 -0
  58. package/docs/reactive.md +21 -9545
  59. package/package.json +18 -5
  60. package/src/decorator.ts +266 -0
  61. package/src/destroyable.ts +199 -0
  62. package/src/eventful.ts +77 -0
  63. package/src/index.d.ts +9 -0
  64. package/src/index.ts +9 -0
  65. package/src/indexable.ts +484 -0
  66. package/src/introspection.ts +59 -0
  67. package/src/iterableWeak.ts +233 -0
  68. package/src/mixins.ts +123 -0
  69. package/src/promiseChain.ts +110 -0
  70. package/src/reactive/array.ts +414 -0
  71. package/src/reactive/change.ts +134 -0
  72. package/src/reactive/debug.ts +517 -0
  73. package/src/reactive/deep-touch.ts +268 -0
  74. package/src/reactive/deep-watch-state.ts +82 -0
  75. package/src/reactive/deep-watch.ts +168 -0
  76. package/src/reactive/effect-context.ts +94 -0
  77. package/src/reactive/effects.ts +1333 -0
  78. package/src/reactive/index.ts +75 -0
  79. package/src/reactive/interface.ts +223 -0
  80. package/src/reactive/map.ts +171 -0
  81. package/src/reactive/mapped.ts +130 -0
  82. package/src/reactive/memoize.ts +107 -0
  83. package/src/reactive/non-reactive-state.ts +49 -0
  84. package/src/reactive/non-reactive.ts +43 -0
  85. package/src/reactive/project.project.md +93 -0
  86. package/src/reactive/project.ts +335 -0
  87. package/src/reactive/proxy-state.ts +27 -0
  88. package/src/reactive/proxy.ts +285 -0
  89. package/src/reactive/record.ts +196 -0
  90. package/src/reactive/register.ts +421 -0
  91. package/src/reactive/set.ts +144 -0
  92. package/src/reactive/tracking.ts +101 -0
  93. package/src/reactive/types.ts +358 -0
  94. package/src/reactive/zone.ts +208 -0
  95. package/src/std-decorators.ts +217 -0
  96. package/src/utils.ts +117 -0
  97. package/dist/chunks/_tslib-C-cuVLvZ.js.map +0 -1
  98. package/dist/chunks/_tslib-CMEnd0VE.esm.js.map +0 -1
  99. package/dist/chunks/decorator-D4DU97Zg.js.map +0 -1
  100. package/dist/chunks/decorator-GnHw1Az7.esm.js.map +0 -1
  101. package/dist/chunks/index-DBScoeCX.esm.js +0 -1960
  102. package/dist/chunks/index-DBScoeCX.esm.js.map +0 -1
  103. package/dist/chunks/index-DOTmXL89.js +0 -1983
  104. package/dist/chunks/index-DOTmXL89.js.map +0 -1
@@ -0,0 +1,414 @@
1
+ import { Indexable } from '../indexable'
2
+ import { touched } from './change'
3
+ import { makeReactiveEntriesIterator, makeReactiveIterator } from './non-reactive'
4
+ import { reactive } from './proxy'
5
+ import { unwrap } from './proxy-state'
6
+ import { dependant } from './tracking'
7
+ import { prototypeForwarding } from './types'
8
+
9
+ export const native = Symbol('native')
10
+ const isArray = Array.isArray
11
+ Array.isArray = ((value: any) =>
12
+ isArray(value) ||
13
+ // biome-ignore lint/suspicious/useIsArray: We are defining it
14
+ (value &&
15
+ typeof value === 'object' &&
16
+ prototypeForwarding in value &&
17
+ Array.isArray(value[prototypeForwarding]))) as any
18
+ export class ReactiveBaseArray {
19
+ declare readonly [native]: any[]
20
+
21
+ // Safe array access with negative indices
22
+ at(index: number): any {
23
+ const actualIndex = index < 0 ? this[native].length + index : index
24
+ dependant(this, actualIndex)
25
+ if (actualIndex < 0 || actualIndex >= this[native].length) return undefined
26
+ return reactive(this[native][actualIndex])
27
+ }
28
+
29
+ // Immutable versions of mutator methods
30
+ toReversed(): any[] {
31
+ dependant(this)
32
+ return reactive(this[native].toReversed())
33
+ }
34
+
35
+ toSorted(compareFn?: (a: any, b: any) => number): any[] {
36
+ dependant(this)
37
+ return reactive(this[native].toSorted(compareFn))
38
+ }
39
+
40
+ toSpliced(start: number, deleteCount?: number, ...items: any[]): any[] {
41
+ dependant(this)
42
+ return deleteCount === undefined
43
+ ? this[native].toSpliced(start)
44
+ : this[native].toSpliced(start, deleteCount, ...items)
45
+ }
46
+
47
+ with(index: number, value: any): any[] {
48
+ dependant(this)
49
+ return reactive(this[native].with(index, value))
50
+ }
51
+
52
+ // Iterator methods with reactivity tracking
53
+ entries() {
54
+ dependant(this)
55
+ return makeReactiveEntriesIterator(this[native].entries())
56
+ }
57
+
58
+ keys() {
59
+ dependant(this, 'length')
60
+ return this[native].keys()
61
+ }
62
+
63
+ values() {
64
+ dependant(this)
65
+ return makeReactiveIterator(this[native].values())
66
+ }
67
+
68
+ [Symbol.iterator]() {
69
+ dependant(this)
70
+ const nativeIterator = this[native][Symbol.iterator]()
71
+ return {
72
+ next() {
73
+ const result = nativeIterator.next()
74
+ if (result.done) {
75
+ return result
76
+ }
77
+ return { value: reactive(result.value), done: false }
78
+ },
79
+ }
80
+ }
81
+
82
+ indexOf(searchElement: any, fromIndex?: number): number {
83
+ dependant(this)
84
+ const unwrappedSearch = unwrap(searchElement)
85
+ // Check both wrapped and unwrapped versions since array may contain either
86
+ const index = this[native].indexOf(unwrappedSearch, fromIndex)
87
+ if (index !== -1) return index
88
+ // If not found with unwrapped, try with wrapped (in case array contains wrapped version)
89
+ return this[native].indexOf(searchElement, fromIndex)
90
+ }
91
+
92
+ lastIndexOf(searchElement: any, fromIndex?: number): number {
93
+ dependant(this)
94
+ const unwrappedSearch = unwrap(searchElement)
95
+ // Check both wrapped and unwrapped versions since array may contain either
96
+ const index = this[native].lastIndexOf(unwrappedSearch, fromIndex)
97
+ if (index !== -1) return index
98
+ // If not found with unwrapped, try with wrapped (in case array contains wrapped version)
99
+ return this[native].lastIndexOf(searchElement, fromIndex)
100
+ }
101
+
102
+ includes(searchElement: any, fromIndex?: number): boolean {
103
+ dependant(this)
104
+ const unwrappedSearch = unwrap(searchElement)
105
+ // Check both wrapped and unwrapped versions since array may contain either
106
+ return (
107
+ this[native].includes(unwrappedSearch, fromIndex) ||
108
+ this[native].includes(searchElement, fromIndex)
109
+ )
110
+ }
111
+
112
+ find(predicate: (this: any, value: any, index: number, obj: any[]) => boolean, thisArg?: any): any
113
+ find(searchElement: any, fromIndex?: number): any
114
+ find(predicateOrElement: any, thisArg?: any): any {
115
+ dependant(this)
116
+ if (typeof predicateOrElement === 'function') {
117
+ const predicate = predicateOrElement as (
118
+ this: any,
119
+ value: any,
120
+ index: number,
121
+ obj: any[]
122
+ ) => boolean
123
+ return reactive(
124
+ this[native].find(
125
+ (value, index, array) => predicate.call(thisArg, reactive(value), index, array),
126
+ thisArg
127
+ )
128
+ )
129
+ }
130
+ const fromIndex = typeof thisArg === 'number' ? thisArg : undefined
131
+ const index = this[native].indexOf(predicateOrElement, fromIndex)
132
+ if (index === -1) return undefined
133
+ return reactive(this[native][index])
134
+ }
135
+
136
+ findIndex(
137
+ predicate: (this: any, value: any, index: number, obj: any[]) => boolean,
138
+ thisArg?: any
139
+ ): number
140
+ findIndex(searchElement: any, fromIndex?: number): number
141
+ findIndex(predicateOrElement: any, thisArg?: any): number {
142
+ dependant(this)
143
+ if (typeof predicateOrElement === 'function') {
144
+ const predicate = predicateOrElement as (
145
+ this: any,
146
+ value: any,
147
+ index: number,
148
+ obj: any[]
149
+ ) => boolean
150
+ return this[native].findIndex(
151
+ (value, index, array) => predicate.call(thisArg, reactive(value), index, array),
152
+ thisArg
153
+ )
154
+ }
155
+ const fromIndex = typeof thisArg === 'number' ? thisArg : undefined
156
+ return this[native].indexOf(predicateOrElement, fromIndex)
157
+ }
158
+
159
+ flat(): any[] {
160
+ dependant(this)
161
+ return reactive(this[native].flat())
162
+ }
163
+
164
+ flatMap(
165
+ callbackfn: (this: any, value: any, index: number, array: any[]) => any[],
166
+ thisArg?: any
167
+ ): any[] {
168
+ dependant(this)
169
+ return reactive(this[native].flatMap(callbackfn, thisArg))
170
+ }
171
+
172
+ filter(callbackfn: (value: any, index: number, array: any[]) => boolean, thisArg?: any): any[] {
173
+ dependant(this)
174
+ return reactive(
175
+ this[native].filter((item, index, array) => callbackfn(reactive(item), index, array), thisArg)
176
+ )
177
+ }
178
+
179
+ map(callbackfn: (value: any, index: number, array: any[]) => any, thisArg?: any): any[] {
180
+ dependant(this)
181
+ return reactive(
182
+ this[native].map((item, index, array) => callbackfn(reactive(item), index, array), thisArg)
183
+ )
184
+ }
185
+
186
+ reduce(
187
+ callbackfn: (previousValue: any, currentValue: any, currentIndex: number, array: any[]) => any,
188
+ initialValue?: any
189
+ ): any {
190
+ dependant(this)
191
+ const result =
192
+ initialValue === undefined
193
+ ? this[native].reduce(callbackfn as any)
194
+ : this[native].reduce(callbackfn as any, initialValue)
195
+ return reactive(result)
196
+ }
197
+
198
+ reduceRight(
199
+ callbackfn: (previousValue: any, currentValue: any, currentIndex: number, array: any[]) => any,
200
+ initialValue?: any
201
+ ): any {
202
+ dependant(this)
203
+ const result =
204
+ initialValue !== undefined
205
+ ? this[native].reduceRight(callbackfn as any, initialValue)
206
+ : (this[native] as any).reduceRight(callbackfn as any)
207
+ return reactive(result)
208
+ }
209
+
210
+ slice(start?: number, end?: number): any[] {
211
+ for (const i of range(start || 0, end || this[native].length - 1)) dependant(this, i)
212
+ return start === undefined
213
+ ? this[native].slice()
214
+ : end === undefined
215
+ ? this[native].slice(start)
216
+ : this[native].slice(start, end)
217
+ }
218
+
219
+ concat(...items: any[]): any[] {
220
+ dependant(this)
221
+ return reactive(this[native].concat(...items))
222
+ }
223
+
224
+ join(separator?: string): string {
225
+ dependant(this)
226
+ return this[native].join(separator as any)
227
+ }
228
+
229
+ forEach(callbackfn: (value: any, index: number, array: any[]) => void, thisArg?: any): void {
230
+ dependant(this)
231
+ this[native].forEach((value, index, array) => {
232
+ callbackfn.call(thisArg, reactive(value), index, array)
233
+ })
234
+ }
235
+
236
+ // TODO: re-implement for fun dependencies? (eg - every only check the first ones until it find some),
237
+ // no need to make it dependant on indexes after the found one
238
+ every(callbackfn: (value: any, index: number, array: any[]) => boolean, thisArg?: any): boolean {
239
+ dependant(this)
240
+ return this[native].every(
241
+ (value, index, array) => callbackfn.call(thisArg, reactive(value), index, array),
242
+ thisArg
243
+ )
244
+ }
245
+
246
+ some(callbackfn: (value: any, index: number, array: any[]) => boolean, thisArg?: any): boolean {
247
+ dependant(this)
248
+ return this[native].some(
249
+ (value, index, array) => callbackfn.call(thisArg, reactive(value), index, array),
250
+ thisArg
251
+ )
252
+ }
253
+ }
254
+ function* index(i: number, { length = true } = {}): IterableIterator<number | 'length'> {
255
+ if (length) yield 'length'
256
+ yield i
257
+ }
258
+
259
+ function* range(
260
+ a: number,
261
+ b: number,
262
+ { length = false } = {}
263
+ ): IterableIterator<number | 'length'> {
264
+ const start = Math.min(a, b)
265
+ const end = Math.max(a, b)
266
+ if (length) yield 'length'
267
+ for (let i = start; i <= end; i++) yield i
268
+ }
269
+ /**
270
+ * Reactive wrapper around JavaScript's Array class with full array method support
271
+ * Tracks length changes, individual index operations, and collection-wide operations
272
+ */
273
+ export class ReactiveArray extends Indexable(ReactiveBaseArray, {
274
+ get(i: number): any {
275
+ dependant(this, i)
276
+ return reactive(this[native][i])
277
+ },
278
+ set(i: number, value: any) {
279
+ const added = i >= this[native].length
280
+ this[native][i] = value
281
+ touched(this, { type: 'set', prop: i }, index(i, { length: added }))
282
+ },
283
+ getLength() {
284
+ dependant(this, 'length')
285
+ return this[native].length
286
+ },
287
+ setLength(value: number) {
288
+ const oldLength = this[native].length
289
+ try {
290
+ this[native].length = value
291
+ } finally {
292
+ touched(this, { type: 'set', prop: 'length' }, range(oldLength, value, { length: true }))
293
+ }
294
+ },
295
+ }) {
296
+ declare length: number
297
+ constructor(original: any[]) {
298
+ super()
299
+ Object.defineProperties(this, {
300
+ // We have to make it double, as [native] must be `unique symbol` - impossible through import
301
+ [native]: { value: original },
302
+ [prototypeForwarding]: { value: original },
303
+ })
304
+ }
305
+
306
+ push(...items: any[]) {
307
+ const oldLength = this[native].length
308
+ try {
309
+ return this[native].push(...items)
310
+ } finally {
311
+ touched(
312
+ this,
313
+ { type: 'bunch', method: 'push' },
314
+ range(oldLength, oldLength + items.length - 1, { length: true })
315
+ )
316
+ }
317
+ }
318
+
319
+ pop() {
320
+ if (this[native].length === 0) return undefined
321
+ try {
322
+ return reactive(this[native].pop())
323
+ } finally {
324
+ touched(this, { type: 'bunch', method: 'pop' }, index(this[native].length))
325
+ }
326
+ }
327
+
328
+ shift() {
329
+ if (this[native].length === 0) return undefined
330
+ try {
331
+ return reactive(this[native].shift())
332
+ } finally {
333
+ touched(
334
+ this,
335
+ { type: 'bunch', method: 'shift' },
336
+ range(0, this[native].length + 1, { length: true })
337
+ )
338
+ }
339
+ }
340
+
341
+ unshift(...items: any[]) {
342
+ try {
343
+ return this[native].unshift(...items)
344
+ } finally {
345
+ touched(
346
+ this,
347
+ { type: 'bunch', method: 'unshift' },
348
+ range(0, this[native].length - items.length, { length: true })
349
+ )
350
+ }
351
+ }
352
+
353
+ splice(start: number, deleteCount?: number, ...items: any[]) {
354
+ const oldLength = this[native].length
355
+ if (deleteCount === undefined) deleteCount = oldLength - start
356
+ try {
357
+ if (deleteCount === undefined) return reactive(this[native].splice(start))
358
+ return reactive(this[native].splice(start, deleteCount, ...items))
359
+ } finally {
360
+ touched(
361
+ this,
362
+ { type: 'bunch', method: 'splice' },
363
+ // TODO: edge cases
364
+ deleteCount === items.length
365
+ ? range(start, start + deleteCount)
366
+ : range(start, oldLength + Math.max(items.length - deleteCount, 0), {
367
+ length: true,
368
+ })
369
+ )
370
+ }
371
+ }
372
+
373
+ reverse() {
374
+ try {
375
+ return this[native].reverse()
376
+ } finally {
377
+ touched(this, { type: 'bunch', method: 'reverse' }, range(0, this[native].length - 1))
378
+ }
379
+ }
380
+
381
+ sort(compareFn?: (a: any, b: any) => number) {
382
+ compareFn = compareFn || ((a, b) => a.toString().localeCompare(b.toString()))
383
+ try {
384
+ return this[native].sort((a, b) => compareFn(reactive(a), reactive(b))) as any
385
+ } finally {
386
+ touched(this, { type: 'bunch', method: 'sort' }, range(0, this[native].length - 1))
387
+ }
388
+ }
389
+
390
+ fill(value: any, start?: number, end?: number) {
391
+ try {
392
+ if (start === undefined) return this[native].fill(value) as any
393
+ if (end === undefined) return this[native].fill(value, start) as any
394
+ return this[native].fill(value, start, end) as any
395
+ } finally {
396
+ touched(this, { type: 'bunch', method: 'fill' }, range(0, this[native].length - 1))
397
+ }
398
+ }
399
+
400
+ copyWithin(target: number, start: number, end?: number) {
401
+ try {
402
+ if (end === undefined) return this[native].copyWithin(target, start) as any
403
+ return this[native].copyWithin(target, start, end) as any
404
+ } finally {
405
+ touched(
406
+ this,
407
+ { type: 'bunch', method: 'copyWithin' },
408
+ // TODO: calculate the range properly
409
+ range(0, this[native].length - 1)
410
+ )
411
+ }
412
+ // Touch all affected indices with a single allProps call
413
+ }
414
+ }
@@ -0,0 +1,134 @@
1
+ import { recordTriggerLink } from './debug'
2
+ import { bubbleUpChange, objectsWithDeepWatchers } from './deep-watch-state'
3
+ import { getActiveEffect, isRunning } from './effect-context'
4
+ import { batch, effectTrackers, opaqueEffects } from './effects'
5
+ import { unwrap } from './proxy-state'
6
+ import { watchers } from './tracking'
7
+ import { allProps, type Evolution, options, type ScopedCallback, type State } from './types'
8
+
9
+ const states = new WeakMap<object, State>()
10
+
11
+ export function addState(obj: any, evolution: Evolution) {
12
+ obj = unwrap(obj)
13
+ const next = {}
14
+ const state = getState(obj)
15
+ if (state) Object.assign(state, { evolution, next })
16
+ states.set(obj, next)
17
+ }
18
+
19
+ /**
20
+ * Gets the current state of a reactive object for evolution tracking
21
+ * @param obj - The reactive object
22
+ * @returns The current state object
23
+ */
24
+ export function getState(obj: any) {
25
+ obj = unwrap(obj)
26
+ let state = states.get(obj)
27
+ if (!state) {
28
+ state = {}
29
+ states.set(obj, state)
30
+ }
31
+ return state
32
+ }
33
+
34
+ export function collectEffects(
35
+ obj: any,
36
+ evolution: Evolution,
37
+ effects: Set<ScopedCallback>,
38
+ objectWatchers: Map<any, Set<ScopedCallback>>,
39
+ ...keyChains: Iterable<any>[]
40
+ ) {
41
+ const sourceEffect = getActiveEffect()
42
+ for (const keys of keyChains)
43
+ for (const key of keys) {
44
+ const deps = objectWatchers.get(key)
45
+ if (deps)
46
+ for (const effect of deps) {
47
+ const runningChain = isRunning(effect)
48
+ if (runningChain) {
49
+ options.skipRunningEffect(effect, runningChain as any)
50
+ continue
51
+ }
52
+ effects.add(effect)
53
+ const trackers = effectTrackers.get(effect)
54
+ recordTriggerLink(sourceEffect, effect, obj, key, evolution)
55
+ if (trackers) {
56
+ for (const tracker of trackers) tracker(obj, evolution, key)
57
+ trackers.delete(effect)
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Triggers effects for a single property change
65
+ * @param obj - The object that changed
66
+ * @param evolution - The type of change
67
+ * @param prop - The property that changed
68
+ */
69
+ export function touched1(obj: any, evolution: Evolution, prop: any) {
70
+ touched(obj, evolution, [prop])
71
+ }
72
+
73
+ /**
74
+ * Triggers effects for property changes
75
+ * @param obj - The object that changed
76
+ * @param evolution - The type of change
77
+ * @param props - The properties that changed
78
+ */
79
+ export function touched(obj: any, evolution: Evolution, props?: Iterable<any>) {
80
+ obj = unwrap(obj)
81
+ addState(obj, evolution)
82
+ const objectWatchers = watchers.get(obj)
83
+ if (objectWatchers) {
84
+ // Note: we have to collect effects to remove duplicates in the specific case when no batch is running
85
+ const effects = new Set<ScopedCallback>()
86
+ if (props) collectEffects(obj, evolution, effects, objectWatchers, [allProps], props)
87
+ else collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys())
88
+ options.touched(obj, evolution, props as any[] | undefined, effects)
89
+ batch(Array.from(effects))
90
+ }
91
+
92
+ // Bubble up changes if this object has deep watchers
93
+ if (objectsWithDeepWatchers.has(obj)) {
94
+ bubbleUpChange(obj, evolution)
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Triggers only opaque effects for property changes
100
+ * Used by deep-touch to ensure opaque listeners are notified even when deep optimization is active
101
+ */
102
+ export function touchedOpaque(obj: any, evolution: Evolution, prop: any) {
103
+ obj = unwrap(obj)
104
+ const objectWatchers = watchers.get(obj)
105
+ if (!objectWatchers) return
106
+
107
+ const deps = objectWatchers.get(prop)
108
+ if (!deps) return
109
+
110
+ const effects = new Set<ScopedCallback>()
111
+ const sourceEffect = getActiveEffect()
112
+
113
+ for (const effect of deps) {
114
+ if (!opaqueEffects.has(effect)) continue
115
+
116
+ const runningChain = isRunning(effect)
117
+ if (runningChain) {
118
+ options.skipRunningEffect(effect, runningChain as any)
119
+ continue
120
+ }
121
+ effects.add(effect)
122
+ const trackers = effectTrackers.get(effect)
123
+ recordTriggerLink(sourceEffect, effect, obj, prop, evolution)
124
+ if (trackers) {
125
+ for (const tracker of trackers) tracker(obj, evolution, prop)
126
+ trackers.delete(effect)
127
+ }
128
+ }
129
+
130
+ if (effects.size > 0) {
131
+ options.touched(obj, evolution, [prop], effects)
132
+ batch(Array.from(effects))
133
+ }
134
+ }