@floegence/floeterm-terminal-web 0.16.6 → 0.17.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.
@@ -6,7 +6,8 @@ function emitSemanticDebugTrace(event) {
6
6
  }
7
7
  // History is owned by a single terminal session. Keep the budget local to the
8
8
  // controller so one busy session cannot evict another session's history.
9
- const MAX_SESSION_HISTORY_CACHE_BYTES = 4 * 1024 * 1024;
9
+ const MAX_SESSION_HISTORY_CACHE_BYTES = 32 * 1024 * 1024;
10
+ const MAX_PAGE_HISTORY_CACHE_BYTES = 128 * 1024 * 1024;
10
11
  const HISTORY_WINDOW_CACHE_TARGET_BYTES = Math.floor(MAX_SESSION_HISTORY_CACHE_BYTES * 0.85);
11
12
  const HISTORY_WINDOW_BASE_MULTIPLIER = 10;
12
13
  const HISTORY_WINDOW_FAST_MULTIPLIER = 20;
@@ -15,6 +16,9 @@ const HISTORY_SCROLL_BURST_GAP_MS = 250;
15
16
  const HISTORY_SCROLL_BURST_DECAY_MS = 500;
16
17
  const HISTORY_SCROLL_BURST_VIEWPORTS = 2;
17
18
  const HISTORY_WINDOW_FORWARD_BIAS = 0.7;
19
+ const HISTORY_PREFETCH_DELAY_MS = 500;
20
+ const HISTORY_PREFETCH_SCREENS = 100;
21
+ const HISTORY_PREFETCH_FRONT_SCREENS = 80;
18
22
  export class HistoryViewportController {
19
23
  constructor(options) {
20
24
  this.options = options;
@@ -40,6 +44,13 @@ export class HistoryViewportController {
40
44
  this.scrollBurstRows = 0;
41
45
  this.scrollBurstAt = 0;
42
46
  this.estimatedHistoryRowBytes = 0;
47
+ this.pendingProjection = false;
48
+ this.skeletonSequence = 0;
49
+ this.nextRequestID = 0;
50
+ this.prefetchTimer = null;
51
+ this.prefetchAbort = null;
52
+ this.prefetchTask = null;
53
+ HistoryViewportController.instances.add(this);
43
54
  this.requestFrame = options.requestAnimationFrame
44
55
  ?? globalThis.requestAnimationFrame?.bind(globalThis)
45
56
  ?? (callback => globalThis.setTimeout(() => callback(performance.now()), 0));
@@ -51,14 +62,22 @@ export class HistoryViewportController {
51
62
  apply(presentation) {
52
63
  if (this.disposed)
53
64
  return;
54
- const invalidated = this.latest !== null && ((this.latest.state.contentEpoch ?? 0) !== (presentation.state.contentEpoch ?? 0)
55
- || this.latest.geometry.generation !== presentation.geometry.generation
56
- || this.latest.geometry.cols !== presentation.geometry.cols
57
- || this.latest.geometry.rows !== presentation.geometry.rows);
65
+ const previous = this.latest;
66
+ const invalidated = previous !== null && ((previous.state.contentEpoch ?? 0) !== (presentation.state.contentEpoch ?? 0)
67
+ || previous.geometry.generation !== presentation.geometry.generation
68
+ || previous.geometry.cols !== presentation.geometry.cols
69
+ || previous.geometry.rows !== presentation.geometry.rows
70
+ || (previous.frame.history.historyEpoch !== undefined
71
+ && presentation.frame.history.historyEpoch !== undefined
72
+ && previous.frame.history.historyEpoch !== presentation.frame.history.historyEpoch));
58
73
  this.latest = presentation;
59
74
  this.options.renderer.apply(presentation);
60
- if (invalidated)
75
+ if (invalidated) {
61
76
  this.resetHistory(true);
77
+ }
78
+ else if (previous) {
79
+ this.remapActiveHistory(previous);
80
+ }
62
81
  this.emitState();
63
82
  }
64
83
  scrollByRows(deltaRows) {
@@ -109,6 +128,7 @@ export class HistoryViewportController {
109
128
  // Invalidate any history request that was started before returning to the
110
129
  // live frontier. Its response must never re-enter the visible surface.
111
130
  this.epoch += 1;
131
+ this.cancelPrefetch();
112
132
  this.desiredOffset = null;
113
133
  this.preferExactViewport = false;
114
134
  this.visible = null;
@@ -121,6 +141,7 @@ export class HistoryViewportController {
121
141
  if (!this.latest || this.disposed || !Number.isFinite(offset))
122
142
  return;
123
143
  const target = clamp(Math.trunc(offset), 0, this.latest.frame.history.screenStartOffset);
144
+ this.cancelPrefetch();
124
145
  const current = this.currentIntentOffset();
125
146
  this.recordScrollIntent(target - current);
126
147
  emitSemanticDebugTrace({ kind: 'history-cache-check', at: performance.now(), target,
@@ -142,18 +163,22 @@ export class HistoryViewportController {
142
163
  const frontierWindow = this.frontier;
143
164
  if (frontierWindow && this.isReusableWindow(frontierWindow, target, this.latest.geometry.rows)) {
144
165
  this.desiredOffset = null;
145
- this.display(sliceHistoryWindow(frontierWindow, target, this.latest.geometry.rows));
166
+ this.display(this.sliceWindow(frontierWindow, target, this.latest.geometry.rows));
146
167
  return;
147
168
  }
148
169
  const window = this.findCachedWindow(target, this.latest.geometry.rows);
149
170
  if (window) {
150
171
  this.desiredOffset = null;
151
172
  this.putCache(window);
152
- this.display(sliceHistoryWindow(window, target, this.latest.geometry.rows));
173
+ this.display(this.sliceWindow(window, target, this.latest.geometry.rows));
153
174
  return;
154
175
  }
155
176
  this.desiredOffset = target;
156
177
  this.error = null;
178
+ // Project a geometry-stable placeholder before the first remote byte
179
+ // arrives. This keeps the wheel gesture visible in the same animation
180
+ // frame instead of leaving the old viewport on screen during the RTT.
181
+ this.projectPending(target);
157
182
  this.startLane();
158
183
  this.emitState();
159
184
  }
@@ -247,6 +272,8 @@ export class HistoryViewportController {
247
272
  return;
248
273
  this.disposed = true;
249
274
  this.epoch += 1;
275
+ this.cancelPrefetch();
276
+ HistoryViewportController.instances.delete(this);
250
277
  if (this.wheelFrame !== null)
251
278
  this.cancelFrame(this.wheelFrame);
252
279
  this.wheelFrame = null;
@@ -295,7 +322,7 @@ export class HistoryViewportController {
295
322
  this.desiredOffset = null;
296
323
  }
297
324
  this.putCache(cachedWindow);
298
- const viewport = sliceHistoryWindow(cachedWindow, target, this.latest.geometry.rows);
325
+ const viewport = this.sliceWindow(cachedWindow, target, this.latest.geometry.rows);
299
326
  if (isCurrentTarget || this.shouldDisplayIntermediate(viewport.offset, desiredOffset)) {
300
327
  this.display(viewport);
301
328
  }
@@ -310,7 +337,7 @@ export class HistoryViewportController {
310
337
  if (isCurrentTarget) {
311
338
  this.desiredOffset = null;
312
339
  }
313
- if (isCurrentTarget || this.shouldDisplayIntermediate(viewport.offset, desiredOffset)) {
340
+ if (isCurrentTarget || (!this.pendingProjection && this.shouldDisplayIntermediate(viewport.offset, desiredOffset))) {
314
341
  this.display(viewport);
315
342
  }
316
343
  }
@@ -343,7 +370,6 @@ export class HistoryViewportController {
343
370
  throw new Error('terminal history requires a live Presentation');
344
371
  const rows = this.latest.geometry.rows;
345
372
  const totalRows = this.latest.frame.history.totalRows;
346
- const revision = this.latest.frame.history.revision;
347
373
  const preferExactViewport = this.preferExactViewport;
348
374
  // A large wheel delta needs one exact target request, but must not poison
349
375
  // the rest of the session. Subsequent small deltas should use a reusable
@@ -354,9 +380,9 @@ export class HistoryViewportController {
354
380
  return this.fetchExactFromBoundary(target, rows);
355
381
  const cachedWindow = this.findCachedWindow(target, rows);
356
382
  if (cachedWindow)
357
- return sliceHistoryWindow(cachedWindow, target, rows);
383
+ return this.sliceWindow(cachedWindow, target, rows);
358
384
  let frontier = this.frontier;
359
- if (frontier && (frontier.revision !== revision || frontier.totalRows !== totalRows)) {
385
+ if (frontier && !this.isCompatibleWindow(frontier)) {
360
386
  this.frontier = null;
361
387
  frontier = null;
362
388
  }
@@ -365,7 +391,7 @@ export class HistoryViewportController {
365
391
  }
366
392
  if (frontier.window !== true)
367
393
  return frontier;
368
- return sliceHistoryWindow(frontier, target, rows);
394
+ return this.sliceWindow(frontier, target, rows);
369
395
  }
370
396
  async fetchExactFromBoundary(target, rows) {
371
397
  // A boundary request already supports exact targeting in one RPC and
@@ -440,7 +466,7 @@ export class HistoryViewportController {
440
466
  // project the nearest viewport that is actually present in this atomic
441
467
  // snapshot instead of issuing a directionally invalid anchored request.
442
468
  const nearest = clamp(target, current.offset, current.offset + current.rows - rows);
443
- return sliceHistoryWindow(current, nearest, rows);
469
+ return this.sliceWindow(current, nearest, rows);
444
470
  }
445
471
  if (!current)
446
472
  throw new Error('terminal history window is unavailable');
@@ -539,14 +565,58 @@ export class HistoryViewportController {
539
565
  this.scrollBurstAt = now;
540
566
  }
541
567
  windowCovers(window, target, rows) {
542
- return target >= window.offset && target + rows <= window.offset + window.rows;
568
+ return this.windowCoversOrdinal(window, target, rows);
569
+ }
570
+ windowCoversOrdinal(window, target, rows) {
571
+ const targetOrdinal = this.offsetToOrdinal(target);
572
+ if (targetOrdinal === null || window.firstRowOrdinal === undefined) {
573
+ return target >= window.offset && target + rows <= window.offset + window.rows;
574
+ }
575
+ const windowStart = window.firstRowOrdinal + window.offset;
576
+ return targetOrdinal >= windowStart && targetOrdinal + rows <= windowStart + window.rows;
577
+ }
578
+ offsetToOrdinal(offset) {
579
+ const latest = this.latest?.frame.history;
580
+ if (!latest || latest.firstRowOrdinal === undefined)
581
+ return null;
582
+ return latest.firstRowOrdinal + offset;
583
+ }
584
+ sliceWindow(window, target, rows) {
585
+ const remapped = remapViewportOffset(window, target, this.latest);
586
+ if (!remapped) {
587
+ throw new SemanticHistoryError('snapshot_superseded', 'semantic history window no longer covers its target ordinal');
588
+ }
589
+ return sliceHistoryWindow(remapped, target, rows);
590
+ }
591
+ remapActiveHistory(previous) {
592
+ if (!this.latest)
593
+ return;
594
+ const previousHistory = previous.frame.history;
595
+ const currentHistory = this.latest.frame.history;
596
+ if (previousHistory.historyEpoch === undefined || currentHistory.historyEpoch === undefined
597
+ || previousHistory.firstRowOrdinal === undefined || currentHistory.firstRowOrdinal === undefined
598
+ || previousHistory.historyEpoch !== currentHistory.historyEpoch)
599
+ return;
600
+ const previousFirstRowOrdinal = previousHistory.firstRowOrdinal;
601
+ const currentFirstRowOrdinal = currentHistory.firstRowOrdinal;
602
+ const remapOffset = (offset) => clamp(previousFirstRowOrdinal + offset - currentFirstRowOrdinal, 0, currentHistory.screenStartOffset);
603
+ if (this.desiredOffset !== null)
604
+ this.desiredOffset = remapOffset(this.desiredOffset);
605
+ if (!this.visible)
606
+ return;
607
+ const target = remapOffset(this.visible.offset);
608
+ const remapped = remapViewportOffset(this.visible, target, this.latest);
609
+ if (remapped)
610
+ this.visible = remapped;
543
611
  }
544
612
  display(viewport) {
545
613
  this.visible = viewport;
614
+ this.pendingProjection = false;
546
615
  this.error = null;
547
616
  this.putCache(viewport);
548
617
  this.projectFrame(viewport.frame);
549
618
  this.evictCache();
619
+ this.schedulePrefetch();
550
620
  this.emitState();
551
621
  }
552
622
  cacheViewport(viewport) {
@@ -560,6 +630,15 @@ export class HistoryViewportController {
560
630
  }
561
631
  this.options.renderer.project(frame);
562
632
  }
633
+ projectPending(target) {
634
+ if (!this.latest)
635
+ return;
636
+ this.pendingProjection = true;
637
+ const pending = createPendingViewport(this.latest, target, this.transportGeneration ?? 1, ++this.skeletonSequence);
638
+ this.visible = pending;
639
+ emitSemanticDebugTrace({ kind: 'history-pending', at: performance.now(), target });
640
+ this.projectFrame(pending.frame);
641
+ }
563
642
  evictCache() {
564
643
  for (;;) {
565
644
  const candidates = [...this.cache.entries()]
@@ -576,6 +655,109 @@ export class HistoryViewportController {
576
655
  emitSemanticDebugTrace({ kind: 'history-cache-state', at: performance.now(),
577
656
  cacheEntries: this.cache.size, cacheBytes: this.cacheBytes,
578
657
  cacheExtraBytes: this.cacheExtraBytes() });
658
+ HistoryViewportController.evictGlobalCaches();
659
+ }
660
+ static evictGlobalCaches() {
661
+ const extraBytes = () => [...HistoryViewportController.instances]
662
+ .reduce((sum, controller) => sum + controller.cacheExtraBytes(), 0);
663
+ while (extraBytes() > MAX_PAGE_HISTORY_CACHE_BYTES) {
664
+ let oldest = null;
665
+ let oldestTouched = Number.POSITIVE_INFINITY;
666
+ for (const controller of HistoryViewportController.instances) {
667
+ const touched = controller.oldestExtraTouched();
668
+ if (touched < oldestTouched) {
669
+ oldestTouched = touched;
670
+ oldest = controller;
671
+ }
672
+ }
673
+ if (!oldest || !Number.isFinite(oldestTouched))
674
+ break;
675
+ oldest.removeOldestExtra();
676
+ }
677
+ }
678
+ oldestExtraTouched() {
679
+ let oldest = Number.POSITIVE_INFINITY;
680
+ for (const item of this.cache.values()) {
681
+ if (item.viewport !== this.visible)
682
+ oldest = Math.min(oldest, item.touched);
683
+ }
684
+ return oldest;
685
+ }
686
+ removeOldestExtra() {
687
+ let key = null;
688
+ let touched = Number.POSITIVE_INFINITY;
689
+ for (const [entryKey, item] of this.cache.entries()) {
690
+ if (item.viewport !== this.visible && item.touched < touched) {
691
+ key = entryKey;
692
+ touched = item.touched;
693
+ }
694
+ }
695
+ if (key !== null)
696
+ this.removeCacheEntry(key);
697
+ }
698
+ schedulePrefetch() {
699
+ if (this.disposed || !this.frontier || this.frontier.window !== true)
700
+ return;
701
+ if (this.prefetchTimer !== null)
702
+ clearTimeout(this.prefetchTimer);
703
+ const epoch = this.epoch;
704
+ this.prefetchTimer = setTimeout(() => {
705
+ this.prefetchTimer = null;
706
+ void this.runPrefetch(epoch);
707
+ }, HISTORY_PREFETCH_DELAY_MS);
708
+ }
709
+ cancelPrefetch() {
710
+ if (this.prefetchTimer !== null)
711
+ clearTimeout(this.prefetchTimer);
712
+ this.prefetchTimer = null;
713
+ this.prefetchAbort?.abort();
714
+ this.prefetchAbort = null;
715
+ this.prefetchTask = null;
716
+ }
717
+ async runPrefetch(epoch) {
718
+ const current = this.frontier;
719
+ const latest = this.latest;
720
+ if (this.disposed || epoch !== this.epoch || !latest || !current || current.window !== true)
721
+ return;
722
+ const rows = latest.geometry.rows;
723
+ const direction = this.scrollDirection || -1;
724
+ const totalRows = latest.frame.history.totalRows;
725
+ const windowRows = Math.min(totalRows, HISTORY_WINDOW_MAX_ROWS, Math.max(rows, rows * HISTORY_PREFETCH_SCREENS));
726
+ const frontRows = rows * HISTORY_PREFETCH_FRONT_SCREENS;
727
+ const target = clamp(current.offset + (direction < 0 ? -frontRows : frontRows), 0, Math.max(0, totalRows - rows));
728
+ const windowStart = this.windowStart(target, rows, windowRows, direction);
729
+ const abort = new AbortController();
730
+ this.prefetchAbort = abort;
731
+ const requestID = `prefetch-${++this.nextRequestID}`;
732
+ this.prefetchTask = runSemanticHistoryRequest(async () => validateHistoryWindow(await this.options.request({
733
+ requestId: requestID,
734
+ priority: 'prefetch',
735
+ signal: abort.signal,
736
+ lane: 'viewport',
737
+ direction: windowStart < current.offset ? 'backward' : 'forward',
738
+ anchor: current.anchor,
739
+ snapshotId: current.snapshotId,
740
+ offset: current.offset,
741
+ targetOffset: windowStart,
742
+ viewportRows: windowRows,
743
+ windowRows,
744
+ })), { priority: 'prefetch', signal: abort.signal }).then(next => {
745
+ if (this.disposed || epoch !== this.epoch || abort.signal.aborted)
746
+ return;
747
+ this.assertTransportGeneration(next);
748
+ this.acceptLineage(next);
749
+ this.frontier = next;
750
+ this.cacheWindow(next);
751
+ }).catch(error => {
752
+ if (!abort.signal.aborted) {
753
+ emitSemanticDebugTrace({ kind: 'history-prefetch-error', at: performance.now(), error });
754
+ }
755
+ }).finally(() => {
756
+ if (this.prefetchAbort === abort)
757
+ this.prefetchAbort = null;
758
+ this.prefetchTask = null;
759
+ });
760
+ await this.prefetchTask;
579
761
  }
580
762
  cacheExtraBytes() {
581
763
  return [...this.cache.values()]
@@ -584,6 +766,7 @@ export class HistoryViewportController {
584
766
  }
585
767
  isCompatible(viewport) {
586
768
  return Boolean(this.latest
769
+ && historyIdentityCompatible(viewport, this.latest)
587
770
  && viewport.contentEpoch === (this.latest.state.contentEpoch ?? 0)
588
771
  && viewport.geometryGeneration === this.latest.geometry.generation
589
772
  && (this.transportGeneration === null || viewport.transportGeneration === this.transportGeneration)
@@ -596,12 +779,12 @@ export class HistoryViewportController {
596
779
  isReusableViewport(viewport) {
597
780
  return Boolean(this.latest
598
781
  && this.isCompatible(viewport)
599
- && viewport.totalRows === this.latest.frame.history.totalRows
600
- && viewport.revision === this.latest.frame.history.revision);
782
+ && historyViewportRangeCompatible(viewport, this.latest));
601
783
  }
602
784
  isCompatibleWindow(viewport) {
603
785
  return Boolean(this.latest
604
786
  && viewport.window === true
787
+ && historyIdentityCompatible(viewport, this.latest)
605
788
  && viewport.contentEpoch === (this.latest.state.contentEpoch ?? 0)
606
789
  && viewport.geometryGeneration === this.latest.geometry.generation
607
790
  && (this.transportGeneration === null || viewport.transportGeneration === this.transportGeneration)
@@ -613,9 +796,9 @@ export class HistoryViewportController {
613
796
  isReusableWindow(viewport, target, rows) {
614
797
  return Boolean(this.latest
615
798
  && this.isCompatibleWindow(viewport)
616
- && viewport.totalRows === this.latest.frame.history.totalRows
799
+ && historyViewportRangeCompatible(viewport, this.latest)
617
800
  && this.windowCovers(viewport, target, rows)
618
- && viewport.revision === this.latest.frame.history.revision);
801
+ && viewport.revision <= this.latest.frame.history.revision);
619
802
  }
620
803
  assertTransportGeneration(viewport) {
621
804
  if (this.transportGeneration === null) {
@@ -628,9 +811,11 @@ export class HistoryViewportController {
628
811
  }
629
812
  resetHistory(projectLatest, error = null) {
630
813
  this.epoch += 1;
814
+ this.cancelPrefetch();
631
815
  this.clearCache();
632
816
  this.frontier = null;
633
817
  this.visible = null;
818
+ this.pendingProjection = false;
634
819
  this.desiredOffset = null;
635
820
  this.preferExactViewport = false;
636
821
  this.scrollDirection = 0;
@@ -645,7 +830,7 @@ export class HistoryViewportController {
645
830
  this.emitState();
646
831
  }
647
832
  shouldDisplayIntermediate(target, desiredOffset) {
648
- if (desiredOffset === null || this.visible === null)
833
+ if (this.pendingProjection || desiredOffset === null || this.visible === null)
649
834
  return false;
650
835
  const currentOffset = this.visible.offset;
651
836
  const minimum = Math.min(currentOffset, desiredOffset);
@@ -655,8 +840,11 @@ export class HistoryViewportController {
655
840
  }
656
841
  findCachedViewport(offset) {
657
842
  for (const item of this.cache.values()) {
658
- if (item.viewport.offset === offset && this.isReusableViewport(item.viewport))
659
- return item.viewport;
843
+ if (!this.isReusableViewport(item.viewport))
844
+ continue;
845
+ const remapped = remapViewportOffset(item.viewport, offset, this.latest);
846
+ if (remapped)
847
+ return remapped;
660
848
  }
661
849
  return null;
662
850
  }
@@ -669,7 +857,7 @@ export class HistoryViewportController {
669
857
  && (!candidate || item.touched > candidate.touched))
670
858
  candidate = item;
671
859
  }
672
- return candidate?.viewport ?? null;
860
+ return candidate ? remapViewportOffset(candidate.viewport, offset, this.latest) : null;
673
861
  }
674
862
  cacheWindow(viewport) {
675
863
  if (viewport.window !== true)
@@ -738,9 +926,152 @@ export class HistoryViewportController {
738
926
  this.options.onState?.(this.getState());
739
927
  }
740
928
  }
929
+ HistoryViewportController.instances = new Set();
741
930
  function clamp(value, minimum, maximum) {
742
931
  return Math.max(minimum, Math.min(maximum, value));
743
932
  }
933
+ function historyIdentityCompatible(viewport, latest) {
934
+ const current = latest.frame.history;
935
+ if (viewport.historyEpoch === undefined || current.historyEpoch === undefined
936
+ || viewport.firstRowOrdinal === undefined || current.firstRowOrdinal === undefined) {
937
+ return true;
938
+ }
939
+ if (viewport.historyEpoch !== current.historyEpoch)
940
+ return false;
941
+ const start = viewport.firstRowOrdinal + viewport.offset;
942
+ const latestStart = current.firstRowOrdinal;
943
+ const latestEnd = latestStart + current.totalRows;
944
+ return start >= latestStart && start + viewport.rows <= latestEnd;
945
+ }
946
+ function historyViewportRangeCompatible(viewport, latest) {
947
+ const current = latest.frame.history;
948
+ if (viewport.firstRowOrdinal === undefined || current.firstRowOrdinal === undefined) {
949
+ return viewport.offset + viewport.rows <= current.totalRows && viewport.totalRows <= current.totalRows;
950
+ }
951
+ const start = viewport.firstRowOrdinal + viewport.offset;
952
+ const end = start + viewport.rows;
953
+ const latestStart = current.firstRowOrdinal;
954
+ return start >= latestStart && end <= latestStart + current.totalRows;
955
+ }
956
+ function remapViewportOffset(viewport, targetOffset, latest) {
957
+ if (!latest)
958
+ return null;
959
+ const current = latest.frame.history;
960
+ if (viewport.historyEpoch !== undefined && current.historyEpoch !== undefined
961
+ && viewport.firstRowOrdinal !== undefined && current.firstRowOrdinal !== undefined) {
962
+ const sourceOrdinal = viewport.firstRowOrdinal + viewport.offset;
963
+ const targetOrdinal = current.firstRowOrdinal + targetOffset;
964
+ if (viewport.window === true) {
965
+ const windowStart = viewport.firstRowOrdinal + viewport.offset;
966
+ if (targetOrdinal < windowStart || targetOrdinal + latest.geometry.rows > windowStart + viewport.rows)
967
+ return null;
968
+ const remappedOffset = windowStart - current.firstRowOrdinal;
969
+ if (remappedOffset === viewport.offset
970
+ && viewport.firstRowOrdinal === current.firstRowOrdinal
971
+ && viewport.totalRows === current.totalRows)
972
+ return viewport;
973
+ const remapped = {
974
+ ...viewport,
975
+ offset: remappedOffset,
976
+ totalRows: current.totalRows,
977
+ screenStartOffset: current.totalRows - viewport.rows,
978
+ firstRowOrdinal: current.firstRowOrdinal,
979
+ screenStartRowOrdinal: current.firstRowOrdinal + current.totalRows - viewport.rows,
980
+ frame: {
981
+ ...viewport.frame,
982
+ history: {
983
+ ...viewport.frame.history,
984
+ totalRows: current.totalRows,
985
+ screenStartOffset: current.totalRows - viewport.rows,
986
+ firstRowOrdinal: current.firstRowOrdinal,
987
+ screenStartRowOrdinal: current.firstRowOrdinal + current.totalRows - viewport.rows,
988
+ },
989
+ },
990
+ };
991
+ return remapped;
992
+ }
993
+ if (sourceOrdinal !== targetOrdinal)
994
+ return null;
995
+ if (viewport.offset === targetOffset
996
+ && viewport.firstRowOrdinal === current.firstRowOrdinal
997
+ && viewport.totalRows === current.totalRows)
998
+ return viewport;
999
+ return {
1000
+ ...viewport,
1001
+ offset: targetOffset,
1002
+ totalRows: current.totalRows,
1003
+ screenStartOffset: current.totalRows - viewport.rows,
1004
+ firstRowOrdinal: current.firstRowOrdinal,
1005
+ screenStartRowOrdinal: current.firstRowOrdinal + current.totalRows - viewport.rows,
1006
+ frame: {
1007
+ ...viewport.frame,
1008
+ history: {
1009
+ ...viewport.frame.history,
1010
+ totalRows: current.totalRows,
1011
+ screenStartOffset: current.totalRows - viewport.rows,
1012
+ firstRowOrdinal: current.firstRowOrdinal,
1013
+ screenStartRowOrdinal: current.firstRowOrdinal + current.totalRows - viewport.rows,
1014
+ },
1015
+ },
1016
+ };
1017
+ }
1018
+ if (viewport.window === true)
1019
+ return viewport;
1020
+ if (viewport.offset !== targetOffset)
1021
+ return null;
1022
+ return viewport;
1023
+ }
1024
+ function createPendingViewport(presentation, offset, transportGeneration, sequence) {
1025
+ const source = presentation.frame;
1026
+ const totalRows = source.history.totalRows;
1027
+ const rows = source.height;
1028
+ const marker = `pending-${sequence}`;
1029
+ const skeletonRow = Array.from({ length: source.width }, () => ({
1030
+ text: '',
1031
+ width: 1,
1032
+ }));
1033
+ const frame = {
1034
+ ...source,
1035
+ rows: Array.from({ length: rows }, () => ({ cells: skeletonRow.map(cell => ({ ...cell })) })),
1036
+ cursor: { ...source.cursor, visible: false },
1037
+ history: {
1038
+ revision: source.history.revision,
1039
+ totalRows,
1040
+ screenStartOffset: totalRows - rows,
1041
+ historyEpoch: source.history.historyEpoch,
1042
+ firstRowOrdinal: source.history.firstRowOrdinal,
1043
+ screenStartRowOrdinal: source.history.firstRowOrdinal === undefined
1044
+ ? source.history.screenStartRowOrdinal
1045
+ : source.history.firstRowOrdinal + totalRows - rows,
1046
+ pending: true,
1047
+ pendingOffset: offset,
1048
+ },
1049
+ graphics: { generation: source.graphics.generation, images: [], placements: [] },
1050
+ };
1051
+ return {
1052
+ snapshotId: marker,
1053
+ lane: 'viewport',
1054
+ revision: source.history.revision,
1055
+ transportGeneration,
1056
+ contentEpoch: presentation.state.contentEpoch ?? 0,
1057
+ geometryGeneration: presentation.geometry.generation,
1058
+ cols: source.width,
1059
+ rows,
1060
+ anchor: marker,
1061
+ firstAvailable: marker,
1062
+ lastAvailable: marker,
1063
+ screenStart: marker,
1064
+ offset,
1065
+ totalRows,
1066
+ screenStartOffset: totalRows - rows,
1067
+ historyEpoch: source.history.historyEpoch,
1068
+ firstRowOrdinal: source.history.firstRowOrdinal,
1069
+ screenStartRowOrdinal: source.history.screenStartRowOrdinal,
1070
+ hasPrevious: offset > 0,
1071
+ hasNext: offset < totalRows - rows,
1072
+ frame,
1073
+ };
1074
+ }
744
1075
  function isLegacyBoundaryTargetRejection(cause) {
745
1076
  if (!(cause instanceof Error))
746
1077
  return false;
@@ -804,6 +1135,11 @@ function sliceHistoryWindow(window, offset, rows) {
804
1135
  revision: window.revision,
805
1136
  totalRows: window.totalRows,
806
1137
  screenStartOffset: window.totalRows - rows,
1138
+ historyEpoch: window.historyEpoch,
1139
+ firstRowOrdinal: window.firstRowOrdinal,
1140
+ screenStartRowOrdinal: window.firstRowOrdinal === undefined
1141
+ ? window.screenStartRowOrdinal
1142
+ : window.firstRowOrdinal + window.totalRows - rows,
807
1143
  },
808
1144
  graphics: { ...frame.graphics, placements },
809
1145
  };
@@ -823,6 +1159,11 @@ function sliceHistoryWindow(window, offset, rows) {
823
1159
  offset,
824
1160
  totalRows: window.totalRows,
825
1161
  screenStartOffset,
1162
+ historyEpoch: window.historyEpoch,
1163
+ firstRowOrdinal: window.firstRowOrdinal,
1164
+ screenStartRowOrdinal: window.firstRowOrdinal === undefined
1165
+ ? window.screenStartRowOrdinal
1166
+ : window.firstRowOrdinal + screenStartOffset,
826
1167
  hasPrevious: offset > 0,
827
1168
  hasNext: offset < screenStartOffset,
828
1169
  frame: slicedFrame,