@ai-react-markdown/engine 2.9.0 → 2.10.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/index.d.cts CHANGED
@@ -1498,22 +1498,29 @@ declare const sanitizeSchema: Schema;
1498
1498
  * exactly the incremental-parse engine's fast path — the controller sits
1499
1499
  * upstream of the renderer and never touches the parse pipeline.
1500
1500
  *
1501
- * Pacing model — an adaptive jitter buffer (audio-playout style):
1502
- * - The controller estimates the source's arrival rate and burst interval
1503
- * with irregular-sampling EMAs (recorded on every append), and derives a
1504
- * target buffer B* bufferFactor × one burst's worth of text the
1505
- * causality floor for smoothing bursts of that period.
1506
- * - Streaming: rate = max(floor, rateEma + (backlog B*) / correctionTau).
1507
- * Feedforward tracks the source speed (bounded lag at any model speed);
1508
- * the feedback term pins the backlog near B*, dipping BELOW the source
1509
- * rate on purpose when the buffer runs low so it can refill instead of
1510
- * running dry between bursts. The floor is a tiny anti-freeze trickle,
1511
- * deliberately smaller than any realistic arrival rate.
1512
- * - Pre-stats (first burst after construction/snap): no estimate exists
1513
- * yet, so the backlog reveals over roughly one correction window.
1501
+ * Pacing model — a completion-deadline law over a gap-quantile estimator
1502
+ * (v2.10 redesign; the prior water-level jitter buffer's rationale and the
1503
+ * falsified alternatives live in the archived design plan):
1504
+ * - Every inter-arrival gap enters a small sliding windowthere is NO
1505
+ * pause classifier. The target interval Î is a high quantile of that
1506
+ * window: two slow-gap confirmations flip the controller into a slower
1507
+ * mode, while leaving it takes the window turning over ("cautious
1508
+ * fast, aggressive slow").
1509
+ * - Streaming: everything on hand should be on screen by the time the
1510
+ * next lump is expected. The horizon H = clamp(bufferFactor × Î + pad,
1511
+ * 16, maxLagMs) stamps a deadline at the last arrival + H, and
1512
+ * rate = backlog / time-to-deadline constant within a period, so a
1513
+ * coarse feed reads as an even typewriter, not pour-then-crawl. The
1514
+ * floor is a tiny anti-freeze trickle. `maxLagMs` is the preset's
1515
+ * max-lag promise AND the pause handler: a gap beyond it saturates H
1516
+ * and cannot change any further decision.
1517
+ * - Pre-stats (first lump after construction/snap — no gap sample yet):
1518
+ * the backlog reveals over roughly one correction window.
1514
1519
  * - Finished: rate = backlog / time-to-deadline, deadline stamped at
1515
- * `finish() + drainMs` — the backlog empties BY the deadline instead of
1516
- * decaying toward it forever.
1520
+ * `finish()` + a rate-continuity window — the tail reveals at most
1521
+ * ~2× the stream's measured throughput, clamped to [drainMs, 3×drainMs]
1522
+ * — so the backlog empties BY the deadline without an end-of-message
1523
+ * pour.
1517
1524
  * - A deadline credit accumulator converts elapsed time (injectable
1518
1525
  * `now()`) into whole-grapheme reveals per scheduled frame — timers
1519
1526
  * carry no state between frames, so throttled/paused schedulers
@@ -1538,39 +1545,63 @@ declare const sanitizeSchema: Schema;
1538
1545
  * latency-vs-smoothness axis (the audio-plugin buffer-preset convention —
1539
1546
  * perceptual parameters resist meaningful numeric tuning).
1540
1547
  *
1541
- * - `'smooth'` — target ~1.7 bursts of buffer: almost never runs dry
1542
- * between server flushes, at the cost of a little extra lag.
1543
- * - `'balanced'` (default) — ~1 burst of buffer, the causality floor for
1544
- * smoothing: minimal lag that can still bridge a typical gap.
1545
- * - `'responsive'` — sub-burst buffer: lowest lag, accepts an occasional
1546
- * visible pause between bursts.
1548
+ * - `'smooth'` — ~1.7 delivery intervals of runway (max lag 3.5s): extra
1549
+ * slack between server flushes, the most delivery coarseness absorbed.
1550
+ * - `'balanced'` (default) — ~1 interval of runway (max lag 2.5s): even
1551
+ * pacing for delivery intervals up to ~2.5s at minimal lag.
1552
+ * - `'responsive'` — ~0.45 of an interval (max lag 0.8s): lowest lag,
1553
+ * accepts visible pauses on coarse or bursty delivery.
1547
1554
  */
1548
1555
  type SmoothStreamPacing = 'smooth' | 'balanced' | 'responsive';
1549
1556
  /** The numeric parameter bundle a {@link SmoothStreamPacing} preset names. */
1550
1557
  interface SmoothStreamPacingParams {
1551
- /** Target buffer as a multiple of one estimated burst's worth of text. */
1558
+ /**
1559
+ * Horizon multiple — "periods of runway": the reveal deadline sits this
1560
+ * many expected delivery intervals after the last arrival. 1.0 means
1561
+ * everything on hand is on screen just as the next chunk is expected.
1562
+ */
1552
1563
  bufferFactor: number;
1553
1564
  /**
1554
- * Feedback time constant (ms): how fast the backlog is steered toward
1555
- * the target buffer. Also the pre-stats reveal window for the first
1556
- * burst, before any arrival estimate exists.
1565
+ * Pre-stats reveal window (ms) for the FIRST lump of a stream, before
1566
+ * any inter-arrival sample exists. The streaming path no longer reads
1567
+ * it the v2.10 deadline law replaced the old feedback term it used to
1568
+ * time-constant.
1557
1569
  */
1558
1570
  correctionTauMs: number;
1559
- /** Smoothing horizon (ms) for the arrival-rate / burst-interval EMAs. */
1571
+ /**
1572
+ * @deprecated Read by nothing since the v2.10 deadline-law redesign (it
1573
+ * used to be the EMA horizon AND, harmfully, the pause-classifier
1574
+ * threshold — the "1.8s cliff"). Kept in the preset objects so their
1575
+ * exported shape is unchanged; setting it has no effect.
1576
+ */
1560
1577
  emaTauMs: number;
1578
+ /**
1579
+ * The preset's max-lag promise (ms): the reveal DEADLINE never targets
1580
+ * more than this far behind the live source (a throttled scheduler can
1581
+ * transiently exceed it; the next tick self-corrects). It is also the
1582
+ * whole pause story —
1583
+ * a gap longer than `maxLagMs / bufferFactor` saturates the horizon and
1584
+ * cannot change any further decision. Pay-per-use: a fine-grained
1585
+ * stream's horizon tracks its own (small) cadence and never approaches
1586
+ * this cap. Full smoothing of a coarse feed requires
1587
+ * delivery-interval ≤ maxLagMs (bufferFactor ≥ 1 presets); steady mean
1588
+ * lag there is about half the delivery period.
1589
+ */
1590
+ maxLagMs?: number;
1561
1591
  /**
1562
1592
  * Anti-freeze floor (grapheme clusters/s). Deliberately tiny — smaller
1563
- * than any realistic arrival rate — so the feedback term can slow the
1564
- * reveal below the source rate to refill the buffer, without ever
1565
- * freezing visible progress entirely.
1593
+ * than any realistic arrival rate — a last-resort trickle so visible
1594
+ * progress never freezes entirely while anything is pending.
1566
1595
  */
1567
1596
  minCharsPerSecond: number;
1568
1597
  /**
1569
- * Hard drain budget after {@link SmoothStreamController.finish}: a
1570
- * deadline is stamped at `finish() + drainMs` and the rate scales with
1571
- * remaining-backlog / remaining-time, so the backlog empties BY the
1572
- * deadline (not asymptotically). Consumed when the deadline is stamped
1573
- * changing it affects the next drain, not one in progress.
1598
+ * Drain-budget FLOOR after {@link SmoothStreamController.finish}: the
1599
+ * drain deadline is stamped at `finish()` plus a rate-continuity window
1600
+ * enough time that the tail reveals at ~2× the stream's measured
1601
+ * throughput clamped to `[drainMs, 3 × drainMs]`. Short backlogs
1602
+ * behave exactly as the pre-v2.10 fixed window did. Consumed when the
1603
+ * deadline is stamped — changing it affects the next drain, not one in
1604
+ * progress.
1574
1605
  */
1575
1606
  drainMs: number;
1576
1607
  }
@@ -1609,8 +1640,9 @@ interface SmoothStreamController {
1609
1640
  update(source: string): void;
1610
1641
  /**
1611
1642
  * Signals end of stream: confirms the held-back trailing grapheme and
1612
- * switches the control law to the `drainMs` window. Not terminal —
1613
- * a later {@link update} resumes animation.
1643
+ * switches the control law to the rate-continuity drain window (floored
1644
+ * at `drainMs`). Not terminal — a later {@link update} resumes
1645
+ * animation.
1614
1646
  */
1615
1647
  finish(): void;
1616
1648
  /** Jumps to `source` instantly, no animation, and clears any backlog.
package/dist/index.d.ts CHANGED
@@ -1498,22 +1498,29 @@ declare const sanitizeSchema: Schema;
1498
1498
  * exactly the incremental-parse engine's fast path — the controller sits
1499
1499
  * upstream of the renderer and never touches the parse pipeline.
1500
1500
  *
1501
- * Pacing model — an adaptive jitter buffer (audio-playout style):
1502
- * - The controller estimates the source's arrival rate and burst interval
1503
- * with irregular-sampling EMAs (recorded on every append), and derives a
1504
- * target buffer B* bufferFactor × one burst's worth of text the
1505
- * causality floor for smoothing bursts of that period.
1506
- * - Streaming: rate = max(floor, rateEma + (backlog B*) / correctionTau).
1507
- * Feedforward tracks the source speed (bounded lag at any model speed);
1508
- * the feedback term pins the backlog near B*, dipping BELOW the source
1509
- * rate on purpose when the buffer runs low so it can refill instead of
1510
- * running dry between bursts. The floor is a tiny anti-freeze trickle,
1511
- * deliberately smaller than any realistic arrival rate.
1512
- * - Pre-stats (first burst after construction/snap): no estimate exists
1513
- * yet, so the backlog reveals over roughly one correction window.
1501
+ * Pacing model — a completion-deadline law over a gap-quantile estimator
1502
+ * (v2.10 redesign; the prior water-level jitter buffer's rationale and the
1503
+ * falsified alternatives live in the archived design plan):
1504
+ * - Every inter-arrival gap enters a small sliding windowthere is NO
1505
+ * pause classifier. The target interval Î is a high quantile of that
1506
+ * window: two slow-gap confirmations flip the controller into a slower
1507
+ * mode, while leaving it takes the window turning over ("cautious
1508
+ * fast, aggressive slow").
1509
+ * - Streaming: everything on hand should be on screen by the time the
1510
+ * next lump is expected. The horizon H = clamp(bufferFactor × Î + pad,
1511
+ * 16, maxLagMs) stamps a deadline at the last arrival + H, and
1512
+ * rate = backlog / time-to-deadline constant within a period, so a
1513
+ * coarse feed reads as an even typewriter, not pour-then-crawl. The
1514
+ * floor is a tiny anti-freeze trickle. `maxLagMs` is the preset's
1515
+ * max-lag promise AND the pause handler: a gap beyond it saturates H
1516
+ * and cannot change any further decision.
1517
+ * - Pre-stats (first lump after construction/snap — no gap sample yet):
1518
+ * the backlog reveals over roughly one correction window.
1514
1519
  * - Finished: rate = backlog / time-to-deadline, deadline stamped at
1515
- * `finish() + drainMs` — the backlog empties BY the deadline instead of
1516
- * decaying toward it forever.
1520
+ * `finish()` + a rate-continuity window — the tail reveals at most
1521
+ * ~2× the stream's measured throughput, clamped to [drainMs, 3×drainMs]
1522
+ * — so the backlog empties BY the deadline without an end-of-message
1523
+ * pour.
1517
1524
  * - A deadline credit accumulator converts elapsed time (injectable
1518
1525
  * `now()`) into whole-grapheme reveals per scheduled frame — timers
1519
1526
  * carry no state between frames, so throttled/paused schedulers
@@ -1538,39 +1545,63 @@ declare const sanitizeSchema: Schema;
1538
1545
  * latency-vs-smoothness axis (the audio-plugin buffer-preset convention —
1539
1546
  * perceptual parameters resist meaningful numeric tuning).
1540
1547
  *
1541
- * - `'smooth'` — target ~1.7 bursts of buffer: almost never runs dry
1542
- * between server flushes, at the cost of a little extra lag.
1543
- * - `'balanced'` (default) — ~1 burst of buffer, the causality floor for
1544
- * smoothing: minimal lag that can still bridge a typical gap.
1545
- * - `'responsive'` — sub-burst buffer: lowest lag, accepts an occasional
1546
- * visible pause between bursts.
1548
+ * - `'smooth'` — ~1.7 delivery intervals of runway (max lag 3.5s): extra
1549
+ * slack between server flushes, the most delivery coarseness absorbed.
1550
+ * - `'balanced'` (default) — ~1 interval of runway (max lag 2.5s): even
1551
+ * pacing for delivery intervals up to ~2.5s at minimal lag.
1552
+ * - `'responsive'` — ~0.45 of an interval (max lag 0.8s): lowest lag,
1553
+ * accepts visible pauses on coarse or bursty delivery.
1547
1554
  */
1548
1555
  type SmoothStreamPacing = 'smooth' | 'balanced' | 'responsive';
1549
1556
  /** The numeric parameter bundle a {@link SmoothStreamPacing} preset names. */
1550
1557
  interface SmoothStreamPacingParams {
1551
- /** Target buffer as a multiple of one estimated burst's worth of text. */
1558
+ /**
1559
+ * Horizon multiple — "periods of runway": the reveal deadline sits this
1560
+ * many expected delivery intervals after the last arrival. 1.0 means
1561
+ * everything on hand is on screen just as the next chunk is expected.
1562
+ */
1552
1563
  bufferFactor: number;
1553
1564
  /**
1554
- * Feedback time constant (ms): how fast the backlog is steered toward
1555
- * the target buffer. Also the pre-stats reveal window for the first
1556
- * burst, before any arrival estimate exists.
1565
+ * Pre-stats reveal window (ms) for the FIRST lump of a stream, before
1566
+ * any inter-arrival sample exists. The streaming path no longer reads
1567
+ * it the v2.10 deadline law replaced the old feedback term it used to
1568
+ * time-constant.
1557
1569
  */
1558
1570
  correctionTauMs: number;
1559
- /** Smoothing horizon (ms) for the arrival-rate / burst-interval EMAs. */
1571
+ /**
1572
+ * @deprecated Read by nothing since the v2.10 deadline-law redesign (it
1573
+ * used to be the EMA horizon AND, harmfully, the pause-classifier
1574
+ * threshold — the "1.8s cliff"). Kept in the preset objects so their
1575
+ * exported shape is unchanged; setting it has no effect.
1576
+ */
1560
1577
  emaTauMs: number;
1578
+ /**
1579
+ * The preset's max-lag promise (ms): the reveal DEADLINE never targets
1580
+ * more than this far behind the live source (a throttled scheduler can
1581
+ * transiently exceed it; the next tick self-corrects). It is also the
1582
+ * whole pause story —
1583
+ * a gap longer than `maxLagMs / bufferFactor` saturates the horizon and
1584
+ * cannot change any further decision. Pay-per-use: a fine-grained
1585
+ * stream's horizon tracks its own (small) cadence and never approaches
1586
+ * this cap. Full smoothing of a coarse feed requires
1587
+ * delivery-interval ≤ maxLagMs (bufferFactor ≥ 1 presets); steady mean
1588
+ * lag there is about half the delivery period.
1589
+ */
1590
+ maxLagMs?: number;
1561
1591
  /**
1562
1592
  * Anti-freeze floor (grapheme clusters/s). Deliberately tiny — smaller
1563
- * than any realistic arrival rate — so the feedback term can slow the
1564
- * reveal below the source rate to refill the buffer, without ever
1565
- * freezing visible progress entirely.
1593
+ * than any realistic arrival rate — a last-resort trickle so visible
1594
+ * progress never freezes entirely while anything is pending.
1566
1595
  */
1567
1596
  minCharsPerSecond: number;
1568
1597
  /**
1569
- * Hard drain budget after {@link SmoothStreamController.finish}: a
1570
- * deadline is stamped at `finish() + drainMs` and the rate scales with
1571
- * remaining-backlog / remaining-time, so the backlog empties BY the
1572
- * deadline (not asymptotically). Consumed when the deadline is stamped
1573
- * changing it affects the next drain, not one in progress.
1598
+ * Drain-budget FLOOR after {@link SmoothStreamController.finish}: the
1599
+ * drain deadline is stamped at `finish()` plus a rate-continuity window
1600
+ * enough time that the tail reveals at ~2× the stream's measured
1601
+ * throughput clamped to `[drainMs, 3 × drainMs]`. Short backlogs
1602
+ * behave exactly as the pre-v2.10 fixed window did. Consumed when the
1603
+ * deadline is stamped — changing it affects the next drain, not one in
1604
+ * progress.
1574
1605
  */
1575
1606
  drainMs: number;
1576
1607
  }
@@ -1609,8 +1640,9 @@ interface SmoothStreamController {
1609
1640
  update(source: string): void;
1610
1641
  /**
1611
1642
  * Signals end of stream: confirms the held-back trailing grapheme and
1612
- * switches the control law to the `drainMs` window. Not terminal —
1613
- * a later {@link update} resumes animation.
1643
+ * switches the control law to the rate-continuity drain window (floored
1644
+ * at `drainMs`). Not terminal — a later {@link update} resumes
1645
+ * animation.
1614
1646
  */
1615
1647
  finish(): void;
1616
1648
  /** Jumps to `source` instantly, no animation, and clears any backlog.
@@ -459,16 +459,15 @@ function settleRefsAndEarliestUnresolved(cp) {
459
459
  var TYPE6_NAMES = new Set(import_micromark_util_html_tag_name.htmlBlockNames);
460
460
  var TABLE_PART_NAMES = /* @__PURE__ */ new Set(["td", "th", "tr", "tbody", "thead", "tfoot", "caption", "col", "colgroup"]);
461
461
  var DOCUMENT_STRUCTURE_NAMES = /* @__PURE__ */ new Set(["html", "head", "body", "frameset"]);
462
+ var NO_ELEMENT_NAMES = /* @__PURE__ */ new Set([...DOCUMENT_STRUCTURE_NAMES, "frame", "image"]);
462
463
  var RETROACTIVE_OPENERS = [
463
464
  "<!doctype",
464
- "<html",
465
- "<head",
466
- "<body",
467
- "<frameset",
468
- "</html",
469
- "</head",
470
- "</body",
471
- "</frameset"
465
+ // DERIVED, so the trailing-partial guard cannot drift from the poison the
466
+ // confirmed lines get. It was transcribed until F28, and the transcription
467
+ // was already two names behind by then. `<frame` subsumes `<frameset`,
468
+ // which is why the derived list is SHORTER than the one it replaces and
469
+ // still covers strictly more.
470
+ ...[...NO_ELEMENT_NAMES].flatMap((name) => [`<${name}`, `</${name}`])
472
471
  ];
473
472
  function tailCarriesRetroactive(text) {
474
473
  const lower = text.toLowerCase();
@@ -721,6 +720,7 @@ function freshCheckpoint(defListEnabled, mathFlow, referenceTaint) {
721
720
  tagBalance: /* @__PURE__ */ new Map(),
722
721
  openTotal: 0,
723
722
  p5Tok: { kind: "data" },
723
+ formPointerMaybeSet: false,
724
724
  openStack: [],
725
725
  mdBlock: { kind: "none" },
726
726
  blankRun: 0,
@@ -871,6 +871,7 @@ function sealReleaseDerived(cp, ln, isBlockStart) {
871
871
  if (DEF_RE.test(ln.text) || FOOTNOTE_DEF_RE.test(ln.text)) return false;
872
872
  return true;
873
873
  }
874
+ var sealReleaseEvaluations = 0;
874
875
  function sealReleaseEnumerated(cp, ln, isBlockStart) {
875
876
  const defShapedLine = DEF_RE.test(ln.text) || FOOTNOTE_DEF_RE.test(ln.text) || cp.defBlockMaybeOpen || cp.fnDefResumable && !(isBlockStart && ln.indent <= 3);
876
877
  const commentOnly = ln.text.replace(/<!--[\s\S]*?-->/g, " ").replace(/<!--[\s\S]*$/, " ").replace(/[ \t\r]/g, "") === "";
@@ -885,10 +886,13 @@ function processConfirmedLine(cp, ln, text) {
885
886
  if (cp.p5SealPending && !ln.blank && cp.mdBlock.kind !== "html" && !(cp.p5Tok.kind === "comment" || cp.p5Tok.kind === "bogus")) {
886
887
  if (sealReleaseDerived(cp, ln, isBlockStart)) {
887
888
  cp.p5SealPending = false;
888
- if (!sealReleaseEnumerated(cp, ln, isBlockStart)) {
889
- console.error(
890
- `[ai-react-markdown] seal-release containment broken at offset ${ln.start}: the derived predicate released a line the retired enumeration withheld (${JSON.stringify(ln.text.slice(0, 80))}).`
891
- );
889
+ if (true) {
890
+ sealReleaseEvaluations += 1;
891
+ if (!sealReleaseEnumerated(cp, ln, isBlockStart)) {
892
+ console.error(
893
+ `[ai-react-markdown] seal-release containment broken at offset ${ln.start}: the derived predicate released a line the retired enumeration withheld (${JSON.stringify(ln.text.slice(0, 80))}).`
894
+ );
895
+ }
892
896
  }
893
897
  }
894
898
  }
@@ -917,7 +921,7 @@ function processConfirmedLine(cp, ln, text) {
917
921
  if (tag === "plaintext") cp.phasePoisonedAt = Math.min(cp.phasePoisonedAt, ln.start);
918
922
  }
919
923
  }
920
- if (DOCUMENT_STRUCTURE_NAMES.has(tag)) cp.phasePoisonedAt = 0;
924
+ if (NO_ELEMENT_NAMES.has(tag)) cp.phasePoisonedAt = 0;
921
925
  if (tag === "template" && !closing) cp.phasePoisonedAt = 0;
922
926
  if (closing) {
923
927
  let idx = -1;
@@ -933,12 +937,14 @@ function processConfirmedLine(cp, ln, text) {
933
937
  return;
934
938
  }
935
939
  cp.openStack.splice(idx, 1);
940
+ if (tag === "form") cp.formPointerMaybeSet = false;
936
941
  const count = cp.tagBalance.get(tag) ?? 0;
937
942
  if (count > 0) {
938
943
  cp.tagBalance.set(tag, count - 1);
939
944
  cp.openTotal -= 1;
940
945
  }
941
946
  } else {
947
+ if (tag === "form") cp.formPointerMaybeSet = true;
942
948
  cp.openStack.push(tag);
943
949
  cp.tagBalance.set(tag, (cp.tagBalance.get(tag) ?? 0) + 1);
944
950
  cp.openTotal += 1;
@@ -1065,7 +1071,11 @@ function processConfirmedLine(cp, ln, text) {
1065
1071
  // mask suppresses them — which is exactly why `openTotal` reads 0
1066
1072
  // and the candidate looked safe), and the 2-5 interiors are the
1067
1073
  // same construct to both grammars.
1068
- htmlBalanced: cp.openTotal === 0 && cp.mdBlock.kind !== "html" && cp.p5Tok.kind !== "bogus",
1074
+ htmlBalanced: cp.openTotal === 0 && cp.mdBlock.kind !== "html" && cp.p5Tok.kind !== "bogus" && // A form pointer parse5 may still be holding makes a LATER `<form>`
1075
+ // in the tail vanish from the full parse while a split parse opens
1076
+ // it — forward independence fails with every other condition clean.
1077
+ // See the field doc.
1078
+ !cp.formPointerMaybeSet,
1069
1079
  hazard: cp.hazardVerdict,
1070
1080
  seamRisk: cp.p5SealPending,
1071
1081
  defListSettled: null
@@ -1461,6 +1471,11 @@ var SCANNER_NAME_LISTS = [
1461
1471
  ["type6", TYPE6_NAMES],
1462
1472
  ["void", VOID_TAGS],
1463
1473
  ["documentStructure", DOCUMENT_STRUCTURE_NAMES],
1474
+ // Added with F28, and it is the list that would have made F28 visible: the
1475
+ // derived census alphabet partitions names by their membership across THIS
1476
+ // table, so before it existed `frame` shared one 39-name class with `div`
1477
+ // and could never be sampled apart from it.
1478
+ ["noElement", NO_ELEMENT_NAMES],
1464
1479
  ["tablePart", TABLE_PART_NAMES],
1465
1480
  ["scopeBarrier", SCOPE_BARRIER_NAMES],
1466
1481
  ["foreignRoot", new Set(FOREIGN_ROOT_NAMES)]
@@ -4428,24 +4443,32 @@ var SMOOTH_STREAM_PACING_PRESETS = Object.freeze({
4428
4443
  correctionTauMs: 280,
4429
4444
  emaTauMs: 2600,
4430
4445
  minCharsPerSecond: 4,
4431
- drainMs: 320
4446
+ drainMs: 320,
4447
+ maxLagMs: 3500
4432
4448
  }),
4433
4449
  balanced: Object.freeze({
4434
4450
  bufferFactor: 1,
4435
4451
  correctionTauMs: 180,
4436
4452
  emaTauMs: 1800,
4437
4453
  minCharsPerSecond: 6,
4438
- drainMs: 240
4454
+ drainMs: 240,
4455
+ maxLagMs: 2500
4439
4456
  }),
4440
4457
  responsive: Object.freeze({
4441
4458
  bufferFactor: 0.45,
4442
4459
  correctionTauMs: 110,
4443
4460
  emaTauMs: 1100,
4444
4461
  minCharsPerSecond: 10,
4445
- drainMs: 150
4462
+ drainMs: 150,
4463
+ maxLagMs: 800
4446
4464
  })
4447
4465
  });
4448
- var MAX_TARGET_BUFFER_MS = 1200;
4466
+ var GAP_WINDOW = 16;
4467
+ var INTERVAL_QUANTILE = 0.9;
4468
+ var HORIZON_PAD_MS = 50;
4469
+ var DEADLINE_FLOOR_MS = 33;
4470
+ var DRAIN_CATCHUP_FACTOR = 2;
4471
+ var DRAIN_MAX_FACTOR = 3;
4449
4472
  var defaultNow = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
4450
4473
  var defaultSchedule = (cb) => {
4451
4474
  if (typeof requestAnimationFrame === "function") {
@@ -4496,18 +4519,18 @@ var createSmoothStreamController = (options = {}) => {
4496
4519
  let lastTickAt = 0;
4497
4520
  let cancelFrame;
4498
4521
  let disposed = false;
4499
- let rateEma;
4500
- let intervalEma;
4522
+ let gapWindow = [];
4501
4523
  let lastArrivalAt;
4502
4524
  const listeners = /* @__PURE__ */ new Set();
4503
4525
  const resolveParams = () => {
4504
- const preset = SMOOTH_STREAM_PACING_PRESETS[options.pacing] ?? SMOOTH_STREAM_PACING_PRESETS.balanced;
4526
+ const preset = Object.hasOwn(SMOOTH_STREAM_PACING_PRESETS, options.pacing) ? SMOOTH_STREAM_PACING_PRESETS[options.pacing] : SMOOTH_STREAM_PACING_PRESETS.balanced;
4505
4527
  return {
4506
4528
  bufferFactor: finiteOr(options.bufferFactor, preset.bufferFactor),
4507
4529
  correctionTauMs: finiteOr(options.correctionTauMs, preset.correctionTauMs),
4508
4530
  emaTauMs: finiteOr(options.emaTauMs, preset.emaTauMs),
4509
4531
  minCharsPerSecond: finiteOr(options.minCharsPerSecond, preset.minCharsPerSecond),
4510
- drainMs: finiteOr(options.drainMs, preset.drainMs)
4532
+ drainMs: finiteOr(options.drainMs, preset.drainMs),
4533
+ maxLagMs: finiteOr(options.maxLagMs, preset.maxLagMs ?? 2500)
4511
4534
  };
4512
4535
  };
4513
4536
  const recordArrival = (t, added) => {
@@ -4518,12 +4541,8 @@ var createSmoothStreamController = (options = {}) => {
4518
4541
  }
4519
4542
  const gap = Math.max(1, t - lastArrivalAt);
4520
4543
  lastArrivalAt = t;
4521
- const tau = Math.max(1, resolveParams().emaTauMs);
4522
- if (gap > tau) return;
4523
- const alpha = 1 - Math.exp(-gap / tau);
4524
- const instantRate = added * 1e3 / gap;
4525
- rateEma = (rateEma ?? 0) + alpha * (instantRate - (rateEma ?? 0));
4526
- intervalEma = (intervalEma ?? 0) + alpha * (gap - (intervalEma ?? 0));
4544
+ gapWindow.push({ gap, added });
4545
+ if (gapWindow.length > GAP_WINDOW) gapWindow.shift();
4527
4546
  };
4528
4547
  const notify = () => {
4529
4548
  visibleCache = source.slice(0, visibleEnd);
@@ -4549,15 +4568,12 @@ var createSmoothStreamController = (options = {}) => {
4549
4568
  let rate;
4550
4569
  if (finished && drainDeadlineAt !== void 0) {
4551
4570
  rate = Math.max(params.minCharsPerSecond, pending.length * 1e3 / Math.max(1, drainDeadlineAt - t));
4552
- } else if (rateEma !== void 0 && intervalEma !== void 0) {
4553
- const targetBuffer = Math.max(
4554
- 1,
4555
- Math.min(params.bufferFactor * rateEma * (intervalEma / 1e3), rateEma * MAX_TARGET_BUFFER_MS / 1e3)
4556
- );
4557
- rate = Math.max(
4558
- params.minCharsPerSecond,
4559
- rateEma + (pending.length - targetBuffer) * 1e3 / Math.max(1, params.correctionTauMs)
4560
- );
4571
+ } else if (gapWindow.length > 0 && lastArrivalAt !== void 0) {
4572
+ const gaps = gapWindow.map((s) => s.gap).sort((a, b) => a - b);
4573
+ const intervalQ = gaps[Math.min(gaps.length - 1, Math.floor(gaps.length * INTERVAL_QUANTILE))];
4574
+ const horizon = Math.max(16, Math.min(params.bufferFactor * intervalQ + HORIZON_PAD_MS, params.maxLagMs));
4575
+ const deadline = Math.max(lastArrivalAt + horizon, t + DEADLINE_FLOOR_MS);
4576
+ rate = Math.max(params.minCharsPerSecond, pending.length * 1e3 / Math.max(1, deadline - t));
4561
4577
  } else {
4562
4578
  rate = Math.max(params.minCharsPerSecond, pending.length * 1e3 / Math.max(1, params.correctionTauMs));
4563
4579
  }
@@ -4603,8 +4619,7 @@ var createSmoothStreamController = (options = {}) => {
4603
4619
  initialized = true;
4604
4620
  finished = false;
4605
4621
  drainDeadlineAt = void 0;
4606
- rateEma = void 0;
4607
- intervalEma = void 0;
4622
+ gapWindow = [];
4608
4623
  lastArrivalAt = void 0;
4609
4624
  source = next;
4610
4625
  visibleEnd = next.length;
@@ -4641,10 +4656,26 @@ var createSmoothStreamController = (options = {}) => {
4641
4656
  if (finished) return;
4642
4657
  finished = true;
4643
4658
  lastArrivalAt = void 0;
4644
- drainDeadlineAt = now() + resolveParams().drainMs;
4645
4659
  if (tentativeEnd > (pending.length > 0 ? pending[pending.length - 1] : visibleEnd)) {
4646
4660
  pending.push(tentativeEnd);
4647
4661
  }
4662
+ const params = resolveParams();
4663
+ let drainWin = params.drainMs;
4664
+ let chars = 0;
4665
+ let ms = 0;
4666
+ for (let i = 0; i < gapWindow.length - 1; i += 1) {
4667
+ chars += gapWindow[i].added;
4668
+ ms += gapWindow[i].gap;
4669
+ }
4670
+ const backlog = source.length - visibleEnd;
4671
+ if (chars > 0 && ms > 0 && backlog > 0) {
4672
+ const throughput = chars * 1e3 / ms;
4673
+ drainWin = Math.min(
4674
+ Math.max(backlog * 1e3 / (DRAIN_CATCHUP_FACTOR * throughput), params.drainMs),
4675
+ DRAIN_MAX_FACTOR * params.drainMs
4676
+ );
4677
+ }
4678
+ drainDeadlineAt = now() + drainWin;
4648
4679
  seam = source.length;
4649
4680
  ensureScheduled();
4650
4681
  },