@signaltree/core 9.2.1 → 9.2.2
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 +163 -151
- package/dist/enhancers/effects/effects.js +74 -0
- package/dist/index.js +1 -0
- package/dist/lib/constants.js +1 -8
- package/dist/lib/entity-signal.js +50 -11
- package/package.json +1 -1
- package/src/index.d.ts +3 -1
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ Follow these principles for idiomatic SignalTree code:
|
|
|
125
125
|
### 1. Expose signals directly (no computed wrappers)
|
|
126
126
|
|
|
127
127
|
```typescript
|
|
128
|
-
const tree = signalTree(initialState);
|
|
128
|
+
const tree = signalTree(initialState);
|
|
129
129
|
const $ = tree.$; // Shorthand for state access
|
|
130
130
|
|
|
131
131
|
// ✅ SignalTree-first: Direct signal exposure
|
|
@@ -218,7 +218,7 @@ Alternatively, await a microtask (`await Promise.resolve()`) to allow the automa
|
|
|
218
218
|
To disable automatic microtask batching for a specific tree instance:
|
|
219
219
|
|
|
220
220
|
```typescript
|
|
221
|
-
const tree = signalTree(initialState, {
|
|
221
|
+
const tree = signalTree(initialState, { batchUpdates: false });
|
|
222
222
|
```
|
|
223
223
|
|
|
224
224
|
Use this only for rare cases that truly require synchronous notifications (most apps should keep batching enabled).
|
|
@@ -293,13 +293,20 @@ const tree = signalTree({
|
|
|
293
293
|
message: 'Hello World',
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
-
// Read values (these are Angular signals)
|
|
296
|
+
// Read values (these are Angular signals — always works)
|
|
297
297
|
console.log(tree.$.count()); // 0
|
|
298
298
|
console.log(tree.$.message()); // 'Hello World'
|
|
299
299
|
|
|
300
|
-
//
|
|
301
|
-
tree.$.count(5)
|
|
302
|
-
|
|
300
|
+
// ⚠️ CALLABLE SYNTAX REQUIRES BUILD TRANSFORM
|
|
301
|
+
// Lines below use tree.$.count(5) setter syntax. This requires the
|
|
302
|
+
// @signaltree/callable-syntax build-time transform (separate dev dependency).
|
|
303
|
+
// WITHOUT the transform, use .set()/.update() instead — these always work:
|
|
304
|
+
// tree.$.count.set(5);
|
|
305
|
+
// tree.$.message.set('Updated!');
|
|
306
|
+
// tree.$.count.update((n) => n + 1);
|
|
307
|
+
// See: https://github.com/JBorgia/signaltree/blob/main/packages/callable-syntax/README.md
|
|
308
|
+
tree.$.count(5); // requires @signaltree/callable-syntax transform
|
|
309
|
+
tree.$.message('Updated!'); // requires @signaltree/callable-syntax transform
|
|
303
310
|
|
|
304
311
|
// Use in an Angular component
|
|
305
312
|
@Component({
|
|
@@ -652,16 +659,37 @@ All enhancers are exported directly from `@signaltree/core`:
|
|
|
652
659
|
|
|
653
660
|
**Data Management:**
|
|
654
661
|
|
|
655
|
-
- `entities()` - Advanced CRUD operations for collections
|
|
656
662
|
- `createAsyncOperation()` - Async operation management with loading/error states
|
|
657
663
|
- `trackAsync()` - Track async operations in your state
|
|
658
664
|
- `serialization()` - State persistence and SSR support
|
|
659
665
|
- `persistence()` - Auto-save to localStorage/IndexedDB
|
|
660
666
|
|
|
667
|
+
**Reactive Side Effects:**
|
|
668
|
+
|
|
669
|
+
- `effects()` - Angular `effect()`-based subscriptions on tree state with cleanup
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
import { signalTree, effects } from '@signaltree/core';
|
|
673
|
+
|
|
674
|
+
const tree = signalTree({ count: 0, user: { name: 'Alice' } })
|
|
675
|
+
.with(effects());
|
|
676
|
+
|
|
677
|
+
// Subscribe with automatic cleanup on destroy
|
|
678
|
+
const unsub = tree.subscribe(state => {
|
|
679
|
+
console.log('State changed:', state.count);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Effect with cleanup callback
|
|
683
|
+
const cleanup = tree.effect(state => {
|
|
684
|
+
console.log('Count:', state.count);
|
|
685
|
+
return () => console.log('Previous effect cleaned up');
|
|
686
|
+
});
|
|
687
|
+
```
|
|
688
|
+
|
|
661
689
|
**Development Tools:**
|
|
662
690
|
|
|
663
691
|
- `devTools()` - Redux DevTools auto-connect, path actions, and time-travel dispatch
|
|
664
|
-
- `
|
|
692
|
+
- `timeTravel()` - Undo/redo functionality
|
|
665
693
|
|
|
666
694
|
#### Additional Packages
|
|
667
695
|
|
|
@@ -678,23 +706,22 @@ These are the **only** separate packages in the SignalTree ecosystem:
|
|
|
678
706
|
```typescript
|
|
679
707
|
import { signalTree, batching, devTools } from '@signaltree/core';
|
|
680
708
|
|
|
681
|
-
// Apply enhancers
|
|
682
|
-
const tree = signalTree({ count: 0 })
|
|
683
|
-
batching()
|
|
684
|
-
devTools() // Development tools
|
|
685
|
-
);
|
|
709
|
+
// Apply enhancers by chaining — each .with() takes a single enhancer
|
|
710
|
+
const tree = signalTree({ count: 0 })
|
|
711
|
+
.with(batching()) // Performance optimization
|
|
712
|
+
.with(devTools()); // Development tools
|
|
686
713
|
```
|
|
687
714
|
|
|
688
715
|
**Performance-Focused Stack:**
|
|
689
716
|
|
|
690
717
|
```typescript
|
|
691
|
-
import { signalTree, batching
|
|
718
|
+
import { signalTree, batching } from '@signaltree/core';
|
|
692
719
|
|
|
720
|
+
// entityMap() markers self-register — no entities() enhancer needed
|
|
693
721
|
const tree = signalTree({
|
|
694
722
|
products: entityMap<Product>(),
|
|
695
723
|
ui: { loading: false },
|
|
696
724
|
})
|
|
697
|
-
.with(entities()) // Efficient CRUD operations (auto-detects entityMap)
|
|
698
725
|
.with(batching()); // Batch updates for optimal rendering
|
|
699
726
|
|
|
700
727
|
// Entity CRUD operations
|
|
@@ -708,20 +735,17 @@ const electronics = tree.$.products.all.filter((p) => p.category === 'electronic
|
|
|
708
735
|
**Full-Stack Application:**
|
|
709
736
|
|
|
710
737
|
```typescript
|
|
711
|
-
import { signalTree, serialization,
|
|
738
|
+
import { signalTree, serialization, timeTravel } from '@signaltree/core';
|
|
712
739
|
|
|
713
740
|
const tree = signalTree({
|
|
714
741
|
user: null as User | null,
|
|
715
742
|
preferences: { theme: 'light' },
|
|
716
|
-
})
|
|
717
|
-
//
|
|
718
|
-
serialization({
|
|
719
|
-
// Auto-save to localStorage
|
|
743
|
+
})
|
|
744
|
+
.with(serialization({ // Auto-save to localStorage
|
|
720
745
|
autoSave: true,
|
|
721
746
|
storage: 'localStorage',
|
|
722
|
-
})
|
|
723
|
-
|
|
724
|
-
);
|
|
747
|
+
}))
|
|
748
|
+
.with(timeTravel()); // Undo/redo support
|
|
725
749
|
|
|
726
750
|
// For async operations, use manual async or async helpers
|
|
727
751
|
async function fetchUser(id: string) {
|
|
@@ -750,12 +774,10 @@ Derived computed signals are preserved across `.with()` chaining, so enhancer co
|
|
|
750
774
|
Enhancers can declare metadata for automatic dependency resolution:
|
|
751
775
|
|
|
752
776
|
```typescript
|
|
753
|
-
//
|
|
754
|
-
const tree = signalTree(state)
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
);
|
|
758
|
-
// Automatically ordered: batching -> devtools
|
|
777
|
+
// Chain enhancers — each .with() takes a single enhancer
|
|
778
|
+
const tree = signalTree(state)
|
|
779
|
+
.with(batching()) // Requires: core, provides: batching
|
|
780
|
+
.with(devTools()); // Requires: core, provides: debugging
|
|
759
781
|
```
|
|
760
782
|
|
|
761
783
|
#### Core Stubs
|
|
@@ -769,10 +791,10 @@ import { signalTree, entityMap, entities } from '@signaltree/core';
|
|
|
769
791
|
const basic = signalTree({ users: [] as User[] });
|
|
770
792
|
basic.$.users.update((users) => [...users, newUser]);
|
|
771
793
|
|
|
772
|
-
// With entityMap
|
|
794
|
+
// With entityMap — entity helpers are automatically available (no enhancer needed)
|
|
773
795
|
const enhanced = signalTree({
|
|
774
796
|
users: entityMap<User>(),
|
|
775
|
-
})
|
|
797
|
+
});
|
|
776
798
|
|
|
777
799
|
enhanced.$.users.addOne(newUser); // ✅ Advanced CRUD operations
|
|
778
800
|
enhanced.$.users.byId(123)(); // ✅ O(1) lookups
|
|
@@ -805,11 +827,8 @@ const tree2 = signalTree(
|
|
|
805
827
|
}
|
|
806
828
|
);
|
|
807
829
|
|
|
808
|
-
// Structural sharing for memory efficiency
|
|
809
|
-
tree.
|
|
810
|
-
...state, // Reuses unchanged parts
|
|
811
|
-
newField: 'value',
|
|
812
|
-
}));
|
|
830
|
+
// Structural sharing for memory efficiency — update individual leaves directly
|
|
831
|
+
tree.$.newField.set('value'); // Only the changed leaf re-emits; siblings are unaffected
|
|
813
832
|
```
|
|
814
833
|
|
|
815
834
|
### 7) Extensibility: Custom Markers & Enhancers
|
|
@@ -1037,15 +1056,33 @@ const tree = signalTree({
|
|
|
1037
1056
|
});
|
|
1038
1057
|
|
|
1039
1058
|
// EntitySignal API
|
|
1040
|
-
tree.$.products.
|
|
1059
|
+
tree.$.products.setAll([
|
|
1041
1060
|
{ id: 1, name: 'Laptop', category: 'electronics', price: 999, inStock: true },
|
|
1042
1061
|
{ id: 2, name: 'Chair', category: 'furniture', price: 199, inStock: false },
|
|
1043
1062
|
]);
|
|
1044
1063
|
|
|
1045
|
-
tree.$.products.all
|
|
1046
|
-
tree.$.products.
|
|
1047
|
-
tree.$.products.
|
|
1048
|
-
tree.$.products.
|
|
1064
|
+
tree.$.products.all; // Signal<Product[]> — getter, call as .all() to read
|
|
1065
|
+
tree.$.products.all(); // Product[] — current value
|
|
1066
|
+
tree.$.products.byId(1); // EntityNode<Product> | undefined — cursor; call () to get value
|
|
1067
|
+
tree.$.products.byId(1)?.(); // Product | undefined — unwrap the cursor
|
|
1068
|
+
|
|
1069
|
+
// Field-level reads and writes via EntityNode
|
|
1070
|
+
// Field properties are computed signals: isSignal() returns true, toObservable() works
|
|
1071
|
+
tree.$.products.byId(1)?.name(); // string — read field reactively
|
|
1072
|
+
tree.$.products.byId(1)?.name.set('New'); // update single field (interceptors fire)
|
|
1073
|
+
tree.$.products.byId(1)?.name.update(n => n.toUpperCase()); // updater
|
|
1074
|
+
tree.$.products.byId(1)?.name.asReadonly(); // Signal<string> — read-only view
|
|
1075
|
+
|
|
1076
|
+
// Entity-level write via callable (replaces entire entity)
|
|
1077
|
+
const node = tree.$.products.byId(1);
|
|
1078
|
+
node?.({ id: 1, name: 'Updated', category: 'electronics', price: 899, inStock: true });
|
|
1079
|
+
|
|
1080
|
+
// Note: writes on a stale node (entity removed) throw "Entity with id X not found"
|
|
1081
|
+
// This is consistent with updateOne() and the rest of the mutation API.
|
|
1082
|
+
tree.$.products.ids; // Signal<number[]> — getter
|
|
1083
|
+
tree.$.products.ids(); // number[] — current value
|
|
1084
|
+
tree.$.products.count; // Signal<number> — getter
|
|
1085
|
+
tree.$.products.count(); // number — current value
|
|
1049
1086
|
|
|
1050
1087
|
// Computed slices (reactive, type-safe)
|
|
1051
1088
|
tree.$.products.electronics(); // Signal<Product[]> - auto-updates
|
|
@@ -1473,30 +1510,27 @@ const tree = signalTree({
|
|
|
1473
1510
|
validationErrors: [] as string[],
|
|
1474
1511
|
});
|
|
1475
1512
|
|
|
1476
|
-
// Safe update with validation
|
|
1513
|
+
// Safe update with validation — read current state, transform, write back to leaves
|
|
1477
1514
|
function safeUpdateItem(id: string, updates: Partial<Item>) {
|
|
1478
1515
|
try {
|
|
1479
|
-
tree
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1516
|
+
const currentItems = tree.$.items();
|
|
1517
|
+
const itemIndex = currentItems.findIndex((item) => item.id === id);
|
|
1518
|
+
if (itemIndex === -1) {
|
|
1519
|
+
throw new Error(`Item with id ${id} not found`);
|
|
1520
|
+
}
|
|
1484
1521
|
|
|
1485
|
-
|
|
1522
|
+
const updatedItem = { ...currentItems[itemIndex], ...updates };
|
|
1486
1523
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1524
|
+
// Validation
|
|
1525
|
+
if (!updatedItem.name?.trim()) {
|
|
1526
|
+
throw new Error('Item name is required');
|
|
1527
|
+
}
|
|
1491
1528
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1529
|
+
const newItems = [...currentItems];
|
|
1530
|
+
newItems[itemIndex] = updatedItem;
|
|
1494
1531
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
validationErrors: [], // Clear errors on success
|
|
1498
|
-
};
|
|
1499
|
-
});
|
|
1532
|
+
tree.$.items.set(newItems);
|
|
1533
|
+
tree.$.validationErrors.set([]); // Clear errors on success
|
|
1500
1534
|
} catch (error) {
|
|
1501
1535
|
tree.$.validationErrors.update((errors) => [...errors, error instanceof Error ? error.message : 'Unknown error']);
|
|
1502
1536
|
}
|
|
@@ -1521,7 +1555,7 @@ const tree = signalTree({
|
|
|
1521
1555
|
// Basic operations included in core
|
|
1522
1556
|
tree.$.users.set([...users, newUser]);
|
|
1523
1557
|
tree.$.ui.loading.set(true);
|
|
1524
|
-
|
|
1558
|
+
// For reactive effects use Angular's built-in effect(): effect(() => console.log(tree.$.count()))
|
|
1525
1559
|
```
|
|
1526
1560
|
|
|
1527
1561
|
### Performance-Enhanced Composition
|
|
@@ -1556,16 +1590,16 @@ const filteredProducts = computed(() => {
|
|
|
1556
1590
|
```typescript
|
|
1557
1591
|
import { signalTree, entityMap, entities } from '@signaltree/core';
|
|
1558
1592
|
|
|
1559
|
-
//
|
|
1593
|
+
// entityMap() markers self-register — entity operations available immediately
|
|
1560
1594
|
const tree = signalTree({
|
|
1561
1595
|
users: entityMap<User>(),
|
|
1562
1596
|
posts: entityMap<Post>(),
|
|
1563
1597
|
ui: { loading: false, error: null as string | null },
|
|
1564
|
-
})
|
|
1598
|
+
});
|
|
1565
1599
|
|
|
1566
1600
|
// Advanced entity operations via tree.$ accessor
|
|
1567
1601
|
tree.$.users.addOne(newUser);
|
|
1568
|
-
tree.$.users.
|
|
1602
|
+
tree.$.users.where((u) => u.active);
|
|
1569
1603
|
tree.$.users.updateMany([{ id: '1', changes: { status: 'active' } }]);
|
|
1570
1604
|
|
|
1571
1605
|
// Entity helpers work with nested structures
|
|
@@ -1583,13 +1617,13 @@ const appTree = signalTree({
|
|
|
1583
1617
|
reports: entityMap<Report>(),
|
|
1584
1618
|
},
|
|
1585
1619
|
},
|
|
1586
|
-
})
|
|
1620
|
+
});
|
|
1587
1621
|
|
|
1588
1622
|
// Access nested entities using tree.$ accessor
|
|
1589
|
-
appTree.$.app.data.users.
|
|
1590
|
-
appTree.$.app.data.products.
|
|
1623
|
+
appTree.$.app.data.users.where((u) => u.isAdmin); // Filtered signal
|
|
1624
|
+
appTree.$.app.data.products.count(); // Count signal
|
|
1591
1625
|
appTree.$.admin.data.logs.all; // All items as array
|
|
1592
|
-
appTree.$.admin.data.reports.
|
|
1626
|
+
appTree.$.admin.data.reports.ids(); // ID array signal
|
|
1593
1627
|
|
|
1594
1628
|
// For async operations, use manual async or async helpers
|
|
1595
1629
|
async function fetchUsers() {
|
|
@@ -1608,36 +1642,29 @@ async function fetchUsers() {
|
|
|
1608
1642
|
### Full-Featured Development Composition
|
|
1609
1643
|
|
|
1610
1644
|
```typescript
|
|
1611
|
-
import { signalTree, batching,
|
|
1645
|
+
import { signalTree, batching, serialization, timeTravel, devTools } from '@signaltree/core';
|
|
1612
1646
|
|
|
1613
|
-
// Full development stack (
|
|
1647
|
+
// Full development stack — chain each enhancer with a separate .with() call
|
|
1648
|
+
// entityMap() markers self-register; no entities() enhancer needed
|
|
1614
1649
|
const tree = signalTree({
|
|
1615
1650
|
app: {
|
|
1616
1651
|
user: null as User | null,
|
|
1617
1652
|
preferences: { theme: 'light' },
|
|
1618
1653
|
data: { users: [], posts: [] },
|
|
1619
1654
|
},
|
|
1620
|
-
})
|
|
1621
|
-
batching()
|
|
1622
|
-
|
|
1623
|
-
// withAsync removed — use async helpers for API integration
|
|
1624
|
-
serialization({
|
|
1625
|
-
// State persistence
|
|
1655
|
+
})
|
|
1656
|
+
.with(batching()) // Performance
|
|
1657
|
+
.with(serialization({ // State persistence
|
|
1626
1658
|
autoSave: true,
|
|
1627
1659
|
storage: 'localStorage',
|
|
1628
|
-
})
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
maxHistory: 50,
|
|
1632
|
-
}),
|
|
1633
|
-
devTools({
|
|
1634
|
-
// Debug tools (dev only)
|
|
1660
|
+
}))
|
|
1661
|
+
.with(timeTravel({ maxHistorySize: 50 })) // Undo/redo
|
|
1662
|
+
.with(devTools({ // Debug tools (dev only)
|
|
1635
1663
|
name: 'MyApp',
|
|
1636
1664
|
enableTimeTravel: true,
|
|
1637
1665
|
includePaths: ['app.*', 'ui.*'],
|
|
1638
1666
|
formatPath: (path) => path.replace(/\.(\d+)/g, '[$1]'),
|
|
1639
|
-
})
|
|
1640
|
-
);
|
|
1667
|
+
}));
|
|
1641
1668
|
|
|
1642
1669
|
// Rich feature set available
|
|
1643
1670
|
async function fetchUser(id: string) {
|
|
@@ -1710,17 +1737,13 @@ In Redux DevTools you will see a single instance named `"MyApp SignalTree"` with
|
|
|
1710
1737
|
import { signalTree, batching, entities, serialization } from '@signaltree/core';
|
|
1711
1738
|
|
|
1712
1739
|
// Production build (no dev tools)
|
|
1713
|
-
const tree = signalTree(initialState)
|
|
1714
|
-
batching()
|
|
1715
|
-
|
|
1716
|
-
// withAsync removed — use async helpers for API integration
|
|
1717
|
-
serialization({
|
|
1718
|
-
// User preferences
|
|
1740
|
+
const tree = signalTree(initialState)
|
|
1741
|
+
.with(batching()) // Performance optimization
|
|
1742
|
+
.with(serialization({ // User preferences
|
|
1719
1743
|
autoSave: true,
|
|
1720
1744
|
storage: 'localStorage',
|
|
1721
1745
|
key: 'app-v1.2.3',
|
|
1722
|
-
})
|
|
1723
|
-
);
|
|
1746
|
+
}));
|
|
1724
1747
|
|
|
1725
1748
|
// Clean, efficient, production-ready
|
|
1726
1749
|
```
|
|
@@ -1728,28 +1751,22 @@ const tree = signalTree(initialState).with(
|
|
|
1728
1751
|
### Conditional Enhancement
|
|
1729
1752
|
|
|
1730
1753
|
```typescript
|
|
1731
|
-
import { signalTree, batching,
|
|
1754
|
+
import { signalTree, batching, devTools, timeTravel } from '@signaltree/core';
|
|
1732
1755
|
|
|
1733
1756
|
const isDevelopment = process.env['NODE_ENV'] === 'development';
|
|
1734
1757
|
|
|
1735
1758
|
// Conditional enhancement based on environment
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
// Development-only features
|
|
1742
|
-
devTools(),
|
|
1743
|
-
withTimeTravel(),
|
|
1744
|
-
]
|
|
1745
|
-
: [])
|
|
1746
|
-
);
|
|
1759
|
+
// entityMap() markers self-register; chain each enhancer with .with()
|
|
1760
|
+
let tree = signalTree(state).with(batching()); // Always include performance
|
|
1761
|
+
if (isDevelopment) {
|
|
1762
|
+
tree = tree.with(devTools()).with(timeTravel()); // Development-only features
|
|
1763
|
+
}
|
|
1747
1764
|
```
|
|
1748
1765
|
|
|
1749
1766
|
### Preset-Based Composition
|
|
1750
1767
|
|
|
1751
1768
|
```typescript
|
|
1752
|
-
import { signalTree, batching, devTools,
|
|
1769
|
+
import { signalTree, batching, devTools, timeTravel } from '@signaltree/core';
|
|
1753
1770
|
|
|
1754
1771
|
// Compose the enhancers you actually need
|
|
1755
1772
|
const devTree = signalTree({
|
|
@@ -1759,7 +1776,7 @@ const devTree = signalTree({
|
|
|
1759
1776
|
})
|
|
1760
1777
|
.with(batching())
|
|
1761
1778
|
.with(devTools())
|
|
1762
|
-
.with(
|
|
1779
|
+
.with(timeTravel());
|
|
1763
1780
|
```
|
|
1764
1781
|
|
|
1765
1782
|
> **9.0.1 note:** Preset factories (`createDevTree`, `TREE_PRESETS`, etc.) were removed. Compose enhancers directly with `.with()`.
|
|
@@ -1779,10 +1796,7 @@ const tree = signalTree(state);
|
|
|
1779
1796
|
// Phase 2: Add performance when needed
|
|
1780
1797
|
const tree2 = tree.with(batching());
|
|
1781
1798
|
|
|
1782
|
-
// Phase 3: Add
|
|
1783
|
-
const tree3 = tree2.with(entities());
|
|
1784
|
-
|
|
1785
|
-
// Phase 4: Add async for API integration
|
|
1799
|
+
// Phase 3: Add async for API integration
|
|
1786
1800
|
// withAsync removed — no explicit async enhancer; use async helpers instead
|
|
1787
1801
|
|
|
1788
1802
|
// Each phase is fully functional and production-ready
|
|
@@ -1801,7 +1815,7 @@ if (needsPerformance) {
|
|
|
1801
1815
|
}
|
|
1802
1816
|
|
|
1803
1817
|
if (needsTimeTravel) {
|
|
1804
|
-
tree = tree.with(
|
|
1818
|
+
tree = tree.with(timeTravel());
|
|
1805
1819
|
}
|
|
1806
1820
|
```
|
|
1807
1821
|
|
|
@@ -1858,7 +1872,7 @@ For fair, reproducible measurements that reflect your app and hardware, use the
|
|
|
1858
1872
|
{{ userTree.$.error() }}
|
|
1859
1873
|
<button (click)="loadUsers()">Retry</button>
|
|
1860
1874
|
</div>
|
|
1861
|
-
} @else { @for (user of users.
|
|
1875
|
+
} @else { @for (user of users.all(); track user.id) {
|
|
1862
1876
|
<div class="user-card">
|
|
1863
1877
|
<h3>{{ user.name }}</h3>
|
|
1864
1878
|
<p>{{ user.email }}</p>
|
|
@@ -1954,18 +1968,6 @@ class UserManagerComponent implements OnInit {
|
|
|
1954
1968
|
}
|
|
1955
1969
|
```
|
|
1956
1970
|
|
|
1957
|
-
]
|
|
1958
|
-
|
|
1959
|
-
}
|
|
1960
|
-
}));
|
|
1961
|
-
|
|
1962
|
-
// Get entire state as plain object
|
|
1963
|
-
const currentState = tree.unwrap();
|
|
1964
|
-
console.log('Current app state:', currentState);
|
|
1965
|
-
|
|
1966
|
-
```
|
|
1967
|
-
});
|
|
1968
|
-
```
|
|
1969
1971
|
|
|
1970
1972
|
## Core features
|
|
1971
1973
|
|
|
@@ -2034,18 +2036,33 @@ async function handleLoadUsers() {
|
|
|
2034
2036
|
|
|
2035
2037
|
### Reactive effects
|
|
2036
2038
|
|
|
2039
|
+
Use Angular's built-in `effect()` to react to signal changes, or the `effects()` enhancer for tree-level subscriptions with managed cleanup:
|
|
2040
|
+
|
|
2037
2041
|
```typescript
|
|
2038
|
-
|
|
2039
|
-
tree.effect((state) => {
|
|
2040
|
-
console.log(`User: ${state.user.name}, Theme: ${state.settings.theme}`);
|
|
2041
|
-
});
|
|
2042
|
+
import { effect } from '@angular/core';
|
|
2042
2043
|
|
|
2043
|
-
//
|
|
2044
|
-
|
|
2045
|
-
|
|
2044
|
+
// React to any signal in the tree — reads are tracked automatically
|
|
2045
|
+
effect(() => {
|
|
2046
|
+
console.log(`User: ${tree.$.user.name()}, Theme: ${tree.$.settings.theme()}`);
|
|
2046
2047
|
});
|
|
2047
2048
|
```
|
|
2048
2049
|
|
|
2050
|
+
Or use the `effects()` enhancer for whole-tree subscriptions:
|
|
2051
|
+
|
|
2052
|
+
```typescript
|
|
2053
|
+
import { signalTree, effects } from '@signaltree/core';
|
|
2054
|
+
|
|
2055
|
+
const tree = signalTree({ count: 0 }).with(effects());
|
|
2056
|
+
|
|
2057
|
+
// Returns cleanup function
|
|
2058
|
+
const stop = tree.subscribe(state => console.log(state.count));
|
|
2059
|
+
// tree.destroy() also cleans up all registered effects
|
|
2060
|
+
```
|
|
2061
|
+
|
|
2062
|
+
> **Note:** `isSignal(tree.$.users.byId(id)?.name)` returns `true` (v9.3+). Entity field
|
|
2063
|
+
> properties are computed signals — they work with `toObservable()`, Angular DevTools, and
|
|
2064
|
+
> any Angular API that accepts a `Signal<T>`.
|
|
2065
|
+
|
|
2049
2066
|
## Core API reference
|
|
2050
2067
|
|
|
2051
2068
|
### signalTree()
|
|
@@ -2059,21 +2076,21 @@ const tree = signalTree(initialState, config?);
|
|
|
2059
2076
|
```typescript
|
|
2060
2077
|
// State access
|
|
2061
2078
|
tree.$.property(); // Read signal value
|
|
2062
|
-
tree.$.property.set(value); //
|
|
2063
|
-
tree.
|
|
2079
|
+
tree.$.property.set(value); // Set signal
|
|
2080
|
+
tree.$.property.update(fn); // Update signal with function
|
|
2064
2081
|
|
|
2065
2082
|
// Tree operations
|
|
2066
|
-
tree.update(updater); // Update entire tree
|
|
2067
2083
|
tree.updateAndReport(updater); // Update + return changed leaf paths (9.1+)
|
|
2068
|
-
tree.effect(fn); // Create reactive effects
|
|
2069
|
-
tree.subscribe(fn); // Manual subscriptions
|
|
2070
2084
|
tree.destroy(); // Cleanup resources
|
|
2071
2085
|
|
|
2072
|
-
// Entity helpers (
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2086
|
+
// Entity helpers (entityMap() self-registers — no enhancer needed)
|
|
2087
|
+
tree.$.users.addOne(user); // Add single entity
|
|
2088
|
+
tree.$.users.byId(id)(); // O(1) lookup by ID (read whole entity)
|
|
2089
|
+
tree.$.users.byId(id)?.name(); // Read single field (computed signal)
|
|
2090
|
+
tree.$.users.byId(id)?.name.set('Bob'); // Write single field (throws if entity removed)
|
|
2091
|
+
tree.$.users.byId(id)?.name.update(fn); // Updater on single field
|
|
2092
|
+
tree.$.users.all; // Signal<E[]> — all entities
|
|
2093
|
+
tree.$.users.where(pred); // Filtered signal
|
|
2077
2094
|
```
|
|
2078
2095
|
|
|
2079
2096
|
### updateAndReport (9.1+)
|
|
@@ -2112,9 +2129,8 @@ const tree = signalTree(initialState).with(batching()).with(timeTravel());
|
|
|
2112
2129
|
All enhancers are included in `@signaltree/core`:
|
|
2113
2130
|
|
|
2114
2131
|
- **batching()** - Batch multiple updates for better performance
|
|
2115
|
-
- **entities()** - Advanced entity management & CRUD operations
|
|
2116
2132
|
- **devTools()** - Redux DevTools integration for debugging
|
|
2117
|
-
- **
|
|
2133
|
+
- **timeTravel()** - Undo/redo functionality & state history
|
|
2118
2134
|
- **serialization()** - State persistence & SSR support
|
|
2119
2135
|
|
|
2120
2136
|
## When to use core only
|
|
@@ -2130,7 +2146,7 @@ Perfect for:
|
|
|
2130
2146
|
Consider enhancers when you need:
|
|
2131
2147
|
|
|
2132
2148
|
- ⚡ Performance optimization (batching)
|
|
2133
|
-
- 🐛 Advanced debugging (devTools,
|
|
2149
|
+
- 🐛 Advanced debugging (devTools, timeTravel)
|
|
2134
2150
|
- 📦 Entity management (entities)
|
|
2135
2151
|
|
|
2136
2152
|
Consider separate packages when you need:
|
|
@@ -2164,7 +2180,7 @@ node_modules/@signaltree/core/skills/using-signaltree/reference/migration-from-n
|
|
|
2164
2180
|
|
|
2165
2181
|
It covers:
|
|
2166
2182
|
|
|
2167
|
-
- A mechanical concept-map table: `signalStore` → tree slice + `Ops`, `withState` → initial state, `withMethods` → `Ops` methods, `withComputed` → `computed()` or `.derived()`, `withHooks` → factory body, `rxMethod` → plain method returning `Observable<void>`, `withEntities` → `entityMap()` marker, `patchState` → tree update, `getState` → `
|
|
2183
|
+
- A mechanical concept-map table: `signalStore` → tree slice + `Ops`, `withState` → initial state, `withMethods` → `Ops` methods, `withComputed` → `computed()` or `.derived()`, `withHooks` → factory body, `rxMethod` → plain method returning `Observable<void>`, `withEntities` → `entityMap()` marker, `patchState` → tree update, `getState` → `tree()` (no-arg call returns current state snapshot`, etc.
|
|
2168
2184
|
- **Three migration strategies** with explicit decision criteria:
|
|
2169
2185
|
- **Big-bang** (1–2 stores, single team): one PR, delete legacy in same commit.
|
|
2170
2186
|
- **Incremental per-domain** (≥3 stores): one PR per store. Includes a **Phase 0** recipe (foundation-only first PR), a sequencing rule (consumers before aggregator removal), and a root-injected `Ops` side-effect hazard warning.
|
|
@@ -2297,14 +2313,10 @@ All enhancers are now consolidated in the core package. The following features a
|
|
|
2297
2313
|
|
|
2298
2314
|
- **batching()** (+1.27KB gzipped) - Batch multiple updates for better performance
|
|
2299
2315
|
|
|
2300
|
-
### Advanced Features
|
|
2301
|
-
|
|
2302
|
-
- **entities()** (+0.97KB gzipped) - Enhanced CRUD operations & entity management
|
|
2303
|
-
|
|
2304
2316
|
### Development Tools
|
|
2305
2317
|
|
|
2306
2318
|
- **devTools()** (+2.49KB gzipped) - Development tools & Redux DevTools integration
|
|
2307
|
-
- **
|
|
2319
|
+
- **timeTravel()** (+1.75KB gzipped) - Undo/redo functionality & state history
|
|
2308
2320
|
|
|
2309
2321
|
### Integration & Convenience
|
|
2310
2322
|
|
|
@@ -2324,7 +2336,7 @@ import {
|
|
|
2324
2336
|
batching,
|
|
2325
2337
|
entities,
|
|
2326
2338
|
devTools,
|
|
2327
|
-
|
|
2339
|
+
timeTravel,
|
|
2328
2340
|
serialization
|
|
2329
2341
|
} from '@signaltree/core';
|
|
2330
2342
|
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { effect, untracked } from '@angular/core';
|
|
2
|
+
import { ENHANCER_META } from '../../lib/types.js';
|
|
3
|
+
|
|
4
|
+
function effects(config = {}) {
|
|
5
|
+
const {
|
|
6
|
+
enabled = true
|
|
7
|
+
} = config;
|
|
8
|
+
const enhancerFn = tree => {
|
|
9
|
+
const cleanupFns = [];
|
|
10
|
+
const methods = {
|
|
11
|
+
effect(effectFn) {
|
|
12
|
+
if (!enabled) {
|
|
13
|
+
return () => {};
|
|
14
|
+
}
|
|
15
|
+
let innerCleanup;
|
|
16
|
+
const effectRef = effect(() => {
|
|
17
|
+
const state = tree();
|
|
18
|
+
if (innerCleanup) {
|
|
19
|
+
untracked(() => innerCleanup());
|
|
20
|
+
}
|
|
21
|
+
innerCleanup = untracked(() => effectFn(state));
|
|
22
|
+
});
|
|
23
|
+
const cleanup = () => {
|
|
24
|
+
if (innerCleanup) {
|
|
25
|
+
innerCleanup();
|
|
26
|
+
}
|
|
27
|
+
effectRef.destroy();
|
|
28
|
+
};
|
|
29
|
+
cleanupFns.push(cleanup);
|
|
30
|
+
return cleanup;
|
|
31
|
+
},
|
|
32
|
+
subscribe(fn) {
|
|
33
|
+
if (!enabled) {
|
|
34
|
+
return () => {};
|
|
35
|
+
}
|
|
36
|
+
const effectRef = effect(() => {
|
|
37
|
+
const state = tree();
|
|
38
|
+
untracked(() => fn(state));
|
|
39
|
+
});
|
|
40
|
+
const cleanup = () => {
|
|
41
|
+
effectRef.destroy();
|
|
42
|
+
};
|
|
43
|
+
cleanupFns.push(cleanup);
|
|
44
|
+
return cleanup;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const originalDestroy = tree.destroy?.bind(tree);
|
|
48
|
+
tree.destroy = () => {
|
|
49
|
+
cleanupFns.forEach(fn => fn());
|
|
50
|
+
cleanupFns.length = 0;
|
|
51
|
+
if (originalDestroy) {
|
|
52
|
+
originalDestroy();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return Object.assign(tree, methods);
|
|
56
|
+
};
|
|
57
|
+
const meta = {
|
|
58
|
+
name: 'effects',
|
|
59
|
+
provides: ['effects']
|
|
60
|
+
};
|
|
61
|
+
enhancerFn.metadata = meta;
|
|
62
|
+
enhancerFn[ENHANCER_META] = meta;
|
|
63
|
+
return enhancerFn;
|
|
64
|
+
}
|
|
65
|
+
function enableEffects() {
|
|
66
|
+
return effects({
|
|
67
|
+
enabled: true
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
Object.assign((config = {}) => effects(config), {
|
|
71
|
+
enable: enableEffects
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export { effects, enableEffects };
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { composeEnhancers, isAnySignal, isNodeAccessor, toWritableSignal } from
|
|
|
10
10
|
export { getPathNotifier } from './lib/path-notifier.js';
|
|
11
11
|
export { createEnhancer, resolveEnhancerOrder } from './enhancers/index.js';
|
|
12
12
|
export { batching } from './enhancers/batching/batching.js';
|
|
13
|
+
export { effects } from './enhancers/effects/effects.js';
|
|
13
14
|
export { timeTravel } from './enhancers/time-travel/time-travel.js';
|
|
14
15
|
export { persistence, serialization } from './enhancers/serialization/serialization.js';
|
|
15
16
|
export { devTools } from './enhancers/devtools/devtools.js';
|
package/dist/lib/constants.js
CHANGED
|
@@ -41,14 +41,7 @@ const DEV_MESSAGES = {
|
|
|
41
41
|
EFFECT_NO_CONTEXT: 'no angular context',
|
|
42
42
|
SUBSCRIBE_NO_CONTEXT: 'no angular context'
|
|
43
43
|
};
|
|
44
|
-
const PROD_MESSAGES =
|
|
45
|
-
const out = {};
|
|
46
|
-
let i = 0;
|
|
47
|
-
for (const k of Object.keys(DEV_MESSAGES)) {
|
|
48
|
-
out[k] = String(i++);
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
})();
|
|
44
|
+
const PROD_MESSAGES = DEV_MESSAGES;
|
|
52
45
|
const _isProdByEnv = Boolean(typeof globalThis === 'object' && globalThis !== null && 'process' in globalThis && typeof globalThis.process === 'object' && 'env' in globalThis.process && globalThis.process.env.NODE_ENV === 'production');
|
|
53
46
|
const _isDev = typeof ngDevMode !== 'undefined' ? Boolean(ngDevMode) : !_isProdByEnv;
|
|
54
47
|
const isDev = _isDev;
|
|
@@ -20,12 +20,39 @@ function createEntitySignal(config, pathNotifier, basePath) {
|
|
|
20
20
|
mapSignal.set(map);
|
|
21
21
|
}
|
|
22
22
|
function createEntityNode(id, entity) {
|
|
23
|
-
const node =
|
|
23
|
+
const node = valueOrUpdater => {
|
|
24
|
+
if (valueOrUpdater === undefined) {
|
|
25
|
+
return mapSignal().get(id);
|
|
26
|
+
}
|
|
27
|
+
const current = storage.get(id);
|
|
28
|
+
if (current === undefined) {
|
|
29
|
+
throw new Error(`Entity with id ${String(id)} not found`);
|
|
30
|
+
}
|
|
31
|
+
if (typeof valueOrUpdater === 'function') {
|
|
32
|
+
api.updateOne(id, valueOrUpdater(current));
|
|
33
|
+
} else {
|
|
34
|
+
api.updateOne(id, valueOrUpdater);
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
24
38
|
for (const key of Object.keys(entity)) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
const fieldKey = key;
|
|
40
|
+
const fieldSignal = computed(() => mapSignal().get(id)?.[fieldKey]);
|
|
41
|
+
Object.assign(fieldSignal, {
|
|
42
|
+
set: value => {
|
|
43
|
+
api.updateOne(id, {
|
|
44
|
+
[fieldKey]: value
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
update: fn => {
|
|
48
|
+
api.updateOne(id, {
|
|
49
|
+
[fieldKey]: fn(mapSignal().get(id)?.[fieldKey])
|
|
50
|
+
});
|
|
28
51
|
},
|
|
52
|
+
asReadonly: () => fieldSignal
|
|
53
|
+
});
|
|
54
|
+
Object.defineProperty(node, key, {
|
|
55
|
+
get: () => fieldSignal,
|
|
29
56
|
enumerable: true,
|
|
30
57
|
configurable: true
|
|
31
58
|
});
|
|
@@ -117,18 +144,29 @@ function createEntitySignal(config, pathNotifier, basePath) {
|
|
|
117
144
|
return id;
|
|
118
145
|
},
|
|
119
146
|
addMany(entities, opts) {
|
|
120
|
-
const
|
|
147
|
+
const mode = opts?.mode ?? 'strict';
|
|
148
|
+
const toProcess = [];
|
|
121
149
|
for (const entity of entities) {
|
|
122
150
|
const id = opts?.selectId?.(entity) ?? selectId(entity);
|
|
123
151
|
if (storage.has(id)) {
|
|
124
|
-
|
|
152
|
+
if (mode === 'strict') {
|
|
153
|
+
throw new Error(`Entity with id ${String(id)} already exists`);
|
|
154
|
+
} else if (mode === 'skip') {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
125
157
|
}
|
|
126
|
-
|
|
158
|
+
toProcess.push({
|
|
159
|
+
entity,
|
|
160
|
+
id
|
|
161
|
+
});
|
|
127
162
|
}
|
|
163
|
+
if (toProcess.length === 0) return [];
|
|
164
|
+
const processedIds = [];
|
|
128
165
|
const addedEntities = [];
|
|
129
|
-
for (
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
for (const {
|
|
167
|
+
entity,
|
|
168
|
+
id
|
|
169
|
+
} of toProcess) {
|
|
132
170
|
let transformedEntity = entity;
|
|
133
171
|
for (const handler of interceptHandlers) {
|
|
134
172
|
const ctx = {
|
|
@@ -145,6 +183,7 @@ function createEntitySignal(config, pathNotifier, basePath) {
|
|
|
145
183
|
}
|
|
146
184
|
storage.set(id, transformedEntity);
|
|
147
185
|
nodeCache.delete(id);
|
|
186
|
+
processedIds.push(id);
|
|
148
187
|
addedEntities.push({
|
|
149
188
|
id,
|
|
150
189
|
entity: transformedEntity
|
|
@@ -165,7 +204,7 @@ function createEntitySignal(config, pathNotifier, basePath) {
|
|
|
165
204
|
handler.onAdd?.(entity, id);
|
|
166
205
|
}
|
|
167
206
|
}
|
|
168
|
-
return
|
|
207
|
+
return processedIds;
|
|
169
208
|
},
|
|
170
209
|
updateOne(id, changes) {
|
|
171
210
|
const entity = storage.get(id);
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { signalTree } from './lib/signal-tree';
|
|
2
|
-
export type { ISignalTree, SignalTree, SignalTreeBase, TreeNode, CallableWritableSignal, AccessibleNode, NodeAccessor, Primitive, NotFn, TreeConfig, Enhancer, EnhancerMeta, EnhancerWithMeta, EntitySignal, EntityMapMarker, EntityConfig, MutationOptions, AddOptions, AddManyOptions, TimeTravelEntry, TimeTravelMethods, EnhancerCleanup, } from './lib/types';
|
|
2
|
+
export type { ISignalTree, SignalTree, SignalTreeBase, TreeNode, CallableWritableSignal, AccessibleNode, NodeAccessor, Primitive, NotFn, TreeConfig, Enhancer, EnhancerMeta, EnhancerWithMeta, EntitySignal, EntityMapMarker, EntityConfig, MutationOptions, AddOptions, AddManyOptions, TimeTravelEntry, TimeTravelMethods, EnhancerCleanup, EffectsMethods, } from './lib/types';
|
|
3
3
|
export { entityMap } from './lib/types';
|
|
4
4
|
export type { ProcessDerived, DeepMergeTree, DerivedFactory, WithDerived, } from './lib/internals/derived-types';
|
|
5
5
|
export { derivedFrom, externalDerived } from './lib/internals/derived-types';
|
|
@@ -15,6 +15,8 @@ export { createEnhancer, resolveEnhancerOrder } from './enhancers/index';
|
|
|
15
15
|
export { ENHANCER_META } from './lib/types';
|
|
16
16
|
export { batching } from './enhancers/batching/batching';
|
|
17
17
|
export type { BatchingConfig, BatchingMethods } from './lib/types';
|
|
18
|
+
export { effects } from './enhancers/effects/effects';
|
|
19
|
+
export type { EffectsConfig } from './enhancers/effects/effects';
|
|
18
20
|
export { timeTravel } from './enhancers/time-travel/time-travel';
|
|
19
21
|
export { serialization, persistence, } from './enhancers/serialization/serialization';
|
|
20
22
|
export { devTools } from './enhancers/devtools/devtools';
|