@signaltree/core 5.1.1 → 5.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +161 -0
- package/dist/enhancers/computed/lib/computed.js +21 -0
- package/dist/enhancers/devtools/lib/devtools.js +321 -0
- package/dist/enhancers/entities/lib/entities.js +93 -0
- package/dist/enhancers/index.js +72 -0
- package/dist/enhancers/memoization/lib/memoization.js +410 -0
- package/dist/enhancers/presets/lib/presets.js +87 -0
- package/dist/enhancers/serialization/constants.js +15 -0
- package/dist/enhancers/serialization/lib/serialization.js +662 -0
- package/dist/enhancers/time-travel/lib/time-travel.js +193 -0
- package/dist/index.js +19 -0
- package/dist/is-built-in-object.js +23 -0
- package/dist/lib/async-helpers.js +77 -0
- package/dist/lib/constants.js +56 -0
- package/dist/lib/entity-signal.js +280 -0
- package/dist/lib/memory/memory-manager.js +164 -0
- package/dist/lib/path-notifier.js +106 -0
- package/dist/lib/performance/diff-engine.js +156 -0
- package/dist/lib/performance/path-index.js +156 -0
- package/dist/lib/performance/update-engine.js +188 -0
- package/dist/lib/security/security-validator.js +121 -0
- package/dist/lib/signal-tree.js +625 -0
- package/dist/lib/types.js +9 -0
- package/dist/lib/utils.js +258 -0
- package/dist/lru-cache.js +64 -0
- package/dist/parse-path.js +13 -0
- package/package.json +1 -1
- package/src/enhancers/batching/index.d.ts +1 -0
- package/src/enhancers/batching/lib/batching.d.ts +16 -0
- package/src/enhancers/batching/test-setup.d.ts +3 -0
- package/src/enhancers/computed/index.d.ts +1 -0
- package/src/enhancers/computed/lib/computed.d.ts +12 -0
- package/src/enhancers/devtools/index.d.ts +1 -0
- package/src/enhancers/devtools/lib/devtools.d.ts +77 -0
- package/src/enhancers/devtools/test-setup.d.ts +3 -0
- package/src/enhancers/entities/index.d.ts +1 -0
- package/src/enhancers/entities/lib/entities.d.ts +20 -0
- package/src/enhancers/entities/test-setup.d.ts +3 -0
- package/src/enhancers/index.d.ts +3 -0
- package/src/enhancers/memoization/index.d.ts +1 -0
- package/src/enhancers/memoization/lib/memoization.d.ts +65 -0
- package/src/enhancers/memoization/test-setup.d.ts +3 -0
- package/src/enhancers/presets/index.d.ts +1 -0
- package/src/enhancers/presets/lib/presets.d.ts +11 -0
- package/src/enhancers/presets/test-setup.d.ts +3 -0
- package/src/enhancers/serialization/constants.d.ts +14 -0
- package/src/enhancers/serialization/index.d.ts +2 -0
- package/src/enhancers/serialization/lib/serialization.d.ts +59 -0
- package/src/enhancers/serialization/test-setup.d.ts +3 -0
- package/src/enhancers/time-travel/index.d.ts +1 -0
- package/src/enhancers/time-travel/lib/time-travel.d.ts +36 -0
- package/src/enhancers/time-travel/lib/utils.d.ts +1 -0
- package/src/enhancers/time-travel/test-setup.d.ts +3 -0
- package/src/enhancers/types.d.ts +74 -0
- package/src/index.d.ts +18 -0
- package/src/lib/async-helpers.d.ts +8 -0
- package/src/lib/constants.d.ts +41 -0
- package/src/lib/entity-signal.d.ts +1 -0
- package/src/lib/memory/memory-manager.d.ts +30 -0
- package/src/lib/path-notifier.d.ts +4 -0
- package/src/lib/performance/diff-engine.d.ts +33 -0
- package/src/lib/performance/path-index.d.ts +25 -0
- package/src/lib/performance/update-engine.d.ts +32 -0
- package/src/lib/security/security-validator.d.ts +33 -0
- package/src/lib/signal-tree.d.ts +8 -0
- package/src/lib/types.d.ts +278 -0
- package/src/lib/utils.d.ts +28 -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 };
|