@signaltree/core 1.0.1 → 1.1.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/README.md +813 -55
- package/fesm2022/signaltree-core.mjs +495 -93
- package/fesm2022/signaltree-core.mjs.map +1 -1
- package/index.d.ts +115 -57
- package/package.json +1 -2
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { signal, isSignal, computed, effect, inject, DestroyRef } from '@angular/core';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
*/
|
|
5
13
|
/**
|
|
6
14
|
* Enhanced equality function inspired by the monolithic implementation.
|
|
7
15
|
* Uses deep equality for arrays and objects, === for primitives.
|
|
8
|
-
*
|
|
16
|
+
* Optimized with early exits and type-specific comparisons.
|
|
9
17
|
*/
|
|
10
18
|
function equal(a, b) {
|
|
11
19
|
// Fast path for reference equality
|
|
@@ -14,23 +22,31 @@ function equal(a, b) {
|
|
|
14
22
|
// Handle null/undefined cases
|
|
15
23
|
if (a == null || b == null)
|
|
16
24
|
return a === b;
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
// Type check first - most efficient early exit
|
|
26
|
+
const typeA = typeof a;
|
|
27
|
+
const typeB = typeof b;
|
|
28
|
+
if (typeA !== typeB)
|
|
29
|
+
return false;
|
|
30
|
+
// For primitives, === check above is sufficient
|
|
31
|
+
if (typeA !== 'object')
|
|
32
|
+
return false;
|
|
33
|
+
// Handle arrays with optimized comparison
|
|
34
|
+
if (Array.isArray(a)) {
|
|
35
|
+
if (!Array.isArray(b) || a.length !== b.length)
|
|
20
36
|
return false;
|
|
21
37
|
return a.every((item, index) => equal(item, b[index]));
|
|
22
38
|
}
|
|
23
|
-
//
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
39
|
+
// Arrays check above handles array vs object mismatch
|
|
40
|
+
if (Array.isArray(b))
|
|
41
|
+
return false;
|
|
42
|
+
// Handle objects with optimized comparison
|
|
43
|
+
const objA = a;
|
|
44
|
+
const objB = b;
|
|
45
|
+
const keysA = Object.keys(objA);
|
|
46
|
+
const keysB = Object.keys(objB);
|
|
47
|
+
if (keysA.length !== keysB.length)
|
|
48
|
+
return false;
|
|
49
|
+
return keysA.every((key) => key in objB && equal(objA[key], objB[key]));
|
|
34
50
|
}
|
|
35
51
|
/**
|
|
36
52
|
* Creates a terminal signal with the enhanced equality function.
|
|
@@ -45,8 +61,48 @@ function terminalSignal(value, customEqual) {
|
|
|
45
61
|
});
|
|
46
62
|
}
|
|
47
63
|
/**
|
|
48
|
-
*
|
|
64
|
+
* LRU Cache implementation for efficient memory management
|
|
65
|
+
*/
|
|
66
|
+
class LRUCache {
|
|
67
|
+
maxSize;
|
|
68
|
+
cache = new Map();
|
|
69
|
+
constructor(maxSize) {
|
|
70
|
+
this.maxSize = maxSize;
|
|
71
|
+
}
|
|
72
|
+
set(key, value) {
|
|
73
|
+
if (this.cache.size >= this.maxSize) {
|
|
74
|
+
const firstKey = this.cache.keys().next().value;
|
|
75
|
+
if (firstKey !== undefined) {
|
|
76
|
+
this.cache.delete(firstKey);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Remove if exists to update position
|
|
80
|
+
this.cache.delete(key);
|
|
81
|
+
this.cache.set(key, value); // Add to end (most recent)
|
|
82
|
+
}
|
|
83
|
+
get(key) {
|
|
84
|
+
const value = this.cache.get(key);
|
|
85
|
+
if (value !== undefined) {
|
|
86
|
+
// Move to end (mark as recently used)
|
|
87
|
+
this.cache.delete(key);
|
|
88
|
+
this.cache.set(key, value);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
clear() {
|
|
93
|
+
this.cache.clear();
|
|
94
|
+
}
|
|
95
|
+
size() {
|
|
96
|
+
return this.cache.size;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Path parsing cache with proper LRU eviction strategy
|
|
100
|
+
const MAX_CACHE_SIZE = 1000;
|
|
101
|
+
const pathCache = new LRUCache(MAX_CACHE_SIZE);
|
|
102
|
+
/**
|
|
103
|
+
* Parses a dot-notation path into an array of keys with LRU memoization.
|
|
49
104
|
* Critical for performance when accessing nested properties frequently.
|
|
105
|
+
* Includes proper LRU cache management to prevent memory leaks.
|
|
50
106
|
*
|
|
51
107
|
* @example
|
|
52
108
|
* ```typescript
|
|
@@ -55,16 +111,19 @@ function terminalSignal(value, customEqual) {
|
|
|
55
111
|
* ```
|
|
56
112
|
*/
|
|
57
113
|
function parsePath(path) {
|
|
58
|
-
if (!pathCache.has(path)) {
|
|
59
|
-
pathCache.set(path, path.split('.'));
|
|
60
|
-
}
|
|
61
114
|
const cached = pathCache.get(path);
|
|
62
|
-
|
|
115
|
+
if (cached) {
|
|
116
|
+
return cached;
|
|
117
|
+
}
|
|
118
|
+
const parts = path.split('.');
|
|
119
|
+
pathCache.set(path, parts);
|
|
120
|
+
return parts;
|
|
63
121
|
}
|
|
64
122
|
/**
|
|
65
123
|
* Creates a lazy signal tree using Proxy for on-demand signal creation.
|
|
66
124
|
* Only creates signals when properties are first accessed, providing
|
|
67
125
|
* massive memory savings for large state objects.
|
|
126
|
+
* Uses WeakMap for memory-safe caching.
|
|
68
127
|
*
|
|
69
128
|
* @param obj - Source object to lazily signalify
|
|
70
129
|
* @param equalityFn - Equality function for signal comparison
|
|
@@ -72,16 +131,68 @@ function parsePath(path) {
|
|
|
72
131
|
* @returns Proxied object that creates signals on first access
|
|
73
132
|
*/
|
|
74
133
|
function createLazySignalTree(obj, equalityFn, basePath = '') {
|
|
134
|
+
// Use Map instead of WeakMap for better control over cleanup
|
|
75
135
|
const signalCache = new Map();
|
|
76
136
|
const nestedProxies = new Map();
|
|
77
|
-
|
|
137
|
+
// Track cleanup functions for nested proxies
|
|
138
|
+
const nestedCleanups = new Map();
|
|
139
|
+
// Enhanced cleanup function
|
|
140
|
+
const cleanup = () => {
|
|
141
|
+
// Clean up all nested proxies first
|
|
142
|
+
nestedCleanups.forEach((cleanupFn) => {
|
|
143
|
+
try {
|
|
144
|
+
cleanupFn();
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
console.warn('Error during nested cleanup:', error);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
nestedCleanups.clear();
|
|
151
|
+
// Clear caches
|
|
152
|
+
signalCache.clear();
|
|
153
|
+
nestedProxies.clear();
|
|
154
|
+
};
|
|
155
|
+
// Enhanced built-in object detection
|
|
156
|
+
const isBuiltInObject = (v) => {
|
|
157
|
+
if (v === null || v === undefined)
|
|
158
|
+
return false;
|
|
159
|
+
return (v instanceof Date ||
|
|
160
|
+
v instanceof RegExp ||
|
|
161
|
+
typeof v === 'function' ||
|
|
162
|
+
v instanceof Map ||
|
|
163
|
+
v instanceof Set ||
|
|
164
|
+
v instanceof WeakMap ||
|
|
165
|
+
v instanceof WeakSet ||
|
|
166
|
+
v instanceof ArrayBuffer ||
|
|
167
|
+
v instanceof DataView ||
|
|
168
|
+
v instanceof Error ||
|
|
169
|
+
v instanceof Promise ||
|
|
170
|
+
v instanceof URL ||
|
|
171
|
+
v instanceof URLSearchParams ||
|
|
172
|
+
v instanceof FormData ||
|
|
173
|
+
v instanceof Blob ||
|
|
174
|
+
(typeof File !== 'undefined' && v instanceof File));
|
|
175
|
+
};
|
|
176
|
+
const proxy = new Proxy(obj, {
|
|
78
177
|
get(target, prop) {
|
|
178
|
+
// Handle cleanup method
|
|
179
|
+
if (prop === '__cleanup__') {
|
|
180
|
+
return cleanup;
|
|
181
|
+
}
|
|
79
182
|
// Handle symbol properties (like Symbol.iterator) normally
|
|
80
183
|
if (typeof prop === 'symbol') {
|
|
81
184
|
return target[prop];
|
|
82
185
|
}
|
|
186
|
+
// Handle inspection methods
|
|
187
|
+
if (prop === 'valueOf' || prop === 'toString') {
|
|
188
|
+
return target[prop];
|
|
189
|
+
}
|
|
83
190
|
const key = prop;
|
|
84
191
|
const path = basePath ? `${basePath}.${key}` : key;
|
|
192
|
+
// Safety check for property existence
|
|
193
|
+
if (!(key in target)) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
85
196
|
const value = target[key];
|
|
86
197
|
// If it's already a signal, return it
|
|
87
198
|
if (isSignal(value)) {
|
|
@@ -99,15 +210,36 @@ function createLazySignalTree(obj, equalityFn, basePath = '') {
|
|
|
99
210
|
if (value &&
|
|
100
211
|
typeof value === 'object' &&
|
|
101
212
|
!Array.isArray(value) &&
|
|
102
|
-
!isSignal(value)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
213
|
+
!isSignal(value) &&
|
|
214
|
+
!isBuiltInObject(value)) {
|
|
215
|
+
try {
|
|
216
|
+
const nestedProxy = createLazySignalTree(value, equalityFn, path);
|
|
217
|
+
nestedProxies.set(path, nestedProxy);
|
|
218
|
+
// Store cleanup function for nested proxy
|
|
219
|
+
const proxyWithCleanup = nestedProxy;
|
|
220
|
+
if (typeof proxyWithCleanup.__cleanup__ === 'function') {
|
|
221
|
+
nestedCleanups.set(path, proxyWithCleanup.__cleanup__);
|
|
222
|
+
}
|
|
223
|
+
return nestedProxy;
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
console.warn(`Failed to create lazy proxy for path "${path}":`, error);
|
|
227
|
+
// Fallback: create a signal for the object
|
|
228
|
+
const fallbackSignal = signal(value, ...(ngDevMode ? [{ debugName: "fallbackSignal", equal: equalityFn }] : [{ equal: equalityFn }]));
|
|
229
|
+
signalCache.set(path, fallbackSignal);
|
|
230
|
+
return fallbackSignal;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Create signal for primitive values, arrays, and built-in objects
|
|
234
|
+
try {
|
|
235
|
+
const newSignal = signal(value, ...(ngDevMode ? [{ debugName: "newSignal", equal: equalityFn }] : [{ equal: equalityFn }]));
|
|
236
|
+
signalCache.set(path, newSignal);
|
|
237
|
+
return newSignal;
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.warn(`Failed to create signal for path "${path}":`, error);
|
|
241
|
+
return value; // Return raw value as fallback
|
|
106
242
|
}
|
|
107
|
-
// Create signal for primitive values and arrays
|
|
108
|
-
const newSignal = signal(value, ...(ngDevMode ? [{ debugName: "newSignal", equal: equalityFn }] : [{ equal: equalityFn }]));
|
|
109
|
-
signalCache.set(path, newSignal);
|
|
110
|
-
return newSignal;
|
|
111
243
|
},
|
|
112
244
|
set(target, prop, value) {
|
|
113
245
|
if (typeof prop === 'symbol') {
|
|
@@ -116,18 +248,30 @@ function createLazySignalTree(obj, equalityFn, basePath = '') {
|
|
|
116
248
|
}
|
|
117
249
|
const key = prop;
|
|
118
250
|
const path = basePath ? `${basePath}.${key}` : key;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
cachedSignal
|
|
251
|
+
try {
|
|
252
|
+
// Update the original object
|
|
253
|
+
target[key] = value;
|
|
254
|
+
// If we have a cached signal, update it
|
|
255
|
+
const cachedSignal = signalCache.get(path);
|
|
256
|
+
if (cachedSignal && 'set' in cachedSignal) {
|
|
257
|
+
cachedSignal.set(value);
|
|
258
|
+
}
|
|
259
|
+
// Clear nested proxy cache if the value type changed
|
|
260
|
+
if (nestedProxies.has(path)) {
|
|
261
|
+
// Clean up the nested proxy
|
|
262
|
+
const nestedCleanup = nestedCleanups.get(path);
|
|
263
|
+
if (nestedCleanup) {
|
|
264
|
+
nestedCleanup();
|
|
265
|
+
nestedCleanups.delete(path);
|
|
266
|
+
}
|
|
267
|
+
nestedProxies.delete(path);
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
125
270
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
271
|
+
catch (error) {
|
|
272
|
+
console.warn(`Failed to set value for path "${path}":`, error);
|
|
273
|
+
return false;
|
|
129
274
|
}
|
|
130
|
-
return true;
|
|
131
275
|
},
|
|
132
276
|
has(target, prop) {
|
|
133
277
|
return prop in target;
|
|
@@ -139,6 +283,7 @@ function createLazySignalTree(obj, equalityFn, basePath = '') {
|
|
|
139
283
|
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
140
284
|
},
|
|
141
285
|
});
|
|
286
|
+
return proxy;
|
|
142
287
|
}
|
|
143
288
|
/**
|
|
144
289
|
* Native deep equality check for arrays and objects.
|
|
@@ -224,59 +369,217 @@ function shallowEqual(a, b) {
|
|
|
224
369
|
return false;
|
|
225
370
|
}
|
|
226
371
|
|
|
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
|
+
*/
|
|
227
386
|
/**
|
|
228
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
|
+
* ```
|
|
229
400
|
*/
|
|
230
401
|
function createEqualityFn(useShallowComparison) {
|
|
231
402
|
return useShallowComparison ? Object.is : equal;
|
|
232
403
|
}
|
|
233
404
|
/**
|
|
234
|
-
* Core function to create a basic SignalTree.
|
|
405
|
+
* Core function to create a basic SignalTree with enhanced safety.
|
|
235
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
|
+
* ```
|
|
236
432
|
*/
|
|
237
433
|
function create(obj, config = {}) {
|
|
238
434
|
const equalityFn = createEqualityFn(config.useShallowComparison ?? false);
|
|
239
|
-
const useLazy = config.useLazySignals ?? true;
|
|
240
|
-
//
|
|
435
|
+
const useLazy = config.useLazySignals ?? true;
|
|
436
|
+
// Create signals using signal-store pattern for perfect type inference
|
|
241
437
|
const signalState = useLazy
|
|
242
438
|
? createLazySignalTree(obj, equalityFn)
|
|
243
|
-
:
|
|
439
|
+
: createSignalStore(obj, equalityFn);
|
|
244
440
|
const resultTree = {
|
|
245
441
|
state: signalState,
|
|
246
442
|
$: signalState, // $ points to the same state object
|
|
247
443
|
};
|
|
248
|
-
|
|
444
|
+
enhanceTree(resultTree);
|
|
249
445
|
return resultTree;
|
|
250
446
|
}
|
|
251
447
|
/**
|
|
252
|
-
* Creates
|
|
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
|
+
* ```
|
|
253
468
|
*/
|
|
254
|
-
function
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
469
|
+
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');
|
|
473
|
+
}
|
|
474
|
+
if (typeof obj !== 'object') {
|
|
475
|
+
// For primitives, just return a signal
|
|
476
|
+
return signal(obj, { equal: equalityFn });
|
|
477
|
+
}
|
|
478
|
+
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
|
|
501
|
+
try {
|
|
502
|
+
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);
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
console.warn(`Failed to create signal store for key "${key}":`, error);
|
|
518
|
+
// Fallback: treat as primitive
|
|
519
|
+
store[key] = signal(value, {
|
|
520
|
+
equal: equalityFn,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
// Create signal for primitives, arrays, and built-in objects
|
|
526
|
+
store[key] = signal(value, {
|
|
527
|
+
equal: equalityFn,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
269
530
|
}
|
|
270
531
|
}
|
|
271
|
-
|
|
532
|
+
catch (error) {
|
|
533
|
+
throw new Error(`Failed to create signal store: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
534
|
+
}
|
|
535
|
+
return store;
|
|
272
536
|
}
|
|
273
537
|
/**
|
|
274
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
|
+
* ```
|
|
275
555
|
*/
|
|
276
|
-
function
|
|
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
|
+
*/
|
|
277
577
|
tree.unwrap = () => {
|
|
278
578
|
// Recursively unwrap with proper typing
|
|
279
579
|
const unwrapObject = (obj) => {
|
|
580
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
581
|
+
return obj;
|
|
582
|
+
}
|
|
280
583
|
const result = {};
|
|
281
584
|
for (const key in obj) {
|
|
282
585
|
const value = obj[key];
|
|
@@ -297,41 +600,124 @@ function enhanceTreeBasic(tree) {
|
|
|
297
600
|
};
|
|
298
601
|
return unwrapObject(tree.state);
|
|
299
602
|
};
|
|
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
|
+
*/
|
|
300
638
|
tree.update = (updater) => {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
639
|
+
const originalState = new Map();
|
|
640
|
+
try {
|
|
641
|
+
const currentValue = tree.unwrap();
|
|
642
|
+
const partialObj = updater(currentValue);
|
|
643
|
+
if (!partialObj || typeof partialObj !== 'object') {
|
|
644
|
+
throw new Error('Updater must return an object');
|
|
645
|
+
}
|
|
646
|
+
// Recursively update with better error handling and rollback
|
|
647
|
+
const updateObject = (target, updates, path = '') => {
|
|
648
|
+
for (const key in updates) {
|
|
649
|
+
if (!Object.prototype.hasOwnProperty.call(updates, key))
|
|
650
|
+
continue;
|
|
651
|
+
const updateValue = updates[key];
|
|
652
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
653
|
+
const currentSignalOrState = target[key];
|
|
654
|
+
try {
|
|
655
|
+
if (isSignal(currentSignalOrState)) {
|
|
656
|
+
// Store original value for potential rollback
|
|
657
|
+
const originalValue = currentSignalOrState();
|
|
658
|
+
originalState.set(currentPath, originalValue);
|
|
659
|
+
// Direct signal update
|
|
660
|
+
currentSignalOrState.set(updateValue);
|
|
661
|
+
}
|
|
662
|
+
else if (typeof updateValue === 'object' &&
|
|
663
|
+
updateValue !== null &&
|
|
664
|
+
!Array.isArray(updateValue) &&
|
|
665
|
+
typeof currentSignalOrState === 'object' &&
|
|
666
|
+
currentSignalOrState !== null &&
|
|
667
|
+
!isSignal(currentSignalOrState)) {
|
|
668
|
+
// Nested object - recurse
|
|
669
|
+
updateObject(currentSignalOrState, updateValue, currentPath);
|
|
670
|
+
}
|
|
671
|
+
else if (currentSignalOrState === undefined) {
|
|
672
|
+
console.warn(`Cannot update non-existent path: ${currentPath}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
console.error(`Failed to update path "${currentPath}":`, error);
|
|
677
|
+
// Continue with other updates rather than failing completely
|
|
678
|
+
}
|
|
313
679
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
680
|
+
};
|
|
681
|
+
updateObject(tree.state, partialObj);
|
|
682
|
+
}
|
|
683
|
+
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()) {
|
|
689
|
+
const pathParts = path.split('.');
|
|
690
|
+
let current = tree.state;
|
|
691
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
692
|
+
current = current[pathParts[i]];
|
|
693
|
+
if (!current)
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
if (current &&
|
|
697
|
+
isSignal(current[pathParts[pathParts.length - 1]])) {
|
|
698
|
+
const signal = current[pathParts[pathParts.length - 1]];
|
|
699
|
+
signal.set(originalValue);
|
|
700
|
+
}
|
|
321
701
|
}
|
|
322
702
|
}
|
|
323
|
-
|
|
324
|
-
|
|
703
|
+
catch (rollbackError) {
|
|
704
|
+
console.error('Rollback failed:', rollbackError);
|
|
705
|
+
}
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
325
708
|
};
|
|
326
709
|
// Pipe implementation for function composition with improved type safety
|
|
327
|
-
tree.pipe = ((...fns
|
|
328
|
-
) => {
|
|
329
|
-
// eslint-disable-line @typescript-eslint/no-explicit-any
|
|
710
|
+
tree.pipe = ((...fns) => {
|
|
330
711
|
if (fns.length === 0) {
|
|
331
712
|
return tree;
|
|
332
713
|
}
|
|
333
|
-
// Type-safe reduce
|
|
334
|
-
return fns.reduce((acc, fn) =>
|
|
714
|
+
// Type-safe reduce with proper function composition
|
|
715
|
+
return fns.reduce((acc, fn) => {
|
|
716
|
+
if (typeof fn !== 'function') {
|
|
717
|
+
throw new Error('All pipe arguments must be functions');
|
|
718
|
+
}
|
|
719
|
+
return fn(acc);
|
|
720
|
+
}, tree);
|
|
335
721
|
});
|
|
336
722
|
// Stub implementations for advanced features (will log warnings)
|
|
337
723
|
tree.batchUpdate = (updater) => {
|
|
@@ -391,6 +777,11 @@ function enhanceTreeBasic(tree) {
|
|
|
391
777
|
return 0;
|
|
392
778
|
};
|
|
393
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__();
|
|
784
|
+
}
|
|
394
785
|
// Basic cleanup for non-enhanced trees
|
|
395
786
|
console.log('[MEMORY-CLEANUP] Basic tree destroyed');
|
|
396
787
|
};
|
|
@@ -442,9 +833,6 @@ function enhanceTreeBasic(tree) {
|
|
|
442
833
|
};
|
|
443
834
|
return tree;
|
|
444
835
|
}
|
|
445
|
-
/**
|
|
446
|
-
* Implementation of the signalTree factory function.
|
|
447
|
-
*/
|
|
448
836
|
function signalTree(obj, configOrPreset) {
|
|
449
837
|
// Handle preset strings
|
|
450
838
|
if (typeof configOrPreset === 'string') {
|
|
@@ -457,6 +845,20 @@ function signalTree(obj, configOrPreset) {
|
|
|
457
845
|
return create(obj, config);
|
|
458
846
|
}
|
|
459
847
|
|
|
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
|
+
|
|
460
862
|
/**
|
|
461
863
|
* Generated bundle index. Do not edit.
|
|
462
864
|
*/
|