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