@signaltree/core 7.3.0 → 7.6.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/dist/enhancers/devtools/devtools.js +446 -17
- package/dist/lib/entity-signal.js +4 -5
- package/dist/lib/signal-tree.js +0 -3
- package/dist/lib/utils.js +36 -1
- package/package.json +1 -1
- package/src/lib/types.d.ts +10 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { signal } from '@angular/core';
|
|
1
|
+
import { effect, signal } from '@angular/core';
|
|
2
2
|
import { copyTreeProperties } from '../utils/copy-tree-properties.js';
|
|
3
|
+
import { getPathNotifier } from '../../lib/path-notifier.js';
|
|
4
|
+
import { snapshotState, applyState } from '../../lib/utils.js';
|
|
3
5
|
|
|
4
6
|
function createActivityTracker() {
|
|
5
7
|
const modules = new Map();
|
|
@@ -130,16 +132,149 @@ function createModularMetrics() {
|
|
|
130
132
|
}
|
|
131
133
|
};
|
|
132
134
|
}
|
|
135
|
+
function toArray(value) {
|
|
136
|
+
if (!value) return [];
|
|
137
|
+
return Array.isArray(value) ? value : [value];
|
|
138
|
+
}
|
|
139
|
+
function matchesPattern(pattern, path) {
|
|
140
|
+
if (pattern === '**') return true;
|
|
141
|
+
if (pattern === path) return true;
|
|
142
|
+
if (pattern.endsWith('.*')) {
|
|
143
|
+
const prefix = pattern.slice(0, -2);
|
|
144
|
+
return path.startsWith(prefix + '.');
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
function defaultFormatPath(path) {
|
|
149
|
+
const segments = path.split('.');
|
|
150
|
+
let formatted = '';
|
|
151
|
+
for (const segment of segments) {
|
|
152
|
+
if (!segment) continue;
|
|
153
|
+
if (/^\d+$/.test(segment)) {
|
|
154
|
+
formatted += `[${segment}]`;
|
|
155
|
+
} else {
|
|
156
|
+
formatted += formatted ? `.${segment}` : segment;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return formatted || path;
|
|
160
|
+
}
|
|
161
|
+
function computeChangedPaths(prev, next, maxDepth, maxArrayLength, path = '', depth = 0, output = []) {
|
|
162
|
+
if (prev === next) return output;
|
|
163
|
+
if (depth >= maxDepth) {
|
|
164
|
+
if (path) output.push(path);
|
|
165
|
+
return output;
|
|
166
|
+
}
|
|
167
|
+
if (prev === null || next === null || prev === undefined || next === undefined) {
|
|
168
|
+
if (path) output.push(path);
|
|
169
|
+
return output;
|
|
170
|
+
}
|
|
171
|
+
const prevType = typeof prev;
|
|
172
|
+
const nextType = typeof next;
|
|
173
|
+
if (prevType !== 'object' || nextType !== 'object') {
|
|
174
|
+
if (path) output.push(path);
|
|
175
|
+
return output;
|
|
176
|
+
}
|
|
177
|
+
if (Array.isArray(prev) && Array.isArray(next)) {
|
|
178
|
+
if (prev.length !== next.length) {
|
|
179
|
+
if (path) output.push(path);
|
|
180
|
+
return output;
|
|
181
|
+
}
|
|
182
|
+
if (prev.length > maxArrayLength) {
|
|
183
|
+
if (path) output.push(path);
|
|
184
|
+
return output;
|
|
185
|
+
}
|
|
186
|
+
for (let i = 0; i < prev.length; i += 1) {
|
|
187
|
+
computeChangedPaths(prev[i], next[i], maxDepth, maxArrayLength, path ? `${path}.${i}` : `${i}`, depth + 1, output);
|
|
188
|
+
}
|
|
189
|
+
return output;
|
|
190
|
+
}
|
|
191
|
+
const prevObj = prev;
|
|
192
|
+
const nextObj = next;
|
|
193
|
+
const keys = new Set([...Object.keys(prevObj), ...Object.keys(nextObj)]);
|
|
194
|
+
if (keys.size === 0) {
|
|
195
|
+
if (path) output.push(path);
|
|
196
|
+
return output;
|
|
197
|
+
}
|
|
198
|
+
for (const key of keys) {
|
|
199
|
+
computeChangedPaths(prevObj[key], nextObj[key], maxDepth, maxArrayLength, path ? `${path}.${key}` : key, depth + 1, output);
|
|
200
|
+
}
|
|
201
|
+
return output;
|
|
202
|
+
}
|
|
203
|
+
function sanitizeState(value, options, depth = 0, seen = new WeakSet()) {
|
|
204
|
+
const {
|
|
205
|
+
maxDepth,
|
|
206
|
+
maxArrayLength,
|
|
207
|
+
maxStringLength
|
|
208
|
+
} = options;
|
|
209
|
+
if (value === null || value === undefined) return value;
|
|
210
|
+
if (typeof value === 'string') {
|
|
211
|
+
if (value.length > maxStringLength) {
|
|
212
|
+
return `${value.slice(0, maxStringLength)}…`;
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
|
217
|
+
if (typeof value === 'bigint') return `${value.toString()}n`;
|
|
218
|
+
if (typeof value === 'symbol') return String(value);
|
|
219
|
+
if (typeof value === 'function') return '[Function]';
|
|
220
|
+
if (depth >= maxDepth) return '[MaxDepth]';
|
|
221
|
+
if (value instanceof Date) return value.toISOString();
|
|
222
|
+
if (value instanceof RegExp) return value.toString();
|
|
223
|
+
if (value instanceof Error) {
|
|
224
|
+
return {
|
|
225
|
+
name: value.name,
|
|
226
|
+
message: value.message,
|
|
227
|
+
stack: value.stack
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (typeof value === 'object') {
|
|
231
|
+
const obj = value;
|
|
232
|
+
if (seen.has(obj)) return '[Circular]';
|
|
233
|
+
seen.add(obj);
|
|
234
|
+
if (value instanceof Map) {
|
|
235
|
+
const entries = Array.from(value.entries()).slice(0, maxArrayLength);
|
|
236
|
+
return entries.map(([k, v]) => [sanitizeState(k, options, depth + 1, seen), sanitizeState(v, options, depth + 1, seen)]);
|
|
237
|
+
}
|
|
238
|
+
if (value instanceof Set) {
|
|
239
|
+
const values = Array.from(value.values()).slice(0, maxArrayLength);
|
|
240
|
+
return values.map(v => sanitizeState(v, options, depth + 1, seen));
|
|
241
|
+
}
|
|
242
|
+
if (Array.isArray(value)) {
|
|
243
|
+
const list = value.slice(0, maxArrayLength);
|
|
244
|
+
return list.map(item => sanitizeState(item, options, depth + 1, seen));
|
|
245
|
+
}
|
|
246
|
+
const result = {};
|
|
247
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
248
|
+
result[key] = sanitizeState(val, options, depth + 1, seen);
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
133
254
|
function devTools(config = {}) {
|
|
134
255
|
const {
|
|
135
256
|
enabled = true,
|
|
136
257
|
treeName = 'SignalTree',
|
|
137
258
|
name,
|
|
138
259
|
enableBrowserDevTools = true,
|
|
260
|
+
enableTimeTravel = true,
|
|
139
261
|
enableLogging = true,
|
|
140
|
-
performanceThreshold = 16
|
|
262
|
+
performanceThreshold = 16,
|
|
263
|
+
includePaths,
|
|
264
|
+
excludePaths,
|
|
265
|
+
formatPath,
|
|
266
|
+
rateLimitMs,
|
|
267
|
+
maxSendsPerSecond,
|
|
268
|
+
maxDepth = 10,
|
|
269
|
+
maxArrayLength = 50,
|
|
270
|
+
maxStringLength = 2000,
|
|
271
|
+
serialize
|
|
141
272
|
} = config;
|
|
142
273
|
const displayName = name ?? treeName;
|
|
274
|
+
const pathInclude = toArray(includePaths);
|
|
275
|
+
const pathExclude = toArray(excludePaths);
|
|
276
|
+
const sendRateLimitMs = maxSendsPerSecond && maxSendsPerSecond > 0 ? Math.ceil(1000 / maxSendsPerSecond) : rateLimitMs ?? 0;
|
|
277
|
+
const formatPathFn = formatPath ?? defaultFormatPath;
|
|
143
278
|
return tree => {
|
|
144
279
|
if (!enabled) {
|
|
145
280
|
const noopMethods = {
|
|
@@ -152,9 +287,227 @@ function devTools(config = {}) {
|
|
|
152
287
|
const logger = enableLogging ? createCompositionLogger() : createNoopLogger();
|
|
153
288
|
const metrics = createModularMetrics();
|
|
154
289
|
const compositionHistory = [];
|
|
290
|
+
const compositionChain = [];
|
|
291
|
+
const trackComposition = modules => {
|
|
292
|
+
compositionHistory.push({
|
|
293
|
+
timestamp: new Date(),
|
|
294
|
+
chain: [...modules]
|
|
295
|
+
});
|
|
296
|
+
metrics.updateMetrics({
|
|
297
|
+
compositionChain: modules
|
|
298
|
+
});
|
|
299
|
+
logger.logComposition(modules, 'with');
|
|
300
|
+
};
|
|
155
301
|
const activeProfiles = new Map();
|
|
156
302
|
let browserDevTools = null;
|
|
303
|
+
let isConnected = false;
|
|
304
|
+
let isApplyingExternalState = false;
|
|
305
|
+
let unsubscribeDevTools = null;
|
|
306
|
+
let unsubscribeNotifier = null;
|
|
307
|
+
let unsubscribeFlush = null;
|
|
308
|
+
let pendingPaths = [];
|
|
309
|
+
let effectRef = null;
|
|
310
|
+
let effectPrimed = false;
|
|
311
|
+
let sendScheduled = false;
|
|
312
|
+
let pendingAction = null;
|
|
313
|
+
let pendingExplicitAction = false;
|
|
314
|
+
let pendingSource;
|
|
315
|
+
let pendingDuration;
|
|
316
|
+
let lastSnapshot = undefined;
|
|
317
|
+
let lastSendAt = 0;
|
|
318
|
+
let sendTimer = null;
|
|
319
|
+
const isPathAllowed = path => {
|
|
320
|
+
if (pathInclude.length > 0) {
|
|
321
|
+
const matched = pathInclude.some(pattern => matchesPattern(pattern, path));
|
|
322
|
+
if (!matched) return false;
|
|
323
|
+
}
|
|
324
|
+
if (pathExclude.length > 0) {
|
|
325
|
+
const blocked = pathExclude.some(pattern => matchesPattern(pattern, path));
|
|
326
|
+
if (blocked) return false;
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
};
|
|
330
|
+
const readSnapshot = () => {
|
|
331
|
+
try {
|
|
332
|
+
if ('$' in tree) {
|
|
333
|
+
return snapshotState(tree.$);
|
|
334
|
+
}
|
|
335
|
+
} catch {}
|
|
336
|
+
return originalTreeCall();
|
|
337
|
+
};
|
|
338
|
+
const buildSerializedState = rawState => {
|
|
339
|
+
if (serialize) {
|
|
340
|
+
try {
|
|
341
|
+
return serialize(rawState);
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
return sanitizeState(rawState, {
|
|
345
|
+
maxDepth,
|
|
346
|
+
maxArrayLength,
|
|
347
|
+
maxStringLength
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
const buildAction = (type, payload, meta) => ({
|
|
351
|
+
type,
|
|
352
|
+
...(payload !== undefined && {
|
|
353
|
+
payload
|
|
354
|
+
}),
|
|
355
|
+
...(meta && {
|
|
356
|
+
meta: meta
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
const flushSend = () => {
|
|
360
|
+
sendScheduled = false;
|
|
361
|
+
if (!browserDevTools || isApplyingExternalState) return;
|
|
362
|
+
const rawSnapshot = readSnapshot();
|
|
363
|
+
const currentSnapshot = rawSnapshot ?? {};
|
|
364
|
+
const sanitized = buildSerializedState(currentSnapshot);
|
|
365
|
+
const defaultPaths = lastSnapshot === undefined ? [] : computeChangedPaths(lastSnapshot, currentSnapshot, maxDepth, maxArrayLength);
|
|
366
|
+
const mergedPaths = Array.from(new Set([...pendingPaths, ...defaultPaths.filter(path => path && isPathAllowed(path))]));
|
|
367
|
+
const formattedPaths = mergedPaths.map(path => formatPathFn(path));
|
|
368
|
+
if (pathInclude.length > 0 && formattedPaths.length === 0 && !pendingExplicitAction) {
|
|
369
|
+
pendingAction = null;
|
|
370
|
+
pendingExplicitAction = false;
|
|
371
|
+
pendingSource = undefined;
|
|
372
|
+
pendingDuration = undefined;
|
|
373
|
+
pendingPaths = [];
|
|
374
|
+
lastSnapshot = currentSnapshot;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const effectiveAction = pendingExplicitAction ? pendingAction : formattedPaths.length === 1 ? buildAction(`SignalTree/${formattedPaths[0]}`, formattedPaths[0]) : formattedPaths.length > 1 ? buildAction('SignalTree/batch', formattedPaths) : buildAction('SignalTree/update');
|
|
378
|
+
const actionMeta = {
|
|
379
|
+
timestamp: Date.now(),
|
|
380
|
+
...(pendingSource && {
|
|
381
|
+
source: pendingSource
|
|
382
|
+
}),
|
|
383
|
+
...(pendingDuration !== undefined && {
|
|
384
|
+
duration: pendingDuration,
|
|
385
|
+
slow: pendingDuration > performanceThreshold
|
|
386
|
+
}),
|
|
387
|
+
...(formattedPaths.length > 0 && {
|
|
388
|
+
paths: formattedPaths
|
|
389
|
+
})
|
|
390
|
+
};
|
|
391
|
+
const actionToSend = buildAction(effectiveAction?.type ?? 'SignalTree/update', effectiveAction?.payload, actionMeta);
|
|
392
|
+
try {
|
|
393
|
+
browserDevTools.send(actionToSend, sanitized);
|
|
394
|
+
} catch {} finally {
|
|
395
|
+
pendingAction = null;
|
|
396
|
+
pendingExplicitAction = false;
|
|
397
|
+
pendingSource = undefined;
|
|
398
|
+
pendingDuration = undefined;
|
|
399
|
+
pendingPaths = [];
|
|
400
|
+
lastSnapshot = currentSnapshot;
|
|
401
|
+
lastSendAt = Date.now();
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
const scheduleSend = (action, meta) => {
|
|
405
|
+
if (isApplyingExternalState) return;
|
|
406
|
+
if (action !== undefined) {
|
|
407
|
+
pendingAction = action;
|
|
408
|
+
pendingExplicitAction = true;
|
|
409
|
+
}
|
|
410
|
+
if (meta?.source) {
|
|
411
|
+
if (!pendingSource) {
|
|
412
|
+
pendingSource = meta.source;
|
|
413
|
+
} else if (pendingSource !== meta.source) {
|
|
414
|
+
pendingSource = 'mixed';
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (meta?.duration !== undefined) {
|
|
418
|
+
pendingDuration = pendingDuration === undefined ? meta.duration : Math.max(pendingDuration, meta.duration);
|
|
419
|
+
}
|
|
420
|
+
if (!browserDevTools) return;
|
|
421
|
+
if (sendScheduled) return;
|
|
422
|
+
sendScheduled = true;
|
|
423
|
+
queueMicrotask(() => {
|
|
424
|
+
if (!browserDevTools) {
|
|
425
|
+
sendScheduled = false;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const now = Date.now();
|
|
429
|
+
const waitMs = Math.max(0, sendRateLimitMs - (now - lastSendAt));
|
|
430
|
+
if (waitMs > 0) {
|
|
431
|
+
if (sendTimer) return;
|
|
432
|
+
sendTimer = setTimeout(() => {
|
|
433
|
+
sendTimer = null;
|
|
434
|
+
flushSend();
|
|
435
|
+
}, waitMs);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
flushSend();
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
const parseDevToolsState = state => {
|
|
442
|
+
if (typeof state === 'string') {
|
|
443
|
+
try {
|
|
444
|
+
return JSON.parse(state);
|
|
445
|
+
} catch {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return state;
|
|
450
|
+
};
|
|
451
|
+
const applyExternalState = state => {
|
|
452
|
+
if (state === undefined || state === null) return;
|
|
453
|
+
isApplyingExternalState = true;
|
|
454
|
+
try {
|
|
455
|
+
if ('$' in tree) {
|
|
456
|
+
applyState(tree.$, state);
|
|
457
|
+
} else {
|
|
458
|
+
originalTreeCall(state);
|
|
459
|
+
}
|
|
460
|
+
} finally {
|
|
461
|
+
isApplyingExternalState = false;
|
|
462
|
+
lastSnapshot = readSnapshot();
|
|
463
|
+
pendingPaths = [];
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const handleDevToolsMessage = message => {
|
|
467
|
+
if (!enableTimeTravel) return;
|
|
468
|
+
if (!message || typeof message !== 'object') return;
|
|
469
|
+
const msg = message;
|
|
470
|
+
if (msg.type !== 'DISPATCH' || !msg.payload?.type) return;
|
|
471
|
+
const actionType = msg.payload.type;
|
|
472
|
+
if (actionType === 'JUMP_TO_STATE' || actionType === 'JUMP_TO_ACTION') {
|
|
473
|
+
const nextState = parseDevToolsState(msg.state);
|
|
474
|
+
applyExternalState(nextState);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (actionType === 'ROLLBACK') {
|
|
478
|
+
const nextState = parseDevToolsState(msg.state);
|
|
479
|
+
applyExternalState(nextState);
|
|
480
|
+
if (browserDevTools) {
|
|
481
|
+
const rawSnapshot = readSnapshot();
|
|
482
|
+
const sanitized = buildSerializedState(rawSnapshot);
|
|
483
|
+
browserDevTools.send('@@INIT', sanitized);
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (actionType === 'COMMIT') {
|
|
488
|
+
if (browserDevTools) {
|
|
489
|
+
const rawSnapshot = readSnapshot();
|
|
490
|
+
const sanitized = buildSerializedState(rawSnapshot);
|
|
491
|
+
browserDevTools.send('@@INIT', sanitized);
|
|
492
|
+
}
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (actionType === 'IMPORT_STATE') {
|
|
496
|
+
const lifted = msg.payload.nextLiftedState;
|
|
497
|
+
const computedStates = lifted?.computedStates ?? [];
|
|
498
|
+
const index = lifted?.currentStateIndex ?? computedStates.length - 1;
|
|
499
|
+
const entry = computedStates[index];
|
|
500
|
+
const nextState = parseDevToolsState(entry?.state);
|
|
501
|
+
applyExternalState(nextState);
|
|
502
|
+
if (browserDevTools) {
|
|
503
|
+
const rawSnapshot = readSnapshot();
|
|
504
|
+
const sanitized = buildSerializedState(rawSnapshot);
|
|
505
|
+
browserDevTools.send('@@INIT', sanitized);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
};
|
|
157
509
|
const initBrowserDevTools = () => {
|
|
510
|
+
if (isConnected) return;
|
|
158
511
|
if (!enableBrowserDevTools || typeof window === 'undefined' || !('__REDUX_DEVTOOLS_EXTENSION__' in window)) {
|
|
159
512
|
return;
|
|
160
513
|
}
|
|
@@ -169,9 +522,20 @@ function devTools(config = {}) {
|
|
|
169
522
|
}
|
|
170
523
|
});
|
|
171
524
|
browserDevTools = {
|
|
172
|
-
send: connection.send
|
|
525
|
+
send: connection.send,
|
|
526
|
+
subscribe: connection.subscribe
|
|
173
527
|
};
|
|
174
|
-
browserDevTools.
|
|
528
|
+
if (browserDevTools.subscribe && !unsubscribeDevTools) {
|
|
529
|
+
browserDevTools.subscribe(handleDevToolsMessage);
|
|
530
|
+
unsubscribeDevTools = () => {
|
|
531
|
+
browserDevTools?.subscribe?.(() => void 0);
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const rawSnapshot = readSnapshot();
|
|
535
|
+
const sanitized = buildSerializedState(rawSnapshot);
|
|
536
|
+
browserDevTools.send('@@INIT', sanitized);
|
|
537
|
+
lastSnapshot = rawSnapshot;
|
|
538
|
+
isConnected = true;
|
|
175
539
|
console.log(`🔗 Connected to Redux DevTools as "${displayName}"`);
|
|
176
540
|
} catch (e) {
|
|
177
541
|
console.warn('[SignalTree] Failed to connect to Redux DevTools:', e);
|
|
@@ -193,13 +557,16 @@ function devTools(config = {}) {
|
|
|
193
557
|
}
|
|
194
558
|
}
|
|
195
559
|
const duration = performance.now() - startTime;
|
|
196
|
-
|
|
560
|
+
originalTreeCall();
|
|
197
561
|
metrics.trackModuleUpdate('core', duration);
|
|
198
562
|
if (duration > performanceThreshold) {
|
|
199
563
|
logger.logPerformanceWarning('core', 'update', duration, performanceThreshold);
|
|
200
564
|
}
|
|
201
565
|
if (browserDevTools) {
|
|
202
|
-
|
|
566
|
+
scheduleSend(undefined, {
|
|
567
|
+
source: 'tree.update',
|
|
568
|
+
duration
|
|
569
|
+
});
|
|
203
570
|
}
|
|
204
571
|
return result;
|
|
205
572
|
};
|
|
@@ -210,6 +577,15 @@ function devTools(config = {}) {
|
|
|
210
577
|
if (typeof enhancer !== 'function') {
|
|
211
578
|
throw new Error('Enhancer must be a function');
|
|
212
579
|
}
|
|
580
|
+
const enhancerName = enhancer.name || 'anonymousEnhancer';
|
|
581
|
+
compositionChain.push(enhancerName);
|
|
582
|
+
trackComposition([...compositionChain]);
|
|
583
|
+
scheduleSend(buildAction('SignalTree/with', {
|
|
584
|
+
enhancer: enhancerName,
|
|
585
|
+
chain: [...compositionChain]
|
|
586
|
+
}), {
|
|
587
|
+
source: 'composition'
|
|
588
|
+
});
|
|
213
589
|
return enhancer(enhancedTree);
|
|
214
590
|
},
|
|
215
591
|
writable: false,
|
|
@@ -234,16 +610,7 @@ function devTools(config = {}) {
|
|
|
234
610
|
activityTracker,
|
|
235
611
|
logger,
|
|
236
612
|
metrics: metrics.signal,
|
|
237
|
-
trackComposition
|
|
238
|
-
compositionHistory.push({
|
|
239
|
-
timestamp: new Date(),
|
|
240
|
-
chain: [...modules]
|
|
241
|
-
});
|
|
242
|
-
metrics.updateMetrics({
|
|
243
|
-
compositionChain: modules
|
|
244
|
-
});
|
|
245
|
-
logger.logComposition(modules, 'with');
|
|
246
|
-
},
|
|
613
|
+
trackComposition,
|
|
247
614
|
startModuleProfiling: module => {
|
|
248
615
|
const profileId = `${module}_${Date.now()}`;
|
|
249
616
|
activeProfiles.set(profileId, {
|
|
@@ -262,8 +629,14 @@ function devTools(config = {}) {
|
|
|
262
629
|
}
|
|
263
630
|
},
|
|
264
631
|
connectDevTools: name => {
|
|
632
|
+
if (!browserDevTools || !isConnected) {
|
|
633
|
+
initBrowserDevTools();
|
|
634
|
+
}
|
|
265
635
|
if (browserDevTools) {
|
|
266
|
-
|
|
636
|
+
const rawSnapshot = readSnapshot();
|
|
637
|
+
const sanitized = buildSerializedState(rawSnapshot);
|
|
638
|
+
browserDevTools.send('@@INIT', sanitized);
|
|
639
|
+
lastSnapshot = rawSnapshot;
|
|
267
640
|
console.log(`🔗 Connected to Redux DevTools as "${name}"`);
|
|
268
641
|
}
|
|
269
642
|
},
|
|
@@ -280,9 +653,65 @@ function devTools(config = {}) {
|
|
|
280
653
|
},
|
|
281
654
|
disconnectDevTools() {
|
|
282
655
|
browserDevTools = null;
|
|
656
|
+
isConnected = false;
|
|
657
|
+
if (unsubscribeNotifier) {
|
|
658
|
+
unsubscribeNotifier();
|
|
659
|
+
unsubscribeNotifier = null;
|
|
660
|
+
}
|
|
661
|
+
if (unsubscribeFlush) {
|
|
662
|
+
unsubscribeFlush();
|
|
663
|
+
unsubscribeFlush = null;
|
|
664
|
+
}
|
|
665
|
+
if (unsubscribeDevTools) {
|
|
666
|
+
unsubscribeDevTools();
|
|
667
|
+
unsubscribeDevTools = null;
|
|
668
|
+
}
|
|
669
|
+
if (effectRef) {
|
|
670
|
+
effectRef.destroy();
|
|
671
|
+
effectRef = null;
|
|
672
|
+
}
|
|
673
|
+
if (sendTimer) {
|
|
674
|
+
clearTimeout(sendTimer);
|
|
675
|
+
sendTimer = null;
|
|
676
|
+
}
|
|
677
|
+
pendingPaths = [];
|
|
678
|
+
sendScheduled = false;
|
|
679
|
+
pendingAction = null;
|
|
680
|
+
pendingExplicitAction = false;
|
|
681
|
+
pendingSource = undefined;
|
|
682
|
+
pendingDuration = undefined;
|
|
683
|
+
lastSnapshot = undefined;
|
|
283
684
|
}
|
|
284
685
|
};
|
|
285
686
|
enhancedTree['__devTools'] = devToolsInterface;
|
|
687
|
+
try {
|
|
688
|
+
initBrowserDevTools();
|
|
689
|
+
const notifier = getPathNotifier();
|
|
690
|
+
unsubscribeNotifier = notifier.subscribe('**', (_value, _prev, path) => {
|
|
691
|
+
if (!isPathAllowed(path)) return;
|
|
692
|
+
pendingPaths.push(path);
|
|
693
|
+
});
|
|
694
|
+
unsubscribeFlush = notifier.onFlush(() => {
|
|
695
|
+
if (!browserDevTools) {
|
|
696
|
+
pendingPaths = [];
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (pendingPaths.length === 0) return;
|
|
700
|
+
scheduleSend(undefined, {
|
|
701
|
+
source: 'path-notifier'
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
effectRef = effect(() => {
|
|
705
|
+
void originalTreeCall();
|
|
706
|
+
if (!effectPrimed) {
|
|
707
|
+
effectPrimed = true;
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
scheduleSend(undefined, {
|
|
711
|
+
source: 'signal'
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
} catch {}
|
|
286
715
|
return Object.assign(enhancedTree, methods);
|
|
287
716
|
};
|
|
288
717
|
}
|
|
@@ -20,13 +20,11 @@ function createEntitySignal(config, pathNotifier, basePath) {
|
|
|
20
20
|
mapSignal.set(map);
|
|
21
21
|
}
|
|
22
22
|
function createEntityNode(id, entity) {
|
|
23
|
-
const node = () =>
|
|
23
|
+
const node = () => mapSignal().get(id);
|
|
24
24
|
for (const key of Object.keys(entity)) {
|
|
25
25
|
Object.defineProperty(node, key, {
|
|
26
26
|
get: () => {
|
|
27
|
-
|
|
28
|
-
const value = current?.[key];
|
|
29
|
-
return () => value;
|
|
27
|
+
return () => mapSignal().get(id)?.[key];
|
|
30
28
|
},
|
|
31
29
|
enumerable: true,
|
|
32
30
|
configurable: true
|
|
@@ -46,7 +44,8 @@ function createEntitySignal(config, pathNotifier, basePath) {
|
|
|
46
44
|
const findCache = new WeakMap();
|
|
47
45
|
const api = {
|
|
48
46
|
byId(id) {
|
|
49
|
-
const
|
|
47
|
+
const map = mapSignal();
|
|
48
|
+
const entity = map.get(id);
|
|
50
49
|
if (!entity) return undefined;
|
|
51
50
|
return getOrCreateNode(id, entity);
|
|
52
51
|
},
|
package/dist/lib/signal-tree.js
CHANGED
package/dist/lib/utils.js
CHANGED
|
@@ -260,5 +260,40 @@ function unwrap(node) {
|
|
|
260
260
|
function snapshotState(state) {
|
|
261
261
|
return unwrap(state);
|
|
262
262
|
}
|
|
263
|
+
function applyState(stateNode, snapshot) {
|
|
264
|
+
if (snapshot === null || snapshot === undefined) return;
|
|
265
|
+
if (typeof snapshot !== 'object') return;
|
|
266
|
+
for (const key of Object.keys(snapshot)) {
|
|
267
|
+
const val = snapshot[key];
|
|
268
|
+
const target = stateNode[key];
|
|
269
|
+
if (isNodeAccessor(target)) {
|
|
270
|
+
if (val && typeof val === 'object') {
|
|
271
|
+
try {
|
|
272
|
+
applyState(target, val);
|
|
273
|
+
} catch {
|
|
274
|
+
try {
|
|
275
|
+
target(val);
|
|
276
|
+
} catch {}
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
try {
|
|
280
|
+
target(val);
|
|
281
|
+
} catch {}
|
|
282
|
+
}
|
|
283
|
+
} else if (isSignal(target)) {
|
|
284
|
+
try {
|
|
285
|
+
target.set?.(val);
|
|
286
|
+
} catch {
|
|
287
|
+
try {
|
|
288
|
+
target(val);
|
|
289
|
+
} catch {}
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
try {
|
|
293
|
+
stateNode[key] = val;
|
|
294
|
+
} catch {}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
263
298
|
|
|
264
|
-
export { composeEnhancers, createLazySignalTree, isAnySignal, isBuiltInObject, isEntityMapMarker, isNodeAccessor, snapshotState, toWritableSignal, unwrap };
|
|
299
|
+
export { applyState, composeEnhancers, createLazySignalTree, isAnySignal, isBuiltInObject, isEntityMapMarker, isNodeAccessor, snapshotState, toWritableSignal, unwrap };
|
package/package.json
CHANGED
package/src/lib/types.d.ts
CHANGED
|
@@ -246,11 +246,21 @@ export interface DevToolsConfig {
|
|
|
246
246
|
enableBrowserDevTools?: boolean;
|
|
247
247
|
enableLogging?: boolean;
|
|
248
248
|
performanceThreshold?: number;
|
|
249
|
+
enableTimeTravel?: boolean;
|
|
249
250
|
name?: string;
|
|
250
251
|
treeName?: string;
|
|
251
252
|
enabled?: boolean;
|
|
252
253
|
logActions?: boolean;
|
|
253
254
|
maxAge?: number;
|
|
255
|
+
rateLimitMs?: number;
|
|
256
|
+
maxSendsPerSecond?: number;
|
|
257
|
+
includePaths?: string[];
|
|
258
|
+
excludePaths?: string[];
|
|
259
|
+
formatPath?: (path: string) => string;
|
|
260
|
+
maxDepth?: number;
|
|
261
|
+
maxArrayLength?: number;
|
|
262
|
+
maxStringLength?: number;
|
|
263
|
+
serialize?: (state: unknown) => unknown;
|
|
254
264
|
features?: {
|
|
255
265
|
jump?: boolean;
|
|
256
266
|
skip?: boolean;
|