sygnal 5.2.1 → 5.3.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.
@@ -5027,7 +5027,12 @@ function makeDOMDriver(container, options = {}) {
5027
5027
  function eventBusDriver(out$) {
5028
5028
  const events = new EventTarget();
5029
5029
  out$.subscribe({
5030
- next: (event) => events.dispatchEvent(new CustomEvent('data', { detail: event })),
5030
+ next: (event) => {
5031
+ events.dispatchEvent(new CustomEvent('data', { detail: event }));
5032
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
5033
+ window.__SYGNAL_DEVTOOLS__.onBusEvent(event);
5034
+ }
5035
+ },
5031
5036
  error: (err) => console.error('[EVENTS driver] Error in sink stream:', err),
5032
5037
  });
5033
5038
  return {
@@ -5681,6 +5686,9 @@ class Component {
5681
5686
  this.sources.props$ = props$.map((val) => {
5682
5687
  const { sygnalFactory, sygnalOptions, ...sanitizedProps } = val;
5683
5688
  this.currentProps = sanitizedProps;
5689
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
5690
+ window.__SYGNAL_DEVTOOLS__.onPropsChanged(this._componentNumber, this.name, sanitizedProps);
5691
+ }
5684
5692
  return val;
5685
5693
  });
5686
5694
  }
@@ -5752,6 +5760,9 @@ class Component {
5752
5760
  }
5753
5761
  }
5754
5762
  dispose() {
5763
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
5764
+ window.__SYGNAL_DEVTOOLS__.onComponentDisposed(this._componentNumber, this.name);
5765
+ }
5755
5766
  // Fire the DISPOSE built-in action so model handlers can run cleanup logic
5756
5767
  const hasDispose = this.model && (this.model[DISPOSE_ACTION] || Object.keys(this.model).some(k => k.includes('|') && k.split('|')[0].trim() === DISPOSE_ACTION));
5757
5768
  if (hasDispose && this.action$ && typeof this.action$.shamefullySendNext === 'function') {
@@ -6149,6 +6160,12 @@ class Component {
6149
6160
  else {
6150
6161
  acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...(this.peers$[name] || []));
6151
6162
  }
6163
+ // Stamp EVENTS sink emissions with emitter component info for devtools
6164
+ if (name === 'EVENTS' && acc[name]) {
6165
+ const _componentNumber = this._componentNumber;
6166
+ const _name = this.name;
6167
+ acc[name] = acc[name].map((ev) => ({ ...ev, __emitterId: _componentNumber, __emitterName: _name }));
6168
+ }
6152
6169
  return acc;
6153
6170
  }, {});
6154
6171
  this.sinks[this.DOMSourceName] = this.vdom$;
@@ -6671,6 +6688,13 @@ class Component {
6671
6688
  if (!isObj(sink$)) {
6672
6689
  throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`);
6673
6690
  }
6691
+ // Notify devtools of collection mount
6692
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
6693
+ const itemName = typeof collectionOf === 'function'
6694
+ ? (collectionOf.componentName || collectionOf.label || collectionOf.name || 'anonymous')
6695
+ : String(collectionOf);
6696
+ window.__SYGNAL_DEVTOOLS__.onCollectionMounted(this._componentNumber, this.name, itemName, typeof stateField === 'string' ? stateField : null);
6697
+ }
6674
6698
  return sink$;
6675
6699
  }
6676
6700
  instantiateSwitchable(el, props$, children$) {
@@ -6809,6 +6833,7 @@ class Component {
6809
6833
  for (const key of Object.keys(props)) {
6810
6834
  const val = props[key];
6811
6835
  if (val && val.__sygnalCommand) {
6836
+ val._targetComponentName = componentName;
6812
6837
  sources.commands$ = makeCommandSource(val);
6813
6838
  break;
6814
6839
  }
@@ -6851,10 +6876,15 @@ class Component {
6851
6876
  const wasReady = this._childReadyState[id];
6852
6877
  this._childReadyState[id] = !!ready;
6853
6878
  // When READY state changes, trigger a re-render
6854
- if (wasReady !== !!ready && this._readyChangedListener) {
6855
- setTimeout(() => {
6856
- this._readyChangedListener?.next(null);
6857
- }, 0);
6879
+ if (wasReady !== !!ready) {
6880
+ if (this._readyChangedListener) {
6881
+ setTimeout(() => {
6882
+ this._readyChangedListener?.next(null);
6883
+ }, 0);
6884
+ }
6885
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
6886
+ window.__SYGNAL_DEVTOOLS__.onReadyChanged(this._componentNumber, this.name, id, !!ready);
6887
+ }
6858
6888
  }
6859
6889
  },
6860
6890
  error: () => { },
@@ -7490,6 +7520,12 @@ class SygnalDevTools {
7490
7520
  case 'TIME_TRAVEL':
7491
7521
  this._timeTravel(msg.payload);
7492
7522
  break;
7523
+ case 'SNAPSHOT':
7524
+ this._takeSnapshot();
7525
+ break;
7526
+ case 'RESTORE_SNAPSHOT':
7527
+ this._restoreSnapshot(msg.payload);
7528
+ break;
7493
7529
  case 'GET_STATE':
7494
7530
  this._sendComponentState(msg.payload.componentId);
7495
7531
  break;
@@ -7509,6 +7545,7 @@ class SygnalDevTools {
7509
7545
  children: [],
7510
7546
  debug: instance._debug,
7511
7547
  createdAt: Date.now(),
7548
+ mviGraph: this._extractMviGraph(instance),
7512
7549
  _instanceRef: new WeakRef(instance),
7513
7550
  };
7514
7551
  this._components.set(componentNumber, meta);
@@ -7533,7 +7570,6 @@ class SygnalDevTools {
7533
7570
  componentId: componentNumber,
7534
7571
  componentName: name,
7535
7572
  state: entry.state,
7536
- historyIndex: this._stateHistory.length - 1,
7537
7573
  });
7538
7574
  }
7539
7575
  onActionDispatched(componentNumber, name, actionType, data) {
@@ -7570,6 +7606,76 @@ class SygnalDevTools {
7570
7606
  componentId: componentNumber,
7571
7607
  componentName: name,
7572
7608
  context: this._safeClone(context),
7609
+ contextTrace: this._buildContextTrace(componentNumber, context),
7610
+ });
7611
+ }
7612
+ onPropsChanged(componentNumber, name, props) {
7613
+ if (!this.connected)
7614
+ return;
7615
+ this._post('PROPS_CHANGED', {
7616
+ componentId: componentNumber,
7617
+ componentName: name,
7618
+ props: this._safeClone(props),
7619
+ });
7620
+ }
7621
+ onBusEvent(event) {
7622
+ if (!this.connected)
7623
+ return;
7624
+ this._post('BUS_EVENT', {
7625
+ type: event.type,
7626
+ data: this._safeClone(event.data),
7627
+ componentId: event.__emitterId ?? null,
7628
+ componentName: event.__emitterName ?? null,
7629
+ timestamp: Date.now(),
7630
+ });
7631
+ }
7632
+ onCommandSent(type, data, targetComponentId, targetComponentName) {
7633
+ if (!this.connected)
7634
+ return;
7635
+ this._post('COMMAND_SENT', {
7636
+ type,
7637
+ data: this._safeClone(data),
7638
+ targetComponentName: targetComponentName ?? null,
7639
+ timestamp: Date.now(),
7640
+ });
7641
+ }
7642
+ onReadyChanged(parentId, parentName, childKey, ready) {
7643
+ if (!this.connected)
7644
+ return;
7645
+ this._post('READY_CHANGED', {
7646
+ parentId,
7647
+ parentName,
7648
+ childKey,
7649
+ ready,
7650
+ timestamp: Date.now(),
7651
+ });
7652
+ }
7653
+ onCollectionMounted(parentId, parentName, itemComponentName, stateField) {
7654
+ const meta = this._components.get(parentId);
7655
+ if (meta) {
7656
+ meta.collection = { itemComponent: itemComponentName, stateField };
7657
+ }
7658
+ if (!this.connected)
7659
+ return;
7660
+ this._post('COLLECTION_MOUNTED', {
7661
+ parentId,
7662
+ parentName,
7663
+ itemComponent: itemComponentName,
7664
+ stateField,
7665
+ });
7666
+ }
7667
+ onComponentDisposed(componentNumber, name) {
7668
+ const meta = this._components.get(componentNumber);
7669
+ if (meta) {
7670
+ meta.disposed = true;
7671
+ meta.disposedAt = Date.now();
7672
+ }
7673
+ if (!this.connected)
7674
+ return;
7675
+ this._post('COMPONENT_DISPOSED', {
7676
+ componentId: componentNumber,
7677
+ componentName: name,
7678
+ timestamp: Date.now(),
7573
7679
  });
7574
7680
  }
7575
7681
  onDebugLog(componentNumber, message) {
@@ -7598,20 +7704,79 @@ class SygnalDevTools {
7598
7704
  }
7599
7705
  }
7600
7706
  }
7601
- _timeTravel({ historyIndex }) {
7602
- const entry = this._stateHistory[historyIndex];
7603
- if (!entry)
7707
+ _timeTravel({ componentId, componentName, state }) {
7708
+ if (componentId == null || !state) {
7709
+ console.warn('[Sygnal DevTools] _timeTravel: missing componentId or state', { componentId, hasState: !!state });
7604
7710
  return;
7711
+ }
7605
7712
  if (typeof window === 'undefined')
7606
7713
  return;
7714
+ const newState = this._safeClone(state);
7715
+ // Try per-component time-travel via the component's STATE sink (reducer stream)
7716
+ const meta = this._components.get(componentId);
7717
+ if (meta) {
7718
+ const instance = meta._instanceRef?.deref();
7719
+ if (!instance) {
7720
+ console.warn(`[Sygnal DevTools] _timeTravel: WeakRef for component #${componentId} (${componentName}) has been GC'd`);
7721
+ }
7722
+ else {
7723
+ // sinks[stateSourceName] is the reducer stream — push a reducer that replaces state
7724
+ const stateSinkName = instance.stateSourceName || 'STATE';
7725
+ const stateSink = instance.sinks?.[stateSinkName];
7726
+ if (stateSink?.shamefullySendNext) {
7727
+ stateSink.shamefullySendNext(() => ({ ...newState }));
7728
+ this._post('TIME_TRAVEL_APPLIED', {
7729
+ componentId,
7730
+ componentName,
7731
+ state: newState,
7732
+ });
7733
+ return;
7734
+ }
7735
+ console.warn(`[Sygnal DevTools] _timeTravel: component #${componentId} (${componentName}) has no STATE sink with shamefullySendNext. sinkName=${stateSinkName}, hasSinks=${!!instance.sinks}, sinkKeys=${instance.sinks ? Object.keys(instance.sinks).join(',') : 'none'}`);
7736
+ }
7737
+ }
7738
+ else {
7739
+ console.warn(`[Sygnal DevTools] _timeTravel: no meta for componentId ${componentId}`);
7740
+ }
7741
+ // Fall back to root STATE sink for root-level components
7607
7742
  const app = window.__SYGNAL_DEVTOOLS_APP__;
7608
7743
  if (app?.sinks?.STATE?.shamefullySendNext) {
7609
- app.sinks.STATE.shamefullySendNext(() => ({ ...entry.state }));
7744
+ app.sinks.STATE.shamefullySendNext(() => ({ ...newState }));
7610
7745
  this._post('TIME_TRAVEL_APPLIED', {
7611
- historyIndex,
7612
- state: entry.state,
7746
+ componentId,
7747
+ componentName,
7748
+ state: newState,
7613
7749
  });
7614
7750
  }
7751
+ else {
7752
+ console.warn(`[Sygnal DevTools] _timeTravel: no fallback root STATE sink available`);
7753
+ }
7754
+ }
7755
+ _takeSnapshot() {
7756
+ const entries = [];
7757
+ for (const [id, meta] of this._components) {
7758
+ if (meta.disposed)
7759
+ continue;
7760
+ const instance = meta._instanceRef?.deref();
7761
+ if (instance?.currentState != null) {
7762
+ entries.push({
7763
+ componentId: id,
7764
+ componentName: meta.name,
7765
+ state: this._safeClone(instance.currentState),
7766
+ });
7767
+ }
7768
+ }
7769
+ this._post('SNAPSHOT_TAKEN', {
7770
+ entries,
7771
+ timestamp: Date.now(),
7772
+ });
7773
+ }
7774
+ _restoreSnapshot(snapshot) {
7775
+ if (!snapshot?.entries)
7776
+ return;
7777
+ for (const entry of snapshot.entries) {
7778
+ this._timeTravel(entry);
7779
+ }
7615
7780
  }
7616
7781
  _sendComponentState(componentId) {
7617
7782
  const meta = this._components.get(componentId);
@@ -7622,11 +7787,38 @@ class SygnalDevTools {
7622
7787
  componentId,
7623
7788
  state: this._safeClone(instance.currentState),
7624
7789
  context: this._safeClone(instance.currentContext),
7790
+ contextTrace: this._buildContextTrace(componentId, instance.currentContext),
7625
7791
  props: this._safeClone(instance.currentProps),
7626
7792
  });
7627
7793
  }
7628
7794
  }
7629
7795
  }
7796
+ _buildContextTrace(componentId, context) {
7797
+ if (!context || typeof context !== 'object')
7798
+ return [];
7799
+ const trace = [];
7800
+ const fields = Object.keys(context);
7801
+ for (const field of fields) {
7802
+ // Walk up parent chain to find which component provides this field
7803
+ let currentId = componentId;
7804
+ let found = false;
7805
+ while (currentId != null) {
7806
+ const meta = this._components.get(currentId);
7807
+ if (!meta)
7808
+ break;
7809
+ if (meta.mviGraph?.contextProvides?.includes(field)) {
7810
+ trace.push({ field, providerId: meta.id, providerName: meta.name });
7811
+ found = true;
7812
+ break;
7813
+ }
7814
+ currentId = meta.parentId;
7815
+ }
7816
+ if (!found) {
7817
+ trace.push({ field, providerId: -1, providerName: 'unknown' });
7818
+ }
7819
+ }
7820
+ return trace;
7821
+ }
7630
7822
  _sendFullTree() {
7631
7823
  const tree = [];
7632
7824
  for (const [, meta] of this._components) {
@@ -7661,6 +7853,44 @@ class SygnalDevTools {
7661
7853
  return '[unserializable]';
7662
7854
  }
7663
7855
  }
7856
+ _extractMviGraph(instance) {
7857
+ if (!instance.model)
7858
+ return null;
7859
+ try {
7860
+ const sources = instance.sourceNames ? [...instance.sourceNames] : [];
7861
+ const actions = [];
7862
+ const model = instance.model;
7863
+ for (const key of Object.keys(model)) {
7864
+ let actionName = key;
7865
+ let entry = model[key];
7866
+ // Handle shorthand 'ACTION | SINK'
7867
+ if (key.includes('|')) {
7868
+ const parts = key.split('|').map((s) => s.trim());
7869
+ if (parts.length === 2 && parts[0] && parts[1]) {
7870
+ actionName = parts[0];
7871
+ actions.push({ name: actionName, sinks: [parts[1]] });
7872
+ continue;
7873
+ }
7874
+ }
7875
+ // Function → targets STATE
7876
+ if (typeof entry === 'function') {
7877
+ actions.push({ name: actionName, sinks: [instance.stateSourceName || 'STATE'] });
7878
+ continue;
7879
+ }
7880
+ // Object → keys are sink names
7881
+ if (entry && typeof entry === 'object') {
7882
+ actions.push({ name: actionName, sinks: Object.keys(entry) });
7883
+ continue;
7884
+ }
7885
+ }
7886
+ const contextProvides = instance.context && typeof instance.context === 'object'
7887
+ ? Object.keys(instance.context) : [];
7888
+ return { sources, actions, contextProvides };
7889
+ }
7890
+ catch (e) {
7891
+ return null;
7892
+ }
7893
+ }
7664
7894
  _serializeMeta(meta) {
7665
7895
  const { _instanceRef, ...rest } = meta;
7666
7896
  return rest;