@signaltree/core 5.1.2 → 5.1.3
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 +116 -55
- package/dist/enhancers/entities/lib/entities.js +1 -1
- package/dist/lib/entity-signal.js +5 -5
- package/dist/lib/signal-tree.js +1 -0
- package/dist/lib/utils.js +3 -0
- package/package.json +1 -1
- package/src/async-helpers.d.ts +8 -0
- package/src/batching.d.ts +16 -0
- package/src/computed.d.ts +12 -0
- package/src/constants.d.ts +14 -0
- package/src/devtools.d.ts +77 -0
- package/src/diff-engine.d.ts +33 -0
- package/src/entities.d.ts +20 -0
- package/src/entity-signal.d.ts +1 -0
- package/src/lib/types.d.ts +5 -5
- package/src/memoization.d.ts +65 -0
- package/src/memory-manager.d.ts +30 -0
- package/src/path-index.d.ts +25 -0
- package/src/path-notifier.d.ts +12 -0
- package/src/presets.d.ts +11 -0
- package/src/security-validator.d.ts +33 -0
- package/src/serialization.d.ts +59 -0
- package/src/signal-tree.d.ts +8 -0
- package/src/test-setup.d.ts +3 -0
- package/src/time-travel.d.ts +36 -0
- package/src/types.d.ts +278 -0
- package/src/update-engine.d.ts +32 -0
- package/src/utils.d.ts +1 -0
package/README.md
CHANGED
|
@@ -81,6 +81,73 @@ Performance and bundle size vary by app shape, build tooling, device, and runtim
|
|
|
81
81
|
- Use the **Benchmark Orchestrator** in the demo app to run calibrated, scenario-based benchmarks across supported libraries with **real-world frequency weighting**. It applies research-based multipliers derived from 40,000+ developer surveys and GitHub analysis, reports statistical summaries (median/p95/p99/stddev), alternates runs to reduce bias, and can export CSV/JSON. When available, memory usage is also reported.
|
|
82
82
|
- Use the bundle analysis scripts in `scripts/` to measure your min+gz sizes. Sizes are approximate and depend on tree-shaking and configuration.
|
|
83
83
|
|
|
84
|
+
## Best Practices (SignalTree-First)
|
|
85
|
+
|
|
86
|
+
> 📖 **Full guide**: [Implementation Patterns](https://github.com/JBorgia/signaltree/blob/main/docs/IMPLEMENTATION_PATTERNS.md)
|
|
87
|
+
|
|
88
|
+
Follow these principles for idiomatic SignalTree code:
|
|
89
|
+
|
|
90
|
+
### 1. Expose signals directly (no computed wrappers)
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const tree = signalTree(initialState).with(withEntities());
|
|
94
|
+
const $ = tree.$; // Shorthand for state access
|
|
95
|
+
|
|
96
|
+
// ✅ SignalTree-first: Direct signal exposure
|
|
97
|
+
return {
|
|
98
|
+
selectedUserId: $.selected.userId, // Direct from $ tree
|
|
99
|
+
loadingState: $.loading.state,
|
|
100
|
+
selectedUser, // Actual derived state (computed)
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ❌ Anti-pattern: Unnecessary computed wrappers
|
|
104
|
+
return {
|
|
105
|
+
selectedUserId: computed(() => $.selected.userId()), // Adds indirection
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 2. Use `ReturnType` inference (SignalTree-first)
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// Let SignalTree infer the type - no manual interface needed!
|
|
113
|
+
import type { createUserTree } from './user.tree';
|
|
114
|
+
export type UserTree = ReturnType<typeof createUserTree>;
|
|
115
|
+
|
|
116
|
+
// Factory function - no explicit return type needed
|
|
117
|
+
export function createUserTree() {
|
|
118
|
+
const tree = signalTree(initialState).with(withEntities());
|
|
119
|
+
return {
|
|
120
|
+
selectedUserId: tree.$.selected.userId, // Type inferred automatically
|
|
121
|
+
// ...
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 3. Use `computed()` only for derived state
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// ✅ Correct: Derived from multiple signals
|
|
130
|
+
const selectedUser = computed(() => {
|
|
131
|
+
const id = $.selected.userId();
|
|
132
|
+
return id ? $.users.byId(id)() : null;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ❌ Wrong: Wrapping an existing signal
|
|
136
|
+
const selectedUserId = computed(() => $.selected.userId()); // Unnecessary!
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 4. Use EntitySignal API directly
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// ✅ SignalTree-native
|
|
143
|
+
const user = $.users.byId(123)(); // O(1) lookup
|
|
144
|
+
const allUsers = $.users.all()(); // Get all
|
|
145
|
+
$.users.setAll(usersFromApi); // Replace all
|
|
146
|
+
|
|
147
|
+
// ❌ NgRx-style (avoid)
|
|
148
|
+
const user = entityMap()[123]; // Requires intermediate object
|
|
149
|
+
```
|
|
150
|
+
|
|
84
151
|
## Quick start
|
|
85
152
|
|
|
86
153
|
### Installation
|
|
@@ -552,22 +619,20 @@ const tree = signalTree({ count: 0 }).with(
|
|
|
552
619
|
import { signalTree, withBatching, withMemoization, withEntities } from '@signaltree/core';
|
|
553
620
|
|
|
554
621
|
const tree = signalTree({
|
|
555
|
-
products:
|
|
622
|
+
products: entityMap<Product>(),
|
|
556
623
|
ui: { loading: false },
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
withEntities() // Efficient CRUD operations
|
|
561
|
-
);
|
|
624
|
+
})
|
|
625
|
+
.with(withEntities()) // Efficient CRUD operations (auto-detects entityMap)
|
|
626
|
+
.with(withBatching()); // Batch updates for optimal rendering
|
|
562
627
|
|
|
563
|
-
//
|
|
564
|
-
tree.
|
|
565
|
-
|
|
566
|
-
ui: { loading: false },
|
|
567
|
-
}));
|
|
628
|
+
// Entity CRUD operations
|
|
629
|
+
tree.$.products.addOne(newProduct);
|
|
630
|
+
tree.$.products.setAll(productsFromApi);
|
|
568
631
|
|
|
569
|
-
|
|
570
|
-
|
|
632
|
+
// Entity queries
|
|
633
|
+
const electronics = tree.$.products
|
|
634
|
+
.all()()
|
|
635
|
+
.filter((p) => p.category === 'electronics');
|
|
571
636
|
```
|
|
572
637
|
|
|
573
638
|
**Full-Stack Application:**
|
|
@@ -644,17 +709,20 @@ const customTree = signalTree(state, TREE_PRESETS.DASHBOARD);
|
|
|
644
709
|
SignalTree Core includes all enhancer functionality built-in. No separate packages needed:
|
|
645
710
|
|
|
646
711
|
```typescript
|
|
647
|
-
import { signalTree, withEntities } from '@signaltree/core';
|
|
712
|
+
import { signalTree, entityMap, withEntities } from '@signaltree/core';
|
|
648
713
|
|
|
649
|
-
|
|
714
|
+
// Without entityMap - use manual array updates
|
|
715
|
+
const basic = signalTree({ users: [] as User[] });
|
|
716
|
+
basic.$.users.update((users) => [...users, newUser]);
|
|
650
717
|
|
|
651
|
-
//
|
|
652
|
-
|
|
718
|
+
// With entityMap + withEntities - use entity helpers
|
|
719
|
+
const enhanced = signalTree({
|
|
720
|
+
users: entityMap<User>(),
|
|
721
|
+
}).with(withEntities());
|
|
653
722
|
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
users.add(newUser); // ✅ Advanced CRUD operations
|
|
723
|
+
enhanced.$.users.addOne(newUser); // ✅ Advanced CRUD operations
|
|
724
|
+
enhanced.$.users.byId(123)(); // ✅ O(1) lookups
|
|
725
|
+
enhanced.$.users.all()(); // ✅ Get all as array
|
|
658
726
|
```
|
|
659
727
|
|
|
660
728
|
Core includes several performance optimizations:
|
|
@@ -843,58 +911,49 @@ const filteredProducts = computed(() => {
|
|
|
843
911
|
### Data Management Composition
|
|
844
912
|
|
|
845
913
|
```typescript
|
|
846
|
-
import { signalTree, withEntities } from '@signaltree/core';
|
|
914
|
+
import { signalTree, entityMap, withEntities } from '@signaltree/core';
|
|
847
915
|
|
|
848
916
|
// Add data management capabilities (+2.77KB total)
|
|
849
917
|
const tree = signalTree({
|
|
850
|
-
users:
|
|
851
|
-
posts:
|
|
852
|
-
ui: { loading: false, error: null },
|
|
853
|
-
}).with(
|
|
854
|
-
withEntities() // Advanced CRUD operations
|
|
855
|
-
);
|
|
918
|
+
users: entityMap<User>(),
|
|
919
|
+
posts: entityMap<Post>(),
|
|
920
|
+
ui: { loading: false, error: null as string | null },
|
|
921
|
+
}).with(withEntities());
|
|
856
922
|
|
|
857
|
-
// Advanced entity operations
|
|
858
|
-
|
|
859
|
-
users.
|
|
860
|
-
users.
|
|
861
|
-
users.updateMany([{ id: '1', changes: { status: 'active' } }]);
|
|
923
|
+
// Advanced entity operations via tree.$ accessor
|
|
924
|
+
tree.$.users.addOne(newUser);
|
|
925
|
+
tree.$.users.selectBy((u) => u.active);
|
|
926
|
+
tree.$.users.updateMany([{ id: '1', changes: { status: 'active' } }]);
|
|
862
927
|
|
|
863
|
-
// Entity helpers
|
|
928
|
+
// Entity helpers work with nested structures
|
|
864
929
|
// Example: deeply nested entities in a domain-driven design pattern
|
|
865
930
|
const appTree = signalTree({
|
|
866
931
|
app: {
|
|
867
932
|
data: {
|
|
868
|
-
users:
|
|
869
|
-
products:
|
|
933
|
+
users: entityMap<User>(),
|
|
934
|
+
products: entityMap<Product>(),
|
|
870
935
|
},
|
|
871
936
|
},
|
|
872
937
|
admin: {
|
|
873
938
|
data: {
|
|
874
|
-
logs:
|
|
875
|
-
reports:
|
|
939
|
+
logs: entityMap<AuditLog>(),
|
|
940
|
+
reports: entityMap<Report>(),
|
|
876
941
|
},
|
|
877
942
|
},
|
|
878
|
-
});
|
|
943
|
+
}).with(withEntities());
|
|
879
944
|
|
|
880
|
-
// Access
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
// All entity methods work seamlessly with nested paths
|
|
887
|
-
appUsers.selectBy((u) => u.isAdmin); // Filtered signal
|
|
888
|
-
appProducts.selectTotal(); // Count signal
|
|
889
|
-
adminLogs.selectAll(); // All items signal
|
|
890
|
-
adminReports.selectIds(); // ID array signal
|
|
945
|
+
// Access nested entities using tree.$ accessor
|
|
946
|
+
appTree.$.app.data.users.selectBy((u) => u.isAdmin); // Filtered signal
|
|
947
|
+
appTree.$.app.data.products.selectTotal(); // Count signal
|
|
948
|
+
appTree.$.admin.data.logs.all()(); // All items as array
|
|
949
|
+
appTree.$.admin.data.reports.selectIds(); // ID array signal
|
|
891
950
|
|
|
892
951
|
// For async operations, use manual async or async helpers
|
|
893
952
|
async function fetchUsers() {
|
|
894
953
|
tree.$.ui.loading.set(true);
|
|
895
954
|
try {
|
|
896
955
|
const users = await api.getUsers();
|
|
897
|
-
tree.$.users.
|
|
956
|
+
tree.$.users.setAll(users);
|
|
898
957
|
} catch (error) {
|
|
899
958
|
tree.$.ui.error.set(error.message);
|
|
900
959
|
} finally {
|
|
@@ -936,10 +995,10 @@ const tree = signalTree({
|
|
|
936
995
|
);
|
|
937
996
|
|
|
938
997
|
// Rich feature set available
|
|
939
|
-
const users = tree.entities<User>('app.data.users');
|
|
940
998
|
async function fetchUser(id: string) {
|
|
941
999
|
return await api.getUser(id);
|
|
942
1000
|
}
|
|
1001
|
+
tree.$.app.data.users.byId(userId)(); // O(1) lookup
|
|
943
1002
|
tree.undo(); // Time travel
|
|
944
1003
|
tree.save(); // Persistence
|
|
945
1004
|
```
|
|
@@ -1308,9 +1367,11 @@ tree.effect(fn); // Create reactive effects
|
|
|
1308
1367
|
tree.subscribe(fn); // Manual subscriptions
|
|
1309
1368
|
tree.destroy(); // Cleanup resources
|
|
1310
1369
|
|
|
1311
|
-
//
|
|
1312
|
-
tree.
|
|
1313
|
-
//
|
|
1370
|
+
// Entity helpers (when using entityMap + withEntities)
|
|
1371
|
+
// tree.$.users.addOne(user); // Add single entity
|
|
1372
|
+
// tree.$.users.byId(id)(); // O(1) lookup by ID
|
|
1373
|
+
// tree.$.users.all()(); // Get all as array
|
|
1374
|
+
// tree.$.users.selectBy(pred); // Filtered signal
|
|
1314
1375
|
```
|
|
1315
1376
|
|
|
1316
1377
|
## Extending with enhancers
|
|
@@ -6,7 +6,7 @@ function isEntityMapMarker(value) {
|
|
|
6
6
|
return Boolean(value && typeof value === 'object' && value['__isEntityMap'] === true);
|
|
7
7
|
}
|
|
8
8
|
function isEntitySignal(value) {
|
|
9
|
-
return !!value && typeof value === 'object' && typeof value['addOne'] === 'function' &&
|
|
9
|
+
return !!value && typeof value === 'object' && typeof value['addOne'] === 'function' && 'all' in value;
|
|
10
10
|
}
|
|
11
11
|
function materializeEntities(tree, notifier = getPathNotifier()) {
|
|
12
12
|
const registry = new Map();
|
|
@@ -41,22 +41,22 @@ class EntitySignalImpl {
|
|
|
41
41
|
}
|
|
42
42
|
return node;
|
|
43
43
|
}
|
|
44
|
-
all() {
|
|
44
|
+
get all() {
|
|
45
45
|
return this.allSignal;
|
|
46
46
|
}
|
|
47
|
-
count() {
|
|
47
|
+
get count() {
|
|
48
48
|
return this.countSignal;
|
|
49
49
|
}
|
|
50
|
-
ids() {
|
|
50
|
+
get ids() {
|
|
51
51
|
return this.idsSignal;
|
|
52
52
|
}
|
|
53
|
-
map() {
|
|
53
|
+
get map() {
|
|
54
54
|
return this.mapSignal;
|
|
55
55
|
}
|
|
56
56
|
has(id) {
|
|
57
57
|
return computed(() => this.storage.has(id));
|
|
58
58
|
}
|
|
59
|
-
isEmpty() {
|
|
59
|
+
get isEmpty() {
|
|
60
60
|
return computed(() => this.storage.size === 0);
|
|
61
61
|
}
|
|
62
62
|
where(predicate) {
|
package/dist/lib/signal-tree.js
CHANGED
|
@@ -459,6 +459,7 @@ function addStubMethods(tree, config) {
|
|
|
459
459
|
};
|
|
460
460
|
};
|
|
461
461
|
tree.entities = () => {
|
|
462
|
+
console.warn('[@signaltree/core] tree.entities() is deprecated and will be removed in v6.0. ' + 'Use entityMap<E>() + withEntities() + tree.$.collectionName instead. ' + 'See https://signaltree.dev/docs/migration for migration guide.');
|
|
462
463
|
if (config.debugMode) {
|
|
463
464
|
console.warn(SIGNAL_TREE_MESSAGES.ENTITY_HELPERS_NOT_AVAILABLE);
|
|
464
465
|
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -71,6 +71,9 @@ function createLazySignalTree(obj, equalityFn, basePath = '', memoryManager) {
|
|
|
71
71
|
if (!(key in target)) return undefined;
|
|
72
72
|
const value = target[key];
|
|
73
73
|
if (isSignal(value)) return value;
|
|
74
|
+
if (value && typeof value === 'object' && typeof value.addOne === 'function' && typeof value.all === 'function') {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
74
77
|
if (isEntityMapMarker(value)) return value;
|
|
75
78
|
if (memoryManager) {
|
|
76
79
|
const cached = memoryManager.getSignal(path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/core",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.3",
|
|
4
4
|
"description": "Lightweight, type-safe signal-based state management for Angular. Core package providing hierarchical signal trees, basic entity management, and async actions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SignalTree } from './types';
|
|
2
|
+
export declare function createAsyncOperation<T, TResult>(name: string, operation: () => Promise<TResult>): (tree: SignalTree<T>) => Promise<TResult>;
|
|
3
|
+
export declare function trackAsync<T>(operation: () => Promise<T>): {
|
|
4
|
+
pending: import("@angular/core").Signal<boolean>;
|
|
5
|
+
error: import("@angular/core").Signal<Error | null>;
|
|
6
|
+
result: import("@angular/core").Signal<T | null>;
|
|
7
|
+
execute: () => Promise<T>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SignalTree } from '../../../lib/types';
|
|
2
|
+
interface BatchingConfig {
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
maxBatchSize?: number;
|
|
5
|
+
autoFlushDelay?: number;
|
|
6
|
+
batchTimeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
interface BatchingSignalTree<T> extends SignalTree<T> {
|
|
9
|
+
batchUpdate(updater: (current: T) => Partial<T>): void;
|
|
10
|
+
}
|
|
11
|
+
export declare function withBatching<T>(config?: BatchingConfig): (tree: SignalTree<T>) => BatchingSignalTree<T>;
|
|
12
|
+
export declare function withHighPerformanceBatching<T>(): (tree: SignalTree<T>) => BatchingSignalTree<T>;
|
|
13
|
+
export declare function flushBatchedUpdates(): void;
|
|
14
|
+
export declare function hasPendingUpdates(): boolean;
|
|
15
|
+
export declare function getBatchQueueSize(): number;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Signal } from '@angular/core';
|
|
2
|
+
import type { TreeNode, SignalTree } from '../../../lib/types';
|
|
3
|
+
export interface ComputedConfig {
|
|
4
|
+
lazy?: boolean;
|
|
5
|
+
memoize?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export type ComputedSignal<T> = Signal<T>;
|
|
8
|
+
export interface ComputedSignalTree<T extends Record<string, unknown>> extends SignalTree<T> {
|
|
9
|
+
computed<U>(computeFn: (tree: TreeNode<T>) => U): ComputedSignal<U>;
|
|
10
|
+
}
|
|
11
|
+
export declare function computedEnhancer(_config?: ComputedConfig): import("../../../lib/types").EnhancerWithMeta<SignalTree<Record<string, unknown>>, ComputedSignalTree<Record<string, unknown>>>;
|
|
12
|
+
export declare function createComputed<T>(dependencies: readonly Signal<unknown>[], computeFn: () => T): ComputedSignal<T>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const TYPE_MARKERS: {
|
|
2
|
+
readonly DATE: "§d";
|
|
3
|
+
readonly REGEXP: "§r";
|
|
4
|
+
readonly MAP: "§m";
|
|
5
|
+
readonly SET: "§s";
|
|
6
|
+
readonly UNDEFINED: "§u";
|
|
7
|
+
readonly NAN: "§n";
|
|
8
|
+
readonly INFINITY: "§i";
|
|
9
|
+
readonly NEG_INFINITY: "§-i";
|
|
10
|
+
readonly BIGINT: "§b";
|
|
11
|
+
readonly SYMBOL: "§y";
|
|
12
|
+
readonly FUNCTION: "§f";
|
|
13
|
+
readonly CIRCULAR: "§c";
|
|
14
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Signal } from '@angular/core';
|
|
2
|
+
import type { SignalTree } from '../../../lib/types';
|
|
3
|
+
export interface ModuleMetadata {
|
|
4
|
+
name: string;
|
|
5
|
+
methods: string[];
|
|
6
|
+
addedAt: Date;
|
|
7
|
+
lastActivity: Date;
|
|
8
|
+
operationCount: number;
|
|
9
|
+
averageExecutionTime: number;
|
|
10
|
+
errorCount: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ModularPerformanceMetrics {
|
|
13
|
+
totalUpdates: number;
|
|
14
|
+
moduleUpdates: Record<string, number>;
|
|
15
|
+
modulePerformance: Record<string, number>;
|
|
16
|
+
compositionChain: string[];
|
|
17
|
+
signalGrowth: Record<string, number>;
|
|
18
|
+
memoryDelta: Record<string, number>;
|
|
19
|
+
moduleCacheStats: Record<string, {
|
|
20
|
+
hits: number;
|
|
21
|
+
misses: number;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface ModuleActivityTracker {
|
|
25
|
+
trackMethodCall: (module: string, method: string, duration: number) => void;
|
|
26
|
+
trackError: (module: string, error: Error, context?: string) => void;
|
|
27
|
+
getModuleActivity: (module: string) => ModuleMetadata | undefined;
|
|
28
|
+
getAllModules: () => ModuleMetadata[];
|
|
29
|
+
}
|
|
30
|
+
export interface CompositionLogger {
|
|
31
|
+
logComposition: (modules: string[], action: 'with' | 'enhance') => void;
|
|
32
|
+
logMethodExecution: (module: string, method: string, args: unknown[], result: unknown) => void;
|
|
33
|
+
logStateChange: (module: string, path: string, oldValue: unknown, newValue: unknown) => void;
|
|
34
|
+
logPerformanceWarning: (module: string, operation: string, duration: number, threshold: number) => void;
|
|
35
|
+
exportLogs: () => Array<{
|
|
36
|
+
timestamp: Date;
|
|
37
|
+
module: string;
|
|
38
|
+
type: 'composition' | 'method' | 'state' | 'performance';
|
|
39
|
+
data: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
export interface ModularDevToolsInterface<_T = unknown> {
|
|
43
|
+
activityTracker: ModuleActivityTracker;
|
|
44
|
+
logger: CompositionLogger;
|
|
45
|
+
metrics: Signal<ModularPerformanceMetrics>;
|
|
46
|
+
trackComposition: (modules: string[]) => void;
|
|
47
|
+
startModuleProfiling: (module: string) => string;
|
|
48
|
+
endModuleProfiling: (profileId: string) => void;
|
|
49
|
+
connectDevTools: (treeName: string) => void;
|
|
50
|
+
exportDebugSession: () => {
|
|
51
|
+
metrics: ModularPerformanceMetrics;
|
|
52
|
+
modules: ModuleMetadata[];
|
|
53
|
+
logs: Array<unknown>;
|
|
54
|
+
compositionHistory: Array<{
|
|
55
|
+
timestamp: Date;
|
|
56
|
+
chain: string[];
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export declare function withDevTools<T>(config?: {
|
|
61
|
+
enabled?: boolean;
|
|
62
|
+
treeName?: string;
|
|
63
|
+
enableBrowserDevTools?: boolean;
|
|
64
|
+
enableLogging?: boolean;
|
|
65
|
+
performanceThreshold?: number;
|
|
66
|
+
}): (tree: SignalTree<T>) => SignalTree<T> & {
|
|
67
|
+
__devTools: ModularDevToolsInterface<T>;
|
|
68
|
+
};
|
|
69
|
+
export declare function enableDevTools<T>(treeName?: string): (tree: SignalTree<T>) => SignalTree<T> & {
|
|
70
|
+
__devTools: ModularDevToolsInterface<T>;
|
|
71
|
+
};
|
|
72
|
+
export declare function withFullDevTools<T>(treeName?: string): (tree: SignalTree<T>) => SignalTree<T> & {
|
|
73
|
+
__devTools: ModularDevToolsInterface<T>;
|
|
74
|
+
};
|
|
75
|
+
export declare function withProductionDevTools<T>(): (tree: SignalTree<T>) => SignalTree<T> & {
|
|
76
|
+
__devTools: ModularDevToolsInterface<T>;
|
|
77
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Path } from './path-index';
|
|
2
|
+
export declare enum ChangeType {
|
|
3
|
+
ADD = "add",
|
|
4
|
+
UPDATE = "update",
|
|
5
|
+
DELETE = "delete",
|
|
6
|
+
REPLACE = "replace"
|
|
7
|
+
}
|
|
8
|
+
export interface Change {
|
|
9
|
+
type: ChangeType;
|
|
10
|
+
path: Path;
|
|
11
|
+
value?: unknown;
|
|
12
|
+
oldValue?: unknown;
|
|
13
|
+
}
|
|
14
|
+
export interface Diff {
|
|
15
|
+
changes: Change[];
|
|
16
|
+
hasChanges: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface DiffOptions {
|
|
19
|
+
maxDepth?: number;
|
|
20
|
+
detectDeletions?: boolean;
|
|
21
|
+
ignoreArrayOrder?: boolean;
|
|
22
|
+
equalityFn?: (a: unknown, b: unknown) => boolean;
|
|
23
|
+
keyValidator?: (key: string) => boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare class DiffEngine {
|
|
26
|
+
private defaultOptions;
|
|
27
|
+
diff(current: unknown, updates: unknown, options?: DiffOptions): Diff;
|
|
28
|
+
private traverse;
|
|
29
|
+
private diffArrays;
|
|
30
|
+
private diffArraysOrdered;
|
|
31
|
+
private diffArraysUnordered;
|
|
32
|
+
private stringify;
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { EntitySignal, SignalTree, EntityAwareTreeNode } from '../../../lib/types';
|
|
2
|
+
interface EntitiesEnhancerConfig {
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function withEntities(config?: EntitiesEnhancerConfig): <T>(tree: SignalTree<T>) => Omit<SignalTree<T>, "state" | "$"> & {
|
|
6
|
+
state: EntityAwareTreeNode<T>;
|
|
7
|
+
$: EntityAwareTreeNode<T>;
|
|
8
|
+
entities<E, K extends string | number>(path: keyof T | string): EntitySignal<E, K>;
|
|
9
|
+
};
|
|
10
|
+
export declare function enableEntities(): <T>(tree: SignalTree<T>) => Omit<SignalTree<T>, "state" | "$"> & {
|
|
11
|
+
state: EntityAwareTreeNode<T>;
|
|
12
|
+
$: EntityAwareTreeNode<T>;
|
|
13
|
+
entities<E, K extends string | number>(path: keyof T | string): EntitySignal<E, K>;
|
|
14
|
+
};
|
|
15
|
+
export declare function withHighPerformanceEntities(): <T>(tree: SignalTree<T>) => Omit<SignalTree<T>, "state" | "$"> & {
|
|
16
|
+
state: EntityAwareTreeNode<T>;
|
|
17
|
+
$: EntityAwareTreeNode<T>;
|
|
18
|
+
entities<E, K extends string | number>(path: keyof T | string): EntitySignal<E, K>;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/src/lib/types.d.ts
CHANGED
|
@@ -180,12 +180,12 @@ export type EntityNode<E> = {
|
|
|
180
180
|
export interface EntitySignal<E, K extends string | number = string> {
|
|
181
181
|
byId(id: K): EntityNode<E> | undefined;
|
|
182
182
|
byIdOrFail(id: K): EntityNode<E>;
|
|
183
|
-
all
|
|
184
|
-
count
|
|
185
|
-
ids
|
|
183
|
+
readonly all: Signal<E[]>;
|
|
184
|
+
readonly count: Signal<number>;
|
|
185
|
+
readonly ids: Signal<K[]>;
|
|
186
186
|
has(id: K): Signal<boolean>;
|
|
187
|
-
isEmpty
|
|
188
|
-
map
|
|
187
|
+
readonly isEmpty: Signal<boolean>;
|
|
188
|
+
readonly map: Signal<ReadonlyMap<K, E>>;
|
|
189
189
|
where(predicate: (entity: E) => boolean): Signal<E[]>;
|
|
190
190
|
find(predicate: (entity: E) => boolean): Signal<E | undefined>;
|
|
191
191
|
addOne(entity: E, opts?: AddOptions<E, K>): K;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SignalTree } from '../../../lib/types';
|
|
2
|
+
export interface MemoizedSignalTree<T> extends SignalTree<T> {
|
|
3
|
+
memoizedUpdate: (updater: (current: T) => Partial<T>, cacheKey?: string) => void;
|
|
4
|
+
clearMemoCache: (key?: string) => void;
|
|
5
|
+
getCacheStats: () => {
|
|
6
|
+
size: number;
|
|
7
|
+
hitRate: number;
|
|
8
|
+
totalHits: number;
|
|
9
|
+
totalMisses: number;
|
|
10
|
+
keys: string[];
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare function cleanupMemoizationCache(): void;
|
|
14
|
+
export interface MemoizationConfig {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
maxCacheSize?: number;
|
|
17
|
+
ttl?: number;
|
|
18
|
+
equality?: 'deep' | 'shallow' | 'reference';
|
|
19
|
+
enableLRU?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function memoize<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn, keyFn?: (...args: TArgs) => string, config?: MemoizationConfig): (...args: TArgs) => TReturn;
|
|
22
|
+
export declare function memoizeShallow<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn, keyFn?: (...args: TArgs) => string): (...args: TArgs) => TReturn;
|
|
23
|
+
export declare function memoizeReference<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn, keyFn?: (...args: TArgs) => string): (...args: TArgs) => TReturn;
|
|
24
|
+
export declare const MEMOIZATION_PRESETS: {
|
|
25
|
+
readonly selector: {
|
|
26
|
+
readonly equality: "reference";
|
|
27
|
+
readonly maxCacheSize: 10;
|
|
28
|
+
readonly enableLRU: false;
|
|
29
|
+
readonly ttl: undefined;
|
|
30
|
+
};
|
|
31
|
+
readonly computed: {
|
|
32
|
+
readonly equality: "shallow";
|
|
33
|
+
readonly maxCacheSize: 100;
|
|
34
|
+
readonly enableLRU: false;
|
|
35
|
+
readonly ttl: undefined;
|
|
36
|
+
};
|
|
37
|
+
readonly deepState: {
|
|
38
|
+
readonly equality: "deep";
|
|
39
|
+
readonly maxCacheSize: 1000;
|
|
40
|
+
readonly enableLRU: true;
|
|
41
|
+
readonly ttl: number;
|
|
42
|
+
};
|
|
43
|
+
readonly highFrequency: {
|
|
44
|
+
readonly equality: "reference";
|
|
45
|
+
readonly maxCacheSize: 5;
|
|
46
|
+
readonly enableLRU: false;
|
|
47
|
+
readonly ttl: undefined;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export declare function withSelectorMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
51
|
+
export declare function withComputedMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
52
|
+
export declare function withDeepStateMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
53
|
+
export declare function withHighFrequencyMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
54
|
+
export declare function withMemoization<T>(config?: MemoizationConfig): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
55
|
+
export declare function enableMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
56
|
+
export declare function withHighPerformanceMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
57
|
+
export declare function withLightweightMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
58
|
+
export declare function withShallowMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
59
|
+
export declare function clearAllCaches(): void;
|
|
60
|
+
export declare function getGlobalCacheStats(): {
|
|
61
|
+
treeCount: number;
|
|
62
|
+
totalSize: number;
|
|
63
|
+
totalHits: number;
|
|
64
|
+
averageCacheSize: number;
|
|
65
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { WritableSignal } from '@angular/core';
|
|
2
|
+
export interface MemoryStats {
|
|
3
|
+
cachedSignals: number;
|
|
4
|
+
cleanedUpSignals: number;
|
|
5
|
+
peakCachedSignals: number;
|
|
6
|
+
manualDisposes: number;
|
|
7
|
+
estimatedMemoryBytes: number;
|
|
8
|
+
}
|
|
9
|
+
export interface MemoryManagerConfig {
|
|
10
|
+
enableAutoCleanup?: boolean;
|
|
11
|
+
debugMode?: boolean;
|
|
12
|
+
onCleanup?: (path: string, stats: MemoryStats) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare class SignalMemoryManager {
|
|
15
|
+
private cache;
|
|
16
|
+
private registry;
|
|
17
|
+
private config;
|
|
18
|
+
private stats;
|
|
19
|
+
constructor(config?: MemoryManagerConfig);
|
|
20
|
+
cacheSignal<T>(path: string, signal: WritableSignal<T>): void;
|
|
21
|
+
getSignal(path: string): WritableSignal<unknown> | undefined;
|
|
22
|
+
hasSignal(path: string): boolean;
|
|
23
|
+
removeSignal(path: string): boolean;
|
|
24
|
+
private handleCleanup;
|
|
25
|
+
getStats(): MemoryStats;
|
|
26
|
+
dispose(): void;
|
|
27
|
+
getCachedPaths(): string[];
|
|
28
|
+
clearStale(): number;
|
|
29
|
+
resetStats(): void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { WritableSignal } from '@angular/core';
|
|
2
|
+
export type PathSegment = string | number;
|
|
3
|
+
export type Path = PathSegment[];
|
|
4
|
+
export declare class PathIndex<T extends object = WritableSignal<any>> {
|
|
5
|
+
private root;
|
|
6
|
+
private pathCache;
|
|
7
|
+
private stats;
|
|
8
|
+
set(path: Path, signal: T): void;
|
|
9
|
+
get(path: Path): T | null;
|
|
10
|
+
has(path: Path): boolean;
|
|
11
|
+
getByPrefix(prefix: Path): Map<string, T>;
|
|
12
|
+
delete(path: Path): boolean;
|
|
13
|
+
clear(): void;
|
|
14
|
+
getStats(): {
|
|
15
|
+
hits: number;
|
|
16
|
+
misses: number;
|
|
17
|
+
sets: number;
|
|
18
|
+
cleanups: number;
|
|
19
|
+
hitRate: number;
|
|
20
|
+
cacheSize: number;
|
|
21
|
+
};
|
|
22
|
+
buildFromTree(tree: unknown, path?: Path): void;
|
|
23
|
+
private pathToString;
|
|
24
|
+
private collectDescendants;
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type PathNotifierInterceptor = (value: unknown, prev: unknown, path: string) => {
|
|
2
|
+
block?: boolean;
|
|
3
|
+
transform?: unknown;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export interface PathNotifier {
|
|
7
|
+
subscribe(pattern: string, handler: (value: unknown, prev: unknown, path: string) => void): () => void;
|
|
8
|
+
intercept(pattern: string, fn: PathNotifierInterceptor): () => void;
|
|
9
|
+
notify(path: string, value: unknown, prev: unknown): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getPathNotifier(): PathNotifier;
|
package/src/presets.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TreeConfig } from '../../../lib/types';
|
|
2
|
+
export type TreePreset = 'basic' | 'performance' | 'development' | 'production';
|
|
3
|
+
export declare const TREE_PRESETS: Record<TreePreset, Partial<TreeConfig>>;
|
|
4
|
+
export declare function createPresetConfig(preset: TreePreset, overrides?: Partial<TreeConfig>): TreeConfig;
|
|
5
|
+
export declare function validatePreset(preset: TreePreset): boolean;
|
|
6
|
+
export declare function getAvailablePresets(): TreePreset[];
|
|
7
|
+
export declare function combinePresets(presets: TreePreset[], overrides?: Partial<TreeConfig>): TreeConfig;
|
|
8
|
+
export declare function createDevTree(overrides?: Partial<TreeConfig>): {
|
|
9
|
+
readonly config: TreeConfig;
|
|
10
|
+
readonly enhancer: (tree: import("../../memoization/lib/memoization").MemoizedSignalTree<unknown>) => import("../../memoization/lib/memoization").MemoizedSignalTree<unknown>;
|
|
11
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type SecurityEventType = 'dangerous-key-blocked' | 'xss-attempt-blocked' | 'function-value-blocked' | 'validation-error';
|
|
2
|
+
export interface SecurityEvent {
|
|
3
|
+
type: SecurityEventType;
|
|
4
|
+
key?: string;
|
|
5
|
+
value?: unknown;
|
|
6
|
+
reason: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
}
|
|
9
|
+
export interface SecurityValidatorConfig {
|
|
10
|
+
preventPrototypePollution?: boolean;
|
|
11
|
+
preventXSS?: boolean;
|
|
12
|
+
preventFunctions?: boolean;
|
|
13
|
+
customDangerousKeys?: string[];
|
|
14
|
+
onSecurityEvent?: (event: SecurityEvent) => void;
|
|
15
|
+
sanitizationMode?: 'strict' | 'permissive';
|
|
16
|
+
}
|
|
17
|
+
export declare class SecurityValidator {
|
|
18
|
+
private readonly config;
|
|
19
|
+
private readonly dangerousKeys;
|
|
20
|
+
constructor(config?: SecurityValidatorConfig);
|
|
21
|
+
validateKey(key: string): void;
|
|
22
|
+
validateValue<T>(value: T): T;
|
|
23
|
+
private sanitize;
|
|
24
|
+
validateKeyValue<T>(key: string, value: T): T;
|
|
25
|
+
isDangerousKey(key: string): boolean;
|
|
26
|
+
getConfig(): Readonly<Required<SecurityValidatorConfig>>;
|
|
27
|
+
}
|
|
28
|
+
export declare const SecurityPresets: {
|
|
29
|
+
strict: () => SecurityValidator;
|
|
30
|
+
standard: () => SecurityValidator;
|
|
31
|
+
permissive: () => SecurityValidator;
|
|
32
|
+
disabled: () => SecurityValidator;
|
|
33
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { SignalTree } from '../../../lib/types';
|
|
2
|
+
import type { EnhancerWithMeta } from '../../../lib/types';
|
|
3
|
+
export interface SerializationConfig {
|
|
4
|
+
includeMetadata?: boolean;
|
|
5
|
+
replacer?: (key: string, value: unknown) => unknown;
|
|
6
|
+
reviver?: (key: string, value: unknown) => unknown;
|
|
7
|
+
preserveTypes?: boolean;
|
|
8
|
+
maxDepth?: number;
|
|
9
|
+
handleCircular?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SerializedState<T = unknown> {
|
|
12
|
+
data: T;
|
|
13
|
+
metadata?: {
|
|
14
|
+
timestamp: number;
|
|
15
|
+
version: string;
|
|
16
|
+
appVersion?: string;
|
|
17
|
+
types?: Record<string, string>;
|
|
18
|
+
circularRefs?: Array<{
|
|
19
|
+
path: string;
|
|
20
|
+
targetPath: string;
|
|
21
|
+
}>;
|
|
22
|
+
nodeMap?: Record<string, 'b' | 'r'>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface SerializableSignalTree<T> extends SignalTree<T> {
|
|
26
|
+
$: any;
|
|
27
|
+
serialize(config?: SerializationConfig): string;
|
|
28
|
+
deserialize(json: string, config?: SerializationConfig): void;
|
|
29
|
+
toJSON(): T;
|
|
30
|
+
fromJSON(data: T, metadata?: SerializedState<T>['metadata']): void;
|
|
31
|
+
snapshot(): SerializedState<T>;
|
|
32
|
+
restore(snapshot: SerializedState<T>): void;
|
|
33
|
+
}
|
|
34
|
+
export interface PersistenceMethods {
|
|
35
|
+
save(): Promise<void>;
|
|
36
|
+
load(): Promise<void>;
|
|
37
|
+
clear(): Promise<void>;
|
|
38
|
+
__flushAutoSave?: () => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export declare function withSerialization<T extends Record<string, unknown> = Record<string, unknown>>(defaultConfig?: SerializationConfig): EnhancerWithMeta<SignalTree<T>, SerializableSignalTree<T>>;
|
|
41
|
+
export declare function enableSerialization<T extends Record<string, unknown> = Record<string, unknown>>(): EnhancerWithMeta<SignalTree<T>, SerializableSignalTree<T>>;
|
|
42
|
+
export interface StorageAdapter {
|
|
43
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
44
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
45
|
+
removeItem(key: string): void | Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export interface PersistenceConfig extends SerializationConfig {
|
|
48
|
+
key: string;
|
|
49
|
+
storage?: StorageAdapter;
|
|
50
|
+
autoSave?: boolean;
|
|
51
|
+
debounceMs?: number;
|
|
52
|
+
autoLoad?: boolean;
|
|
53
|
+
skipCache?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export declare function withPersistence<T extends Record<string, unknown> = Record<string, unknown>>(config: PersistenceConfig): EnhancerWithMeta<SignalTree<T>, SerializableSignalTree<T> & PersistenceMethods>;
|
|
56
|
+
export declare function createStorageAdapter(getItem: (key: string) => string | null | Promise<string | null>, setItem: (key: string, value: string) => void | Promise<void>, removeItem: (key: string) => void | Promise<void>): StorageAdapter;
|
|
57
|
+
export declare function createIndexedDBAdapter(dbName?: string, storeName?: string): StorageAdapter;
|
|
58
|
+
export declare function applySerialization<T extends Record<string, unknown>>(tree: SignalTree<T>): SerializableSignalTree<T>;
|
|
59
|
+
export declare function applyPersistence<T extends Record<string, unknown>>(tree: SignalTree<T>, cfg: PersistenceConfig): SerializableSignalTree<T> & PersistenceMethods;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SignalTree, TreeConfig, TreePreset, EnhancerWithMeta, NodeAccessor } from './types';
|
|
2
|
+
export declare function isNodeAccessor(value: unknown): value is NodeAccessor<unknown>;
|
|
3
|
+
export declare function signalTree<T>(obj: T): SignalTree<T>;
|
|
4
|
+
export declare function signalTree<T>(obj: T, preset: TreePreset): SignalTree<T>;
|
|
5
|
+
export declare function signalTree<T>(obj: T, config: TreeConfig): SignalTree<T>;
|
|
6
|
+
export declare function signalTree<T extends Record<string, unknown>>(obj: Required<T>, configOrPreset?: TreeConfig | TreePreset): SignalTree<Required<T>>;
|
|
7
|
+
export declare function signalTree<T>(obj: T, configOrPreset?: TreeConfig | TreePreset): SignalTree<T>;
|
|
8
|
+
export declare function applyEnhancer<T, O>(tree: SignalTree<T>, enhancer: EnhancerWithMeta<SignalTree<T>, O>): O;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SignalTree } from '../../../lib/types';
|
|
2
|
+
export interface TimeTravelEntry<T> {
|
|
3
|
+
state: T;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
action: string;
|
|
6
|
+
payload?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface TimeTravelInterface<T> {
|
|
9
|
+
undo(): boolean;
|
|
10
|
+
redo(): boolean;
|
|
11
|
+
getHistory(): TimeTravelEntry<T>[];
|
|
12
|
+
resetHistory(): void;
|
|
13
|
+
jumpTo(index: number): boolean;
|
|
14
|
+
getCurrentIndex(): number;
|
|
15
|
+
canUndo(): boolean;
|
|
16
|
+
canRedo(): boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface TimeTravelConfig {
|
|
19
|
+
maxHistorySize?: number;
|
|
20
|
+
includePayload?: boolean;
|
|
21
|
+
actionNames?: {
|
|
22
|
+
update?: string;
|
|
23
|
+
set?: string;
|
|
24
|
+
batch?: string;
|
|
25
|
+
[key: string]: string | undefined;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function withTimeTravel<T>(config?: TimeTravelConfig): (tree: SignalTree<T>) => SignalTree<T> & {
|
|
29
|
+
__timeTravel: TimeTravelInterface<T>;
|
|
30
|
+
};
|
|
31
|
+
export declare function enableTimeTravel<T>(maxHistorySize?: number): (tree: SignalTree<T>) => SignalTree<T> & {
|
|
32
|
+
__timeTravel: TimeTravelInterface<T>;
|
|
33
|
+
};
|
|
34
|
+
export declare function getTimeTravel<T>(tree: SignalTree<T> & {
|
|
35
|
+
__timeTravel?: TimeTravelInterface<T>;
|
|
36
|
+
}): TimeTravelInterface<T> | undefined;
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { Signal, WritableSignal } from '@angular/core';
|
|
2
|
+
import type { SecurityValidatorConfig } from './security/security-validator';
|
|
3
|
+
export type NotFn<T> = T extends (...args: unknown[]) => unknown ? never : T;
|
|
4
|
+
declare module '@angular/core' {
|
|
5
|
+
interface WritableSignal<T> {
|
|
6
|
+
(value: NotFn<T>): void;
|
|
7
|
+
(updater: (current: T) => T): void;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export type Primitive = string | number | boolean | null | undefined | bigint | symbol;
|
|
11
|
+
export type BuiltInObject = Date | RegExp | ((...args: unknown[]) => unknown) | Map<unknown, unknown> | Set<unknown> | WeakMap<object, unknown> | WeakSet<object> | ArrayBuffer | DataView | Error | Promise<unknown> | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array | URL | URLSearchParams | FormData | Blob | File | Headers | Request | Response | AbortController | AbortSignal;
|
|
12
|
+
export type Unwrap<T> = [T] extends [WritableSignal<infer U>] ? U : [T] extends [Signal<infer U>] ? U : [T] extends [BuiltInObject] ? T : [T] extends [readonly unknown[]] ? T : [T] extends [EntityMapMarker<infer E, infer K>] ? EntitySignal<E, K> : [T] extends [object] ? {
|
|
13
|
+
[K in keyof T]: Unwrap<T[K]>;
|
|
14
|
+
} : T;
|
|
15
|
+
export interface NodeAccessor<T> {
|
|
16
|
+
(): T;
|
|
17
|
+
(value: T): void;
|
|
18
|
+
(updater: (current: T) => T): void;
|
|
19
|
+
}
|
|
20
|
+
export type AccessibleNode<T> = NodeAccessor<T> & TreeNode<T>;
|
|
21
|
+
export type CallableWritableSignal<T> = WritableSignal<T> & {
|
|
22
|
+
(value: NotFn<T>): void;
|
|
23
|
+
(updater: (current: T) => T): void;
|
|
24
|
+
};
|
|
25
|
+
export type TreeNode<T> = {
|
|
26
|
+
[K in keyof T]: [T[K]] extends [EntityMapMarker<infer E, infer Key>] ? EntitySignal<E, Key> : [T[K]] extends [readonly unknown[]] ? CallableWritableSignal<T[K]> : [T[K]] extends [object] ? [T[K]] extends [Signal<unknown>] ? T[K] : [T[K]] extends [BuiltInObject] ? CallableWritableSignal<T[K]> : [T[K]] extends [(...args: unknown[]) => unknown] ? CallableWritableSignal<T[K]> : AccessibleNode<T[K]> : CallableWritableSignal<T[K]>;
|
|
27
|
+
};
|
|
28
|
+
export type RemoveSignalMethods<T> = T extends infer U ? U : never;
|
|
29
|
+
export type DeepPath<T, Prefix extends string = '', Depth extends readonly number[] = []> = Depth['length'] extends 5 ? never : {
|
|
30
|
+
[K in keyof T]: K extends string ? T[K] extends readonly unknown[] ? `${Prefix}${K}` : T[K] extends object ? T[K] extends Signal<unknown> ? never : T[K] extends BuiltInObject ? never : T[K] extends (...args: unknown[]) => unknown ? never : `${Prefix}${K}` | DeepPath<T[K], `${Prefix}${K}.`, [...Depth, 1]> : never : never;
|
|
31
|
+
}[keyof T];
|
|
32
|
+
export type DeepAccess<T, Path extends string> = Path extends `${infer First}.${infer Rest}` ? First extends keyof T ? DeepAccess<T[First] & object, Rest> : never : Path extends keyof T ? T[Path] : never;
|
|
33
|
+
export interface EnhancerMeta {
|
|
34
|
+
name?: string;
|
|
35
|
+
requires?: string[];
|
|
36
|
+
provides?: string[];
|
|
37
|
+
}
|
|
38
|
+
export type Enhancer<Input = unknown, Output = unknown> = (input: Input) => Output;
|
|
39
|
+
export type EnhancerWithMeta<Input = unknown, Output = unknown> = Enhancer<Input, Output> & {
|
|
40
|
+
metadata?: EnhancerMeta;
|
|
41
|
+
};
|
|
42
|
+
export declare const ENHANCER_META: unique symbol;
|
|
43
|
+
export type ChainResult<Start, E extends Array<EnhancerWithMeta<unknown, unknown>>> = E extends [infer H, ...infer R] ? H extends EnhancerWithMeta<SignalTree<unknown>, infer O> ? R extends Array<EnhancerWithMeta<unknown, unknown>> ? ChainResult<O, R> : O : H extends EnhancerWithMeta<infer I, infer O> ? Start extends I ? R extends Array<EnhancerWithMeta<unknown, unknown>> ? ChainResult<O, R> : O : unknown : unknown : Start;
|
|
44
|
+
export interface WithMethod<T> {
|
|
45
|
+
(): SignalTree<T>;
|
|
46
|
+
<O>(enhancer: (input: SignalTree<T>) => O): O;
|
|
47
|
+
<O1, O2>(e1: (input: SignalTree<T>) => O1, e2: (input: O1) => O2): O2;
|
|
48
|
+
<O1, O2, O3>(e1: (input: SignalTree<T>) => O1, e2: (input: O1) => O2, e3: (input: O2) => O3): O3;
|
|
49
|
+
<O>(enhancer: EnhancerWithMeta<SignalTree<T>, O>): O;
|
|
50
|
+
<O1, O2>(e1: EnhancerWithMeta<SignalTree<T>, O1>, e2: EnhancerWithMeta<O1, O2>): O2;
|
|
51
|
+
<O1, O2, O3>(e1: EnhancerWithMeta<SignalTree<T>, O1>, e2: EnhancerWithMeta<O1, O2>, e3: EnhancerWithMeta<O2, O3>): O3;
|
|
52
|
+
}
|
|
53
|
+
export type SignalTree<T> = NodeAccessor<T> & {
|
|
54
|
+
state: TreeNode<T>;
|
|
55
|
+
$: TreeNode<T>;
|
|
56
|
+
with: WithMethod<T>;
|
|
57
|
+
destroy(): void;
|
|
58
|
+
dispose?(): void;
|
|
59
|
+
effect(fn: (tree: T) => void): void;
|
|
60
|
+
subscribe(fn: (tree: T) => void): () => void;
|
|
61
|
+
batch(updater: (tree: T) => void): void;
|
|
62
|
+
batchUpdate(updater: (current: T) => Partial<T>): void;
|
|
63
|
+
memoize<R>(fn: (tree: T) => R, cacheKey?: string): Signal<R>;
|
|
64
|
+
memoizedUpdate(updater: (current: T) => Partial<T>, cacheKey?: string): void;
|
|
65
|
+
clearMemoCache(key?: string): void;
|
|
66
|
+
getCacheStats(): {
|
|
67
|
+
size: number;
|
|
68
|
+
hitRate: number;
|
|
69
|
+
totalHits: number;
|
|
70
|
+
totalMisses: number;
|
|
71
|
+
keys: string[];
|
|
72
|
+
};
|
|
73
|
+
optimize(): void;
|
|
74
|
+
clearCache(): void;
|
|
75
|
+
invalidatePattern(pattern: string): number;
|
|
76
|
+
updateOptimized?(updates: Partial<T>, options?: {
|
|
77
|
+
batch?: boolean;
|
|
78
|
+
batchSize?: number;
|
|
79
|
+
maxDepth?: number;
|
|
80
|
+
ignoreArrayOrder?: boolean;
|
|
81
|
+
equalityFn?: (a: unknown, b: unknown) => boolean;
|
|
82
|
+
}): {
|
|
83
|
+
changed: boolean;
|
|
84
|
+
duration: number;
|
|
85
|
+
changedPaths: string[];
|
|
86
|
+
stats?: {
|
|
87
|
+
totalPaths: number;
|
|
88
|
+
optimizedPaths: number;
|
|
89
|
+
batchedUpdates: number;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
getMetrics(): PerformanceMetrics;
|
|
93
|
+
entities<E extends {
|
|
94
|
+
id: string | number;
|
|
95
|
+
}>(entityKey?: keyof T): EntityHelpers<E>;
|
|
96
|
+
undo(): void;
|
|
97
|
+
redo(): void;
|
|
98
|
+
getHistory(): TimeTravelEntry<T>[];
|
|
99
|
+
resetHistory(): void;
|
|
100
|
+
jumpTo?: (index: number) => void;
|
|
101
|
+
canUndo?: () => boolean;
|
|
102
|
+
canRedo?: () => boolean;
|
|
103
|
+
getCurrentIndex?: () => number;
|
|
104
|
+
};
|
|
105
|
+
export type TreePreset = 'basic' | 'performance' | 'development' | 'production';
|
|
106
|
+
export interface TreeConfig {
|
|
107
|
+
batchUpdates?: boolean;
|
|
108
|
+
useMemoization?: boolean;
|
|
109
|
+
enableTimeTravel?: boolean;
|
|
110
|
+
useLazySignals?: boolean;
|
|
111
|
+
useShallowComparison?: boolean;
|
|
112
|
+
maxCacheSize?: number;
|
|
113
|
+
trackPerformance?: boolean;
|
|
114
|
+
treeName?: string;
|
|
115
|
+
enableDevTools?: boolean;
|
|
116
|
+
debugMode?: boolean;
|
|
117
|
+
useStructuralSharing?: boolean;
|
|
118
|
+
security?: SecurityValidatorConfig;
|
|
119
|
+
}
|
|
120
|
+
export interface PerformanceMetrics {
|
|
121
|
+
updates: number;
|
|
122
|
+
computations: number;
|
|
123
|
+
cacheHits: number;
|
|
124
|
+
cacheMisses: number;
|
|
125
|
+
averageUpdateTime: number;
|
|
126
|
+
}
|
|
127
|
+
export interface EntityConfig<E, K extends string | number = string> {
|
|
128
|
+
selectId?: (entity: E) => K;
|
|
129
|
+
hooks?: {
|
|
130
|
+
beforeAdd?: (entity: E) => E | false;
|
|
131
|
+
beforeUpdate?: (id: K, changes: Partial<E>) => Partial<E> | false;
|
|
132
|
+
beforeRemove?: (id: K, entity: E) => boolean;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
declare const ENTITY_MAP_BRAND: unique symbol;
|
|
136
|
+
export interface EntityMapMarker<E, K extends string | number> {
|
|
137
|
+
readonly [ENTITY_MAP_BRAND]: {
|
|
138
|
+
__entity: E;
|
|
139
|
+
__key: K;
|
|
140
|
+
};
|
|
141
|
+
readonly __isEntityMap: true;
|
|
142
|
+
readonly __entityMapConfig?: EntityConfig<E, K>;
|
|
143
|
+
}
|
|
144
|
+
export declare function entityMap<E, K extends string | number = E extends {
|
|
145
|
+
id: infer I extends string | number;
|
|
146
|
+
} ? I : string>(config?: EntityConfig<E, K>): EntityMapMarker<E, K>;
|
|
147
|
+
export interface MutationOptions {
|
|
148
|
+
onError?: (error: Error) => void;
|
|
149
|
+
}
|
|
150
|
+
export interface AddOptions<E, K> extends MutationOptions {
|
|
151
|
+
selectId?: (entity: E) => K;
|
|
152
|
+
}
|
|
153
|
+
export interface AddManyOptions<E, K> extends AddOptions<E, K> {
|
|
154
|
+
mode?: 'strict' | 'skip' | 'overwrite';
|
|
155
|
+
}
|
|
156
|
+
export interface TapHandlers<E, K extends string | number> {
|
|
157
|
+
onAdd?: (entity: E, id: K) => void;
|
|
158
|
+
onUpdate?: (id: K, changes: Partial<E>, entity: E) => void;
|
|
159
|
+
onRemove?: (id: K, entity: E) => void;
|
|
160
|
+
onChange?: () => void;
|
|
161
|
+
}
|
|
162
|
+
export interface InterceptContext<T> {
|
|
163
|
+
block(reason?: string): void;
|
|
164
|
+
transform(value: T): void;
|
|
165
|
+
readonly blocked: boolean;
|
|
166
|
+
readonly blockReason: string | undefined;
|
|
167
|
+
}
|
|
168
|
+
export interface InterceptHandlers<E, K extends string | number> {
|
|
169
|
+
onAdd?: (entity: E, ctx: InterceptContext<E>) => void | Promise<void>;
|
|
170
|
+
onUpdate?: (id: K, changes: Partial<E>, ctx: InterceptContext<Partial<E>>) => void | Promise<void>;
|
|
171
|
+
onRemove?: (id: K, entity: E, ctx: InterceptContext<void>) => void | Promise<void>;
|
|
172
|
+
}
|
|
173
|
+
export type EntityNode<E> = {
|
|
174
|
+
(): E;
|
|
175
|
+
(value: E): void;
|
|
176
|
+
(updater: (current: E) => E): void;
|
|
177
|
+
} & {
|
|
178
|
+
[P in keyof E]: E[P] extends object ? E[P] extends readonly unknown[] ? CallableWritableSignal<E[P]> : EntityNode<E[P]> : CallableWritableSignal<E[P]>;
|
|
179
|
+
};
|
|
180
|
+
export interface EntitySignal<E, K extends string | number = string> {
|
|
181
|
+
byId(id: K): EntityNode<E> | undefined;
|
|
182
|
+
byIdOrFail(id: K): EntityNode<E>;
|
|
183
|
+
all(): Signal<E[]>;
|
|
184
|
+
count(): Signal<number>;
|
|
185
|
+
ids(): Signal<K[]>;
|
|
186
|
+
has(id: K): Signal<boolean>;
|
|
187
|
+
isEmpty(): Signal<boolean>;
|
|
188
|
+
map(): Signal<ReadonlyMap<K, E>>;
|
|
189
|
+
where(predicate: (entity: E) => boolean): Signal<E[]>;
|
|
190
|
+
find(predicate: (entity: E) => boolean): Signal<E | undefined>;
|
|
191
|
+
addOne(entity: E, opts?: AddOptions<E, K>): K;
|
|
192
|
+
addMany(entities: E[], opts?: AddManyOptions<E, K>): K[];
|
|
193
|
+
updateOne(id: K, changes: Partial<E>, opts?: MutationOptions): void;
|
|
194
|
+
updateMany(ids: K[], changes: Partial<E>, opts?: MutationOptions): void;
|
|
195
|
+
updateWhere(predicate: (entity: E) => boolean, changes: Partial<E>): number;
|
|
196
|
+
upsertOne(entity: E, opts?: AddOptions<E, K>): K;
|
|
197
|
+
upsertMany(entities: E[], opts?: AddOptions<E, K>): K[];
|
|
198
|
+
removeOne(id: K, opts?: MutationOptions): void;
|
|
199
|
+
removeMany(ids: K[], opts?: MutationOptions): void;
|
|
200
|
+
removeWhere(predicate: (entity: E) => boolean): number;
|
|
201
|
+
clear(): void;
|
|
202
|
+
removeAll(): void;
|
|
203
|
+
setAll(entities: E[], opts?: AddOptions<E, K>): void;
|
|
204
|
+
tap(handlers: TapHandlers<E, K>): () => void;
|
|
205
|
+
intercept(handlers: InterceptHandlers<E, K>): () => void;
|
|
206
|
+
}
|
|
207
|
+
export interface EntityHelpers<E extends {
|
|
208
|
+
id: string | number;
|
|
209
|
+
}> {
|
|
210
|
+
add(entity: E): void;
|
|
211
|
+
update(id: E['id'], updates: Partial<E>): void;
|
|
212
|
+
remove(id: E['id']): void;
|
|
213
|
+
upsert(entity: E): void;
|
|
214
|
+
selectById(id: E['id']): Signal<E | undefined>;
|
|
215
|
+
selectBy(predicate: (entity: E) => boolean): Signal<E[]>;
|
|
216
|
+
selectIds(): Signal<Array<string | number>>;
|
|
217
|
+
selectAll(): Signal<E[]>;
|
|
218
|
+
selectTotal(): Signal<number>;
|
|
219
|
+
clear(): void;
|
|
220
|
+
}
|
|
221
|
+
export interface LoggingConfig {
|
|
222
|
+
name?: string;
|
|
223
|
+
filter?: (path: string) => boolean;
|
|
224
|
+
collapsed?: boolean;
|
|
225
|
+
onLog?: (entry: LogEntry) => void;
|
|
226
|
+
}
|
|
227
|
+
export interface LogEntry {
|
|
228
|
+
path: string;
|
|
229
|
+
prev: unknown;
|
|
230
|
+
value: unknown;
|
|
231
|
+
timestamp: number;
|
|
232
|
+
}
|
|
233
|
+
export interface ValidationConfig<T> {
|
|
234
|
+
validators: Array<{
|
|
235
|
+
match: (path: string) => boolean;
|
|
236
|
+
validate: (value: T, path: string) => void | never;
|
|
237
|
+
}>;
|
|
238
|
+
onError?: (error: Error, path: string) => void;
|
|
239
|
+
}
|
|
240
|
+
export interface PersistenceConfig {
|
|
241
|
+
key: string;
|
|
242
|
+
storage?: Storage;
|
|
243
|
+
debounceMs?: number;
|
|
244
|
+
filter?: (path: string) => boolean;
|
|
245
|
+
serialize?: (state: unknown) => string;
|
|
246
|
+
deserialize?: (json: string) => unknown;
|
|
247
|
+
}
|
|
248
|
+
export interface DevToolsConfig {
|
|
249
|
+
name?: string;
|
|
250
|
+
maxAge?: number;
|
|
251
|
+
features?: {
|
|
252
|
+
jump?: boolean;
|
|
253
|
+
skip?: boolean;
|
|
254
|
+
reorder?: boolean;
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
export type EntityType<T> = T extends EntitySignal<infer E, infer K extends string | number> ? E : never;
|
|
258
|
+
export type EntityKeyType<T> = T extends EntitySignal<unknown, infer K extends string | number> ? K : never;
|
|
259
|
+
export type IsEntityMap<T> = T extends EntityMapMarker<unknown, infer K extends string | number> ? true : false;
|
|
260
|
+
export type EntityAwareTreeNode<T> = {
|
|
261
|
+
[K in keyof T]: T[K] extends EntityMapMarker<infer E, infer Key> ? EntitySignal<E, Key> : T[K] extends object ? EntityAwareTreeNode<T[K]> : CallableWritableSignal<T[K]>;
|
|
262
|
+
};
|
|
263
|
+
export type PathHandler = (value: unknown, prev: unknown, path: string) => void;
|
|
264
|
+
export type PathInterceptor = (ctx: {
|
|
265
|
+
path: string;
|
|
266
|
+
value: unknown;
|
|
267
|
+
prev: unknown;
|
|
268
|
+
blocked: boolean;
|
|
269
|
+
blockReason?: string;
|
|
270
|
+
}, next: () => void) => void | Promise<void>;
|
|
271
|
+
export interface TimeTravelEntry<T> {
|
|
272
|
+
action: string;
|
|
273
|
+
timestamp: number;
|
|
274
|
+
state: T;
|
|
275
|
+
payload?: unknown;
|
|
276
|
+
}
|
|
277
|
+
export declare function isSignalTree<T>(value: unknown): value is SignalTree<T>;
|
|
278
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PathIndex } from './path-index';
|
|
2
|
+
import type { DiffOptions } from './diff-engine';
|
|
3
|
+
export interface UpdateOptions extends DiffOptions {
|
|
4
|
+
batch?: boolean;
|
|
5
|
+
batchSize?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface UpdateResult {
|
|
8
|
+
changed: boolean;
|
|
9
|
+
duration: number;
|
|
10
|
+
changedPaths: string[];
|
|
11
|
+
stats?: {
|
|
12
|
+
totalPaths: number;
|
|
13
|
+
optimizedPaths: number;
|
|
14
|
+
batchedUpdates: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare class OptimizedUpdateEngine {
|
|
18
|
+
private pathIndex;
|
|
19
|
+
private diffEngine;
|
|
20
|
+
constructor(tree: unknown);
|
|
21
|
+
update(tree: unknown, updates: unknown, options?: UpdateOptions): UpdateResult;
|
|
22
|
+
rebuildIndex(tree: unknown): void;
|
|
23
|
+
getIndexStats(): ReturnType<PathIndex['getStats']>;
|
|
24
|
+
private createPatches;
|
|
25
|
+
private createPatch;
|
|
26
|
+
private calculatePriority;
|
|
27
|
+
private sortPatches;
|
|
28
|
+
private applyPatches;
|
|
29
|
+
private batchApplyPatches;
|
|
30
|
+
private applyPatch;
|
|
31
|
+
private isEqual;
|
|
32
|
+
}
|
package/src/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { deepClone, deepEqual } from '@signaltree/shared';
|