@signaltree/core 7.6.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 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**: [Implementation Patterns](https://github.com/JBorgia/signaltree/blob/main/docs/IMPLEMENTATION_PATTERNS.md)
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()` - Advanced CRUD operations for collections
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 integration
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, entities } from '@signaltree/core';
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, entities } from '@signaltree/core';
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 + entities - use entity helpers
791
+ // With entityMap (v7+ auto-processed) - use entity helpers
792
792
  const enhanced = signalTree({
793
793
  users: entityMap<User>(),
794
- }).with(entities());
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, entities } from '@signaltree/core';
1577
+ import { signalTree, entityMap } from '@signaltree/core';
1578
1578
 
1579
- // Add data management capabilities (+2.77KB total)
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
- }).with(entities());
1584
+ });
1585
1585
 
1586
1586
  // Advanced entity operations via tree.$ accessor
1587
1587
  tree.$.users.addOne(newUser);
1588
- tree.$.users.selectBy((u) => u.active);
1589
- tree.$.users.updateMany([{ id: '1', changes: { status: 'active' } }]);
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
- }).with(entities());
1606
+ });
1607
1607
 
1608
1608
  // Access nested entities using tree.$ accessor
1609
- appTree.$.app.data.users.selectBy((u) => u.isAdmin); // Filtered signal
1610
- appTree.$.app.data.products.selectTotal(); // Count signal
1611
- appTree.$.admin.data.logs.all; // All items as array
1612
- appTree.$.admin.data.reports.selectIds(); // ID array signal
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, entities, serialization, withTimeTravel, devTools } from '@signaltree/core';
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
- trace: true,
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, entities, serialization } from '@signaltree/core';
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, entities, devTools, withTimeTravel } from '@signaltree/core';
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.with(entities());
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.selectAll()(); track user.id) {
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; // Get all as array
2037
- // tree.$.users.selectBy(pred); // Filtered signal
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()** - Advanced entity management & CRUD operations
2058
- - **devTools()** - Redux DevTools integration for debugging
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) - Enhanced CRUD operations & entity management
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) - Development tools & Redux DevTools integration
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
@@ -216,7 +216,7 @@ function sanitizeState(value, options, depth = 0, seen = new WeakSet()) {
216
216
  if (typeof value === 'number' || typeof value === 'boolean') return value;
217
217
  if (typeof value === 'bigint') return `${value.toString()}n`;
218
218
  if (typeof value === 'symbol') return String(value);
219
- if (typeof value === 'function') return '[Function]';
219
+ if (typeof value === 'function') return undefined;
220
220
  if (depth >= maxDepth) return '[MaxDepth]';
221
221
  if (value instanceof Date) return value.toISOString();
222
222
  if (value instanceof RegExp) return value.toString();
@@ -245,6 +245,7 @@ function sanitizeState(value, options, depth = 0, seen = new WeakSet()) {
245
245
  }
246
246
  const result = {};
247
247
  for (const [key, val] of Object.entries(obj)) {
248
+ if (typeof val === 'function') continue;
248
249
  result[key] = sanitizeState(val, options, depth + 1, seen);
249
250
  }
250
251
  return result;
@@ -299,6 +300,7 @@ function devTools(config = {}) {
299
300
  logger.logComposition(modules, 'with');
300
301
  };
301
302
  const activeProfiles = new Map();
303
+ let browserDevToolsConnection = null;
302
304
  let browserDevTools = null;
303
305
  let isConnected = false;
304
306
  let isApplyingExternalState = false;
@@ -374,7 +376,7 @@ function devTools(config = {}) {
374
376
  lastSnapshot = currentSnapshot;
375
377
  return;
376
378
  }
377
- const effectiveAction = pendingExplicitAction ? pendingAction : formattedPaths.length === 1 ? buildAction(`SignalTree/${formattedPaths[0]}`, formattedPaths[0]) : formattedPaths.length > 1 ? buildAction('SignalTree/batch', formattedPaths) : buildAction('SignalTree/update');
379
+ const effectiveAction = pendingExplicitAction ? pendingAction : formattedPaths.length === 1 ? buildAction(`SignalTree/${formattedPaths[0]}`, formattedPaths[0]) : formattedPaths.length > 1 ? buildAction('SignalTree/update', formattedPaths) : buildAction('SignalTree/update');
378
380
  const actionMeta = {
379
381
  timestamp: Date.now(),
380
382
  ...(pendingSource && {
@@ -521,15 +523,20 @@ function devTools(config = {}) {
521
523
  skip: true
522
524
  }
523
525
  });
526
+ browserDevToolsConnection = connection;
524
527
  browserDevTools = {
525
528
  send: connection.send,
526
529
  subscribe: connection.subscribe
527
530
  };
528
531
  if (browserDevTools.subscribe && !unsubscribeDevTools) {
529
- browserDevTools.subscribe(handleDevToolsMessage);
530
- unsubscribeDevTools = () => {
531
- browserDevTools?.subscribe?.(() => void 0);
532
- };
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
+ }
533
540
  }
534
541
  const rawSnapshot = readSnapshot();
535
542
  const sanitized = buildSerializedState(rawSnapshot);
@@ -652,7 +659,17 @@ function devTools(config = {}) {
652
659
  initBrowserDevTools();
653
660
  },
654
661
  disconnectDevTools() {
662
+ try {
663
+ unsubscribeDevTools?.();
664
+ } catch {}
665
+ try {
666
+ browserDevToolsConnection?.unsubscribe?.();
667
+ } catch {}
668
+ try {
669
+ browserDevToolsConnection?.disconnect?.();
670
+ } catch {}
655
671
  browserDevTools = null;
672
+ browserDevToolsConnection = null;
656
673
  isConnected = false;
657
674
  if (unsubscribeNotifier) {
658
675
  unsubscribeNotifier();
@@ -662,10 +679,7 @@ function devTools(config = {}) {
662
679
  unsubscribeFlush();
663
680
  unsubscribeFlush = null;
664
681
  }
665
- if (unsubscribeDevTools) {
666
- unsubscribeDevTools();
667
- unsubscribeDevTools = null;
668
- }
682
+ unsubscribeDevTools = null;
669
683
  if (effectRef) {
670
684
  effectRef.destroy();
671
685
  effectRef = null;
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)) {
@@ -263,6 +271,12 @@ function snapshotState(state) {
263
271
  function applyState(stateNode, snapshot) {
264
272
  if (snapshot === null || snapshot === undefined) return;
265
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
+ }
266
280
  for (const key of Object.keys(snapshot)) {
267
281
  const val = snapshot[key];
268
282
  const target = stateNode[key];
@@ -288,6 +302,14 @@ function applyState(stateNode, snapshot) {
288
302
  target(val);
289
303
  } catch {}
290
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
+ }
291
313
  } else {
292
314
  try {
293
315
  stateNode[key] = val;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/core",
3
- "version": "7.6.0",
3
+ "version": "7.6.1",
4
4
  "description": "Reactive JSON for Angular. JSON branches, reactive leaves. No actions. No reducers. No selectors.",
5
5
  "license": "MIT",
6
6
  "type": "module",