jssm 5.154.0 → 5.155.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.
package/README.md CHANGED
@@ -18,10 +18,10 @@ Please edit the file it's derived from, instead: `./src/md/readme_base.md`
18
18
 
19
19
 
20
20
 
21
- * Generated for version 5.154.0 at 6/28/2026, 7:29:51 PM
21
+ * Generated for version 5.155.1 at 6/28/2026, 7:52:33 PM
22
22
 
23
23
  -->
24
- # jssm 5.154.0
24
+ # jssm 5.155.1
25
25
 
26
26
  [**Try the live editor**](https://stonecypher.github.io/jssm-viz-demo/graph_explorer.html) ·
27
27
  [Documentation](https://stonecypher.github.io/jssm/docs/) ·
@@ -312,7 +312,7 @@ That decision shows up everywhere downstream:
312
312
  or run `npm run benny` against your own machine.
313
313
 
314
314
  - **More thoroughly tested than any other JavaScript state-machine
315
- library.** 7,810 tests at 100.0% line coverage
315
+ library.** 7,815 tests at 100.0% line coverage
316
316
  ([report](https://coveralls.io/github/StoneCypher/jssm)), plus
317
317
  fuzz testing via `fast-check`, with parser test data across ten natural
318
318
  languages and Emoji.
@@ -445,11 +445,11 @@ If your contribution is missing here, please open an issue.
445
445
 
446
446
  <br/>
447
447
 
448
- ***7,810 tests***, run 82,852 times.
448
+ ***7,815 tests***, run 82,857 times.
449
449
 
450
- - 7,052 specs with 100.0% coverage
451
- - 758 fuzz tests with 54.8% coverage
452
- - 9,666 TypeScript lines - 0.8 tests per line, 8.6 generated tests per line
450
+ - 7,057 specs with 100.0% coverage
451
+ - 758 fuzz tests with 54.7% coverage
452
+ - 9,686 TypeScript lines - 0.8 tests per line, 8.6 generated tests per line
453
453
 
454
454
  [![Actions Status](https://github.com/StoneCypher/jssm/workflows/Node%20CI/badge.svg)](https://github.com/StoneCypher/jssm/actions)
455
455
  [![NPM version](https://img.shields.io/npm/v/jssm.svg)](https://www.npmjs.com/package/jssm)
@@ -3107,6 +3107,11 @@
3107
3107
  ],
3108
3108
  "description": "DEFLATE `bytes` (raw, headerless) via the platform `CompressionStream`."
3109
3109
  },
3110
+ {
3111
+ "kind": "variable",
3112
+ "name": "MAX_PERMALINK_INFLATE_BYTES",
3113
+ "description": "Hard ceiling on the inflated size of a permalink, in bytes. A permalink rides\r\nin a URL an attacker can hand a victim, and inflate_raw runs on it\r\nautomatically on page load, so an uncapped inflate is a decompression-bomb\r\nvector (a tiny `#m=…` could expand to hundreds of MB and OOM the tab). This is\r\ngenerous for real FSL (text — even a vast machine is well under a megabyte)."
3114
+ },
3110
3115
  {
3111
3116
  "kind": "function",
3112
3117
  "name": "inflate_raw",
@@ -3123,7 +3128,7 @@
3123
3128
  }
3124
3129
  }
3125
3130
  ],
3126
- "description": "Inverse of deflate_raw."
3131
+ "description": "Inverse of deflate_raw, reading the stream in chunks and aborting once\r\nthe inflated output would exceed MAX_PERMALINK_INFLATE_BYTES (a\r\ndecompression-bomb guard — see that constant)."
3127
3132
  },
3128
3133
  {
3129
3134
  "kind": "function",
@@ -3331,6 +3336,14 @@
3331
3336
  "module": "src/ts/wc/fsl_permalink.ts"
3332
3337
  }
3333
3338
  },
3339
+ {
3340
+ "kind": "js",
3341
+ "name": "MAX_PERMALINK_INFLATE_BYTES",
3342
+ "declaration": {
3343
+ "name": "MAX_PERMALINK_INFLATE_BYTES",
3344
+ "module": "src/ts/wc/fsl_permalink.ts"
3345
+ }
3346
+ },
3334
3347
  {
3335
3348
  "kind": "js",
3336
3349
  "name": "inflate_raw",
@@ -3663,7 +3676,7 @@
3663
3676
  "declarations": [
3664
3677
  {
3665
3678
  "kind": "class",
3666
- "description": "`<fsl-stochastic>` — a statistical/Monte-Carlo explorer for a parent\n`<fsl-instance>` (fsl#1384). Builds its own throwaway machine from the\nhost's `.fsl` source (never touching the live machine) and renders\naggregate run statistics in-panel. Standalone (no host) the controls are\ndisabled.",
3679
+ "description": "`<fsl-stochastic>` — a statistical/Monte-Carlo explorer for a parent\r\n`<fsl-instance>` (fsl#1384). Builds its own throwaway machine from the\r\nhost's `.fsl` source (never touching the live machine) and renders\r\naggregate run statistics in-panel. Standalone (no host) the controls are\r\ndisabled.",
3667
3680
  "name": "FslStochastic",
3668
3681
  "cssParts": [
3669
3682
  {
@@ -3750,7 +3763,7 @@
3750
3763
  {
3751
3764
  "kind": "field",
3752
3765
  "name": "play",
3753
- "description": "Animate the batch: accumulate runs incrementally via `requestAnimationFrame`,\nredrawing as they land. Resolves when the batch completes or is paused-to-stop.\n\nFalls back to immediate (synchronous chunk) scheduling under jsdom where\n`requestAnimationFrame` is undefined."
3766
+ "description": "Animate the batch: accumulate runs incrementally via `requestAnimationFrame`,\r\nredrawing as they land. Resolves when the batch completes or is paused-to-stop.\r\n\r\nFalls back to immediate (synchronous chunk) scheduling under jsdom where\r\n`requestAnimationFrame` is undefined."
3754
3767
  },
3755
3768
  {
3756
3769
  "kind": "method",
@@ -3812,7 +3825,7 @@
3812
3825
  "description": "The effective RNG seed used for this batch."
3813
3826
  }
3814
3827
  ],
3815
- "description": "Fold accumulated counters into a rendered summary. Shared by play\nfor incremental rendering during animation."
3828
+ "description": "Fold accumulated counters into a rendered summary. Shared by play\r\nfor incremental rendering during animation."
3816
3829
  },
3817
3830
  {
3818
3831
  "kind": "field",
@@ -23563,7 +23563,7 @@ var constants = /*#__PURE__*/Object.freeze({
23563
23563
  * Useful for runtime diagnostics and for embedding in serialized machine
23564
23564
  * snapshots so that deserializers can detect version-skew.
23565
23565
  */
23566
- const version = "5.154.0";
23566
+ const version = "5.155.1";
23567
23567
 
23568
23568
  // whargarbl lots of these return arrays could/should be sets
23569
23569
  const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
@@ -29436,7 +29436,19 @@ async function deflate_raw(bytes) {
29436
29436
  return new Uint8Array(await new Response(stream.readable).arrayBuffer());
29437
29437
  }
29438
29438
  /**
29439
- * Inverse of {@link deflate_raw}.
29439
+ * Hard ceiling on the inflated size of a permalink, in bytes. A permalink rides
29440
+ * in a URL an attacker can hand a victim, and {@link inflate_raw} runs on it
29441
+ * automatically on page load, so an uncapped inflate is a decompression-bomb
29442
+ * vector (a tiny `#m=…` could expand to hundreds of MB and OOM the tab). This is
29443
+ * generous for real FSL (text — even a vast machine is well under a megabyte).
29444
+ */
29445
+ const MAX_PERMALINK_INFLATE_BYTES = 5 * 1024 * 1024;
29446
+ /**
29447
+ * Inverse of {@link deflate_raw}, reading the stream in chunks and aborting once
29448
+ * the inflated output would exceed {@link MAX_PERMALINK_INFLATE_BYTES} (a
29449
+ * decompression-bomb guard — see that constant).
29450
+ *
29451
+ * @throws RangeError when the inflated output exceeds the cap.
29440
29452
  *
29441
29453
  * @example
29442
29454
  * new TextDecoder().decode(await inflate_raw(await deflate_raw(new TextEncoder().encode("hi")))); // "hi"
@@ -29446,7 +29458,30 @@ async function inflate_raw(bytes) {
29446
29458
  const writer = stream.writable.getWriter();
29447
29459
  void writer.write(bytes);
29448
29460
  void writer.close();
29449
- return new Uint8Array(await new Response(stream.readable).arrayBuffer());
29461
+ // Read incrementally; stopping past the cap leaves the stream half-drained, so
29462
+ // backpressure halts further inflation and the abandoned stream is GC'd. We do
29463
+ // not cancel (which would abort the writable and leak an unhandled rejection).
29464
+ const reader = stream.readable.getReader();
29465
+ const chunks = [];
29466
+ let total = 0;
29467
+ for (;;) {
29468
+ const { done, value } = await reader.read();
29469
+ if (done) {
29470
+ break;
29471
+ }
29472
+ total += value.length;
29473
+ if (total > MAX_PERMALINK_INFLATE_BYTES) {
29474
+ throw new RangeError(`permalink inflate exceeded ${MAX_PERMALINK_INFLATE_BYTES} bytes`);
29475
+ }
29476
+ chunks.push(value);
29477
+ }
29478
+ const out = new Uint8Array(total);
29479
+ let offset = 0;
29480
+ for (const chunk of chunks) {
29481
+ out.set(chunk, offset);
29482
+ offset += chunk.length;
29483
+ }
29484
+ return out;
29450
29485
  }
29451
29486
  /**
29452
29487
  * Encode FSL to a `<scheme><payload>` segment value (the part after `key=`).
@@ -29475,14 +29510,28 @@ async function decode_machine(segment) {
29475
29510
  const plain = scheme === '1' ? await inflate_raw(bytes) : bytes;
29476
29511
  return new TextDecoder().decode(plain);
29477
29512
  }
29478
- /** Split a fragment (leading `#` optional) into `[key, value]` pairs, dropping empties. */
29513
+ /** `decodeURIComponent` that returns its input untouched on a malformed escape,
29514
+ * so a hand-mangled fragment never throws out of {@link read_fragment_param}. */
29515
+ function safe_decode(text) {
29516
+ try {
29517
+ return decodeURIComponent(text);
29518
+ }
29519
+ catch (_a) {
29520
+ return text;
29521
+ }
29522
+ }
29523
+ /**
29524
+ * Split a fragment (leading `#` optional) into `[key, value]` pairs, dropping
29525
+ * empties. Keys are percent-decoded (they are percent-encoded on write by
29526
+ * {@link set_fragment_param}); values are the URL-safe base64 payload as-is.
29527
+ */
29479
29528
  function fragment_pairs(hash) {
29480
29529
  const body = hash.startsWith('#') ? hash.slice(1) : hash;
29481
29530
  return body.split('&').filter(Boolean).map(seg => {
29482
29531
  const eq = seg.indexOf('=');
29483
29532
  return eq === -1
29484
- ? [seg, '']
29485
- : [seg.slice(0, eq), seg.slice(eq + 1)];
29533
+ ? [safe_decode(seg), '']
29534
+ : [safe_decode(seg.slice(0, eq)), seg.slice(eq + 1)];
29486
29535
  });
29487
29536
  }
29488
29537
  /**
@@ -29513,7 +29562,9 @@ function set_fragment_param(hash, key, value) {
29513
29562
  else {
29514
29563
  pairs[at] = [key, value];
29515
29564
  }
29516
- return pairs.map(([k, v]) => `${k}=${v}`).join('&');
29565
+ // Percent-encode the key so an `id`/`uhash` containing `=`, `&`, or `#` cannot
29566
+ // break segmentation or collide with a sibling. Values are URL-safe base64.
29567
+ return pairs.map(([k, v]) => `${encodeURIComponent(k)}=${v}`).join('&');
29517
29568
  }
29518
29569
  /**
29519
29570
  * The fragment key an element owns: its `uhash` attribute if set, else its
@@ -29581,6 +29632,13 @@ class FslPermalinkSync {
29581
29632
  }
29582
29633
  try {
29583
29634
  const fsl = await decode_machine(segment);
29635
+ // The decode is async; if the host was disconnected while it ran, drop the
29636
+ // result rather than mutating a detached element (and triggering a stray
29637
+ // rebuild on a later reconnect). A reconnect runs hostConnected → _restore
29638
+ // afresh.
29639
+ if (!this.host.isConnected) {
29640
+ return;
29641
+ }
29584
29642
  this._last = segment;
29585
29643
  this.host.fsl = fsl;
29586
29644
  }
package/dist/cdn/viz.js CHANGED
@@ -23588,7 +23588,7 @@ var constants = /*#__PURE__*/Object.freeze({
23588
23588
  * Useful for runtime diagnostics and for embedding in serialized machine
23589
23589
  * snapshots so that deserializers can detect version-skew.
23590
23590
  */
23591
- const version = "5.154.0";
23591
+ const version = "5.155.1";
23592
23592
 
23593
23593
  // whargarbl lots of these return arrays could/should be sets
23594
23594
  const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
@@ -108,7 +108,7 @@ function parseFslArgs(argv, spec) {
108
108
  return { positional, flags, helpText };
109
109
  }
110
110
 
111
- const getVersion = () => "5.154.0";
111
+ const getVersion = () => "5.155.1";
112
112
  const SPEC = {
113
113
  flags: {
114
114
  help: { short: "h", boolean: true },