@floegence/floeterm-terminal-web 0.16.3 → 0.16.5
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 +9 -1
- package/dist/entries/semantic.d.ts +1 -1
- package/dist/entries/semantic.d.ts.map +1 -1
- package/dist/live/transport.d.ts.map +1 -1
- package/dist/live/transport.js +35 -4
- package/dist/live/transport.js.map +1 -1
- package/dist/semantic/HistoryViewportController.d.ts +29 -7
- package/dist/semantic/HistoryViewportController.d.ts.map +1 -1
- package/dist/semantic/HistoryViewportController.js +490 -146
- package/dist/semantic/HistoryViewportController.js.map +1 -1
- package/dist/semantic/RendererSurface.d.ts +18 -2
- package/dist/semantic/RendererSurface.d.ts.map +1 -1
- package/dist/semantic/RendererSurface.js +156 -6
- package/dist/semantic/RendererSurface.js.map +1 -1
- package/dist/semantic/presentation.d.ts +8 -0
- package/dist/semantic/presentation.d.ts.map +1 -1
- package/dist/semantic/presentation.js +54 -2
- package/dist/semantic/presentation.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,49 +1,23 @@
|
|
|
1
|
-
import { isStructuralSemanticHistoryError, SemanticHistoryError, validateHistoryViewport, } from './presentation.js';
|
|
1
|
+
import { isStructuralSemanticHistoryError, SemanticHistoryError, validateHistoryWindow, validateHistoryViewport, } from './presentation.js';
|
|
2
2
|
import { runSemanticHistoryRequest } from './historyRequestScheduler.js';
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
remove(owner, key) {
|
|
19
|
-
this.entries.delete(this.entryKey(owner, key));
|
|
20
|
-
}
|
|
21
|
-
clear(owner) {
|
|
22
|
-
for (const [key, entry] of this.entries) {
|
|
23
|
-
if (entry.owner === owner)
|
|
24
|
-
this.entries.delete(key);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
enforce() {
|
|
28
|
-
let total = [...this.entries.values()].reduce((sum, entry) => sum + entry.bytes, 0);
|
|
29
|
-
while (total > MAX_GLOBAL_HISTORY_CACHE_BYTES) {
|
|
30
|
-
const candidate = [...this.entries.values()]
|
|
31
|
-
.filter(entry => entry.evictable())
|
|
32
|
-
.sort((left, right) => Number(right.hidden()) - Number(left.hidden()) || left.touched - right.touched)[0];
|
|
33
|
-
if (!candidate)
|
|
34
|
-
return;
|
|
35
|
-
candidate.evict();
|
|
36
|
-
total = [...this.entries.values()].reduce((sum, entry) => sum + entry.bytes, 0);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
entryKey(owner, key) {
|
|
40
|
-
return `${owner}:${key}`;
|
|
41
|
-
}
|
|
42
|
-
}();
|
|
3
|
+
function emitSemanticDebugTrace(event) {
|
|
4
|
+
const target = globalThis;
|
|
5
|
+
target.__floetermSemanticTrace?.(event);
|
|
6
|
+
}
|
|
7
|
+
// History is owned by a single terminal session. Keep the budget local to the
|
|
8
|
+
// controller so one busy session cannot evict another session's history.
|
|
9
|
+
const MAX_SESSION_HISTORY_CACHE_BYTES = 4 * 1024 * 1024;
|
|
10
|
+
const HISTORY_WINDOW_CACHE_TARGET_BYTES = Math.floor(MAX_SESSION_HISTORY_CACHE_BYTES * 0.85);
|
|
11
|
+
const HISTORY_WINDOW_BASE_MULTIPLIER = 10;
|
|
12
|
+
const HISTORY_WINDOW_FAST_MULTIPLIER = 20;
|
|
13
|
+
const HISTORY_WINDOW_MAX_ROWS = 4000;
|
|
14
|
+
const HISTORY_SCROLL_BURST_GAP_MS = 250;
|
|
15
|
+
const HISTORY_SCROLL_BURST_DECAY_MS = 500;
|
|
16
|
+
const HISTORY_SCROLL_BURST_VIEWPORTS = 2;
|
|
17
|
+
const HISTORY_WINDOW_FORWARD_BIAS = 0.7;
|
|
43
18
|
export class HistoryViewportController {
|
|
44
19
|
constructor(options) {
|
|
45
20
|
this.options = options;
|
|
46
|
-
this.cacheOwner = globalHistoryCache.owner();
|
|
47
21
|
this.latest = null;
|
|
48
22
|
this.visible = null;
|
|
49
23
|
this.frontier = null;
|
|
@@ -51,23 +25,28 @@ export class HistoryViewportController {
|
|
|
51
25
|
this.cacheBytes = 0;
|
|
52
26
|
this.historyAnchor = null;
|
|
53
27
|
this.desiredOffset = null;
|
|
54
|
-
this.prefetchOffset = null;
|
|
55
28
|
this.lane = null;
|
|
56
29
|
this.epoch = 0;
|
|
57
30
|
this.transportGeneration = null;
|
|
58
31
|
this.touch = 0;
|
|
59
32
|
this.disposed = false;
|
|
60
|
-
this.viewVisible = true;
|
|
61
33
|
this.error = null;
|
|
62
34
|
this.wheelResidualRows = 0;
|
|
63
35
|
this.wheelFrame = null;
|
|
64
|
-
this.
|
|
36
|
+
this.handlingWheelFrame = false;
|
|
37
|
+
this.preferExactViewport = false;
|
|
38
|
+
this.scrollDirection = 0;
|
|
39
|
+
this.scrollBurstDirection = 0;
|
|
40
|
+
this.scrollBurstRows = 0;
|
|
41
|
+
this.scrollBurstAt = 0;
|
|
42
|
+
this.estimatedHistoryRowBytes = 0;
|
|
65
43
|
this.requestFrame = options.requestAnimationFrame
|
|
66
44
|
?? globalThis.requestAnimationFrame?.bind(globalThis)
|
|
67
45
|
?? (callback => globalThis.setTimeout(() => callback(performance.now()), 0));
|
|
68
46
|
this.cancelFrame = options.cancelAnimationFrame
|
|
69
47
|
?? globalThis.cancelAnimationFrame?.bind(globalThis)
|
|
70
48
|
?? (handle => globalThis.clearTimeout(handle));
|
|
49
|
+
this.now = options.now ?? (() => performance.now());
|
|
71
50
|
}
|
|
72
51
|
apply(presentation) {
|
|
73
52
|
if (this.disposed)
|
|
@@ -88,9 +67,8 @@ export class HistoryViewportController {
|
|
|
88
67
|
const rows = Math.trunc(deltaRows);
|
|
89
68
|
if (rows === 0)
|
|
90
69
|
return;
|
|
91
|
-
const current = this.
|
|
70
|
+
const current = this.currentIntentOffset();
|
|
92
71
|
const target = clamp(current + rows, 0, this.latest.frame.history.screenStartOffset);
|
|
93
|
-
this.lastDirection = rows < 0 ? -1 : 1;
|
|
94
72
|
this.showOffset(target);
|
|
95
73
|
}
|
|
96
74
|
handleWheel(delta, deltaMode) {
|
|
@@ -103,63 +81,109 @@ export class HistoryViewportController {
|
|
|
103
81
|
? delta * viewportRows
|
|
104
82
|
: delta / this.options.renderer.getCellMetrics().cellHeightCssPx;
|
|
105
83
|
this.wheelResidualRows += rows;
|
|
84
|
+
if (Math.abs(rows) >= viewportRows)
|
|
85
|
+
this.preferExactViewport = true;
|
|
106
86
|
if (this.wheelFrame !== null)
|
|
107
87
|
return;
|
|
108
88
|
this.wheelFrame = this.requestFrame(() => {
|
|
109
89
|
this.wheelFrame = null;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
90
|
+
this.handlingWheelFrame = true;
|
|
91
|
+
try {
|
|
92
|
+
const wholeRows = this.wheelResidualRows < 0
|
|
93
|
+
? Math.ceil(this.wheelResidualRows)
|
|
94
|
+
: Math.floor(this.wheelResidualRows);
|
|
95
|
+
this.wheelResidualRows -= wholeRows;
|
|
96
|
+
this.scrollByRows(wholeRows);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
this.handlingWheelFrame = false;
|
|
100
|
+
}
|
|
115
101
|
});
|
|
116
102
|
}
|
|
117
103
|
showStart() {
|
|
118
|
-
this.lastDirection = -1;
|
|
119
104
|
this.showOffset(0);
|
|
120
105
|
}
|
|
121
106
|
showLatest() {
|
|
122
107
|
if (!this.latest || this.disposed)
|
|
123
108
|
return;
|
|
109
|
+
// Invalidate any history request that was started before returning to the
|
|
110
|
+
// live frontier. Its response must never re-enter the visible surface.
|
|
111
|
+
this.epoch += 1;
|
|
124
112
|
this.desiredOffset = null;
|
|
125
|
-
this.
|
|
113
|
+
this.preferExactViewport = false;
|
|
126
114
|
this.visible = null;
|
|
127
|
-
this.frontier = null;
|
|
128
|
-
this.clearCache();
|
|
129
|
-
this.historyAnchor = null;
|
|
130
115
|
this.error = null;
|
|
131
|
-
this.
|
|
116
|
+
this.projectFrame(null);
|
|
117
|
+
this.evictCache();
|
|
132
118
|
this.emitState();
|
|
133
119
|
}
|
|
134
120
|
showOffset(offset) {
|
|
135
121
|
if (!this.latest || this.disposed || !Number.isFinite(offset))
|
|
136
122
|
return;
|
|
137
123
|
const target = clamp(Math.trunc(offset), 0, this.latest.frame.history.screenStartOffset);
|
|
124
|
+
const current = this.currentIntentOffset();
|
|
125
|
+
this.recordScrollIntent(target - current);
|
|
126
|
+
emitSemanticDebugTrace({ kind: 'history-cache-check', at: performance.now(), target,
|
|
127
|
+
frontierOffset: this.frontier?.offset ?? null, frontierRows: this.frontier?.rows ?? null,
|
|
128
|
+
cacheEntries: this.cache.size, cacheBytes: this.cacheBytes,
|
|
129
|
+
cacheExtraBytes: this.cacheExtraBytes() });
|
|
138
130
|
if (target === this.latest.frame.history.screenStartOffset) {
|
|
139
131
|
this.showLatest();
|
|
140
132
|
return;
|
|
141
133
|
}
|
|
142
134
|
const cached = this.findCachedViewport(target);
|
|
143
|
-
if (cached
|
|
135
|
+
if (cached) {
|
|
144
136
|
this.desiredOffset = null;
|
|
145
137
|
this.display(cached);
|
|
146
138
|
return;
|
|
147
139
|
}
|
|
140
|
+
// The active frontier is the hottest window. Keep it usable even if the
|
|
141
|
+
// bounded LRU has evicted its map entry between adjacent wheel frames.
|
|
142
|
+
const frontierWindow = this.frontier;
|
|
143
|
+
if (frontierWindow && this.isReusableWindow(frontierWindow, target, this.latest.geometry.rows)) {
|
|
144
|
+
this.desiredOffset = null;
|
|
145
|
+
this.display(sliceHistoryWindow(frontierWindow, target, this.latest.geometry.rows));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const window = this.findCachedWindow(target, this.latest.geometry.rows);
|
|
149
|
+
if (window) {
|
|
150
|
+
this.desiredOffset = null;
|
|
151
|
+
this.putCache(window);
|
|
152
|
+
this.display(sliceHistoryWindow(window, target, this.latest.geometry.rows));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
148
155
|
this.desiredOffset = target;
|
|
149
156
|
this.error = null;
|
|
150
157
|
this.startLane();
|
|
151
158
|
this.emitState();
|
|
152
159
|
}
|
|
153
|
-
|
|
154
|
-
if (this.
|
|
155
|
-
return;
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
+
currentIntentOffset() {
|
|
161
|
+
if (!this.latest)
|
|
162
|
+
return 0;
|
|
163
|
+
const liveOffset = this.latest.frame.history.screenStartOffset;
|
|
164
|
+
if (this.desiredOffset !== null)
|
|
165
|
+
return clamp(this.desiredOffset, 0, liveOffset);
|
|
166
|
+
if (!this.visible)
|
|
167
|
+
return liveOffset;
|
|
168
|
+
// A bounded runtime scrollback may evict old rows while an immutable
|
|
169
|
+
// history snapshot remains visible. Its old absolute offset can then sit
|
|
170
|
+
// beyond the new live frontier. Preserve the viewport's distance from the
|
|
171
|
+
// tail before applying the next user delta; clamping the stale absolute
|
|
172
|
+
// coordinate directly would incorrectly jump the user back to live.
|
|
173
|
+
if (this.visible.screenStartOffset > liveOffset) {
|
|
174
|
+
const distanceFromLive = this.visible.screenStartOffset - this.visible.offset;
|
|
175
|
+
return clamp(liveOffset - distanceFromLive, 0, liveOffset);
|
|
160
176
|
}
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
return clamp(this.visible.offset, 0, liveOffset);
|
|
178
|
+
}
|
|
179
|
+
setVisible(_visible) {
|
|
180
|
+
if (this.disposed)
|
|
181
|
+
return;
|
|
182
|
+
// Keep this session's bounded history cache warm while another workbench
|
|
183
|
+
// view is active. Every reuse still passes the live revision, content,
|
|
184
|
+
// geometry, and transport-generation checks; retaining the immutable
|
|
185
|
+
// window therefore removes the first-scroll cold RPC without permitting
|
|
186
|
+
// stale terminal content to be projected after returning to this view.
|
|
163
187
|
}
|
|
164
188
|
reset() {
|
|
165
189
|
if (this.disposed)
|
|
@@ -204,7 +228,6 @@ export class HistoryViewportController {
|
|
|
204
228
|
throw new SemanticHistoryError('snapshot_superseded', 'semantic history viewport does not match the live surface');
|
|
205
229
|
}
|
|
206
230
|
this.desiredOffset = null;
|
|
207
|
-
this.prefetchOffset = null;
|
|
208
231
|
this.frontier = null;
|
|
209
232
|
this.clearCache();
|
|
210
233
|
this.historyAnchor = null;
|
|
@@ -231,7 +254,6 @@ export class HistoryViewportController {
|
|
|
231
254
|
this.visible = null;
|
|
232
255
|
this.frontier = null;
|
|
233
256
|
this.desiredOffset = null;
|
|
234
|
-
this.prefetchOffset = null;
|
|
235
257
|
}
|
|
236
258
|
startLane() {
|
|
237
259
|
if (this.lane || this.disposed)
|
|
@@ -240,38 +262,56 @@ export class HistoryViewportController {
|
|
|
240
262
|
this.lane = this.runLane(epoch).finally(() => {
|
|
241
263
|
this.lane = null;
|
|
242
264
|
this.emitState();
|
|
243
|
-
if (!this.disposed &&
|
|
265
|
+
if (!this.disposed && this.desiredOffset !== null)
|
|
244
266
|
this.startLane();
|
|
245
267
|
});
|
|
246
268
|
}
|
|
247
269
|
async runLane(epoch) {
|
|
248
|
-
let activeWasPrefetch = false;
|
|
249
270
|
try {
|
|
250
271
|
while (!this.disposed && this.epoch === epoch) {
|
|
251
|
-
const target = this.desiredOffset
|
|
252
|
-
const isPrefetch = this.desiredOffset === null && this.prefetchOffset !== null;
|
|
253
|
-
activeWasPrefetch = isPrefetch;
|
|
272
|
+
const target = this.desiredOffset;
|
|
254
273
|
if (target === null)
|
|
255
274
|
return;
|
|
256
|
-
if (isPrefetch)
|
|
257
|
-
this.prefetchOffset = null;
|
|
258
275
|
const cached = this.findCachedViewport(target);
|
|
259
|
-
if (cached
|
|
260
|
-
|
|
276
|
+
if (cached) {
|
|
277
|
+
const isCurrentTarget = this.desiredOffset === target;
|
|
278
|
+
const desiredOffset = this.desiredOffset;
|
|
279
|
+
if (isCurrentTarget) {
|
|
261
280
|
this.desiredOffset = null;
|
|
281
|
+
}
|
|
282
|
+
// A completed, validated viewport is still useful while a newer
|
|
283
|
+
// wheel target is in flight, but only while it moves toward that
|
|
284
|
+
// target. Never project an older response after a direction change.
|
|
285
|
+
if (isCurrentTarget || this.shouldDisplayIntermediate(cached.offset, desiredOffset)) {
|
|
262
286
|
this.display(cached);
|
|
263
287
|
}
|
|
264
288
|
continue;
|
|
265
289
|
}
|
|
290
|
+
const cachedWindow = this.findCachedWindow(target, this.latest?.geometry.rows ?? 0);
|
|
291
|
+
if (cachedWindow && this.latest) {
|
|
292
|
+
const isCurrentTarget = this.desiredOffset === target;
|
|
293
|
+
const desiredOffset = this.desiredOffset;
|
|
294
|
+
if (isCurrentTarget) {
|
|
295
|
+
this.desiredOffset = null;
|
|
296
|
+
}
|
|
297
|
+
this.putCache(cachedWindow);
|
|
298
|
+
const viewport = sliceHistoryWindow(cachedWindow, target, this.latest.geometry.rows);
|
|
299
|
+
if (isCurrentTarget || this.shouldDisplayIntermediate(viewport.offset, desiredOffset)) {
|
|
300
|
+
this.display(viewport);
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
266
304
|
const viewport = await this.fetchOffset(target);
|
|
267
305
|
if (this.disposed || this.epoch !== epoch)
|
|
268
306
|
return;
|
|
269
|
-
this.frontier = viewport;
|
|
270
307
|
this.cacheViewport(viewport);
|
|
271
|
-
|
|
308
|
+
const isCurrentTarget = this.desiredOffset === target;
|
|
309
|
+
const desiredOffset = this.desiredOffset;
|
|
310
|
+
if (isCurrentTarget) {
|
|
272
311
|
this.desiredOffset = null;
|
|
312
|
+
}
|
|
313
|
+
if (isCurrentTarget || this.shouldDisplayIntermediate(viewport.offset, desiredOffset)) {
|
|
273
314
|
this.display(viewport);
|
|
274
|
-
this.schedulePrefetch(viewport);
|
|
275
315
|
}
|
|
276
316
|
}
|
|
277
317
|
}
|
|
@@ -281,61 +321,231 @@ export class HistoryViewportController {
|
|
|
281
321
|
this.error = cause instanceof Error ? cause : new Error(String(cause));
|
|
282
322
|
this.frontier = null;
|
|
283
323
|
this.desiredOffset = null;
|
|
284
|
-
this.
|
|
285
|
-
if (activeWasPrefetch)
|
|
286
|
-
this.error = null;
|
|
287
|
-
else if (isStructuralSemanticHistoryError(this.error))
|
|
324
|
+
if (isStructuralSemanticHistoryError(this.error))
|
|
288
325
|
this.resetHistory(true, this.error);
|
|
289
326
|
}
|
|
290
327
|
}
|
|
291
328
|
async fetchOffset(target) {
|
|
329
|
+
try {
|
|
330
|
+
return await this.fetchOffsetAttempt(target);
|
|
331
|
+
}
|
|
332
|
+
catch (cause) {
|
|
333
|
+
if (!isRecoverableHistoryLineageError(cause))
|
|
334
|
+
throw cause;
|
|
335
|
+
this.frontier = null;
|
|
336
|
+
this.clearCache();
|
|
337
|
+
this.historyAnchor = null;
|
|
338
|
+
return await this.fetchOffsetAttempt(this.desiredOffset ?? target);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async fetchOffsetAttempt(target) {
|
|
292
342
|
if (!this.latest)
|
|
293
343
|
throw new Error('terminal history requires a live Presentation');
|
|
294
344
|
const rows = this.latest.geometry.rows;
|
|
345
|
+
const totalRows = this.latest.frame.history.totalRows;
|
|
346
|
+
const revision = this.latest.frame.history.revision;
|
|
347
|
+
const preferExactViewport = this.preferExactViewport;
|
|
348
|
+
// A large wheel delta needs one exact target request, but must not poison
|
|
349
|
+
// the rest of the session. Subsequent small deltas should use a reusable
|
|
350
|
+
// history window again.
|
|
351
|
+
this.preferExactViewport = false;
|
|
352
|
+
const windowRows = preferExactViewport ? rows : this.adaptiveWindowRows(rows, totalRows);
|
|
353
|
+
if (windowRows <= rows)
|
|
354
|
+
return this.fetchExactFromBoundary(target, rows);
|
|
355
|
+
const cachedWindow = this.findCachedWindow(target, rows);
|
|
356
|
+
if (cachedWindow)
|
|
357
|
+
return sliceHistoryWindow(cachedWindow, target, rows);
|
|
295
358
|
let frontier = this.frontier;
|
|
296
|
-
if (
|
|
297
|
-
frontier =
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
359
|
+
if (frontier && (frontier.revision !== revision || frontier.totalRows !== totalRows)) {
|
|
360
|
+
this.frontier = null;
|
|
361
|
+
frontier = null;
|
|
362
|
+
}
|
|
363
|
+
if (!frontier || !this.isReusableWindow(frontier, target, rows)) {
|
|
364
|
+
frontier = await this.fetchWindow(target, rows, windowRows);
|
|
365
|
+
}
|
|
366
|
+
if (frontier.window !== true)
|
|
367
|
+
return frontier;
|
|
368
|
+
return sliceHistoryWindow(frontier, target, rows);
|
|
369
|
+
}
|
|
370
|
+
async fetchExactFromBoundary(target, rows) {
|
|
371
|
+
// A boundary request already supports exact targeting in one RPC and
|
|
372
|
+
// atomically establishes its own native lineage. Reusing an old exact
|
|
373
|
+
// frontier offers no round-trip saving, but it opens a race where live
|
|
374
|
+
// output evicts the anchor after the client checks its revision and before
|
|
375
|
+
// the runtime processes the anchored request.
|
|
376
|
+
const frontier = validateHistoryViewport(await this.fetchBoundary(target === 0 ? 'start' : 'end', target, rows));
|
|
377
|
+
this.assertTransportGeneration(frontier);
|
|
378
|
+
this.acceptLineage(frontier, true);
|
|
379
|
+
this.frontier = frontier;
|
|
380
|
+
return this.fetchExactOffset(frontier, target, rows);
|
|
381
|
+
}
|
|
382
|
+
async fetchExactOffset(frontier, target, rows) {
|
|
383
|
+
if (frontier.offset === target)
|
|
384
|
+
return frontier;
|
|
385
|
+
const direction = target < frontier.offset ? 'backward' : 'forward';
|
|
386
|
+
const next = validateHistoryViewport(await runSemanticHistoryRequest(() => this.options.request({
|
|
387
|
+
lane: 'viewport',
|
|
388
|
+
direction,
|
|
389
|
+
anchor: frontier.anchor,
|
|
390
|
+
snapshotId: frontier.snapshotId,
|
|
391
|
+
offset: frontier.offset,
|
|
392
|
+
targetOffset: target,
|
|
393
|
+
viewportRows: rows,
|
|
394
|
+
})));
|
|
395
|
+
this.assertTransportGeneration(next);
|
|
396
|
+
this.acceptLineage(next);
|
|
397
|
+
this.frontier = next;
|
|
398
|
+
return next;
|
|
399
|
+
}
|
|
400
|
+
async fetchWindow(target, rows, windowRows) {
|
|
401
|
+
const windowStart = this.windowStart(target, rows, windowRows, this.scrollDirection);
|
|
402
|
+
let current = this.frontier;
|
|
403
|
+
if (current && this.isCompatibleWindow(current)
|
|
404
|
+
&& current.offset === windowStart && !this.isReusableWindow(current, target, rows)) {
|
|
405
|
+
current = null;
|
|
406
|
+
}
|
|
407
|
+
if (!current || !this.isCompatibleWindow(current)) {
|
|
408
|
+
// A start/end boundary request always replaces the server-side view for
|
|
409
|
+
// this lane and therefore always establishes a new anchor. This is also
|
|
410
|
+
// how an exact viewport is upgraded to a reusable window. Clear every
|
|
411
|
+
// cache entry from the old lineage before accepting that replacement.
|
|
412
|
+
const allowLineageReplacement = true;
|
|
413
|
+
const response = await this.fetchBoundary(target === 0 ? 'start' : 'end', windowStart, windowRows, rows);
|
|
414
|
+
// Keep the old exact-viewport contract as a fallback for older runtimes
|
|
415
|
+
// while the fast-debug runtime is being rolled out.
|
|
416
|
+
if (response.window !== true) {
|
|
417
|
+
current = validateHistoryViewport(response);
|
|
418
|
+
this.assertTransportGeneration(current);
|
|
419
|
+
this.acceptLineage(current, allowLineageReplacement);
|
|
420
|
+
this.frontier = current;
|
|
421
|
+
return this.fetchExactOffset(current, this.desiredOffset ?? target, rows);
|
|
422
|
+
}
|
|
423
|
+
current = validateHistoryWindow(response);
|
|
424
|
+
this.assertTransportGeneration(current);
|
|
425
|
+
this.acceptLineage(current, allowLineageReplacement);
|
|
426
|
+
this.cacheWindow(current);
|
|
427
|
+
// This boundary response is already one validated, immutable snapshot
|
|
428
|
+
// captured for the requested target. Live output may advance `latest`
|
|
429
|
+
// while the RPC and payload chunks are in flight, so revision equality
|
|
430
|
+
// is no longer a valid reason to navigate away from this fresh window.
|
|
431
|
+
// Doing so used to synthesize a zero-distance `forward` request when the
|
|
432
|
+
// response started exactly at windowStart; the runtime correctly rejected
|
|
433
|
+
// that request as an invalid anchor and continuous agent output could make
|
|
434
|
+
// the single recovery attempt repeat the same race.
|
|
435
|
+
this.frontier = current;
|
|
436
|
+
if (this.windowCovers(current, target, rows))
|
|
437
|
+
return current;
|
|
438
|
+
// Bounded native scrollback can legitimately move past the requested
|
|
439
|
+
// absolute offset while the boundary capture is running. In that case,
|
|
440
|
+
// project the nearest viewport that is actually present in this atomic
|
|
441
|
+
// snapshot instead of issuing a directionally invalid anchored request.
|
|
442
|
+
const nearest = clamp(target, current.offset, current.offset + current.rows - rows);
|
|
443
|
+
return sliceHistoryWindow(current, nearest, rows);
|
|
304
444
|
}
|
|
305
|
-
let current = frontier;
|
|
306
445
|
if (!current)
|
|
307
|
-
throw new Error('terminal history
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
return current;
|
|
311
|
-
const direction = intended < current.offset ? 'backward' : 'forward';
|
|
312
|
-
const scrollDeltaRows = Math.abs(current.offset - intended);
|
|
313
|
-
if (scrollDeltaRows <= 0)
|
|
446
|
+
throw new Error('terminal history window is unavailable');
|
|
447
|
+
if (this.isReusableWindow(current, target, rows)) {
|
|
448
|
+
this.frontier = current;
|
|
314
449
|
return current;
|
|
450
|
+
}
|
|
451
|
+
const direction = windowStart < current.offset ? 'backward' : 'forward';
|
|
315
452
|
const offset = current.offset;
|
|
316
|
-
const next =
|
|
453
|
+
const next = validateHistoryWindow(await runSemanticHistoryRequest(() => this.options.request({
|
|
317
454
|
lane: 'viewport',
|
|
318
455
|
direction,
|
|
319
456
|
anchor: current.anchor,
|
|
320
457
|
snapshotId: current.snapshotId,
|
|
321
458
|
offset,
|
|
322
|
-
targetOffset:
|
|
323
|
-
viewportRows:
|
|
459
|
+
targetOffset: windowStart,
|
|
460
|
+
viewportRows: windowRows,
|
|
461
|
+
windowRows,
|
|
324
462
|
})));
|
|
325
463
|
this.assertTransportGeneration(next);
|
|
326
464
|
this.acceptLineage(next);
|
|
327
465
|
if (next.offset === offset || (direction === 'backward' && next.offset > offset)
|
|
328
466
|
|| (direction === 'forward' && next.offset < offset)) {
|
|
329
|
-
throw new SemanticHistoryError('malformed_snapshot', 'semantic history
|
|
467
|
+
throw new SemanticHistoryError('malformed_snapshot', 'semantic history window did not advance toward its target');
|
|
330
468
|
}
|
|
331
|
-
|
|
332
|
-
|
|
469
|
+
this.frontier = next;
|
|
470
|
+
this.cacheWindow(next);
|
|
471
|
+
return next;
|
|
472
|
+
}
|
|
473
|
+
async fetchBoundary(direction, target, viewportRows, legacyViewportRows = viewportRows) {
|
|
474
|
+
try {
|
|
475
|
+
return await runSemanticHistoryRequest(() => this.options.request({
|
|
476
|
+
lane: 'viewport',
|
|
477
|
+
direction,
|
|
478
|
+
targetOffset: target,
|
|
479
|
+
viewportRows,
|
|
480
|
+
...(viewportRows === legacyViewportRows ? {} : { windowRows: viewportRows }),
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
catch (cause) {
|
|
484
|
+
if (!isLegacyBoundaryTargetRejection(cause))
|
|
485
|
+
throw cause;
|
|
486
|
+
return await runSemanticHistoryRequest(() => this.options.request({
|
|
487
|
+
lane: 'viewport',
|
|
488
|
+
direction,
|
|
489
|
+
viewportRows: legacyViewportRows,
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
windowStart(target, rows, windowRows, direction) {
|
|
494
|
+
if (!this.latest)
|
|
495
|
+
return target;
|
|
496
|
+
const totalRows = this.latest.frame.history.totalRows;
|
|
497
|
+
const maximum = Math.max(0, totalRows - windowRows);
|
|
498
|
+
const bufferRows = Math.max(0, windowRows - rows);
|
|
499
|
+
const rowsBeforeTarget = direction < 0
|
|
500
|
+
? Math.round(bufferRows * HISTORY_WINDOW_FORWARD_BIAS)
|
|
501
|
+
: direction > 0
|
|
502
|
+
? Math.round(bufferRows * (1 - HISTORY_WINDOW_FORWARD_BIAS))
|
|
503
|
+
: Math.round(bufferRows / 2);
|
|
504
|
+
return clamp(target - rowsBeforeTarget, 0, maximum);
|
|
505
|
+
}
|
|
506
|
+
adaptiveWindowRows(rows, totalRows) {
|
|
507
|
+
if (!this.latest)
|
|
508
|
+
return rows;
|
|
509
|
+
const now = this.now();
|
|
510
|
+
const fast = this.scrollBurstDirection !== 0
|
|
511
|
+
&& now - this.scrollBurstAt <= HISTORY_SCROLL_BURST_DECAY_MS
|
|
512
|
+
&& this.scrollBurstRows >= rows * HISTORY_SCROLL_BURST_VIEWPORTS;
|
|
513
|
+
const multiplier = fast ? HISTORY_WINDOW_FAST_MULTIPLIER : HISTORY_WINDOW_BASE_MULTIPLIER;
|
|
514
|
+
const liveRowBytes = estimateFrameBytes(this.latest.frame) / Math.max(1, this.latest.frame.height);
|
|
515
|
+
const rowBytes = Math.max(256, this.estimatedHistoryRowBytes || liveRowBytes);
|
|
516
|
+
const cacheLimitedRows = Math.max(rows, Math.floor(HISTORY_WINDOW_CACHE_TARGET_BYTES / rowBytes));
|
|
517
|
+
const windowRows = Math.max(rows, Math.min(totalRows, HISTORY_WINDOW_MAX_ROWS, rows * multiplier, cacheLimitedRows));
|
|
518
|
+
emitSemanticDebugTrace({
|
|
519
|
+
kind: 'history-window-plan', at: now, direction: this.scrollDirection,
|
|
520
|
+
multiplier, viewportRows: rows, windowRows, cacheLimitedRows,
|
|
521
|
+
estimatedHistoryRowBytes: rowBytes, scrollBurstRows: this.scrollBurstRows,
|
|
522
|
+
});
|
|
523
|
+
return windowRows;
|
|
524
|
+
}
|
|
525
|
+
recordScrollIntent(deltaRows) {
|
|
526
|
+
if (deltaRows === 0)
|
|
527
|
+
return;
|
|
528
|
+
const direction = Math.sign(deltaRows);
|
|
529
|
+
const now = this.now();
|
|
530
|
+
if (direction === this.scrollBurstDirection
|
|
531
|
+
&& now - this.scrollBurstAt <= HISTORY_SCROLL_BURST_GAP_MS) {
|
|
532
|
+
this.scrollBurstRows += Math.abs(deltaRows);
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
this.scrollBurstDirection = direction;
|
|
536
|
+
this.scrollBurstRows = Math.abs(deltaRows);
|
|
537
|
+
}
|
|
538
|
+
this.scrollDirection = direction;
|
|
539
|
+
this.scrollBurstAt = now;
|
|
540
|
+
}
|
|
541
|
+
windowCovers(window, target, rows) {
|
|
542
|
+
return target >= window.offset && target + rows <= window.offset + window.rows;
|
|
333
543
|
}
|
|
334
544
|
display(viewport) {
|
|
335
545
|
this.visible = viewport;
|
|
336
546
|
this.error = null;
|
|
337
547
|
this.putCache(viewport);
|
|
338
|
-
this.
|
|
548
|
+
this.projectFrame(viewport.frame);
|
|
339
549
|
this.evictCache();
|
|
340
550
|
this.emitState();
|
|
341
551
|
}
|
|
@@ -343,33 +553,34 @@ export class HistoryViewportController {
|
|
|
343
553
|
this.putCache(viewport);
|
|
344
554
|
this.evictCache();
|
|
345
555
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
556
|
+
projectFrame(frame) {
|
|
557
|
+
if (this.handlingWheelFrame && this.options.renderer.projectInCurrentAnimationFrame) {
|
|
558
|
+
this.options.renderer.projectInCurrentAnimationFrame(frame);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this.options.renderer.project(frame);
|
|
562
|
+
}
|
|
563
|
+
evictCache() {
|
|
349
564
|
for (;;) {
|
|
350
565
|
const candidates = [...this.cache.entries()]
|
|
351
566
|
.filter(([, item]) => item.viewport !== this.visible)
|
|
352
567
|
.sort((left, right) => left[1].touched - right[1].touched);
|
|
353
568
|
const extraBytes = candidates.reduce((sum, candidate) => sum + candidate[1].bytes, 0);
|
|
354
|
-
|
|
355
|
-
if (!dropHiddenExtras && extraBytes <= MAX_HISTORY_CACHE_BYTES && extraCells <= maxExtraCells)
|
|
569
|
+
if (extraBytes <= MAX_SESSION_HISTORY_CACHE_BYTES)
|
|
356
570
|
break;
|
|
357
571
|
const oldest = candidates[0];
|
|
358
572
|
if (!oldest)
|
|
359
573
|
break;
|
|
360
574
|
this.removeCacheEntry(oldest[0]);
|
|
361
575
|
}
|
|
576
|
+
emitSemanticDebugTrace({ kind: 'history-cache-state', at: performance.now(),
|
|
577
|
+
cacheEntries: this.cache.size, cacheBytes: this.cacheBytes,
|
|
578
|
+
cacheExtraBytes: this.cacheExtraBytes() });
|
|
362
579
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const target = clamp(viewport.offset + this.lastDirection * rows, 0, viewport.screenStartOffset);
|
|
368
|
-
if (target === viewport.offset || this.findCachedViewport(target))
|
|
369
|
-
return;
|
|
370
|
-
this.prefetchOffset = target;
|
|
371
|
-
if (!this.lane)
|
|
372
|
-
this.startLane();
|
|
580
|
+
cacheExtraBytes() {
|
|
581
|
+
return [...this.cache.values()]
|
|
582
|
+
.filter(item => item.viewport !== this.visible)
|
|
583
|
+
.reduce((sum, item) => sum + item.bytes, 0);
|
|
373
584
|
}
|
|
374
585
|
isCompatible(viewport) {
|
|
375
586
|
return Boolean(this.latest
|
|
@@ -379,7 +590,32 @@ export class HistoryViewportController {
|
|
|
379
590
|
&& (viewport.lane ?? 'viewport') === 'viewport'
|
|
380
591
|
&& (this.historyAnchor === null || viewport.anchor === this.historyAnchor)
|
|
381
592
|
&& viewport.cols === this.latest.geometry.cols
|
|
382
|
-
&& viewport.rows === this.latest.geometry.rows
|
|
593
|
+
&& viewport.rows === this.latest.geometry.rows
|
|
594
|
+
&& viewport.window !== true);
|
|
595
|
+
}
|
|
596
|
+
isReusableViewport(viewport) {
|
|
597
|
+
return Boolean(this.latest
|
|
598
|
+
&& this.isCompatible(viewport)
|
|
599
|
+
&& viewport.totalRows === this.latest.frame.history.totalRows
|
|
600
|
+
&& viewport.revision === this.latest.frame.history.revision);
|
|
601
|
+
}
|
|
602
|
+
isCompatibleWindow(viewport) {
|
|
603
|
+
return Boolean(this.latest
|
|
604
|
+
&& viewport.window === true
|
|
605
|
+
&& viewport.contentEpoch === (this.latest.state.contentEpoch ?? 0)
|
|
606
|
+
&& viewport.geometryGeneration === this.latest.geometry.generation
|
|
607
|
+
&& (this.transportGeneration === null || viewport.transportGeneration === this.transportGeneration)
|
|
608
|
+
&& (viewport.lane ?? 'viewport') === 'viewport'
|
|
609
|
+
&& (this.historyAnchor === null || viewport.anchor === this.historyAnchor)
|
|
610
|
+
&& viewport.cols === this.latest.geometry.cols
|
|
611
|
+
&& viewport.rows >= this.latest.geometry.rows);
|
|
612
|
+
}
|
|
613
|
+
isReusableWindow(viewport, target, rows) {
|
|
614
|
+
return Boolean(this.latest
|
|
615
|
+
&& this.isCompatibleWindow(viewport)
|
|
616
|
+
&& viewport.totalRows === this.latest.frame.history.totalRows
|
|
617
|
+
&& this.windowCovers(viewport, target, rows)
|
|
618
|
+
&& viewport.revision === this.latest.frame.history.revision);
|
|
383
619
|
}
|
|
384
620
|
assertTransportGeneration(viewport) {
|
|
385
621
|
if (this.transportGeneration === null) {
|
|
@@ -396,20 +632,57 @@ export class HistoryViewportController {
|
|
|
396
632
|
this.frontier = null;
|
|
397
633
|
this.visible = null;
|
|
398
634
|
this.desiredOffset = null;
|
|
399
|
-
this.
|
|
635
|
+
this.preferExactViewport = false;
|
|
636
|
+
this.scrollDirection = 0;
|
|
637
|
+
this.scrollBurstDirection = 0;
|
|
638
|
+
this.scrollBurstRows = 0;
|
|
639
|
+
this.scrollBurstAt = 0;
|
|
640
|
+
this.estimatedHistoryRowBytes = 0;
|
|
400
641
|
this.historyAnchor = null;
|
|
401
642
|
this.error = error;
|
|
402
643
|
if (projectLatest)
|
|
403
644
|
this.options.renderer.project(null);
|
|
404
645
|
this.emitState();
|
|
405
646
|
}
|
|
647
|
+
shouldDisplayIntermediate(target, desiredOffset) {
|
|
648
|
+
if (desiredOffset === null || this.visible === null)
|
|
649
|
+
return false;
|
|
650
|
+
const currentOffset = this.visible.offset;
|
|
651
|
+
const minimum = Math.min(currentOffset, desiredOffset);
|
|
652
|
+
const maximum = Math.max(currentOffset, desiredOffset);
|
|
653
|
+
return target >= minimum && target <= maximum
|
|
654
|
+
&& Math.abs(desiredOffset - target) < Math.abs(desiredOffset - currentOffset);
|
|
655
|
+
}
|
|
406
656
|
findCachedViewport(offset) {
|
|
407
657
|
for (const item of this.cache.values()) {
|
|
408
|
-
if (item.viewport.offset === offset && this.
|
|
658
|
+
if (item.viewport.offset === offset && this.isReusableViewport(item.viewport))
|
|
409
659
|
return item.viewport;
|
|
410
660
|
}
|
|
411
661
|
return null;
|
|
412
662
|
}
|
|
663
|
+
findCachedWindow(offset, rows) {
|
|
664
|
+
if (rows <= 0)
|
|
665
|
+
return null;
|
|
666
|
+
let candidate = null;
|
|
667
|
+
for (const item of this.cache.values()) {
|
|
668
|
+
if (this.isReusableWindow(item.viewport, offset, rows)
|
|
669
|
+
&& (!candidate || item.touched > candidate.touched))
|
|
670
|
+
candidate = item;
|
|
671
|
+
}
|
|
672
|
+
return candidate?.viewport ?? null;
|
|
673
|
+
}
|
|
674
|
+
cacheWindow(viewport) {
|
|
675
|
+
if (viewport.window !== true)
|
|
676
|
+
return;
|
|
677
|
+
const observedRowBytes = estimateViewportBytes(viewport) / Math.max(1, viewport.rows);
|
|
678
|
+
this.estimatedHistoryRowBytes = this.estimatedHistoryRowBytes === 0
|
|
679
|
+
? observedRowBytes
|
|
680
|
+
: observedRowBytes > this.estimatedHistoryRowBytes
|
|
681
|
+
? observedRowBytes
|
|
682
|
+
: this.estimatedHistoryRowBytes * 0.75 + observedRowBytes * 0.25;
|
|
683
|
+
this.putCache(viewport);
|
|
684
|
+
this.evictCache();
|
|
685
|
+
}
|
|
413
686
|
putCache(viewport) {
|
|
414
687
|
if ((viewport.lane ?? 'viewport') !== 'viewport') {
|
|
415
688
|
throw new SemanticHistoryError('malformed_snapshot', 'semantic history viewport belongs to another lane');
|
|
@@ -419,33 +692,26 @@ export class HistoryViewportController {
|
|
|
419
692
|
}
|
|
420
693
|
if (this.historyAnchor === null)
|
|
421
694
|
this.historyAnchor = viewport.anchor;
|
|
422
|
-
|
|
695
|
+
// Keep a sliced viewport separate from its source window. Reusing the same
|
|
696
|
+
// key would replace the reusable window as soon as it is displayed once.
|
|
697
|
+
const key = `${viewport.snapshotId}:${viewport.offset}:${viewport.window === true ? 'window' : 'viewport'}`;
|
|
698
|
+
const previous = this.cache.get(key);
|
|
699
|
+
this.touch += 1;
|
|
700
|
+
if (previous?.viewport === viewport) {
|
|
701
|
+
this.cache.set(key, { ...previous, touched: this.touch });
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
423
704
|
const bytes = estimateViewportBytes(viewport);
|
|
424
705
|
const cells = viewport.cols * viewport.rows;
|
|
425
|
-
const previous = this.cache.get(key);
|
|
426
706
|
if (previous)
|
|
427
707
|
this.cacheBytes -= previous.bytes;
|
|
428
|
-
this.touch += 1;
|
|
429
708
|
const item = { viewport, touched: this.touch, bytes, cells };
|
|
430
709
|
this.cache.set(key, item);
|
|
431
710
|
this.cacheBytes += bytes;
|
|
432
|
-
globalHistoryCache.put({
|
|
433
|
-
owner: this.cacheOwner,
|
|
434
|
-
key,
|
|
435
|
-
bytes,
|
|
436
|
-
touched: item.touched,
|
|
437
|
-
hidden: () => !this.viewVisible,
|
|
438
|
-
evictable: () => {
|
|
439
|
-
const current = this.cache.get(key)?.viewport;
|
|
440
|
-
return Boolean(current && current !== this.visible);
|
|
441
|
-
},
|
|
442
|
-
evict: () => this.removeCacheEntry(key),
|
|
443
|
-
});
|
|
444
711
|
}
|
|
445
712
|
clearCache() {
|
|
446
713
|
this.cache.clear();
|
|
447
714
|
this.cacheBytes = 0;
|
|
448
|
-
globalHistoryCache.clear(this.cacheOwner);
|
|
449
715
|
}
|
|
450
716
|
removeCacheEntry(key) {
|
|
451
717
|
const item = this.cache.get(key);
|
|
@@ -455,7 +721,6 @@ export class HistoryViewportController {
|
|
|
455
721
|
this.cacheBytes -= item.bytes;
|
|
456
722
|
if (this.frontier === item.viewport)
|
|
457
723
|
this.frontier = null;
|
|
458
|
-
globalHistoryCache.remove(this.cacheOwner, key);
|
|
459
724
|
}
|
|
460
725
|
acceptLineage(viewport, allowReplacement = false) {
|
|
461
726
|
if ((viewport.lane ?? 'viewport') !== 'viewport') {
|
|
@@ -476,12 +741,91 @@ export class HistoryViewportController {
|
|
|
476
741
|
function clamp(value, minimum, maximum) {
|
|
477
742
|
return Math.max(minimum, Math.min(maximum, value));
|
|
478
743
|
}
|
|
744
|
+
function isLegacyBoundaryTargetRejection(cause) {
|
|
745
|
+
if (!(cause instanceof Error))
|
|
746
|
+
return false;
|
|
747
|
+
const code = Number(cause.code);
|
|
748
|
+
return code === 400 && cause.message.toLowerCase().includes('failed to read semantic history');
|
|
749
|
+
}
|
|
750
|
+
function isRecoverableHistoryLineageError(cause) {
|
|
751
|
+
if (!(cause instanceof SemanticHistoryError)
|
|
752
|
+
|| (cause.kind !== 'anchor_invalid' && cause.kind !== 'snapshot_superseded'))
|
|
753
|
+
return false;
|
|
754
|
+
// The RPC adapter normally preserves the 409/412 code as `cause`, but a
|
|
755
|
+
// transport implementation is allowed to surface the typed semantic error
|
|
756
|
+
// directly. The semantic kind is the authoritative contract here; making
|
|
757
|
+
// recovery depend on an adapter-specific numeric code exposed the user to
|
|
758
|
+
// stale-anchor errors whenever that code was wrapped or omitted.
|
|
759
|
+
const rpcCause = cause.cause;
|
|
760
|
+
// A local `snapshot_superseded` means two validated responses disagreed on
|
|
761
|
+
// lineage and must remain a hard failure. Only an unwrapped anchor error is
|
|
762
|
+
// safe to recover from without an adapter code: native history can expire
|
|
763
|
+
// an anchor while the transport is still healthy.
|
|
764
|
+
if (!rpcCause)
|
|
765
|
+
return cause.kind === 'anchor_invalid';
|
|
766
|
+
const code = rpcCause instanceof Error
|
|
767
|
+
? Number(rpcCause.code)
|
|
768
|
+
: Number.NaN;
|
|
769
|
+
return code === 409 || code === 412 || rpcCause instanceof SemanticHistoryError;
|
|
770
|
+
}
|
|
479
771
|
function estimateViewportBytes(viewport) {
|
|
480
|
-
|
|
481
|
-
|
|
772
|
+
return 256 + estimateFrameBytes(viewport.frame);
|
|
773
|
+
}
|
|
774
|
+
function estimateFrameBytes(frame) {
|
|
775
|
+
let bytes = 0;
|
|
776
|
+
for (const row of frame.rows) {
|
|
482
777
|
for (const cell of row.cells)
|
|
483
778
|
bytes += 16 + cell.text.length * 2 + (cell.hyperlink?.length ?? 0) * 2;
|
|
484
779
|
}
|
|
485
780
|
return bytes;
|
|
486
781
|
}
|
|
782
|
+
function sliceHistoryWindow(window, offset, rows) {
|
|
783
|
+
if (window.window !== true || offset < window.offset || offset + rows > window.offset + window.rows) {
|
|
784
|
+
throw new SemanticHistoryError('malformed_snapshot', 'semantic history window does not cover its target viewport');
|
|
785
|
+
}
|
|
786
|
+
const rowStart = offset - window.offset;
|
|
787
|
+
const frame = window.frame;
|
|
788
|
+
const end = rowStart + rows;
|
|
789
|
+
const screenStartOffset = window.totalRows - rows;
|
|
790
|
+
const cursorVisible = frame.cursor.visible && frame.cursor.y >= rowStart && frame.cursor.y < end;
|
|
791
|
+
const placements = frame.graphics.placements
|
|
792
|
+
.filter(placement => !placement.visible || (placement.viewportRow + placement.gridRows > rowStart && placement.viewportRow < end))
|
|
793
|
+
.map(placement => ({ ...placement, viewportRow: placement.viewportRow - rowStart }));
|
|
794
|
+
const slicedFrame = {
|
|
795
|
+
...frame,
|
|
796
|
+
height: rows,
|
|
797
|
+
rows: frame.rows.slice(rowStart, end),
|
|
798
|
+
cursor: {
|
|
799
|
+
...frame.cursor,
|
|
800
|
+
y: cursorVisible ? frame.cursor.y - rowStart : 0,
|
|
801
|
+
visible: cursorVisible,
|
|
802
|
+
},
|
|
803
|
+
history: {
|
|
804
|
+
revision: window.revision,
|
|
805
|
+
totalRows: window.totalRows,
|
|
806
|
+
screenStartOffset: window.totalRows - rows,
|
|
807
|
+
},
|
|
808
|
+
graphics: { ...frame.graphics, placements },
|
|
809
|
+
};
|
|
810
|
+
return validateHistoryViewport({
|
|
811
|
+
snapshotId: `${window.snapshotId}:${offset}`,
|
|
812
|
+
lane: window.lane,
|
|
813
|
+
revision: window.revision,
|
|
814
|
+
transportGeneration: window.transportGeneration,
|
|
815
|
+
contentEpoch: window.contentEpoch,
|
|
816
|
+
geometryGeneration: window.geometryGeneration,
|
|
817
|
+
cols: window.cols,
|
|
818
|
+
rows,
|
|
819
|
+
anchor: window.anchor,
|
|
820
|
+
firstAvailable: window.firstAvailable,
|
|
821
|
+
lastAvailable: window.lastAvailable,
|
|
822
|
+
screenStart: window.screenStart,
|
|
823
|
+
offset,
|
|
824
|
+
totalRows: window.totalRows,
|
|
825
|
+
screenStartOffset,
|
|
826
|
+
hasPrevious: offset > 0,
|
|
827
|
+
hasNext: offset < screenStartOffset,
|
|
828
|
+
frame: slicedFrame,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
487
831
|
//# sourceMappingURL=HistoryViewportController.js.map
|