@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 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).with(entities());
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
- if (typeof ngDevMode === 'undefined' || ngDevMode) {
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
- function enableEntities() {
14
- return entities();
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';
@@ -0,0 +1,5 @@
1
+ function externalDerived() {
2
+ return fn => fn;
3
+ }
4
+
5
+ export { externalDerived };
@@ -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(entities()).with(timeTravel()).with(devTools());
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(entities()).with(timeTravel(config.timeTravel)).with(devTools(config.devTools));
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.2",
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): <T>(tree: ISignalTree<T>) => ISignalTree<T> & EntitiesEnabled;
6
- export declare function enableEntities(): <T>(tree: ISignalTree<T>) => ISignalTree<T> & EntitiesEnabled;
7
- export declare function highPerformanceEntities(): <T>(tree: ISignalTree<T>) => ISignalTree<T> & EntitiesEnabled;
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;