@signaltree/core 4.2.0 → 5.0.0

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
@@ -446,7 +446,7 @@ const activeUsers = computed(() => tree.$.users().filter((user) => user.active))
446
446
 
447
447
  ### 4) Manual async state management
448
448
 
449
- Core provides basic state updates. For advanced async helpers, use the built-in middleware functions (`createAsyncOperation`, `trackAsync`):
449
+ Core provides basic state updates. For advanced async helpers, use the built-in async helpers (`createAsyncOperation`, `trackAsync`):
450
450
 
451
451
  ```typescript
452
452
  const tree = signalTree({
@@ -519,12 +519,6 @@ All enhancers are exported directly from `@signaltree/core`:
519
519
  - `withDevTools()` - Redux DevTools integration
520
520
  - `withTimeTravel()` - Undo/redo functionality
521
521
 
522
- **Integration:**
523
-
524
- - `withMiddleware()` - State interceptors and logging
525
- - `createLoggingMiddleware()` - Built-in logging middleware
526
- - `createValidationMiddleware()` - Built-in validation middleware
527
-
528
522
  **Presets:**
529
523
 
530
524
  - `createDevTree()` - Pre-configured development setup
@@ -585,7 +579,7 @@ const tree = signalTree({
585
579
  user: null as User | null,
586
580
  preferences: { theme: 'light' },
587
581
  }).with(
588
- // withAsync removed — API integration patterns are now covered by middleware helpers
582
+ // withAsync removed — API integration patterns are now covered by async helpers
589
583
  withSerialization({
590
584
  // Auto-save to localStorage
591
585
  autoSave: true,
@@ -594,7 +588,7 @@ const tree = signalTree({
594
588
  withTimeTravel() // Undo/redo support
595
589
  );
596
590
 
597
- // For async operations, use manual async or middleware helpers
591
+ // For async operations, use manual async or async helpers
598
592
  async function fetchUser(id: string) {
599
593
  tree.$.loading.set(true);
600
594
  try {
@@ -895,7 +889,7 @@ appProducts.selectTotal(); // Count signal
895
889
  adminLogs.selectAll(); // All items signal
896
890
  adminReports.selectIds(); // ID array signal
897
891
 
898
- // For async operations, use manual async or middleware helpers
892
+ // For async operations, use manual async or async helpers
899
893
  async function fetchUsers() {
900
894
  tree.$.ui.loading.set(true);
901
895
  try {
@@ -924,7 +918,7 @@ const tree = signalTree({
924
918
  }).with(
925
919
  withBatching(), // Performance
926
920
  withEntities(), // Data management
927
- // withAsync removed — use middleware helpers for API integration
921
+ // withAsync removed — use async helpers for API integration
928
922
  withSerialization({
929
923
  // State persistence
930
924
  autoSave: true,
@@ -959,7 +953,7 @@ import { signalTree, withBatching, withEntities, withSerialization } from '@sign
959
953
  const tree = signalTree(initialState).with(
960
954
  withBatching(), // Performance optimization
961
955
  withEntities(), // Data management
962
- // withAsync removed — use middleware helpers for API integration
956
+ // withAsync removed — use async helpers for API integration
963
957
  withSerialization({
964
958
  // User preferences
965
959
  autoSave: true,
@@ -1029,7 +1023,7 @@ const tree2 = tree.with(withBatching());
1029
1023
  const tree3 = tree2.with(withEntities());
1030
1024
 
1031
1025
  // Phase 4: Add async for API integration
1032
- // withAsync removed — no explicit async enhancer; use middleware helpers instead
1026
+ // withAsync removed — no explicit async enhancer; use async helpers instead
1033
1027
 
1034
1028
  // Each phase is fully functional and production-ready
1035
1029
  ```
@@ -1316,7 +1310,7 @@ tree.destroy(); // Cleanup resources
1316
1310
 
1317
1311
  // Extended features (built into @signaltree/core)
1318
1312
  tree.entities<T>(key); // Entity helpers (use withEntities enhancer)
1319
- // For async operations, use manual async or middleware helpers like createAsyncOperation or trackAsync
1313
+ // For async operations, use manual async or async helpers like createAsyncOperation or trackAsync
1320
1314
  ```
1321
1315
 
1322
1316
  ## Extending with enhancers
@@ -1336,7 +1330,6 @@ All enhancers are included in `@signaltree/core`:
1336
1330
 
1337
1331
  - **withBatching()** - Batch multiple updates for better performance
1338
1332
  - **withMemoization()** - Intelligent caching & performance optimization
1339
- - **withMiddleware()** - Middleware system & state interceptors
1340
1333
  - **withEntities()** - Advanced entity management & CRUD operations
1341
1334
  - **withDevTools()** - Redux DevTools integration for debugging
1342
1335
  - **withTimeTravel()** - Undo/redo functionality & state history
@@ -1358,8 +1351,7 @@ Consider enhancers when you need:
1358
1351
 
1359
1352
  - ⚡ Performance optimization (withBatching, withMemoization)
1360
1353
  - 🐛 Advanced debugging (withDevTools, withTimeTravel)
1361
- - Entity management (withEntities)
1362
- - 🔌 Middleware patterns (withMiddleware)
1354
+ - 📦 Entity management (withEntities)
1363
1355
 
1364
1356
  Consider separate packages when you need:
1365
1357
 
@@ -1494,7 +1486,6 @@ All enhancers are now consolidated in the core package. The following features a
1494
1486
 
1495
1487
  ### Advanced Features
1496
1488
 
1497
- - **withMiddleware()** (+1.89KB gzipped) - Middleware system & state interceptors
1498
1489
  - **withEntities()** (+0.97KB gzipped) - Enhanced CRUD operations & entity management
1499
1490
 
1500
1491
  ### Development Tools
@@ -1,114 +1,66 @@
1
- import { computed } from '@angular/core';
2
- import { isAnySignal, isNodeAccessor } from '../../../lib/utils.js';
1
+ import { EntitySignalImpl } from '../../../lib/entity-signal.js';
2
+ import { getPathNotifier } from '../../../lib/path-notifier.js';
3
+ import { isNodeAccessor } from '../../../lib/utils.js';
3
4
 
4
- function resolveNestedSignal(tree, path) {
5
- const pathStr = String(path);
6
- if (!pathStr.includes('.')) {
7
- const signal = tree.state[pathStr];
8
- if (!signal) {
9
- throw new Error(`Entity key '${pathStr}' does not exist in the state. Available top-level keys: ${Object.keys(tree.state).join(', ')}`);
5
+ function isEntityMapMarker(value) {
6
+ return Boolean(value && typeof value === 'object' && value['__isEntityMap'] === true);
7
+ }
8
+ function isEntitySignal(value) {
9
+ return !!value && typeof value === 'object' && typeof value['addOne'] === 'function' && typeof value['all'] === 'function';
10
+ }
11
+ function materializeEntities(tree, notifier = getPathNotifier()) {
12
+ const registry = new Map();
13
+ const state = tree.state;
14
+ const visit = (parent, key, value, path) => {
15
+ const nextPath = [...path, key];
16
+ if (isEntityMapMarker(value)) {
17
+ const basePath = nextPath.join('.');
18
+ const config = value.__entityMapConfig ?? {};
19
+ const entitySignal = new EntitySignalImpl(config, notifier, basePath);
20
+ if (parent) {
21
+ try {
22
+ parent[key] = entitySignal;
23
+ } catch {}
24
+ }
25
+ try {
26
+ tree[key] = entitySignal;
27
+ } catch {}
28
+ registry.set(basePath, entitySignal);
29
+ return;
10
30
  }
11
- return signal;
31
+ if (isNodeAccessor(value)) {
32
+ const nodeAsAny = value;
33
+ for (const childKey of Object.keys(nodeAsAny)) {
34
+ visit(nodeAsAny, childKey, nodeAsAny[childKey], nextPath);
35
+ }
36
+ return;
37
+ }
38
+ if (value && typeof value === 'object') {
39
+ for (const childKey of Object.keys(value)) {
40
+ visit(value, childKey, value[childKey], nextPath);
41
+ }
42
+ }
43
+ };
44
+ for (const key of Object.keys(state)) {
45
+ visit(state, key, state[key], []);
12
46
  }
47
+ return registry;
48
+ }
49
+ function resolveEntitySignal(tree, registry, path) {
50
+ const pathStr = String(path);
51
+ const existing = registry.get(pathStr);
52
+ if (existing) return existing;
13
53
  const segments = pathStr.split('.');
14
54
  let current = tree.state;
15
- for (let i = 0; i < segments.length; i++) {
16
- const segment = segments[i];
17
- if (isAnySignal(current)) {
18
- current = current();
19
- }
55
+ for (const segment of segments) {
56
+ if (!current) break;
20
57
  current = current[segment];
21
- if (current === undefined) {
22
- const attemptedPath = segments.slice(0, i + 1).join('.');
23
- throw new Error(`Entity path '${pathStr}' is invalid: '${attemptedPath}' does not exist in the state`);
24
- }
25
58
  }
26
- if (isAnySignal(current)) {
59
+ if (isEntitySignal(current)) {
60
+ registry.set(pathStr, current);
27
61
  return current;
28
62
  }
29
- throw new Error(`Entity path '${pathStr}' does not resolve to a signal. Ensure all parent levels in the path are valid nested objects.`);
30
- }
31
- function createEntityHelpers(tree, entityKey) {
32
- const getEntitySignal = () => {
33
- const signal = resolveNestedSignal(tree, entityKey);
34
- if (!isAnySignal(signal)) {
35
- throw new Error(`Entity key '${String(entityKey)}' is not a signal. This should not happen with SignalTree.`);
36
- }
37
- const castSignal = signal;
38
- const value = castSignal();
39
- if (!Array.isArray(value)) {
40
- throw new Error(`Entity key '${String(entityKey)}' does not contain an array. Current type: ${typeof value}`);
41
- }
42
- return castSignal;
43
- };
44
- const setSignalValue = (signal, value) => {
45
- if (isNodeAccessor(signal)) {
46
- signal(value);
47
- } else {
48
- signal.set(value);
49
- }
50
- };
51
- return {
52
- add: entity => {
53
- const entitySignal = getEntitySignal();
54
- const currentEntities = entitySignal();
55
- if (currentEntities.some(e => e.id === entity.id)) {
56
- throw new Error(`Entity with id '${entity.id}' already exists`);
57
- }
58
- setSignalValue(entitySignal, [...currentEntities, entity]);
59
- },
60
- update: (id, updates) => {
61
- const entitySignal = getEntitySignal();
62
- const currentEntities = entitySignal();
63
- const updatedEntities = currentEntities.map(entity => entity.id === id ? {
64
- ...entity,
65
- ...updates
66
- } : entity);
67
- setSignalValue(entitySignal, updatedEntities);
68
- },
69
- remove: id => {
70
- const entitySignal = getEntitySignal();
71
- const currentEntities = entitySignal();
72
- const filteredEntities = currentEntities.filter(entity => entity.id !== id);
73
- setSignalValue(entitySignal, filteredEntities);
74
- },
75
- upsert: entity => {
76
- const entitySignal = getEntitySignal();
77
- const currentEntities = entitySignal();
78
- const index = currentEntities.findIndex(e => e.id === entity.id);
79
- if (index >= 0) {
80
- const updatedEntities = [...currentEntities];
81
- updatedEntities[index] = entity;
82
- setSignalValue(entitySignal, updatedEntities);
83
- } else {
84
- setSignalValue(entitySignal, [...currentEntities, entity]);
85
- }
86
- },
87
- selectById: id => {
88
- const entitySignal = getEntitySignal();
89
- return computed(() => entitySignal().find(entity => entity.id === id));
90
- },
91
- selectBy: predicate => {
92
- const entitySignal = getEntitySignal();
93
- return computed(() => entitySignal().filter(predicate));
94
- },
95
- selectIds: () => {
96
- const entitySignal = getEntitySignal();
97
- return computed(() => entitySignal().map(entity => entity.id));
98
- },
99
- selectAll: () => {
100
- const entitySignal = getEntitySignal();
101
- return entitySignal;
102
- },
103
- selectTotal: () => {
104
- const entitySignal = getEntitySignal();
105
- return computed(() => entitySignal().length);
106
- },
107
- clear: () => {
108
- const entitySignal = getEntitySignal();
109
- setSignalValue(entitySignal, []);
110
- }
111
- };
63
+ throw new Error(`Entity path '${pathStr}' is not configured. Define it with entityMap() in your initial state.`);
112
64
  }
113
65
  function withEntities(config = {}) {
114
66
  const {
@@ -118,9 +70,10 @@ function withEntities(config = {}) {
118
70
  if (!enabled) {
119
71
  return tree;
120
72
  }
73
+ const registry = materializeEntities(tree);
121
74
  const enhancedTree = Object.assign(tree, {
122
- entities(entityKey) {
123
- return createEntityHelpers(tree, entityKey);
75
+ entities(path) {
76
+ return resolveEntitySignal(tree, registry, path);
124
77
  }
125
78
  });
126
79
  return enhancedTree;
@@ -133,7 +86,8 @@ function enableEntities() {
133
86
  }
134
87
  function withHighPerformanceEntities() {
135
88
  return withEntities({
136
- enabled: true});
89
+ enabled: true
90
+ });
137
91
  }
138
92
 
139
93
  export { enableEntities, withEntities, withHighPerformanceEntities };
@@ -1,3 +1,4 @@
1
+ import { computed } from '@angular/core';
1
2
  import { isNodeAccessor } from '../../../lib/utils.js';
2
3
  import { LRUCache } from '../../../lru-cache.js';
3
4
  import { deepEqual } from '../../../deep-equal.js';
@@ -281,6 +282,25 @@ function withMemoization(config = {}) {
281
282
  });
282
283
  applyUpdateResult(result);
283
284
  };
285
+ const memoizeResultCache = createMemoCacheStore(MAX_CACHE_SIZE, true);
286
+ tree.memoize = (fn, cacheKey) => {
287
+ return computed(() => {
288
+ const currentState = originalTreeCall();
289
+ const key = cacheKey || generateCacheKey(fn, [currentState]);
290
+ const cached = memoizeResultCache.get(key);
291
+ if (cached && equalityFn(cached.deps, [currentState])) {
292
+ return cached.value;
293
+ }
294
+ const result = fn(currentState);
295
+ memoizeResultCache.set(key, {
296
+ value: result,
297
+ deps: [currentState],
298
+ timestamp: Date.now(),
299
+ hitCount: 1
300
+ });
301
+ return result;
302
+ });
303
+ };
284
304
  tree.clearMemoCache = key => {
285
305
  if (key) {
286
306
  cache.delete(key);
@@ -2,7 +2,6 @@ import { composeEnhancers } from '../../../lib/utils.js';
2
2
  import { withHighPerformanceBatching, withBatching } from '../../batching/lib/batching.js';
3
3
  import { withDevTools } from '../../devtools/lib/devtools.js';
4
4
  import { withHighPerformanceMemoization, withMemoization } from '../../memoization/lib/memoization.js';
5
- import { withMiddleware } from '../../middleware/lib/middleware.js';
6
5
  import { withTimeTravel } from '../../time-travel/lib/time-travel.js';
7
6
 
8
7
  const TREE_PRESETS = {
@@ -76,7 +75,7 @@ function combinePresets(presets, overrides = {}) {
76
75
  }
77
76
  function createDevTree(overrides = {}) {
78
77
  const config = createPresetConfig('development', overrides);
79
- const composed = composeEnhancers(withBatching(), withHighPerformanceBatching(), withMemoization(), withHighPerformanceMemoization(), withMiddleware(), withTimeTravel(), withDevTools({
78
+ const composed = composeEnhancers(withBatching(), withHighPerformanceBatching(), withMemoization(), withHighPerformanceMemoization(), withTimeTravel(), withDevTools({
80
79
  treeName: config.treeName ?? 'SignalTree Dev'
81
80
  }));
82
81
  return {
@@ -544,6 +544,8 @@ function withPersistence(config) {
544
544
  }
545
545
  if (autoSave) {
546
546
  let saveTimeout;
547
+ let previousState = JSON.stringify(tree());
548
+ let pollingActive = true;
547
549
  const triggerAutoSave = () => {
548
550
  if (saveTimeout) {
549
551
  clearTimeout(saveTimeout);
@@ -554,28 +556,30 @@ function withPersistence(config) {
554
556
  });
555
557
  }, debounceMs);
556
558
  };
557
- const watchSignals = (obj, path = '') => {
558
- if (!obj || typeof obj !== 'object') return;
559
- for (const [key, value] of Object.entries(obj)) {
560
- if (isSignal(value)) {
561
- const signal = value;
562
- let previousValue = signal();
563
- const checkForChanges = () => {
564
- const currentValue = signal();
565
- if (currentValue !== previousValue) {
566
- previousValue = currentValue;
567
- triggerAutoSave();
568
- }
569
- setTimeout(checkForChanges, 50);
570
- };
571
- setTimeout(checkForChanges, 0);
572
- } else if (value && typeof value === 'object') {
573
- watchSignals(value, path ? `${path}.${key}` : key);
559
+ let usingReactiveSubscription = false;
560
+ try {
561
+ tree.subscribe(() => {
562
+ const currentState = JSON.stringify(tree());
563
+ if (currentState !== previousState) {
564
+ previousState = currentState;
565
+ triggerAutoSave();
574
566
  }
575
- }
576
- };
577
- watchSignals(tree.state);
567
+ });
568
+ usingReactiveSubscription = true;
569
+ } catch {
570
+ const checkForChanges = () => {
571
+ if (!pollingActive) return;
572
+ const currentState = JSON.stringify(tree());
573
+ if (currentState !== previousState) {
574
+ previousState = currentState;
575
+ triggerAutoSave();
576
+ }
577
+ setTimeout(checkForChanges, 100);
578
+ };
579
+ setTimeout(checkForChanges, 0);
580
+ }
578
581
  enhanced.__flushAutoSave = () => {
582
+ pollingActive = false;
579
583
  if (saveTimeout) {
580
584
  clearTimeout(saveTimeout);
581
585
  saveTimeout = undefined;
package/dist/index.js CHANGED
@@ -1,16 +1,15 @@
1
1
  export { signalTree } from './lib/signal-tree.js';
2
+ export { ENHANCER_META, entityMap } from './lib/types.js';
2
3
  export { composeEnhancers, createLazySignalTree, isAnySignal, isNodeAccessor, toWritableSignal } from './lib/utils.js';
3
4
  export { SecurityPresets, SecurityValidator } from './lib/security/security-validator.js';
4
5
  export { createEnhancer, resolveEnhancerOrder } from './enhancers/index.js';
5
- export { ENHANCER_META } from './lib/types.js';
6
6
  export { flushBatchedUpdates, getBatchQueueSize, hasPendingUpdates, withBatching, withHighPerformanceBatching } from './enhancers/batching/lib/batching.js';
7
7
  export { cleanupMemoizationCache, clearAllCaches, getGlobalCacheStats, memoize, memoizeReference, memoizeShallow, withComputedMemoization, withDeepStateMemoization, withHighFrequencyMemoization, withHighPerformanceMemoization, withLightweightMemoization, withMemoization, withSelectorMemoization, withShallowMemoization } from './enhancers/memoization/lib/memoization.js';
8
8
  export { enableTimeTravel, getTimeTravel, withTimeTravel } from './enhancers/time-travel/lib/time-travel.js';
9
9
  export { enableEntities, withEntities, withHighPerformanceEntities } from './enhancers/entities/lib/entities.js';
10
10
  export { applyPersistence, applySerialization, createIndexedDBAdapter, createStorageAdapter, enableSerialization, withPersistence, withSerialization } from './enhancers/serialization/lib/serialization.js';
11
11
  export { enableDevTools, withDevTools, withFullDevTools, withProductionDevTools } from './enhancers/devtools/lib/devtools.js';
12
- export { createLoggingMiddleware, createValidationMiddleware, withMiddleware } from './enhancers/middleware/lib/middleware.js';
13
- export { createAsyncOperation, trackAsync } from './enhancers/middleware/lib/async-helpers.js';
12
+ export { createAsyncOperation, trackAsync } from './lib/async-helpers.js';
14
13
  export { TREE_PRESETS, combinePresets, createDevTree, createPresetConfig, getAvailablePresets, validatePreset } from './enhancers/presets/lib/presets.js';
15
14
  export { computedEnhancer, createComputed } from './enhancers/computed/lib/computed.js';
16
15
  export { SIGNAL_TREE_CONSTANTS, SIGNAL_TREE_MESSAGES } from './lib/constants.js';