@cplieger/web-terminal-engine 6.0.2 → 6.0.4
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 +11 -11
- package/jsr.json +1 -1
- package/package.json +6 -6
- package/src/connection.ts +13 -19
- package/src/intervals.ts +5 -23
- package/src/scroll.ts +380 -81
- package/src/store.ts +75 -215
- package/src/types.ts +3 -5
- package/src/wire-binary.ts +2 -2
package/src/store.ts
CHANGED
|
@@ -199,7 +199,7 @@ function evictionBatch(maxLines: number): number {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
* Demand-paged-scrollback residency constants
|
|
202
|
+
* Demand-paged-scrollback residency constants.
|
|
203
203
|
*
|
|
204
204
|
* The store holds a small RESIDENT TAIL plus a disposable BROWSE CACHE of
|
|
205
205
|
* pages fetched on demand. The two budgets are enforced by separate mechanisms
|
|
@@ -357,7 +357,7 @@ export class LineStore {
|
|
|
357
357
|
private altDirty = false;
|
|
358
358
|
private resetPending = false;
|
|
359
359
|
|
|
360
|
-
// --- demand-paged scrollback
|
|
360
|
+
// --- demand-paged scrollback ---
|
|
361
361
|
|
|
362
362
|
/**
|
|
363
363
|
* The RESIDENT TAIL cap, and the one deliberately mutable piece of residency
|
|
@@ -425,15 +425,10 @@ export class LineStore {
|
|
|
425
425
|
private browseActivityMs = 0;
|
|
426
426
|
|
|
427
427
|
/**
|
|
428
|
-
* Whether the last resumeAck declared paging
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
* A BOOLEAN, false until an ack says otherwise, not a tri-state with a
|
|
434
|
-
* pre-ack "unknown". The pre-ack instant and a declared `unsupported` want the
|
|
435
|
-
* same answer — assert only what the client's own bookkeeping proves — so a
|
|
436
|
-
* third state would be a distinction nothing reads.
|
|
428
|
+
* Whether the last resumeAck declared paging; the renderer needs it for the
|
|
429
|
+
* top-of-store marker and cannot derive it. A boolean, false until an ack says
|
|
430
|
+
* otherwise: before the first ack and under `unsupported` the answer is the
|
|
431
|
+
* same, so a third state would be read by nothing.
|
|
437
432
|
*/
|
|
438
433
|
private paging = false;
|
|
439
434
|
|
|
@@ -503,7 +498,7 @@ export class LineStore {
|
|
|
503
498
|
* consumer-tunable through the renderer/UI plumb.
|
|
504
499
|
*/
|
|
505
500
|
constructor(maxLines?: number) {
|
|
506
|
-
// Constructor-knob semantics
|
|
501
|
+
// Constructor-knob semantics: an OMITTED
|
|
507
502
|
// cap means "engine's choice", so it holds the legacy value until paging is
|
|
508
503
|
// declared and then flips to the small target. A SUPPLIED cap is an
|
|
509
504
|
// explicit memory decision by the consumer and holds in EVERY capability
|
|
@@ -531,31 +526,12 @@ export class LineStore {
|
|
|
531
526
|
}
|
|
532
527
|
|
|
533
528
|
/**
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
* the application repaints in place, so the content held at that absolute
|
|
541
|
-
* index is what the app most recently drew there — provisional until the row
|
|
542
|
-
* scrolls off and the server commits it. A row arriving in a SCROLL or history
|
|
543
|
-
* frame is the opposite: the server has committed it, at that index, for good.
|
|
544
|
-
*
|
|
545
|
-
* One number rather than a per-row flag or a second key set. The rows above
|
|
546
|
-
* this floor are always a contiguous suffix of the store, because a screen
|
|
547
|
-
* frame's window is contiguous and lowers the floor to its base, while a
|
|
548
|
-
* durable frame that reaches the floor from below raises it past the range it
|
|
549
|
-
* delivered. `paged-scrollback.md` §5.3 records why a parallel structure over
|
|
550
|
-
* the same map is the shape to avoid: the one that was tried, a maintained
|
|
551
|
-
* companion count, was wrong at four mutation sites and reached -96.
|
|
552
|
-
*
|
|
553
|
-
* Movement is deliberately asymmetric. Lowering is unconditional, because a
|
|
554
|
-
* screen frame is proof its window is provisional. Raising happens only when a
|
|
555
|
-
* durable frame reaches the floor from at or below it, so a chunk landing
|
|
556
|
-
* above a still-unconfirmed gap leaves the floor where it is. That direction
|
|
557
|
-
* is the safe one to be wrong in: too low costs a few replayed rows, too high
|
|
558
|
-
* is the defect this exists to prevent.
|
|
529
|
+
* Lowest index the server has not confirmed as committed (+Infinity when all
|
|
530
|
+
* is). A SCREEN-frame row is a repaintable position, provisional until it
|
|
531
|
+
* scrolls off; a SCROLL or history row is committed. Those rows form a
|
|
532
|
+
* contiguous suffix, so one number suffices. It lowers on any screen frame and
|
|
533
|
+
* rises only when a durable frame reaches it from below: too low replays a few
|
|
534
|
+
* rows, too high is the defect this prevents.
|
|
559
535
|
*/
|
|
560
536
|
private unconfirmedFrom = Number.POSITIVE_INFINITY;
|
|
561
537
|
|
|
@@ -661,27 +637,12 @@ export class LineStore {
|
|
|
661
637
|
* changed window row at its absolute index (base + y).
|
|
662
638
|
*/
|
|
663
639
|
applyScreen(msg: ScreenMessage): void {
|
|
664
|
-
// ED3 FIRST,
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
// Handling it inside the main-screen branch made every alt-active ED3 a total
|
|
671
|
-
// no-op: not one of §5.5's three effects ran. `clear; vim foo` typed as one
|
|
672
|
-
// line is enough (clear's terminfo emits CSI 3 J, vim's smcup follows in the
|
|
673
|
-
// same PTY burst, both inside one flush interval), as is `reset` inside any
|
|
674
|
-
// full-screen app. The erased transcript stayed resident and — because the
|
|
675
|
-
// server's ring Clear() preserves `committed`, so post-ED3 lines keep
|
|
676
|
-
// numbering from where the erased ones stopped — it spliced CONTIGUOUSLY into
|
|
677
|
-
// later output with no index gap, no gap marker and no trim marker. The
|
|
678
|
-
// in-flight window also stayed open, so a reply already on the wire put the
|
|
679
|
-
// erased rows back through the one path exempt from guard 2.
|
|
680
|
-
//
|
|
681
|
-
// `msg.base` is the correct bound on either buffer: in alt it is
|
|
682
|
-
// `committedBefore` (alt accrues no history, so the base stays frozen), which
|
|
683
|
-
// is one past the newest committed line, so "below base" is the whole
|
|
684
|
-
// scrollback either way.
|
|
640
|
+
// ED3 FIRST, before the screen-mode dispatch: the server attaches the flag
|
|
641
|
+
// to whatever frame it builds next, alt included, so handling it only on
|
|
642
|
+
// the main path made an alt-active ED3 (`clear; vim foo`, `reset` in a
|
|
643
|
+
// full-screen app) a no-op whose erased transcript spliced contiguously into
|
|
644
|
+
// later output. `msg.base` bounds either buffer: in alt it is frozen at
|
|
645
|
+
// `committedBefore`, one past the newest committed line.
|
|
685
646
|
if (msg.scrollbackCleared) {
|
|
686
647
|
this.applyScrollbackCleared(msg.base);
|
|
687
648
|
}
|
|
@@ -867,9 +828,9 @@ export class LineStore {
|
|
|
867
828
|
}
|
|
868
829
|
}
|
|
869
830
|
|
|
870
|
-
// --- demand-paged scrollback: public surface
|
|
831
|
+
// --- demand-paged scrollback: public surface ---
|
|
871
832
|
|
|
872
|
-
/** Whether the last resumeAck declared demand paging
|
|
833
|
+
/** Whether the last resumeAck declared demand paging. */
|
|
873
834
|
pagingDeclared(): boolean {
|
|
874
835
|
return this.paging;
|
|
875
836
|
}
|
|
@@ -879,7 +840,7 @@ export class LineStore {
|
|
|
879
840
|
return this.serverOldest;
|
|
880
841
|
}
|
|
881
842
|
|
|
882
|
-
/** The lowest index still worth requesting
|
|
843
|
+
/** The lowest index still worth requesting. */
|
|
883
844
|
pagingFloorIndex(): number {
|
|
884
845
|
return this.pagingFloor;
|
|
885
846
|
}
|
|
@@ -957,7 +918,7 @@ export class LineStore {
|
|
|
957
918
|
}
|
|
958
919
|
// `gapHigh > pagingFloor`, deliberately NOT gapLow: after a ring exhaustion
|
|
959
920
|
// followed by a later tail trim, the frontier's low edge EQUALS the floor,
|
|
960
|
-
// and that reopened frontier must stay fetchable
|
|
921
|
+
// and that reopened frontier must stay fetchable.
|
|
961
922
|
const live = candidates.filter((g) => g.hi > this.pagingFloor && g.hi > g.lo);
|
|
962
923
|
const near = live.filter((g) => abs >= g.lo - threshold && abs <= g.hi + threshold);
|
|
963
924
|
near.sort((a, b) => Math.abs(abs - a.hi) - Math.abs(abs - b.hi));
|
|
@@ -990,7 +951,7 @@ export class LineStore {
|
|
|
990
951
|
// A correlated frame wider than the current slot — a timed-out larger reply
|
|
991
952
|
// racing a shrunken retry — has its out-of-window lines routed through the
|
|
992
953
|
// ordinary guards instead, where they are typically refused below the
|
|
993
|
-
// watermark
|
|
954
|
+
// watermark. Without the clip, a stale oversized reply would apply
|
|
994
955
|
// content the client did not ask for on this attempt.
|
|
995
956
|
const s = this.solicitedPending;
|
|
996
957
|
if (s === null) {
|
|
@@ -1030,7 +991,7 @@ export class LineStore {
|
|
|
1030
991
|
}
|
|
1031
992
|
if (applied.length === 0) {
|
|
1032
993
|
// An empty or fully-refused reply changes no classification. The caller
|
|
1033
|
-
// still owns the floor raise for an empty reply
|
|
994
|
+
// still owns the floor raise for an empty reply.
|
|
1034
995
|
this.enforceCap();
|
|
1035
996
|
return;
|
|
1036
997
|
}
|
|
@@ -1101,19 +1062,12 @@ export class LineStore {
|
|
|
1101
1062
|
* safe too.
|
|
1102
1063
|
*/
|
|
1103
1064
|
private enforceBrowseBudget(target: number, viewportAbs: number): void {
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
1109
|
-
//
|
|
1110
|
-
// exempt while still exceeding the target. Two retained runs either side of
|
|
1111
|
-
// the reader's position do it: 400 rows at [0,400), 400 at [600,1000), the
|
|
1112
|
-
// reader in the hole at 500, and a jump ack classifies all 800 against a
|
|
1113
|
-
// target of 500. Nothing is evictable, the pass stops, and the store keeps
|
|
1114
|
-
// 800 — the bounded overshoot the design prefers to blanking the rows under
|
|
1115
|
-
// the reader. Under BROWSE_CACHE_CAP the same arithmetic never bites, which
|
|
1116
|
-
// is what the dead-code claim was really about.
|
|
1065
|
+
// The viewport exemption is reachable: the resume-ack pass targets
|
|
1066
|
+
// PREFETCH_THRESHOLD while the band is 2 * PREFETCH_THRESHOLD + 1 wide, so
|
|
1067
|
+
// runs either side of a reader in a hole (400 at [0,400), 400 at
|
|
1068
|
+
// [600,1000), reader at 500) are all exempt yet over a 500 target. The pass
|
|
1069
|
+
// then stops and keeps them, the bounded overshoot preferred to blanking
|
|
1070
|
+
// rows under the reader.
|
|
1117
1071
|
const exemptLo = viewportAbs - PREFETCH_THRESHOLD;
|
|
1118
1072
|
const exemptHi = viewportAbs + PREFETCH_THRESHOLD + 1;
|
|
1119
1073
|
const exempt = (abs: number) => abs >= exemptLo && abs < exemptHi;
|
|
@@ -1152,18 +1106,11 @@ export class LineStore {
|
|
|
1152
1106
|
}
|
|
1153
1107
|
|
|
1154
1108
|
/**
|
|
1155
|
-
* The resume-ack transition
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
* store owns residency, the renderer owns the viewport. Split across
|
|
1161
|
-
* callbacks, an implementer can interleave them; here they cannot.
|
|
1162
|
-
*
|
|
1163
|
-
* `committed`/`serverOldest` are NULLABLE AS A PAIR: a server too old to
|
|
1164
|
-
* carry the ack's length-gated bounds tail sends neither, and inventing zeros
|
|
1165
|
-
* would lower the floor and forge a jump. Such an ack still runs the epoch
|
|
1166
|
-
* reset and the capability read, and skips the two steps that have no inputs.
|
|
1109
|
+
* The resume-ack transition as ONE store operation, because the order of its
|
|
1110
|
+
* steps is load-bearing and its inputs span three layers (connection, store,
|
|
1111
|
+
* renderer). `committed`/`serverOldest` are NULLABLE AS A PAIR: an older
|
|
1112
|
+
* server sends neither, and inventing zeros would lower the floor and forge a
|
|
1113
|
+
* jump, so such an ack runs only the epoch reset and the capability read.
|
|
1167
1114
|
*/
|
|
1168
1115
|
applyResumeAck(ack: {
|
|
1169
1116
|
/** The boot epoch changed: everything retained is from another server. */
|
|
@@ -1246,24 +1193,12 @@ export class LineStore {
|
|
|
1246
1193
|
// as a key set there is nothing to compare — the containment test below
|
|
1247
1194
|
// reads the set itself and has one answer regardless of which step grew it.
|
|
1248
1195
|
if (reclassified) {
|
|
1249
|
-
// CONTAINMENT: a FOLLOWING viewport is
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
//
|
|
1255
|
-
// in two different structures. It tested whether the row UNDER the reader
|
|
1256
|
-
// was cache — a proxy that fails in exactly the shape
|
|
1257
|
-
// paging exists for: a reader whose position is an index the store does not
|
|
1258
|
-
// currently hold. Both doors to that shape are ordinary — a hole inside the
|
|
1259
|
-
// cache, and an ARMED RESTORE anchor (§7.2/§7.3 of
|
|
1260
|
-
// scroll-position-fidelity.md — the renderer deliberately passes the position
|
|
1261
|
-
// the reader is REGAINING, whose row may since have been dropped). Either
|
|
1262
|
-
// way the reader is unambiguously reading history and the proxy answered
|
|
1263
|
-
// "no", draining their depth to the small target: measured 801 rows kept
|
|
1264
|
-
// instead of 2420. The range version got that case right by accident (a hull
|
|
1265
|
-
// spans holes) and the key version got it wrong by accident; asking only
|
|
1266
|
-
// what the design means removes both.
|
|
1196
|
+
// CONTAINMENT: a FOLLOWING viewport is looking at the live tail, so the
|
|
1197
|
+
// disposable band drains to the small target; a reader in HISTORY keeps
|
|
1198
|
+
// the full cache and the TTL cleans up. Ask only `following`: whether the
|
|
1199
|
+
// row under the reader is cache fails for a hole in the cache and for an
|
|
1200
|
+
// armed restore anchor whose row was dropped, and drained such a reader
|
|
1201
|
+
// to 801 rows instead of 2420.
|
|
1267
1202
|
this.enforceBrowseBudget(
|
|
1268
1203
|
ack.following ? PREFETCH_THRESHOLD : BROWSE_CACHE_CAP,
|
|
1269
1204
|
ack.viewportAbs,
|
|
@@ -1274,13 +1209,10 @@ export class LineStore {
|
|
|
1274
1209
|
|
|
1275
1210
|
/**
|
|
1276
1211
|
* The cap flip: set the effective tail cap to the supported target and
|
|
1277
|
-
* reclassify the excess tail as browse cache
|
|
1278
|
-
*
|
|
1212
|
+
* reclassify the excess tail as browse cache; returns whether anything moved.
|
|
1279
1213
|
* The band keeps the newest `max(supportedTarget, windowHeight)` tail lines
|
|
1280
|
-
* and
|
|
1281
|
-
*
|
|
1282
|
-
* evictable. (§5.2's replay-jump band deliberately DOES include the old
|
|
1283
|
-
* window rows, but only because it retires the descriptor first.)
|
|
1214
|
+
* and never crosses `win.base`, because browse cache is evictable and a live
|
|
1215
|
+
* window row must not be.
|
|
1284
1216
|
*/
|
|
1285
1217
|
private confirmPaging(): boolean {
|
|
1286
1218
|
this.effectiveTailCap = this.supportedTarget;
|
|
@@ -1303,45 +1235,12 @@ export class LineStore {
|
|
|
1303
1235
|
}
|
|
1304
1236
|
|
|
1305
1237
|
/**
|
|
1306
|
-
* Predict where the
|
|
1307
|
-
*
|
|
1308
|
-
* cache before
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
* ring has moved past the client's `haveThrough`. Scoping detection to the
|
|
1313
|
-
* clamp alone would miss the shape that already ships.
|
|
1314
|
-
*
|
|
1315
|
-
* The band deliberately includes the OLD window rows and RETIRES the window
|
|
1316
|
-
* descriptor, because the batch's own window frame re-establishes it at the
|
|
1317
|
-
* new base — those rows stop being window rows the moment it lands.
|
|
1318
|
-
*
|
|
1319
|
-
* The band top is `sentHaveThrough` and deliberately does NOT follow the wire
|
|
1320
|
-
* value down now that `replayBoundary()` supplies it. The two questions are
|
|
1321
|
-
* different: the wire value asks "what will I not ask for", the band asks "what
|
|
1322
|
-
* did I claim to hold before the server answered". Rows above the sent value
|
|
1323
|
-
* arrived from the server as committed content, so they are real history and
|
|
1324
|
-
* must not be reclassified as disposable cache; rows this store holds only
|
|
1325
|
-
* provisionally sit at or above the boundary and are covered by the replay,
|
|
1326
|
-
* which now starts at `boundary + 1` precisely because of it.
|
|
1327
|
-
*
|
|
1328
|
-
* One residual, deliberately not addressed here. When the replay start is
|
|
1329
|
-
* CLAMPED above the boundary (`committed - replayMax`, reachable when a
|
|
1330
|
-
* background tab printed more than roughly `tailCap` lines), the provisional
|
|
1331
|
-
* rows are neither replayed nor banded, so they stay tail-classified and lose
|
|
1332
|
-
* the browse budget's TTL sweep. Widening the band to cover them would change a
|
|
1333
|
-
* pinned invariant that two existing cases assert, so it is recorded in
|
|
1334
|
-
* docs/resume-watermark.md rather than changed in passing.
|
|
1335
|
-
*
|
|
1336
|
-
* The retirement leaves the same transition's later steps reading a window of
|
|
1337
|
-
* height 0, so state precisely what that costs rather than forbidding it: the
|
|
1338
|
-
* only window-derived bound they touch is `enforceCap`'s, which loses the live
|
|
1339
|
-
* screen's eviction protection for that one pass. It is harmless HERE and not
|
|
1340
|
-
* by luck — after the jump the tail holds only the keys above
|
|
1341
|
-
* `sentHaveThrough`, so `tailCount` is far below the bound and the pass returns
|
|
1342
|
-
* at its first gate; and were it to run, the walk is oldest-first while the
|
|
1343
|
-
* screen rows are the newest. A future step that evaluates a window bound with
|
|
1344
|
-
* consequences must re-establish the descriptor first.
|
|
1238
|
+
* Predict where the replay will START and, if above the sent `haveThrough`
|
|
1239
|
+
* (a replay clamp or a plain eviction gap), reclassify the stranded band as
|
|
1240
|
+
* browse cache before the batch applies. The band's top is `sentHaveThrough`
|
|
1241
|
+
* (rows above it are committed history). It retires the window descriptor, so
|
|
1242
|
+
* a later step in this transition that relies on a window bound must restore
|
|
1243
|
+
* it first.
|
|
1345
1244
|
*/
|
|
1346
1245
|
private predictReplayJump(
|
|
1347
1246
|
committed: number,
|
|
@@ -1488,25 +1387,18 @@ export class LineStore {
|
|
|
1488
1387
|
this.resetPending = true;
|
|
1489
1388
|
// CONTENT-derived paging state goes with the content: a new boot epoch shares
|
|
1490
1389
|
// no absolute indices with the old one, so the cache, the in-flight window and
|
|
1491
|
-
// the floor are all meaningless at once
|
|
1390
|
+
// the floor are all meaningless at once.
|
|
1492
1391
|
this.browse.clear();
|
|
1493
1392
|
this.solicitedPending = null;
|
|
1494
1393
|
this.pagingFloor = 0;
|
|
1495
1394
|
this.browseActivityMs = 0;
|
|
1496
1395
|
// CAPABILITY is deliberately NOT reset here. It describes the SERVER on the
|
|
1497
1396
|
// other end of the socket, not the content, and only a resumeAck can restate
|
|
1498
|
-
// it
|
|
1499
|
-
//
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
// on alt-screen entry, nowhere near a resume. Until the next reconnect that
|
|
1504
|
-
// left `pagingDeclared()` false while the transport was still perfectly able
|
|
1505
|
-
// to page, so §5.4's top marker asserted "earlier output trimmed" about
|
|
1506
|
-
// history the trigger was about to fetch, and the tail cap reverted to the
|
|
1507
|
-
// compatibility value — discarding the residency reduction that is the whole
|
|
1508
|
-
// point of the feature on the device it exists for. `applyResumeAck` now
|
|
1509
|
-
// states the capability explicitly instead of relying on a reset to clear it.
|
|
1397
|
+
// it. `render.resetScreen()` and `render.resetScrollback()` reach this on
|
|
1398
|
+
// alt-screen entry, nowhere near a resume; clearing it there would leave
|
|
1399
|
+
// `pagingDeclared()` false until the next reconnect, so the top marker would
|
|
1400
|
+
// claim "earlier output trimmed" about fetchable history and the tail cap
|
|
1401
|
+
// would revert to the compatibility value.
|
|
1510
1402
|
}
|
|
1511
1403
|
|
|
1512
1404
|
/**
|
|
@@ -1533,17 +1425,11 @@ export class LineStore {
|
|
|
1533
1425
|
return null;
|
|
1534
1426
|
}
|
|
1535
1427
|
const cap = Number.isInteger(maxLines) && maxLines > 0 ? maxLines : this.effectiveTailCap;
|
|
1536
|
-
// Persist the LIVE TAIL only
|
|
1537
|
-
//
|
|
1538
|
-
//
|
|
1539
|
-
//
|
|
1540
|
-
//
|
|
1541
|
-
// the cache is disposable by construction (recovery is one page fetch), and
|
|
1542
|
-
// persisting it would spend storage writes on data the design defines as
|
|
1543
|
-
// throwaway and hydrate interior holes into a fresh store.
|
|
1544
|
-
//
|
|
1545
|
-
// The result is that a hydrated store is always ONE contiguous tail, which
|
|
1546
|
-
// is what keeps its derived `everEvictedThrough = oldest - 1` honest.
|
|
1428
|
+
// Persist the LIVE TAIL only: walk down from `highest` and stop at the first
|
|
1429
|
+
// absent index OR browse member, since a fetched page can sit flush against
|
|
1430
|
+
// the tail with no numeric gap. Cache is disposable (one page fetch), and a
|
|
1431
|
+
// hydrated store must be ONE contiguous tail to keep its derived
|
|
1432
|
+
// `everEvictedThrough = oldest - 1` honest.
|
|
1547
1433
|
const tail: number[] = [];
|
|
1548
1434
|
for (let abs = this.highest; abs >= 0 && tail.length < cap; abs--) {
|
|
1549
1435
|
if (!this.lines.has(abs) || this.browse.has(abs)) {
|
|
@@ -1732,22 +1618,12 @@ export class LineStore {
|
|
|
1732
1618
|
if (!Number.isInteger(abs) || abs < 0) {
|
|
1733
1619
|
return;
|
|
1734
1620
|
}
|
|
1735
|
-
// Guard 2:
|
|
1736
|
-
// EXCEPT inside the
|
|
1737
|
-
//
|
|
1738
|
-
//
|
|
1739
|
-
//
|
|
1740
|
-
//
|
|
1741
|
-
// must degrade to a drawn screen, not to window rows silently dropped
|
|
1742
|
-
// forever (the property test drives exactly that shape). Same doctrine
|
|
1743
|
-
// as enforceCap's "the live window is never evictable".
|
|
1744
|
-
//
|
|
1745
|
-
// The SOLICITED exception is what makes demand paging possible at all:
|
|
1746
|
-
// a page is by definition a re-fetch of history below the watermark, so
|
|
1747
|
-
// the watermark alone cannot decide staleness any more. What keeps it
|
|
1748
|
-
// safe is that the exception is scoped to one in-flight request window —
|
|
1749
|
-
// an UNSOLICITED line below the watermark is still refused, exactly as
|
|
1750
|
-
// before (docs/paged-scrollback.md §5.1).
|
|
1621
|
+
// Guard 2: refuse an index at or below the permanent-eviction watermark (a
|
|
1622
|
+
// stale re-send), EXCEPT inside the live window (a hostile frame whose base
|
|
1623
|
+
// retreats must still draw, as enforceCap never evicts the window) and
|
|
1624
|
+
// EXCEPT inside the in-flight request window: a page is by definition a
|
|
1625
|
+
// re-fetch below the watermark, and an UNSOLICITED line there is still
|
|
1626
|
+
// refused.
|
|
1751
1627
|
if (abs <= this.everEvictedThrough && !this.inWindow(abs) && !this.isSolicited(abs)) {
|
|
1752
1628
|
return;
|
|
1753
1629
|
}
|
|
@@ -1794,28 +1670,12 @@ export class LineStore {
|
|
|
1794
1670
|
return s !== null && abs >= s.lo && abs < s.hi;
|
|
1795
1671
|
}
|
|
1796
1672
|
|
|
1797
|
-
// applyScrollbackCleared handles ED3 (
|
|
1798
|
-
//
|
|
1799
|
-
//
|
|
1800
|
-
//
|
|
1801
|
-
//
|
|
1802
|
-
//
|
|
1803
|
-
//
|
|
1804
|
-
// Dropping the lines is only ONE of the three effects §5.5 requires, and the
|
|
1805
|
-
// other two are what stop the erased history coming back:
|
|
1806
|
-
//
|
|
1807
|
-
// - The in-flight request window is CANCELLED. It is the store's standing
|
|
1808
|
-
// permission to admit lines below its stale-re-send watermark, so a reply
|
|
1809
|
-
// already in flight when the app erased its scrollback would otherwise
|
|
1810
|
-
// re-apply the very rows the app just discarded — through the one path that
|
|
1811
|
-
// is exempt from the staleness guard.
|
|
1812
|
-
// - The paging FLOOR snaps to the cleared bound: nothing at or below it is
|
|
1813
|
-
// worth requesting any more. Without it the trigger re-fetches the erased
|
|
1814
|
-
// range from a server that still holds it, and the user watches erased
|
|
1815
|
-
// output scroll back into view.
|
|
1816
|
-
//
|
|
1817
|
-
// Both run even when this client holds no line below `base`: an in-flight
|
|
1818
|
-
// request and a stale floor are not conditional on local residency.
|
|
1673
|
+
// applyScrollbackCleared handles ED3 (which an inline TUI such as kiro-cli
|
|
1674
|
+
// emits on every resize): drop lines below `base`, leave everEvictedThrough
|
|
1675
|
+
// alone (a deliberate erase is not a trim), CANCEL the in-flight request
|
|
1676
|
+
// (its reply would re-admit the erased rows through the guard-2 exemption),
|
|
1677
|
+
// and snap the paging floor to `base` (or the trigger re-fetches them). The
|
|
1678
|
+
// last two run even when no line below `base` is held.
|
|
1819
1679
|
private applyScrollbackCleared(base: number): void {
|
|
1820
1680
|
this.solicitedPending = null;
|
|
1821
1681
|
this.raisePagingFloor(base);
|
|
@@ -1895,7 +1755,7 @@ export class LineStore {
|
|
|
1895
1755
|
}
|
|
1896
1756
|
|
|
1897
1757
|
private enforceCap(): void {
|
|
1898
|
-
//
|
|
1758
|
+
// The gate and the victim
|
|
1899
1759
|
// target read `tailCount`, not `lines.size`. Browse cache lives under its
|
|
1900
1760
|
// OWN budget, enforced at page-apply time, so live output must not be able
|
|
1901
1761
|
// to evict a page the reader is browsing — at the default batch that would
|
package/src/types.ts
CHANGED
|
@@ -260,11 +260,9 @@ export type ControlMessage =
|
|
|
260
260
|
haveThrough: number;
|
|
261
261
|
protocolVersion: number;
|
|
262
262
|
/**
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* paging, and clamped by BOTH sides to the same constant so the value
|
|
267
|
-
* sent equals the value honored (docs/paged-scrollback.md §4.5).
|
|
263
|
+
* Ask for a replay smaller than the server's own bound. Clamped by BOTH
|
|
264
|
+
* sides to the same constant (MAX_REPLAY_LINES) so the value sent
|
|
265
|
+
* equals the value honored.
|
|
268
266
|
*/
|
|
269
267
|
replayMax?: number;
|
|
270
268
|
}
|
package/src/wire-binary.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Binary wire decoder. Mirrors wire_binary.go on the server.
|
|
2
2
|
//
|
|
3
|
-
// All multi-byte integers are little-endian.
|
|
4
|
-
//
|
|
3
|
+
// All multi-byte integers are little-endian. The frame layouts are in
|
|
4
|
+
// docs/wire-protocol.md "Byte layout" at the repository root.
|
|
5
5
|
|
|
6
6
|
import type {
|
|
7
7
|
WireRun,
|