@signaltree/core 4.2.1 → 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 +9 -18
- package/dist/enhancers/entities/lib/entities.js +59 -102
- package/dist/enhancers/presets/lib/presets.js +1 -2
- package/dist/enhancers/serialization/lib/serialization.js +24 -20
- package/dist/index.js +2 -3
- package/dist/lib/entity-signal.js +280 -0
- package/dist/lib/path-notifier.js +106 -0
- package/dist/lib/signal-tree.js +12 -12
- package/dist/lib/types.js +7 -1
- package/package.json +1 -1
- package/src/enhancers/entities/lib/entities.d.ts +14 -16
- package/src/enhancers/presets/lib/presets.d.ts +1 -1
- package/src/enhancers/types.d.ts +0 -31
- package/src/index.d.ts +3 -3
- package/src/{enhancers/middleware/lib → lib}/async-helpers.d.ts +1 -1
- package/src/lib/entity-signal.d.ts +1 -0
- package/src/lib/path-notifier.d.ts +4 -0
- package/src/lib/types.d.ts +127 -7
- package/dist/enhancers/middleware/lib/middleware.js +0 -156
- package/src/enhancers/middleware/index.d.ts +0 -2
- package/src/enhancers/middleware/jest.config.d.ts +0 -15
- package/src/enhancers/middleware/lib/middleware.d.ts +0 -11
- package/src/enhancers/middleware/test-setup.d.ts +0 -1
- /package/dist/{enhancers/middleware/lib → lib}/async-helpers.js +0 -0
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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,111 +1,66 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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;
|
|
30
|
+
}
|
|
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;
|
|
10
37
|
}
|
|
11
|
-
|
|
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 (
|
|
16
|
-
|
|
55
|
+
for (const segment of segments) {
|
|
56
|
+
if (!current) break;
|
|
17
57
|
current = current[segment];
|
|
18
|
-
if (current === undefined) {
|
|
19
|
-
const attemptedPath = segments.slice(0, i + 1).join('.');
|
|
20
|
-
throw new Error(`Entity path '${pathStr}' is invalid: '${attemptedPath}' does not exist in the state`);
|
|
21
|
-
}
|
|
22
58
|
}
|
|
23
|
-
if (
|
|
59
|
+
if (isEntitySignal(current)) {
|
|
60
|
+
registry.set(pathStr, current);
|
|
24
61
|
return current;
|
|
25
62
|
}
|
|
26
|
-
throw new Error(`Entity path '${pathStr}'
|
|
27
|
-
}
|
|
28
|
-
function createEntityHelpers(tree, entityKey) {
|
|
29
|
-
const getEntitySignal = () => {
|
|
30
|
-
const signal = resolveNestedSignal(tree, entityKey);
|
|
31
|
-
if (!isAnySignal(signal)) {
|
|
32
|
-
throw new Error(`Entity key '${String(entityKey)}' is not a signal. This should not happen with SignalTree.`);
|
|
33
|
-
}
|
|
34
|
-
const castSignal = signal;
|
|
35
|
-
const value = castSignal();
|
|
36
|
-
if (!Array.isArray(value)) {
|
|
37
|
-
throw new Error(`Entity key '${String(entityKey)}' does not contain an array. Current type: ${typeof value}`);
|
|
38
|
-
}
|
|
39
|
-
return castSignal;
|
|
40
|
-
};
|
|
41
|
-
const setSignalValue = (signal, value) => {
|
|
42
|
-
if (isNodeAccessor(signal)) {
|
|
43
|
-
signal(value);
|
|
44
|
-
} else {
|
|
45
|
-
signal.set(value);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
return {
|
|
49
|
-
add: entity => {
|
|
50
|
-
const entitySignal = getEntitySignal();
|
|
51
|
-
const currentEntities = entitySignal();
|
|
52
|
-
if (currentEntities.some(e => e.id === entity.id)) {
|
|
53
|
-
throw new Error(`Entity with id '${entity.id}' already exists`);
|
|
54
|
-
}
|
|
55
|
-
setSignalValue(entitySignal, [...currentEntities, entity]);
|
|
56
|
-
},
|
|
57
|
-
update: (id, updates) => {
|
|
58
|
-
const entitySignal = getEntitySignal();
|
|
59
|
-
const currentEntities = entitySignal();
|
|
60
|
-
const updatedEntities = currentEntities.map(entity => entity.id === id ? {
|
|
61
|
-
...entity,
|
|
62
|
-
...updates
|
|
63
|
-
} : entity);
|
|
64
|
-
setSignalValue(entitySignal, updatedEntities);
|
|
65
|
-
},
|
|
66
|
-
remove: id => {
|
|
67
|
-
const entitySignal = getEntitySignal();
|
|
68
|
-
const currentEntities = entitySignal();
|
|
69
|
-
const filteredEntities = currentEntities.filter(entity => entity.id !== id);
|
|
70
|
-
setSignalValue(entitySignal, filteredEntities);
|
|
71
|
-
},
|
|
72
|
-
upsert: entity => {
|
|
73
|
-
const entitySignal = getEntitySignal();
|
|
74
|
-
const currentEntities = entitySignal();
|
|
75
|
-
const index = currentEntities.findIndex(e => e.id === entity.id);
|
|
76
|
-
if (index >= 0) {
|
|
77
|
-
const updatedEntities = [...currentEntities];
|
|
78
|
-
updatedEntities[index] = entity;
|
|
79
|
-
setSignalValue(entitySignal, updatedEntities);
|
|
80
|
-
} else {
|
|
81
|
-
setSignalValue(entitySignal, [...currentEntities, entity]);
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
selectById: id => {
|
|
85
|
-
const entitySignal = getEntitySignal();
|
|
86
|
-
return computed(() => entitySignal().find(entity => entity.id === id));
|
|
87
|
-
},
|
|
88
|
-
selectBy: predicate => {
|
|
89
|
-
const entitySignal = getEntitySignal();
|
|
90
|
-
return computed(() => entitySignal().filter(predicate));
|
|
91
|
-
},
|
|
92
|
-
selectIds: () => {
|
|
93
|
-
const entitySignal = getEntitySignal();
|
|
94
|
-
return computed(() => entitySignal().map(entity => entity.id));
|
|
95
|
-
},
|
|
96
|
-
selectAll: () => {
|
|
97
|
-
const entitySignal = getEntitySignal();
|
|
98
|
-
return entitySignal;
|
|
99
|
-
},
|
|
100
|
-
selectTotal: () => {
|
|
101
|
-
const entitySignal = getEntitySignal();
|
|
102
|
-
return computed(() => entitySignal().length);
|
|
103
|
-
},
|
|
104
|
-
clear: () => {
|
|
105
|
-
const entitySignal = getEntitySignal();
|
|
106
|
-
setSignalValue(entitySignal, []);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
63
|
+
throw new Error(`Entity path '${pathStr}' is not configured. Define it with entityMap() in your initial state.`);
|
|
109
64
|
}
|
|
110
65
|
function withEntities(config = {}) {
|
|
111
66
|
const {
|
|
@@ -115,9 +70,10 @@ function withEntities(config = {}) {
|
|
|
115
70
|
if (!enabled) {
|
|
116
71
|
return tree;
|
|
117
72
|
}
|
|
73
|
+
const registry = materializeEntities(tree);
|
|
118
74
|
const enhancedTree = Object.assign(tree, {
|
|
119
|
-
entities(
|
|
120
|
-
return
|
|
75
|
+
entities(path) {
|
|
76
|
+
return resolveEntitySignal(tree, registry, path);
|
|
121
77
|
}
|
|
122
78
|
});
|
|
123
79
|
return enhancedTree;
|
|
@@ -130,7 +86,8 @@ function enableEntities() {
|
|
|
130
86
|
}
|
|
131
87
|
function withHighPerformanceEntities() {
|
|
132
88
|
return withEntities({
|
|
133
|
-
enabled: true
|
|
89
|
+
enabled: true
|
|
90
|
+
});
|
|
134
91
|
}
|
|
135
92
|
|
|
136
93
|
export { enableEntities, withEntities, withHighPerformanceEntities };
|
|
@@ -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(),
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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 {
|
|
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';
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { signal, computed } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
class EntitySignalImpl {
|
|
4
|
+
pathNotifier;
|
|
5
|
+
basePath;
|
|
6
|
+
storage = new Map();
|
|
7
|
+
allSignal;
|
|
8
|
+
countSignal;
|
|
9
|
+
idsSignal;
|
|
10
|
+
mapSignal;
|
|
11
|
+
nodeCache = new Map();
|
|
12
|
+
selectId;
|
|
13
|
+
tapHandlers = [];
|
|
14
|
+
interceptHandlers = [];
|
|
15
|
+
constructor(config, pathNotifier, basePath) {
|
|
16
|
+
this.pathNotifier = pathNotifier;
|
|
17
|
+
this.basePath = basePath;
|
|
18
|
+
this.selectId = config.selectId ?? (entity => entity['id']);
|
|
19
|
+
this.allSignal = signal([]);
|
|
20
|
+
this.countSignal = signal(0);
|
|
21
|
+
this.idsSignal = signal([]);
|
|
22
|
+
this.mapSignal = signal(new Map());
|
|
23
|
+
return new Proxy(this, {
|
|
24
|
+
get: (target, prop) => {
|
|
25
|
+
if (typeof prop === 'string' && !isNaN(Number(prop))) {
|
|
26
|
+
return target.byId(Number(prop));
|
|
27
|
+
}
|
|
28
|
+
return target[prop];
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
byId(id) {
|
|
33
|
+
const entity = this.storage.get(id);
|
|
34
|
+
if (!entity) return undefined;
|
|
35
|
+
return this.getOrCreateNode(id, entity);
|
|
36
|
+
}
|
|
37
|
+
byIdOrFail(id) {
|
|
38
|
+
const node = this.byId(id);
|
|
39
|
+
if (!node) {
|
|
40
|
+
throw new Error(`Entity with id ${String(id)} not found`);
|
|
41
|
+
}
|
|
42
|
+
return node;
|
|
43
|
+
}
|
|
44
|
+
all() {
|
|
45
|
+
return this.allSignal;
|
|
46
|
+
}
|
|
47
|
+
count() {
|
|
48
|
+
return this.countSignal;
|
|
49
|
+
}
|
|
50
|
+
ids() {
|
|
51
|
+
return this.idsSignal;
|
|
52
|
+
}
|
|
53
|
+
map() {
|
|
54
|
+
return this.mapSignal;
|
|
55
|
+
}
|
|
56
|
+
has(id) {
|
|
57
|
+
return computed(() => this.storage.has(id));
|
|
58
|
+
}
|
|
59
|
+
isEmpty() {
|
|
60
|
+
return computed(() => this.storage.size === 0);
|
|
61
|
+
}
|
|
62
|
+
where(predicate) {
|
|
63
|
+
return computed(() => {
|
|
64
|
+
const result = [];
|
|
65
|
+
for (const entity of this.storage.values()) {
|
|
66
|
+
if (predicate(entity)) {
|
|
67
|
+
result.push(entity);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
find(predicate) {
|
|
74
|
+
return computed(() => {
|
|
75
|
+
for (const entity of this.storage.values()) {
|
|
76
|
+
if (predicate(entity)) {
|
|
77
|
+
return entity;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
addOne(entity, opts) {
|
|
84
|
+
const id = opts?.selectId?.(entity) ?? this.selectId(entity);
|
|
85
|
+
if (this.storage.has(id)) {
|
|
86
|
+
throw new Error(`Entity with id ${String(id)} already exists`);
|
|
87
|
+
}
|
|
88
|
+
let transformedEntity = entity;
|
|
89
|
+
for (const handler of this.interceptHandlers) {
|
|
90
|
+
const ctx = {
|
|
91
|
+
block: reason => {
|
|
92
|
+
throw new Error(`Cannot add entity: ${reason || 'blocked by interceptor'}`);
|
|
93
|
+
},
|
|
94
|
+
transform: value => {
|
|
95
|
+
transformedEntity = value;
|
|
96
|
+
},
|
|
97
|
+
blocked: false,
|
|
98
|
+
blockReason: undefined
|
|
99
|
+
};
|
|
100
|
+
handler.onAdd?.(entity, ctx);
|
|
101
|
+
}
|
|
102
|
+
this.storage.set(id, transformedEntity);
|
|
103
|
+
this.nodeCache.delete(id);
|
|
104
|
+
this.updateSignals();
|
|
105
|
+
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, transformedEntity, undefined);
|
|
106
|
+
for (const handler of this.tapHandlers) {
|
|
107
|
+
handler.onAdd?.(transformedEntity, id);
|
|
108
|
+
}
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
addMany(entities, opts) {
|
|
112
|
+
const ids = [];
|
|
113
|
+
for (const entity of entities) {
|
|
114
|
+
ids.push(this.addOne(entity, opts));
|
|
115
|
+
}
|
|
116
|
+
return ids;
|
|
117
|
+
}
|
|
118
|
+
updateOne(id, changes) {
|
|
119
|
+
const entity = this.storage.get(id);
|
|
120
|
+
if (!entity) {
|
|
121
|
+
throw new Error(`Entity with id ${String(id)} not found`);
|
|
122
|
+
}
|
|
123
|
+
const prev = entity;
|
|
124
|
+
let transformedChanges = changes;
|
|
125
|
+
for (const handler of this.interceptHandlers) {
|
|
126
|
+
const ctx = {
|
|
127
|
+
block: reason => {
|
|
128
|
+
throw new Error(`Cannot update entity: ${reason || 'blocked by interceptor'}`);
|
|
129
|
+
},
|
|
130
|
+
transform: value => {
|
|
131
|
+
transformedChanges = value;
|
|
132
|
+
},
|
|
133
|
+
blocked: false,
|
|
134
|
+
blockReason: undefined
|
|
135
|
+
};
|
|
136
|
+
handler.onUpdate?.(id, changes, ctx);
|
|
137
|
+
}
|
|
138
|
+
const finalUpdated = {
|
|
139
|
+
...entity,
|
|
140
|
+
...transformedChanges
|
|
141
|
+
};
|
|
142
|
+
this.storage.set(id, finalUpdated);
|
|
143
|
+
this.nodeCache.delete(id);
|
|
144
|
+
this.updateSignals();
|
|
145
|
+
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, finalUpdated, prev);
|
|
146
|
+
for (const handler of this.tapHandlers) {
|
|
147
|
+
handler.onUpdate?.(id, transformedChanges, finalUpdated);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
updateMany(ids, changes) {
|
|
151
|
+
for (const id of ids) {
|
|
152
|
+
this.updateOne(id, changes);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
updateWhere(predicate, changes) {
|
|
156
|
+
let count = 0;
|
|
157
|
+
for (const [id, entity] of this.storage) {
|
|
158
|
+
if (predicate(entity)) {
|
|
159
|
+
this.updateOne(id, changes);
|
|
160
|
+
count++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return count;
|
|
164
|
+
}
|
|
165
|
+
removeOne(id) {
|
|
166
|
+
const entity = this.storage.get(id);
|
|
167
|
+
if (!entity) {
|
|
168
|
+
throw new Error(`Entity with id ${String(id)} not found`);
|
|
169
|
+
}
|
|
170
|
+
for (const handler of this.interceptHandlers) {
|
|
171
|
+
const ctx = {
|
|
172
|
+
block: reason => {
|
|
173
|
+
throw new Error(`Cannot remove entity: ${reason || 'blocked by interceptor'}`);
|
|
174
|
+
},
|
|
175
|
+
transform: () => {},
|
|
176
|
+
blocked: false,
|
|
177
|
+
blockReason: undefined
|
|
178
|
+
};
|
|
179
|
+
handler.onRemove?.(id, entity, ctx);
|
|
180
|
+
}
|
|
181
|
+
this.storage.delete(id);
|
|
182
|
+
this.nodeCache.delete(id);
|
|
183
|
+
this.updateSignals();
|
|
184
|
+
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, undefined, entity);
|
|
185
|
+
for (const handler of this.tapHandlers) {
|
|
186
|
+
handler.onRemove?.(id, entity);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
removeMany(ids) {
|
|
190
|
+
for (const id of ids) {
|
|
191
|
+
this.removeOne(id);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
removeWhere(predicate) {
|
|
195
|
+
const idsToRemove = [];
|
|
196
|
+
for (const [id, entity] of this.storage) {
|
|
197
|
+
if (predicate(entity)) {
|
|
198
|
+
idsToRemove.push(id);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let count = 0;
|
|
202
|
+
for (const id of idsToRemove) {
|
|
203
|
+
this.removeOne(id);
|
|
204
|
+
count++;
|
|
205
|
+
}
|
|
206
|
+
return count;
|
|
207
|
+
}
|
|
208
|
+
upsertOne(entity, opts) {
|
|
209
|
+
const id = opts?.selectId?.(entity) ?? this.selectId(entity);
|
|
210
|
+
if (this.storage.has(id)) {
|
|
211
|
+
this.updateOne(id, entity);
|
|
212
|
+
} else {
|
|
213
|
+
this.addOne(entity, opts);
|
|
214
|
+
}
|
|
215
|
+
return id;
|
|
216
|
+
}
|
|
217
|
+
upsertMany(entities, opts) {
|
|
218
|
+
return entities.map(e => this.upsertOne(e, opts));
|
|
219
|
+
}
|
|
220
|
+
clear() {
|
|
221
|
+
this.storage.clear();
|
|
222
|
+
this.nodeCache.clear();
|
|
223
|
+
this.updateSignals();
|
|
224
|
+
}
|
|
225
|
+
removeAll() {
|
|
226
|
+
this.clear();
|
|
227
|
+
}
|
|
228
|
+
setAll(entities, opts) {
|
|
229
|
+
this.clear();
|
|
230
|
+
this.addMany(entities, opts);
|
|
231
|
+
}
|
|
232
|
+
tap(handlers) {
|
|
233
|
+
this.tapHandlers.push(handlers);
|
|
234
|
+
return () => {
|
|
235
|
+
const idx = this.tapHandlers.indexOf(handlers);
|
|
236
|
+
if (idx > -1) this.tapHandlers.splice(idx, 1);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
intercept(handlers) {
|
|
240
|
+
this.interceptHandlers.push(handlers);
|
|
241
|
+
return () => {
|
|
242
|
+
const idx = this.interceptHandlers.indexOf(handlers);
|
|
243
|
+
if (idx > -1) this.interceptHandlers.splice(idx, 1);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
updateSignals() {
|
|
247
|
+
const entities = Array.from(this.storage.values());
|
|
248
|
+
const ids = Array.from(this.storage.keys());
|
|
249
|
+
const map = new Map(this.storage);
|
|
250
|
+
this.allSignal.set(entities);
|
|
251
|
+
this.countSignal.set(entities.length);
|
|
252
|
+
this.idsSignal.set(ids);
|
|
253
|
+
this.mapSignal.set(map);
|
|
254
|
+
}
|
|
255
|
+
getOrCreateNode(id, entity) {
|
|
256
|
+
let node = this.nodeCache.get(id);
|
|
257
|
+
if (!node) {
|
|
258
|
+
node = this.createEntityNode(id, entity);
|
|
259
|
+
this.nodeCache.set(id, node);
|
|
260
|
+
}
|
|
261
|
+
return node;
|
|
262
|
+
}
|
|
263
|
+
createEntityNode(id, entity) {
|
|
264
|
+
const node = () => this.storage.get(id);
|
|
265
|
+
for (const key of Object.keys(entity)) {
|
|
266
|
+
Object.defineProperty(node, key, {
|
|
267
|
+
get: () => {
|
|
268
|
+
const current = this.storage.get(id);
|
|
269
|
+
const value = current?.[key];
|
|
270
|
+
return () => value;
|
|
271
|
+
},
|
|
272
|
+
enumerable: true,
|
|
273
|
+
configurable: true
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return node;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export { EntitySignalImpl };
|