@ai-react-markdown/engine 2.9.1 → 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.
@@ -4443,24 +4443,32 @@ var SMOOTH_STREAM_PACING_PRESETS = Object.freeze({
4443
4443
  correctionTauMs: 280,
4444
4444
  emaTauMs: 2600,
4445
4445
  minCharsPerSecond: 4,
4446
- drainMs: 320
4446
+ drainMs: 320,
4447
+ maxLagMs: 3500
4447
4448
  }),
4448
4449
  balanced: Object.freeze({
4449
4450
  bufferFactor: 1,
4450
4451
  correctionTauMs: 180,
4451
4452
  emaTauMs: 1800,
4452
4453
  minCharsPerSecond: 6,
4453
- drainMs: 240
4454
+ drainMs: 240,
4455
+ maxLagMs: 2500
4454
4456
  }),
4455
4457
  responsive: Object.freeze({
4456
4458
  bufferFactor: 0.45,
4457
4459
  correctionTauMs: 110,
4458
4460
  emaTauMs: 1100,
4459
4461
  minCharsPerSecond: 10,
4460
- drainMs: 150
4462
+ drainMs: 150,
4463
+ maxLagMs: 800
4461
4464
  })
4462
4465
  });
4463
- 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;
4464
4472
  var defaultNow = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
4465
4473
  var defaultSchedule = (cb) => {
4466
4474
  if (typeof requestAnimationFrame === "function") {
@@ -4511,18 +4519,18 @@ var createSmoothStreamController = (options = {}) => {
4511
4519
  let lastTickAt = 0;
4512
4520
  let cancelFrame;
4513
4521
  let disposed = false;
4514
- let rateEma;
4515
- let intervalEma;
4522
+ let gapWindow = [];
4516
4523
  let lastArrivalAt;
4517
4524
  const listeners = /* @__PURE__ */ new Set();
4518
4525
  const resolveParams = () => {
4519
- 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;
4520
4527
  return {
4521
4528
  bufferFactor: finiteOr(options.bufferFactor, preset.bufferFactor),
4522
4529
  correctionTauMs: finiteOr(options.correctionTauMs, preset.correctionTauMs),
4523
4530
  emaTauMs: finiteOr(options.emaTauMs, preset.emaTauMs),
4524
4531
  minCharsPerSecond: finiteOr(options.minCharsPerSecond, preset.minCharsPerSecond),
4525
- drainMs: finiteOr(options.drainMs, preset.drainMs)
4532
+ drainMs: finiteOr(options.drainMs, preset.drainMs),
4533
+ maxLagMs: finiteOr(options.maxLagMs, preset.maxLagMs ?? 2500)
4526
4534
  };
4527
4535
  };
4528
4536
  const recordArrival = (t, added) => {
@@ -4533,12 +4541,8 @@ var createSmoothStreamController = (options = {}) => {
4533
4541
  }
4534
4542
  const gap = Math.max(1, t - lastArrivalAt);
4535
4543
  lastArrivalAt = t;
4536
- const tau = Math.max(1, resolveParams().emaTauMs);
4537
- if (gap > tau) return;
4538
- const alpha = 1 - Math.exp(-gap / tau);
4539
- const instantRate = added * 1e3 / gap;
4540
- rateEma = (rateEma ?? 0) + alpha * (instantRate - (rateEma ?? 0));
4541
- intervalEma = (intervalEma ?? 0) + alpha * (gap - (intervalEma ?? 0));
4544
+ gapWindow.push({ gap, added });
4545
+ if (gapWindow.length > GAP_WINDOW) gapWindow.shift();
4542
4546
  };
4543
4547
  const notify = () => {
4544
4548
  visibleCache = source.slice(0, visibleEnd);
@@ -4564,15 +4568,12 @@ var createSmoothStreamController = (options = {}) => {
4564
4568
  let rate;
4565
4569
  if (finished && drainDeadlineAt !== void 0) {
4566
4570
  rate = Math.max(params.minCharsPerSecond, pending.length * 1e3 / Math.max(1, drainDeadlineAt - t));
4567
- } else if (rateEma !== void 0 && intervalEma !== void 0) {
4568
- const targetBuffer = Math.max(
4569
- 1,
4570
- Math.min(params.bufferFactor * rateEma * (intervalEma / 1e3), rateEma * MAX_TARGET_BUFFER_MS / 1e3)
4571
- );
4572
- rate = Math.max(
4573
- params.minCharsPerSecond,
4574
- rateEma + (pending.length - targetBuffer) * 1e3 / Math.max(1, params.correctionTauMs)
4575
- );
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));
4576
4577
  } else {
4577
4578
  rate = Math.max(params.minCharsPerSecond, pending.length * 1e3 / Math.max(1, params.correctionTauMs));
4578
4579
  }
@@ -4618,8 +4619,7 @@ var createSmoothStreamController = (options = {}) => {
4618
4619
  initialized = true;
4619
4620
  finished = false;
4620
4621
  drainDeadlineAt = void 0;
4621
- rateEma = void 0;
4622
- intervalEma = void 0;
4622
+ gapWindow = [];
4623
4623
  lastArrivalAt = void 0;
4624
4624
  source = next;
4625
4625
  visibleEnd = next.length;
@@ -4656,10 +4656,26 @@ var createSmoothStreamController = (options = {}) => {
4656
4656
  if (finished) return;
4657
4657
  finished = true;
4658
4658
  lastArrivalAt = void 0;
4659
- drainDeadlineAt = now() + resolveParams().drainMs;
4660
4659
  if (tentativeEnd > (pending.length > 0 ? pending[pending.length - 1] : visibleEnd)) {
4661
4660
  pending.push(tentativeEnd);
4662
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;
4663
4679
  seam = source.length;
4664
4680
  ensureScheduled();
4665
4681
  },