@signaltree/core 4.1.7 → 4.2.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 +68 -16
- package/dist/enhancers/entities/lib/entities.js +32 -8
- package/dist/lib/constants.js +0 -1
- package/dist/lib/signal-tree.js +0 -6
- package/package.json +1 -1
- package/src/index.d.ts +1 -1
- package/src/lib/constants.d.ts +0 -1
- package/src/lib/types.d.ts +4 -13
package/README.md
CHANGED
|
@@ -594,11 +594,18 @@ const tree = signalTree({
|
|
|
594
594
|
withTimeTravel() // Undo/redo support
|
|
595
595
|
);
|
|
596
596
|
|
|
597
|
-
//
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
597
|
+
// For async operations, use manual async or middleware helpers
|
|
598
|
+
async function fetchUser(id: string) {
|
|
599
|
+
tree.$.loading.set(true);
|
|
600
|
+
try {
|
|
601
|
+
const user = await api.getUser(id);
|
|
602
|
+
tree.$.user.set(user);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
tree.$.loading.set(error.message);
|
|
605
|
+
} finally {
|
|
606
|
+
tree.$.loading.set(false);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
602
609
|
|
|
603
610
|
// Automatic state persistence
|
|
604
611
|
tree.$.preferences.theme('dark'); // Auto-saved
|
|
@@ -859,12 +866,47 @@ users.add(newUser);
|
|
|
859
866
|
users.selectBy((u) => u.active);
|
|
860
867
|
users.updateMany([{ id: '1', changes: { status: 'active' } }]);
|
|
861
868
|
|
|
862
|
-
//
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
869
|
+
// Entity helpers support nested paths for organized state structures
|
|
870
|
+
// Example: deeply nested entities in a domain-driven design pattern
|
|
871
|
+
const appTree = signalTree({
|
|
872
|
+
app: {
|
|
873
|
+
data: {
|
|
874
|
+
users: [] as User[],
|
|
875
|
+
products: [] as Product[],
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
admin: {
|
|
879
|
+
data: {
|
|
880
|
+
logs: [] as AuditLog[],
|
|
881
|
+
reports: [] as Report[],
|
|
882
|
+
},
|
|
883
|
+
},
|
|
867
884
|
});
|
|
885
|
+
|
|
886
|
+
// Access deeply nested entities using dot notation
|
|
887
|
+
const appUsers = appTree.entities<User>('app.data.users');
|
|
888
|
+
const appProducts = appTree.entities<Product>('app.data.products');
|
|
889
|
+
const adminLogs = appTree.entities<AuditLog>('admin.data.logs');
|
|
890
|
+
const adminReports = appTree.entities<Report>('admin.data.reports');
|
|
891
|
+
|
|
892
|
+
// All entity methods work seamlessly with nested paths
|
|
893
|
+
appUsers.selectBy((u) => u.isAdmin); // Filtered signal
|
|
894
|
+
appProducts.selectTotal(); // Count signal
|
|
895
|
+
adminLogs.selectAll(); // All items signal
|
|
896
|
+
adminReports.selectIds(); // ID array signal
|
|
897
|
+
|
|
898
|
+
// For async operations, use manual async or middleware helpers
|
|
899
|
+
async function fetchUsers() {
|
|
900
|
+
tree.$.ui.loading.set(true);
|
|
901
|
+
try {
|
|
902
|
+
const users = await api.getUsers();
|
|
903
|
+
tree.$.users.set(users);
|
|
904
|
+
} catch (error) {
|
|
905
|
+
tree.$.ui.error.set(error.message);
|
|
906
|
+
} finally {
|
|
907
|
+
tree.$.ui.loading.set(false);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
868
910
|
```
|
|
869
911
|
|
|
870
912
|
### Full-Featured Development Composition
|
|
@@ -901,7 +943,9 @@ const tree = signalTree({
|
|
|
901
943
|
|
|
902
944
|
// Rich feature set available
|
|
903
945
|
const users = tree.entities<User>('app.data.users');
|
|
904
|
-
|
|
946
|
+
async function fetchUser(id: string) {
|
|
947
|
+
return await api.getUser(id);
|
|
948
|
+
}
|
|
905
949
|
tree.undo(); // Time travel
|
|
906
950
|
tree.save(); // Persistence
|
|
907
951
|
```
|
|
@@ -1272,7 +1316,7 @@ tree.destroy(); // Cleanup resources
|
|
|
1272
1316
|
|
|
1273
1317
|
// Extended features (built into @signaltree/core)
|
|
1274
1318
|
tree.entities<T>(key); // Entity helpers (use withEntities enhancer)
|
|
1275
|
-
|
|
1319
|
+
// For async operations, use manual async or middleware helpers like createAsyncOperation or trackAsync
|
|
1276
1320
|
```
|
|
1277
1321
|
|
|
1278
1322
|
## Extending with enhancers
|
|
@@ -1355,10 +1399,18 @@ async loadUsers() {
|
|
|
1355
1399
|
}
|
|
1356
1400
|
}
|
|
1357
1401
|
|
|
1358
|
-
// Or use
|
|
1359
|
-
loadUsers =
|
|
1360
|
-
|
|
1361
|
-
|
|
1402
|
+
// Or use manual async patterns
|
|
1403
|
+
loadUsers = async () => {
|
|
1404
|
+
tree.$.loading.set(true);
|
|
1405
|
+
try {
|
|
1406
|
+
const users = await api.getUsers();
|
|
1407
|
+
tree.$.users.set(users);
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
tree.$.error.set(error instanceof Error ? error.message : 'Unknown error');
|
|
1410
|
+
} finally {
|
|
1411
|
+
tree.$.loading.set(false);
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1362
1414
|
```
|
|
1363
1415
|
|
|
1364
1416
|
## Examples
|
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
import { computed } from '@angular/core';
|
|
2
2
|
import { isAnySignal, isNodeAccessor } from '../../../lib/utils.js';
|
|
3
3
|
|
|
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(', ')}`);
|
|
10
|
+
}
|
|
11
|
+
return signal;
|
|
12
|
+
}
|
|
13
|
+
const segments = pathStr.split('.');
|
|
14
|
+
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
|
+
}
|
|
20
|
+
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
|
+
}
|
|
26
|
+
if (isAnySignal(current)) {
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
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
|
+
}
|
|
4
31
|
function createEntityHelpers(tree, entityKey) {
|
|
5
32
|
const getEntitySignal = () => {
|
|
6
|
-
const
|
|
7
|
-
if (!
|
|
8
|
-
throw new Error(`Entity key '${String(entityKey)}' does not exist in the state`);
|
|
9
|
-
}
|
|
10
|
-
if (!isAnySignal(stateProperty)) {
|
|
33
|
+
const signal = resolveNestedSignal(tree, entityKey);
|
|
34
|
+
if (!isAnySignal(signal)) {
|
|
11
35
|
throw new Error(`Entity key '${String(entityKey)}' is not a signal. This should not happen with SignalTree.`);
|
|
12
36
|
}
|
|
13
|
-
const
|
|
14
|
-
const value =
|
|
37
|
+
const castSignal = signal;
|
|
38
|
+
const value = castSignal();
|
|
15
39
|
if (!Array.isArray(value)) {
|
|
16
40
|
throw new Error(`Entity key '${String(entityKey)}' does not contain an array. Current type: ${typeof value}`);
|
|
17
41
|
}
|
|
18
|
-
return
|
|
42
|
+
return castSignal;
|
|
19
43
|
};
|
|
20
44
|
const setSignalValue = (signal, value) => {
|
|
21
45
|
if (isNodeAccessor(signal)) {
|
package/dist/lib/constants.js
CHANGED
|
@@ -27,7 +27,6 @@ const DEV_MESSAGES = {
|
|
|
27
27
|
MEMOIZE_NOT_ENABLED: 'memoize disabled',
|
|
28
28
|
MIDDLEWARE_NOT_AVAILABLE: 'middleware missing',
|
|
29
29
|
ENTITY_HELPERS_NOT_AVAILABLE: 'entity helpers missing',
|
|
30
|
-
ASYNC_ACTIONS_NOT_AVAILABLE: 'async actions missing',
|
|
31
30
|
TIME_TRAVEL_NOT_AVAILABLE: 'time travel missing',
|
|
32
31
|
OPTIMIZE_NOT_AVAILABLE: 'optimize missing',
|
|
33
32
|
UPDATE_OPTIMIZED_NOT_AVAILABLE: 'update optimized missing',
|
package/dist/lib/signal-tree.js
CHANGED
|
@@ -464,12 +464,6 @@ function addStubMethods(tree, config) {
|
|
|
464
464
|
}
|
|
465
465
|
return {};
|
|
466
466
|
};
|
|
467
|
-
tree.asyncAction = (operation, asyncConfig = {}) => {
|
|
468
|
-
if (config.debugMode) {
|
|
469
|
-
console.warn(SIGNAL_TREE_MESSAGES.ASYNC_ACTIONS_NOT_AVAILABLE);
|
|
470
|
-
}
|
|
471
|
-
return {};
|
|
472
|
-
};
|
|
473
467
|
tree.undo = () => {
|
|
474
468
|
if (config.debugMode) {
|
|
475
469
|
console.warn(SIGNAL_TREE_MESSAGES.TIME_TRAVEL_NOT_AVAILABLE);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
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,
|
package/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { signalTree } from './lib/signal-tree';
|
|
2
|
-
export type { SignalTree, TreeNode, RemoveSignalMethods, Primitive, BuiltInObject, NotFn, TreeConfig, TreePreset, Enhancer, EnhancerMeta, EnhancerWithMeta, ChainResult, WithMethod, Middleware, PerformanceMetrics, EntityHelpers,
|
|
2
|
+
export type { SignalTree, TreeNode, RemoveSignalMethods, Primitive, BuiltInObject, NotFn, DeepPath, DeepAccess, TreeConfig, TreePreset, Enhancer, EnhancerMeta, EnhancerWithMeta, ChainResult, WithMethod, Middleware, PerformanceMetrics, EntityHelpers, TimeTravelEntry, } from './lib/types';
|
|
3
3
|
export { equal, deepEqual, isNodeAccessor, isAnySignal, toWritableSignal, parsePath, composeEnhancers, isBuiltInObject, createLazySignalTree, } from './lib/utils';
|
|
4
4
|
export { SecurityValidator, SecurityPresets, type SecurityEvent, type SecurityEventType, type SecurityValidatorConfig, } from './lib/security/security-validator';
|
|
5
5
|
export { createEnhancer, resolveEnhancerOrder } from './enhancers/index';
|
package/src/lib/constants.d.ts
CHANGED
|
@@ -25,7 +25,6 @@ export declare const SIGNAL_TREE_MESSAGES: Readonly<{
|
|
|
25
25
|
readonly MEMOIZE_NOT_ENABLED: "memoize disabled";
|
|
26
26
|
readonly MIDDLEWARE_NOT_AVAILABLE: "middleware missing";
|
|
27
27
|
readonly ENTITY_HELPERS_NOT_AVAILABLE: "entity helpers missing";
|
|
28
|
-
readonly ASYNC_ACTIONS_NOT_AVAILABLE: "async actions missing";
|
|
29
28
|
readonly TIME_TRAVEL_NOT_AVAILABLE: "time travel missing";
|
|
30
29
|
readonly OPTIMIZE_NOT_AVAILABLE: "optimize missing";
|
|
31
30
|
readonly UPDATE_OPTIMIZED_NOT_AVAILABLE: "update optimized missing";
|
package/src/lib/types.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export type TreeNode<T> = {
|
|
|
26
26
|
[K in keyof T]: 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
27
|
};
|
|
28
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;
|
|
29
33
|
export interface EnhancerMeta {
|
|
30
34
|
name?: string;
|
|
31
35
|
requires?: string[];
|
|
@@ -91,7 +95,6 @@ export type SignalTree<T> = NodeAccessor<T> & {
|
|
|
91
95
|
entities<E extends {
|
|
92
96
|
id: string | number;
|
|
93
97
|
}>(entityKey?: keyof T): EntityHelpers<E>;
|
|
94
|
-
asyncAction<TInput, TResult>(operation: (input: TInput) => Promise<TResult>, config?: AsyncActionConfig<T, TResult>): AsyncAction<TInput, TResult>;
|
|
95
98
|
undo(): void;
|
|
96
99
|
redo(): void;
|
|
97
100
|
getHistory(): TimeTravelEntry<T>[];
|
|
@@ -143,18 +146,6 @@ export interface EntityHelpers<E extends {
|
|
|
143
146
|
selectTotal(): Signal<number>;
|
|
144
147
|
clear(): void;
|
|
145
148
|
}
|
|
146
|
-
export interface AsyncActionConfig<T, TResult> {
|
|
147
|
-
onStart?: (state: T) => Partial<T>;
|
|
148
|
-
onSuccess?: (result: TResult, state: T) => Partial<T>;
|
|
149
|
-
onError?: (error: Error, state: T) => Partial<T>;
|
|
150
|
-
onComplete?: (state: T) => Partial<T>;
|
|
151
|
-
}
|
|
152
|
-
export interface AsyncAction<TInput, TResult> {
|
|
153
|
-
execute(input: TInput): Promise<TResult>;
|
|
154
|
-
pending: Signal<boolean>;
|
|
155
|
-
error: Signal<Error | null>;
|
|
156
|
-
result: Signal<TResult | null>;
|
|
157
|
-
}
|
|
158
149
|
export interface TimeTravelEntry<T> {
|
|
159
150
|
action: string;
|
|
160
151
|
timestamp: number;
|