@floegence/floeterm-terminal-web 0.5.18 → 0.5.20
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 +33 -0
- package/dist/core/PagedTerminalOutputCoordinator.d.ts +29 -1
- package/dist/core/PagedTerminalOutputCoordinator.d.ts.map +1 -1
- package/dist/core/PagedTerminalOutputCoordinator.js +412 -16
- package/dist/core/PagedTerminalOutputCoordinator.js.map +1 -1
- package/dist/core/TerminalCore.d.ts +6 -2
- package/dist/core/TerminalCore.d.ts.map +1 -1
- package/dist/core/TerminalCore.js +168 -36
- package/dist/core/TerminalCore.js.map +1 -1
- package/dist/fabric/BeamtermFabricRenderer.d.ts.map +1 -1
- package/dist/fabric/BeamtermFabricRenderer.js +3 -0
- package/dist/fabric/BeamtermFabricRenderer.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/TerminalInitializationScheduler.d.ts +24 -8
- package/dist/internal/TerminalInitializationScheduler.d.ts.map +1 -1
- package/dist/internal/TerminalInitializationScheduler.js +61 -28
- package/dist/internal/TerminalInitializationScheduler.js.map +1 -1
- package/dist/manager/TerminalInstanceController.d.ts +0 -2
- package/dist/manager/TerminalInstanceController.d.ts.map +1 -1
- package/dist/manager/TerminalInstanceController.js +38 -54
- package/dist/manager/TerminalInstanceController.js.map +1 -1
- package/dist/sessions/TerminalSessionsCoordinator.d.ts +6 -0
- package/dist/sessions/TerminalSessionsCoordinator.d.ts.map +1 -1
- package/dist/sessions/TerminalSessionsCoordinator.js +66 -16
- package/dist/sessions/TerminalSessionsCoordinator.js.map +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { scheduleUiTurn } from '../internal/scheduleUiTurn';
|
|
1
2
|
const DEFAULT_POLICY = {
|
|
2
3
|
maxRetainedLiveChunks: 2048,
|
|
3
4
|
maxRetainedLiveBytes: 8 * 1024 * 1024,
|
|
4
5
|
retryDelaysMs: [250, 1000, 4000],
|
|
5
6
|
maxWriteBatchBytes: 256 * 1024,
|
|
6
7
|
};
|
|
8
|
+
const DEFAULT_PREPARED_HISTORY_MAX_BYTES = 32 * 1024 * 1024;
|
|
7
9
|
const normalizePositiveInteger = (value, fallback) => (typeof value === 'number' && Number.isSafeInteger(value) && value > 0
|
|
8
10
|
? value
|
|
9
11
|
: fallback);
|
|
@@ -33,6 +35,213 @@ class HistoryContractError extends Error {
|
|
|
33
35
|
this.firstRetainedSequence = firstRetainedSequence;
|
|
34
36
|
}
|
|
35
37
|
}
|
|
38
|
+
const createAbortError = () => {
|
|
39
|
+
if (typeof DOMException !== 'undefined')
|
|
40
|
+
return new DOMException('Operation aborted', 'AbortError');
|
|
41
|
+
const error = new Error('Operation aborted');
|
|
42
|
+
error.name = 'AbortError';
|
|
43
|
+
return error;
|
|
44
|
+
};
|
|
45
|
+
const throwIfAborted = (signal) => {
|
|
46
|
+
if (signal?.aborted)
|
|
47
|
+
throw createAbortError();
|
|
48
|
+
};
|
|
49
|
+
const waitForPreparedPage = (promise, signal) => {
|
|
50
|
+
if (!signal)
|
|
51
|
+
return promise;
|
|
52
|
+
if (signal.aborted)
|
|
53
|
+
return Promise.reject(createAbortError());
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const onAbort = () => reject(createAbortError());
|
|
56
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
57
|
+
promise.then(value => {
|
|
58
|
+
signal.removeEventListener('abort', onAbort);
|
|
59
|
+
resolve(value);
|
|
60
|
+
}, error => {
|
|
61
|
+
signal.removeEventListener('abort', onAbort);
|
|
62
|
+
reject(error);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const yieldHistoryPreparation = async (signal, yieldControl) => {
|
|
67
|
+
throwIfAborted(signal);
|
|
68
|
+
if (yieldControl) {
|
|
69
|
+
await yieldControl();
|
|
70
|
+
throwIfAborted(signal);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await new Promise((resolve, reject) => {
|
|
74
|
+
const cancelTurn = scheduleUiTurn(() => {
|
|
75
|
+
signal?.removeEventListener('abort', onAbort);
|
|
76
|
+
resolve();
|
|
77
|
+
});
|
|
78
|
+
const onAbort = () => {
|
|
79
|
+
cancelTurn();
|
|
80
|
+
reject(createAbortError());
|
|
81
|
+
};
|
|
82
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
const clonePreparedChunk = (chunk) => {
|
|
86
|
+
const immutableData = new Uint8Array(chunk.data);
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
get data() {
|
|
89
|
+
return new Uint8Array(immutableData);
|
|
90
|
+
},
|
|
91
|
+
...(chunk.sequence !== undefined ? { sequence: chunk.sequence } : {}),
|
|
92
|
+
...(chunk.timestampMs !== undefined ? { timestampMs: chunk.timestampMs } : {}),
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
export const preparePagedTerminalHistory = async (options) => {
|
|
96
|
+
const requestedStartSequence = normalizeSequence(options.startSequence ?? 1, 'startSequence');
|
|
97
|
+
const maxBytes = normalizePositiveInteger(options.maxBytes, DEFAULT_PREPARED_HISTORY_MAX_BYTES);
|
|
98
|
+
let startSequence = Math.max(1, requestedStartSequence);
|
|
99
|
+
let cursor;
|
|
100
|
+
let historyGeneration;
|
|
101
|
+
let snapshotEndSequence;
|
|
102
|
+
let firstRetainedSequence;
|
|
103
|
+
let coveredThroughSequence = Math.max(0, startSequence - 1);
|
|
104
|
+
let byteLength = 0;
|
|
105
|
+
let pageCount = 0;
|
|
106
|
+
let chunks = [];
|
|
107
|
+
let rebaseAttempts = 0;
|
|
108
|
+
const requestSignal = options.signal ?? new AbortController().signal;
|
|
109
|
+
while (true) {
|
|
110
|
+
throwIfAborted(options.signal);
|
|
111
|
+
const page = await waitForPreparedPage(options.fetchPage({
|
|
112
|
+
startSequence,
|
|
113
|
+
endSequence: snapshotEndSequence,
|
|
114
|
+
historyGeneration,
|
|
115
|
+
cursor,
|
|
116
|
+
maxBytes: Math.max(1, maxBytes - byteLength),
|
|
117
|
+
signal: requestSignal,
|
|
118
|
+
}), options.signal);
|
|
119
|
+
throwIfAborted(options.signal);
|
|
120
|
+
pageCount += 1;
|
|
121
|
+
if (!Object.prototype.hasOwnProperty.call(page, 'coveredThroughSequence')) {
|
|
122
|
+
throw new HistoryContractError('history_contract_missing', 'coveredThroughSequence is required');
|
|
123
|
+
}
|
|
124
|
+
if (!Object.prototype.hasOwnProperty.call(page, 'snapshotEndSequence')) {
|
|
125
|
+
throw new HistoryContractError('history_contract_missing', 'snapshotEndSequence is required');
|
|
126
|
+
}
|
|
127
|
+
if (!Object.prototype.hasOwnProperty.call(page, 'historyGeneration')) {
|
|
128
|
+
throw new HistoryContractError('history_contract_missing', 'historyGeneration is required');
|
|
129
|
+
}
|
|
130
|
+
if (!Object.prototype.hasOwnProperty.call(page, 'firstRetainedSequence')
|
|
131
|
+
&& !Object.prototype.hasOwnProperty.call(page, 'firstAvailableSequence')) {
|
|
132
|
+
throw new HistoryContractError('history_contract_missing', 'firstRetainedSequence is required');
|
|
133
|
+
}
|
|
134
|
+
const pageCoverage = normalizeSequence(page.coveredThroughSequence, 'coveredThroughSequence');
|
|
135
|
+
const pageSnapshotEnd = normalizeSequence(page.snapshotEndSequence, 'snapshotEndSequence');
|
|
136
|
+
const pageGeneration = normalizeSequence(page.historyGeneration, 'historyGeneration');
|
|
137
|
+
const pageFirstRetained = normalizeSequence(page.firstRetainedSequence ?? page.firstAvailableSequence, 'firstRetainedSequence');
|
|
138
|
+
const generationChanged = historyGeneration !== undefined && pageGeneration !== historyGeneration;
|
|
139
|
+
const snapshotChanged = snapshotEndSequence !== undefined && pageSnapshotEnd !== snapshotEndSequence;
|
|
140
|
+
const retentionAdvanced = firstRetainedSequence !== undefined
|
|
141
|
+
&& pageFirstRetained > firstRetainedSequence;
|
|
142
|
+
if (historyGeneration !== undefined
|
|
143
|
+
&& (page.historyReset || page.historyTruncated || generationChanged || snapshotChanged || retentionAdvanced)) {
|
|
144
|
+
if (rebaseAttempts >= 2) {
|
|
145
|
+
throw new HistoryContractError('history_contract_invalid', 'terminal history changed repeatedly while preparing a stable snapshot');
|
|
146
|
+
}
|
|
147
|
+
rebaseAttempts += 1;
|
|
148
|
+
startSequence = Math.max(1, pageFirstRetained);
|
|
149
|
+
cursor = undefined;
|
|
150
|
+
historyGeneration = undefined;
|
|
151
|
+
snapshotEndSequence = undefined;
|
|
152
|
+
firstRetainedSequence = undefined;
|
|
153
|
+
coveredThroughSequence = startSequence - 1;
|
|
154
|
+
byteLength = 0;
|
|
155
|
+
chunks = [];
|
|
156
|
+
await yieldHistoryPreparation(options.signal, options.yieldControl);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (pageCoverage < coveredThroughSequence) {
|
|
160
|
+
throw new HistoryContractError('history_contract_invalid', 'coveredThroughSequence regressed');
|
|
161
|
+
}
|
|
162
|
+
if (pageCoverage > pageSnapshotEnd) {
|
|
163
|
+
throw new HistoryContractError('history_contract_invalid', 'coveredThroughSequence exceeded snapshotEndSequence');
|
|
164
|
+
}
|
|
165
|
+
let previousChunkSequence = coveredThroughSequence;
|
|
166
|
+
for (const chunk of page.chunks) {
|
|
167
|
+
const sequence = normalizeSequence(chunk.sequence, 'chunk.sequence');
|
|
168
|
+
if (sequence <= previousChunkSequence || sequence > pageCoverage) {
|
|
169
|
+
throw new HistoryContractError('history_contract_invalid', 'prepared history chunks must have strictly increasing sequences within page coverage');
|
|
170
|
+
}
|
|
171
|
+
previousChunkSequence = sequence;
|
|
172
|
+
}
|
|
173
|
+
if (pageCoverage > Math.max(0, pageFirstRetained - 1)
|
|
174
|
+
&& previousChunkSequence !== pageCoverage) {
|
|
175
|
+
throw new HistoryContractError('history_contract_invalid', 'prepared history page did not include its covered terminal sequence');
|
|
176
|
+
}
|
|
177
|
+
historyGeneration = pageGeneration;
|
|
178
|
+
snapshotEndSequence = pageSnapshotEnd;
|
|
179
|
+
firstRetainedSequence = pageFirstRetained;
|
|
180
|
+
if (pageFirstRetained > startSequence) {
|
|
181
|
+
coveredThroughSequence = Math.max(coveredThroughSequence, pageFirstRetained - 1);
|
|
182
|
+
}
|
|
183
|
+
const pageBytes = page.chunks.reduce((sum, chunk) => sum + chunk.data.byteLength, 0);
|
|
184
|
+
const remainingBytes = maxBytes - byteLength;
|
|
185
|
+
if (pageBytes <= remainingBytes) {
|
|
186
|
+
chunks.push(...page.chunks.map(clonePreparedChunk));
|
|
187
|
+
byteLength += pageBytes;
|
|
188
|
+
coveredThroughSequence = Math.max(coveredThroughSequence, pageCoverage);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
for (const chunk of page.chunks) {
|
|
192
|
+
const sequence = normalizeSequence(chunk.sequence, 'chunk.sequence');
|
|
193
|
+
if (byteLength + chunk.data.byteLength > maxBytes)
|
|
194
|
+
break;
|
|
195
|
+
chunks.push(clonePreparedChunk(chunk));
|
|
196
|
+
byteLength += chunk.data.byteLength;
|
|
197
|
+
coveredThroughSequence = Math.max(coveredThroughSequence, sequence);
|
|
198
|
+
}
|
|
199
|
+
return Object.freeze({
|
|
200
|
+
chunks: Object.freeze(chunks),
|
|
201
|
+
requestedStartSequence,
|
|
202
|
+
firstRetainedSequence,
|
|
203
|
+
coveredThroughSequence,
|
|
204
|
+
snapshotEndSequence,
|
|
205
|
+
historyGeneration,
|
|
206
|
+
byteLength,
|
|
207
|
+
pageCount,
|
|
208
|
+
complete: false,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (!page.hasMore) {
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
chunks: Object.freeze(chunks),
|
|
214
|
+
requestedStartSequence,
|
|
215
|
+
firstRetainedSequence,
|
|
216
|
+
coveredThroughSequence,
|
|
217
|
+
snapshotEndSequence,
|
|
218
|
+
historyGeneration,
|
|
219
|
+
byteLength,
|
|
220
|
+
pageCount,
|
|
221
|
+
complete: coveredThroughSequence >= snapshotEndSequence,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (byteLength >= maxBytes) {
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
chunks: Object.freeze(chunks),
|
|
227
|
+
requestedStartSequence,
|
|
228
|
+
firstRetainedSequence,
|
|
229
|
+
coveredThroughSequence,
|
|
230
|
+
snapshotEndSequence,
|
|
231
|
+
historyGeneration,
|
|
232
|
+
byteLength,
|
|
233
|
+
pageCount,
|
|
234
|
+
complete: false,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (page.nextCursor === undefined) {
|
|
238
|
+
throw new HistoryContractError('history_contract_invalid', 'nextCursor is required when hasMore is true');
|
|
239
|
+
}
|
|
240
|
+
cursor = page.nextCursor;
|
|
241
|
+
startSequence = pageCoverage + 1;
|
|
242
|
+
await yieldHistoryPreparation(options.signal, options.yieldControl);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
36
245
|
class PagedTerminalOutputCoordinator {
|
|
37
246
|
constructor(options) {
|
|
38
247
|
this.active = true;
|
|
@@ -51,6 +260,7 @@ class PagedTerminalOutputCoordinator {
|
|
|
51
260
|
this.lastError = null;
|
|
52
261
|
this.failure = null;
|
|
53
262
|
this.recoveryStartSequence = 1;
|
|
263
|
+
this.explicitAttachFence = false;
|
|
54
264
|
this.recoveryKind = 'initial';
|
|
55
265
|
this.recoveryRunning = false;
|
|
56
266
|
this.needsRebase = false;
|
|
@@ -62,6 +272,8 @@ class PagedTerminalOutputCoordinator {
|
|
|
62
272
|
this.activeWriters = 0;
|
|
63
273
|
this.writerQuiescenceWaiters = [];
|
|
64
274
|
this.pausedRecoveryPending = false;
|
|
275
|
+
this.droppedLiveThroughSequence = 0;
|
|
276
|
+
this.preparedHistory = null;
|
|
65
277
|
this.options = options;
|
|
66
278
|
this.policy = {
|
|
67
279
|
maxRetainedLiveChunks: normalizePositiveInteger(options.policy?.maxRetainedLiveChunks, DEFAULT_POLICY.maxRetainedLiveChunks),
|
|
@@ -72,9 +284,13 @@ class PagedTerminalOutputCoordinator {
|
|
|
72
284
|
maxWriteBatchBytes: normalizePositiveInteger(options.policy?.maxWriteBatchBytes, DEFAULT_POLICY.maxWriteBatchBytes),
|
|
73
285
|
};
|
|
74
286
|
}
|
|
75
|
-
async attach(startSequence = 1) {
|
|
287
|
+
async attach(startSequence = 1, snapshotEndSequence, options) {
|
|
288
|
+
const attachGeneration = this.beginAttach(startSequence);
|
|
289
|
+
await this.completeAttach(attachGeneration, snapshotEndSequence, options);
|
|
290
|
+
}
|
|
291
|
+
beginAttach(startSequence = 1) {
|
|
76
292
|
if (this.disposed)
|
|
77
|
-
return;
|
|
293
|
+
return this.generation;
|
|
78
294
|
this.cancelRecovery();
|
|
79
295
|
this.resolveBaselineWaiters();
|
|
80
296
|
this.retainedLive = [];
|
|
@@ -88,10 +304,37 @@ class PagedTerminalOutputCoordinator {
|
|
|
88
304
|
this.scheduledThroughSequence = this.coveredThroughSequence;
|
|
89
305
|
this.recoveryKind = 'initial';
|
|
90
306
|
this.recoveryStartSequence = Math.max(0, Math.floor(startSequence));
|
|
307
|
+
this.recoveryEndSequence = undefined;
|
|
308
|
+
this.explicitAttachFence = false;
|
|
309
|
+
this.droppedLiveThroughSequence = 0;
|
|
310
|
+
this.needsRebase = false;
|
|
91
311
|
this.retryAttempt = 0;
|
|
92
312
|
this.failure = null;
|
|
93
313
|
this.lastError = null;
|
|
94
314
|
this.historyRebasePrepared = false;
|
|
315
|
+
this.preparedHistory = null;
|
|
316
|
+
this.setState('idle');
|
|
317
|
+
return this.generation;
|
|
318
|
+
}
|
|
319
|
+
async completeAttach(attachGeneration, snapshotEndSequence, options = {}) {
|
|
320
|
+
if (this.disposed || !this.isCurrent(attachGeneration))
|
|
321
|
+
return;
|
|
322
|
+
this.explicitAttachFence = snapshotEndSequence !== undefined;
|
|
323
|
+
this.recoveryEndSequence = normalizeSequence(snapshotEndSequence, 'snapshotEndSequence', true);
|
|
324
|
+
this.preparedHistory = this.acceptPreparedHistory(options.preparedHistory);
|
|
325
|
+
if (this.explicitAttachFence && this.recoveryEndSequence === 0) {
|
|
326
|
+
if (this.needsRebase) {
|
|
327
|
+
this.prepareRetainedLiveRebase();
|
|
328
|
+
await this.runRecovery();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
this.baselineReady = true;
|
|
332
|
+
this.resolveBaselineWaiters();
|
|
333
|
+
this.emitState();
|
|
334
|
+
this.setState('live');
|
|
335
|
+
this.drainRetainedLive();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
95
338
|
await this.runRecovery();
|
|
96
339
|
}
|
|
97
340
|
waitForBaseline() {
|
|
@@ -163,10 +406,14 @@ class PagedTerminalOutputCoordinator {
|
|
|
163
406
|
this.baselineReady = false;
|
|
164
407
|
this.coveredThroughSequence = Math.max(0, Math.floor(startSequence) - 1);
|
|
165
408
|
this.scheduledThroughSequence = this.coveredThroughSequence;
|
|
409
|
+
this.recoveryEndSequence = undefined;
|
|
410
|
+
this.explicitAttachFence = false;
|
|
411
|
+
this.droppedLiveThroughSequence = 0;
|
|
166
412
|
this.retryAttempt = 0;
|
|
167
413
|
this.failure = null;
|
|
168
414
|
this.lastError = null;
|
|
169
415
|
this.historyRebasePrepared = false;
|
|
416
|
+
this.preparedHistory = null;
|
|
170
417
|
this.setState('idle');
|
|
171
418
|
this.resolveBaselineWaiters();
|
|
172
419
|
}
|
|
@@ -203,6 +450,7 @@ class PagedTerminalOutputCoordinator {
|
|
|
203
450
|
this.pendingLiveWrites = [];
|
|
204
451
|
this.liveWriteScheduled = false;
|
|
205
452
|
this.pausedRecoveryPending = false;
|
|
453
|
+
this.preparedHistory = null;
|
|
206
454
|
this.setState('disposed');
|
|
207
455
|
this.resolveBaselineWaiters();
|
|
208
456
|
}
|
|
@@ -211,9 +459,69 @@ class PagedTerminalOutputCoordinator {
|
|
|
211
459
|
return;
|
|
212
460
|
this.recoveryKind = 'catch-up';
|
|
213
461
|
this.recoveryStartSequence = Math.max(0, Math.floor(startSequence));
|
|
462
|
+
const firstRetainedSequence = this.firstRetainedLiveSequence();
|
|
463
|
+
this.recoveryEndSequence = firstRetainedSequence === undefined
|
|
464
|
+
? undefined
|
|
465
|
+
: Math.max(0, firstRetainedSequence - 1);
|
|
214
466
|
this.retryAttempt = 0;
|
|
215
467
|
void this.runRecovery();
|
|
216
468
|
}
|
|
469
|
+
acceptPreparedHistory(preparedHistory) {
|
|
470
|
+
if (!preparedHistory || this.recoveryEndSequence === undefined)
|
|
471
|
+
return null;
|
|
472
|
+
try {
|
|
473
|
+
const requestedStart = normalizeSequence(preparedHistory.requestedStartSequence, 'preparedHistory.requestedStartSequence');
|
|
474
|
+
const firstRetained = normalizeSequence(preparedHistory.firstRetainedSequence, 'preparedHistory.firstRetainedSequence');
|
|
475
|
+
const coveredThrough = normalizeSequence(preparedHistory.coveredThroughSequence, 'preparedHistory.coveredThroughSequence');
|
|
476
|
+
const snapshotEnd = normalizeSequence(preparedHistory.snapshotEndSequence, 'preparedHistory.snapshotEndSequence');
|
|
477
|
+
normalizeSequence(preparedHistory.historyGeneration, 'preparedHistory.historyGeneration');
|
|
478
|
+
normalizeSequence(preparedHistory.byteLength, 'preparedHistory.byteLength');
|
|
479
|
+
normalizeSequence(preparedHistory.pageCount, 'preparedHistory.pageCount');
|
|
480
|
+
const expectedStart = Math.max(1, this.recoveryStartSequence);
|
|
481
|
+
if (requestedStart > expectedStart)
|
|
482
|
+
return null;
|
|
483
|
+
if (firstRetained > coveredThrough + 1)
|
|
484
|
+
return null;
|
|
485
|
+
if (coveredThrough < expectedStart - 1 || coveredThrough > snapshotEnd)
|
|
486
|
+
return null;
|
|
487
|
+
if (snapshotEnd > this.recoveryEndSequence || coveredThrough > this.recoveryEndSequence)
|
|
488
|
+
return null;
|
|
489
|
+
if (!Array.isArray(preparedHistory.chunks))
|
|
490
|
+
return null;
|
|
491
|
+
let previousChunkSequence = Math.max(0, firstRetained - 1);
|
|
492
|
+
let preparedBytes = 0;
|
|
493
|
+
for (const chunk of preparedHistory.chunks) {
|
|
494
|
+
if (!(chunk.data instanceof Uint8Array))
|
|
495
|
+
return null;
|
|
496
|
+
const sequence = normalizeSequence(chunk.sequence, 'preparedHistory.chunk.sequence');
|
|
497
|
+
if (sequence <= previousChunkSequence || sequence > coveredThrough)
|
|
498
|
+
return null;
|
|
499
|
+
previousChunkSequence = sequence;
|
|
500
|
+
preparedBytes += chunk.data.byteLength;
|
|
501
|
+
}
|
|
502
|
+
if (preparedBytes !== preparedHistory.byteLength)
|
|
503
|
+
return null;
|
|
504
|
+
if (coveredThrough >= Math.max(1, firstRetained)
|
|
505
|
+
&& previousChunkSequence !== coveredThrough)
|
|
506
|
+
return null;
|
|
507
|
+
if (preparedHistory.complete !== (coveredThrough >= snapshotEnd))
|
|
508
|
+
return null;
|
|
509
|
+
if (firstRetained > expectedStart) {
|
|
510
|
+
this.options.clear?.();
|
|
511
|
+
this.options.onHistoryTruncated?.('history-evicted');
|
|
512
|
+
}
|
|
513
|
+
return preparedHistory;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
discardPreparedHistoryForFullRecovery() {
|
|
520
|
+
this.preparedHistory = null;
|
|
521
|
+
this.coveredThroughSequence = Math.max(0, this.recoveryStartSequence - 1);
|
|
522
|
+
this.scheduledThroughSequence = this.coveredThroughSequence;
|
|
523
|
+
this.historyRebasePrepared = false;
|
|
524
|
+
}
|
|
217
525
|
async runRecovery() {
|
|
218
526
|
if (this.disposed || this.recoveryRunning)
|
|
219
527
|
return;
|
|
@@ -230,13 +538,26 @@ class PagedTerminalOutputCoordinator {
|
|
|
230
538
|
await this.writeChain;
|
|
231
539
|
if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
|
|
232
540
|
return;
|
|
233
|
-
const
|
|
541
|
+
const preparedHistory = this.preparedHistory;
|
|
542
|
+
const historyChunks = preparedHistory
|
|
543
|
+
? preparedHistory.chunks.map(item => ({ ...item, data: new Uint8Array(item.data), source: 'history' }))
|
|
544
|
+
: [];
|
|
234
545
|
let cursor;
|
|
235
|
-
let startSequence =
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
let
|
|
546
|
+
let startSequence = preparedHistory
|
|
547
|
+
? Math.max(1, preparedHistory.coveredThroughSequence
|
|
548
|
+
+ (preparedHistory.coveredThroughSequence < (this.recoveryEndSequence ?? 0) ? 1 : 0))
|
|
549
|
+
: this.recoveryStartSequence;
|
|
550
|
+
let snapshotEnd = this.recoveryEndSequence;
|
|
551
|
+
let historyGeneration = preparedHistory?.historyGeneration;
|
|
552
|
+
let coveredEnd = preparedHistory?.coveredThroughSequence ?? this.coveredThroughSequence;
|
|
553
|
+
let firstPage = preparedHistory === null;
|
|
554
|
+
const preparedBaseline = preparedHistory === null
|
|
555
|
+
? undefined
|
|
556
|
+
: Math.max(Math.max(1, this.recoveryStartSequence) - 1, preparedHistory.firstRetainedSequence - 1);
|
|
557
|
+
if (preparedBaseline !== undefined) {
|
|
558
|
+
this.coveredThroughSequence = preparedBaseline;
|
|
559
|
+
this.scheduledThroughSequence = preparedBaseline;
|
|
560
|
+
}
|
|
240
561
|
do {
|
|
241
562
|
const page = await this.options.fetchPage({
|
|
242
563
|
startSequence,
|
|
@@ -250,29 +571,78 @@ class PagedTerminalOutputCoordinator {
|
|
|
250
571
|
const pageSnapshotEnd = normalizeSequence(page.snapshotEndSequence, 'snapshotEndSequence', true);
|
|
251
572
|
const pageGeneration = normalizeSequence(page.historyGeneration, 'historyGeneration', true);
|
|
252
573
|
const firstRetained = normalizeSequence(page.firstRetainedSequence ?? page.firstAvailableSequence, 'firstRetainedSequence', true);
|
|
574
|
+
if (preparedHistory !== null
|
|
575
|
+
&& (pageSnapshotEnd === undefined || pageGeneration === undefined || firstRetained === undefined)) {
|
|
576
|
+
this.discardPreparedHistoryForFullRecovery();
|
|
577
|
+
this.recoveryRunning = false;
|
|
578
|
+
await this.runRecovery();
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
253
581
|
const generationChanged = historyGeneration !== undefined
|
|
254
582
|
&& pageGeneration !== undefined
|
|
255
583
|
&& pageGeneration !== historyGeneration;
|
|
256
584
|
const effectivePageStart = Math.max(1, startSequence);
|
|
257
585
|
const retentionAdvanced = firstRetained !== undefined
|
|
258
|
-
&& firstRetained > effectivePageStart
|
|
586
|
+
&& (firstRetained > effectivePageStart
|
|
587
|
+
|| (preparedHistory !== null && firstRetained > preparedHistory.firstRetainedSequence));
|
|
259
588
|
const generationNeedsRebase = page.historyReset || generationChanged;
|
|
260
589
|
const retentionNeedsRebase = page.historyTruncated || retentionAdvanced;
|
|
590
|
+
const fencedRangeFullyEvicted = this.recoveryEndSequence !== undefined
|
|
591
|
+
&& firstRetained !== undefined
|
|
592
|
+
&& firstRetained > this.recoveryEndSequence;
|
|
261
593
|
if (!this.historyRebasePrepared && (generationNeedsRebase || retentionNeedsRebase)) {
|
|
594
|
+
this.preparedHistory = null;
|
|
595
|
+
if (fencedRangeFullyEvicted) {
|
|
596
|
+
this.options.clear?.();
|
|
597
|
+
this.options.onHistoryTruncated?.('history-evicted');
|
|
598
|
+
if (preparedHistory !== null)
|
|
599
|
+
historyChunks.length = 0;
|
|
600
|
+
if (generationNeedsRebase) {
|
|
601
|
+
this.historyRebasePrepared = true;
|
|
602
|
+
this.recoveryRunning = false;
|
|
603
|
+
await this.runRecovery();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
this.prepareHistoryGenerationRebase(firstRetained);
|
|
609
|
+
this.recoveryRunning = false;
|
|
610
|
+
await this.runRecovery();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (this.historyRebasePrepared && generationNeedsRebase) {
|
|
615
|
+
throw new HistoryContractError('history_contract_invalid', 'history generation reset persisted after rebase');
|
|
616
|
+
}
|
|
617
|
+
let coverage;
|
|
618
|
+
try {
|
|
619
|
+
coverage = this.validatePage(page, coveredEnd);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
if (preparedHistory === null)
|
|
623
|
+
throw error;
|
|
624
|
+
this.preparedHistory = null;
|
|
262
625
|
this.prepareHistoryGenerationRebase(firstRetained);
|
|
263
626
|
this.recoveryRunning = false;
|
|
264
627
|
await this.runRecovery();
|
|
265
628
|
return;
|
|
266
629
|
}
|
|
267
|
-
if (
|
|
268
|
-
throw new HistoryContractError('history_contract_invalid', '
|
|
630
|
+
if (snapshotEnd !== undefined && coverage > snapshotEnd) {
|
|
631
|
+
throw new HistoryContractError('history_contract_invalid', 'coveredThroughSequence exceeded the recovery fence');
|
|
269
632
|
}
|
|
270
|
-
const coverage = this.validatePage(page, coveredEnd);
|
|
271
633
|
if (firstPage) {
|
|
272
|
-
|
|
634
|
+
if (this.recoveryEndSequence !== undefined) {
|
|
635
|
+
if (pageSnapshotEnd !== undefined && pageSnapshotEnd !== this.recoveryEndSequence) {
|
|
636
|
+
throw new HistoryContractError('history_contract_invalid', 'snapshotEndSequence does not match the requested recovery fence');
|
|
637
|
+
}
|
|
638
|
+
snapshotEnd = this.recoveryEndSequence;
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
snapshotEnd = pageSnapshotEnd;
|
|
642
|
+
}
|
|
273
643
|
historyGeneration = pageGeneration;
|
|
274
644
|
const effectiveStart = Math.max(1, startSequence);
|
|
275
|
-
if (firstRetained !== undefined && firstRetained > effectiveStart) {
|
|
645
|
+
if (!fencedRangeFullyEvicted && firstRetained !== undefined && firstRetained > effectiveStart) {
|
|
276
646
|
if (!this.historyRebasePrepared) {
|
|
277
647
|
this.options.clear?.();
|
|
278
648
|
this.options.onHistoryTruncated?.('history-evicted');
|
|
@@ -284,6 +654,13 @@ class PagedTerminalOutputCoordinator {
|
|
|
284
654
|
}
|
|
285
655
|
else {
|
|
286
656
|
if (snapshotEnd !== undefined && pageSnapshotEnd !== undefined && pageSnapshotEnd !== snapshotEnd) {
|
|
657
|
+
if (preparedHistory !== null) {
|
|
658
|
+
this.preparedHistory = null;
|
|
659
|
+
this.prepareHistoryGenerationRebase(firstRetained);
|
|
660
|
+
this.recoveryRunning = false;
|
|
661
|
+
await this.runRecovery();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
287
664
|
throw new HistoryContractError('history_contract_invalid', 'snapshotEndSequence changed during pagination');
|
|
288
665
|
}
|
|
289
666
|
}
|
|
@@ -300,6 +677,9 @@ class PagedTerminalOutputCoordinator {
|
|
|
300
677
|
} while (!controller.signal.aborted);
|
|
301
678
|
if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
|
|
302
679
|
return;
|
|
680
|
+
if (this.recoveryEndSequence !== undefined && coveredEnd < this.recoveryEndSequence) {
|
|
681
|
+
throw new HistoryContractError('history_coverage_incomplete', 'terminal history coverage did not reach the recovery fence');
|
|
682
|
+
}
|
|
303
683
|
if (this.needsRebase) {
|
|
304
684
|
this.prepareRetainedLiveRebase();
|
|
305
685
|
this.recoveryRunning = false;
|
|
@@ -332,6 +712,7 @@ class PagedTerminalOutputCoordinator {
|
|
|
332
712
|
this.retryAttempt = 0;
|
|
333
713
|
this.lastError = null;
|
|
334
714
|
this.failure = null;
|
|
715
|
+
this.preparedHistory = null;
|
|
335
716
|
this.setState('live');
|
|
336
717
|
this.recoveryRunning = false;
|
|
337
718
|
this.drainRetainedLive();
|
|
@@ -380,8 +761,8 @@ class PagedTerminalOutputCoordinator {
|
|
|
380
761
|
for (const item of this.retainedLive) {
|
|
381
762
|
const sequence = this.chunkSequence(item);
|
|
382
763
|
if (sequence !== undefined && sequence <= coveredEnd) {
|
|
383
|
-
|
|
384
|
-
|
|
764
|
+
// A live copy preserves terminal query/response semantics that history replay suppresses.
|
|
765
|
+
selected.set(sequence, item);
|
|
385
766
|
}
|
|
386
767
|
else {
|
|
387
768
|
remaining.push(item);
|
|
@@ -467,6 +848,10 @@ class PagedTerminalOutputCoordinator {
|
|
|
467
848
|
if (!removed)
|
|
468
849
|
break;
|
|
469
850
|
this.retainedLiveBytes -= removed.data.byteLength;
|
|
851
|
+
const removedSequence = this.chunkSequence(removed);
|
|
852
|
+
if (removedSequence !== undefined) {
|
|
853
|
+
this.droppedLiveThroughSequence = Math.max(this.droppedLiveThroughSequence, removedSequence);
|
|
854
|
+
}
|
|
470
855
|
this.needsRebase = true;
|
|
471
856
|
}
|
|
472
857
|
this.emitState();
|
|
@@ -476,6 +861,9 @@ class PagedTerminalOutputCoordinator {
|
|
|
476
861
|
this.historyRebasePrepared = false;
|
|
477
862
|
this.recoveryKind = this.baselineReady ? 'catch-up' : 'initial';
|
|
478
863
|
this.recoveryStartSequence = 0;
|
|
864
|
+
const firstRetainedSequence = this.firstRetainedLiveSequence();
|
|
865
|
+
this.recoveryEndSequence = Math.max(this.droppedLiveThroughSequence, firstRetainedSequence === undefined ? 0 : firstRetainedSequence - 1) || undefined;
|
|
866
|
+
this.droppedLiveThroughSequence = 0;
|
|
479
867
|
this.coveredThroughSequence = 0;
|
|
480
868
|
this.scheduledThroughSequence = 0;
|
|
481
869
|
this.options.clear?.();
|
|
@@ -502,6 +890,14 @@ class PagedTerminalOutputCoordinator {
|
|
|
502
890
|
const sequence = this.chunkSequence(chunk);
|
|
503
891
|
if (sequence !== undefined && sequence <= this.scheduledThroughSequence)
|
|
504
892
|
return;
|
|
893
|
+
if (sequence !== undefined
|
|
894
|
+
&& this.explicitAttachFence
|
|
895
|
+
&& this.scheduledThroughSequence === 0
|
|
896
|
+
&& sequence > 1) {
|
|
897
|
+
this.retainLive(chunk);
|
|
898
|
+
this.beginCatchUp(1);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
505
901
|
if (sequence !== undefined
|
|
506
902
|
&& this.scheduledThroughSequence > 0
|
|
507
903
|
&& sequence > this.scheduledThroughSequence + 1) {
|