@signaltree/core 6.3.0 → 6.4.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/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { signalTree } from './lib/signal-tree.js';
2
2
  export { ENHANCER_META, entityMap } from './lib/types.js';
3
+ export { derived, isDerivedMarker } from './lib/markers/derived.js';
3
4
  export { composeEnhancers, createLazySignalTree, isAnySignal, isNodeAccessor, toWritableSignal } from './lib/utils.js';
4
5
  export { createEditSession } from './lib/edit-session.js';
5
6
  export { getPathNotifier } from './lib/path-notifier.js';
@@ -0,0 +1,59 @@
1
+ import { computed, isSignal } from '@angular/core';
2
+ import { isDerivedMarker } from '../markers/derived.js';
3
+
4
+ function isSignalLike(value) {
5
+ return isSignal(value);
6
+ }
7
+ function ensurePathAndGetTarget($, path) {
8
+ if (!path) return $;
9
+ const parts = path.split('.');
10
+ let current = $;
11
+ for (const part of parts) {
12
+ if (!(part in current)) {
13
+ current[part] = {};
14
+ }
15
+ current = current[part];
16
+ }
17
+ return current;
18
+ }
19
+ function mergeDerivedState($, derivedDef, path = '') {
20
+ if (!derivedDef || typeof derivedDef !== 'object') {
21
+ return;
22
+ }
23
+ for (const [key, value] of Object.entries(derivedDef)) {
24
+ const currentPath = path ? `${path}.${key}` : key;
25
+ if (isDerivedMarker(value)) {
26
+ const target = ensurePathAndGetTarget($, path);
27
+ if (key in target && isSignalLike(target[key])) {
28
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
29
+ console.warn(`SignalTree: Derived "${currentPath}" overwrites source signal. ` + `Consider using a different key to avoid confusion.`);
30
+ }
31
+ }
32
+ target[key] = computed(value.factory);
33
+ } else if (isSignalLike(value)) {
34
+ const target = ensurePathAndGetTarget($, path);
35
+ if (key in target && isSignalLike(target[key])) {
36
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
37
+ console.warn(`SignalTree: Derived signal "${currentPath}" overwrites source signal. ` + `Consider using a different key to avoid confusion.`);
38
+ }
39
+ }
40
+ target[key] = value;
41
+ } else if (typeof value === 'object' && value !== null) {
42
+ const target = ensurePathAndGetTarget($, path);
43
+ if (!(key in target)) {
44
+ target[key] = {};
45
+ } else if (isSignalLike(target[key])) {
46
+ throw new Error(`SignalTree: Cannot merge derived object into "${currentPath}" ` + `because source is a signal. Either make source an object or use a different key.`);
47
+ }
48
+ mergeDerivedState($, value, currentPath);
49
+ }
50
+ }
51
+ }
52
+ function applyDerivedFactories($, factories) {
53
+ for (const factory of factories) {
54
+ const derivedDef = factory($);
55
+ mergeDerivedState($, derivedDef);
56
+ }
57
+ }
58
+
59
+ export { applyDerivedFactories, mergeDerivedState };
@@ -0,0 +1,12 @@
1
+ const DERIVED_MARKER = Symbol.for('signaltree:derived');
2
+ function derived(factory) {
3
+ return {
4
+ [DERIVED_MARKER]: true,
5
+ factory
6
+ };
7
+ }
8
+ function isDerivedMarker(value) {
9
+ return value !== null && typeof value === 'object' && DERIVED_MARKER in value && value[DERIVED_MARKER] === true;
10
+ }
11
+
12
+ export { derived, isDerivedMarker };
@@ -1,5 +1,6 @@
1
1
  import { signal, isSignal } from '@angular/core';
2
2
  import { SIGNAL_TREE_MESSAGES, SIGNAL_TREE_CONSTANTS } from './constants.js';
3
+ import { applyDerivedFactories } from './internals/merge-derived.js';
3
4
  import { SignalMemoryManager } from './memory/memory-manager.js';
4
5
  import { getPathNotifier } from './path-notifier.js';
5
6
  import { SecurityValidator } from './security/security-validator.js';
@@ -275,8 +276,112 @@ function create(initialState, config) {
275
276
  }
276
277
  return tree;
277
278
  }
278
- function signalTree(initialState, config = {}) {
279
- return create(initialState, config);
279
+ function signalTree(initialState, configOrDerived) {
280
+ const isFactory = typeof configOrDerived === 'function';
281
+ const config = isFactory ? {} : configOrDerived ?? {};
282
+ const baseTree = create(initialState, config);
283
+ const builder = createBuilder(baseTree);
284
+ if (isFactory) {
285
+ return builder.derived(configOrDerived);
286
+ }
287
+ return builder;
288
+ }
289
+ function createBuilder(baseTree) {
290
+ const derivedQueue = [];
291
+ let isFinalized = false;
292
+ const finalize = () => {
293
+ if (isFinalized) return;
294
+ isFinalized = true;
295
+ if (derivedQueue.length > 0) {
296
+ applyDerivedFactories(baseTree.$, derivedQueue);
297
+ }
298
+ };
299
+ const builder = function (arg) {
300
+ if (arguments.length === 0) {
301
+ return baseTree();
302
+ }
303
+ return baseTree(arg);
304
+ };
305
+ builder[NODE_ACCESSOR_SYMBOL] = true;
306
+ Object.defineProperty(builder, 'state', {
307
+ get() {
308
+ finalize();
309
+ return baseTree.state;
310
+ },
311
+ enumerable: false,
312
+ configurable: true
313
+ });
314
+ Object.defineProperty(builder, '$', {
315
+ get() {
316
+ finalize();
317
+ return baseTree.$;
318
+ },
319
+ enumerable: false,
320
+ configurable: true
321
+ });
322
+ Object.defineProperty(builder, 'with', {
323
+ value: function (enhancer) {
324
+ const enhanced = baseTree.with(enhancer);
325
+ const newBuilder = createBuilder(enhanced);
326
+ for (const key of Object.keys(enhanced)) {
327
+ if (key !== '$' && key !== 'state' && key !== 'with' && key !== 'bind' && key !== 'destroy' && key !== 'derived') {
328
+ try {
329
+ newBuilder[key] = enhanced[key];
330
+ } catch {}
331
+ }
332
+ }
333
+ for (const factory of derivedQueue) {
334
+ newBuilder.derived(factory);
335
+ }
336
+ return newBuilder;
337
+ },
338
+ enumerable: false,
339
+ writable: false,
340
+ configurable: true
341
+ });
342
+ if (typeof baseTree.bind === 'function') {
343
+ Object.defineProperty(builder, 'bind', {
344
+ value: baseTree.bind.bind(baseTree),
345
+ enumerable: false,
346
+ writable: false,
347
+ configurable: true
348
+ });
349
+ } else {
350
+ Object.defineProperty(builder, 'bind', {
351
+ value: () => builder,
352
+ enumerable: false,
353
+ writable: false,
354
+ configurable: true
355
+ });
356
+ }
357
+ if (typeof baseTree.destroy === 'function') {
358
+ Object.defineProperty(builder, 'destroy', {
359
+ value: baseTree.destroy.bind(baseTree),
360
+ enumerable: false,
361
+ writable: true,
362
+ configurable: true
363
+ });
364
+ } else {
365
+ Object.defineProperty(builder, 'destroy', {
366
+ value: () => {},
367
+ enumerable: false,
368
+ writable: true,
369
+ configurable: true
370
+ });
371
+ }
372
+ Object.defineProperty(builder, 'derived', {
373
+ value: function (factory) {
374
+ if (isFinalized) {
375
+ throw new Error('SignalTree: Cannot add derived() after tree.$ has been accessed. ' + 'Chain all .derived() calls before accessing $.');
376
+ }
377
+ derivedQueue.push(factory);
378
+ return builder;
379
+ },
380
+ enumerable: false,
381
+ writable: false,
382
+ configurable: true
383
+ });
384
+ return builder;
280
385
  }
281
386
 
282
387
  export { isNodeAccessor, signalTree };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/core",
3
- "version": "6.3.0",
3
+ "version": "6.4.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,6 +1,9 @@
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';
5
+ export type { SignalTreeBuilder } from './lib/internals/builder-types';
6
+ export { derived, isDerivedMarker, type DerivedMarker, type DerivedType, } from './lib/markers/derived';
4
7
  export { equal, deepEqual, isNodeAccessor, isAnySignal, toWritableSignal, parsePath, composeEnhancers, isBuiltInObject, createLazySignalTree, } from './lib/utils';
5
8
  export { createEditSession, type EditSession, type UndoRedoHistory, } from './lib/edit-session';
6
9
  export { getPathNotifier } from './lib/path-notifier';
@@ -0,0 +1,13 @@
1
+ import type { ProcessDerived } from './derived-types';
2
+ import type { ISignalTree, TreeNode } from '../types';
3
+ export interface SignalTreeBuilder<TSource, TAccum = TreeNode<TSource>> {
4
+ (): TSource;
5
+ (value: TSource): void;
6
+ (updater: (current: TSource) => TSource): void;
7
+ readonly $: TAccum;
8
+ readonly state: TAccum;
9
+ with<TAdded>(enhancer: (tree: ISignalTree<TSource>) => ISignalTree<TSource> & TAdded): SignalTreeBuilder<TSource, TAccum> & TAdded;
10
+ bind(thisArg?: unknown): (value?: TSource) => TSource | void;
11
+ destroy(): void;
12
+ derived<TDerived extends object>(factory: ($: TAccum) => TDerived): SignalTreeBuilder<TSource, TAccum & ProcessDerived<TDerived>>;
13
+ }
@@ -0,0 +1,10 @@
1
+ import { Signal } from '@angular/core';
2
+ import type { DerivedMarker } from '../markers/derived';
3
+ import type { TreeNode } from '../types';
4
+ export type ProcessDerived<T> = T extends DerivedMarker<infer R> ? Signal<R> : T extends Signal<infer S> ? Signal<S> : T extends object ? {
5
+ [P in keyof T]: ProcessDerived<T[P]>;
6
+ } : never;
7
+ export type DeepMergeTree<TSource, TDerived> = {
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
+ };
10
+ export type DerivedFactory<TSource, TDerived> = ($: TreeNode<TSource>) => TDerived;
@@ -0,0 +1,4 @@
1
+ type AnyRecord = Record<string, unknown>;
2
+ export declare function mergeDerivedState($: AnyRecord, derivedDef: unknown, path?: string): void;
3
+ export declare function applyDerivedFactories($: AnyRecord, factories: Array<($: AnyRecord) => object>): void;
4
+ export {};
@@ -0,0 +1,10 @@
1
+ declare const DERIVED_MARKER: unique symbol;
2
+ export interface DerivedMarker<T> {
3
+ readonly [DERIVED_MARKER]: true;
4
+ readonly factory: () => T;
5
+ readonly __type?: T;
6
+ }
7
+ export type DerivedType<T> = T extends DerivedMarker<infer R> ? R : never;
8
+ export declare function derived<T>(factory: () => T): DerivedMarker<T>;
9
+ export declare function isDerivedMarker(value: unknown): value is DerivedMarker<unknown>;
10
+ export {};
@@ -0,0 +1 @@
1
+ export { derived, isDerivedMarker, getDerivedMarkerSymbol, type DerivedMarker, type DerivedType, } from './derived';
@@ -1,3 +1,6 @@
1
- import type { TreeConfig, NodeAccessor, ISignalTree } from './types';
1
+ import { SignalTreeBuilder } from './internals/builder-types';
2
+ import { ProcessDerived } from './internals/derived-types';
3
+ import type { TreeNode, TreeConfig, NodeAccessor } from './types';
2
4
  export declare function isNodeAccessor(value: unknown): value is NodeAccessor<unknown>;
3
- export declare function signalTree<T extends object>(initialState: T, config?: TreeConfig): ISignalTree<T>;
5
+ export declare function signalTree<T extends object, TDerived extends object>(initialState: T, derivedFactory: ($: TreeNode<T>) => TDerived): SignalTreeBuilder<T, TreeNode<T> & ProcessDerived<TDerived>>;
6
+ export declare function signalTree<T extends object>(initialState: T, config?: TreeConfig): SignalTreeBuilder<T, TreeNode<T>>;