@signaltree/core 8.0.0 → 8.0.1

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.
@@ -1,10 +1,11 @@
1
- import { effect, signal } from '@angular/core';
1
+ import { signal } from '@angular/core';
2
2
  import { copyTreeProperties } from '../utils/copy-tree-properties.js';
3
+ import { applyState, snapshotState } from '../../lib/utils.js';
3
4
  import { getPathNotifier } from '../../lib/path-notifier.js';
4
- import { snapshotState, applyState } from '../../lib/utils.js';
5
5
 
6
- function createActivityTracker() {
6
+ function createActivityTracker(options) {
7
7
  const modules = new Map();
8
+ const enableConsole = options?.enableConsole === true;
8
9
  return {
9
10
  trackMethodCall: (module, method, duration) => {
10
11
  const existing = modules.get(module);
@@ -29,13 +30,15 @@ function createActivityTracker() {
29
30
  if (existing) {
30
31
  existing.errorCount++;
31
32
  }
32
- console.error(`❌ [${module}] Error${context ? ` in ${context}` : ''}:`, error);
33
+ if (enableConsole) {
34
+ console.error(`❌ [${module}] Error${context ? ` in ${context}` : ''}:`, error);
35
+ }
33
36
  },
34
37
  getModuleActivity: module => modules.get(module),
35
38
  getAllModules: () => Array.from(modules.values())
36
39
  };
37
40
  }
38
- function createCompositionLogger() {
41
+ function createCompositionLogger(options) {
39
42
  const logs = [];
40
43
  const addLog = (module, type, data) => {
41
44
  logs.push({
@@ -54,7 +57,9 @@ function createCompositionLogger() {
54
57
  modules,
55
58
  action
56
59
  });
57
- console.log(`🔗 Composition ${action}:`, modules.join(' → '));
60
+ {
61
+ console.log(`🔗 Composition ${action}:`, modules.join(' → '));
62
+ }
58
63
  },
59
64
  logMethodExecution: (module, method, args, result) => {
60
65
  addLog(module, 'method', {
@@ -62,10 +67,12 @@ function createCompositionLogger() {
62
67
  args,
63
68
  result
64
69
  });
65
- console.debug(`🔧 [${module}] ${method}`, {
66
- args,
67
- result
68
- });
70
+ {
71
+ console.debug(`🔧 [${module}] ${method}`, {
72
+ args,
73
+ result
74
+ });
75
+ }
69
76
  },
70
77
  logStateChange: (module, path, oldValue, newValue) => {
71
78
  addLog(module, 'state', {
@@ -73,10 +80,12 @@ function createCompositionLogger() {
73
80
  oldValue,
74
81
  newValue
75
82
  });
76
- console.debug(`📝 [${module}] State change at ${path}:`, {
77
- from: oldValue,
78
- to: newValue
79
- });
83
+ {
84
+ console.debug(`📝 [${module}] State change at ${path}:`, {
85
+ from: oldValue,
86
+ to: newValue
87
+ });
88
+ }
80
89
  },
81
90
  logPerformanceWarning: (module, operation, duration, threshold) => {
82
91
  addLog(module, 'performance', {
@@ -84,7 +93,9 @@ function createCompositionLogger() {
84
93
  duration,
85
94
  threshold
86
95
  });
87
- console.warn(`⚠️ [${module}] Slow ${operation}: ${duration.toFixed(2)}ms (threshold: ${threshold}ms)`);
96
+ {
97
+ console.warn(`⚠️ [${module}] Slow ${operation}: ${duration.toFixed(2)}ms (threshold: ${threshold}ms)`);
98
+ }
88
99
  },
89
100
  exportLogs: () => [...logs]
90
101
  };
@@ -252,6 +263,415 @@ function sanitizeState(value, options, depth = 0, seen = new WeakSet()) {
252
263
  }
253
264
  return value;
254
265
  }
266
+ function parseDevToolsState(state) {
267
+ if (typeof state === 'string') {
268
+ try {
269
+ return JSON.parse(state);
270
+ } catch {
271
+ return undefined;
272
+ }
273
+ }
274
+ return state;
275
+ }
276
+ function buildDevToolsAction(type, payload, meta) {
277
+ return {
278
+ type,
279
+ ...(payload !== undefined && {
280
+ payload
281
+ }),
282
+ ...(meta && {
283
+ meta: meta
284
+ })
285
+ };
286
+ }
287
+ const GLOBAL_GROUPS_KEY = '__SIGNALTREE_DEVTOOLS_GROUPS__';
288
+ const GLOBAL_MARKER_KEY = '__SIGNALTREE_DEVTOOLS_GLOBAL_MARKER__';
289
+ function getRegistryHost() {
290
+ return typeof window !== 'undefined' ? window : globalThis;
291
+ }
292
+ function ensureGlobalMarker() {
293
+ const host = getRegistryHost();
294
+ if (!host[GLOBAL_MARKER_KEY]) {
295
+ host[GLOBAL_MARKER_KEY] = `marker_${Math.random().toString(36).slice(2)}`;
296
+ }
297
+ return host[GLOBAL_MARKER_KEY];
298
+ }
299
+ function getGlobalDevToolsGroups() {
300
+ const host = getRegistryHost();
301
+ if (!host[GLOBAL_GROUPS_KEY]) {
302
+ host[GLOBAL_GROUPS_KEY] = new Map();
303
+ }
304
+ return host[GLOBAL_GROUPS_KEY];
305
+ }
306
+ const devToolsGroups = getGlobalDevToolsGroups();
307
+ const GLOBAL_CONNECTIONS_KEY = '__SIGNALTREE_DEVTOOLS_CONNECTIONS__';
308
+ function getGlobalDevToolsConnections() {
309
+ const host = getRegistryHost();
310
+ ensureGlobalMarker();
311
+ if (!host[GLOBAL_CONNECTIONS_KEY]) {
312
+ host[GLOBAL_CONNECTIONS_KEY] = new Map();
313
+ }
314
+ return host[GLOBAL_CONNECTIONS_KEY];
315
+ }
316
+ const devToolsConnections = getGlobalDevToolsConnections();
317
+ function getOrCreateDevToolsGroup(groupId, displayName) {
318
+ if (devToolsGroups.has(groupId)) {
319
+ return devToolsGroups.get(groupId);
320
+ }
321
+ const trees = new Map();
322
+ const lastSnapshots = new Map();
323
+ const lastSerialized = new Map();
324
+ const pendingPathsByTree = new Map();
325
+ let browserDevToolsConnection = null;
326
+ let browserDevTools = null;
327
+ let devToolsExtension = null;
328
+ let unsubscribeDevTools = null;
329
+ let isConnected = false;
330
+ let isApplyingExternalState = false;
331
+ let sendScheduled = false;
332
+ let sendTimer = null;
333
+ let lastSendAt = 0;
334
+ let sendRateLimitMs = 0;
335
+ let pendingAction = null;
336
+ let pendingExplicitAction = false;
337
+ let pendingSource;
338
+ let pendingDuration;
339
+ const sendAggregated = (actionType, payload) => {
340
+ if (!browserDevTools) return;
341
+ const aggregated = buildAggregatedState();
342
+ browserDevTools.send(buildDevToolsAction(actionType, payload), aggregated);
343
+ };
344
+ const sendEmptyState = () => {
345
+ try {
346
+ sendAggregated('SignalTree/empty', {
347
+ groupId
348
+ });
349
+ } catch {}
350
+ pendingPathsByTree.clear();
351
+ lastSnapshots.clear();
352
+ lastSerialized.clear();
353
+ };
354
+ const updateRateLimit = () => {
355
+ sendRateLimitMs = 0;
356
+ for (const tree of trees.values()) {
357
+ if (typeof tree.sendRateLimitMs === 'number') {
358
+ sendRateLimitMs = Math.max(sendRateLimitMs, tree.sendRateLimitMs);
359
+ }
360
+ }
361
+ };
362
+ const buildAggregatedState = () => {
363
+ const aggregated = {};
364
+ for (const [treeKey, tree] of trees) {
365
+ const snapshot = tree.readSnapshot();
366
+ const lastSnapshot = lastSnapshots.get(treeKey);
367
+ let serialized;
368
+ if (lastSnapshot === snapshot && lastSerialized.has(treeKey)) {
369
+ serialized = lastSerialized.get(treeKey);
370
+ } else {
371
+ lastSnapshots.set(treeKey, snapshot);
372
+ serialized = tree.buildSerializedState(snapshot);
373
+ lastSerialized.set(treeKey, serialized);
374
+ }
375
+ aggregated[treeKey] = serialized;
376
+ }
377
+ return aggregated;
378
+ };
379
+ const sendInit = () => {
380
+ if (!browserDevTools) return;
381
+ const aggregated = buildAggregatedState();
382
+ browserDevTools.send('@@INIT', aggregated);
383
+ };
384
+ const applyExternalState = state => {
385
+ if (state === undefined || state === null) return;
386
+ isApplyingExternalState = true;
387
+ try {
388
+ for (const [treeKey, tree] of trees) {
389
+ if (!tree.enableTimeTravel) continue;
390
+ const treeState = state?.[treeKey];
391
+ if (treeState !== undefined) {
392
+ tree.applyExternalState(treeState);
393
+ }
394
+ }
395
+ } finally {
396
+ isApplyingExternalState = false;
397
+ for (const treeKey of trees.keys()) {
398
+ lastSnapshots.delete(treeKey);
399
+ pendingPathsByTree.delete(treeKey);
400
+ }
401
+ }
402
+ };
403
+ const handleDevToolsMessage = message => {
404
+ if (!message || typeof message !== 'object') return;
405
+ const msg = message;
406
+ const messageType = typeof msg.type === 'string' ? msg.type : undefined;
407
+ if (messageType === 'START') {
408
+ if (browserDevTools && trees.size > 0) {
409
+ const elapsed = Date.now() - lastSendAt;
410
+ if (elapsed < 500) return;
411
+ const aggregated = buildAggregatedState();
412
+ browserDevTools.send({
413
+ type: 'SignalTree/reconnect'
414
+ }, aggregated);
415
+ lastSendAt = Date.now();
416
+ }
417
+ return;
418
+ }
419
+ const anyTimeTravelEnabled = Array.from(trees.values()).some(t => t.enableTimeTravel);
420
+ if (!anyTimeTravelEnabled) return;
421
+ if (messageType !== 'DISPATCH') return;
422
+ const actionType = msg.payload && typeof msg.payload.type === 'string' ? msg.payload.type : undefined;
423
+ if (!actionType) return;
424
+ if (actionType === 'JUMP_TO_STATE' || actionType === 'JUMP_TO_ACTION') {
425
+ const nextState = parseDevToolsState(msg.state);
426
+ applyExternalState(nextState);
427
+ return;
428
+ }
429
+ if (actionType === 'ROLLBACK') {
430
+ const nextState = parseDevToolsState(msg.state);
431
+ applyExternalState(nextState);
432
+ sendInit();
433
+ return;
434
+ }
435
+ if (actionType === 'COMMIT') {
436
+ sendInit();
437
+ return;
438
+ }
439
+ if (actionType === 'IMPORT_STATE') {
440
+ const lifted = msg.payload?.nextLiftedState;
441
+ const computedStates = Array.isArray(lifted?.computedStates) ? lifted.computedStates : [];
442
+ const indexRaw = typeof lifted?.currentStateIndex === 'number' ? lifted.currentStateIndex : computedStates.length - 1;
443
+ const index = Math.max(0, Math.min(indexRaw, computedStates.length - 1));
444
+ const entry = computedStates[index];
445
+ const nextState = parseDevToolsState(entry?.state);
446
+ applyExternalState(nextState);
447
+ sendInit();
448
+ }
449
+ };
450
+ const initBrowserDevTools = () => {
451
+ if (isConnected) return;
452
+ if (typeof window === 'undefined' || !('__REDUX_DEVTOOLS_EXTENSION__' in window)) {
453
+ return;
454
+ }
455
+ const waiters = new Set();
456
+ devToolsConnections.set(groupId, {
457
+ status: 'connecting',
458
+ connection: null,
459
+ tools: null,
460
+ subscribed: false,
461
+ unsubscribe: null,
462
+ waiters
463
+ });
464
+ try {
465
+ const devToolsExt = window['__REDUX_DEVTOOLS_EXTENSION__'];
466
+ devToolsExtension = devToolsExt;
467
+ const connection = devToolsExt.connect({
468
+ name: displayName,
469
+ instanceId: groupId,
470
+ features: {
471
+ dispatch: true,
472
+ jump: true,
473
+ skip: true
474
+ }
475
+ });
476
+ browserDevToolsConnection = connection;
477
+ browserDevTools = {
478
+ send: connection.send,
479
+ subscribe: connection.subscribe
480
+ };
481
+ if (browserDevTools.subscribe && !unsubscribeDevTools) {
482
+ const maybeUnsubscribe = browserDevTools.subscribe(handleDevToolsMessage);
483
+ if (typeof maybeUnsubscribe === 'function') {
484
+ unsubscribeDevTools = maybeUnsubscribe;
485
+ } else {
486
+ unsubscribeDevTools = () => {
487
+ browserDevTools?.subscribe?.(() => void 0);
488
+ };
489
+ }
490
+ }
491
+ const connEntry = devToolsConnections.get(groupId);
492
+ if (connEntry) {
493
+ connEntry.status = 'connected';
494
+ connEntry.connection = browserDevToolsConnection;
495
+ connEntry.tools = browserDevTools;
496
+ connEntry.subscribed = !!browserDevTools?.subscribe;
497
+ connEntry.unsubscribe = unsubscribeDevTools;
498
+ for (const waiter of connEntry.waiters) {
499
+ try {
500
+ waiter(connEntry);
501
+ } catch {}
502
+ }
503
+ connEntry.waiters.clear();
504
+ }
505
+ sendInit();
506
+ isConnected = true;
507
+ } catch (e) {
508
+ devToolsConnections.delete(groupId);
509
+ console.warn('[SignalTree] Failed to connect to Redux DevTools:', e);
510
+ }
511
+ };
512
+ const flushSend = () => {
513
+ sendScheduled = false;
514
+ if (!browserDevTools || isApplyingExternalState) return;
515
+ const aggregatedState = {};
516
+ const allFormattedPaths = [];
517
+ for (const [treeKey, tree] of trees) {
518
+ const currentSnapshot = tree.readSnapshot();
519
+ lastSnapshots.set(treeKey, currentSnapshot);
520
+ const serialized = tree.buildSerializedState(currentSnapshot);
521
+ lastSerialized.set(treeKey, serialized);
522
+ aggregatedState[treeKey] = serialized;
523
+ const pendingRaw = pendingPathsByTree.get(treeKey) ?? [];
524
+ const allowedRaw = pendingRaw.filter(p => p && tree.isPathAllowed(p));
525
+ if (allowedRaw.length > 0) {
526
+ const formatted = Array.from(new Set(allowedRaw)).map(path => tree.formatPathFn(`${treeKey}.${path}`));
527
+ allFormattedPaths.push(...formatted);
528
+ }
529
+ }
530
+ const hasMeta = pendingSource !== undefined || pendingDuration !== undefined;
531
+ if (allFormattedPaths.length === 0 && !pendingExplicitAction && !hasMeta) {
532
+ pendingAction = null;
533
+ pendingExplicitAction = false;
534
+ pendingSource = undefined;
535
+ pendingDuration = undefined;
536
+ pendingPathsByTree.clear();
537
+ lastSendAt = Date.now();
538
+ return;
539
+ }
540
+ const effectiveAction = pendingExplicitAction && pendingAction ? pendingAction : allFormattedPaths.length === 1 ? buildDevToolsAction(`SignalTree/${allFormattedPaths[0]}`, allFormattedPaths[0]) : allFormattedPaths.length > 1 ? buildDevToolsAction('SignalTree/update', allFormattedPaths) : buildDevToolsAction('SignalTree/update');
541
+ const actionMeta = {
542
+ timestamp: Date.now(),
543
+ ...(pendingSource && {
544
+ source: pendingSource
545
+ }),
546
+ ...(pendingDuration !== undefined && {
547
+ duration: pendingDuration,
548
+ slow: pendingDuration > 16
549
+ }),
550
+ ...(allFormattedPaths.length > 0 && {
551
+ paths: allFormattedPaths
552
+ })
553
+ };
554
+ const actionToSend = buildDevToolsAction(effectiveAction?.type ?? 'SignalTree/update', effectiveAction?.payload, actionMeta);
555
+ try {
556
+ browserDevTools.send(actionToSend, aggregatedState);
557
+ } catch {} finally {
558
+ pendingAction = null;
559
+ pendingExplicitAction = false;
560
+ pendingSource = undefined;
561
+ pendingDuration = undefined;
562
+ pendingPathsByTree.clear();
563
+ lastSendAt = Date.now();
564
+ }
565
+ };
566
+ const scheduleSend = (action, meta) => {
567
+ if (isApplyingExternalState) return;
568
+ if (action !== undefined) {
569
+ if (!pendingExplicitAction && pendingAction == null) {
570
+ pendingAction = action;
571
+ pendingExplicitAction = true;
572
+ } else {
573
+ pendingAction = null;
574
+ pendingExplicitAction = false;
575
+ }
576
+ }
577
+ if (meta?.source) {
578
+ if (!pendingSource) {
579
+ pendingSource = meta.source;
580
+ } else if (pendingSource !== meta.source) {
581
+ pendingSource = 'mixed';
582
+ }
583
+ }
584
+ if (meta?.duration !== undefined) {
585
+ pendingDuration = pendingDuration === undefined ? meta.duration : Math.max(pendingDuration, meta.duration);
586
+ }
587
+ if (!browserDevTools) return;
588
+ if (sendScheduled) return;
589
+ sendScheduled = true;
590
+ queueMicrotask(() => {
591
+ if (!browserDevTools) {
592
+ sendScheduled = false;
593
+ return;
594
+ }
595
+ const now = Date.now();
596
+ const waitMs = Math.max(0, sendRateLimitMs - (now - lastSendAt));
597
+ if (waitMs > 0) {
598
+ if (sendTimer) return;
599
+ sendTimer = setTimeout(() => {
600
+ sendTimer = null;
601
+ flushSend();
602
+ }, waitMs);
603
+ return;
604
+ }
605
+ flushSend();
606
+ });
607
+ };
608
+ const registerTree = (treeKey, tree) => {
609
+ const wasConnected = isConnected;
610
+ trees.set(treeKey, tree);
611
+ updateRateLimit();
612
+ initBrowserDevTools();
613
+ if (browserDevTools) {
614
+ const snapshot = tree.readSnapshot();
615
+ lastSnapshots.set(treeKey, snapshot);
616
+ lastSerialized.set(treeKey, tree.buildSerializedState(snapshot));
617
+ if (trees.size === 1 && !wasConnected) {
618
+ sendInit();
619
+ } else {
620
+ sendAggregated('SignalTree/register', {
621
+ treeKey
622
+ });
623
+ }
624
+ }
625
+ return () => {
626
+ trees.delete(treeKey);
627
+ pendingPathsByTree.delete(treeKey);
628
+ lastSnapshots.delete(treeKey);
629
+ lastSerialized.delete(treeKey);
630
+ updateRateLimit();
631
+ if (trees.size === 0) {
632
+ sendEmptyState();
633
+ try {
634
+ unsubscribeDevTools?.();
635
+ } catch {}
636
+ try {
637
+ browserDevToolsConnection?.unsubscribe?.();
638
+ } catch {}
639
+ try {
640
+ browserDevToolsConnection?.disconnect?.();
641
+ } catch {}
642
+ unsubscribeDevTools = null;
643
+ browserDevToolsConnection = null;
644
+ browserDevTools = null;
645
+ devToolsExtension = null;
646
+ isConnected = false;
647
+ devToolsConnections.delete(groupId);
648
+ devToolsGroups.delete(groupId);
649
+ return;
650
+ }
651
+ if (browserDevTools) {
652
+ sendAggregated('SignalTree/unregister', {
653
+ treeKey
654
+ });
655
+ }
656
+ };
657
+ };
658
+ const enqueue = (treeKey, pendingPaths, action, meta) => {
659
+ if (pendingPaths && pendingPaths.length > 0) {
660
+ const existing = pendingPathsByTree.get(treeKey) ?? [];
661
+ pendingPathsByTree.set(treeKey, [...existing, ...pendingPaths]);
662
+ }
663
+ scheduleSend(action, meta);
664
+ };
665
+ const group = {
666
+ registerTree,
667
+ enqueue,
668
+ connect() {
669
+ initBrowserDevTools();
670
+ }
671
+ };
672
+ devToolsGroups.set(groupId, group);
673
+ return group;
674
+ }
255
675
  function devTools(config = {}) {
256
676
  const {
257
677
  enabled = true,
@@ -259,7 +679,7 @@ function devTools(config = {}) {
259
679
  name,
260
680
  enableBrowserDevTools = true,
261
681
  enableTimeTravel = true,
262
- enableLogging = true,
682
+ enableLogging = false,
263
683
  performanceThreshold = 16,
264
684
  includePaths,
265
685
  excludePaths,
@@ -269,9 +689,11 @@ function devTools(config = {}) {
269
689
  maxDepth = 10,
270
690
  maxArrayLength = 50,
271
691
  maxStringLength = 2000,
272
- serialize
692
+ serialize,
693
+ aggregatedReduxInstance
273
694
  } = config;
274
695
  const displayName = name ?? treeName;
696
+ const groupId = aggregatedReduxInstance?.id ?? displayName;
275
697
  const pathInclude = toArray(includePaths);
276
698
  const pathExclude = toArray(excludePaths);
277
699
  const sendRateLimitMs = maxSendsPerSecond && maxSendsPerSecond > 0 ? Math.ceil(1000 / maxSendsPerSecond) : rateLimitMs ?? 0;
@@ -284,7 +706,9 @@ function devTools(config = {}) {
284
706
  };
285
707
  return Object.assign(tree, noopMethods);
286
708
  }
287
- const activityTracker = createActivityTracker();
709
+ const activityTracker = createActivityTracker({
710
+ enableConsole: enableLogging
711
+ });
288
712
  const logger = enableLogging ? createCompositionLogger() : createNoopLogger();
289
713
  const metrics = createModularMetrics();
290
714
  const compositionHistory = [];
@@ -302,14 +726,11 @@ function devTools(config = {}) {
302
726
  const activeProfiles = new Map();
303
727
  let browserDevToolsConnection = null;
304
728
  let browserDevTools = null;
729
+ let devToolsExtension = null;
305
730
  let isConnected = false;
306
731
  let isApplyingExternalState = false;
307
732
  let unsubscribeDevTools = null;
308
- let unsubscribeNotifier = null;
309
- let unsubscribeFlush = null;
310
733
  let pendingPaths = [];
311
- let effectRef = null;
312
- let effectPrimed = false;
313
734
  let sendScheduled = false;
314
735
  let pendingAction = null;
315
736
  let pendingExplicitAction = false;
@@ -358,15 +779,33 @@ function devTools(config = {}) {
358
779
  meta: meta
359
780
  })
360
781
  });
782
+ let lastSerializedJson;
361
783
  const flushSend = () => {
362
784
  sendScheduled = false;
363
785
  if (!browserDevTools || isApplyingExternalState) return;
364
786
  const rawSnapshot = readSnapshot();
365
787
  const currentSnapshot = rawSnapshot ?? {};
366
788
  const sanitized = buildSerializedState(currentSnapshot);
367
- const defaultPaths = lastSnapshot === undefined ? [] : computeChangedPaths(lastSnapshot, currentSnapshot, maxDepth, maxArrayLength);
368
- const mergedPaths = Array.from(new Set([...pendingPaths, ...defaultPaths.filter(path => path && isPathAllowed(path))]));
369
- const formattedPaths = mergedPaths.map(path => formatPathFn(path));
789
+ let currentSerializedJson;
790
+ try {
791
+ currentSerializedJson = JSON.stringify(sanitized);
792
+ } catch {
793
+ currentSerializedJson = '';
794
+ }
795
+ const stateActuallyChanged = lastSerializedJson === undefined || currentSerializedJson !== lastSerializedJson;
796
+ const pendingAllowedPaths = pendingPaths.filter(path => path && isPathAllowed(path));
797
+ if (!stateActuallyChanged && !pendingExplicitAction && pendingAllowedPaths.length === 0) {
798
+ pendingAction = null;
799
+ pendingExplicitAction = false;
800
+ pendingSource = undefined;
801
+ pendingDuration = undefined;
802
+ pendingPaths = [];
803
+ lastSnapshot = currentSnapshot;
804
+ lastSerializedJson = currentSerializedJson;
805
+ return;
806
+ }
807
+ const rawPathsForNaming = pendingAllowedPaths.length > 0 ? Array.from(new Set(pendingAllowedPaths)) : lastSnapshot === undefined ? [] : computeChangedPaths(lastSnapshot, currentSnapshot, maxDepth, maxArrayLength).filter(path => path && isPathAllowed(path));
808
+ const formattedPaths = rawPathsForNaming.map(path => formatPathFn(path));
370
809
  if (pathInclude.length > 0 && formattedPaths.length === 0 && !pendingExplicitAction) {
371
810
  pendingAction = null;
372
811
  pendingExplicitAction = false;
@@ -400,6 +839,7 @@ function devTools(config = {}) {
400
839
  pendingDuration = undefined;
401
840
  pendingPaths = [];
402
841
  lastSnapshot = currentSnapshot;
842
+ lastSerializedJson = currentSerializedJson;
403
843
  lastSendAt = Date.now();
404
844
  }
405
845
  };
@@ -440,15 +880,23 @@ function devTools(config = {}) {
440
880
  flushSend();
441
881
  });
442
882
  };
443
- const parseDevToolsState = state => {
444
- if (typeof state === 'string') {
445
- try {
446
- return JSON.parse(state);
447
- } catch {
448
- return undefined;
449
- }
883
+ const sendInit = () => {
884
+ if (!browserDevTools) return;
885
+ const rawSnapshot = readSnapshot() ?? {};
886
+ const serialized = buildSerializedState(rawSnapshot);
887
+ browserDevTools.send('@@INIT', serialized);
888
+ lastSnapshot = rawSnapshot;
889
+ try {
890
+ lastSerializedJson = JSON.stringify(serialized);
891
+ } catch {
892
+ lastSerializedJson = undefined;
450
893
  }
451
- return state;
894
+ lastSendAt = Date.now();
895
+ pendingPaths = [];
896
+ pendingAction = null;
897
+ pendingExplicitAction = false;
898
+ pendingSource = undefined;
899
+ pendingDuration = undefined;
452
900
  };
453
901
  const applyExternalState = state => {
454
902
  if (state === undefined || state === null) return;
@@ -466,11 +914,34 @@ function devTools(config = {}) {
466
914
  }
467
915
  };
468
916
  const handleDevToolsMessage = message => {
469
- if (!enableTimeTravel) return;
470
917
  if (!message || typeof message !== 'object') return;
471
918
  const msg = message;
472
- if (msg.type !== 'DISPATCH' || !msg.payload?.type) return;
473
- const actionType = msg.payload.type;
919
+ const messageType = typeof msg.type === 'string' ? msg.type : undefined;
920
+ if (messageType === 'START') {
921
+ if (lastSnapshot === undefined) {
922
+ sendInit();
923
+ } else if (browserDevTools) {
924
+ const elapsed = Date.now() - lastSendAt;
925
+ if (elapsed < 500) return;
926
+ const rawSnapshot = readSnapshot() ?? {};
927
+ const serialized = buildSerializedState(rawSnapshot);
928
+ browserDevTools.send({
929
+ type: 'SignalTree/reconnect'
930
+ }, serialized);
931
+ lastSnapshot = rawSnapshot;
932
+ try {
933
+ lastSerializedJson = JSON.stringify(serialized);
934
+ } catch {
935
+ lastSerializedJson = undefined;
936
+ }
937
+ lastSendAt = Date.now();
938
+ }
939
+ return;
940
+ }
941
+ if (!enableTimeTravel) return;
942
+ if (messageType !== 'DISPATCH') return;
943
+ const actionType = msg.payload && typeof msg.payload.type === 'string' ? msg.payload.type : undefined;
944
+ if (!actionType) return;
474
945
  if (actionType === 'JUMP_TO_STATE' || actionType === 'JUMP_TO_ACTION') {
475
946
  const nextState = parseDevToolsState(msg.state);
476
947
  applyExternalState(nextState);
@@ -479,44 +950,63 @@ function devTools(config = {}) {
479
950
  if (actionType === 'ROLLBACK') {
480
951
  const nextState = parseDevToolsState(msg.state);
481
952
  applyExternalState(nextState);
482
- if (browserDevTools) {
483
- const rawSnapshot = readSnapshot();
484
- const sanitized = buildSerializedState(rawSnapshot);
485
- browserDevTools.send('@@INIT', sanitized);
486
- }
953
+ sendInit();
487
954
  return;
488
955
  }
489
956
  if (actionType === 'COMMIT') {
490
- if (browserDevTools) {
491
- const rawSnapshot = readSnapshot();
492
- const sanitized = buildSerializedState(rawSnapshot);
493
- browserDevTools.send('@@INIT', sanitized);
494
- }
957
+ sendInit();
495
958
  return;
496
959
  }
497
960
  if (actionType === 'IMPORT_STATE') {
498
- const lifted = msg.payload.nextLiftedState;
499
- const computedStates = lifted?.computedStates ?? [];
500
- const index = lifted?.currentStateIndex ?? computedStates.length - 1;
961
+ const lifted = msg.payload?.nextLiftedState;
962
+ const computedStates = Array.isArray(lifted?.computedStates) ? lifted.computedStates : [];
963
+ const indexRaw = typeof lifted?.currentStateIndex === 'number' ? lifted.currentStateIndex : computedStates.length - 1;
964
+ const index = Math.max(0, Math.min(indexRaw, computedStates.length - 1));
501
965
  const entry = computedStates[index];
502
966
  const nextState = parseDevToolsState(entry?.state);
503
967
  applyExternalState(nextState);
504
- if (browserDevTools) {
505
- const rawSnapshot = readSnapshot();
506
- const sanitized = buildSerializedState(rawSnapshot);
507
- browserDevTools.send('@@INIT', sanitized);
508
- }
968
+ sendInit();
509
969
  }
510
970
  };
971
+ let groupUnregister = null;
511
972
  const initBrowserDevTools = () => {
973
+ if (aggregatedReduxInstance) {
974
+ const group = getOrCreateDevToolsGroup(aggregatedReduxInstance.id, aggregatedReduxInstance.name ?? displayName);
975
+ if (!groupUnregister) {
976
+ groupUnregister = group.registerTree(treeName, {
977
+ readSnapshot,
978
+ buildSerializedState,
979
+ applyExternalState,
980
+ formatPathFn,
981
+ isPathAllowed,
982
+ enableTimeTravel,
983
+ maxDepth,
984
+ maxArrayLength,
985
+ maxStringLength,
986
+ sendRateLimitMs
987
+ });
988
+ }
989
+ return;
990
+ }
512
991
  if (isConnected) return;
513
992
  if (!enableBrowserDevTools || typeof window === 'undefined' || !('__REDUX_DEVTOOLS_EXTENSION__' in window)) {
514
993
  return;
515
994
  }
995
+ const waiters = new Set();
996
+ devToolsConnections.set(groupId, {
997
+ status: 'connecting',
998
+ connection: null,
999
+ tools: null,
1000
+ subscribed: false,
1001
+ unsubscribe: null,
1002
+ waiters
1003
+ });
516
1004
  try {
517
1005
  const devToolsExt = window['__REDUX_DEVTOOLS_EXTENSION__'];
1006
+ devToolsExtension = devToolsExt;
518
1007
  const connection = devToolsExt.connect({
519
1008
  name: displayName,
1009
+ instanceId: groupId,
520
1010
  features: {
521
1011
  dispatch: true,
522
1012
  jump: true,
@@ -538,13 +1028,27 @@ function devTools(config = {}) {
538
1028
  };
539
1029
  }
540
1030
  }
541
- const rawSnapshot = readSnapshot();
542
- const sanitized = buildSerializedState(rawSnapshot);
543
- browserDevTools.send('@@INIT', sanitized);
544
- lastSnapshot = rawSnapshot;
1031
+ const connEntry = devToolsConnections.get(groupId);
1032
+ if (connEntry) {
1033
+ connEntry.status = 'connected';
1034
+ connEntry.connection = browserDevToolsConnection;
1035
+ connEntry.tools = browserDevTools;
1036
+ connEntry.subscribed = !!browserDevTools?.subscribe;
1037
+ connEntry.unsubscribe = unsubscribeDevTools;
1038
+ for (const waiter of connEntry.waiters) {
1039
+ try {
1040
+ waiter(connEntry);
1041
+ } catch {}
1042
+ }
1043
+ connEntry.waiters.clear();
1044
+ }
1045
+ sendInit();
545
1046
  isConnected = true;
546
- console.log(`🔗 Connected to Redux DevTools as "${displayName}"`);
1047
+ if (enableLogging) {
1048
+ console.log(`🔗 Connected to Redux DevTools as "${displayName}"`);
1049
+ }
547
1050
  } catch (e) {
1051
+ devToolsConnections.delete(groupId);
548
1052
  console.warn('[SignalTree] Failed to connect to Redux DevTools:', e);
549
1053
  }
550
1054
  };
@@ -569,7 +1073,16 @@ function devTools(config = {}) {
569
1073
  if (duration > performanceThreshold) {
570
1074
  logger.logPerformanceWarning('core', 'update', duration, performanceThreshold);
571
1075
  }
572
- if (browserDevTools) {
1076
+ if (aggregatedReduxInstance) {
1077
+ const group = devToolsGroups.get(aggregatedReduxInstance.id);
1078
+ if (group) {
1079
+ group.enqueue(treeName, [], undefined, {
1080
+ timestamp: Date.now(),
1081
+ source: 'tree.update',
1082
+ duration
1083
+ });
1084
+ }
1085
+ } else if (browserDevTools) {
573
1086
  scheduleSend(undefined, {
574
1087
  source: 'tree.update',
575
1088
  duration
@@ -631,58 +1144,152 @@ function devTools(config = {}) {
631
1144
  const profile = activeProfiles.get(profileId);
632
1145
  if (profile) {
633
1146
  const duration = performance.now() - profile.startTime;
634
- activityTracker.trackMethodCall(profile.module, profile.operation, duration);
1147
+ metrics.trackModuleUpdate(profile.module, duration);
635
1148
  activeProfiles.delete(profileId);
636
1149
  }
637
1150
  },
638
- connectDevTools: name => {
639
- if (!browserDevTools || !isConnected) {
640
- initBrowserDevTools();
641
- }
642
- if (browserDevTools) {
643
- const rawSnapshot = readSnapshot();
644
- const sanitized = buildSerializedState(rawSnapshot);
645
- browserDevTools.send('@@INIT', sanitized);
646
- lastSnapshot = rawSnapshot;
647
- console.log(`🔗 Connected to Redux DevTools as "${name}"`);
648
- }
649
- },
650
- exportDebugSession: () => ({
651
- metrics: metrics.signal(),
652
- modules: activityTracker.getAllModules(),
653
- logs: logger.exportLogs(),
654
- compositionHistory: [...compositionHistory]
655
- })
656
- };
657
- const methods = {
658
- connectDevTools() {
1151
+ connectDevTools: () => {
659
1152
  initBrowserDevTools();
660
1153
  },
661
- disconnectDevTools() {
662
- try {
663
- unsubscribeDevTools?.();
664
- } catch {}
665
- try {
666
- browserDevToolsConnection?.unsubscribe?.();
667
- } catch {}
668
- try {
669
- browserDevToolsConnection?.disconnect?.();
670
- } catch {}
671
- browserDevTools = null;
672
- browserDevToolsConnection = null;
673
- isConnected = false;
674
- if (unsubscribeNotifier) {
675
- unsubscribeNotifier();
676
- unsubscribeNotifier = null;
1154
+ exportDebugSession: () => {
1155
+ return {
1156
+ metrics: metrics.signal(),
1157
+ modules: activityTracker.getAllModules(),
1158
+ logs: logger.exportLogs(),
1159
+ compositionHistory
1160
+ };
1161
+ }
1162
+ };
1163
+ const shouldFilterByOwnership = Boolean(aggregatedReduxInstance);
1164
+ const treeTopKeys = new Set();
1165
+ const refreshTreeTopKeys = () => {
1166
+ treeTopKeys.clear();
1167
+ try {
1168
+ if ('$' in tree) {
1169
+ for (const key of Object.keys(tree.$)) {
1170
+ treeTopKeys.add(key);
1171
+ }
677
1172
  }
678
- if (unsubscribeFlush) {
679
- unsubscribeFlush();
680
- unsubscribeFlush = null;
1173
+ } catch {}
1174
+ };
1175
+ refreshTreeTopKeys();
1176
+ const isPathOwnedByTree = path => {
1177
+ if (!shouldFilterByOwnership) return true;
1178
+ if (treeTopKeys.size === 0) return true;
1179
+ const root = path.split('.')[0];
1180
+ if (treeTopKeys.has(root)) return true;
1181
+ refreshTreeTopKeys();
1182
+ return treeTopKeys.has(root);
1183
+ };
1184
+ const notifier = getPathNotifier();
1185
+ const restoreInterceptors = [];
1186
+ try {
1187
+ if ('$' in tree) {
1188
+ const treeNode = tree.$;
1189
+ for (const key of treeTopKeys) {
1190
+ const sig = treeNode[key];
1191
+ if (typeof sig === 'function' && 'set' in sig && 'update' in sig && typeof sig.set === 'function' && typeof sig.update === 'function' && !('add' in sig) && !('remove' in sig)) {
1192
+ const original = sig;
1193
+ const originalSet = original.set.bind(original);
1194
+ const originalUpdate = original.update.bind(original);
1195
+ restoreInterceptors.push(() => {
1196
+ original.set = originalSet;
1197
+ original.update = originalUpdate;
1198
+ });
1199
+ original.set = value => {
1200
+ const prev = original();
1201
+ originalSet(value);
1202
+ const next = original();
1203
+ if (next !== prev) {
1204
+ notifier.notify(key, next, prev);
1205
+ }
1206
+ };
1207
+ original.update = updater => {
1208
+ const prev = original();
1209
+ originalUpdate(updater);
1210
+ const next = original();
1211
+ if (next !== prev) {
1212
+ notifier.notify(key, next, prev);
1213
+ }
1214
+ };
1215
+ }
681
1216
  }
682
- unsubscribeDevTools = null;
683
- if (effectRef) {
684
- effectRef.destroy();
685
- effectRef = null;
1217
+ }
1218
+ } catch {}
1219
+ let unsubscribePathNotifier = null;
1220
+ let unsubscribePathFlush = null;
1221
+ unsubscribePathNotifier = notifier.subscribe('**', (_value, _prev, path) => {
1222
+ if (isApplyingExternalState) return;
1223
+ if (!isPathOwnedByTree(path)) return;
1224
+ if (!isPathAllowed(path)) return;
1225
+ pendingPaths.push(path);
1226
+ });
1227
+ unsubscribePathFlush = notifier.onFlush(() => {
1228
+ if (isApplyingExternalState) return;
1229
+ if (pendingPaths.length === 0) return;
1230
+ if (aggregatedReduxInstance) {
1231
+ const group = devToolsGroups.get(aggregatedReduxInstance.id);
1232
+ if (group) {
1233
+ group.enqueue(treeName, pendingPaths, undefined, {
1234
+ timestamp: Date.now(),
1235
+ source: 'path-notifier'
1236
+ });
1237
+ pendingPaths = [];
1238
+ }
1239
+ } else {
1240
+ if (!browserDevTools) {
1241
+ pendingPaths = [];
1242
+ return;
1243
+ }
1244
+ scheduleSend(undefined, {
1245
+ source: 'path-notifier'
1246
+ });
1247
+ }
1248
+ });
1249
+ const result = Object.assign(enhancedTree, {
1250
+ __devTools: devToolsInterface,
1251
+ connectDevTools: devToolsInterface.connectDevTools,
1252
+ exportDebugSession: devToolsInterface.exportDebugSession,
1253
+ disconnectDevTools: () => {
1254
+ if (unsubscribePathNotifier) {
1255
+ unsubscribePathNotifier();
1256
+ unsubscribePathNotifier = null;
1257
+ }
1258
+ if (unsubscribePathFlush) {
1259
+ unsubscribePathFlush();
1260
+ unsubscribePathFlush = null;
1261
+ }
1262
+ for (const restore of restoreInterceptors) {
1263
+ try {
1264
+ restore();
1265
+ } catch {}
1266
+ }
1267
+ if (aggregatedReduxInstance) {
1268
+ if (groupUnregister) {
1269
+ groupUnregister();
1270
+ groupUnregister = null;
1271
+ }
1272
+ } else {
1273
+ if (browserDevToolsConnection) {
1274
+ try {
1275
+ browserDevToolsConnection.init({});
1276
+ } catch {}
1277
+ }
1278
+ try {
1279
+ unsubscribeDevTools?.();
1280
+ } catch {}
1281
+ try {
1282
+ browserDevToolsConnection?.unsubscribe?.();
1283
+ } catch {}
1284
+ try {
1285
+ browserDevToolsConnection?.disconnect?.();
1286
+ } catch {}
1287
+ browserDevToolsConnection = null;
1288
+ browserDevTools = null;
1289
+ devToolsExtension = null;
1290
+ isConnected = false;
1291
+ unsubscribeDevTools = null;
1292
+ devToolsConnections.delete(groupId);
686
1293
  }
687
1294
  if (sendTimer) {
688
1295
  clearTimeout(sendTimer);
@@ -695,38 +1302,13 @@ function devTools(config = {}) {
695
1302
  pendingSource = undefined;
696
1303
  pendingDuration = undefined;
697
1304
  lastSnapshot = undefined;
1305
+ lastSerializedJson = undefined;
698
1306
  }
699
- };
700
- enhancedTree['__devTools'] = devToolsInterface;
701
- try {
1307
+ });
1308
+ if (enableBrowserDevTools) {
702
1309
  initBrowserDevTools();
703
- const notifier = getPathNotifier();
704
- unsubscribeNotifier = notifier.subscribe('**', (_value, _prev, path) => {
705
- if (!isPathAllowed(path)) return;
706
- pendingPaths.push(path);
707
- });
708
- unsubscribeFlush = notifier.onFlush(() => {
709
- if (!browserDevTools) {
710
- pendingPaths = [];
711
- return;
712
- }
713
- if (pendingPaths.length === 0) return;
714
- scheduleSend(undefined, {
715
- source: 'path-notifier'
716
- });
717
- });
718
- effectRef = effect(() => {
719
- void originalTreeCall();
720
- if (!effectPrimed) {
721
- effectPrimed = true;
722
- return;
723
- }
724
- scheduleSend(undefined, {
725
- source: 'signal'
726
- });
727
- });
728
- } catch {}
729
- return Object.assign(enhancedTree, methods);
1310
+ }
1311
+ return result;
730
1312
  };
731
1313
  }
732
1314
  function enableDevTools(treeName = 'SignalTree') {