@signaltree/core 1.1.1 → 1.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.
@@ -1,45 +1,47 @@
1
1
  import { signal, isSignal, computed, effect, inject, DestroyRef } from '@angular/core';
2
2
 
3
- /**
4
- * SignalTree Utility Functions - Recursive Typing Implementation
5
- *
6
- * COPYRIGHT NOTICE:
7
- * This file contains proprietary utility functions for the recursive typing system.
8
- * The createLazySignalTree function and built-in object detection methods are
9
- * protected intellectual property of Jonathan D Borgia.
10
- *
11
- * Licensed under Fair Source License - see LICENSE file for complete terms.
12
- */
13
- /**
14
- * Enhanced equality function inspired by the monolithic implementation.
15
- * Uses deep equality for arrays and objects, === for primitives.
16
- * Optimized with early exits and type-specific comparisons.
17
- */
18
3
  function equal(a, b) {
19
- // Fast path for reference equality
20
4
  if (a === b)
21
5
  return true;
22
- // Handle null/undefined cases
23
6
  if (a == null || b == null)
24
7
  return a === b;
25
- // Type check first - most efficient early exit
26
8
  const typeA = typeof a;
27
9
  const typeB = typeof b;
28
10
  if (typeA !== typeB)
29
11
  return false;
30
- // For primitives, === check above is sufficient
31
12
  if (typeA !== 'object')
32
13
  return false;
33
- // Handle arrays with optimized comparison
14
+ if (a instanceof Date && b instanceof Date) {
15
+ return a.getTime() === b.getTime();
16
+ }
17
+ if (a instanceof RegExp && b instanceof RegExp) {
18
+ return a.source === b.source && a.flags === b.flags;
19
+ }
20
+ if (a instanceof Map && b instanceof Map) {
21
+ if (a.size !== b.size)
22
+ return false;
23
+ for (const [key, value] of a) {
24
+ if (!b.has(key) || !equal(value, b.get(key)))
25
+ return false;
26
+ }
27
+ return true;
28
+ }
29
+ if (a instanceof Set && b instanceof Set) {
30
+ if (a.size !== b.size)
31
+ return false;
32
+ for (const value of a) {
33
+ if (!b.has(value))
34
+ return false;
35
+ }
36
+ return true;
37
+ }
34
38
  if (Array.isArray(a)) {
35
39
  if (!Array.isArray(b) || a.length !== b.length)
36
40
  return false;
37
41
  return a.every((item, index) => equal(item, b[index]));
38
42
  }
39
- // Arrays check above handles array vs object mismatch
40
43
  if (Array.isArray(b))
41
44
  return false;
42
- // Handle objects with optimized comparison
43
45
  const objA = a;
44
46
  const objB = b;
45
47
  const keysA = Object.keys(objA);
@@ -48,21 +50,11 @@ function equal(a, b) {
48
50
  return false;
49
51
  return keysA.every((key) => key in objB && equal(objA[key], objB[key]));
50
52
  }
51
- /**
52
- * Creates a terminal signal with the enhanced equality function.
53
- * This should be used instead of Angular's signal() when you want
54
- * the same deep equality behavior as signalTree.
55
- *
56
- * Inspired by the monolithic implementation's terminal signal creation.
57
- */
58
53
  function terminalSignal(value, customEqual) {
59
54
  return signal(value, {
60
55
  equal: customEqual || equal,
61
56
  });
62
57
  }
63
- /**
64
- * LRU Cache implementation for efficient memory management
65
- */
66
58
  class LRUCache {
67
59
  maxSize;
68
60
  cache = new Map();
@@ -76,14 +68,12 @@ class LRUCache {
76
68
  this.cache.delete(firstKey);
77
69
  }
78
70
  }
79
- // Remove if exists to update position
80
71
  this.cache.delete(key);
81
- this.cache.set(key, value); // Add to end (most recent)
72
+ this.cache.set(key, value);
82
73
  }
83
74
  get(key) {
84
75
  const value = this.cache.get(key);
85
76
  if (value !== undefined) {
86
- // Move to end (mark as recently used)
87
77
  this.cache.delete(key);
88
78
  this.cache.set(key, value);
89
79
  }
@@ -96,20 +86,8 @@ class LRUCache {
96
86
  return this.cache.size;
97
87
  }
98
88
  }
99
- // Path parsing cache with proper LRU eviction strategy
100
89
  const MAX_CACHE_SIZE = 1000;
101
90
  const pathCache = new LRUCache(MAX_CACHE_SIZE);
102
- /**
103
- * Parses a dot-notation path into an array of keys with LRU memoization.
104
- * Critical for performance when accessing nested properties frequently.
105
- * Includes proper LRU cache management to prevent memory leaks.
106
- *
107
- * @example
108
- * ```typescript
109
- * const keys1 = parsePath('user.name'); // Splits and caches
110
- * const keys2 = parsePath('user.name'); // Returns cached result
111
- * ```
112
- */
113
91
  function parsePath(path) {
114
92
  const cached = pathCache.get(path);
115
93
  if (cached) {
@@ -119,26 +97,11 @@ function parsePath(path) {
119
97
  pathCache.set(path, parts);
120
98
  return parts;
121
99
  }
122
- /**
123
- * Creates a lazy signal tree using Proxy for on-demand signal creation.
124
- * Only creates signals when properties are first accessed, providing
125
- * massive memory savings for large state objects.
126
- * Uses WeakMap for memory-safe caching.
127
- *
128
- * @param obj - Source object to lazily signalify
129
- * @param equalityFn - Equality function for signal comparison
130
- * @param basePath - Base path for nested objects (internal use)
131
- * @returns Proxied object that creates signals on first access
132
- */
133
100
  function createLazySignalTree(obj, equalityFn, basePath = '') {
134
- // Use Map instead of WeakMap for better control over cleanup
135
101
  const signalCache = new Map();
136
102
  const nestedProxies = new Map();
137
- // Track cleanup functions for nested proxies
138
103
  const nestedCleanups = new Map();
139
- // Enhanced cleanup function
140
104
  const cleanup = () => {
141
- // Clean up all nested proxies first
142
105
  nestedCleanups.forEach((cleanupFn) => {
143
106
  try {
144
107
  cleanupFn();
@@ -148,11 +111,9 @@ function createLazySignalTree(obj, equalityFn, basePath = '') {
148
111
  }
149
112
  });
150
113
  nestedCleanups.clear();
151
- // Clear caches
152
114
  signalCache.clear();
153
115
  nestedProxies.clear();
154
116
  };
155
- // Enhanced built-in object detection
156
117
  const isBuiltInObject = (v) => {
157
118
  if (v === null || v === undefined)
158
119
  return false;
@@ -175,38 +136,30 @@ function createLazySignalTree(obj, equalityFn, basePath = '') {
175
136
  };
176
137
  const proxy = new Proxy(obj, {
177
138
  get(target, prop) {
178
- // Handle cleanup method
179
139
  if (prop === '__cleanup__') {
180
140
  return cleanup;
181
141
  }
182
- // Handle symbol properties (like Symbol.iterator) normally
183
142
  if (typeof prop === 'symbol') {
184
143
  return target[prop];
185
144
  }
186
- // Handle inspection methods
187
145
  if (prop === 'valueOf' || prop === 'toString') {
188
146
  return target[prop];
189
147
  }
190
148
  const key = prop;
191
149
  const path = basePath ? `${basePath}.${key}` : key;
192
- // Safety check for property existence
193
150
  if (!(key in target)) {
194
151
  return undefined;
195
152
  }
196
153
  const value = target[key];
197
- // If it's already a signal, return it
198
154
  if (isSignal(value)) {
199
155
  return value;
200
156
  }
201
- // Check if we already have a signal for this path
202
157
  if (signalCache.has(path)) {
203
158
  return signalCache.get(path);
204
159
  }
205
- // Check if we have a nested proxy cached
206
160
  if (nestedProxies.has(path)) {
207
161
  return nestedProxies.get(path);
208
162
  }
209
- // Handle nested objects - create lazy proxy
210
163
  if (value &&
211
164
  typeof value === 'object' &&
212
165
  !Array.isArray(value) &&
@@ -215,7 +168,6 @@ function createLazySignalTree(obj, equalityFn, basePath = '') {
215
168
  try {
216
169
  const nestedProxy = createLazySignalTree(value, equalityFn, path);
217
170
  nestedProxies.set(path, nestedProxy);
218
- // Store cleanup function for nested proxy
219
171
  const proxyWithCleanup = nestedProxy;
220
172
  if (typeof proxyWithCleanup.__cleanup__ === 'function') {
221
173
  nestedCleanups.set(path, proxyWithCleanup.__cleanup__);
@@ -224,13 +176,11 @@ function createLazySignalTree(obj, equalityFn, basePath = '') {
224
176
  }
225
177
  catch (error) {
226
178
  console.warn(`Failed to create lazy proxy for path "${path}":`, error);
227
- // Fallback: create a signal for the object
228
179
  const fallbackSignal = signal(value, ...(ngDevMode ? [{ debugName: "fallbackSignal", equal: equalityFn }] : [{ equal: equalityFn }]));
229
180
  signalCache.set(path, fallbackSignal);
230
181
  return fallbackSignal;
231
182
  }
232
183
  }
233
- // Create signal for primitive values, arrays, and built-in objects
234
184
  try {
235
185
  const newSignal = signal(value, ...(ngDevMode ? [{ debugName: "newSignal", equal: equalityFn }] : [{ equal: equalityFn }]));
236
186
  signalCache.set(path, newSignal);
@@ -238,7 +188,7 @@ function createLazySignalTree(obj, equalityFn, basePath = '') {
238
188
  }
239
189
  catch (error) {
240
190
  console.warn(`Failed to create signal for path "${path}":`, error);
241
- return value; // Return raw value as fallback
191
+ return value;
242
192
  }
243
193
  },
244
194
  set(target, prop, value) {
@@ -249,16 +199,12 @@ function createLazySignalTree(obj, equalityFn, basePath = '') {
249
199
  const key = prop;
250
200
  const path = basePath ? `${basePath}.${key}` : key;
251
201
  try {
252
- // Update the original object
253
202
  target[key] = value;
254
- // If we have a cached signal, update it
255
203
  const cachedSignal = signalCache.get(path);
256
204
  if (cachedSignal && 'set' in cachedSignal) {
257
205
  cachedSignal.set(value);
258
206
  }
259
- // Clear nested proxy cache if the value type changed
260
207
  if (nestedProxies.has(path)) {
261
- // Clean up the nested proxy
262
208
  const nestedCleanup = nestedCleanups.get(path);
263
209
  if (nestedCleanup) {
264
210
  nestedCleanup();
@@ -285,61 +231,7 @@ function createLazySignalTree(obj, equalityFn, basePath = '') {
285
231
  });
286
232
  return proxy;
287
233
  }
288
- /**
289
- * Native deep equality check for arrays and objects.
290
- * Handles all common cases that lodash.isEqual handles for our use cases.
291
- */
292
- function deepEqual(a, b) {
293
- // Same reference or primitives
294
- if (a === b)
295
- return true;
296
- // Handle null/undefined
297
- if (a == null || b == null)
298
- return false;
299
- // Different types
300
- if (typeof a !== typeof b)
301
- return false;
302
- // Handle dates
303
- if (a instanceof Date && b instanceof Date) {
304
- return a.getTime() === b.getTime();
305
- }
306
- // Handle arrays
307
- if (Array.isArray(a) && Array.isArray(b)) {
308
- if (a.length !== b.length)
309
- return false;
310
- for (let i = 0; i < a.length; i++) {
311
- if (!deepEqual(a[i], b[i]))
312
- return false;
313
- }
314
- return true;
315
- }
316
- // Handle objects (but not arrays, dates, or other special objects)
317
- if (typeof a === 'object' &&
318
- typeof b === 'object' &&
319
- !Array.isArray(a) &&
320
- !Array.isArray(b) &&
321
- !(a instanceof Date) &&
322
- !(b instanceof Date)) {
323
- const objA = a;
324
- const objB = b;
325
- const keysA = Object.keys(objA);
326
- const keysB = Object.keys(objB);
327
- if (keysA.length !== keysB.length)
328
- return false;
329
- for (const key of keysA) {
330
- if (!(key in objB))
331
- return false;
332
- if (!deepEqual(objA[key], objB[key]))
333
- return false;
334
- }
335
- return true;
336
- }
337
- // For all other cases (primitives that aren't equal)
338
- return false;
339
- }
340
- /**
341
- * Shallow equality check for objects and arrays.
342
- */
234
+ const deepEqual = equal;
343
235
  function shallowEqual(a, b) {
344
236
  if (a === b)
345
237
  return true;
@@ -369,281 +261,274 @@ function shallowEqual(a, b) {
369
261
  return false;
370
262
  }
371
263
 
372
- /**
373
- * SignalTree Core Implementation - Recursive Typing Engine
374
- *
375
- * COPYRIGHT NOTICE:
376
- * This file contains the proprietary recursive typing implementation protected
377
- * under the SignalTree license. The signal-store pattern, recursive type-runtime
378
- * alignment, and "initiation defines structure" paradigm are exclusive intellectual
379
- * property of Jonathan D Borgia.
380
- *
381
- * The createSignalStore and createLazySignalTree functions implement patented
382
- * approaches to recursive type preservation that are strictly protected.
383
- *
384
- * Licensed under Fair Source License - see LICENSE file for complete terms.
385
- */
386
- /**
387
- * Creates an equality function based on configuration.
388
- *
389
- * @param useShallowComparison - If true, uses Object.is for comparison; otherwise uses deep equality
390
- * @returns A function that compares two values for equality
391
- *
392
- * @example
393
- * ```typescript
394
- * const shallowEqual = createEqualityFn(true);
395
- * const deepEqual = createEqualityFn(false);
396
- *
397
- * shallowEqual({ a: 1 }, { a: 1 }); // false (different objects)
398
- * deepEqual({ a: 1 }, { a: 1 }); // true (same structure and values)
399
- * ```
400
- */
264
+ function estimateObjectSize(obj, maxDepth = 3, currentDepth = 0) {
265
+ if (currentDepth >= maxDepth)
266
+ return 1;
267
+ if (obj === null || obj === undefined)
268
+ return 0;
269
+ if (typeof obj !== 'object')
270
+ return 1;
271
+ let size = 0;
272
+ try {
273
+ if (Array.isArray(obj)) {
274
+ size = obj.length;
275
+ const sampleSize = Math.min(3, obj.length);
276
+ for (let i = 0; i < sampleSize; i++) {
277
+ size += estimateObjectSize(obj[i], maxDepth, currentDepth + 1) * 0.1;
278
+ }
279
+ }
280
+ else {
281
+ const keys = Object.keys(obj);
282
+ size = keys.length;
283
+ const sampleSize = Math.min(5, keys.length);
284
+ for (let i = 0; i < sampleSize; i++) {
285
+ const value = obj[keys[i]];
286
+ size += estimateObjectSize(value, maxDepth, currentDepth + 1) * 0.5;
287
+ }
288
+ }
289
+ }
290
+ catch {
291
+ return 1;
292
+ }
293
+ return Math.floor(size);
294
+ }
295
+ function shouldUseLazy(obj, config) {
296
+ if (config.useLazySignals !== undefined) {
297
+ return config.useLazySignals;
298
+ }
299
+ if (config.debugMode || config.enableDevTools) {
300
+ return false;
301
+ }
302
+ if (config.batchUpdates && config.useMemoization) {
303
+ return true;
304
+ }
305
+ const estimatedSize = estimateObjectSize(obj);
306
+ return estimatedSize > 50;
307
+ }
401
308
  function createEqualityFn(useShallowComparison) {
402
309
  return useShallowComparison ? Object.is : equal;
403
310
  }
404
- /**
405
- * Core function to create a basic SignalTree with enhanced safety.
406
- * This provides the minimal functionality without advanced features.
407
- *
408
- * CRITICAL: Uses flexible typing - accepts ANY type T, not T
409
- *
410
- * @template T - The state object type (NO constraints - maximum flexibility)
411
- * @param obj - The initial state object
412
- * @param config - Configuration options for the tree
413
- * @returns A basic SignalTree with core functionality
414
- *
415
- * @example
416
- * ```typescript
417
- * const tree = create({
418
- * count: 0,
419
- * user: { name: 'John', age: 30 },
420
- * items: [1, 2, 3],
421
- * metadata: new Map(), // Any object type!
422
- * fn: () => 'hello' // Even functions!
423
- * }, {
424
- * useLazySignals: true,
425
- * useShallowComparison: false
426
- * });
427
- *
428
- * // Access nested signals
429
- * console.log(tree.$.count()); // 0
430
- * tree.$.user.name.set('Jane');
431
- * ```
432
- */
311
+ function isBuiltInObject(v) {
312
+ if (v === null || v === undefined)
313
+ return false;
314
+ if (v instanceof Date ||
315
+ v instanceof RegExp ||
316
+ typeof v === 'function' ||
317
+ v instanceof Map ||
318
+ v instanceof Set ||
319
+ v instanceof WeakMap ||
320
+ v instanceof WeakSet ||
321
+ v instanceof ArrayBuffer ||
322
+ v instanceof DataView ||
323
+ v instanceof Error ||
324
+ v instanceof Promise) {
325
+ return true;
326
+ }
327
+ if (v instanceof Int8Array ||
328
+ v instanceof Uint8Array ||
329
+ v instanceof Uint8ClampedArray ||
330
+ v instanceof Int16Array ||
331
+ v instanceof Uint16Array ||
332
+ v instanceof Int32Array ||
333
+ v instanceof Uint32Array ||
334
+ v instanceof Float32Array ||
335
+ v instanceof Float64Array ||
336
+ v instanceof BigInt64Array ||
337
+ v instanceof BigUint64Array) {
338
+ return true;
339
+ }
340
+ if (typeof window !== 'undefined') {
341
+ if (v instanceof URL ||
342
+ v instanceof URLSearchParams ||
343
+ v instanceof FormData ||
344
+ v instanceof Blob ||
345
+ (typeof File !== 'undefined' && v instanceof File) ||
346
+ (typeof FileList !== 'undefined' && v instanceof FileList) ||
347
+ (typeof Headers !== 'undefined' && v instanceof Headers) ||
348
+ (typeof Request !== 'undefined' && v instanceof Request) ||
349
+ (typeof Response !== 'undefined' && v instanceof Response) ||
350
+ (typeof AbortController !== 'undefined' &&
351
+ v instanceof AbortController) ||
352
+ (typeof AbortSignal !== 'undefined' && v instanceof AbortSignal)) {
353
+ return true;
354
+ }
355
+ }
356
+ try {
357
+ const NodeBuffer = globalThis?.Buffer;
358
+ if (NodeBuffer && v instanceof NodeBuffer) {
359
+ return true;
360
+ }
361
+ }
362
+ catch {
363
+ }
364
+ return false;
365
+ }
433
366
  function create(obj, config = {}) {
367
+ if (obj === null || obj === undefined) {
368
+ throw new Error('Cannot create SignalTree from null or undefined');
369
+ }
434
370
  const equalityFn = createEqualityFn(config.useShallowComparison ?? false);
435
- const useLazy = config.useLazySignals ?? true;
436
- // Create signals using signal-store pattern for perfect type inference
437
- const signalState = useLazy
438
- ? createLazySignalTree(obj, equalityFn)
439
- : createSignalStore(obj, equalityFn);
371
+ const useLazy = shouldUseLazy(obj, config);
372
+ if (config.debugMode) {
373
+ const estimatedSize = estimateObjectSize(obj);
374
+ console.log(`[SignalTree] Creating tree with ${useLazy ? 'lazy' : 'eager'} strategy (estimated size: ${estimatedSize})`);
375
+ }
376
+ let signalState;
377
+ try {
378
+ if (useLazy && typeof obj === 'object') {
379
+ signalState = createLazySignalTree(obj, equalityFn);
380
+ }
381
+ else {
382
+ signalState = createSignalStore(obj, equalityFn);
383
+ }
384
+ }
385
+ catch (error) {
386
+ if (useLazy) {
387
+ console.warn('[SignalTree] Lazy creation failed, falling back to eager:', error);
388
+ signalState = createSignalStore(obj, equalityFn);
389
+ }
390
+ else {
391
+ throw error;
392
+ }
393
+ }
440
394
  const resultTree = {
441
395
  state: signalState,
442
- $: signalState, // $ points to the same state object
396
+ $: signalState,
443
397
  };
444
- enhanceTree(resultTree);
398
+ enhanceTree(resultTree, config);
445
399
  return resultTree;
446
400
  }
447
- /**
448
- * Creates signals using signal-store pattern for perfect type inference.
449
- * This is the key function that preserves exact type relationships recursively.
450
- * Based on the original signal-store pattern that maintains type information.
451
- *
452
- * @template T - The object type to process (preserves exact type structure)
453
- * @param obj - The object to convert to signals
454
- * @param equalityFn - Function to compare values for equality
455
- * @returns A deeply signalified version maintaining exact type structure
456
- *
457
- * @example
458
- * ```typescript
459
- * const store = createSignalStore({
460
- * user: { name: 'John', age: 30 },
461
- * settings: { theme: 'dark' }
462
- * }, Object.is);
463
- *
464
- * // Perfect type inference maintained throughout
465
- * store.user.name.set('Jane');
466
- * console.log(store.settings.theme()); // 'dark'
467
- * ```
468
- */
469
401
  function createSignalStore(obj, equalityFn) {
470
- // Input validation
471
- if (obj === null || obj === undefined) {
472
- throw new Error('Cannot create signal store from null or undefined');
402
+ if (obj === null || obj === undefined || typeof obj !== 'object') {
403
+ return signal(obj, { equal: equalityFn });
404
+ }
405
+ if (Array.isArray(obj)) {
406
+ return signal(obj, { equal: equalityFn });
473
407
  }
474
- if (typeof obj !== 'object') {
475
- // For primitives, just return a signal
408
+ if (isBuiltInObject(obj)) {
476
409
  return signal(obj, { equal: equalityFn });
477
410
  }
478
411
  const store = {};
479
- // Enhanced built-in object detection
480
- const isBuiltInObject = (v) => {
481
- if (v === null || v === undefined)
482
- return false;
483
- return (v instanceof Date ||
484
- v instanceof RegExp ||
485
- typeof v === 'function' ||
486
- v instanceof Map ||
487
- v instanceof Set ||
488
- v instanceof WeakMap ||
489
- v instanceof WeakSet ||
490
- v instanceof ArrayBuffer ||
491
- v instanceof DataView ||
492
- v instanceof Error ||
493
- v instanceof Promise ||
494
- v instanceof URL ||
495
- v instanceof URLSearchParams ||
496
- v instanceof FormData ||
497
- v instanceof Blob ||
498
- (typeof File !== 'undefined' && v instanceof File));
499
- };
500
- // Safe object iteration
412
+ const processedObjects = new WeakSet();
413
+ if (processedObjects.has(obj)) {
414
+ console.warn('[SignalTree] Circular reference detected, creating reference signal');
415
+ return signal(obj, { equal: equalityFn });
416
+ }
417
+ processedObjects.add(obj);
501
418
  try {
502
419
  for (const [key, value] of Object.entries(obj)) {
503
- const isObj = (v) => typeof v === 'object' &&
504
- v !== null &&
505
- !Array.isArray(v) &&
506
- !isBuiltInObject(v);
507
- // Enhanced safety: Never double-wrap signals
508
- if (isSignal(value)) {
509
- store[key] = value;
510
- }
511
- else if (isObj(value)) {
512
- // CRITICAL: Recursive call with type preservation
513
- try {
514
- store[key] = createSignalStore(value, equalityFn);
420
+ try {
421
+ if (typeof key === 'symbol')
422
+ continue;
423
+ if (isSignal(value)) {
424
+ store[key] = value;
425
+ continue;
515
426
  }
516
- catch (error) {
517
- console.warn(`Failed to create signal store for key "${key}":`, error);
518
- // Fallback: treat as primitive
427
+ if (value === null || value === undefined) {
428
+ store[key] = signal(value, {
429
+ equal: equalityFn,
430
+ });
431
+ }
432
+ else if (typeof value !== 'object') {
433
+ store[key] = signal(value, {
434
+ equal: equalityFn,
435
+ });
436
+ }
437
+ else if (Array.isArray(value) || isBuiltInObject(value)) {
519
438
  store[key] = signal(value, {
520
439
  equal: equalityFn,
521
440
  });
522
441
  }
442
+ else {
443
+ store[key] = createSignalStore(value, equalityFn);
444
+ }
523
445
  }
524
- else {
525
- // Create signal for primitives, arrays, and built-in objects
446
+ catch (error) {
447
+ console.warn(`[SignalTree] Failed to create signal for key "${key}":`, error);
526
448
  store[key] = signal(value, {
527
449
  equal: equalityFn,
528
450
  });
529
451
  }
530
452
  }
453
+ const symbols = Object.getOwnPropertySymbols(obj);
454
+ for (const sym of symbols) {
455
+ const value = obj[sym];
456
+ try {
457
+ if (isSignal(value)) {
458
+ store[sym] = value;
459
+ }
460
+ else {
461
+ store[sym] = signal(value, {
462
+ equal: equalityFn,
463
+ });
464
+ }
465
+ }
466
+ catch (error) {
467
+ console.warn(`[SignalTree] Failed to create signal for symbol key:`, error);
468
+ }
469
+ }
531
470
  }
532
471
  catch (error) {
533
472
  throw new Error(`Failed to create signal store: ${error instanceof Error ? error.message : 'Unknown error'}`);
534
473
  }
535
474
  return store;
536
475
  }
537
- /**
538
- * Enhances a tree with basic functionality (unwrap, update, pipe).
539
- * Adds core methods that every SignalTree needs for basic operation.
540
- *
541
- * @template T - The state object type
542
- * @param tree - The tree to enhance with basic functionality
543
- * @returns The enhanced tree with unwrap, update, and pipe methods
544
- *
545
- * @example
546
- * ```typescript
547
- * const basicTree = { state: signalState, $: signalState };
548
- * enhanceTree(basicTree);
549
- *
550
- * // Now has basic methods:
551
- * const currentState = basicTree.unwrap();
552
- * basicTree.update(state => ({ ...state, count: state.count + 1 }));
553
- * const enhancedTree = basicTree.pipe(withSomeFeature());
554
- * ```
555
- */
556
- function enhanceTree(tree) {
557
- /**
558
- * Unwraps the current state by reading all signal values.
559
- * Recursively converts the signal tree back to plain JavaScript values.
560
- *
561
- * @returns The current state as a plain object
562
- *
563
- * @example
564
- * ```typescript
565
- * const tree = signalTree({
566
- * user: { name: 'John', age: 30 },
567
- * count: 0
568
- * });
569
- *
570
- * tree.$.user.name.set('Jane');
571
- * tree.$.count.set(5);
572
- *
573
- * const currentState = tree.unwrap();
574
- * // { user: { name: 'Jane', age: 30 }, count: 5 }
575
- * ```
576
- */
476
+ function enhanceTree(tree, config = {}) {
477
+ const isLazy = config.useLazySignals ?? shouldUseLazy(tree.state, config);
577
478
  tree.unwrap = () => {
578
- // Recursively unwrap with proper typing
579
479
  const unwrapObject = (obj) => {
580
480
  if (typeof obj !== 'object' || obj === null) {
581
481
  return obj;
582
482
  }
483
+ if (isSignal(obj)) {
484
+ return obj();
485
+ }
486
+ if (isSignal(obj)) {
487
+ const value = obj();
488
+ if (Array.isArray(value)) {
489
+ return value;
490
+ }
491
+ }
583
492
  const result = {};
584
493
  for (const key in obj) {
494
+ if (!Object.prototype.hasOwnProperty.call(obj, key))
495
+ continue;
585
496
  const value = obj[key];
586
497
  if (isSignal(value)) {
587
498
  result[key] = value();
588
499
  }
589
500
  else if (typeof value === 'object' &&
590
501
  value !== null &&
591
- !Array.isArray(value)) {
592
- // Nested signal state
502
+ !Array.isArray(value) &&
503
+ !isBuiltInObject(value)) {
593
504
  result[key] = unwrapObject(value);
594
505
  }
595
506
  else {
596
507
  result[key] = value;
597
508
  }
598
509
  }
510
+ const symbols = Object.getOwnPropertySymbols(obj);
511
+ for (const sym of symbols) {
512
+ const value = obj[sym];
513
+ if (isSignal(value)) {
514
+ result[sym] = value();
515
+ }
516
+ else {
517
+ result[sym] = value;
518
+ }
519
+ }
599
520
  return result;
600
521
  };
601
522
  return unwrapObject(tree.state);
602
523
  };
603
- /**
604
- * Updates the state using an updater function.
605
- * The updater receives the current state and returns a partial update.
606
- * Automatically handles nested signal updates.
607
- *
608
- * @param updater - Function that receives current state and returns partial updates
609
- *
610
- * @example
611
- * ```typescript
612
- * const tree = signalTree({
613
- * user: { name: 'John', age: 30 },
614
- * count: 0,
615
- * todos: []
616
- * });
617
- *
618
- * // Simple update
619
- * tree.update(state => ({ count: state.count + 1 }));
620
- *
621
- * // Nested update
622
- * tree.update(state => ({
623
- * user: { ...state.user, age: state.user.age + 1 },
624
- * todos: [...state.todos, { id: 1, text: 'New todo' }]
625
- * }));
626
- *
627
- * // Conditional update
628
- * tree.update(state =>
629
- * state.count < 10
630
- * ? { count: state.count + 1 }
631
- * : { count: 0, user: { ...state.user, name: 'Reset' } }
632
- * );
633
- * ```
634
- */
635
- /**
636
- * Enhanced update with error handling and rollback capability
637
- */
638
524
  tree.update = (updater) => {
639
- const originalState = new Map();
525
+ const transactionLog = [];
640
526
  try {
641
527
  const currentValue = tree.unwrap();
642
528
  const partialObj = updater(currentValue);
643
529
  if (!partialObj || typeof partialObj !== 'object') {
644
530
  throw new Error('Updater must return an object');
645
531
  }
646
- // Recursively update with better error handling and rollback
647
532
  const updateObject = (target, updates, path = '') => {
648
533
  for (const key in updates) {
649
534
  if (!Object.prototype.hasOwnProperty.call(updates, key))
@@ -653,39 +538,42 @@ function enhanceTree(tree) {
653
538
  const currentSignalOrState = target[key];
654
539
  try {
655
540
  if (isSignal(currentSignalOrState)) {
656
- // Store original value for potential rollback
657
541
  const originalValue = currentSignalOrState();
658
- originalState.set(currentPath, originalValue);
659
- // Direct signal update
542
+ transactionLog.push({
543
+ path: currentPath,
544
+ oldValue: originalValue,
545
+ newValue: updateValue,
546
+ });
660
547
  currentSignalOrState.set(updateValue);
661
548
  }
662
549
  else if (typeof updateValue === 'object' &&
663
550
  updateValue !== null &&
664
551
  !Array.isArray(updateValue) &&
552
+ !isBuiltInObject(updateValue) &&
665
553
  typeof currentSignalOrState === 'object' &&
666
554
  currentSignalOrState !== null &&
667
555
  !isSignal(currentSignalOrState)) {
668
- // Nested object - recurse
669
556
  updateObject(currentSignalOrState, updateValue, currentPath);
670
557
  }
671
558
  else if (currentSignalOrState === undefined) {
672
- console.warn(`Cannot update non-existent path: ${currentPath}`);
559
+ console.warn(`[SignalTree] Cannot update non-existent path: ${currentPath}`);
673
560
  }
674
561
  }
675
562
  catch (error) {
676
- console.error(`Failed to update path "${currentPath}":`, error);
677
- // Continue with other updates rather than failing completely
563
+ console.error(`[SignalTree] Failed to update path "${currentPath}":`, error);
564
+ throw error;
678
565
  }
679
566
  }
680
567
  };
681
568
  updateObject(tree.state, partialObj);
569
+ if (config.debugMode && transactionLog.length > 0) {
570
+ console.log('[SignalTree] Update transaction:', transactionLog);
571
+ }
682
572
  }
683
573
  catch (error) {
684
- // Rollback on error
685
- console.error('Update failed, attempting rollback:', error);
686
- try {
687
- // Attempt to restore original values
688
- for (const [path, originalValue] of originalState.entries()) {
574
+ console.error('[SignalTree] Update failed, attempting rollback:', error);
575
+ for (const { path, oldValue } of transactionLog.reverse()) {
576
+ try {
689
577
  const pathParts = path.split('.');
690
578
  let current = tree.state;
691
579
  for (let i = 0; i < pathParts.length - 1; i++) {
@@ -693,42 +581,68 @@ function enhanceTree(tree) {
693
581
  if (!current)
694
582
  break;
695
583
  }
696
- if (current &&
697
- isSignal(current[pathParts[pathParts.length - 1]])) {
698
- const signal = current[pathParts[pathParts.length - 1]];
699
- signal.set(originalValue);
584
+ if (current) {
585
+ const lastKey = pathParts[pathParts.length - 1];
586
+ const targetSignal = current[lastKey];
587
+ if (isSignal(targetSignal)) {
588
+ targetSignal.set(oldValue);
589
+ }
700
590
  }
701
591
  }
702
- }
703
- catch (rollbackError) {
704
- console.error('Rollback failed:', rollbackError);
592
+ catch (rollbackError) {
593
+ console.error('[SignalTree] Rollback failed for path:', path, rollbackError);
594
+ }
705
595
  }
706
596
  throw error;
707
597
  }
708
598
  };
709
- // Pipe implementation for function composition with improved type safety
710
599
  tree.pipe = ((...fns) => {
711
600
  if (fns.length === 0) {
712
601
  return tree;
713
602
  }
714
- // Type-safe reduce with proper function composition
715
603
  return fns.reduce((acc, fn) => {
716
604
  if (typeof fn !== 'function') {
717
605
  throw new Error('All pipe arguments must be functions');
718
606
  }
719
- return fn(acc);
607
+ try {
608
+ return fn(acc);
609
+ }
610
+ catch (error) {
611
+ console.error('[SignalTree] Pipe function failed:', error);
612
+ throw error;
613
+ }
720
614
  }, tree);
721
615
  });
722
- // Stub implementations for advanced features (will log warnings)
616
+ tree.destroy = () => {
617
+ try {
618
+ if (isLazy) {
619
+ const state = tree.state;
620
+ if (state && typeof state === 'object' && '__cleanup__' in state) {
621
+ const cleanup = state.__cleanup__;
622
+ if (typeof cleanup === 'function') {
623
+ cleanup();
624
+ }
625
+ }
626
+ }
627
+ if (config.debugMode) {
628
+ console.log('[SignalTree] Tree destroyed');
629
+ }
630
+ }
631
+ catch (error) {
632
+ console.error('[SignalTree] Error during cleanup:', error);
633
+ }
634
+ };
635
+ addStubMethods(tree, config);
636
+ return tree;
637
+ }
638
+ function addStubMethods(tree, config) {
723
639
  tree.batchUpdate = (updater) => {
724
- console.warn('⚠️ batchUpdate() called but batching is not enabled.', '\nTo enable batch updates, install @signaltree/batching');
725
- // Fallback: Just call update directly
640
+ console.warn('⚠️ batchUpdate() called but batching is not enabled.', 'To enable batch updates, install @signaltree/batching');
726
641
  tree.update(updater);
727
642
  };
728
643
  tree.memoize = (fn, cacheKey) => {
729
- console.warn('⚠️ memoize() called but memoization is not enabled.', '\nTo enable memoized computations, install @signaltree/memoization');
730
- // Fallback: Use simple Angular computed without memoization
731
- void cacheKey; // Mark as intentionally unused
644
+ console.warn('⚠️ memoize() called but memoization is not enabled.', 'To enable memoized computations, install @signaltree/memoization');
645
+ void cacheKey;
732
646
  return computed(() => fn(tree.unwrap()));
733
647
  };
734
648
  tree.effect = (fn) => {
@@ -736,8 +650,9 @@ function enhanceTree(tree) {
736
650
  effect(() => fn(tree.unwrap()));
737
651
  }
738
652
  catch (error) {
739
- // Fallback for test environments without injection context
740
- console.warn('Effect requires Angular injection context', error);
653
+ if (config.debugMode) {
654
+ console.warn('Effect requires Angular injection context', error);
655
+ }
741
656
  }
742
657
  };
743
658
  tree.subscribe = (fn) => {
@@ -757,37 +672,31 @@ function enhanceTree(tree) {
757
672
  return unsubscribe;
758
673
  }
759
674
  catch (error) {
760
- // Fallback for test environment - call once immediately
761
- console.warn('Subscribe requires Angular injection context', error);
675
+ if (config.debugMode) {
676
+ console.warn('Subscribe requires Angular injection context', error);
677
+ }
762
678
  fn(tree.unwrap());
763
679
  return () => {
764
- // No-op unsubscribe
765
680
  };
766
681
  }
767
682
  };
768
- // Stub implementations for performance features
769
683
  tree.optimize = () => {
770
- console.warn('⚠️ optimize() called but tree optimization is not available.', '\nTo enable optimization, install @signaltree/memoization');
684
+ if (config.debugMode) {
685
+ console.warn('⚠️ optimize() called but tree optimization is not available.');
686
+ }
771
687
  };
772
688
  tree.clearCache = () => {
773
- console.warn('⚠️ clearCache() called but caching is not available.', '\nTo enable caching, install @signaltree/memoization');
689
+ if (config.debugMode) {
690
+ console.warn('⚠️ clearCache() called but caching is not available.');
691
+ }
774
692
  };
775
693
  tree.invalidatePattern = () => {
776
- console.warn('⚠️ invalidatePattern() called but performance optimization is not enabled.', '\nTo enable pattern invalidation, install @signaltree/memoization');
777
- return 0;
778
- };
779
- tree.destroy = () => {
780
- // Clean up lazy signal proxies if they exist
781
- const state = tree.state;
782
- if (state && typeof state === 'object' && '__cleanup__' in state) {
783
- state.__cleanup__();
694
+ if (config.debugMode) {
695
+ console.warn('⚠️ invalidatePattern() called but performance optimization is not enabled.');
784
696
  }
785
- // Basic cleanup for non-enhanced trees
786
- console.log('[MEMORY-CLEANUP] Basic tree destroyed');
697
+ return 0;
787
698
  };
788
699
  tree.getMetrics = () => {
789
- console.warn('⚠️ getMetrics() called but performance tracking is not enabled.', '\nTo enable performance tracking, install @signaltree/middleware');
790
- // Return minimal metrics when tracking not enabled
791
700
  return {
792
701
  updates: 0,
793
702
  computations: 0,
@@ -796,72 +705,90 @@ function enhanceTree(tree) {
796
705
  averageUpdateTime: 0,
797
706
  };
798
707
  };
799
- // Stub implementations for middleware
800
708
  tree.addTap = (middleware) => {
801
- console.warn('⚠️ addTap() called but middleware support is not available.', '\nTo enable middleware, install @signaltree/middleware');
802
- void middleware; // Mark as intentionally unused
709
+ if (config.debugMode) {
710
+ console.warn('⚠️ addTap() called but middleware support is not available.');
711
+ }
712
+ void middleware;
803
713
  };
804
714
  tree.removeTap = (id) => {
805
- console.warn('⚠️ removeTap() called but middleware support is not available.', '\nTo enable middleware, install @signaltree/middleware');
806
- void id; // Mark as intentionally unused
715
+ if (config.debugMode) {
716
+ console.warn('⚠️ removeTap() called but middleware support is not available.');
717
+ }
718
+ void id;
807
719
  };
808
- // Stub implementations for entity helpers
809
720
  tree.asCrud = () => {
810
- console.warn('⚠️ asCrud() called but entity helpers are not available.', '\nTo enable entity helpers, install @signaltree/entities');
721
+ if (config.debugMode) {
722
+ console.warn('⚠️ asCrud() called but entity helpers are not available.');
723
+ }
811
724
  return {};
812
725
  };
813
- // Stub implementations for async actions
814
- tree.asyncAction = (operation, config = {}) => {
815
- console.warn('⚠️ asyncAction() called but async actions are not available.', '\nTo enable async actions, install @signaltree/async');
726
+ tree.asyncAction = (operation, asyncConfig = {}) => {
727
+ if (config.debugMode) {
728
+ console.warn('⚠️ asyncAction() called but async actions are not available.');
729
+ }
816
730
  void operation;
817
- void config;
731
+ void asyncConfig;
818
732
  return {};
819
733
  };
820
- // Stub implementations for time travel
821
734
  tree.undo = () => {
822
- console.warn('⚠️ undo() called but time travel is not available.', '\nTo enable time travel, install @signaltree/time-travel');
735
+ if (config.debugMode) {
736
+ console.warn('⚠️ undo() called but time travel is not available.');
737
+ }
823
738
  };
824
739
  tree.redo = () => {
825
- console.warn('⚠️ redo() called but time travel is not available.', '\nTo enable time travel, install @signaltree/time-travel');
740
+ if (config.debugMode) {
741
+ console.warn('⚠️ redo() called but time travel is not available.');
742
+ }
826
743
  };
827
744
  tree.getHistory = () => {
828
- console.warn('⚠️ getHistory() called but time travel is not available.', '\nTo enable time travel, install @signaltree/time-travel');
745
+ if (config.debugMode) {
746
+ console.warn('⚠️ getHistory() called but time travel is not available.');
747
+ }
829
748
  return [];
830
749
  };
831
750
  tree.resetHistory = () => {
832
- console.warn('⚠️ resetHistory() called but time travel is not available.', '\nTo enable time travel, install @signaltree/time-travel');
751
+ if (config.debugMode) {
752
+ console.warn('⚠️ resetHistory() called but time travel is not available.');
753
+ }
833
754
  };
834
- return tree;
835
755
  }
836
756
  function signalTree(obj, configOrPreset) {
837
- // Handle preset strings
838
757
  if (typeof configOrPreset === 'string') {
839
- console.warn('⚠️ Preset configurations are not available in @signaltree/core.', '\nTo use presets, install @signaltree/presets');
840
- // Fallback to basic configuration
841
- return create(obj, {});
758
+ const presetConfigs = {
759
+ basic: {
760
+ useLazySignals: false,
761
+ debugMode: false,
762
+ },
763
+ performance: {
764
+ useLazySignals: true,
765
+ batchUpdates: true,
766
+ useMemoization: true,
767
+ useShallowComparison: true,
768
+ },
769
+ development: {
770
+ useLazySignals: false,
771
+ debugMode: true,
772
+ enableDevTools: true,
773
+ trackPerformance: true,
774
+ },
775
+ production: {
776
+ useLazySignals: true,
777
+ batchUpdates: true,
778
+ useMemoization: true,
779
+ debugMode: false,
780
+ },
781
+ };
782
+ const config = presetConfigs[configOrPreset];
783
+ if (!config) {
784
+ console.warn(`Unknown preset: ${configOrPreset}, using default configuration`);
785
+ return create(obj, {});
786
+ }
787
+ return create(obj, config);
842
788
  }
843
- // Handle configuration objects or default (smart enhancement)
844
789
  const config = configOrPreset || {};
845
790
  return create(obj, config);
846
791
  }
847
792
 
848
- /**
849
- * SignalTree Core Types - Recursive Typing System
850
- *
851
- * COPYRIGHT NOTICE:
852
- * This file contains proprietary recursive typing innovations protected under
853
- * the SignalTree license. The DeepSignalify<T> recursive type system and
854
- * related implementations are exclusive intellectual property of Jonathan D Borgia.
855
- *
856
- * Unauthorized extraction, copying, or reimplementation of these recursive typing
857
- * concepts is strictly prohibited and constitutes copyright infringement.
858
- *
859
- * Licensed under Fair Source License - see LICENSE file for complete terms.
860
- */
861
-
862
- /**
863
- * Generated bundle index. Do not edit.
864
- */
865
-
866
793
  export { createLazySignalTree, deepEqual, equal, parsePath, shallowEqual, signalTree, terminalSignal };
867
794
  //# sourceMappingURL=signaltree-core.mjs.map