@signaltree/core 4.1.7 → 4.2.1

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
@@ -594,11 +594,18 @@ const tree = signalTree({
594
594
  withTimeTravel() // Undo/redo support
595
595
  );
596
596
 
597
- // Advanced async operations
598
- const fetchUser = tree.asyncAction(async (id: string) => api.getUser(id), {
599
- loadingKey: 'loading',
600
- onSuccess: (user) => ({ user }),
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
- // Powerful async actions
863
- const fetchUsers = tree.asyncAction(async () => api.getUsers(), {
864
- loadingKey: 'ui.loading',
865
- errorKey: 'ui.error',
866
- onSuccess: (users) => ({ users }),
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
- const fetchUser = tree.asyncAction(api.getUser);
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
- tree.asyncAction(fn, config?); // Async operations (use createAsyncOperation helper)
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 middleware helpers for async actions (createAsyncOperation / trackAsync)
1359
- loadUsers = tree.asyncAction(() => api.getUsers(), {
1360
- onSuccess: (users) => ({ users }),
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,42 @@
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
+ current = current[segment];
18
+ if (current === undefined) {
19
+ const attemptedPath = segments.slice(0, i + 1).join('.');
20
+ throw new Error(`Entity path '${pathStr}' is invalid: '${attemptedPath}' does not exist in the state`);
21
+ }
22
+ }
23
+ if (isAnySignal(current)) {
24
+ return current;
25
+ }
26
+ throw new Error(`Entity path '${pathStr}' does not resolve to a signal. Ensure all parent levels in the path are valid nested objects.`);
27
+ }
4
28
  function createEntityHelpers(tree, entityKey) {
5
29
  const getEntitySignal = () => {
6
- const stateProperty = tree.state[entityKey];
7
- if (!stateProperty) {
8
- throw new Error(`Entity key '${String(entityKey)}' does not exist in the state`);
9
- }
10
- if (!isAnySignal(stateProperty)) {
30
+ const signal = resolveNestedSignal(tree, entityKey);
31
+ if (!isAnySignal(signal)) {
11
32
  throw new Error(`Entity key '${String(entityKey)}' is not a signal. This should not happen with SignalTree.`);
12
33
  }
13
- const signal = stateProperty;
14
- const value = signal();
34
+ const castSignal = signal;
35
+ const value = castSignal();
15
36
  if (!Array.isArray(value)) {
16
37
  throw new Error(`Entity key '${String(entityKey)}' does not contain an array. Current type: ${typeof value}`);
17
38
  }
18
- return signal;
39
+ return castSignal;
19
40
  };
20
41
  const setSignalValue = (signal, value) => {
21
42
  if (isNodeAccessor(signal)) {
@@ -1,3 +1,4 @@
1
+ import { computed } from '@angular/core';
1
2
  import { isNodeAccessor } from '../../../lib/utils.js';
2
3
  import { LRUCache } from '../../../lru-cache.js';
3
4
  import { deepEqual } from '../../../deep-equal.js';
@@ -281,6 +282,25 @@ function withMemoization(config = {}) {
281
282
  });
282
283
  applyUpdateResult(result);
283
284
  };
285
+ const memoizeResultCache = createMemoCacheStore(MAX_CACHE_SIZE, true);
286
+ tree.memoize = (fn, cacheKey) => {
287
+ return computed(() => {
288
+ const currentState = originalTreeCall();
289
+ const key = cacheKey || generateCacheKey(fn, [currentState]);
290
+ const cached = memoizeResultCache.get(key);
291
+ if (cached && equalityFn(cached.deps, [currentState])) {
292
+ return cached.value;
293
+ }
294
+ const result = fn(currentState);
295
+ memoizeResultCache.set(key, {
296
+ value: result,
297
+ deps: [currentState],
298
+ timestamp: Date.now(),
299
+ hitCount: 1
300
+ });
301
+ return result;
302
+ });
303
+ };
284
304
  tree.clearMemoCache = key => {
285
305
  if (key) {
286
306
  cache.delete(key);
@@ -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',
@@ -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.1.7",
3
+ "version": "4.2.1",
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,
@@ -7,16 +7,16 @@ interface EntityConfig {
7
7
  export declare function withEntities(config?: EntityConfig): <T>(tree: SignalTree<T>) => SignalTree<T> & {
8
8
  entities<E extends {
9
9
  id: string | number;
10
- }>(entityKey: keyof T): EntityHelpers<E>;
10
+ }>(entityKey: keyof T | string): EntityHelpers<E>;
11
11
  };
12
12
  export declare function enableEntities(): <T>(tree: SignalTree<T>) => SignalTree<T> & {
13
13
  entities<E extends {
14
14
  id: string | number;
15
- }>(entityKey: keyof T): EntityHelpers<E>;
15
+ }>(entityKey: keyof T | string): EntityHelpers<E>;
16
16
  };
17
17
  export declare function withHighPerformanceEntities(): <T>(tree: SignalTree<T>) => SignalTree<T> & {
18
18
  entities<E extends {
19
19
  id: string | number;
20
- }>(entityKey: keyof T): EntityHelpers<E>;
20
+ }>(entityKey: keyof T | string): EntityHelpers<E>;
21
21
  };
22
22
  export {};
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, AsyncActionConfig, AsyncAction, TimeTravelEntry, } from './lib/types';
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';
@@ -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";
@@ -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;