mutts 1.0.7 → 1.0.9

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 (191) hide show
  1. package/README.md +72 -17
  2. package/dist/browser.cjs +3111 -0
  3. package/dist/browser.cjs.map +1 -0
  4. package/dist/browser.d.ts +1391 -1
  5. package/dist/browser.esm.js +264 -35
  6. package/dist/browser.esm.js.map +1 -1
  7. package/dist/chunks/async-browser-lvzLOCgk.cjs +297 -0
  8. package/dist/chunks/async-browser-lvzLOCgk.cjs.map +1 -0
  9. package/dist/chunks/async-core-CRLKP3l-.cjs +29 -0
  10. package/dist/chunks/async-core-CRLKP3l-.cjs.map +1 -0
  11. package/dist/chunks/async-node-C3DeIb0y.cjs +96 -0
  12. package/dist/chunks/async-node-C3DeIb0y.cjs.map +1 -0
  13. package/dist/chunks/index-2vea86wD.esm.js +3011 -0
  14. package/dist/chunks/index-2vea86wD.esm.js.map +1 -0
  15. package/dist/chunks/index-CtA2AWl3.esm.js +4376 -0
  16. package/dist/chunks/index-CtA2AWl3.esm.js.map +1 -0
  17. package/dist/chunks/index-VTO-b2vR.cjs +4461 -0
  18. package/dist/chunks/index-VTO-b2vR.cjs.map +1 -0
  19. package/dist/debug/debug.d.ts +122 -0
  20. package/dist/debug/debug.d.ts.map +1 -0
  21. package/dist/debug/index.d.ts +4 -0
  22. package/dist/debug/index.d.ts.map +1 -0
  23. package/dist/debug/lineage-panel.d.ts +5 -0
  24. package/dist/debug/lineage-panel.d.ts.map +1 -0
  25. package/dist/debug/lineage.d.ts +79 -0
  26. package/dist/debug/lineage.d.ts.map +1 -0
  27. package/dist/debug.cjs +35 -0
  28. package/dist/debug.cjs.map +1 -0
  29. package/dist/debug.d.ts +209 -0
  30. package/dist/debug.esm.js +2 -0
  31. package/dist/debug.esm.js.map +1 -0
  32. package/dist/devtools/devtool/devtools.d.ts +1 -0
  33. package/dist/devtools/devtool/devtools.d.ts.map +1 -0
  34. package/dist/devtools/devtool/panel.d.ts +2 -0
  35. package/dist/devtools/devtool/panel.d.ts.map +1 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/mutts.umd.js +1 -1
  38. package/dist/mutts.umd.js.map +1 -1
  39. package/dist/mutts.umd.min.js +1 -1
  40. package/dist/mutts.umd.min.js.map +1 -1
  41. package/dist/node.cjs +107 -0
  42. package/dist/node.cjs.map +1 -0
  43. package/dist/node.d.ts +2 -2
  44. package/dist/node.esm.js +86 -33
  45. package/dist/node.esm.js.map +1 -1
  46. package/dist/src/async/browser.d.ts +2 -0
  47. package/dist/src/async/browser.d.ts.map +1 -0
  48. package/dist/src/async/index.d.ts +19 -0
  49. package/dist/src/async/index.d.ts.map +1 -0
  50. package/dist/src/async/node.d.ts +2 -0
  51. package/dist/src/async/node.d.ts.map +1 -0
  52. package/dist/src/decorator.d.ts +106 -0
  53. package/dist/src/decorator.d.ts.map +1 -0
  54. package/dist/src/destroyable.d.ts +87 -0
  55. package/dist/src/destroyable.d.ts.map +1 -0
  56. package/dist/src/entry-browser.d.ts +3 -0
  57. package/dist/src/entry-browser.d.ts.map +1 -0
  58. package/dist/src/entry-node.d.ts +3 -0
  59. package/dist/src/entry-node.d.ts.map +1 -0
  60. package/dist/src/eventful.d.ts +20 -0
  61. package/dist/src/eventful.d.ts.map +1 -0
  62. package/dist/src/flavored.d.ts +33 -0
  63. package/dist/src/flavored.d.ts.map +1 -0
  64. package/dist/src/index.d.ts +14 -0
  65. package/dist/src/index.d.ts.map +1 -0
  66. package/dist/src/indexable.d.ts +243 -0
  67. package/dist/src/indexable.d.ts.map +1 -0
  68. package/dist/src/introspection.d.ts +27 -0
  69. package/dist/src/introspection.d.ts.map +1 -0
  70. package/dist/src/iterableWeak.d.ts +53 -0
  71. package/dist/src/iterableWeak.d.ts.map +1 -0
  72. package/dist/src/mixins.d.ts +25 -0
  73. package/dist/src/mixins.d.ts.map +1 -0
  74. package/dist/src/promiseChain.d.ts +20 -0
  75. package/dist/src/promiseChain.d.ts.map +1 -0
  76. package/dist/src/reactive/array.d.ts +48 -0
  77. package/dist/src/reactive/array.d.ts.map +1 -0
  78. package/dist/src/reactive/buffer.d.ts +120 -0
  79. package/dist/src/reactive/buffer.d.ts.map +1 -0
  80. package/dist/src/reactive/change.d.ts +29 -0
  81. package/dist/src/reactive/change.d.ts.map +1 -0
  82. package/dist/src/reactive/deep-touch.d.ts +28 -0
  83. package/dist/src/reactive/deep-touch.d.ts.map +1 -0
  84. package/dist/src/reactive/deep-watch-state.d.ts +25 -0
  85. package/dist/src/reactive/deep-watch-state.d.ts.map +1 -0
  86. package/dist/src/reactive/deep-watch.d.ts +20 -0
  87. package/dist/src/reactive/deep-watch.d.ts.map +1 -0
  88. package/dist/src/reactive/describe.d.ts +12 -0
  89. package/dist/src/reactive/describe.d.ts.map +1 -0
  90. package/dist/src/reactive/effect-context.d.ts +34 -0
  91. package/dist/src/reactive/effect-context.d.ts.map +1 -0
  92. package/dist/src/reactive/effects.d.ts +164 -0
  93. package/dist/src/reactive/effects.d.ts.map +1 -0
  94. package/dist/src/reactive/index.d.ts +19 -0
  95. package/dist/src/reactive/index.d.ts.map +1 -0
  96. package/dist/src/reactive/map.d.ts +28 -0
  97. package/dist/src/reactive/map.d.ts.map +1 -0
  98. package/dist/src/reactive/memoize.d.ts +28 -0
  99. package/dist/src/reactive/memoize.d.ts.map +1 -0
  100. package/dist/src/reactive/non-reactive-state.d.ts +9 -0
  101. package/dist/src/reactive/non-reactive-state.d.ts.map +1 -0
  102. package/dist/src/reactive/non-reactive.d.ts +11 -0
  103. package/dist/src/reactive/non-reactive.d.ts.map +1 -0
  104. package/dist/src/reactive/project.d.ts +40 -0
  105. package/dist/src/reactive/project.d.ts.map +1 -0
  106. package/dist/src/reactive/proxy-state.d.ts +8 -0
  107. package/dist/src/reactive/proxy-state.d.ts.map +1 -0
  108. package/dist/src/reactive/proxy.d.ts +23 -0
  109. package/dist/src/reactive/proxy.d.ts.map +1 -0
  110. package/dist/src/reactive/record.d.ts +115 -0
  111. package/dist/src/reactive/record.d.ts.map +1 -0
  112. package/dist/src/reactive/register.d.ts +125 -0
  113. package/dist/src/reactive/register.d.ts.map +1 -0
  114. package/dist/src/reactive/registry.d.ts +21 -0
  115. package/dist/src/reactive/registry.d.ts.map +1 -0
  116. package/dist/src/reactive/set.d.ts +26 -0
  117. package/dist/src/reactive/set.d.ts.map +1 -0
  118. package/dist/src/reactive/tracking.d.ts +7 -0
  119. package/dist/src/reactive/tracking.d.ts.map +1 -0
  120. package/dist/src/reactive/types.d.ts +424 -0
  121. package/dist/src/reactive/types.d.ts.map +1 -0
  122. package/dist/src/reactive/watch.d.ts +48 -0
  123. package/dist/src/reactive/watch.d.ts.map +1 -0
  124. package/dist/src/std-decorators.d.ts +45 -0
  125. package/dist/src/std-decorators.d.ts.map +1 -0
  126. package/dist/src/utils.d.ts +49 -0
  127. package/dist/src/utils.d.ts.map +1 -0
  128. package/dist/src/zone.d.ts +40 -0
  129. package/dist/src/zone.d.ts.map +1 -0
  130. package/dist/types-DaHFfhlN.d.ts +414 -0
  131. package/docs/ai/api-reference.md +1 -1
  132. package/docs/ai/manual.md +443 -17
  133. package/docs/events.md +57 -2
  134. package/docs/flavored.md +210 -0
  135. package/docs/reactive/advanced.md +2 -3
  136. package/docs/reactive/attend.md +143 -0
  137. package/docs/reactive/collections.md +55 -0
  138. package/docs/reactive/core.md +120 -4
  139. package/docs/reactive/debugging.md +8 -10
  140. package/docs/reactive/describe.md +85 -0
  141. package/docs/reactive/error-handling.md +121 -0
  142. package/docs/reactive/project.md +1 -1
  143. package/docs/reactive/scan.md +215 -0
  144. package/docs/reactive.md +2 -0
  145. package/docs/std-decorators.md +171 -52
  146. package/docs/zone.md +9 -6
  147. package/package.json +44 -27
  148. package/src/async/browser.ts +277 -41
  149. package/src/async/index.ts +23 -4
  150. package/src/async/node.ts +78 -32
  151. package/src/entry-browser.ts +5 -0
  152. package/src/entry-node.ts +5 -0
  153. package/src/eventful.ts +82 -49
  154. package/src/flavored.ts +106 -0
  155. package/src/index.d.ts +12 -9
  156. package/src/index.ts +2 -1
  157. package/src/introspection.ts +1 -1
  158. package/src/reactive/array.ts +160 -80
  159. package/src/reactive/buffer.ts +187 -27
  160. package/src/reactive/change.ts +16 -23
  161. package/src/reactive/deep-touch.ts +19 -14
  162. package/src/reactive/deep-watch-state.ts +3 -3
  163. package/src/reactive/deep-watch.ts +7 -4
  164. package/src/reactive/describe.ts +39 -0
  165. package/src/reactive/effect-context.ts +71 -6
  166. package/src/reactive/effects.ts +445 -393
  167. package/src/reactive/index.ts +12 -14
  168. package/src/reactive/map.ts +8 -24
  169. package/src/reactive/memoize.ts +26 -3
  170. package/src/reactive/project.md +107 -0
  171. package/src/reactive/project.ts +48 -71
  172. package/src/reactive/proxy.ts +21 -3
  173. package/src/reactive/record.ts +30 -45
  174. package/src/reactive/register.ts +138 -19
  175. package/src/reactive/registry.ts +22 -9
  176. package/src/reactive/set.ts +7 -20
  177. package/src/reactive/tracking.ts +9 -12
  178. package/src/reactive/types.ts +104 -24
  179. package/src/reactive/{interface.ts → watch.ts} +29 -59
  180. package/src/std-decorators.ts +56 -17
  181. package/src/utils.ts +3 -4
  182. package/src/zone.ts +43 -28
  183. package/dist/browser.js +0 -161
  184. package/dist/browser.js.map +0 -1
  185. package/dist/chunks/index-BFYK02LG.js +0 -6378
  186. package/dist/chunks/index-BFYK02LG.js.map +0 -1
  187. package/dist/chunks/index-CNR6QRUl.esm.js +0 -6291
  188. package/dist/chunks/index-CNR6QRUl.esm.js.map +0 -1
  189. package/dist/node.js +0 -136
  190. package/dist/node.js.map +0 -1
  191. package/src/reactive/debug.ts +0 -517
@@ -1,14 +1,39 @@
1
- import { FunctionWrapper } from '../zone'
1
+ import { Eventful } from '../eventful'
2
2
  import { ArrayReadForward, forwardArray, getAt, Indexable, setAt } from '../indexable'
3
+ import type { FunctionWrapper } from '../zone'
3
4
  import { effect } from './effects'
4
- import { unreactive } from './interface'
5
5
  import { reactive } from './proxy'
6
- import { type ScopedCallback } from './types'
7
-
8
- // TODO: use register in a real-world crud situation, have "events" for add, delete, update
6
+ import type { EffectCleanup } from './types'
7
+ import { unreactive } from './watch'
9
8
 
10
9
  type KeyFunction<T, K extends PropertyKey> = (item: T) => K
11
10
 
11
+ /**
12
+ * Events emitted by the Register for CRUD operations
13
+ */
14
+ export interface RegisterEvents<T, K extends PropertyKey> {
15
+ /**
16
+ * Emitted when a new item is added to the register
17
+ */
18
+ add: (item: T, key: K, index: number) => void
19
+ /**
20
+ * Emitted when an item is removed from the register
21
+ */
22
+ delete: (item: T, key: K, index: number) => void
23
+ /**
24
+ * Emitted when an item's value is updated (same key, new value)
25
+ */
26
+ update: (oldItem: T, newItem: T, key: K, index: number) => void
27
+ /**
28
+ * Emitted when an item's key changes (rekey operation)
29
+ */
30
+ rekey: (item: T, oldKey: K, newKey: K, index: number) => void
31
+ /**
32
+ * Index signature for EventsBase compatibility
33
+ */
34
+ [key: string]: (...args: any[]) => void
35
+ }
36
+
12
37
  // Helper to work around TypeScript limitation: base class expressions cannot reference class type parameters
13
38
  function getRegisterBase<T>() {
14
39
  class RegisterBase extends Indexable(ArrayReadForward, {
@@ -37,7 +62,6 @@ function getRegisterBase<T>() {
37
62
  interface RegisterInstance<T> extends ArrayReadForward<T> {
38
63
  [index: number]: T
39
64
  }
40
- // TODO: What to do with prototype forwarding ?
41
65
  @unreactive
42
66
  class RegisterClass<T, K extends PropertyKey = PropertyKey>
43
67
  extends getRegisterBase<any>()
@@ -50,9 +74,54 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
50
74
  readonly #keys: K[]
51
75
  readonly #values: Map<K, T>
52
76
  readonly #usage = new Map<K, number>()
53
- readonly #valueInfo = new Map<T, { key: K; stop?: ScopedCallback }>()
54
- readonly #keyEffects = new Set<ScopedCallback>()
77
+ readonly #valueInfo = new Map<T, { key: K; stop?: EffectCleanup }>()
78
+ readonly #keyEffects = new Set<EffectCleanup>()
55
79
  readonly #ascend: FunctionWrapper
80
+ readonly #events = new Eventful<RegisterEvents<T, K>>()
81
+
82
+ /**
83
+ * Register event listeners for CRUD operations
84
+ */
85
+ on(events: Partial<RegisterEvents<T, K>>): void
86
+ on<EventType extends keyof RegisterEvents<T, K>>(
87
+ event: EventType,
88
+ cb: RegisterEvents<T, K>[EventType]
89
+ ): () => void
90
+ on<EventType extends keyof RegisterEvents<T, K>>(
91
+ eventOrEvents: EventType | Partial<RegisterEvents<T, K>>,
92
+ cb?: RegisterEvents<T, K>[EventType]
93
+ ): () => void {
94
+ // @ts-expect-error Delegate to Eventful
95
+ return this.#events.on(eventOrEvents, cb)
96
+ }
97
+
98
+ /**
99
+ * Remove event listeners
100
+ */
101
+ off(events: Partial<RegisterEvents<T, K>>): void
102
+ off<EventType extends keyof RegisterEvents<T, K>>(
103
+ event: EventType,
104
+ cb?: RegisterEvents<T, K>[EventType]
105
+ ): void
106
+ off<EventType extends keyof RegisterEvents<T, K>>(
107
+ eventOrEvents: EventType | Partial<RegisterEvents<T, K>>,
108
+ cb?: RegisterEvents<T, K>[EventType]
109
+ ): void {
110
+ // @ts-expect-error Delegate to Eventful
111
+ this.#events.off(eventOrEvents, cb)
112
+ }
113
+
114
+ /**
115
+ * Register a global hook that receives all events
116
+ */
117
+ hook(
118
+ cb: <EventType extends keyof RegisterEvents<T, K>>(
119
+ event: EventType,
120
+ ...args: Parameters<RegisterEvents<T, K>[EventType]>
121
+ ) => void
122
+ ): () => void {
123
+ return this.#events.hook(cb)
124
+ }
56
125
 
57
126
  constructor(keyFn: KeyFunction<T, K>, initial?: Iterable<T>) {
58
127
  super()
@@ -84,7 +153,7 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
84
153
  info!.key = nextKey
85
154
  })
86
155
  info!.stop = stop
87
- this.#keyEffects.add(stop)
156
+ this.#keyEffects.add(stop as EffectCleanup)
88
157
  })
89
158
  if (info.key === undefined) throw new Error('Register key function must return a property key')
90
159
  return info.key
@@ -96,10 +165,17 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
96
165
  throw new Error('Register key function must return a property key')
97
166
  }
98
167
 
99
- private setKeyValue(key: K, value: T) {
168
+ private setKeyValue(key: K, value: T, index?: number) {
100
169
  const existing = this.#values.get(key)
101
- if (existing !== undefined && existing !== value) this.cleanupValue(existing)
102
- this.#values.set(key, value)
170
+ if (existing !== undefined && existing !== value) {
171
+ this.cleanupValue(existing)
172
+ this.#values.set(key, value)
173
+ if (index !== undefined) {
174
+ this.#events.emit('update', existing, value, key, index)
175
+ }
176
+ } else {
177
+ this.#values.set(key, value)
178
+ }
103
179
  }
104
180
 
105
181
  private cleanupValue(value: T) {
@@ -128,13 +204,20 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
128
204
  if (!count) return
129
205
  const existingCount = this.#usage.get(newKey) ?? 0
130
206
  this.setKeyValue(newKey, value)
207
+ let index = -1
131
208
  for (let i = 0; i < this.#keys.length; i++)
132
- if (Object.is(this.#keys[i], oldKey)) this.#keys[i] = newKey
209
+ if (Object.is(this.#keys[i], oldKey)) {
210
+ this.#keys[i] = newKey
211
+ if (index === -1) index = i
212
+ }
133
213
  this.#usage.set(newKey, existingCount + count)
134
214
  this.#usage.delete(oldKey)
135
215
  this.#values.delete(oldKey)
136
216
  const updatedInfo = this.#valueInfo.get(value)
137
217
  if (updatedInfo) updatedInfo.key = newKey
218
+ if (index !== -1) {
219
+ this.#events.emit('rekey', value, oldKey, newKey, index)
220
+ }
138
221
  }
139
222
 
140
223
  private incrementUsage(key: K) {
@@ -142,14 +225,18 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
142
225
  this.#usage.set(key, count + 1)
143
226
  }
144
227
 
145
- private decrementUsage(key: K) {
228
+ private decrementUsage(key: K, index?: number) {
146
229
  const count = this.#usage.get(key)
147
230
  if (!count) return
148
231
  if (count <= 1) {
149
232
  const value = this.#values.get(key)
150
233
  this.#usage.delete(key)
151
234
  this.#values.delete(key)
152
- if (value !== undefined) this.cleanupValue(value)
235
+ if (value !== undefined) {
236
+ this.cleanupValue(value)
237
+ const idx = index ?? this.#keys.indexOf(key)
238
+ this.#events.emit('delete', value, key, idx)
239
+ }
153
240
  } else {
154
241
  this.#usage.set(key, count - 1)
155
242
  }
@@ -170,19 +257,25 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
170
257
  private assignAt(index: number, key: K, value: T) {
171
258
  const oldKey = this.#keys[index]
172
259
  if (oldKey !== undefined && Object.is(oldKey, key)) {
260
+ const oldValue = this.#values.get(key)
173
261
  this.setKeyValue(key, value)
262
+ if (oldValue !== undefined && oldValue !== value) {
263
+ this.#events.emit('update', oldValue, value, key, index)
264
+ }
174
265
  return
175
266
  }
176
267
  if (oldKey !== undefined) this.decrementUsage(oldKey as K)
177
268
  this.#keys[index] = key
178
269
  this.incrementUsage(key)
179
270
  this.setKeyValue(key, value)
271
+ this.#events.emit('add', value, key, index)
180
272
  }
181
273
 
182
274
  private insertKeyValue(index: number, key: K, value: T) {
183
275
  this.#keys.splice(index, 0, key)
184
276
  this.incrementUsage(key)
185
277
  this.setKeyValue(key, value)
278
+ this.#events.emit('add', value, key, index)
186
279
  }
187
280
 
188
281
  private rebuildFrom(values: T[]) {
@@ -255,10 +348,11 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
255
348
  for (const item of items) keysToInsert.push(this.ensureKey(item))
256
349
  const removedKeys = this.#keys.splice(normalizedStart, actualDelete, ...keysToInsert)
257
350
  const removedValues: T[] = []
258
- for (const key of removedKeys) {
351
+ for (let i = 0; i < removedKeys.length; i++) {
352
+ const key = removedKeys[i]
259
353
  if (key === undefined) continue
260
354
  const value = this.#values.get(key as K)
261
- this.decrementUsage(key as K)
355
+ this.decrementUsage(key as K, normalizedStart + i)
262
356
  removedValues.push(value as T)
263
357
  }
264
358
  for (let i = 0; i < keysToInsert.length; i++) {
@@ -296,7 +390,7 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
296
390
  const [key] = this.#keys.splice(index, 1)
297
391
  if (key === undefined) return undefined
298
392
  const value = this.#values.get(key as K)
299
- this.decrementUsage(key as K)
393
+ this.decrementUsage(key as K, index)
300
394
  return value
301
395
  }
302
396
 
@@ -336,7 +430,10 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
336
430
  update(...values: T[]): void {
337
431
  for (const value of values) {
338
432
  const key = this.ensureKey(value)
339
- if (this.#values.has(key)) this.setKeyValue(key, value)
433
+ if (this.#values.has(key)) {
434
+ const index = this.#keys.indexOf(key)
435
+ this.setKeyValue(key, value, index)
436
+ }
340
437
  }
341
438
  }
342
439
 
@@ -411,6 +508,28 @@ export const Register = RegisterClass as new <T, K extends PropertyKey = Propert
411
508
  initial?: Iterable<T>
412
509
  ) => Register<T, K>
413
510
 
511
+ /**
512
+ * Creates a reactive Register - an ordered, array-like collection with stable key-based identity.
513
+ *
514
+ * Register combines array semantics (indexable access, ordering, iteration) with Map-like
515
+ * key-based lookups. Items with the same key share the same value instance, making it ideal
516
+ * for UI lists keyed by IDs or when you need to preserve identity across reorders.
517
+ *
518
+ * @param keyFn - Function that extracts the key from each item
519
+ * @param initial - Optional initial items to populate the register
520
+ * @returns A reactive Register instance
521
+ *
522
+ * @example
523
+ * ```typescript
524
+ * const users = register(
525
+ * (user: User) => user.id,
526
+ * [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
527
+ * )
528
+ *
529
+ * users.push({ id: 3, name: 'Charlie' })
530
+ * const bob = users.get(2) // Get by key
531
+ * ```
532
+ */
414
533
  export function register<T, K extends PropertyKey = PropertyKey>(
415
534
  keyFn: KeyFunction<T, K>,
416
535
  initial?: Iterable<T>
@@ -1,19 +1,32 @@
1
- import { rootFunction, type ScopedCallback } from './types'
1
+ import { type EffectCleanup, type EffectNode, type EffectTrigger, rootFunction } from './types'
2
2
 
3
3
  // Track which effects are watching which reactive objects for cleanup
4
- export const effectToReactiveObjects = new WeakMap<ScopedCallback, Set<object>>()
4
+ export let effectToReactiveObjects = new WeakMap<EffectTrigger, Set<object>>()
5
5
 
6
6
  // Track effects per reactive object and property
7
- export const watchers = new WeakMap<object, Map<any, Set<ScopedCallback>>>()
7
+ export let watchers = new WeakMap<object, Map<any, Set<EffectTrigger>>>()
8
8
 
9
- // runEffect -> set<stop>
10
- export const effectChildren = new WeakMap<ScopedCallback, Set<ScopedCallback>>()
9
+ // Track effect metadata and relationships
10
+ export let effectNodes = new WeakMap<EffectTrigger, EffectNode>()
11
11
 
12
- // Track parent effect relationships for hierarchy traversal (used in deep touch filtering)
13
- export const effectParent = new WeakMap<ScopedCallback, ScopedCallback | undefined>()
12
+ export function getEffectNode(effect: EffectTrigger): EffectNode {
13
+ let node = effectNodes.get(effect)
14
+ if (!node) {
15
+ node = {}
16
+ effectNodes.set(effect, node)
17
+ }
18
+ return node
19
+ }
14
20
 
15
21
  // Track reverse mapping to ensure unicity: One Root -> One Function
16
- const reverseRoots = new WeakMap<any, WeakRef<Function>>()
22
+ let reverseRoots = new WeakMap<any, WeakRef<Function>>()
23
+
24
+ export function resetRegistry() {
25
+ effectToReactiveObjects = new WeakMap()
26
+ watchers = new WeakMap()
27
+ effectNodes = new WeakMap()
28
+ reverseRoots = new WeakMap()
29
+ }
17
30
 
18
31
  /**
19
32
  * Marks a function with its root function for effect tracking
@@ -56,4 +69,4 @@ export function markWithRoot<T extends Function>(fn: T, root: any): T {
56
69
  export function getRoot<T extends Function | undefined>(fn: T): T {
57
70
  while (fn && rootFunction in fn) fn = fn[rootFunction] as T
58
71
  return fn
59
- }
72
+ }
@@ -9,9 +9,6 @@ import { dependant } from './tracking'
9
9
  * Only tracks individual value operations, no size tracking (WeakSet limitation)
10
10
  */
11
11
  export abstract class ReactiveWeakSet<T extends object> extends WeakSet<T> {
12
- get [Symbol.toStringTag]() {
13
- return 'ReactiveWeakSet'
14
- }
15
12
  add(value: T): this {
16
13
  const had = this.has(value)
17
14
  this.add(value)
@@ -41,10 +38,6 @@ export abstract class ReactiveWeakSet<T extends object> extends WeakSet<T> {
41
38
  * Tracks size changes, individual value operations, and collection-wide operations
42
39
  */
43
40
  export abstract class ReactiveSet<T> extends Set<T> {
44
- get [Symbol.toStringTag]() {
45
- return 'ReactiveSet'
46
- }
47
-
48
41
  get size(): number {
49
42
  // size depends on the wrapper instance, like Map counterpart
50
43
  dependant(this, 'size')
@@ -112,19 +105,13 @@ export abstract class ReactiveSet<T> extends Set<T> {
112
105
 
113
106
  [Symbol.iterator](): SetIterator<T> {
114
107
  dependant(contentRef(this))
115
- const nativeIterator = this[Symbol.iterator]()
116
- return {
117
- next() {
118
- const result = nativeIterator.next()
119
- if (result.done) {
120
- return result
121
- }
122
- return { value: reactive(result.value), done: false }
123
- },
124
- [Symbol.iterator]() {
125
- return this
126
- },
127
- [Symbol.dispose]() {},
108
+ const it: SetIterator<T> = Set.prototype[Symbol.iterator].call(this)
109
+ const nativeNext = it.next.bind(it)
110
+ it.next = () => {
111
+ const result = nativeNext()
112
+ if (result.done) return result
113
+ return { value: reactive(result.value), done: false }
128
114
  }
115
+ return it
129
116
  }
130
117
  }
@@ -1,10 +1,7 @@
1
1
  import { getActiveEffect } from './effect-context'
2
2
  import { unwrap } from './proxy-state'
3
- import {
4
- effectToReactiveObjects,
5
- watchers,
6
- } from './registry'
7
- import { allProps, type ScopedCallback } from './types'
3
+ import { effectToReactiveObjects, watchers } from './registry'
4
+ import { allProps, type EffectTrigger } from './types'
8
5
 
9
6
  /**
10
7
  * Marks a property as a dependency of the current effect
@@ -16,20 +13,20 @@ export function dependant(obj: any, prop: any = allProps) {
16
13
  const currentActiveEffect = getActiveEffect()
17
14
 
18
15
  // Early return if no active effect, tracking disabled, or invalid prop
19
- if (
20
- !currentActiveEffect ||
21
- (typeof prop === 'symbol' && prop !== allProps)
22
- )
23
- return
16
+ if (!currentActiveEffect || (typeof prop === 'symbol' && prop !== allProps)) return
24
17
 
18
+ if ('dependencyHook' in currentActiveEffect) {
19
+ // @ts-expect-error We declared it nowhere - it's okay as it's really internal and for edge-case debug purpose only
20
+ currentActiveEffect.dependencyHook(obj, prop)
21
+ }
25
22
  let objectWatchers = watchers.get(obj)
26
23
  if (!objectWatchers) {
27
- objectWatchers = new Map<PropertyKey, Set<ScopedCallback>>()
24
+ objectWatchers = new Map<PropertyKey, Set<EffectTrigger>>()
28
25
  watchers.set(obj, objectWatchers)
29
26
  }
30
27
  let deps = objectWatchers.get(prop)
31
28
  if (!deps) {
32
- deps = new Set<ScopedCallback>()
29
+ deps = new Set<EffectTrigger>()
33
30
  objectWatchers.set(prop, deps)
34
31
  }
35
32
  deps.add(currentActiveEffect)
@@ -1,15 +1,17 @@
1
1
  // biome-ignore-all lint/suspicious/noConfusingVoidType: Type 'void' is not assignable to type 'ScopedCallback | undefined'.
2
2
  // Argument of type '() => void' is not assignable to parameter of type '(dep: DependencyFunction) => ScopedCallback | undefined'.
3
3
 
4
- import { FunctionWrapper } from "../zone"
4
+ import type { FunctionWrapper } from '../zone'
5
+
6
+ export type EffectAccessEvents = {
7
+ triggered(event: string, ...args: any[]): void
8
+ }
5
9
 
6
10
  /**
7
- * Dependency access passed to user callbacks within effects/watch
11
+ * Effect access passed to user callbacks within effects/watch
8
12
  * Provides functions to track dependencies and information about the effect execution
9
13
  */
10
- export interface DependencyAccess {
11
- // TODO: remove tracked (async is managed)
12
- // TODO: remove ascend (make a global like `untracked` who withEffect(parentEffect, () => {}))
14
+ export interface EffectAccess {
13
15
  /**
14
16
  * Tracks dependencies in the current effect context
15
17
  * Use this for normal dependency tracking within the effect
@@ -64,10 +66,60 @@ export interface DependencyAccess {
64
66
  // It automatically preserves effect context across Promise boundaries (.then, .catch, .finally)
65
67
 
66
68
  /**
67
- * Type for effect cleanup functions
69
+ * Base type for effect callbacks - simple function without additional properties
68
70
  */
69
71
  export type ScopedCallback = () => void
70
72
 
73
+ /**
74
+ * Type for effect cleanup functions with stopped state tracking
75
+ */
76
+ export type EffectCleanup = ScopedCallback & {
77
+ [stopped]: boolean
78
+ }
79
+
80
+ // Debug type for stack frames
81
+ export type StackFrame = {
82
+ functionName: string
83
+ fileName: string
84
+ lineNumber: number
85
+ columnNumber: number
86
+ raw: string
87
+ }
88
+
89
+ export type EffectTracking = (obj: any, evolution: Evolution, prop: any, effect: EffectTrigger) => void
90
+
91
+ /**
92
+ * Centralized node for all effect metadata and relationships
93
+ */
94
+ export interface EffectNode {
95
+ // Graph relationships
96
+ parent?: EffectTrigger
97
+ children?: Set<EffectCleanup>
98
+
99
+ // Lifecycle
100
+ cleanup?: ScopedCallback
101
+ stopped?: boolean
102
+
103
+ // Error handling
104
+ forwardThrow?: CatchFunction
105
+ catchers?: CatchFunction[]
106
+
107
+ // Debug / Metadata
108
+ creationStack?: StackFrame[]
109
+ dependencyHook?: (obj: any, prop: any) => void
110
+
111
+ // Tracking
112
+ trackers?: EffectTracking[]
113
+
114
+ // Configuration
115
+ isOpaque?: boolean
116
+ }
117
+
118
+ /**
119
+ * Type for the `runEffect` function of an effect - argument-less function to call to trigger the effect
120
+ */
121
+ export type EffectTrigger = ScopedCallback
122
+
71
123
  /**
72
124
  * Async execution mode for effects
73
125
  * - `cancel`: Cancel previous async execution when dependencies change (default)
@@ -91,6 +143,14 @@ export interface EffectOptions {
91
143
  * Use this for effects that depend on object identity (like memoize).
92
144
  */
93
145
  opaque?: boolean
146
+ /**
147
+ * Used for debugging purpose. Provides a callback to be called every time a dependency is created.
148
+ */
149
+ dependencyHook?: (obj: any, prop: any) => void
150
+ /**
151
+ * Used for debugging purpose. Provides a name for the effect.
152
+ */
153
+ name?: string
94
154
  }
95
155
 
96
156
  /**
@@ -149,6 +209,12 @@ export const stopped = Symbol('stopped')
149
209
  */
150
210
  export const cleanup = Symbol('cleanup')
151
211
 
212
+ export const forwardThrow = Symbol('throw')
213
+
214
+ export type EffectCloser = (error?: any) => void
215
+ //biome-ignore lint/suspicious/noConfusingVoidType: We have to
216
+ export type CatchFunction = (error: any) => EffectCloser | undefined | void
217
+
152
218
  /**
153
219
  * Context for a running projection item effect
154
220
  */
@@ -193,15 +259,11 @@ export type MaxReactionDebugInfo = {
193
259
  effect: string
194
260
  }
195
261
 
196
- export type BrokenEffectsDebugInfo = {
197
- code: ReactiveErrorCode.BrokenEffects
198
- cause: any
199
- }
200
-
201
262
  export type GenericDebugInfo = {
202
263
  code: ReactiveErrorCode
203
264
  causalChain?: string[]
204
- creationStack?: string
265
+ creationStack?: string | any[]
266
+ lineage?: any[]
205
267
  [key: string]: any
206
268
  }
207
269
 
@@ -209,7 +271,6 @@ export type ReactiveDebugInfo =
209
271
  | CycleDebugInfo
210
272
  | MaxDepthDebugInfo
211
273
  | MaxReactionDebugInfo
212
- | BrokenEffectsDebugInfo
213
274
  | GenericDebugInfo
214
275
 
215
276
  /**
@@ -271,13 +332,13 @@ export const options = {
271
332
  * @param props - The properties that changed
272
333
  * @param deps - The dependencies that changed
273
334
  */
274
- touched: (_obj: any, _evolution: Evolution, _props?: any[], _deps?: Set<ScopedCallback>) => {},
335
+ touched: (_obj: any, _evolution: Evolution, _props?: any[], _deps?: Set<EffectTrigger>) => {},
275
336
  /**
276
337
  * Debug purpose: called when an effect is skipped because it's already running
277
338
  * @param effect - The effect that is already running
278
339
  * @param runningChain - The array of effects from the detected one to the currently running one
279
340
  */
280
- skipRunningEffect: (_effect: ScopedCallback) => {},
341
+ skipRunningEffect: (_effect: EffectTrigger) => {},
281
342
  /**
282
343
  * Debug purpose: maximum effect chain (like call stack max depth)
283
344
  * Used to prevent infinite loops
@@ -327,20 +388,20 @@ export const options = {
327
388
  /**
328
389
  * How to handle cycles detected in effect batches.
329
390
  *
330
- * - `'none'` (Default): High-performance mode. Disables dependency graph maintenance and
391
+ * - `'production'` (Default): High-performance mode. Disables dependency graph maintenance and
331
392
  * Topological Sorting in favor of a simple FIFO queue. Use this for trustworthy, acyclic UI code.
332
- * Cycle detection is heuristic (uses execution counts).
393
+ * Cycle detection is heuristic (uses maxEffectChain execution counts).
333
394
  *
334
- * - `'throw'`: Traditional Topological Sorting. Guarantees dependency order and catches
335
- * circular dependencies mathematically before execution.
395
+ * - `'development'`: Maintains direct dependency graph for early cycle detection during edge creation.
396
+ * Catches cycles before effects execute via DFS check when adding edges. Throws immediately with
397
+ * basic path information. Good balance of debugging help with moderate overhead.
336
398
  *
337
- * - `'warn'`: Topological sorting, but logs a warning instead of throwing on cycles.
338
- * - `'break'`: Topological sorting, but silently breaks cycles.
339
- * - `'strict'`: Prevents cycle creation by checking the graph *during* dependency discovery.
399
+ * - `'debug'`: Full diagnostic mode with transitive closures and topological sorting.
400
+ * Provides detailed cycle path reporting. Highest overhead but most informative for bug hunting.
340
401
  *
341
- * @default 'none'
402
+ * @default 'production'
342
403
  */
343
- cycleHandling: 'none' as 'none' | 'throw' | 'warn' | 'break' | 'strict',
404
+ cycleHandling: 'development' as 'production' | 'development' | 'debug',
344
405
  /**
345
406
  * Internal flag used by memoization discrepancy detector to avoid counting calls in tests
346
407
  * @warning Do not modify this flag manually, this flag is given by the engine
@@ -437,4 +498,23 @@ export const options = {
437
498
  }
438
499
  // biome-ignore-end lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
439
500
 
501
+ type CallableOption = {
502
+ [K in keyof typeof options]: (typeof options)[K] extends ((...args: any[]) => any) | undefined
503
+ ? K
504
+ : never
505
+ }[keyof typeof options]
506
+
507
+ export function optionCall<K extends CallableOption>(
508
+ name: K,
509
+ ...args: NonNullable<(typeof options)[K]> extends (...a: infer A) => unknown ? A : never
510
+ ): void {
511
+ const fn = options[name]
512
+ if (typeof fn !== 'function') return
513
+ try {
514
+ ;(fn as Function)(...args)
515
+ } catch (error) {
516
+ options.warn(`options.${name} threw`, error)
517
+ }
518
+ }
519
+
440
520
  export { type State, nativeReactive, rootFunction }