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,517 @@
1
+ /**
2
+ * Debug utilities for the reactivity system
3
+ * - Captures effect metadata (names, parent relationships)
4
+ * - Records cause → consequence edges with object/prop labels
5
+ * - Provides graph data for tooling (DevTools panel, etc.)
6
+ */
7
+
8
+ import { effectParent, effectToReactiveObjects, getRoot } from './tracking'
9
+ import { allProps, type Evolution, options, type ScopedCallback } from './types'
10
+
11
+ const EXTERNAL_SOURCE = Symbol('external-source')
12
+ type SourceEffect = ScopedCallback | typeof EXTERNAL_SOURCE
13
+
14
+ let devtoolsEnabled = false
15
+
16
+ // Registry for debugging (populated lazily when DevTools are enabled)
17
+ const debugEffectRegistry = new Set<ScopedCallback>()
18
+ const debugObjectRegistry = new Set<object>()
19
+
20
+ // Human-friendly names
21
+ const effectNames = new WeakMap<ScopedCallback, string>()
22
+ const objectNames = new WeakMap<object, string>()
23
+ let effectCounter = 0
24
+ let objectCounter = 0
25
+
26
+ // Cause/consequence edges aggregated by (source, target, descriptor)
27
+ interface TriggerRecord {
28
+ label: string
29
+ object: object
30
+ prop: any
31
+ evolution: Evolution
32
+ count: number
33
+ lastTriggered: number
34
+ }
35
+
36
+ const triggerGraph = new Map<SourceEffect, Map<ScopedCallback, Map<string, TriggerRecord>>>()
37
+
38
+ export type NodeKind = 'effect' | 'external' | 'state'
39
+ export type EdgeKind = 'cause' | 'dependency' | 'trigger'
40
+
41
+ export interface EffectNode {
42
+ id: string
43
+ label: string
44
+ type: NodeKind
45
+ depth: number
46
+ parentId?: string
47
+ debugName?: string
48
+ }
49
+
50
+ export interface ObjectNode {
51
+ id: string
52
+ label: string
53
+ type: NodeKind
54
+ debugName?: string
55
+ }
56
+
57
+ export interface GraphEdge {
58
+ id: string
59
+ source: string
60
+ target: string
61
+ type: EdgeKind
62
+ label: string
63
+ count?: number
64
+ }
65
+
66
+ export interface ReactivityGraph {
67
+ nodes: Array<EffectNode | ObjectNode>
68
+ edges: GraphEdge[]
69
+ meta: {
70
+ generatedAt: number
71
+ devtoolsEnabled: boolean
72
+ }
73
+ }
74
+
75
+ function ensureEffectName(effect: ScopedCallback): string {
76
+ let name = effectNames.get(effect)
77
+ if (!name) {
78
+ const root = getRoot(effect)
79
+ name = root?.name?.trim() || `effect_${++effectCounter}`
80
+ effectNames.set(effect, name)
81
+ }
82
+ return name
83
+ }
84
+
85
+ function ensureObjectName(obj: object): string {
86
+ let name = objectNames.get(obj)
87
+ if (!name) {
88
+ const ctorName = (obj as any)?.constructor?.name
89
+ const base = ctorName && ctorName !== 'Object' ? ctorName : 'object'
90
+ name = `${base}_${++objectCounter}`
91
+ objectNames.set(obj, name)
92
+ }
93
+ return name
94
+ }
95
+
96
+ function describeProp(obj: object, prop: any): string {
97
+ const objectName = ensureObjectName(obj)
98
+ if (prop === allProps) return `${objectName}.*`
99
+ if (typeof prop === 'symbol') return `${objectName}.${prop.description ?? prop.toString()}`
100
+ return `${objectName}.${String(prop)}`
101
+ }
102
+
103
+ function addEffectToRegistry(effect: ScopedCallback) {
104
+ if (!effect || debugEffectRegistry.has(effect)) return
105
+ debugEffectRegistry.add(effect)
106
+ const deps = effectToReactiveObjects.get(effect)
107
+ if (deps) {
108
+ for (const obj of deps) {
109
+ documentObject(obj)
110
+ }
111
+ }
112
+ }
113
+
114
+ function documentObject(obj: object) {
115
+ if (!debugObjectRegistry.has(obj)) {
116
+ dbRegisterObject(obj)
117
+ }
118
+ }
119
+
120
+ function dbRegisterObject(obj: object) {
121
+ debugObjectRegistry.add(obj)
122
+ ensureObjectName(obj)
123
+ }
124
+
125
+ function ensureParentChains(effects: Set<ScopedCallback>) {
126
+ const queue = Array.from(effects)
127
+ for (let i = 0; i < queue.length; i++) {
128
+ const effect = queue[i]
129
+ const parent = effectParent.get(effect)
130
+ if (parent && !effects.has(parent)) {
131
+ effects.add(parent)
132
+ queue.push(parent)
133
+ }
134
+ }
135
+ }
136
+
137
+ function ensureTriggerContainers(source: SourceEffect) {
138
+ let targetMap = triggerGraph.get(source)
139
+ if (!targetMap) {
140
+ targetMap = new Map()
141
+ triggerGraph.set(source, targetMap)
142
+ }
143
+ return targetMap
144
+ }
145
+
146
+ function ensureTriggerRecord(
147
+ source: SourceEffect,
148
+ target: ScopedCallback,
149
+ label: string,
150
+ obj: object,
151
+ prop: any,
152
+ evolution: Evolution
153
+ ): TriggerRecord {
154
+ const targetMap = ensureTriggerContainers(source)
155
+ let labelMap = targetMap.get(target)
156
+ if (!labelMap) {
157
+ labelMap = new Map()
158
+ targetMap.set(target, labelMap)
159
+ }
160
+ let record = labelMap.get(label)
161
+ if (!record) {
162
+ record = { label, object: obj, prop, evolution, count: 0, lastTriggered: Date.now() }
163
+ labelMap.set(label, record)
164
+ }
165
+ return record
166
+ }
167
+
168
+ /**
169
+ * Assign a debug-friendly name to an effect (shown in DevTools)
170
+ */
171
+ export function setEffectName(effect: ScopedCallback, name: string) {
172
+ effectNames.set(effect, name)
173
+ }
174
+
175
+ /**
176
+ * Assign a debug-friendly name to a reactive object
177
+ */
178
+ export function setObjectName(obj: object, name: string) {
179
+ objectNames.set(obj, name)
180
+ debugObjectRegistry.add(obj)
181
+ }
182
+
183
+ /**
184
+ * Register an effect so it appears in the DevTools graph
185
+ */
186
+ export function registerEffectForDebug(effect: ScopedCallback) {
187
+ if (!effect || !devtoolsEnabled) return
188
+ addEffectToRegistry(effect)
189
+ }
190
+
191
+ /**
192
+ * Register a reactive object so it appears in the DevTools graph
193
+ */
194
+ export function registerObjectForDebug(obj: object) {
195
+ if (!devtoolsEnabled) return
196
+ documentObject(obj)
197
+ }
198
+
199
+ /**
200
+ * Records a cause → consequence relationship between effects.
201
+ * @param source - The effect performing the write (undefined if external/user input)
202
+ * @param target - The effect that re-ran because of the write
203
+ * @param obj - The reactive object that changed
204
+ * @param prop - The property that changed
205
+ * @param evolution - The type of change (set/add/del/bunch)
206
+ */
207
+ export function recordTriggerLink(
208
+ source: ScopedCallback | undefined,
209
+ target: ScopedCallback,
210
+ obj: object,
211
+ prop: any,
212
+ evolution: Evolution
213
+ ) {
214
+ if (options.introspection.enableHistory) {
215
+ addToMutationHistory(source, target, obj, prop, evolution)
216
+ }
217
+ if (!devtoolsEnabled) return
218
+ addEffectToRegistry(target)
219
+ if (source) addEffectToRegistry(source)
220
+ const descriptor = describeProp(obj, prop)
221
+ const record = ensureTriggerRecord(
222
+ source ?? EXTERNAL_SOURCE,
223
+ target,
224
+ descriptor,
225
+ obj,
226
+ prop,
227
+ evolution
228
+ )
229
+ record.count += 1
230
+ record.lastTriggered = Date.now()
231
+ documentObject(obj)
232
+ }
233
+
234
+ /**
235
+ * Traces back the chain of triggers that led to a specific effect
236
+ * @param effect The effect to trace back
237
+ * @param limit Max depth
238
+ */
239
+ export function getTriggerChain(effect: ScopedCallback, limit = 5): string[] {
240
+ const chain: string[] = []
241
+ let current = effect
242
+ for (let i = 0; i < limit; i++) {
243
+ // Find who triggered 'current'
244
+ // We need to reverse search the triggerGraph (source -> target)
245
+ // This is expensive O(Edges) but okay for error reporting
246
+ let foundSource: ScopedCallback | undefined
247
+ let foundReason = ''
248
+
249
+ search: for (const [source, targetMap] of triggerGraph) {
250
+ for (const [target, labelMap] of targetMap) {
251
+ if (target === current) {
252
+ // Found a source! Use the most recent trigger record
253
+ let lastTime = 0
254
+ for (const record of labelMap.values()) {
255
+ if (record.lastTriggered > lastTime) {
256
+ lastTime = record.lastTriggered
257
+ foundReason = record.label
258
+ foundSource = source === EXTERNAL_SOURCE ? undefined : (source as ScopedCallback)
259
+ }
260
+ }
261
+ if (foundSource || foundReason) break search
262
+ }
263
+ }
264
+ }
265
+
266
+ if (foundSource) {
267
+ chain.push(
268
+ `${ensureEffectName(foundSource)} -> (${foundReason}) -> ${ensureEffectName(current)}`
269
+ )
270
+ current = foundSource
271
+ } else if (foundReason) {
272
+ chain.push(`External -> (${foundReason}) -> ${ensureEffectName(current)}`)
273
+ break
274
+ } else {
275
+ break
276
+ }
277
+ }
278
+ return chain.reverse()
279
+ }
280
+
281
+ function buildEffectNodes(allEffects: Set<ScopedCallback>) {
282
+ const nodes: EffectNode[] = []
283
+ const nodeByEffect = new Map<ScopedCallback, EffectNode>()
284
+
285
+ const ordered = Array.from(allEffects)
286
+ for (const effect of ordered) {
287
+ const label = ensureEffectName(effect)
288
+ const node: EffectNode = {
289
+ id: `effect_${nodes.length}`,
290
+ label,
291
+ type: 'effect',
292
+ depth: 0,
293
+ debugName: label,
294
+ }
295
+ nodes.push(node)
296
+ nodeByEffect.set(effect, node)
297
+ }
298
+
299
+ const depthCache = new Map<ScopedCallback, number>()
300
+ const computeDepth = (effect: ScopedCallback | undefined): number => {
301
+ if (!effect) return 0
302
+ const cached = depthCache.get(effect)
303
+ if (cached !== undefined) return cached
304
+ const parent = effectParent.get(effect)
305
+ const depth = computeDepth(parent) + (parent ? 1 : 0)
306
+ depthCache.set(effect, depth)
307
+ return depth
308
+ }
309
+
310
+ for (const [effect, node] of nodeByEffect) {
311
+ node.depth = computeDepth(effect)
312
+ const parent = effectParent.get(effect)
313
+ if (parent) {
314
+ const parentNode = nodeByEffect.get(parent)
315
+ if (parentNode) {
316
+ node.parentId = parentNode.id
317
+ }
318
+ }
319
+ }
320
+
321
+ return { nodes, nodeByEffect }
322
+ }
323
+
324
+ /**
325
+ * Builds a graph representing current reactive state (effects, objects, and trigger edges)
326
+ */
327
+ export function buildReactivityGraph(): ReactivityGraph {
328
+ const nodes: Array<EffectNode | ObjectNode> = []
329
+ const edges: GraphEdge[] = []
330
+ const nodeIds = new Map<ScopedCallback | object | SourceEffect, string>()
331
+
332
+ const allEffects = new Set<ScopedCallback>(debugEffectRegistry)
333
+ ensureParentChains(allEffects)
334
+ const { nodes: effectNodes, nodeByEffect } = buildEffectNodes(allEffects)
335
+ for (const node of effectNodes) nodes.push(node)
336
+ for (const [effect, node] of nodeByEffect) {
337
+ nodeIds.set(effect, node.id)
338
+ }
339
+
340
+ // Object nodes (optional, used for dependency inspection)
341
+ for (const obj of debugObjectRegistry) {
342
+ const id = `object_${nodes.length}`
343
+ nodes.push({ id, label: ensureObjectName(obj), type: 'state', debugName: objectNames.get(obj) })
344
+ nodeIds.set(obj, id)
345
+ }
346
+
347
+ // External source node (user/system outside of effects)
348
+ if (triggerGraph.has(EXTERNAL_SOURCE)) {
349
+ const externalId = `effect_external`
350
+ nodes.push({ id: externalId, label: 'External', type: 'external', depth: 0 })
351
+ nodeIds.set(EXTERNAL_SOURCE, externalId)
352
+ }
353
+
354
+ // Dependency edges (effect → object)
355
+ for (const effect of allEffects) {
356
+ const effectId = nodeIds.get(effect)
357
+ if (!effectId) continue
358
+ const deps = effectToReactiveObjects.get(effect)
359
+ if (!deps) continue
360
+ for (const obj of deps) {
361
+ const objId = nodeIds.get(obj)
362
+ if (!objId) continue
363
+ edges.push({
364
+ id: `${effectId}->${objId}`,
365
+ source: effectId,
366
+ target: objId,
367
+ type: 'dependency',
368
+ label: 'depends',
369
+ })
370
+ }
371
+ }
372
+
373
+ // Cause edges (effect/object/prop → effect)
374
+ for (const [source, targetMap] of triggerGraph) {
375
+ for (const [targetEffect, labelMap] of targetMap) {
376
+ const targetId = nodeIds.get(targetEffect)
377
+ if (!targetId) continue
378
+ const sourceId = nodeIds.get(source)
379
+ if (!sourceId) continue
380
+ for (const record of labelMap.values()) {
381
+ edges.push({
382
+ id: `${sourceId}->${targetId}:${record.label}`,
383
+ source: sourceId,
384
+ target: targetId,
385
+ type: 'cause',
386
+ label: record.count > 1 ? `${record.label} (${record.count})` : record.label,
387
+ count: record.count,
388
+ })
389
+ }
390
+ }
391
+ }
392
+
393
+ return {
394
+ nodes,
395
+ edges,
396
+ meta: {
397
+ generatedAt: Date.now(),
398
+ devtoolsEnabled,
399
+ },
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Enables the DevTools bridge and exposes the debug API on window.
405
+ * Call as early as possible in development builds.
406
+ */
407
+ export function enableDevTools() {
408
+ if (typeof window === 'undefined') return
409
+ if (devtoolsEnabled) return
410
+ devtoolsEnabled = true
411
+
412
+ // @ts-expect-error - global window extension
413
+ window.__MUTTS_DEVTOOLS__ = {
414
+ getGraph: buildReactivityGraph,
415
+ setEffectName,
416
+ setObjectName,
417
+ registerEffect: registerEffectForDebug,
418
+ registerObject: registerObjectForDebug,
419
+ }
420
+ }
421
+
422
+ export function forceEnableGraphTracking() {
423
+ devtoolsEnabled = true
424
+ }
425
+
426
+ export function isDevtoolsEnabled() {
427
+ return devtoolsEnabled
428
+ }
429
+
430
+ // --- Introspection API ---
431
+
432
+ /**
433
+ * Returns the raw dependency graph data structure.
434
+ * This is useful for programmatic analysis of the reactive system.
435
+ */
436
+ export function getDependencyGraph() {
437
+ return {
438
+ nodes: buildReactivityGraph().nodes,
439
+ edges: buildReactivityGraph().edges,
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Returns a list of effects that depend on the given object.
445
+ */
446
+ export function getDependents(obj: object): ScopedCallback[] {
447
+ const dependents: ScopedCallback[] = []
448
+ // Scan the trigger graph for effects triggered by this object
449
+ // This is O(E) where E is the number of edges, might need optimization for large graphs
450
+ // but acceptable for introspection
451
+ for (const [_source, targetMap] of triggerGraph) {
452
+ for (const [targetEffect, labelMap] of targetMap) {
453
+ for (const record of labelMap.values()) {
454
+ if (record.object === obj) {
455
+ dependents.push(targetEffect)
456
+ }
457
+ }
458
+ }
459
+ }
460
+ // Also check direct dependencies (dependency graph)
461
+ // We don't have a direct obj -> effect map without walking all effects
462
+ // unless we use `watchers` from tracking.ts but that's internal
463
+ return [...new Set(dependents)]
464
+ }
465
+
466
+ /**
467
+ * Returns a list of objects that the given effect depends on.
468
+ */
469
+ export function getDependencies(effect: ScopedCallback): object[] {
470
+ const deps = effectToReactiveObjects.get(effect)
471
+ return deps ? Array.from(deps) : []
472
+ }
473
+
474
+ // --- Mutation History ---
475
+
476
+ export interface MutationRecord {
477
+ id: number
478
+ timestamp: number
479
+ source: string
480
+ target: string
481
+ objectName: string
482
+ prop: string
483
+ type: string
484
+ }
485
+
486
+ const mutationHistory: MutationRecord[] = []
487
+ let mutationCounter = 0
488
+
489
+ function addToMutationHistory(
490
+ source: ScopedCallback | undefined,
491
+ target: ScopedCallback,
492
+ obj: object,
493
+ prop: any,
494
+ evolution: Evolution
495
+ ) {
496
+ const record: MutationRecord = {
497
+ id: ++mutationCounter,
498
+ timestamp: Date.now(),
499
+ source: source ? ensureEffectName(source) : 'External',
500
+ target: ensureEffectName(target),
501
+ objectName: ensureObjectName(obj),
502
+ prop: String(prop),
503
+ type: evolution.type,
504
+ }
505
+
506
+ mutationHistory.push(record)
507
+ if (mutationHistory.length > options.introspection.historySize) {
508
+ mutationHistory.shift()
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Get the recent mutation history
514
+ */
515
+ export function getMutationHistory(): MutationRecord[] {
516
+ return [...mutationHistory]
517
+ }