mutts 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/browser.d.ts +2 -0
- package/dist/browser.esm.js +70 -0
- package/dist/browser.esm.js.map +1 -0
- package/dist/browser.js +161 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunks/{index-Cvxdw6Ax.js → index-BFYK02LG.js} +5377 -4059
- package/dist/chunks/index-BFYK02LG.js.map +1 -0
- package/dist/chunks/{index-qiWwozOc.esm.js → index-CNR6QRUl.esm.js} +5247 -3963
- package/dist/chunks/index-CNR6QRUl.esm.js.map +1 -0
- package/dist/mutts.umd.js +1 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.esm.js +45 -0
- package/dist/node.esm.js.map +1 -0
- package/dist/node.js +136 -0
- package/dist/node.js.map +1 -0
- package/docs/ai/api-reference.md +0 -2
- package/docs/ai/manual.md +14 -95
- package/docs/reactive/advanced.md +7 -111
- package/docs/reactive/collections.md +0 -125
- package/docs/reactive/core.md +27 -24
- package/docs/reactive/debugging.md +168 -0
- package/docs/reactive/project.md +1 -1
- package/docs/reactive/scan.md +78 -0
- package/docs/reactive.md +8 -6
- package/docs/std-decorators.md +1 -0
- package/docs/zone.md +88 -0
- package/package.json +47 -65
- package/src/async/browser.ts +87 -0
- package/src/async/index.ts +8 -0
- package/src/async/node.ts +46 -0
- package/src/decorator.ts +15 -9
- package/src/destroyable.ts +4 -4
- package/src/index.ts +54 -0
- package/src/indexable.ts +42 -0
- package/src/mixins.ts +2 -2
- package/src/reactive/array.ts +149 -141
- package/src/reactive/buffer.ts +168 -0
- package/src/reactive/change.ts +3 -3
- package/src/reactive/debug.ts +1 -1
- package/src/reactive/deep-touch.ts +1 -1
- package/src/reactive/deep-watch.ts +1 -1
- package/src/reactive/effect-context.ts +15 -91
- package/src/reactive/effects.ts +138 -170
- package/src/reactive/index.ts +10 -13
- package/src/reactive/interface.ts +20 -33
- package/src/reactive/map.ts +48 -61
- package/src/reactive/memoize.ts +87 -31
- package/src/reactive/project.ts +43 -22
- package/src/reactive/proxy.ts +18 -43
- package/src/reactive/record.ts +3 -3
- package/src/reactive/register.ts +5 -7
- package/src/reactive/registry.ts +59 -0
- package/src/reactive/set.ts +42 -56
- package/src/reactive/tracking.ts +5 -62
- package/src/reactive/types.ts +79 -19
- package/src/std-decorators.ts +9 -9
- package/src/utils.ts +203 -19
- package/src/zone.ts +127 -0
- package/dist/chunks/_tslib-BgjropY9.js +0 -81
- package/dist/chunks/_tslib-BgjropY9.js.map +0 -1
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js +0 -75
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +0 -1
- package/dist/chunks/decorator-DLvrD0UF.js +0 -265
- package/dist/chunks/decorator-DLvrD0UF.js.map +0 -1
- package/dist/chunks/decorator-DqiszP7i.esm.js +0 -253
- package/dist/chunks/decorator-DqiszP7i.esm.js.map +0 -1
- package/dist/chunks/index-Cvxdw6Ax.js.map +0 -1
- package/dist/chunks/index-qiWwozOc.esm.js.map +0 -1
- package/dist/decorator.d.ts +0 -107
- package/dist/decorator.esm.js +0 -2
- package/dist/decorator.esm.js.map +0 -1
- package/dist/decorator.js +0 -11
- package/dist/decorator.js.map +0 -1
- package/dist/destroyable.d.ts +0 -90
- package/dist/destroyable.esm.js +0 -109
- package/dist/destroyable.esm.js.map +0 -1
- package/dist/destroyable.js +0 -116
- package/dist/destroyable.js.map +0 -1
- package/dist/eventful.d.ts +0 -20
- package/dist/eventful.esm.js +0 -66
- package/dist/eventful.esm.js.map +0 -1
- package/dist/eventful.js +0 -68
- package/dist/eventful.js.map +0 -1
- package/dist/index.d.ts +0 -19
- package/dist/index.esm.js +0 -8
- package/dist/index.esm.js.map +0 -1
- package/dist/index.js +0 -95
- package/dist/index.js.map +0 -1
- package/dist/indexable.d.ts +0 -243
- package/dist/indexable.esm.js +0 -285
- package/dist/indexable.esm.js.map +0 -1
- package/dist/indexable.js +0 -291
- package/dist/indexable.js.map +0 -1
- package/dist/promiseChain.d.ts +0 -21
- package/dist/promiseChain.esm.js +0 -78
- package/dist/promiseChain.esm.js.map +0 -1
- package/dist/promiseChain.js +0 -80
- package/dist/promiseChain.js.map +0 -1
- package/dist/reactive.d.ts +0 -885
- package/dist/reactive.esm.js +0 -5
- package/dist/reactive.esm.js.map +0 -1
- package/dist/reactive.js +0 -59
- package/dist/reactive.js.map +0 -1
- package/dist/std-decorators.d.ts +0 -52
- package/dist/std-decorators.esm.js +0 -196
- package/dist/std-decorators.esm.js.map +0 -1
- package/dist/std-decorators.js +0 -204
- package/dist/std-decorators.js.map +0 -1
- package/src/reactive/mapped.ts +0 -129
- package/src/reactive/zone.ts +0 -208
package/docs/reactive/core.md
CHANGED
|
@@ -105,7 +105,7 @@ const result = memoized(user)
|
|
|
105
105
|
**5. Map over arrays:**
|
|
106
106
|
```typescript
|
|
107
107
|
const source = reactive([1, 2, 3])
|
|
108
|
-
const doubled =
|
|
108
|
+
const doubled = project(source, ({ value }) => value * 2)
|
|
109
109
|
// [2, 4, 6]
|
|
110
110
|
|
|
111
111
|
source.push(4) // doubled automatically becomes [2, 4, 6, 8]
|
|
@@ -446,13 +446,13 @@ state.c = 15 // Does NOT trigger effect
|
|
|
446
446
|
|
|
447
447
|
### Async Effects and the `access` Parameter
|
|
448
448
|
|
|
449
|
-
The `effect` function provides a special `access` parameter with `tracked` and `ascend` functions that restore the active effect context for dependency tracking in asynchronous operations.
|
|
449
|
+
The `effect` function provides a special `access` parameter with `tracked` and `ascend` functions that restore the active effect context for dependency tracking in asynchronous operations.
|
|
450
450
|
|
|
451
|
-
|
|
451
|
+
In modern `mutts`, this is powered by the **Zone system**. When you use `configureAsyncZone()`, the active effect context is automatically preserved across `await` points and timers, making manual use of `tracked` optional for these cases.
|
|
452
452
|
|
|
453
|
-
|
|
453
|
+
#### The Problem with Async Effects
|
|
454
454
|
|
|
455
|
-
|
|
455
|
+
Traditionally, in JavaScript, async functions lose their context when they yield control.
|
|
456
456
|
|
|
457
457
|
#### Understanding the active effect context
|
|
458
458
|
|
|
@@ -463,37 +463,42 @@ The reactive system uses a global active effect variable to track which effect i
|
|
|
463
463
|
effect(() => {
|
|
464
464
|
// active effect = this effect
|
|
465
465
|
const value = state.count // ✅ Tracked (active effect is set)
|
|
466
|
-
// active effect = this effect (still set)
|
|
467
466
|
const another = state.name // ✅ Tracked (active effect is still set)
|
|
468
467
|
})
|
|
469
468
|
|
|
470
|
-
// Async effect -
|
|
469
|
+
// Async effect WITHOUT configureAsyncZone() - context is lost after await
|
|
471
470
|
effect(async () => {
|
|
472
|
-
|
|
473
|
-
const value = state.count // ✅ Tracked (active effect is set)
|
|
471
|
+
const value = state.count // ✅ Tracked
|
|
474
472
|
|
|
475
|
-
await someAsyncOperation()
|
|
473
|
+
await someAsyncOperation()
|
|
476
474
|
|
|
477
|
-
//
|
|
478
|
-
const another = state.name // ❌ NOT tracked
|
|
475
|
+
// context is lost!
|
|
476
|
+
const another = state.name // ❌ NOT tracked
|
|
479
477
|
})
|
|
480
478
|
|
|
481
|
-
// Async effect
|
|
482
|
-
effect(async (
|
|
483
|
-
|
|
484
|
-
|
|
479
|
+
// Async effect WITH configureAsyncZone() - context is preserved
|
|
480
|
+
effect(async () => {
|
|
481
|
+
const value = state.count // ✅ Tracked
|
|
482
|
+
|
|
483
|
+
await someAsyncOperation()
|
|
485
484
|
|
|
486
|
-
|
|
485
|
+
// context is automatically restored!
|
|
486
|
+
const another = state.name // ✅ Tracked
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
// Using access.tracked() for manual restoration
|
|
490
|
+
effect(async ({ tracked }) => {
|
|
491
|
+
await someAsyncOperation()
|
|
487
492
|
|
|
488
|
-
//
|
|
489
|
-
const another = tracked(() => state.name) // ✅ Tracked
|
|
493
|
+
// Useful for non-patched APIs or explicit scoping
|
|
494
|
+
const another = tracked(() => state.name) // ✅ Tracked
|
|
490
495
|
})
|
|
491
496
|
```
|
|
492
497
|
|
|
493
|
-
#### Key Benefits of
|
|
498
|
+
#### Key Benefits of the Zone System
|
|
494
499
|
|
|
495
|
-
1.
|
|
496
|
-
2.
|
|
500
|
+
1. **Automatic Restoration**: With `configureAsyncZone()`, most native async APIs (Promises, timers) automatically preserve the reactive context.
|
|
501
|
+
2. **Manual Control**: `access.tracked()` allows you to manually "passport" the context into third-party libraries or unmanaged callbacks.
|
|
497
502
|
|
|
498
503
|
### Using `ascend` for Parent Effect Tracking
|
|
499
504
|
|
|
@@ -518,8 +523,6 @@ effect(({ ascend }) => {
|
|
|
518
523
|
}
|
|
519
524
|
})
|
|
520
525
|
```
|
|
521
|
-
<|tool▁calls▁begin|><|tool▁call▁begin|>
|
|
522
|
-
grep
|
|
523
526
|
|
|
524
527
|
**When to use `ascend`:**
|
|
525
528
|
- When creating child effects that should be cleaned up with their parent
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Debugging Tools
|
|
2
|
+
|
|
3
|
+
The `mutts` reactive system provides several built-in tools to help track down synchronization issues, dependency leaks, and infinite loops. These tools are primarily exposed through the `reactiveOptions` object.
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> **Performance Cost**: Most debugging tools incur a significant performance overhead. They should be enabled only during development or within test environments. Re-running computations for discrepancy detection, in particular, effectively doubles the cost of reactive updates.
|
|
7
|
+
|
|
8
|
+
## The `reactiveOptions` Reference
|
|
9
|
+
|
|
10
|
+
The `reactiveOptions` object (exported from `mutts/reactive`) allows you to hook into the reactive system's internals.
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { reactiveOptions } from 'mutts/reactive';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Lifecycle Hooks
|
|
17
|
+
|
|
18
|
+
These hooks are called during the execution of effects and computed values.
|
|
19
|
+
|
|
20
|
+
- **`enter(effect: Function)`**: Called when an effect or memoized function starts executing.
|
|
21
|
+
- **`leave(effect: Function)`**: Called when an effect or memoized function finishes.
|
|
22
|
+
- **`touched(obj: any, evolution: Evolution)`**: Called whenever a reactive object is "touched" (accessed or modified). This is the lowest-level hook for observing system activity.
|
|
23
|
+
|
|
24
|
+
### Effect Chaining & Batching
|
|
25
|
+
|
|
26
|
+
- **`beginChain(targets: Function[]) / endChain()`**: Called when a batch of effects starts and ends its execution.
|
|
27
|
+
- **`maxEffectChain`**: (Default: `100`) Limits the depth of synchronous effect triggering to prevent stack overflows.
|
|
28
|
+
- **`maxTriggerPerBatch`**: (Default: `10`) Limits how many times a single effect can be triggered within the same batch. Useful for detecting aggressive re-computation or infinite cycles in `cycleHandling: 'none'` mode.
|
|
29
|
+
|
|
30
|
+
## Cycle Detection
|
|
31
|
+
|
|
32
|
+
`mutts` automatically detects circular dependencies (e.g., Effect A updates State X, which triggers Effect B, which updates State Y, which triggers Effect A).
|
|
33
|
+
|
|
34
|
+
### Configuration
|
|
35
|
+
|
|
36
|
+
You can control how cycles are handled via `reactiveOptions.cycleHandling`:
|
|
37
|
+
|
|
38
|
+
- **`'none'`** (Default): High-performance FIFO mode. Disables the dependency graph and topological sorting.
|
|
39
|
+
- **`'throw'`**: Throws a `ReactiveError` with a detailed path.
|
|
40
|
+
- **`'warn'`**: Logs a warning but breaks the cycle to allow the application to continue.
|
|
41
|
+
- **`'break'`**: Silently breaks the cycle.
|
|
42
|
+
- **`'strict'`**: Performs a graph check *before* execution to prevent cycles from even starting. This has the highest overhead.
|
|
43
|
+
|
|
44
|
+
### Topological vs. Flat Mode Detection
|
|
45
|
+
|
|
46
|
+
| Mode | `cycleHandling` | Detection Method | Error Code |
|
|
47
|
+
| :--- | :--- | :--- | :--- |
|
|
48
|
+
| **Topological** | `'throw'` (or other) | **Mathematical**: Analyzes the dependency graph. | `CYCLE_DETECTED` |
|
|
49
|
+
| **Flat Mode** | `'none'` (Default) | **Heuristic**: Counts executions per batch. | `MAX_REACTION_EXCEEDED` |
|
|
50
|
+
|
|
51
|
+
In **Topological mode**, the system maintains a transitive closure of all effects, allowing it to know instantly if an effect is its own cause. In **Flat mode**, the system is "blind" to the graph and relies on the execution threshold (`maxTriggerPerBatch`) to interrupt infinite loops.
|
|
52
|
+
|
|
53
|
+
## Memoization Discrepancy Detection
|
|
54
|
+
|
|
55
|
+
The most powerful debugging tool in `mutts` is the **Discrepancy Detector**. It helps identify "missing dependencies"—reactive values used inside a computation that the system isn't tracking.
|
|
56
|
+
|
|
57
|
+
### How it Works
|
|
58
|
+
|
|
59
|
+
When `reactiveOptions.onMemoizationDiscrepancy` is set:
|
|
60
|
+
1. Every time a `memoize()` function is called, it checks its cache.
|
|
61
|
+
2. If a cached value exists, the function is immediately **executed a second time** in a completely untracked context.
|
|
62
|
+
3. If the results differ, your callback is triggered.
|
|
63
|
+
|
|
64
|
+
### Usage
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
reactiveOptions.onMemoizationDiscrepancy = (cached, fresh, fn, args, cause) => {
|
|
68
|
+
console.error(`Discrepancy in ${fn.name || 'anonymous'}!`, {
|
|
69
|
+
cached,
|
|
70
|
+
fresh,
|
|
71
|
+
cause // 'calculation' or 'comparison'
|
|
72
|
+
});
|
|
73
|
+
throw new Error('Memoization discrepancy detected');
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If the cause is 'comparison', it means that, at first computation, calling the function twice gives different results.
|
|
78
|
+
|
|
79
|
+
If the cause is 'calculation', it means that, when asked the value while the cache was still valid, the function returned a different result.
|
|
80
|
+
|
|
81
|
+
### `isVerificationRun`
|
|
82
|
+
|
|
83
|
+
During the "second run" of a discrepancy check, `reactiveOptions.isVerificationRun` is set to `true`. You can use this flag in your own code to avoid side effects (like incrementing counters) that should only happen once during the primary execution.
|
|
84
|
+
|
|
85
|
+
> [!IMPORTANT]
|
|
86
|
+
> Do not modify `isVerificationRun` manually; it is managed by the engine.
|
|
87
|
+
|
|
88
|
+
## Introspection API
|
|
89
|
+
|
|
90
|
+
For programmatic analysis of the reactive system, `mutts` provides a dedicated introspection module. This is particularly useful for AI agents and developer tools.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { enableIntrospection, getDependencyGraph, getMutationHistory, snapshot } from 'mutts/introspection';
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Enabling Introspection
|
|
97
|
+
|
|
98
|
+
Introspection features (like history tracking) are memory-intensive and disabled by default.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Enable history tracking (default size: 50)
|
|
102
|
+
enableIntrospection({ historySize: 100 });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Dependency Graph
|
|
106
|
+
|
|
107
|
+
You can retrieve the full dependency graph to understand how objects and effects are linked.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const graph = getDependencyGraph();
|
|
111
|
+
// Returns: { nodes: Array<EffectNode | ObjectNode>, edges: Array<GraphEdge> }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Mutation History
|
|
115
|
+
|
|
116
|
+
If `enableHistory` is on, you can inspect the sequence of mutations that have occurred.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const history = getMutationHistory();
|
|
120
|
+
// Each record contains type, property, old/new values, and causal source.
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Structured Error Handling
|
|
124
|
+
|
|
125
|
+
When the reactive system encounters a critical failure (like a cycle or max depth exceeded), it throws a `ReactiveError` containing rich diagnostic information.
|
|
126
|
+
|
|
127
|
+
### `ReactiveErrorCode`
|
|
128
|
+
|
|
129
|
+
Always check `error.debugInfo.code` to identify the failure type:
|
|
130
|
+
- `CYCLE_DETECTED`: A circular dependency was found.
|
|
131
|
+
- `MAX_DEPTH_EXCEEDED`: The synchronous effect chain reached `maxEffectChain`.
|
|
132
|
+
- `MAX_REACTION_EXCEEDED`: An effect was triggered too many times in a single batch.
|
|
133
|
+
- `WRITE_IN_COMPUTED`: An attempt was made to modify reactive state inside a `memoize` or `derived` function.
|
|
134
|
+
|
|
135
|
+
### Rich Debug Info
|
|
136
|
+
|
|
137
|
+
The `debugInfo` property on `ReactiveError` includes:
|
|
138
|
+
- **`causalChain`**: A string array describing the logical path of modifications leading to the error.
|
|
139
|
+
- **`creationStack`**: The stack trace of where the effect was originally created, helping you locate the source in your code.
|
|
140
|
+
- **`cycle`**: (For `CYCLE_DETECTED`) The names of the effects that form the loop.
|
|
141
|
+
|
|
142
|
+
## Best Practices for Debugging
|
|
143
|
+
|
|
144
|
+
### Naming Effects
|
|
145
|
+
|
|
146
|
+
Always provide a name for your effects to make debug logs and error messages readable:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
effect(() => {
|
|
150
|
+
// ...
|
|
151
|
+
}, { name: 'UpdateSidebarCounter' });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Activation & Deactivation
|
|
155
|
+
|
|
156
|
+
Since these are runtime options, you can toggle them based on your environment:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
if (process.env.NODE_ENV === 'development') {
|
|
160
|
+
reactiveOptions.cycleHandling = 'throw';
|
|
161
|
+
reactiveOptions.onMemoizationDiscrepancy = myHandler;
|
|
162
|
+
enableIntrospection();
|
|
163
|
+
} else {
|
|
164
|
+
// Ensure they are off in production for performance
|
|
165
|
+
reactiveOptions.onMemoizationDiscrepancy = undefined;
|
|
166
|
+
reactiveOptions.cycleHandling = 'break';
|
|
167
|
+
}
|
|
168
|
+
```
|
package/docs/reactive/project.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
- `memoize` caches results but invalidates on `Map.set`, so large effects still re-run.
|
|
14
14
|
- `organized` (designed for `Record` sources) creates per-key effects so downstream work reruns only for the touched key; this matches the desired behaviour.
|
|
15
|
-
- **Gap:** `organized` operates on plain objects: key enumeration relies on property iteration and `
|
|
15
|
+
- **Gap:** `organized` operates on plain objects: key enumeration relies on property iteration and `FoolProof.get/set`. Registers and other keyed collections (`Map`, `Register`, custom stores) need the same per-entry orchestration without converting to records.
|
|
16
16
|
|
|
17
17
|
### Completed Evolution: `project` Implementation
|
|
18
18
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Reactive Scan
|
|
2
|
+
|
|
3
|
+
The `scan` function perform a reactive accumulation over an array of items. Unlike a standard `Array.reduce`, it is designed to be highly efficient in a reactive system, particularly when items are moved or changed, by returning a reactive array of all intermediate results.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
In a typical reactive system, calling `array.reduce(...)` inside an `effect` means the entire reduction re-runs every time the array structure or a single item changes.
|
|
8
|
+
|
|
9
|
+
Reactive `scan` solves this by maintaining a chain of **reactive intermediates**. Each item in the source array is linked to an intermediate that depends on the *previous* intermediate's result.
|
|
10
|
+
|
|
11
|
+
## Key Features
|
|
12
|
+
|
|
13
|
+
- **Fine-Grained Reactivity**: Changing a property on an item only re-computes the accumulated value for that item and its successors.
|
|
14
|
+
- **Move Optimization**: If a subsequence of items moves together (e.g., sorting or splicing), their intermediates are reused. As long as an item's predecessor in the array hasn't changed, its accumulated value is hit from the cache.
|
|
15
|
+
- **Duplicate Support**: Correctly handles multiple occurrences of the same object instance.
|
|
16
|
+
- **Memory Safety**: Uses `WeakMap` for intermediate storage, ensuring data is cleared when source items are garbage collected.
|
|
17
|
+
- **Granular Sync**: Uses per-index effects to sync results, preventing broad dependency tracking of the source array in every calculation.
|
|
18
|
+
|
|
19
|
+
## Basic Usage
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { reactive, scan } from 'mutts/reactive'
|
|
23
|
+
|
|
24
|
+
const source = reactive([
|
|
25
|
+
{ id: 'A', val: 1 },
|
|
26
|
+
{ id: 'B', val: 2 },
|
|
27
|
+
{ id: 'C', val: 3 },
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
// result is a reactive array: [1, 3, 6]
|
|
31
|
+
const result = scan(source, (acc, item) => acc + item.val, 0)
|
|
32
|
+
|
|
33
|
+
// Updating an item only re-computes for that position and successors
|
|
34
|
+
source[1].val = 10
|
|
35
|
+
// result stays [1, 11, 14]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## How it Works
|
|
39
|
+
|
|
40
|
+
The implementation consists of:
|
|
41
|
+
1. **A Main Effect**: Tracks the structure of the source array (length and item identities). It manages a list of `Intermediate` objects and stays updated on their `prev` links.
|
|
42
|
+
2. **Intermediates**: Class instances that link `val` and `prev`. They expose an `acc` getter decorated with `@memoize`.
|
|
43
|
+
3. **Index Sync Effects**: Granular effects (one per result index) that subscribe to `indexToIntermediate[i].acc`.
|
|
44
|
+
|
|
45
|
+
This "Project-like" architecture ensures that the main loop only does structural work, while the actual logic propagation is handled by the dependency chain of the intermediates.
|
|
46
|
+
|
|
47
|
+
## API Reference
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
function scan<Input extends object, Output>(
|
|
51
|
+
source: readonly Input[],
|
|
52
|
+
callback: (acc: Output, val: Input) => Output,
|
|
53
|
+
initialValue: Output
|
|
54
|
+
): ScanResult<Output>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Parameters
|
|
58
|
+
- `source`: The source array. All items must be objects (WeakKeys) to enable intermediate caching.
|
|
59
|
+
- `callback`: The accumulator function `(acc, val) => nextAcc`.
|
|
60
|
+
- `initialValue`: The value used as the accumulator for the first item.
|
|
61
|
+
|
|
62
|
+
### Returns
|
|
63
|
+
A reactive array of accumulated values. It includes a `[cleanup]` symbol that should be called to stop the reactive tracking.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { cleanup } from 'mutts/reactive'
|
|
67
|
+
// ...
|
|
68
|
+
result[cleanup]()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Performance Comparison
|
|
72
|
+
|
|
73
|
+
| Operation | Standard `Array.reduce` in `effect` | Reactive `scan` |
|
|
74
|
+
| :--- | :--- | :--- |
|
|
75
|
+
| **Initial Run** | O(N) calls | O(N) calls |
|
|
76
|
+
| **Modify Item at `i`** | O(N) calls (entire reduction) | O(N-i) calls |
|
|
77
|
+
| **Append Item** | O(N+1) calls | 1 call |
|
|
78
|
+
| **Move Item** | O(N) calls | O(affected chain) |
|
package/docs/reactive.md
CHANGED
|
@@ -11,7 +11,8 @@ The Mutts Reactive System documentation has been split into focused sections for
|
|
|
11
11
|
* **[Reactive Collections](./reactive/collections.md#collections)**: Map, Set, WeakMap, WeakSet
|
|
12
12
|
* **[Reactive Arrays](./reactive/collections.md#reactivearray)**: Full array method support
|
|
13
13
|
* **[Register](./reactive/collections.md#register)**: ID-keyed ordered collections
|
|
14
|
-
* **[Projections](./reactive/collections.md#projection)**: `project`, `
|
|
14
|
+
* **[Projections](./reactive/collections.md#projection)**: `project`, `organized`
|
|
15
|
+
* **[Scan](./reactive/scan.md)**: Reactive scan and accumulation
|
|
15
16
|
|
|
16
17
|
## [Advanced Topics](./reactive/advanced.md)
|
|
17
18
|
* **[Atomic Operations](./reactive/advanced.md#atomic-operations)**: Batching and Bidirectional binding
|
|
@@ -20,9 +21,10 @@ The Mutts Reactive System documentation has been split into focused sections for
|
|
|
20
21
|
* **[Memoization](./reactive/advanced.md#memoization)**: Caching strategies
|
|
21
22
|
* **[Debugging](./reactive/advanced.md#debugging-and-development)**: Cycle detection and troubleshooting
|
|
22
23
|
|
|
23
|
-
## Debugging
|
|
24
|
+
## [Debugging and Troubleshooting](./reactive/debugging.md)
|
|
25
|
+
* **[Reactive Options](./reactive/debugging.md#the-reactiveoptions-reference)**: Global debug hooks and configuration
|
|
26
|
+
* **[Cycle Detection](./reactive/debugging.md#cycle-detection)**: Configuration and troubleshooting circular dependencies
|
|
27
|
+
* **[Memoization Discrepancy](./reactive/debugging.md#memoization-discrepancy-detection)**: Identifying missing dependencies
|
|
28
|
+
* **[Introspection API](./reactive/debugging.md#introspection-api)**: Programmatic analysis and dependency graphs
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
- still unfinished
|
|
27
|
-
- not deactivatable
|
|
28
|
-
- harming the performances of the application
|
|
30
|
+
* **[Performance](./reactive/debugging.md#performance-cost)**: Understanding the cost of debugging tools
|
package/docs/std-decorators.md
CHANGED
package/docs/zone.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Zones (`mutts/zone`)
|
|
2
|
+
|
|
3
|
+
Zones provide a high-performance **context management system** that follows the execution flow, ensuring your variables "stay put" even across asynchronous boundaries like `Promises`, `setTimeout`, or `queueMicrotask`.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
A `Zone` represents a piece of storage that is scoped to the current execution block.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Zone } from 'mutts/zone';
|
|
11
|
+
|
|
12
|
+
const myZone = new Zone<string>();
|
|
13
|
+
|
|
14
|
+
myZone.with("context-value", () => {
|
|
15
|
+
// Inside this function, the zone is active
|
|
16
|
+
console.log(myZone.active); // "context-value"
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
console.log(myZone.active); // undefined
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Async Propagation
|
|
23
|
+
|
|
24
|
+
By default, zones are lost when an async operation yields control (e.g., after `await`). To fix this, `mutts` provides `configureAsyncZone()`.
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { configureAsyncZone, asyncZone, Zone } from 'mutts/zone';
|
|
28
|
+
|
|
29
|
+
const requestId = new Zone<string>();
|
|
30
|
+
|
|
31
|
+
// 1. Tell the global aggregator to track this zone
|
|
32
|
+
asyncZone.add(requestId);
|
|
33
|
+
|
|
34
|
+
// 2. Patch global async primitives (once per app)
|
|
35
|
+
configureAsyncZone();
|
|
36
|
+
|
|
37
|
+
// 3. Usage
|
|
38
|
+
requestId.with("req-123", async () => {
|
|
39
|
+
await somePromise();
|
|
40
|
+
// Context is automatically preserved across await!
|
|
41
|
+
console.log(requestId.active); // "req-123"
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Core API
|
|
46
|
+
|
|
47
|
+
### `AZone<T>` (Abstract)
|
|
48
|
+
The base class for all zone implementations.
|
|
49
|
+
- `active: T | undefined`: The current value in the zone.
|
|
50
|
+
- `with<R>(value: T, fn: () => R): R`: Executes `fn` with `value` set as active.
|
|
51
|
+
- `root<R>(fn: () => R): R`: Executes `fn` with the zone cleared (undefined).
|
|
52
|
+
- `zoned: FunctionWrapper`: A getter that returns a function which, when called, restores the zone to its **current** state.
|
|
53
|
+
|
|
54
|
+
### `Zone<T>`
|
|
55
|
+
Simple stack-based storage.
|
|
56
|
+
|
|
57
|
+
### `ZoneHistory<T>`
|
|
58
|
+
A zone wrapper that maintains a `history` of previously active values in the current stack.
|
|
59
|
+
- Useful for **Cycle Detection**.
|
|
60
|
+
- Prevents re-entering the same value if already in the history.
|
|
61
|
+
- `present: AZone<T>`: Access the current value without the history overhead.
|
|
62
|
+
|
|
63
|
+
### `ZoneAggregator`
|
|
64
|
+
Combines multiple zones into one.
|
|
65
|
+
- Entering an aggregator (with `.with()`) enters all its member zones.
|
|
66
|
+
- `asyncZone` is a global aggregator used for the async patches.
|
|
67
|
+
|
|
68
|
+
## Manual Context Bridging
|
|
69
|
+
|
|
70
|
+
If you are using an API that `mutts` doesn't automatically patch, you can use the `.zoned` capture mechanism:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const wrap = myZone.zoned; // Snapshot the current context
|
|
74
|
+
|
|
75
|
+
// Pass the wrapper to an unmanaged callback
|
|
76
|
+
externalLib.on('event', () => {
|
|
77
|
+
wrap(() => {
|
|
78
|
+
console.log("Context is back:", myZone.active);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Integration with Reactivity
|
|
84
|
+
|
|
85
|
+
The `mutts` reactivity system uses zones internally to track the `activeEffect`.
|
|
86
|
+
- Every `effect()` execution runs inside the `effectHistory` zone.
|
|
87
|
+
- Circular dependency detection is powered by `ZoneHistory`.
|
|
88
|
+
- Async effects survive `await` because `effectHistory` is a member of the global `asyncZone`.
|
package/package.json
CHANGED
|
@@ -1,71 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mutts",
|
|
3
3
|
"description": "Modern UTility TS: A collection of TypeScript utilities",
|
|
4
|
-
"version": "1.0.
|
|
5
|
-
"main": "dist/
|
|
6
|
-
"module": "dist/
|
|
7
|
-
"types": "dist/
|
|
4
|
+
"version": "1.0.7",
|
|
5
|
+
"main": "dist/browser.js",
|
|
6
|
+
"module": "dist/browser.esm.js",
|
|
7
|
+
"types": "dist/browser.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
"node": {
|
|
11
|
+
"types": "./dist/node.d.ts",
|
|
12
|
+
"import": "./dist/node.esm.js",
|
|
13
|
+
"require": "./dist/node.js"
|
|
14
|
+
},
|
|
15
|
+
"default": {
|
|
16
|
+
"types": "./dist/browser.d.ts",
|
|
17
|
+
"import": "./dist/browser.esm.js",
|
|
18
|
+
"require": "./dist/browser.js"
|
|
19
|
+
}
|
|
15
20
|
},
|
|
16
|
-
"./
|
|
17
|
-
"types": "./dist/
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"require": "./dist/decorator.js"
|
|
21
|
+
"./browser": {
|
|
22
|
+
"types": "./dist/browser.d.ts",
|
|
23
|
+
"import": "./dist/browser.esm.js",
|
|
24
|
+
"require": "./dist/browser.js"
|
|
21
25
|
},
|
|
22
|
-
"./
|
|
23
|
-
"types": "./dist/
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"require": "./dist/reactive.js"
|
|
27
|
-
},
|
|
28
|
-
"./eventful": {
|
|
29
|
-
"types": "./dist/eventful.d.ts",
|
|
30
|
-
"source": "./src/eventful.ts",
|
|
31
|
-
"import": "./dist/eventful.esm.js",
|
|
32
|
-
"require": "./dist/eventful.js"
|
|
33
|
-
},
|
|
34
|
-
"./indexable": {
|
|
35
|
-
"types": "./dist/indexable.d.ts",
|
|
36
|
-
"source": "./src/indexable.ts",
|
|
37
|
-
"import": "./dist/indexable.esm.js",
|
|
38
|
-
"require": "./dist/indexable.js"
|
|
39
|
-
},
|
|
40
|
-
"./promiseChain": {
|
|
41
|
-
"types": "./dist/promiseChain.d.ts",
|
|
42
|
-
"source": "./src/promiseChain.ts",
|
|
43
|
-
"import": "./dist/promiseChain.esm.js",
|
|
44
|
-
"require": "./dist/promiseChain.js"
|
|
45
|
-
},
|
|
46
|
-
"./destroyable": {
|
|
47
|
-
"types": "./dist/destroyable.d.ts",
|
|
48
|
-
"source": "./src/destroyable.ts",
|
|
49
|
-
"import": "./dist/destroyable.esm.js",
|
|
50
|
-
"require": "./dist/destroyable.js"
|
|
51
|
-
},
|
|
52
|
-
"./std-decorators": {
|
|
53
|
-
"types": "./dist/std-decorators.d.ts",
|
|
54
|
-
"source": "./src/std-decorators.ts",
|
|
55
|
-
"import": "./dist/std-decorators.esm.js",
|
|
56
|
-
"require": "./dist/std-decorators.js"
|
|
26
|
+
"./node": {
|
|
27
|
+
"types": "./dist/node.d.ts",
|
|
28
|
+
"import": "./dist/node.esm.js",
|
|
29
|
+
"require": "./dist/node.js"
|
|
57
30
|
},
|
|
58
31
|
"./src": {
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
32
|
+
"node": {
|
|
33
|
+
"types": "./src/async/node.ts",
|
|
34
|
+
"import": "./src/async/node.ts"
|
|
35
|
+
},
|
|
36
|
+
"default": {
|
|
37
|
+
"types": "./src/async/browser.ts",
|
|
38
|
+
"import": "./src/async/browser.ts"
|
|
39
|
+
}
|
|
63
40
|
},
|
|
64
|
-
"./
|
|
65
|
-
"
|
|
41
|
+
"./src/browser": {
|
|
42
|
+
"types": "./src/async/browser.ts",
|
|
43
|
+
"import": "./src/async/browser.ts"
|
|
66
44
|
},
|
|
67
|
-
"./
|
|
68
|
-
"
|
|
45
|
+
"./src/node": {
|
|
46
|
+
"types": "./src/async/node.ts",
|
|
47
|
+
"import": "./src/async/node.ts"
|
|
69
48
|
}
|
|
70
49
|
},
|
|
71
50
|
"files": [
|
|
@@ -80,14 +59,14 @@
|
|
|
80
59
|
"build": "npm run build:js && npm run build:devtools",
|
|
81
60
|
"build:watch": "rollup -c --watch",
|
|
82
61
|
"prepublishOnly": "npm run build",
|
|
83
|
-
"test": "
|
|
84
|
-
"test:coverage": "
|
|
85
|
-
"test:coverage:watch": "
|
|
86
|
-
"test:legacy": "TSCONFIG=tsconfig.legacy.json
|
|
87
|
-
"test:modern": "TSCONFIG=tsconfig.modern.json
|
|
88
|
-
"test:profile": "RUN_PROFILING=1
|
|
89
|
-
"test:profile:benchmark": "RUN_PROFILING=1
|
|
90
|
-
"test:profile:detailed": "RUN_PROFILING=1 node --prof node_modules
|
|
62
|
+
"test": "NODE_OPTIONS=--expose-gc jest",
|
|
63
|
+
"test:coverage": "NODE_OPTIONS=--expose-gc jest --coverage",
|
|
64
|
+
"test:coverage:watch": "NODE_OPTIONS=--expose-gc jest --coverage --watch",
|
|
65
|
+
"test:legacy": "TSCONFIG=tsconfig.legacy.json jest --detectOpenHandles --testPathPatterns=decorator",
|
|
66
|
+
"test:modern": "TSCONFIG=tsconfig.modern.json jest --detectOpenHandles --testPathPatterns=decorator",
|
|
67
|
+
"test:profile": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc jest --testPathPatterns=profiling",
|
|
68
|
+
"test:profile:benchmark": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc jest --testPathPatterns=profiling --testNamePattern=benchmark",
|
|
69
|
+
"test:profile:detailed": "RUN_PROFILING=1 node --prof node_modules/jest/bin/jest.js --testPathPatterns=profiling --no-coverage",
|
|
91
70
|
"benchmark:save": "tsx tests/profiling/benchmark.ts save",
|
|
92
71
|
"benchmark:compare": "tsx tests/profiling/benchmark.ts compare",
|
|
93
72
|
"benchmark:list": "tsx tests/profiling/benchmark.ts list",
|
|
@@ -123,11 +102,14 @@
|
|
|
123
102
|
},
|
|
124
103
|
"devDependencies": {
|
|
125
104
|
"@biomejs/biome": "^2.0.6",
|
|
105
|
+
"@jest/globals": "^30.2.0",
|
|
126
106
|
"@rollup/plugin-commonjs": "^28.0.6",
|
|
107
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
127
108
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
128
109
|
"@rollup/plugin-terser": "^0.4.4",
|
|
129
110
|
"@rollup/plugin-typescript": "^12.1.4",
|
|
130
111
|
"@types/jest": "^30.0.0",
|
|
112
|
+
"@types/node": "^22.10.10",
|
|
131
113
|
"jest": "^30.0.4",
|
|
132
114
|
"rollup": "^4.52.2",
|
|
133
115
|
"rollup-plugin-copy": "^3.5.0",
|