@signaltree/core 7.1.2 → 7.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 +119 -2
- package/dist/enhancers/entities/entities.js +3 -20
- package/dist/index.js +1 -0
- package/dist/lib/internals/derived-types.js +5 -0
- package/dist/lib/presets.js +2 -3
- package/package.json +1 -1
- package/src/enhancers/entities/entities.d.ts +4 -8
- package/src/index.d.ts +2 -1
- package/src/lib/internals/derived-types.d.ts +8 -0
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ Follow these principles for idiomatic SignalTree code:
|
|
|
110
110
|
### 1. Expose signals directly (no computed wrappers)
|
|
111
111
|
|
|
112
112
|
```typescript
|
|
113
|
-
const tree = signalTree(initialState).with(entities())
|
|
113
|
+
const tree = signalTree(initialState); // No .with(entities()) needed in v7+ (deprecated in v6, removed in v7)
|
|
114
114
|
const $ = tree.$; // Shorthand for state access
|
|
115
115
|
|
|
116
116
|
// ✅ SignalTree-first: Direct signal exposure
|
|
@@ -135,7 +135,7 @@ export type UserTree = ReturnType<typeof createUserTree>;
|
|
|
135
135
|
|
|
136
136
|
// Factory function - no explicit return type needed
|
|
137
137
|
export function createUserTree() {
|
|
138
|
-
const tree = signalTree(initialState)
|
|
138
|
+
const tree = signalTree(initialState); // entities() not needed in v7+
|
|
139
139
|
return {
|
|
140
140
|
selectedUserId: tree.$.selected.userId, // Type inferred automatically
|
|
141
141
|
// ...
|
|
@@ -897,6 +897,123 @@ tree.log('Tree created');
|
|
|
897
897
|
>
|
|
898
898
|
> 📱 **Interactive demo**: [Demo App](/custom-extensions)
|
|
899
899
|
|
|
900
|
+
### 8) Derived State Tiers
|
|
901
|
+
|
|
902
|
+
SignalTree supports **derived state** via the `.derived()` method, which allows you to add computed signals that build on base state or previous derived tiers.
|
|
903
|
+
|
|
904
|
+
#### Basic Usage (Inline Derived)
|
|
905
|
+
|
|
906
|
+
When derived functions are defined inline, TypeScript automatically infers all types:
|
|
907
|
+
|
|
908
|
+
```typescript
|
|
909
|
+
import { signalTree, entityMap } from '@signaltree/core';
|
|
910
|
+
import { computed } from '@angular/core';
|
|
911
|
+
|
|
912
|
+
const tree = signalTree({
|
|
913
|
+
users: entityMap<User, number>(),
|
|
914
|
+
selectedUserId: null as number | null,
|
|
915
|
+
})
|
|
916
|
+
.derived(($) => ({
|
|
917
|
+
// Tier 1: Entity resolution
|
|
918
|
+
selectedUser: computed(() => {
|
|
919
|
+
const id = $.selectedUserId();
|
|
920
|
+
return id != null ? $.users.byId(id)?.() ?? null : null;
|
|
921
|
+
}),
|
|
922
|
+
}))
|
|
923
|
+
.derived(($) => ({
|
|
924
|
+
// Tier 2: Complex logic (can access $.selectedUser from Tier 1)
|
|
925
|
+
isAdmin: computed(() => $.selectedUser()?.role === 'admin'),
|
|
926
|
+
}));
|
|
927
|
+
|
|
928
|
+
// Usage
|
|
929
|
+
tree.$.selectedUser(); // User | null (computed signal)
|
|
930
|
+
tree.$.isAdmin(); // boolean (computed signal)
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
#### External Derived Functions (Modular Architecture)
|
|
934
|
+
|
|
935
|
+
For larger applications, you may want to organize derived tiers into separate files. **This requires explicit typing** because TypeScript cannot infer types across file boundaries.
|
|
936
|
+
|
|
937
|
+
SignalTree provides two utilities for external derived functions:
|
|
938
|
+
|
|
939
|
+
- **`externalDerived<TTree>()`** - Curried helper function that provides type context for your derived function
|
|
940
|
+
- **`WithDerived<TTree, TDerivedFn>`** - Type utility to build intermediate tree types
|
|
941
|
+
|
|
942
|
+
```typescript
|
|
943
|
+
// app-tree.ts
|
|
944
|
+
import { signalTree, entityMap, WithDerived } from '@signaltree/core';
|
|
945
|
+
import { entityResolutionDerived } from './derived/tier-entity-resolution';
|
|
946
|
+
import { complexLogicDerived } from './derived/tier-complex-logic';
|
|
947
|
+
|
|
948
|
+
// Define base tree type
|
|
949
|
+
export type AppTreeBase = ReturnType<typeof signalTree<ReturnType<typeof createBaseState>>>;
|
|
950
|
+
|
|
951
|
+
// Build intermediate types using WithDerived
|
|
952
|
+
export type AppTreeWithTier1 = WithDerived<AppTreeBase, typeof entityResolutionDerived>;
|
|
953
|
+
export type AppTreeWithTier2 = WithDerived<AppTreeWithTier1, typeof complexLogicDerived>;
|
|
954
|
+
|
|
955
|
+
function createBaseState() {
|
|
956
|
+
return {
|
|
957
|
+
users: entityMap<User, number>(),
|
|
958
|
+
selectedUserId: null as number | null,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export function createAppTree() {
|
|
963
|
+
return signalTree(createBaseState()).derived(entityResolutionDerived).derived(complexLogicDerived);
|
|
964
|
+
}
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
```typescript
|
|
968
|
+
// derived/tier-entity-resolution.ts
|
|
969
|
+
import { computed } from '@angular/core';
|
|
970
|
+
import { externalDerived } from '@signaltree/core';
|
|
971
|
+
import type { AppTreeBase } from '../app-tree';
|
|
972
|
+
|
|
973
|
+
// externalDerived provides the type context for $ via curried syntax
|
|
974
|
+
export const entityResolutionDerived = externalDerived<AppTreeBase>()(($) => ({
|
|
975
|
+
selectedUser: computed(() => {
|
|
976
|
+
const id = $.selectedUserId();
|
|
977
|
+
return id != null ? $.users.byId(id)?.() ?? null : null;
|
|
978
|
+
}),
|
|
979
|
+
}));
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
// derived/tier-complex-logic.ts
|
|
984
|
+
import { computed } from '@angular/core';
|
|
985
|
+
import { externalDerived } from '@signaltree/core';
|
|
986
|
+
import type { AppTreeWithTier1 } from '../app-tree';
|
|
987
|
+
|
|
988
|
+
// This tier has access to $.selectedUser from Tier 1
|
|
989
|
+
export const complexLogicDerived = externalDerived<AppTreeWithTier1>()(($) => ({
|
|
990
|
+
isAdmin: computed(() => $.selectedUser()?.role === 'admin'),
|
|
991
|
+
displayName: computed(() => {
|
|
992
|
+
const user = $.selectedUser();
|
|
993
|
+
return user ? `${user.firstName} ${user.lastName}` : 'No user selected';
|
|
994
|
+
}),
|
|
995
|
+
}));
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
#### Why External Functions Need Typing
|
|
999
|
+
|
|
1000
|
+
When a function is defined in a separate file, TypeScript analyzes it **in isolation** before knowing how it will be used. The type inference happens at the **definition site**, not the **call site**:
|
|
1001
|
+
|
|
1002
|
+
```typescript
|
|
1003
|
+
// ❌ TypeScript can't infer $ - this file is compiled before app-tree.ts uses it
|
|
1004
|
+
export function myDerived($) {
|
|
1005
|
+
// $ is 'any'
|
|
1006
|
+
return { foo: computed(() => $.bar()) }; // Error: $ has no properties
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// ✅ externalDerived provides the type context (curried syntax)
|
|
1010
|
+
export const myDerived = externalDerived<AppTreeBase>()(($) => ({
|
|
1011
|
+
foo: computed(() => $.bar()), // $ is properly typed
|
|
1012
|
+
}));
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
**Key point**: `externalDerived` is **only needed for functions defined in separate files**. Inline functions automatically inherit types from the chain. Note the curried syntax: `externalDerived<TreeType>()(fn)` - this allows TypeScript to infer the return type while you specify the tree type.
|
|
1016
|
+
|
|
900
1017
|
## Error handling examples
|
|
901
1018
|
|
|
902
1019
|
### Manual async error handling
|
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
function entities(config = {}) {
|
|
2
|
-
|
|
3
|
-
console.warn('SignalTree: entities() enhancer is deprecated in v7. ' + 'EntityMap markers are now automatically processed. ' + 'Remove .with(entities()) from your code. ' + 'This enhancer will be removed in v8.');
|
|
4
|
-
}
|
|
5
|
-
const {
|
|
6
|
-
enabled = true
|
|
7
|
-
} = config;
|
|
8
|
-
return tree => {
|
|
9
|
-
tree.__entitiesEnabled = true;
|
|
10
|
-
return tree;
|
|
11
|
-
};
|
|
2
|
+
throw new Error('entities() has been removed. Remove `.with(entities())` from your code; v7+ auto-processes EntityMap markers.');
|
|
12
3
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
function highPerformanceEntities() {
|
|
17
|
-
return entities();
|
|
18
|
-
}
|
|
19
|
-
Object.assign(entities, {
|
|
20
|
-
highPerformance: highPerformanceEntities,
|
|
21
|
-
enable: enableEntities
|
|
22
|
-
});
|
|
4
|
+
const enableEntities = entities;
|
|
5
|
+
const highPerformanceEntities = entities;
|
|
23
6
|
|
|
24
7
|
export { enableEntities, entities, highPerformanceEntities };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { signalTree } from './lib/signal-tree.js';
|
|
2
2
|
export { ENHANCER_META } from './lib/types.js';
|
|
3
|
+
export { externalDerived } from './lib/internals/derived-types.js';
|
|
3
4
|
export { isDerivedMarker } from './lib/markers/derived.js';
|
|
4
5
|
export { LoadingState, isStatusMarker, status } from './lib/markers/status.js';
|
|
5
6
|
export { isStoredMarker, stored } from './lib/markers/stored.js';
|
package/dist/lib/presets.js
CHANGED
|
@@ -2,19 +2,18 @@ import { signalTree } from './signal-tree.js';
|
|
|
2
2
|
import { effects } from '../enhancers/effects/effects.js';
|
|
3
3
|
import { batching } from '../enhancers/batching/batching.js';
|
|
4
4
|
import { memoization } from '../enhancers/memoization/memoization.js';
|
|
5
|
-
import { entities } from '../enhancers/entities/entities.js';
|
|
6
5
|
import { timeTravel } from '../enhancers/time-travel/time-travel.js';
|
|
7
6
|
import { devTools } from '../enhancers/devtools/devtools.js';
|
|
8
7
|
|
|
9
8
|
function createDevTree(initialState, config = {}) {
|
|
10
9
|
if (arguments.length === 0) {
|
|
11
|
-
const enhancer = tree => tree.with(effects()).with(batching()).with(memoization()).with(
|
|
10
|
+
const enhancer = tree => tree.with(effects()).with(batching()).with(memoization()).with(timeTravel()).with(devTools());
|
|
12
11
|
return {
|
|
13
12
|
enhancer
|
|
14
13
|
};
|
|
15
14
|
}
|
|
16
15
|
const base = signalTree(initialState, config);
|
|
17
|
-
const enhanced = base.with(effects()).with(batching(config.batching)).with(memoization(config.memoization)).with(
|
|
16
|
+
const enhanced = base.with(effects()).with(batching(config.batching)).with(memoization(config.memoization)).with(timeTravel(config.timeTravel)).with(devTools(config.devTools));
|
|
18
17
|
return enhanced;
|
|
19
18
|
}
|
|
20
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/core",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.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
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import type { ISignalTree, EntitiesEnabled } from '../../lib/types';
|
|
2
1
|
export interface EntitiesEnhancerConfig {
|
|
3
2
|
enabled?: boolean;
|
|
4
3
|
}
|
|
5
|
-
export declare function entities(config?: EntitiesEnhancerConfig):
|
|
6
|
-
export declare
|
|
7
|
-
export declare
|
|
8
|
-
export declare const withEntities: typeof entities
|
|
9
|
-
highPerformance: typeof highPerformanceEntities;
|
|
10
|
-
enable: typeof enableEntities;
|
|
11
|
-
};
|
|
4
|
+
export declare function entities(config?: EntitiesEnhancerConfig): void;
|
|
5
|
+
export declare const enableEntities: typeof entities;
|
|
6
|
+
export declare const highPerformanceEntities: typeof entities;
|
|
7
|
+
export declare const withEntities: typeof entities;
|
package/src/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { signalTree } from './lib/signal-tree';
|
|
2
2
|
export type { ISignalTree, SignalTree, SignalTreeBase, FullSignalTree, ProdSignalTree, TreeNode, CallableWritableSignal, AccessibleNode, NodeAccessor, Primitive, NotFn, TreeConfig, TreePreset, Enhancer, EnhancerMeta, EnhancerWithMeta, EntitySignal, EntityMapMarker, EntityConfig, MutationOptions, AddOptions, AddManyOptions, TimeTravelEntry, TimeTravelMethods, } from './lib/types';
|
|
3
3
|
export { entityMap } from './lib/types';
|
|
4
|
-
export type { ProcessDerived, DeepMergeTree, DerivedFactory, } from './lib/internals/derived-types';
|
|
4
|
+
export type { ProcessDerived, DeepMergeTree, DerivedFactory, WithDerived, } from './lib/internals/derived-types';
|
|
5
|
+
export { externalDerived } from './lib/internals/derived-types';
|
|
5
6
|
export type { SignalTreeBuilder } from './lib/internals/builder-types';
|
|
6
7
|
export { isDerivedMarker, type DerivedMarker, type DerivedType, } from './lib/markers/derived';
|
|
7
8
|
export { status, isStatusMarker, LoadingState, type StatusMarker, type StatusSignal, type StatusConfig, } from './lib/markers/status';
|
|
@@ -8,3 +8,11 @@ export type DeepMergeTree<TSource, TDerived> = {
|
|
|
8
8
|
[K in keyof TSource | keyof TDerived]: K extends keyof TSource ? K extends keyof TDerived ? TSource[K] extends object ? TDerived[K] extends object ? TDerived[K] extends DerivedMarker<infer R> ? Signal<R> : TSource[K] & DeepMergeTree<TSource[K], ProcessDerived<TDerived[K]>> : TSource[K] : ProcessDerived<TDerived[K]> : TSource[K] : K extends keyof TDerived ? ProcessDerived<TDerived[K]> : never;
|
|
9
9
|
};
|
|
10
10
|
export type DerivedFactory<TSource, TDerived> = ($: TreeNode<TSource>) => TDerived;
|
|
11
|
+
export type WithDerived<TTree extends {
|
|
12
|
+
$: object;
|
|
13
|
+
}, TDerivedFn extends ($: TTree['$']) => object> = TTree & {
|
|
14
|
+
$: TTree['$'] & ReturnType<TDerivedFn>;
|
|
15
|
+
};
|
|
16
|
+
export declare function externalDerived<TTree extends {
|
|
17
|
+
$: object;
|
|
18
|
+
}>(): <TReturn extends object>(fn: ($: TTree['$']) => TReturn) => ($: any) => TReturn;
|