@signaltree/core 7.3.0 → 7.6.1
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 +44 -43
- package/dist/enhancers/devtools/devtools.js +460 -17
- package/dist/lib/entity-signal.js +4 -5
- package/dist/lib/signal-tree.js +0 -3
- package/dist/lib/utils.js +58 -1
- package/package.json +1 -1
- package/src/lib/types.d.ts +10 -0
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ Performance and bundle size vary by app shape, build tooling, device, and runtim
|
|
|
118
118
|
|
|
119
119
|
## Best Practices (SignalTree-First)
|
|
120
120
|
|
|
121
|
-
> 📖 **Full guide**: [
|
|
121
|
+
> 📖 **Full guide**: [Architecture Guide](https://github.com/JBorgia/signaltree/blob/main/docs/architecture/signaltree-architecture-guide.md)
|
|
122
122
|
|
|
123
123
|
Follow these principles for idiomatic SignalTree code:
|
|
124
124
|
|
|
@@ -164,7 +164,7 @@ export function createUserTree() {
|
|
|
164
164
|
// ✅ Correct: Derived from multiple signals
|
|
165
165
|
const selectedUser = computed(() => {
|
|
166
166
|
const id = $.selected.userId();
|
|
167
|
-
return id ? $.users.byId(id)() : null;
|
|
167
|
+
return id ? $.users.byId(id)?.() ?? null : null;
|
|
168
168
|
});
|
|
169
169
|
|
|
170
170
|
// ❌ Wrong: Wrapping an existing signal
|
|
@@ -175,8 +175,8 @@ const selectedUserId = computed(() => $.selected.userId()); // Unnecessary!
|
|
|
175
175
|
|
|
176
176
|
```typescript
|
|
177
177
|
// ✅ SignalTree-native
|
|
178
|
-
const user = $.users.byId(123)(); // O(1) lookup
|
|
179
|
-
const allUsers = $.users.all; // Get all
|
|
178
|
+
const user = $.users.byId(123)?.(); // O(1) lookup
|
|
179
|
+
const allUsers = $.users.all(); // Get all
|
|
180
180
|
$.users.setAll(usersFromApi); // Replace all
|
|
181
181
|
|
|
182
182
|
// ❌ NgRx-style (avoid)
|
|
@@ -650,7 +650,7 @@ All enhancers are exported directly from `@signaltree/core`:
|
|
|
650
650
|
|
|
651
651
|
**Data Management:**
|
|
652
652
|
|
|
653
|
-
- `entities()` -
|
|
653
|
+
- `entities()` - Legacy helper (not required when using `entityMap()` in v7)
|
|
654
654
|
- `createAsyncOperation()` - Async operation management with loading/error states
|
|
655
655
|
- `trackAsync()` - Track async operations in your state
|
|
656
656
|
- `serialization()` - State persistence and SSR support
|
|
@@ -658,7 +658,7 @@ All enhancers are exported directly from `@signaltree/core`:
|
|
|
658
658
|
|
|
659
659
|
**Development Tools:**
|
|
660
660
|
|
|
661
|
-
- `devTools()` - Redux DevTools
|
|
661
|
+
- `devTools()` - Redux DevTools auto-connect, path actions, and time-travel dispatch
|
|
662
662
|
- `withTimeTravel()` - Undo/redo functionality
|
|
663
663
|
|
|
664
664
|
**Presets:**
|
|
@@ -691,14 +691,12 @@ const tree = signalTree({ count: 0 }).with(
|
|
|
691
691
|
**Performance-Focused Stack:**
|
|
692
692
|
|
|
693
693
|
```typescript
|
|
694
|
-
import { signalTree, batching, memoization,
|
|
694
|
+
import { signalTree, batching, memoization, entityMap } from '@signaltree/core';
|
|
695
695
|
|
|
696
696
|
const tree = signalTree({
|
|
697
697
|
products: entityMap<Product>(),
|
|
698
698
|
ui: { loading: false },
|
|
699
|
-
})
|
|
700
|
-
.with(entities()) // Efficient CRUD operations (auto-detects entityMap)
|
|
701
|
-
.with(batching()); // Batch updates for optimal rendering
|
|
699
|
+
}).with(batching()); // Batch updates for optimal rendering
|
|
702
700
|
|
|
703
701
|
// Entity CRUD operations
|
|
704
702
|
tree.$.products.addOne(newProduct);
|
|
@@ -748,6 +746,8 @@ tree.undo(); // Revert changes
|
|
|
748
746
|
|
|
749
747
|
#### Enhancer Metadata & Ordering
|
|
750
748
|
|
|
749
|
+
Derived computed signals are preserved across `.with()` chaining, so enhancer composition does not recreate signal identities.
|
|
750
|
+
|
|
751
751
|
Enhancers can declare metadata for automatic dependency resolution:
|
|
752
752
|
|
|
753
753
|
```typescript
|
|
@@ -782,20 +782,20 @@ const customTree = signalTree(state, TREE_PRESETS.DASHBOARD);
|
|
|
782
782
|
SignalTree Core includes all enhancer functionality built-in. No separate packages needed:
|
|
783
783
|
|
|
784
784
|
```typescript
|
|
785
|
-
import { signalTree, entityMap
|
|
785
|
+
import { signalTree, entityMap } from '@signaltree/core';
|
|
786
786
|
|
|
787
787
|
// Without entityMap - use manual array updates
|
|
788
788
|
const basic = signalTree({ users: [] as User[] });
|
|
789
789
|
basic.$.users.update((users) => [...users, newUser]);
|
|
790
790
|
|
|
791
|
-
// With entityMap +
|
|
791
|
+
// With entityMap (v7+ auto-processed) - use entity helpers
|
|
792
792
|
const enhanced = signalTree({
|
|
793
793
|
users: entityMap<User>(),
|
|
794
|
-
})
|
|
794
|
+
});
|
|
795
795
|
|
|
796
796
|
enhanced.$.users.addOne(newUser); // ✅ Advanced CRUD operations
|
|
797
|
-
enhanced.$.users.byId(123)(); // ✅ O(1) lookups
|
|
798
|
-
enhanced.$.users.all; // ✅ Get all as array
|
|
797
|
+
enhanced.$.users.byId(123)?.(); // ✅ O(1) lookups
|
|
798
|
+
enhanced.$.users.all(); // ✅ Get all as array
|
|
799
799
|
```
|
|
800
800
|
|
|
801
801
|
Core includes several performance optimizations:
|
|
@@ -908,7 +908,7 @@ const tree = signalTree({ count: 0 }).with(withLogger());
|
|
|
908
908
|
tree.log('Tree created');
|
|
909
909
|
```
|
|
910
910
|
|
|
911
|
-
> 📖 **Full guide**: [Custom Markers & Enhancers](https://github.com/JBorgia/signaltree/blob/main/docs/custom-markers-enhancers.md)
|
|
911
|
+
> 📖 **Full guide**: [Custom Markers & Enhancers](https://github.com/JBorgia/signaltree/blob/main/docs/guides/custom-markers-enhancers.md)
|
|
912
912
|
>
|
|
913
913
|
> 📱 **Interactive demo**: [Demo App](/custom-extensions)
|
|
914
914
|
|
|
@@ -1574,19 +1574,19 @@ const filteredProducts = computed(() => {
|
|
|
1574
1574
|
### Data Management Composition
|
|
1575
1575
|
|
|
1576
1576
|
```typescript
|
|
1577
|
-
import { signalTree, entityMap
|
|
1577
|
+
import { signalTree, entityMap } from '@signaltree/core';
|
|
1578
1578
|
|
|
1579
|
-
// Add data management capabilities (
|
|
1579
|
+
// Add data management capabilities (entityMap is auto-processed)
|
|
1580
1580
|
const tree = signalTree({
|
|
1581
1581
|
users: entityMap<User>(),
|
|
1582
1582
|
posts: entityMap<Post>(),
|
|
1583
1583
|
ui: { loading: false, error: null as string | null },
|
|
1584
|
-
})
|
|
1584
|
+
});
|
|
1585
1585
|
|
|
1586
1586
|
// Advanced entity operations via tree.$ accessor
|
|
1587
1587
|
tree.$.users.addOne(newUser);
|
|
1588
|
-
tree.$.users.
|
|
1589
|
-
tree.$.users.updateMany([
|
|
1588
|
+
tree.$.users.where((u) => u.active); // Filtered signal
|
|
1589
|
+
tree.$.users.updateMany(['1'], { status: 'active' });
|
|
1590
1590
|
|
|
1591
1591
|
// Entity helpers work with nested structures
|
|
1592
1592
|
// Example: deeply nested entities in a domain-driven design pattern
|
|
@@ -1603,13 +1603,13 @@ const appTree = signalTree({
|
|
|
1603
1603
|
reports: entityMap<Report>(),
|
|
1604
1604
|
},
|
|
1605
1605
|
},
|
|
1606
|
-
})
|
|
1606
|
+
});
|
|
1607
1607
|
|
|
1608
1608
|
// Access nested entities using tree.$ accessor
|
|
1609
|
-
appTree.$.app.data.users.
|
|
1610
|
-
appTree.$.app.data.products.
|
|
1611
|
-
appTree.$.admin.data.logs.all; // All items as array
|
|
1612
|
-
appTree.$.admin.data.reports.
|
|
1609
|
+
appTree.$.app.data.users.where((u) => u.isAdmin); // Filtered signal
|
|
1610
|
+
appTree.$.app.data.products.count(); // Count
|
|
1611
|
+
appTree.$.admin.data.logs.all(); // All items as array
|
|
1612
|
+
appTree.$.admin.data.reports.ids(); // ID array
|
|
1613
1613
|
|
|
1614
1614
|
// For async operations, use manual async or async helpers
|
|
1615
1615
|
async function fetchUsers() {
|
|
@@ -1628,7 +1628,7 @@ async function fetchUsers() {
|
|
|
1628
1628
|
### Full-Featured Development Composition
|
|
1629
1629
|
|
|
1630
1630
|
```typescript
|
|
1631
|
-
import { signalTree, batching,
|
|
1631
|
+
import { signalTree, batching, serialization, withTimeTravel, devTools } from '@signaltree/core';
|
|
1632
1632
|
|
|
1633
1633
|
// Full development stack (example)
|
|
1634
1634
|
const tree = signalTree({
|
|
@@ -1639,7 +1639,6 @@ const tree = signalTree({
|
|
|
1639
1639
|
},
|
|
1640
1640
|
}).with(
|
|
1641
1641
|
batching(), // Performance
|
|
1642
|
-
entities(), // Data management
|
|
1643
1642
|
// withAsync removed — use async helpers for API integration
|
|
1644
1643
|
serialization({
|
|
1645
1644
|
// State persistence
|
|
@@ -1653,7 +1652,9 @@ const tree = signalTree({
|
|
|
1653
1652
|
devTools({
|
|
1654
1653
|
// Debug tools (dev only)
|
|
1655
1654
|
name: 'MyApp',
|
|
1656
|
-
|
|
1655
|
+
enableTimeTravel: true,
|
|
1656
|
+
includePaths: ['app.*', 'ui.*'],
|
|
1657
|
+
formatPath: (path) => path.replace(/\.(\d+)/g, '[$1]'),
|
|
1657
1658
|
})
|
|
1658
1659
|
);
|
|
1659
1660
|
|
|
@@ -1661,7 +1662,7 @@ const tree = signalTree({
|
|
|
1661
1662
|
async function fetchUser(id: string) {
|
|
1662
1663
|
return await api.getUser(id);
|
|
1663
1664
|
}
|
|
1664
|
-
tree.$.app.data.users.byId(userId)(); // O(1) lookup
|
|
1665
|
+
tree.$.app.data.users.byId(userId)?.(); // O(1) lookup
|
|
1665
1666
|
tree.undo(); // Time travel
|
|
1666
1667
|
tree.save(); // Persistence
|
|
1667
1668
|
```
|
|
@@ -1669,12 +1670,11 @@ tree.save(); // Persistence
|
|
|
1669
1670
|
### Production-Ready Composition
|
|
1670
1671
|
|
|
1671
1672
|
```typescript
|
|
1672
|
-
import { signalTree, batching,
|
|
1673
|
+
import { signalTree, batching, serialization } from '@signaltree/core';
|
|
1673
1674
|
|
|
1674
1675
|
// Production build (no dev tools)
|
|
1675
1676
|
const tree = signalTree(initialState).with(
|
|
1676
1677
|
batching(), // Performance optimization
|
|
1677
|
-
entities(), // Data management
|
|
1678
1678
|
// withAsync removed — use async helpers for API integration
|
|
1679
1679
|
serialization({
|
|
1680
1680
|
// User preferences
|
|
@@ -1690,14 +1690,13 @@ const tree = signalTree(initialState).with(
|
|
|
1690
1690
|
### Conditional Enhancement
|
|
1691
1691
|
|
|
1692
1692
|
```typescript
|
|
1693
|
-
import { signalTree, batching,
|
|
1693
|
+
import { signalTree, batching, devTools, withTimeTravel } from '@signaltree/core';
|
|
1694
1694
|
|
|
1695
1695
|
const isDevelopment = process.env['NODE_ENV'] === 'development';
|
|
1696
1696
|
|
|
1697
1697
|
// Conditional enhancement based on environment
|
|
1698
1698
|
const tree = signalTree(state).with(
|
|
1699
1699
|
batching(), // Always include performance
|
|
1700
|
-
entities(), // Always include data management
|
|
1701
1700
|
...(isDevelopment
|
|
1702
1701
|
? [
|
|
1703
1702
|
// Development-only features
|
|
@@ -1742,7 +1741,7 @@ const tree = signalTree(state);
|
|
|
1742
1741
|
const tree2 = tree.with(batching());
|
|
1743
1742
|
|
|
1744
1743
|
// Phase 3: Add data management for collections
|
|
1745
|
-
const tree3 = tree2
|
|
1744
|
+
const tree3 = tree2; // entityMap is auto-processed in v7
|
|
1746
1745
|
|
|
1747
1746
|
// Phase 4: Add async for API integration
|
|
1748
1747
|
// withAsync removed — no explicit async enhancer; use async helpers instead
|
|
@@ -1820,7 +1819,7 @@ For fair, reproducible measurements that reflect your app and hardware, use the
|
|
|
1820
1819
|
{{ userTree.$.error() }}
|
|
1821
1820
|
<button (click)="loadUsers()">Retry</button>
|
|
1822
1821
|
</div>
|
|
1823
|
-
} @else { @for (user of users
|
|
1822
|
+
} @else { @for (user of users(); track user.id) {
|
|
1824
1823
|
<div class="user-card">
|
|
1825
1824
|
<h3>{{ user.name }}</h3>
|
|
1826
1825
|
<p>{{ user.email }}</p>
|
|
@@ -1848,6 +1847,8 @@ class UserManagerComponent implements OnInit {
|
|
|
1848
1847
|
form: { id: '', name: '', email: '' },
|
|
1849
1848
|
});
|
|
1850
1849
|
|
|
1850
|
+
readonly users = this.userTree.$.users;
|
|
1851
|
+
|
|
1851
1852
|
constructor(private userService: UserService) {}
|
|
1852
1853
|
|
|
1853
1854
|
ngOnInit() {
|
|
@@ -2032,9 +2033,9 @@ tree.destroy(); // Cleanup resources
|
|
|
2032
2033
|
|
|
2033
2034
|
// Entity helpers (when using entityMap + entities)
|
|
2034
2035
|
// tree.$.users.addOne(user); // Add single entity
|
|
2035
|
-
// tree.$.users.byId(id)(); // O(1) lookup by ID
|
|
2036
|
-
// tree.$.users.all;
|
|
2037
|
-
// tree.$.users.
|
|
2036
|
+
// tree.$.users.byId(id)?.(); // O(1) lookup by ID
|
|
2037
|
+
// tree.$.users.all(); // Get all as array
|
|
2038
|
+
// tree.$.users.where(pred)(); // Filtered array
|
|
2038
2039
|
```
|
|
2039
2040
|
|
|
2040
2041
|
## Extending with enhancers
|
|
@@ -2054,8 +2055,8 @@ All enhancers are included in `@signaltree/core`:
|
|
|
2054
2055
|
|
|
2055
2056
|
- **batching()** - Batch multiple updates for better performance
|
|
2056
2057
|
- **memoization()** - Intelligent caching & performance optimization
|
|
2057
|
-
- **entities()** -
|
|
2058
|
-
- **devTools()** -
|
|
2058
|
+
- **entities()** - Legacy helper (not required when using `entityMap()` in v7)
|
|
2059
|
+
- **devTools()** - Auto-connect, path actions, and time-travel dispatch
|
|
2059
2060
|
- **withTimeTravel()** - Undo/redo functionality & state history
|
|
2060
2061
|
- **serialization()** - State persistence & SSR support
|
|
2061
2062
|
- **createDevTree()** - Pre-configured development setup
|
|
@@ -2210,11 +2211,11 @@ All enhancers are now consolidated in the core package. The following features a
|
|
|
2210
2211
|
|
|
2211
2212
|
### Advanced Features
|
|
2212
2213
|
|
|
2213
|
-
- **entities()** (+0.97KB gzipped) -
|
|
2214
|
+
- **entities()** (+0.97KB gzipped) - Legacy helper (not required when using `entityMap()` in v7)
|
|
2214
2215
|
|
|
2215
2216
|
### Development Tools
|
|
2216
2217
|
|
|
2217
|
-
- **devTools()** (+2.49KB gzipped) -
|
|
2218
|
+
- **devTools()** (+2.49KB gzipped) - Auto-connect, path actions, and time-travel dispatch
|
|
2218
2219
|
- **withTimeTravel()** (+1.75KB gzipped) - Undo/redo functionality & state history
|
|
2219
2220
|
|
|
2220
2221
|
### Integration & Convenience
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { signal } from '@angular/core';
|
|
1
|
+
import { effect, signal } from '@angular/core';
|
|
2
2
|
import { copyTreeProperties } from '../utils/copy-tree-properties.js';
|
|
3
|
+
import { getPathNotifier } from '../../lib/path-notifier.js';
|
|
4
|
+
import { snapshotState, applyState } from '../../lib/utils.js';
|
|
3
5
|
|
|
4
6
|
function createActivityTracker() {
|
|
5
7
|
const modules = new Map();
|
|
@@ -130,16 +132,150 @@ function createModularMetrics() {
|
|
|
130
132
|
}
|
|
131
133
|
};
|
|
132
134
|
}
|
|
135
|
+
function toArray(value) {
|
|
136
|
+
if (!value) return [];
|
|
137
|
+
return Array.isArray(value) ? value : [value];
|
|
138
|
+
}
|
|
139
|
+
function matchesPattern(pattern, path) {
|
|
140
|
+
if (pattern === '**') return true;
|
|
141
|
+
if (pattern === path) return true;
|
|
142
|
+
if (pattern.endsWith('.*')) {
|
|
143
|
+
const prefix = pattern.slice(0, -2);
|
|
144
|
+
return path.startsWith(prefix + '.');
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
function defaultFormatPath(path) {
|
|
149
|
+
const segments = path.split('.');
|
|
150
|
+
let formatted = '';
|
|
151
|
+
for (const segment of segments) {
|
|
152
|
+
if (!segment) continue;
|
|
153
|
+
if (/^\d+$/.test(segment)) {
|
|
154
|
+
formatted += `[${segment}]`;
|
|
155
|
+
} else {
|
|
156
|
+
formatted += formatted ? `.${segment}` : segment;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return formatted || path;
|
|
160
|
+
}
|
|
161
|
+
function computeChangedPaths(prev, next, maxDepth, maxArrayLength, path = '', depth = 0, output = []) {
|
|
162
|
+
if (prev === next) return output;
|
|
163
|
+
if (depth >= maxDepth) {
|
|
164
|
+
if (path) output.push(path);
|
|
165
|
+
return output;
|
|
166
|
+
}
|
|
167
|
+
if (prev === null || next === null || prev === undefined || next === undefined) {
|
|
168
|
+
if (path) output.push(path);
|
|
169
|
+
return output;
|
|
170
|
+
}
|
|
171
|
+
const prevType = typeof prev;
|
|
172
|
+
const nextType = typeof next;
|
|
173
|
+
if (prevType !== 'object' || nextType !== 'object') {
|
|
174
|
+
if (path) output.push(path);
|
|
175
|
+
return output;
|
|
176
|
+
}
|
|
177
|
+
if (Array.isArray(prev) && Array.isArray(next)) {
|
|
178
|
+
if (prev.length !== next.length) {
|
|
179
|
+
if (path) output.push(path);
|
|
180
|
+
return output;
|
|
181
|
+
}
|
|
182
|
+
if (prev.length > maxArrayLength) {
|
|
183
|
+
if (path) output.push(path);
|
|
184
|
+
return output;
|
|
185
|
+
}
|
|
186
|
+
for (let i = 0; i < prev.length; i += 1) {
|
|
187
|
+
computeChangedPaths(prev[i], next[i], maxDepth, maxArrayLength, path ? `${path}.${i}` : `${i}`, depth + 1, output);
|
|
188
|
+
}
|
|
189
|
+
return output;
|
|
190
|
+
}
|
|
191
|
+
const prevObj = prev;
|
|
192
|
+
const nextObj = next;
|
|
193
|
+
const keys = new Set([...Object.keys(prevObj), ...Object.keys(nextObj)]);
|
|
194
|
+
if (keys.size === 0) {
|
|
195
|
+
if (path) output.push(path);
|
|
196
|
+
return output;
|
|
197
|
+
}
|
|
198
|
+
for (const key of keys) {
|
|
199
|
+
computeChangedPaths(prevObj[key], nextObj[key], maxDepth, maxArrayLength, path ? `${path}.${key}` : key, depth + 1, output);
|
|
200
|
+
}
|
|
201
|
+
return output;
|
|
202
|
+
}
|
|
203
|
+
function sanitizeState(value, options, depth = 0, seen = new WeakSet()) {
|
|
204
|
+
const {
|
|
205
|
+
maxDepth,
|
|
206
|
+
maxArrayLength,
|
|
207
|
+
maxStringLength
|
|
208
|
+
} = options;
|
|
209
|
+
if (value === null || value === undefined) return value;
|
|
210
|
+
if (typeof value === 'string') {
|
|
211
|
+
if (value.length > maxStringLength) {
|
|
212
|
+
return `${value.slice(0, maxStringLength)}…`;
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
|
217
|
+
if (typeof value === 'bigint') return `${value.toString()}n`;
|
|
218
|
+
if (typeof value === 'symbol') return String(value);
|
|
219
|
+
if (typeof value === 'function') return undefined;
|
|
220
|
+
if (depth >= maxDepth) return '[MaxDepth]';
|
|
221
|
+
if (value instanceof Date) return value.toISOString();
|
|
222
|
+
if (value instanceof RegExp) return value.toString();
|
|
223
|
+
if (value instanceof Error) {
|
|
224
|
+
return {
|
|
225
|
+
name: value.name,
|
|
226
|
+
message: value.message,
|
|
227
|
+
stack: value.stack
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (typeof value === 'object') {
|
|
231
|
+
const obj = value;
|
|
232
|
+
if (seen.has(obj)) return '[Circular]';
|
|
233
|
+
seen.add(obj);
|
|
234
|
+
if (value instanceof Map) {
|
|
235
|
+
const entries = Array.from(value.entries()).slice(0, maxArrayLength);
|
|
236
|
+
return entries.map(([k, v]) => [sanitizeState(k, options, depth + 1, seen), sanitizeState(v, options, depth + 1, seen)]);
|
|
237
|
+
}
|
|
238
|
+
if (value instanceof Set) {
|
|
239
|
+
const values = Array.from(value.values()).slice(0, maxArrayLength);
|
|
240
|
+
return values.map(v => sanitizeState(v, options, depth + 1, seen));
|
|
241
|
+
}
|
|
242
|
+
if (Array.isArray(value)) {
|
|
243
|
+
const list = value.slice(0, maxArrayLength);
|
|
244
|
+
return list.map(item => sanitizeState(item, options, depth + 1, seen));
|
|
245
|
+
}
|
|
246
|
+
const result = {};
|
|
247
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
248
|
+
if (typeof val === 'function') continue;
|
|
249
|
+
result[key] = sanitizeState(val, options, depth + 1, seen);
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
133
255
|
function devTools(config = {}) {
|
|
134
256
|
const {
|
|
135
257
|
enabled = true,
|
|
136
258
|
treeName = 'SignalTree',
|
|
137
259
|
name,
|
|
138
260
|
enableBrowserDevTools = true,
|
|
261
|
+
enableTimeTravel = true,
|
|
139
262
|
enableLogging = true,
|
|
140
|
-
performanceThreshold = 16
|
|
263
|
+
performanceThreshold = 16,
|
|
264
|
+
includePaths,
|
|
265
|
+
excludePaths,
|
|
266
|
+
formatPath,
|
|
267
|
+
rateLimitMs,
|
|
268
|
+
maxSendsPerSecond,
|
|
269
|
+
maxDepth = 10,
|
|
270
|
+
maxArrayLength = 50,
|
|
271
|
+
maxStringLength = 2000,
|
|
272
|
+
serialize
|
|
141
273
|
} = config;
|
|
142
274
|
const displayName = name ?? treeName;
|
|
275
|
+
const pathInclude = toArray(includePaths);
|
|
276
|
+
const pathExclude = toArray(excludePaths);
|
|
277
|
+
const sendRateLimitMs = maxSendsPerSecond && maxSendsPerSecond > 0 ? Math.ceil(1000 / maxSendsPerSecond) : rateLimitMs ?? 0;
|
|
278
|
+
const formatPathFn = formatPath ?? defaultFormatPath;
|
|
143
279
|
return tree => {
|
|
144
280
|
if (!enabled) {
|
|
145
281
|
const noopMethods = {
|
|
@@ -152,9 +288,228 @@ function devTools(config = {}) {
|
|
|
152
288
|
const logger = enableLogging ? createCompositionLogger() : createNoopLogger();
|
|
153
289
|
const metrics = createModularMetrics();
|
|
154
290
|
const compositionHistory = [];
|
|
291
|
+
const compositionChain = [];
|
|
292
|
+
const trackComposition = modules => {
|
|
293
|
+
compositionHistory.push({
|
|
294
|
+
timestamp: new Date(),
|
|
295
|
+
chain: [...modules]
|
|
296
|
+
});
|
|
297
|
+
metrics.updateMetrics({
|
|
298
|
+
compositionChain: modules
|
|
299
|
+
});
|
|
300
|
+
logger.logComposition(modules, 'with');
|
|
301
|
+
};
|
|
155
302
|
const activeProfiles = new Map();
|
|
303
|
+
let browserDevToolsConnection = null;
|
|
156
304
|
let browserDevTools = null;
|
|
305
|
+
let isConnected = false;
|
|
306
|
+
let isApplyingExternalState = false;
|
|
307
|
+
let unsubscribeDevTools = null;
|
|
308
|
+
let unsubscribeNotifier = null;
|
|
309
|
+
let unsubscribeFlush = null;
|
|
310
|
+
let pendingPaths = [];
|
|
311
|
+
let effectRef = null;
|
|
312
|
+
let effectPrimed = false;
|
|
313
|
+
let sendScheduled = false;
|
|
314
|
+
let pendingAction = null;
|
|
315
|
+
let pendingExplicitAction = false;
|
|
316
|
+
let pendingSource;
|
|
317
|
+
let pendingDuration;
|
|
318
|
+
let lastSnapshot = undefined;
|
|
319
|
+
let lastSendAt = 0;
|
|
320
|
+
let sendTimer = null;
|
|
321
|
+
const isPathAllowed = path => {
|
|
322
|
+
if (pathInclude.length > 0) {
|
|
323
|
+
const matched = pathInclude.some(pattern => matchesPattern(pattern, path));
|
|
324
|
+
if (!matched) return false;
|
|
325
|
+
}
|
|
326
|
+
if (pathExclude.length > 0) {
|
|
327
|
+
const blocked = pathExclude.some(pattern => matchesPattern(pattern, path));
|
|
328
|
+
if (blocked) return false;
|
|
329
|
+
}
|
|
330
|
+
return true;
|
|
331
|
+
};
|
|
332
|
+
const readSnapshot = () => {
|
|
333
|
+
try {
|
|
334
|
+
if ('$' in tree) {
|
|
335
|
+
return snapshotState(tree.$);
|
|
336
|
+
}
|
|
337
|
+
} catch {}
|
|
338
|
+
return originalTreeCall();
|
|
339
|
+
};
|
|
340
|
+
const buildSerializedState = rawState => {
|
|
341
|
+
if (serialize) {
|
|
342
|
+
try {
|
|
343
|
+
return serialize(rawState);
|
|
344
|
+
} catch {}
|
|
345
|
+
}
|
|
346
|
+
return sanitizeState(rawState, {
|
|
347
|
+
maxDepth,
|
|
348
|
+
maxArrayLength,
|
|
349
|
+
maxStringLength
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
const buildAction = (type, payload, meta) => ({
|
|
353
|
+
type,
|
|
354
|
+
...(payload !== undefined && {
|
|
355
|
+
payload
|
|
356
|
+
}),
|
|
357
|
+
...(meta && {
|
|
358
|
+
meta: meta
|
|
359
|
+
})
|
|
360
|
+
});
|
|
361
|
+
const flushSend = () => {
|
|
362
|
+
sendScheduled = false;
|
|
363
|
+
if (!browserDevTools || isApplyingExternalState) return;
|
|
364
|
+
const rawSnapshot = readSnapshot();
|
|
365
|
+
const currentSnapshot = rawSnapshot ?? {};
|
|
366
|
+
const sanitized = buildSerializedState(currentSnapshot);
|
|
367
|
+
const defaultPaths = lastSnapshot === undefined ? [] : computeChangedPaths(lastSnapshot, currentSnapshot, maxDepth, maxArrayLength);
|
|
368
|
+
const mergedPaths = Array.from(new Set([...pendingPaths, ...defaultPaths.filter(path => path && isPathAllowed(path))]));
|
|
369
|
+
const formattedPaths = mergedPaths.map(path => formatPathFn(path));
|
|
370
|
+
if (pathInclude.length > 0 && formattedPaths.length === 0 && !pendingExplicitAction) {
|
|
371
|
+
pendingAction = null;
|
|
372
|
+
pendingExplicitAction = false;
|
|
373
|
+
pendingSource = undefined;
|
|
374
|
+
pendingDuration = undefined;
|
|
375
|
+
pendingPaths = [];
|
|
376
|
+
lastSnapshot = currentSnapshot;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const effectiveAction = pendingExplicitAction ? pendingAction : formattedPaths.length === 1 ? buildAction(`SignalTree/${formattedPaths[0]}`, formattedPaths[0]) : formattedPaths.length > 1 ? buildAction('SignalTree/update', formattedPaths) : buildAction('SignalTree/update');
|
|
380
|
+
const actionMeta = {
|
|
381
|
+
timestamp: Date.now(),
|
|
382
|
+
...(pendingSource && {
|
|
383
|
+
source: pendingSource
|
|
384
|
+
}),
|
|
385
|
+
...(pendingDuration !== undefined && {
|
|
386
|
+
duration: pendingDuration,
|
|
387
|
+
slow: pendingDuration > performanceThreshold
|
|
388
|
+
}),
|
|
389
|
+
...(formattedPaths.length > 0 && {
|
|
390
|
+
paths: formattedPaths
|
|
391
|
+
})
|
|
392
|
+
};
|
|
393
|
+
const actionToSend = buildAction(effectiveAction?.type ?? 'SignalTree/update', effectiveAction?.payload, actionMeta);
|
|
394
|
+
try {
|
|
395
|
+
browserDevTools.send(actionToSend, sanitized);
|
|
396
|
+
} catch {} finally {
|
|
397
|
+
pendingAction = null;
|
|
398
|
+
pendingExplicitAction = false;
|
|
399
|
+
pendingSource = undefined;
|
|
400
|
+
pendingDuration = undefined;
|
|
401
|
+
pendingPaths = [];
|
|
402
|
+
lastSnapshot = currentSnapshot;
|
|
403
|
+
lastSendAt = Date.now();
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const scheduleSend = (action, meta) => {
|
|
407
|
+
if (isApplyingExternalState) return;
|
|
408
|
+
if (action !== undefined) {
|
|
409
|
+
pendingAction = action;
|
|
410
|
+
pendingExplicitAction = true;
|
|
411
|
+
}
|
|
412
|
+
if (meta?.source) {
|
|
413
|
+
if (!pendingSource) {
|
|
414
|
+
pendingSource = meta.source;
|
|
415
|
+
} else if (pendingSource !== meta.source) {
|
|
416
|
+
pendingSource = 'mixed';
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (meta?.duration !== undefined) {
|
|
420
|
+
pendingDuration = pendingDuration === undefined ? meta.duration : Math.max(pendingDuration, meta.duration);
|
|
421
|
+
}
|
|
422
|
+
if (!browserDevTools) return;
|
|
423
|
+
if (sendScheduled) return;
|
|
424
|
+
sendScheduled = true;
|
|
425
|
+
queueMicrotask(() => {
|
|
426
|
+
if (!browserDevTools) {
|
|
427
|
+
sendScheduled = false;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const now = Date.now();
|
|
431
|
+
const waitMs = Math.max(0, sendRateLimitMs - (now - lastSendAt));
|
|
432
|
+
if (waitMs > 0) {
|
|
433
|
+
if (sendTimer) return;
|
|
434
|
+
sendTimer = setTimeout(() => {
|
|
435
|
+
sendTimer = null;
|
|
436
|
+
flushSend();
|
|
437
|
+
}, waitMs);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
flushSend();
|
|
441
|
+
});
|
|
442
|
+
};
|
|
443
|
+
const parseDevToolsState = state => {
|
|
444
|
+
if (typeof state === 'string') {
|
|
445
|
+
try {
|
|
446
|
+
return JSON.parse(state);
|
|
447
|
+
} catch {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return state;
|
|
452
|
+
};
|
|
453
|
+
const applyExternalState = state => {
|
|
454
|
+
if (state === undefined || state === null) return;
|
|
455
|
+
isApplyingExternalState = true;
|
|
456
|
+
try {
|
|
457
|
+
if ('$' in tree) {
|
|
458
|
+
applyState(tree.$, state);
|
|
459
|
+
} else {
|
|
460
|
+
originalTreeCall(state);
|
|
461
|
+
}
|
|
462
|
+
} finally {
|
|
463
|
+
isApplyingExternalState = false;
|
|
464
|
+
lastSnapshot = readSnapshot();
|
|
465
|
+
pendingPaths = [];
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
const handleDevToolsMessage = message => {
|
|
469
|
+
if (!enableTimeTravel) return;
|
|
470
|
+
if (!message || typeof message !== 'object') return;
|
|
471
|
+
const msg = message;
|
|
472
|
+
if (msg.type !== 'DISPATCH' || !msg.payload?.type) return;
|
|
473
|
+
const actionType = msg.payload.type;
|
|
474
|
+
if (actionType === 'JUMP_TO_STATE' || actionType === 'JUMP_TO_ACTION') {
|
|
475
|
+
const nextState = parseDevToolsState(msg.state);
|
|
476
|
+
applyExternalState(nextState);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (actionType === 'ROLLBACK') {
|
|
480
|
+
const nextState = parseDevToolsState(msg.state);
|
|
481
|
+
applyExternalState(nextState);
|
|
482
|
+
if (browserDevTools) {
|
|
483
|
+
const rawSnapshot = readSnapshot();
|
|
484
|
+
const sanitized = buildSerializedState(rawSnapshot);
|
|
485
|
+
browserDevTools.send('@@INIT', sanitized);
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (actionType === 'COMMIT') {
|
|
490
|
+
if (browserDevTools) {
|
|
491
|
+
const rawSnapshot = readSnapshot();
|
|
492
|
+
const sanitized = buildSerializedState(rawSnapshot);
|
|
493
|
+
browserDevTools.send('@@INIT', sanitized);
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (actionType === 'IMPORT_STATE') {
|
|
498
|
+
const lifted = msg.payload.nextLiftedState;
|
|
499
|
+
const computedStates = lifted?.computedStates ?? [];
|
|
500
|
+
const index = lifted?.currentStateIndex ?? computedStates.length - 1;
|
|
501
|
+
const entry = computedStates[index];
|
|
502
|
+
const nextState = parseDevToolsState(entry?.state);
|
|
503
|
+
applyExternalState(nextState);
|
|
504
|
+
if (browserDevTools) {
|
|
505
|
+
const rawSnapshot = readSnapshot();
|
|
506
|
+
const sanitized = buildSerializedState(rawSnapshot);
|
|
507
|
+
browserDevTools.send('@@INIT', sanitized);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
};
|
|
157
511
|
const initBrowserDevTools = () => {
|
|
512
|
+
if (isConnected) return;
|
|
158
513
|
if (!enableBrowserDevTools || typeof window === 'undefined' || !('__REDUX_DEVTOOLS_EXTENSION__' in window)) {
|
|
159
514
|
return;
|
|
160
515
|
}
|
|
@@ -168,10 +523,26 @@ function devTools(config = {}) {
|
|
|
168
523
|
skip: true
|
|
169
524
|
}
|
|
170
525
|
});
|
|
526
|
+
browserDevToolsConnection = connection;
|
|
171
527
|
browserDevTools = {
|
|
172
|
-
send: connection.send
|
|
528
|
+
send: connection.send,
|
|
529
|
+
subscribe: connection.subscribe
|
|
173
530
|
};
|
|
174
|
-
browserDevTools.
|
|
531
|
+
if (browserDevTools.subscribe && !unsubscribeDevTools) {
|
|
532
|
+
const maybeUnsubscribe = browserDevTools.subscribe(handleDevToolsMessage);
|
|
533
|
+
if (typeof maybeUnsubscribe === 'function') {
|
|
534
|
+
unsubscribeDevTools = maybeUnsubscribe;
|
|
535
|
+
} else {
|
|
536
|
+
unsubscribeDevTools = () => {
|
|
537
|
+
browserDevTools?.subscribe?.(() => void 0);
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const rawSnapshot = readSnapshot();
|
|
542
|
+
const sanitized = buildSerializedState(rawSnapshot);
|
|
543
|
+
browserDevTools.send('@@INIT', sanitized);
|
|
544
|
+
lastSnapshot = rawSnapshot;
|
|
545
|
+
isConnected = true;
|
|
175
546
|
console.log(`🔗 Connected to Redux DevTools as "${displayName}"`);
|
|
176
547
|
} catch (e) {
|
|
177
548
|
console.warn('[SignalTree] Failed to connect to Redux DevTools:', e);
|
|
@@ -193,13 +564,16 @@ function devTools(config = {}) {
|
|
|
193
564
|
}
|
|
194
565
|
}
|
|
195
566
|
const duration = performance.now() - startTime;
|
|
196
|
-
|
|
567
|
+
originalTreeCall();
|
|
197
568
|
metrics.trackModuleUpdate('core', duration);
|
|
198
569
|
if (duration > performanceThreshold) {
|
|
199
570
|
logger.logPerformanceWarning('core', 'update', duration, performanceThreshold);
|
|
200
571
|
}
|
|
201
572
|
if (browserDevTools) {
|
|
202
|
-
|
|
573
|
+
scheduleSend(undefined, {
|
|
574
|
+
source: 'tree.update',
|
|
575
|
+
duration
|
|
576
|
+
});
|
|
203
577
|
}
|
|
204
578
|
return result;
|
|
205
579
|
};
|
|
@@ -210,6 +584,15 @@ function devTools(config = {}) {
|
|
|
210
584
|
if (typeof enhancer !== 'function') {
|
|
211
585
|
throw new Error('Enhancer must be a function');
|
|
212
586
|
}
|
|
587
|
+
const enhancerName = enhancer.name || 'anonymousEnhancer';
|
|
588
|
+
compositionChain.push(enhancerName);
|
|
589
|
+
trackComposition([...compositionChain]);
|
|
590
|
+
scheduleSend(buildAction('SignalTree/with', {
|
|
591
|
+
enhancer: enhancerName,
|
|
592
|
+
chain: [...compositionChain]
|
|
593
|
+
}), {
|
|
594
|
+
source: 'composition'
|
|
595
|
+
});
|
|
213
596
|
return enhancer(enhancedTree);
|
|
214
597
|
},
|
|
215
598
|
writable: false,
|
|
@@ -234,16 +617,7 @@ function devTools(config = {}) {
|
|
|
234
617
|
activityTracker,
|
|
235
618
|
logger,
|
|
236
619
|
metrics: metrics.signal,
|
|
237
|
-
trackComposition
|
|
238
|
-
compositionHistory.push({
|
|
239
|
-
timestamp: new Date(),
|
|
240
|
-
chain: [...modules]
|
|
241
|
-
});
|
|
242
|
-
metrics.updateMetrics({
|
|
243
|
-
compositionChain: modules
|
|
244
|
-
});
|
|
245
|
-
logger.logComposition(modules, 'with');
|
|
246
|
-
},
|
|
620
|
+
trackComposition,
|
|
247
621
|
startModuleProfiling: module => {
|
|
248
622
|
const profileId = `${module}_${Date.now()}`;
|
|
249
623
|
activeProfiles.set(profileId, {
|
|
@@ -262,8 +636,14 @@ function devTools(config = {}) {
|
|
|
262
636
|
}
|
|
263
637
|
},
|
|
264
638
|
connectDevTools: name => {
|
|
639
|
+
if (!browserDevTools || !isConnected) {
|
|
640
|
+
initBrowserDevTools();
|
|
641
|
+
}
|
|
265
642
|
if (browserDevTools) {
|
|
266
|
-
|
|
643
|
+
const rawSnapshot = readSnapshot();
|
|
644
|
+
const sanitized = buildSerializedState(rawSnapshot);
|
|
645
|
+
browserDevTools.send('@@INIT', sanitized);
|
|
646
|
+
lastSnapshot = rawSnapshot;
|
|
267
647
|
console.log(`🔗 Connected to Redux DevTools as "${name}"`);
|
|
268
648
|
}
|
|
269
649
|
},
|
|
@@ -279,10 +659,73 @@ function devTools(config = {}) {
|
|
|
279
659
|
initBrowserDevTools();
|
|
280
660
|
},
|
|
281
661
|
disconnectDevTools() {
|
|
662
|
+
try {
|
|
663
|
+
unsubscribeDevTools?.();
|
|
664
|
+
} catch {}
|
|
665
|
+
try {
|
|
666
|
+
browserDevToolsConnection?.unsubscribe?.();
|
|
667
|
+
} catch {}
|
|
668
|
+
try {
|
|
669
|
+
browserDevToolsConnection?.disconnect?.();
|
|
670
|
+
} catch {}
|
|
282
671
|
browserDevTools = null;
|
|
672
|
+
browserDevToolsConnection = null;
|
|
673
|
+
isConnected = false;
|
|
674
|
+
if (unsubscribeNotifier) {
|
|
675
|
+
unsubscribeNotifier();
|
|
676
|
+
unsubscribeNotifier = null;
|
|
677
|
+
}
|
|
678
|
+
if (unsubscribeFlush) {
|
|
679
|
+
unsubscribeFlush();
|
|
680
|
+
unsubscribeFlush = null;
|
|
681
|
+
}
|
|
682
|
+
unsubscribeDevTools = null;
|
|
683
|
+
if (effectRef) {
|
|
684
|
+
effectRef.destroy();
|
|
685
|
+
effectRef = null;
|
|
686
|
+
}
|
|
687
|
+
if (sendTimer) {
|
|
688
|
+
clearTimeout(sendTimer);
|
|
689
|
+
sendTimer = null;
|
|
690
|
+
}
|
|
691
|
+
pendingPaths = [];
|
|
692
|
+
sendScheduled = false;
|
|
693
|
+
pendingAction = null;
|
|
694
|
+
pendingExplicitAction = false;
|
|
695
|
+
pendingSource = undefined;
|
|
696
|
+
pendingDuration = undefined;
|
|
697
|
+
lastSnapshot = undefined;
|
|
283
698
|
}
|
|
284
699
|
};
|
|
285
700
|
enhancedTree['__devTools'] = devToolsInterface;
|
|
701
|
+
try {
|
|
702
|
+
initBrowserDevTools();
|
|
703
|
+
const notifier = getPathNotifier();
|
|
704
|
+
unsubscribeNotifier = notifier.subscribe('**', (_value, _prev, path) => {
|
|
705
|
+
if (!isPathAllowed(path)) return;
|
|
706
|
+
pendingPaths.push(path);
|
|
707
|
+
});
|
|
708
|
+
unsubscribeFlush = notifier.onFlush(() => {
|
|
709
|
+
if (!browserDevTools) {
|
|
710
|
+
pendingPaths = [];
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (pendingPaths.length === 0) return;
|
|
714
|
+
scheduleSend(undefined, {
|
|
715
|
+
source: 'path-notifier'
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
effectRef = effect(() => {
|
|
719
|
+
void originalTreeCall();
|
|
720
|
+
if (!effectPrimed) {
|
|
721
|
+
effectPrimed = true;
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
scheduleSend(undefined, {
|
|
725
|
+
source: 'signal'
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
} catch {}
|
|
286
729
|
return Object.assign(enhancedTree, methods);
|
|
287
730
|
};
|
|
288
731
|
}
|
|
@@ -20,13 +20,11 @@ function createEntitySignal(config, pathNotifier, basePath) {
|
|
|
20
20
|
mapSignal.set(map);
|
|
21
21
|
}
|
|
22
22
|
function createEntityNode(id, entity) {
|
|
23
|
-
const node = () =>
|
|
23
|
+
const node = () => mapSignal().get(id);
|
|
24
24
|
for (const key of Object.keys(entity)) {
|
|
25
25
|
Object.defineProperty(node, key, {
|
|
26
26
|
get: () => {
|
|
27
|
-
|
|
28
|
-
const value = current?.[key];
|
|
29
|
-
return () => value;
|
|
27
|
+
return () => mapSignal().get(id)?.[key];
|
|
30
28
|
},
|
|
31
29
|
enumerable: true,
|
|
32
30
|
configurable: true
|
|
@@ -46,7 +44,8 @@ function createEntitySignal(config, pathNotifier, basePath) {
|
|
|
46
44
|
const findCache = new WeakMap();
|
|
47
45
|
const api = {
|
|
48
46
|
byId(id) {
|
|
49
|
-
const
|
|
47
|
+
const map = mapSignal();
|
|
48
|
+
const entity = map.get(id);
|
|
50
49
|
if (!entity) return undefined;
|
|
51
50
|
return getOrCreateNode(id, entity);
|
|
52
51
|
},
|
package/dist/lib/signal-tree.js
CHANGED
package/dist/lib/utils.js
CHANGED
|
@@ -180,6 +180,8 @@ function unwrap(node) {
|
|
|
180
180
|
} else {
|
|
181
181
|
result[key] = unwrappedValue;
|
|
182
182
|
}
|
|
183
|
+
} else if (typeof value === 'function') {
|
|
184
|
+
continue;
|
|
183
185
|
} else if (typeof value === 'object' && value !== null && !Array.isArray(value) && !isBuiltInObject(value)) {
|
|
184
186
|
result[key] = unwrap(value);
|
|
185
187
|
} else {
|
|
@@ -212,6 +214,9 @@ function unwrap(node) {
|
|
|
212
214
|
if (typeof v === 'function') continue;
|
|
213
215
|
}
|
|
214
216
|
const value = node[key];
|
|
217
|
+
if (typeof value === 'function' && !isNodeAccessor(value) && !isSignal(value)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
215
220
|
if (isNodeAccessor(value)) {
|
|
216
221
|
const unwrappedValue = value();
|
|
217
222
|
if (typeof unwrappedValue === 'object' && unwrappedValue !== null && !Array.isArray(unwrappedValue) && !isBuiltInObject(unwrappedValue)) {
|
|
@@ -235,6 +240,9 @@ function unwrap(node) {
|
|
|
235
240
|
const symbols = Object.getOwnPropertySymbols(node);
|
|
236
241
|
for (const sym of symbols) {
|
|
237
242
|
const value = node[sym];
|
|
243
|
+
if (typeof value === 'function' && !isNodeAccessor(value) && !isSignal(value)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
238
246
|
if (isNodeAccessor(value)) {
|
|
239
247
|
const unwrappedValue = value();
|
|
240
248
|
if (typeof unwrappedValue === 'object' && unwrappedValue !== null && !Array.isArray(unwrappedValue) && !isBuiltInObject(unwrappedValue)) {
|
|
@@ -260,5 +268,54 @@ function unwrap(node) {
|
|
|
260
268
|
function snapshotState(state) {
|
|
261
269
|
return unwrap(state);
|
|
262
270
|
}
|
|
271
|
+
function applyState(stateNode, snapshot) {
|
|
272
|
+
if (snapshot === null || snapshot === undefined) return;
|
|
273
|
+
if (typeof snapshot !== 'object') return;
|
|
274
|
+
if (stateNode && typeof stateNode === 'object' && typeof stateNode.setAll === 'function' && snapshot && typeof snapshot === 'object' && Array.isArray(snapshot.all)) {
|
|
275
|
+
try {
|
|
276
|
+
stateNode.setAll(snapshot.all);
|
|
277
|
+
return;
|
|
278
|
+
} catch {}
|
|
279
|
+
}
|
|
280
|
+
for (const key of Object.keys(snapshot)) {
|
|
281
|
+
const val = snapshot[key];
|
|
282
|
+
const target = stateNode[key];
|
|
283
|
+
if (isNodeAccessor(target)) {
|
|
284
|
+
if (val && typeof val === 'object') {
|
|
285
|
+
try {
|
|
286
|
+
applyState(target, val);
|
|
287
|
+
} catch {
|
|
288
|
+
try {
|
|
289
|
+
target(val);
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
try {
|
|
294
|
+
target(val);
|
|
295
|
+
} catch {}
|
|
296
|
+
}
|
|
297
|
+
} else if (isSignal(target)) {
|
|
298
|
+
try {
|
|
299
|
+
target.set?.(val);
|
|
300
|
+
} catch {
|
|
301
|
+
try {
|
|
302
|
+
target(val);
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
305
|
+
} else if (target && typeof target === 'object' && val && typeof val === 'object' && !Array.isArray(target) && !Array.isArray(val)) {
|
|
306
|
+
try {
|
|
307
|
+
applyState(target, val);
|
|
308
|
+
} catch {
|
|
309
|
+
try {
|
|
310
|
+
stateNode[key] = val;
|
|
311
|
+
} catch {}
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
try {
|
|
315
|
+
stateNode[key] = val;
|
|
316
|
+
} catch {}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
263
320
|
|
|
264
|
-
export { composeEnhancers, createLazySignalTree, isAnySignal, isBuiltInObject, isEntityMapMarker, isNodeAccessor, snapshotState, toWritableSignal, unwrap };
|
|
321
|
+
export { applyState, composeEnhancers, createLazySignalTree, isAnySignal, isBuiltInObject, isEntityMapMarker, isNodeAccessor, snapshotState, toWritableSignal, unwrap };
|
package/package.json
CHANGED
package/src/lib/types.d.ts
CHANGED
|
@@ -246,11 +246,21 @@ export interface DevToolsConfig {
|
|
|
246
246
|
enableBrowserDevTools?: boolean;
|
|
247
247
|
enableLogging?: boolean;
|
|
248
248
|
performanceThreshold?: number;
|
|
249
|
+
enableTimeTravel?: boolean;
|
|
249
250
|
name?: string;
|
|
250
251
|
treeName?: string;
|
|
251
252
|
enabled?: boolean;
|
|
252
253
|
logActions?: boolean;
|
|
253
254
|
maxAge?: number;
|
|
255
|
+
rateLimitMs?: number;
|
|
256
|
+
maxSendsPerSecond?: number;
|
|
257
|
+
includePaths?: string[];
|
|
258
|
+
excludePaths?: string[];
|
|
259
|
+
formatPath?: (path: string) => string;
|
|
260
|
+
maxDepth?: number;
|
|
261
|
+
maxArrayLength?: number;
|
|
262
|
+
maxStringLength?: number;
|
|
263
|
+
serialize?: (state: unknown) => unknown;
|
|
254
264
|
features?: {
|
|
255
265
|
jump?: boolean;
|
|
256
266
|
skip?: boolean;
|