jssm 5.162.11 → 5.162.13

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.
@@ -661,6 +661,8 @@ function permalink_key_for(host) {
661
661
 
662
662
  /** Debounce before a live edit is written to the URL fragment. */
663
663
  const PERMALINK_WRITE_DEBOUNCE_MS = 300;
664
+ /** Marks that this connection has not observed its URL segment yet. */
665
+ const UNOBSERVED_SEGMENT = Symbol('unobserved permalink segment');
664
666
  /**
665
667
  * Binds an `<fsl-instance>` to a segment of the URL fragment: restores from it
666
668
  * on connect and writes back (debounced, via `history.replaceState`) whenever
@@ -668,8 +670,12 @@ const PERMALINK_WRITE_DEBOUNCE_MS = 300;
668
670
  * returns `null`), so an `fsl-instance` without an `id`/`uhash` never touches
669
671
  * `location`.
670
672
  *
671
- * Echo guard: `_last` holds the segment most recently read or written, so a
672
- * restore→rebuild→write cycle and a self-induced `hashchange` are both no-ops.
673
+ * Echo guard: `_observed` holds this connection's current URL segment, including
674
+ * explicit absence, so unrelated hash changes and restore→rebuild→write cycles
675
+ * are no-ops. Async operations share a revision: every observed segment change
676
+ * or source write invalidates all older work, and disconnecting invalidates all
677
+ * work. This gives cross-direction latest-operation-wins semantics without
678
+ * allowing unrelated hash changes to cancel valid writes.
673
679
  * @example
674
680
  * // In an element's constructor:
675
681
  * new FslPermalinkSync(this); // reads <el id="k">'s #k=… on connect, writes it on edit
@@ -677,13 +683,17 @@ const PERMALINK_WRITE_DEBOUNCE_MS = 300;
677
683
  class FslPermalinkSync {
678
684
  constructor(host) {
679
685
  this.key = null;
680
- this._last = null;
686
+ this._observed = UNOBSERVED_SEGMENT;
687
+ this._connected = false;
688
+ this._revision = 0;
681
689
  this._onRebuilt = () => { this._scheduleWrite(); };
682
690
  this._onHashChange = () => { void this._restore(); };
683
691
  this.host = host;
684
692
  host.addController(this);
685
693
  }
694
+ /** Observe the keyed URL segment and attach synchronization listeners. */
686
695
  hostConnected() {
696
+ this._connected = true;
687
697
  this.key = permalink_key_for(this.host);
688
698
  if (this.key === null) {
689
699
  return;
@@ -692,33 +702,48 @@ class FslPermalinkSync {
692
702
  this.host.addEventListener('fsl-machine-rebuilt', this._onRebuilt);
693
703
  addEventListener('hashchange', this._onHashChange);
694
704
  }
705
+ /** Invalidate work, forget observations, detach listeners, and cancel timers. */
695
706
  hostDisconnected() {
696
- if (this.key === null) {
697
- return;
707
+ this._connected = false;
708
+ this._revision += 1;
709
+ this._observed = UNOBSERVED_SEGMENT;
710
+ if (this.key !== null) {
711
+ this.host.removeEventListener('fsl-machine-rebuilt', this._onRebuilt);
712
+ removeEventListener('hashchange', this._onHashChange);
698
713
  }
699
- this.host.removeEventListener('fsl-machine-rebuilt', this._onRebuilt);
700
- removeEventListener('hashchange', this._onHashChange);
701
714
  if (this._timer !== undefined) {
702
715
  clearTimeout(this._timer);
703
716
  this._timer = undefined;
704
717
  }
705
718
  }
706
- /** Read our segment and, if new, push it into the host (overriding declared source). */
719
+ /**
720
+ * Observe this instance's newest segment and push it into the host. A changed
721
+ * observation supersedes older restores and writes; an unchanged observation
722
+ * leaves them valid. Declared source is overridden only when this decode is
723
+ * still the latest operation and the actual fragment is unchanged after
724
+ * awaiting it, even when a corresponding hashchange handler is still queued.
725
+ */
707
726
  async _restore() {
708
- const segment = read_fragment_param(location.hash, this.key);
709
- if (segment === null || segment === this._last) {
727
+ const key = this.key;
728
+ const segment = read_fragment_param(location.hash, key);
729
+ if (segment === this._observed) {
730
+ return;
731
+ }
732
+ this._observed = segment;
733
+ this._revision += 1;
734
+ if (segment === null) {
710
735
  return;
711
736
  }
737
+ const revision = this._revision;
712
738
  try {
713
739
  const fsl = await decode_machine(segment);
714
- // The decode is async; if the host was disconnected while it ran, drop the
715
- // result rather than mutating a detached element (and triggering a stray
716
- // rebuild on a later reconnect). A reconnect runs hostConnected → _restore
717
- // afresh.
718
- if (!this.host.isConnected) {
740
+ const currentSegment = read_fragment_param(location.hash, key);
741
+ if (!this._connected ||
742
+ revision !== this._revision ||
743
+ key !== this.key ||
744
+ currentSegment !== segment) {
719
745
  return;
720
746
  }
721
- this._last = segment;
722
747
  this.host.fsl = fsl;
723
748
  }
724
749
  catch (_a) {
@@ -726,22 +751,43 @@ class FslPermalinkSync {
726
751
  // declared source intact. A bad URL never bricks the page.
727
752
  }
728
753
  }
754
+ /** Debounce the newest rebuild and immediately supersede older restores or writes. */
729
755
  _scheduleWrite() {
756
+ const revision = this._revision += 1;
730
757
  if (this._timer !== undefined) {
731
758
  clearTimeout(this._timer);
732
759
  }
733
- this._timer = setTimeout(() => { void this._write(); }, PERMALINK_WRITE_DEBOUNCE_MS);
760
+ this._timer = setTimeout(() => {
761
+ this._timer = undefined;
762
+ void this._write(revision);
763
+ }, PERMALINK_WRITE_DEBOUNCE_MS);
734
764
  }
735
- /** Encode the current source and merge it into the fragment, history-silently. */
736
- async _write() {
765
+ /**
766
+ * Encode the scheduled source and merge it into the fragment only when no
767
+ * newer source write, observed segment change, or disconnect superseded it.
768
+ * The actual fragment must also match the observation captured before the
769
+ * await, covering URL changes whose hashchange handler is still queued. A
770
+ * successful write becomes the current observation, preventing echo work.
771
+ */
772
+ async _write(revision) {
773
+ const key = this.key;
774
+ const fsl = this.host.fsl;
775
+ const observed = this._observed;
737
776
  try {
738
- const segment = await encode_machine(this.host.fsl);
739
- if (segment === this._last) {
777
+ const segment = await encode_machine(fsl);
778
+ const currentSegment = read_fragment_param(location.hash, key);
779
+ if (!this._connected ||
780
+ revision !== this._revision ||
781
+ key !== this.key ||
782
+ currentSegment !== observed) {
783
+ return;
784
+ }
785
+ if (segment === observed) {
740
786
  return;
741
787
  }
742
- this._last = segment;
743
- const fragment = set_fragment_param(location.hash, this.key, segment);
788
+ const fragment = set_fragment_param(location.hash, key, segment);
744
789
  history.replaceState(history.state, '', `#${fragment}`);
790
+ this._observed = segment;
745
791
  }
746
792
  catch (_a) {
747
793
  // No compression support: skip the write rather than throw.
package/dist/wc/viz.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { css, LitElement, html } from 'lit';
2
2
  import { property, state } from 'lit/decorators.js';
3
3
  import { unsafeHTML } from 'lit/directives/unsafe-html.js';
4
- import { machine_to_svg_string, fsl_to_svg_string } from 'jssm/viz';
4
+ import { machine_to_svg_string, fsl_to_svg_string, slug_for } from 'jssm/viz';
5
5
 
6
6
  /**
7
7
  * Shared helpers for the dual-prefix (`fsl-` canonical, `jssm-` synonym)
@@ -137,6 +137,7 @@ function normalize_viz_error(e) {
137
137
  * supplying it emits a `console.warn` for developer feedback.
138
138
  * @element fsl-viz
139
139
  * @cssproperty [--jssm-viz-min-height=100px] - Minimum height of the rendered SVG container.
140
+ * @cssproperty [--jssm-viz-max-height=none] - Maximum height of the control; the rendered SVG stays bounded (aspect preserved, letterboxed) within it. Equivalent to setting `max-height` on the host from outside, without shadow surgery.
140
141
  * @fires {CustomEvent<{ message: string; location?: unknown }>} viz-error - Fires when the FSL source fails to parse or render.
141
142
  */
142
143
  class FslViz extends LitElement {
@@ -377,15 +378,27 @@ class FslViz extends LitElement {
377
378
  * Programmatically highlights one execution trace (a path of state names)
378
379
  * through the rendered graph, optionally fading everything off the path.
379
380
  *
380
- * Matches nodes by their Graphviz `<title>` (the state name) and edges by
381
- * the `from->to` title Graphviz emits, applying inline style overrides so
382
- * the highlight composes over — and is reversible against — the default
383
- * rendering (see {@link clearHighlights}, which this calls first). No-ops
384
- * when detached, before the first render, or given an empty trace.
381
+ * Matches nodes by their Graphviz `<title>` and edges by the `from->to`
382
+ * title Graphviz emits, applying inline style overrides so the highlight
383
+ * composes over — and is reversible against — the default rendering (see
384
+ * {@link clearHighlights}, which this calls first). No-ops when detached,
385
+ * before the first render, or given an empty trace.
386
+ *
387
+ * Because dot generation slugs state names into node identifiers (fsl#1935
388
+ * — `'Wrong Pin'` renders with `<title>wrong-pin</title>`), each trace name
389
+ * is matched in **both** its raw form and its slugged form, using the same
390
+ * `slug_for` the dot generator uses. Display names (`'Red'`, `'Wrong Pin'`,
391
+ * `'Röd'`) and already-slug-form names (`'red'`, `'wrong-pin'`) therefore
392
+ * both work. Names whose slug is empty (e.g. `'!!!'`, which renders under
393
+ * an indexed `node-N` title) are only matchable by passing that literal
394
+ * `node-N` title.
385
395
  *
386
396
  * ```typescript
387
397
  * // Highlight a -> b -> c in green, without dimming the rest:
388
398
  * viz.highlightTrace(['a', 'b', 'c'], { color: '#2e7d32', fadeOthers: false });
399
+ *
400
+ * // Display-form names match their slugged titles:
401
+ * viz.highlightTrace(['Wrong Pin', 'Alarm']); // titles wrong-pin, alarm
389
402
  * ```
390
403
  * @param trace Ordered state names describing the path (e.g.
391
404
  * `['A', 'B', 'C']`). Consecutive pairs select the edges
@@ -408,10 +421,26 @@ class FslViz extends LitElement {
408
421
  const fadeOthers = options.fadeOthers !== false; // default: true — undefined must fade
409
422
  const targetNodes = new Set();
410
423
  const targetEdges = new Set();
411
- for (let i = 0; i < trace.length; i++) {
412
- targetNodes.add(trace[i]);
424
+ // fsl#1935: dot generation slugs state names for node identity (see
425
+ // slug_for in jssm_viz.ts), so rendered <title>s carry 'wrong-pin', not
426
+ // 'Wrong Pin'. Match each trace name in both raw and slugged forms; for
427
+ // names already in slug form the two coincide, preserving old behavior.
428
+ const title_forms_of = (name) => {
429
+ const slug = slug_for(name);
430
+ return (slug !== '' && slug !== name) ? [name, slug] : [name];
431
+ };
432
+ for (const [i, name] of trace.entries()) {
433
+ const from_forms = title_forms_of(name);
434
+ for (const form of from_forms) {
435
+ targetNodes.add(form);
436
+ }
413
437
  if (i < trace.length - 1) {
414
- targetEdges.add(`${trace[i]}->${trace[i + 1]}`);
438
+ const to_forms = title_forms_of(trace[i + 1]);
439
+ for (const from_form of from_forms) {
440
+ for (const to_form of to_forms) {
441
+ targetEdges.add(`${from_form}->${to_form}`);
442
+ }
443
+ }
415
444
  }
416
445
  }
417
446
  const allNodes = container.querySelectorAll('.node');
@@ -461,10 +490,18 @@ FslViz.styles = css `
461
490
  :host {
462
491
  display: block;
463
492
  min-height: var(--jssm-viz-min-height, 100px);
493
+ /* #1934: embedder sizing seam — cap the control via the custom property
494
+ (or plain external max-height on the host) without shadow surgery. */
495
+ max-height: var(--jssm-viz-max-height, none);
464
496
  }
465
497
  .container {
466
498
  width: 100%;
467
499
  height: 100%;
500
+ /* #1934: when the host's height is auto (content-driven under an
501
+ external max-height cap), the percentage heights below collapse to
502
+ auto and stop constraining anything. Inheriting the host's computed
503
+ max-height re-threads the cap down to the svg. */
504
+ max-height: inherit;
468
505
  display: flex;
469
506
  align-items: center;
470
507
  justify-content: center;
@@ -475,6 +512,14 @@ FslViz.styles = css `
475
512
  the 90% box centered in the flex container yields the 5% inset. */
476
513
  width: 90%;
477
514
  height: 90%;
515
+ /* #1934: with an auto-height host the 90% height collapses and the svg
516
+ falls back to ratio-derived (or intrinsic pt) sizing, which can run
517
+ far past an external max-height on the host. The inherited cap keeps
518
+ that fallback bounded; Graphviz SVGs carry viewBox + the default
519
+ preserveAspectRatio, so the capped viewport letterboxes cleanly
520
+ instead of distorting. */
521
+ max-width: 100%;
522
+ max-height: inherit;
478
523
  }
479
524
 
480
525
  /* Smoothly animate the inline style overrides applied by highlightTrace()
@@ -1,4 +1,4 @@
1
- import { FslToolbar, FslActions, FslFooter, FslHelp, FslHistory, FslDataInspector, FslHookLog, FslSimulation, FslExport, FslStochastic } from './widgets.js';
1
+ import { FslToolbar, FslActions, FslFooter, FslHelp, FslHistory, FslDataInspector, FslHookLog, FslSimulation, FslExport, FslStochastic, FslInfoPanel } from './widgets.js';
2
2
  export * from './widgets.js';
3
3
 
4
4
  /**
@@ -45,7 +45,8 @@ function define_canonical(canonical_tag, CanonicalClass) {
45
45
 
46
46
  /**
47
47
  * Bundle entry: registers the entire light fsl-* widget suite (toolbar, actions,
48
- * footer, help, history, data-inspector, hook-log, simulation, export, stochastic) in one import.
48
+ * footer, help, history, data-inspector, hook-log, simulation, export, stochastic,
49
+ * info-panel) in one import.
49
50
  * Canonical `fsl-*` tags only — no deprecated `jssm-*` synonyms. Registration
50
51
  * is idempotent, so this composes safely with the per-widget `*.define` modules.
51
52
  */
@@ -59,3 +60,4 @@ define_canonical('fsl-hook-log', FslHookLog);
59
60
  define_canonical('fsl-simulation', FslSimulation);
60
61
  define_canonical('fsl-export', FslExport);
61
62
  define_canonical('fsl-stochastic', FslStochastic);
63
+ define_canonical('fsl-info-panel', FslInfoPanel);