@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.
- package/README.md +1 -1
- package/dist/entries/semantic.d.ts +1 -1
- package/dist/entries/semantic.d.ts.map +1 -1
- package/dist/live/transport.d.ts +1 -0
- package/dist/live/transport.d.ts.map +1 -1
- package/dist/live/transport.js +85 -19
- package/dist/live/transport.js.map +1 -1
- package/dist/semantic/HistoryViewportController.d.ts +18 -0
- package/dist/semantic/HistoryViewportController.d.ts.map +1 -1
- package/dist/semantic/HistoryViewportController.js +365 -24
- package/dist/semantic/HistoryViewportController.js.map +1 -1
- package/dist/semantic/RendererSurface.d.ts +7 -0
- package/dist/semantic/RendererSurface.d.ts.map +1 -1
- package/dist/semantic/RendererSurface.js +81 -12
- package/dist/semantic/RendererSurface.js.map +1 -1
- package/dist/semantic/historyRequestScheduler.d.ts +5 -1
- package/dist/semantic/historyRequestScheduler.d.ts.map +1 -1
- package/dist/semantic/historyRequestScheduler.js +52 -14
- package/dist/semantic/historyRequestScheduler.js.map +1 -1
- package/dist/semantic/presentation.d.ts +21 -0
- package/dist/semantic/presentation.d.ts.map +1 -1
- package/dist/semantic/presentation.js +21 -1
- package/dist/semantic/presentation.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 =
|
|
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
|
|
55
|
-
|
|
56
|
-
||
|
|
57
|
-
||
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
383
|
+
return this.sliceWindow(cachedWindow, target, rows);
|
|
358
384
|
let frontier = this.frontier;
|
|
359
|
-
if (frontier && (frontier
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
799
|
+
&& historyViewportRangeCompatible(viewport, this.latest)
|
|
617
800
|
&& this.windowCovers(viewport, target, rows)
|
|
618
|
-
&& viewport.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 (
|
|
659
|
-
|
|
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
|
|
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,
|