@signaltree/core 5.1.1 → 5.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.
Files changed (92) hide show
  1. package/README.md +116 -55
  2. package/dist/constants.js +6 -0
  3. package/dist/deep-clone.js +80 -0
  4. package/dist/deep-equal.js +41 -0
  5. package/dist/enhancers/batching/lib/batching.js +161 -0
  6. package/dist/enhancers/computed/lib/computed.js +21 -0
  7. package/dist/enhancers/devtools/lib/devtools.js +321 -0
  8. package/dist/enhancers/entities/lib/entities.js +93 -0
  9. package/dist/enhancers/index.js +72 -0
  10. package/dist/enhancers/memoization/lib/memoization.js +410 -0
  11. package/dist/enhancers/presets/lib/presets.js +87 -0
  12. package/dist/enhancers/serialization/constants.js +15 -0
  13. package/dist/enhancers/serialization/lib/serialization.js +662 -0
  14. package/dist/enhancers/time-travel/lib/time-travel.js +193 -0
  15. package/dist/index.js +19 -0
  16. package/dist/is-built-in-object.js +23 -0
  17. package/dist/lib/async-helpers.js +77 -0
  18. package/dist/lib/constants.js +56 -0
  19. package/dist/lib/entity-signal.js +280 -0
  20. package/dist/lib/memory/memory-manager.js +164 -0
  21. package/dist/lib/path-notifier.js +106 -0
  22. package/dist/lib/performance/diff-engine.js +156 -0
  23. package/dist/lib/performance/path-index.js +156 -0
  24. package/dist/lib/performance/update-engine.js +188 -0
  25. package/dist/lib/security/security-validator.js +121 -0
  26. package/dist/lib/signal-tree.js +626 -0
  27. package/dist/lib/types.js +9 -0
  28. package/dist/lib/utils.js +261 -0
  29. package/dist/lru-cache.js +64 -0
  30. package/dist/parse-path.js +13 -0
  31. package/package.json +1 -1
  32. package/src/async-helpers.d.ts +8 -0
  33. package/src/batching.d.ts +16 -0
  34. package/src/computed.d.ts +12 -0
  35. package/src/constants.d.ts +14 -0
  36. package/src/devtools.d.ts +77 -0
  37. package/src/diff-engine.d.ts +33 -0
  38. package/src/enhancers/batching/index.d.ts +1 -0
  39. package/src/enhancers/batching/lib/batching.d.ts +16 -0
  40. package/src/enhancers/batching/test-setup.d.ts +3 -0
  41. package/src/enhancers/computed/index.d.ts +1 -0
  42. package/src/enhancers/computed/lib/computed.d.ts +12 -0
  43. package/src/enhancers/devtools/index.d.ts +1 -0
  44. package/src/enhancers/devtools/lib/devtools.d.ts +77 -0
  45. package/src/enhancers/devtools/test-setup.d.ts +3 -0
  46. package/src/enhancers/entities/index.d.ts +1 -0
  47. package/src/enhancers/entities/lib/entities.d.ts +20 -0
  48. package/src/enhancers/entities/test-setup.d.ts +3 -0
  49. package/src/enhancers/index.d.ts +3 -0
  50. package/src/enhancers/memoization/index.d.ts +1 -0
  51. package/src/enhancers/memoization/lib/memoization.d.ts +65 -0
  52. package/src/enhancers/memoization/test-setup.d.ts +3 -0
  53. package/src/enhancers/presets/index.d.ts +1 -0
  54. package/src/enhancers/presets/lib/presets.d.ts +11 -0
  55. package/src/enhancers/presets/test-setup.d.ts +3 -0
  56. package/src/enhancers/serialization/constants.d.ts +14 -0
  57. package/src/enhancers/serialization/index.d.ts +2 -0
  58. package/src/enhancers/serialization/lib/serialization.d.ts +59 -0
  59. package/src/enhancers/serialization/test-setup.d.ts +3 -0
  60. package/src/enhancers/time-travel/index.d.ts +1 -0
  61. package/src/enhancers/time-travel/lib/time-travel.d.ts +36 -0
  62. package/src/enhancers/time-travel/lib/utils.d.ts +1 -0
  63. package/src/enhancers/time-travel/test-setup.d.ts +3 -0
  64. package/src/enhancers/types.d.ts +74 -0
  65. package/src/entities.d.ts +20 -0
  66. package/src/entity-signal.d.ts +1 -0
  67. package/src/index.d.ts +18 -0
  68. package/src/lib/async-helpers.d.ts +8 -0
  69. package/src/lib/constants.d.ts +41 -0
  70. package/src/lib/entity-signal.d.ts +1 -0
  71. package/src/lib/memory/memory-manager.d.ts +30 -0
  72. package/src/lib/path-notifier.d.ts +4 -0
  73. package/src/lib/performance/diff-engine.d.ts +33 -0
  74. package/src/lib/performance/path-index.d.ts +25 -0
  75. package/src/lib/performance/update-engine.d.ts +32 -0
  76. package/src/lib/security/security-validator.d.ts +33 -0
  77. package/src/lib/signal-tree.d.ts +8 -0
  78. package/src/lib/types.d.ts +278 -0
  79. package/src/lib/utils.d.ts +28 -0
  80. package/src/memoization.d.ts +65 -0
  81. package/src/memory-manager.d.ts +30 -0
  82. package/src/path-index.d.ts +25 -0
  83. package/src/path-notifier.d.ts +12 -0
  84. package/src/presets.d.ts +11 -0
  85. package/src/security-validator.d.ts +33 -0
  86. package/src/serialization.d.ts +59 -0
  87. package/src/signal-tree.d.ts +8 -0
  88. package/src/test-setup.d.ts +3 -0
  89. package/src/time-travel.d.ts +36 -0
  90. package/src/types.d.ts +278 -0
  91. package/src/update-engine.d.ts +32 -0
  92. package/src/utils.d.ts +1 -0
@@ -0,0 +1,662 @@
1
+ import { isSignal } from '@angular/core';
2
+ import { TYPE_MARKERS } from '../constants.js';
3
+
4
+ const DEFAULT_CONFIG = {
5
+ includeMetadata: true,
6
+ replacer: undefined,
7
+ reviver: undefined,
8
+ preserveTypes: true,
9
+ maxDepth: 50,
10
+ handleCircular: true
11
+ };
12
+ function unwrapObjectSafely(obj, visited = new WeakSet(), depth = 0, maxDepth = 50, preserveTypes = true) {
13
+ if (depth > maxDepth) return '[Max Depth Exceeded]';
14
+ if (obj === null || typeof obj !== 'object') {
15
+ if (!preserveTypes) return obj;
16
+ if (obj === undefined) return {
17
+ [TYPE_MARKERS.UNDEFINED]: true
18
+ };
19
+ if (typeof obj === 'number') {
20
+ if (Number.isNaN(obj)) return {
21
+ [TYPE_MARKERS.NAN]: true
22
+ };
23
+ if (obj === Infinity) return {
24
+ [TYPE_MARKERS.INFINITY]: true
25
+ };
26
+ if (obj === -Infinity) return {
27
+ [TYPE_MARKERS.NEG_INFINITY]: true
28
+ };
29
+ return obj;
30
+ }
31
+ if (typeof obj === 'bigint') return {
32
+ [TYPE_MARKERS.BIGINT]: String(obj)
33
+ };
34
+ if (typeof obj === 'symbol') return {
35
+ [TYPE_MARKERS.SYMBOL]: String(obj)
36
+ };
37
+ return obj;
38
+ }
39
+ if (typeof obj === 'function') {
40
+ try {
41
+ const result = obj();
42
+ return unwrapObjectSafely(result, visited, depth + 1, maxDepth, preserveTypes);
43
+ } catch {
44
+ return '[Function]';
45
+ }
46
+ }
47
+ if (visited.has(obj)) return '[Circular Reference]';
48
+ if (isSignal(obj)) return obj();
49
+ if (preserveTypes) {
50
+ if (obj instanceof Date) return {
51
+ [TYPE_MARKERS.DATE]: obj.toISOString()
52
+ };
53
+ if (obj instanceof RegExp) return {
54
+ [TYPE_MARKERS.REGEXP]: {
55
+ source: obj.source,
56
+ flags: obj.flags
57
+ }
58
+ };
59
+ if (obj instanceof Map) {
60
+ return {
61
+ [TYPE_MARKERS.MAP]: Array.from(obj.entries())
62
+ };
63
+ }
64
+ if (obj instanceof Set) {
65
+ return {
66
+ [TYPE_MARKERS.SET]: Array.from(obj.values())
67
+ };
68
+ }
69
+ } else {
70
+ if (obj instanceof Date || obj instanceof RegExp) return obj;
71
+ }
72
+ visited.add(obj);
73
+ try {
74
+ if (Array.isArray(obj)) {
75
+ const arr = obj.map(item => unwrapObjectSafely(item, visited, depth + 1, maxDepth, preserveTypes));
76
+ visited.delete(obj);
77
+ return arr;
78
+ }
79
+ const out = {};
80
+ for (const [k, v] of Object.entries(obj)) {
81
+ if ((k === 'set' || k === 'update') && typeof v === 'function' && !isSignal(v)) continue;
82
+ if (isSignal(v)) {
83
+ out[k] = unwrapObjectSafely(v(), visited, depth + 1, maxDepth, preserveTypes);
84
+ } else if (typeof v === 'function' && k !== 'set' && k !== 'update') {
85
+ try {
86
+ const callResult = v();
87
+ out[k] = unwrapObjectSafely(callResult, visited, depth + 1, maxDepth, preserveTypes);
88
+ } catch {
89
+ continue;
90
+ }
91
+ } else {
92
+ out[k] = unwrapObjectSafely(v, visited, depth + 1, maxDepth, preserveTypes);
93
+ }
94
+ }
95
+ visited.delete(obj);
96
+ return out;
97
+ } catch {
98
+ visited.delete(obj);
99
+ return '[Serialization Error]';
100
+ }
101
+ }
102
+ function detectCircularReferences(obj, path = '', seen = new WeakSet(), paths = new Map()) {
103
+ const circular = [];
104
+ if (obj === null || typeof obj !== 'object') {
105
+ return circular;
106
+ }
107
+ if (seen.has(obj)) {
108
+ const targetPath = paths.get(obj) || '';
109
+ circular.push({
110
+ path,
111
+ targetPath
112
+ });
113
+ return circular;
114
+ }
115
+ seen.add(obj);
116
+ paths.set(obj, path);
117
+ if (Array.isArray(obj)) {
118
+ const arrObj = obj;
119
+ for (let i = 0; i < arrObj.length; i++) {
120
+ const itemPath = path ? `${path}[${i}]` : `[${i}]`;
121
+ const childCircular = detectCircularReferences(arrObj[i], itemPath, seen, paths);
122
+ circular.push(...childCircular);
123
+ }
124
+ } else {
125
+ for (const [key, value] of Object.entries(obj)) {
126
+ const propPath = path ? `${path}.${key}` : key;
127
+ const childCircular = detectCircularReferences(value, propPath, seen, paths);
128
+ circular.push(...childCircular);
129
+ }
130
+ }
131
+ seen.delete(obj);
132
+ return circular;
133
+ }
134
+ function createReplacer(config) {
135
+ const seen = new WeakSet();
136
+ const circularPaths = new Map();
137
+ return function replacer(key, value) {
138
+ if (config.replacer) {
139
+ value = config.replacer.call(this, key, value);
140
+ }
141
+ if (isSignal(value)) {
142
+ return value();
143
+ }
144
+ if (value && typeof value === 'object') {
145
+ if (seen.has(value)) {
146
+ if (config.handleCircular) {
147
+ const targetPath = circularPaths.get(value) || '';
148
+ return {
149
+ [TYPE_MARKERS.CIRCULAR]: targetPath
150
+ };
151
+ }
152
+ return undefined;
153
+ }
154
+ seen.add(value);
155
+ const currentPath = key || '';
156
+ circularPaths.set(value, currentPath);
157
+ }
158
+ return value;
159
+ };
160
+ }
161
+ function resolveCircularReferences(obj, circularPaths) {
162
+ for (const {
163
+ path,
164
+ targetPath
165
+ } of circularPaths) {
166
+ const pathParts = path.split(/\.|\[|\]/).filter(Boolean);
167
+ const targetParts = targetPath.split(/\.|\[|\]/).filter(Boolean);
168
+ let current = obj;
169
+ for (let i = 0; i < pathParts.length - 1; i++) {
170
+ current = current[pathParts[i]];
171
+ if (!current) break;
172
+ }
173
+ let target = obj;
174
+ for (const part of targetParts) {
175
+ target = target[part];
176
+ if (!target) break;
177
+ }
178
+ if (current && target) {
179
+ current[pathParts[pathParts.length - 1]] = target;
180
+ }
181
+ }
182
+ }
183
+ function withSerialization(defaultConfig = {}) {
184
+ const enhancer = tree => {
185
+ const enhanced = tree;
186
+ enhanced.toJSON = () => {
187
+ return tree();
188
+ };
189
+ enhanced.fromJSON = (data, metadata) => {
190
+ const restoreSpecialTypes = value => {
191
+ if (!value || typeof value !== 'object') {
192
+ return value;
193
+ }
194
+ if (TYPE_MARKERS.UNDEFINED in value) {
195
+ return undefined;
196
+ }
197
+ if (TYPE_MARKERS.NAN in value) {
198
+ return NaN;
199
+ }
200
+ if (TYPE_MARKERS.INFINITY in value) {
201
+ return Infinity;
202
+ }
203
+ if (TYPE_MARKERS.NEG_INFINITY in value) {
204
+ return -Infinity;
205
+ }
206
+ if (TYPE_MARKERS.BIGINT in value) {
207
+ return BigInt(value[TYPE_MARKERS.BIGINT]);
208
+ }
209
+ if (TYPE_MARKERS.SYMBOL in value) {
210
+ return Symbol.for(value[TYPE_MARKERS.SYMBOL]);
211
+ }
212
+ if (TYPE_MARKERS.DATE in value) {
213
+ return new Date(value[TYPE_MARKERS.DATE]);
214
+ }
215
+ if (TYPE_MARKERS.REGEXP in value) {
216
+ const regexpData = value[TYPE_MARKERS.REGEXP];
217
+ return new RegExp(regexpData.source, regexpData.flags);
218
+ }
219
+ if (TYPE_MARKERS.MAP in value) {
220
+ return new Map(value[TYPE_MARKERS.MAP]);
221
+ }
222
+ if (TYPE_MARKERS.SET in value) {
223
+ return new Set(value[TYPE_MARKERS.SET]);
224
+ }
225
+ if (Array.isArray(value)) {
226
+ return value.map(restoreSpecialTypes);
227
+ }
228
+ const result = {};
229
+ for (const [k, v] of Object.entries(value)) {
230
+ result[k] = restoreSpecialTypes(v);
231
+ }
232
+ return result;
233
+ };
234
+ const restoredData = restoreSpecialTypes(data);
235
+ function resolveAliasSignal(path, key) {
236
+ let node = tree.$;
237
+ if (path && node) {
238
+ for (const part of path.split('.')) {
239
+ if (!part) continue;
240
+ const next = node[part];
241
+ if (!next || typeof next !== 'object' && typeof next !== 'function') {
242
+ node = undefined;
243
+ break;
244
+ }
245
+ node = next;
246
+ }
247
+ }
248
+ const candidate = node?.[key];
249
+ return isSignal(candidate) ? candidate : undefined;
250
+ }
251
+ function updateSignals(target, source, path = '') {
252
+ if (!target || !source) return;
253
+ for (const key in source) {
254
+ if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
255
+ const sourceValue = source[key];
256
+ const direct = target[key];
257
+ const targetSignal = isSignal(direct) ? direct : typeof direct === 'function' && 'set' in direct && typeof direct.set === 'function' ? direct : resolveAliasSignal(path, key);
258
+ if (targetSignal) {
259
+ targetSignal.set(sourceValue);
260
+ continue;
261
+ }
262
+ if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue) && direct && (typeof direct === 'object' || typeof direct === 'function' && !('set' in direct)) && !isSignal(direct)) {
263
+ updateSignals(direct, sourceValue, path ? `${path}.${key}` : key);
264
+ }
265
+ }
266
+ }
267
+ const nodeMap = metadata?.nodeMap;
268
+ if (nodeMap && Object.keys(nodeMap).length > 0) {
269
+ if (nodeMap[''] === 'r') {
270
+ const rootAlias = tree.$;
271
+ if (rootAlias && typeof rootAlias.set === 'function') {
272
+ rootAlias.set(restoredData);
273
+ }
274
+ }
275
+ for (const [path, kind] of Object.entries(nodeMap)) {
276
+ if (path === '') continue;
277
+ if (kind !== 'b') continue;
278
+ const parts = path.split(/\.|\[|\]/).filter(Boolean);
279
+ let node = tree.$;
280
+ for (const p of parts) {
281
+ if (!node) break;
282
+ node = node[p] ?? undefined;
283
+ }
284
+ if (node && (isSignal(node) || typeof node === 'function' && 'set' in node && typeof node.set === 'function')) {
285
+ let current = restoredData;
286
+ for (const p of parts) {
287
+ if (current == null) {
288
+ current = undefined;
289
+ break;
290
+ }
291
+ if (typeof current === 'object') {
292
+ current = current[p];
293
+ } else {
294
+ current = undefined;
295
+ break;
296
+ }
297
+ }
298
+ try {
299
+ node.set(current);
300
+ } catch {}
301
+ }
302
+ }
303
+ updateSignals(tree.state, restoredData);
304
+ return;
305
+ }
306
+ updateSignals(tree.state, restoredData);
307
+ };
308
+ function encodeSpecials(v, preserveTypes) {
309
+ if (!preserveTypes) return v;
310
+ if (v === undefined) return {
311
+ [TYPE_MARKERS.UNDEFINED]: true
312
+ };
313
+ if (typeof v === 'number') {
314
+ if (Number.isNaN(v)) return {
315
+ [TYPE_MARKERS.NAN]: true
316
+ };
317
+ if (v === Infinity) return {
318
+ [TYPE_MARKERS.INFINITY]: true
319
+ };
320
+ if (v === -Infinity) return {
321
+ [TYPE_MARKERS.NEG_INFINITY]: true
322
+ };
323
+ return v;
324
+ }
325
+ if (typeof v === 'bigint') return {
326
+ [TYPE_MARKERS.BIGINT]: String(v)
327
+ };
328
+ if (typeof v === 'symbol') return {
329
+ [TYPE_MARKERS.SYMBOL]: String(v)
330
+ };
331
+ if (v instanceof Date) return {
332
+ [TYPE_MARKERS.DATE]: v.toISOString()
333
+ };
334
+ if (v instanceof RegExp) return {
335
+ [TYPE_MARKERS.REGEXP]: {
336
+ source: v.source,
337
+ flags: v.flags
338
+ }
339
+ };
340
+ if (v instanceof Map) return {
341
+ [TYPE_MARKERS.MAP]: Array.from(v.entries())
342
+ };
343
+ if (v instanceof Set) return {
344
+ [TYPE_MARKERS.SET]: Array.from(v.values())
345
+ };
346
+ if (Array.isArray(v)) return v.map(x => encodeSpecials(x, preserveTypes));
347
+ if (v && typeof v === 'object') {
348
+ const out = {};
349
+ for (const [k, val] of Object.entries(v)) out[k] = encodeSpecials(val, preserveTypes);
350
+ return out;
351
+ }
352
+ return v;
353
+ }
354
+ enhanced.serialize = config => {
355
+ const fullConfig = {
356
+ ...DEFAULT_CONFIG,
357
+ ...defaultConfig,
358
+ ...config
359
+ };
360
+ const raw = unwrapObjectSafely(tree.state, new WeakSet(), 0, fullConfig.maxDepth, fullConfig.preserveTypes);
361
+ const state = encodeSpecials(raw, fullConfig.preserveTypes);
362
+ const circularPaths = fullConfig.handleCircular ? detectCircularReferences(state) : [];
363
+ const data = {
364
+ data: state
365
+ };
366
+ const nodeMap = {};
367
+ try {
368
+ const rootAlias = tree.$;
369
+ if (rootAlias && typeof rootAlias.set === 'function') {
370
+ nodeMap[''] = 'r';
371
+ }
372
+ const visited = new WeakSet();
373
+ const isBranch = v => isSignal(v) || typeof v === 'function' && 'set' in v && typeof v.set === 'function';
374
+ const walkAlias = (obj, path = '') => {
375
+ if (!obj || typeof obj !== 'object' && typeof obj !== 'function') return;
376
+ const ref = obj;
377
+ if (visited.has(ref)) return;
378
+ visited.add(ref);
379
+ if (path && isBranch(obj)) {
380
+ nodeMap[path] = 'b';
381
+ }
382
+ for (const [k, v] of Object.entries(obj)) {
383
+ const childPath = path ? `${path}.${k}` : k;
384
+ walkAlias(v, childPath);
385
+ }
386
+ };
387
+ if (rootAlias) walkAlias(rootAlias);
388
+ } catch {}
389
+ if (fullConfig.includeMetadata) {
390
+ data.metadata = {
391
+ timestamp: Date.now(),
392
+ version: '1.0.0',
393
+ ...(circularPaths.length > 0 && {
394
+ circularRefs: circularPaths
395
+ }),
396
+ ...(Object.keys(nodeMap).length > 0 && {
397
+ nodeMap
398
+ })
399
+ };
400
+ }
401
+ const replacer = createReplacer(fullConfig);
402
+ const json = JSON.stringify(data, replacer, 2);
403
+ return json;
404
+ };
405
+ enhanced.deserialize = (json, config) => {
406
+ const fullConfig = {
407
+ ...DEFAULT_CONFIG,
408
+ ...defaultConfig,
409
+ ...config
410
+ };
411
+ try {
412
+ const parsed = JSON.parse(json);
413
+ const {
414
+ data,
415
+ metadata
416
+ } = parsed;
417
+ if (metadata?.circularRefs && fullConfig.handleCircular) {
418
+ resolveCircularReferences(data, metadata.circularRefs);
419
+ }
420
+ enhanced.fromJSON(data, metadata);
421
+ if (tree.__config?.debugMode) {
422
+ console.log('[SignalTree] State restored from serialized data', {
423
+ timestamp: metadata?.timestamp,
424
+ version: metadata?.version
425
+ });
426
+ }
427
+ } catch (error) {
428
+ console.error('[SignalTree] Failed to deserialize:', error);
429
+ throw new Error(`Failed to deserialize SignalTree state: ${error instanceof Error ? error.message : 'Unknown error'}`);
430
+ }
431
+ };
432
+ enhanced.snapshot = () => {
433
+ const state = enhanced.toJSON();
434
+ const circularPaths = detectCircularReferences(state);
435
+ return {
436
+ data: JSON.parse(JSON.stringify(state)),
437
+ metadata: {
438
+ timestamp: Date.now(),
439
+ version: '1.0.0',
440
+ ...(circularPaths.length > 0 && {
441
+ circularRefs: circularPaths
442
+ })
443
+ }
444
+ };
445
+ };
446
+ enhanced.restore = snapshot => {
447
+ const {
448
+ data,
449
+ metadata
450
+ } = snapshot;
451
+ if (metadata?.circularRefs) {
452
+ resolveCircularReferences(data, metadata.circularRefs);
453
+ }
454
+ enhanced.fromJSON(data, metadata);
455
+ };
456
+ return enhanced;
457
+ };
458
+ enhancer.metadata = {
459
+ name: 'serialization'
460
+ };
461
+ return enhancer;
462
+ }
463
+ function enableSerialization() {
464
+ return withSerialization({
465
+ includeMetadata: true,
466
+ preserveTypes: true,
467
+ handleCircular: true
468
+ });
469
+ }
470
+ function withPersistence(config) {
471
+ const {
472
+ key,
473
+ storage = typeof window !== 'undefined' ? window.localStorage : undefined,
474
+ autoSave = true,
475
+ debounceMs = 1000,
476
+ autoLoad = true,
477
+ ...serializationConfig
478
+ } = config;
479
+ if (!storage) {
480
+ throw new Error('No storage adapter available. Provide a storage adapter in the config.');
481
+ }
482
+ const storageAdapter = storage;
483
+ function enhancer(tree) {
484
+ const serializable = withSerialization(serializationConfig)(tree);
485
+ const enhanced = serializable;
486
+ let lastCacheKey = null;
487
+ enhanced.save = async () => {
488
+ try {
489
+ const cacheKey = enhanced.serialize({
490
+ ...serializationConfig,
491
+ includeMetadata: false
492
+ });
493
+ if (config.skipCache || cacheKey !== lastCacheKey) {
494
+ const serialized = enhanced.serialize(serializationConfig);
495
+ await Promise.resolve(storageAdapter.setItem(key, serialized));
496
+ lastCacheKey = cacheKey;
497
+ if (tree.__config?.debugMode) {
498
+ console.log(`[SignalTree] State saved to storage key: ${key}`);
499
+ }
500
+ } else if (tree.__config?.debugMode) {
501
+ console.log(`[SignalTree] State unchanged, skipping storage write for key: ${key}`);
502
+ }
503
+ } catch (error) {
504
+ console.error('[SignalTree] Failed to save state:', error);
505
+ throw error;
506
+ }
507
+ };
508
+ enhanced.load = async () => {
509
+ try {
510
+ const data = await Promise.resolve(storageAdapter.getItem(key));
511
+ if (data) {
512
+ enhanced.deserialize(data, serializationConfig);
513
+ lastCacheKey = enhanced.serialize({
514
+ ...serializationConfig,
515
+ includeMetadata: false
516
+ });
517
+ if (tree.__config?.debugMode) {
518
+ console.log(`[SignalTree] State loaded from storage key: ${key}`);
519
+ }
520
+ }
521
+ } catch (error) {
522
+ console.error('[SignalTree] Failed to load state:', error);
523
+ throw error;
524
+ }
525
+ };
526
+ enhanced.clear = async () => {
527
+ try {
528
+ await Promise.resolve(storageAdapter.removeItem(key));
529
+ lastCacheKey = null;
530
+ if (tree.__config?.debugMode) {
531
+ console.log(`[SignalTree] State cleared from storage key: ${key}`);
532
+ }
533
+ } catch (error) {
534
+ console.error('[SignalTree] Failed to clear state:', error);
535
+ throw error;
536
+ }
537
+ };
538
+ if (autoLoad) {
539
+ setTimeout(() => {
540
+ enhanced.load().catch(error => {
541
+ console.warn('[SignalTree] Auto-load failed:', error);
542
+ });
543
+ }, 0);
544
+ }
545
+ if (autoSave) {
546
+ let saveTimeout;
547
+ let previousState = JSON.stringify(tree());
548
+ let pollingActive = true;
549
+ const triggerAutoSave = () => {
550
+ if (saveTimeout) {
551
+ clearTimeout(saveTimeout);
552
+ }
553
+ saveTimeout = setTimeout(() => {
554
+ enhanced.save().catch(error => {
555
+ console.error('[SignalTree] Auto-save failed:', error);
556
+ });
557
+ }, debounceMs);
558
+ };
559
+ try {
560
+ tree.subscribe(() => {
561
+ const currentState = JSON.stringify(tree());
562
+ if (currentState !== previousState) {
563
+ previousState = currentState;
564
+ triggerAutoSave();
565
+ }
566
+ });
567
+ } catch {
568
+ const checkForChanges = () => {
569
+ if (!pollingActive) return;
570
+ const currentState = JSON.stringify(tree());
571
+ if (currentState !== previousState) {
572
+ previousState = currentState;
573
+ triggerAutoSave();
574
+ }
575
+ setTimeout(checkForChanges, 100);
576
+ };
577
+ setTimeout(checkForChanges, 0);
578
+ }
579
+ enhanced.__flushAutoSave = () => {
580
+ pollingActive = false;
581
+ if (saveTimeout) {
582
+ clearTimeout(saveTimeout);
583
+ saveTimeout = undefined;
584
+ return enhanced.save();
585
+ }
586
+ return Promise.resolve();
587
+ };
588
+ }
589
+ return enhanced;
590
+ }
591
+ enhancer.metadata = {
592
+ name: 'persistence'
593
+ };
594
+ return enhancer;
595
+ }
596
+ function createStorageAdapter(getItem, setItem, removeItem) {
597
+ return {
598
+ getItem,
599
+ setItem,
600
+ removeItem
601
+ };
602
+ }
603
+ function createIndexedDBAdapter(dbName = 'SignalTreeDB', storeName = 'states') {
604
+ let db = null;
605
+ const openDB = async () => {
606
+ if (db) return db;
607
+ return new Promise((resolve, reject) => {
608
+ const request = indexedDB.open(dbName, 1);
609
+ request.onerror = () => reject(request.error);
610
+ request.onsuccess = () => {
611
+ db = request.result;
612
+ resolve(db);
613
+ };
614
+ request.onupgradeneeded = event => {
615
+ const database = event.target.result;
616
+ if (!database.objectStoreNames.contains(storeName)) {
617
+ database.createObjectStore(storeName);
618
+ }
619
+ };
620
+ });
621
+ };
622
+ return {
623
+ async getItem(key) {
624
+ const database = await openDB();
625
+ return new Promise((resolve, reject) => {
626
+ const transaction = database.transaction([storeName], 'readonly');
627
+ const store = transaction.objectStore(storeName);
628
+ const request = store.get(key);
629
+ request.onerror = () => reject(request.error);
630
+ request.onsuccess = () => resolve(request.result || null);
631
+ });
632
+ },
633
+ async setItem(key, value) {
634
+ const database = await openDB();
635
+ return new Promise((resolve, reject) => {
636
+ const transaction = database.transaction([storeName], 'readwrite');
637
+ const store = transaction.objectStore(storeName);
638
+ const request = store.put(value, key);
639
+ request.onerror = () => reject(request.error);
640
+ request.onsuccess = () => resolve();
641
+ });
642
+ },
643
+ async removeItem(key) {
644
+ const database = await openDB();
645
+ return new Promise((resolve, reject) => {
646
+ const transaction = database.transaction([storeName], 'readwrite');
647
+ const store = transaction.objectStore(storeName);
648
+ const request = store.delete(key);
649
+ request.onerror = () => reject(request.error);
650
+ request.onsuccess = () => resolve();
651
+ });
652
+ }
653
+ };
654
+ }
655
+ function applySerialization(tree) {
656
+ return withSerialization()(tree);
657
+ }
658
+ function applyPersistence(tree, cfg) {
659
+ return withPersistence(cfg)(tree);
660
+ }
661
+
662
+ export { applyPersistence, applySerialization, createIndexedDBAdapter, createStorageAdapter, enableSerialization, withPersistence, withSerialization };