@floegence/floeterm-terminal-web 0.5.14 → 0.5.16
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 +22 -5
- package/dist/core/PagedTerminalOutputCoordinator.d.ts +29 -2
- package/dist/core/PagedTerminalOutputCoordinator.d.ts.map +1 -1
- package/dist/core/PagedTerminalOutputCoordinator.js +376 -134
- package/dist/core/PagedTerminalOutputCoordinator.js.map +1 -1
- package/dist/core/TerminalCore.d.ts +2 -0
- package/dist/core/TerminalCore.d.ts.map +1 -1
- package/dist/core/TerminalCore.js +9 -1
- package/dist/core/TerminalCore.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { createTerminalOutputPipeline, } from './TerminalOutputPipeline';
|
|
2
1
|
const DEFAULT_POLICY = {
|
|
3
2
|
maxRetainedLiveChunks: 2048,
|
|
4
3
|
maxRetainedLiveBytes: 8 * 1024 * 1024,
|
|
5
4
|
retryDelaysMs: [250, 1000, 4000],
|
|
5
|
+
maxWriteBatchBytes: 256 * 1024,
|
|
6
6
|
};
|
|
7
|
-
const normalizePositiveInteger = (value, fallback) => (typeof value === 'number' && Number.
|
|
8
|
-
?
|
|
7
|
+
const normalizePositiveInteger = (value, fallback) => (typeof value === 'number' && Number.isSafeInteger(value) && value > 0
|
|
8
|
+
? value
|
|
9
9
|
: fallback);
|
|
10
|
+
const normalizeSequence = (value, field, optional = false) => {
|
|
11
|
+
if (value === undefined && optional)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
14
|
+
throw new HistoryContractError('history_contract_invalid', `${field} must be a non-negative safe integer`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
10
18
|
const concatData = (chunks) => {
|
|
11
19
|
const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
12
20
|
const result = new Uint8Array(total);
|
|
@@ -17,22 +25,40 @@ const concatData = (chunks) => {
|
|
|
17
25
|
}
|
|
18
26
|
return result;
|
|
19
27
|
};
|
|
28
|
+
class HistoryContractError extends Error {
|
|
29
|
+
constructor(code, message, firstRetainedSequence) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'HistoryContractError';
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.firstRetainedSequence = firstRetainedSequence;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
20
36
|
class PagedTerminalOutputCoordinator {
|
|
21
37
|
constructor(options) {
|
|
22
38
|
this.active = true;
|
|
23
39
|
this.state = 'idle';
|
|
40
|
+
this.baselineReady = false;
|
|
24
41
|
this.coveredThroughSequence = 0;
|
|
42
|
+
this.scheduledThroughSequence = 0;
|
|
25
43
|
this.retainedLive = [];
|
|
26
44
|
this.retainedLiveBytes = 0;
|
|
27
45
|
this.retryAttempt = 0;
|
|
28
46
|
this.retryTimer = null;
|
|
29
47
|
this.abortController = null;
|
|
30
48
|
this.generation = 0;
|
|
49
|
+
this.recoverySerial = 0;
|
|
31
50
|
this.disposed = false;
|
|
32
51
|
this.lastError = null;
|
|
52
|
+
this.failure = null;
|
|
33
53
|
this.recoveryStartSequence = 1;
|
|
34
54
|
this.recoveryKind = 'initial';
|
|
55
|
+
this.recoveryRunning = false;
|
|
35
56
|
this.needsRebase = false;
|
|
57
|
+
this.historyRebasePrepared = false;
|
|
58
|
+
this.writeChain = Promise.resolve();
|
|
59
|
+
this.pendingLiveWrites = [];
|
|
60
|
+
this.liveWriteScheduled = false;
|
|
61
|
+
this.baselineWaiters = [];
|
|
36
62
|
this.options = options;
|
|
37
63
|
this.policy = {
|
|
38
64
|
maxRetainedLiveChunks: normalizePositiveInteger(options.policy?.maxRetainedLiveChunks, DEFAULT_POLICY.maxRetainedLiveChunks),
|
|
@@ -40,39 +66,46 @@ class PagedTerminalOutputCoordinator {
|
|
|
40
66
|
retryDelaysMs: options.policy?.retryDelaysMs !== undefined
|
|
41
67
|
? options.policy.retryDelaysMs.map(delay => Math.max(0, Math.floor(delay)))
|
|
42
68
|
: DEFAULT_POLICY.retryDelaysMs,
|
|
69
|
+
maxWriteBatchBytes: normalizePositiveInteger(options.policy?.maxWriteBatchBytes, DEFAULT_POLICY.maxWriteBatchBytes),
|
|
43
70
|
};
|
|
44
|
-
const pipelinePolicy = {
|
|
45
|
-
maxInactiveChunks: this.policy.maxRetainedLiveChunks,
|
|
46
|
-
maxInactiveBytes: this.policy.maxRetainedLiveBytes,
|
|
47
|
-
};
|
|
48
|
-
this.pipeline = createTerminalOutputPipeline({
|
|
49
|
-
write: (_data, chunks) => this.writeAcceptedChunks(chunks),
|
|
50
|
-
isInteractive: () => true,
|
|
51
|
-
policy: pipelinePolicy,
|
|
52
|
-
scheduler: options.scheduler,
|
|
53
|
-
});
|
|
54
71
|
}
|
|
55
72
|
async attach(startSequence = 1) {
|
|
56
73
|
if (this.disposed)
|
|
57
74
|
return;
|
|
58
75
|
this.cancelRecovery();
|
|
76
|
+
this.resolveBaselineWaiters();
|
|
59
77
|
this.retainedLive = [];
|
|
60
78
|
this.retainedLiveBytes = 0;
|
|
79
|
+
this.pendingLiveWrites = [];
|
|
80
|
+
this.liveWriteScheduled = false;
|
|
81
|
+
this.writeChain = Promise.resolve();
|
|
82
|
+
this.baselineReady = false;
|
|
61
83
|
this.coveredThroughSequence = Math.max(0, Math.floor(startSequence) - 1);
|
|
62
|
-
this.
|
|
84
|
+
this.scheduledThroughSequence = this.coveredThroughSequence;
|
|
63
85
|
this.recoveryKind = 'initial';
|
|
64
86
|
this.recoveryStartSequence = Math.max(0, Math.floor(startSequence));
|
|
65
87
|
this.retryAttempt = 0;
|
|
88
|
+
this.failure = null;
|
|
89
|
+
this.lastError = null;
|
|
90
|
+
this.historyRebasePrepared = false;
|
|
66
91
|
await this.runRecovery();
|
|
67
92
|
}
|
|
93
|
+
waitForBaseline() {
|
|
94
|
+
const snapshot = this.getSnapshot();
|
|
95
|
+
if (snapshot.baselineReady || snapshot.state === 'failed' || snapshot.disposed) {
|
|
96
|
+
return Promise.resolve(snapshot);
|
|
97
|
+
}
|
|
98
|
+
return new Promise(resolve => this.baselineWaiters.push(resolve));
|
|
99
|
+
}
|
|
68
100
|
pushLive(chunk) {
|
|
69
101
|
if (this.disposed)
|
|
70
102
|
return;
|
|
103
|
+
const tagged = { ...chunk, source: 'live' };
|
|
71
104
|
if (this.state !== 'live' || !this.canRenderLive()) {
|
|
72
|
-
this.retainLive(
|
|
105
|
+
this.retainLive(tagged);
|
|
73
106
|
return;
|
|
74
107
|
}
|
|
75
|
-
this.acceptLive(
|
|
108
|
+
this.acceptLive(tagged);
|
|
76
109
|
}
|
|
77
110
|
setActive(active) {
|
|
78
111
|
if (this.disposed)
|
|
@@ -80,16 +113,13 @@ class PagedTerminalOutputCoordinator {
|
|
|
80
113
|
this.active = active;
|
|
81
114
|
if (active) {
|
|
82
115
|
if (this.needsRebase) {
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.options.onHistoryTruncated?.('retained-live-overflow');
|
|
88
|
-
void this.runRecovery();
|
|
116
|
+
if (!this.recoveryRunning) {
|
|
117
|
+
this.prepareRetainedLiveRebase();
|
|
118
|
+
void this.runRecovery();
|
|
119
|
+
}
|
|
89
120
|
}
|
|
90
|
-
else {
|
|
91
|
-
this.drainRetainedLive(
|
|
92
|
-
this.pipeline.flush();
|
|
121
|
+
else if (this.state === 'live') {
|
|
122
|
+
this.drainRetainedLive();
|
|
93
123
|
}
|
|
94
124
|
}
|
|
95
125
|
this.emitState();
|
|
@@ -101,28 +131,39 @@ class PagedTerminalOutputCoordinator {
|
|
|
101
131
|
this.options.clear?.();
|
|
102
132
|
this.retainedLive = [];
|
|
103
133
|
this.retainedLiveBytes = 0;
|
|
134
|
+
this.pendingLiveWrites = [];
|
|
135
|
+
this.liveWriteScheduled = false;
|
|
136
|
+
this.writeChain = Promise.resolve();
|
|
137
|
+
this.baselineReady = false;
|
|
104
138
|
this.coveredThroughSequence = Math.max(0, Math.floor(startSequence) - 1);
|
|
105
|
-
this.
|
|
139
|
+
this.scheduledThroughSequence = this.coveredThroughSequence;
|
|
106
140
|
this.retryAttempt = 0;
|
|
141
|
+
this.failure = null;
|
|
107
142
|
this.lastError = null;
|
|
143
|
+
this.historyRebasePrepared = false;
|
|
108
144
|
this.setState('idle');
|
|
145
|
+
this.resolveBaselineWaiters();
|
|
109
146
|
}
|
|
110
147
|
retry() {
|
|
111
148
|
if (this.disposed || this.state !== 'failed')
|
|
112
149
|
return;
|
|
113
150
|
this.retryAttempt = 0;
|
|
151
|
+
this.failure = null;
|
|
114
152
|
void this.runRecovery();
|
|
115
153
|
}
|
|
116
154
|
getSnapshot() {
|
|
117
155
|
return {
|
|
118
156
|
state: this.state,
|
|
119
157
|
active: this.active,
|
|
158
|
+
baselineReady: this.baselineReady,
|
|
120
159
|
coveredThroughSequence: this.coveredThroughSequence,
|
|
121
|
-
retainedLiveChunks: this.retainedLive.length
|
|
122
|
-
retainedLiveBytes: this.retainedLiveBytes
|
|
160
|
+
retainedLiveChunks: this.retainedLive.length,
|
|
161
|
+
retainedLiveBytes: this.retainedLiveBytes,
|
|
123
162
|
retryAttempt: this.retryAttempt,
|
|
124
163
|
retryScheduled: this.retryTimer !== null,
|
|
164
|
+
failure: this.failure,
|
|
125
165
|
lastError: this.lastError,
|
|
166
|
+
attachGeneration: this.generation,
|
|
126
167
|
disposed: this.disposed,
|
|
127
168
|
};
|
|
128
169
|
}
|
|
@@ -133,11 +174,13 @@ class PagedTerminalOutputCoordinator {
|
|
|
133
174
|
this.cancelRecovery();
|
|
134
175
|
this.retainedLive = [];
|
|
135
176
|
this.retainedLiveBytes = 0;
|
|
136
|
-
this.
|
|
177
|
+
this.pendingLiveWrites = [];
|
|
178
|
+
this.liveWriteScheduled = false;
|
|
137
179
|
this.setState('disposed');
|
|
180
|
+
this.resolveBaselineWaiters();
|
|
138
181
|
}
|
|
139
182
|
beginCatchUp(startSequence) {
|
|
140
|
-
if (this.disposed || this.state === 'initial-replay')
|
|
183
|
+
if (this.disposed || this.recoveryRunning || this.state === 'initial-replay')
|
|
141
184
|
return;
|
|
142
185
|
this.recoveryKind = 'catch-up';
|
|
143
186
|
this.recoveryStartSequence = Math.max(0, Math.floor(startSequence));
|
|
@@ -145,99 +188,254 @@ class PagedTerminalOutputCoordinator {
|
|
|
145
188
|
void this.runRecovery();
|
|
146
189
|
}
|
|
147
190
|
async runRecovery() {
|
|
148
|
-
if (this.disposed)
|
|
191
|
+
if (this.disposed || this.recoveryRunning)
|
|
149
192
|
return;
|
|
150
193
|
this.cancelRecovery(false);
|
|
151
|
-
const generation =
|
|
194
|
+
const generation = this.generation;
|
|
195
|
+
const recoverySerial = ++this.recoverySerial;
|
|
152
196
|
const controller = new AbortController();
|
|
153
197
|
this.abortController = controller;
|
|
154
198
|
this.lastError = null;
|
|
199
|
+
this.failure = null;
|
|
200
|
+
this.recoveryRunning = true;
|
|
155
201
|
this.setState(this.recoveryKind === 'initial' ? 'initial-replay' : 'catching-up');
|
|
156
202
|
try {
|
|
157
|
-
|
|
203
|
+
await this.writeChain;
|
|
204
|
+
if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
|
|
205
|
+
return;
|
|
206
|
+
const historyChunks = [];
|
|
158
207
|
let cursor;
|
|
159
208
|
let startSequence = this.recoveryStartSequence;
|
|
209
|
+
let snapshotEnd;
|
|
210
|
+
let historyGeneration;
|
|
211
|
+
let coveredEnd = this.coveredThroughSequence;
|
|
160
212
|
let firstPage = true;
|
|
161
213
|
do {
|
|
162
214
|
const page = await this.options.fetchPage({
|
|
163
215
|
startSequence,
|
|
216
|
+
endSequence: snapshotEnd,
|
|
217
|
+
historyGeneration,
|
|
164
218
|
cursor,
|
|
165
219
|
signal: controller.signal,
|
|
166
220
|
});
|
|
167
|
-
if (this.
|
|
221
|
+
if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
|
|
222
|
+
return;
|
|
223
|
+
const pageSnapshotEnd = normalizeSequence(page.snapshotEndSequence, 'snapshotEndSequence', true);
|
|
224
|
+
const pageGeneration = normalizeSequence(page.historyGeneration, 'historyGeneration', true);
|
|
225
|
+
const firstRetained = normalizeSequence(page.firstRetainedSequence ?? page.firstAvailableSequence, 'firstRetainedSequence', true);
|
|
226
|
+
const generationChanged = historyGeneration !== undefined
|
|
227
|
+
&& pageGeneration !== undefined
|
|
228
|
+
&& pageGeneration !== historyGeneration;
|
|
229
|
+
const effectivePageStart = Math.max(1, startSequence);
|
|
230
|
+
const retentionAdvanced = firstRetained !== undefined
|
|
231
|
+
&& firstRetained > effectivePageStart;
|
|
232
|
+
const generationNeedsRebase = page.historyReset || generationChanged;
|
|
233
|
+
const retentionNeedsRebase = page.historyTruncated || retentionAdvanced;
|
|
234
|
+
if (!this.historyRebasePrepared && (generationNeedsRebase || retentionNeedsRebase)) {
|
|
235
|
+
this.prepareHistoryGenerationRebase(firstRetained);
|
|
236
|
+
this.recoveryRunning = false;
|
|
237
|
+
await this.runRecovery();
|
|
168
238
|
return;
|
|
169
|
-
const firstAvailable = page.firstAvailableSequence;
|
|
170
|
-
if (firstPage && firstAvailable && startSequence > 0 && firstAvailable > startSequence) {
|
|
171
|
-
this.options.clear?.();
|
|
172
|
-
this.options.onHistoryTruncated?.('history-evicted');
|
|
173
|
-
this.coveredThroughSequence = firstAvailable - 1;
|
|
174
|
-
this.pipeline.reset({ startSequence: firstAvailable });
|
|
175
239
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
240
|
+
if (this.historyRebasePrepared && generationNeedsRebase) {
|
|
241
|
+
throw new HistoryContractError('history_contract_invalid', 'history generation reset persisted after rebase');
|
|
242
|
+
}
|
|
243
|
+
const coverage = this.validatePage(page, coveredEnd);
|
|
244
|
+
if (firstPage) {
|
|
245
|
+
snapshotEnd = pageSnapshotEnd;
|
|
246
|
+
historyGeneration = pageGeneration;
|
|
247
|
+
const effectiveStart = Math.max(1, startSequence);
|
|
248
|
+
if (firstRetained !== undefined && firstRetained > effectiveStart) {
|
|
249
|
+
if (!this.historyRebasePrepared) {
|
|
250
|
+
this.options.clear?.();
|
|
251
|
+
this.options.onHistoryTruncated?.('history-evicted');
|
|
252
|
+
}
|
|
253
|
+
this.coveredThroughSequence = Math.max(0, (firstRetained ?? effectiveStart) - 1);
|
|
254
|
+
this.scheduledThroughSequence = this.coveredThroughSequence;
|
|
255
|
+
}
|
|
256
|
+
this.historyRebasePrepared = false;
|
|
181
257
|
}
|
|
182
|
-
|
|
183
|
-
|
|
258
|
+
else {
|
|
259
|
+
if (snapshotEnd !== undefined && pageSnapshotEnd !== undefined && pageSnapshotEnd !== snapshotEnd) {
|
|
260
|
+
throw new HistoryContractError('history_contract_invalid', 'snapshotEndSequence changed during pagination');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
firstPage = false;
|
|
264
|
+
historyChunks.push(...page.chunks.map(item => ({ ...item, source: 'history' })));
|
|
265
|
+
coveredEnd = Math.max(coveredEnd, coverage);
|
|
184
266
|
cursor = page.nextCursor;
|
|
185
|
-
startSequence = this.coveredThroughSequence + 1;
|
|
186
267
|
if (!page.hasMore)
|
|
187
268
|
break;
|
|
269
|
+
if (cursor === undefined) {
|
|
270
|
+
throw new HistoryContractError('history_contract_invalid', 'nextCursor is required when hasMore is true');
|
|
271
|
+
}
|
|
272
|
+
startSequence = coverage + 1;
|
|
188
273
|
} while (!controller.signal.aborted);
|
|
189
|
-
if (this.
|
|
274
|
+
if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
|
|
190
275
|
return;
|
|
191
276
|
if (this.needsRebase) {
|
|
192
|
-
this.
|
|
193
|
-
this.
|
|
194
|
-
this.pipeline.reset({ startSequence: 1 });
|
|
195
|
-
this.coveredThroughSequence = 0;
|
|
196
|
-
this.recoveryStartSequence = 0;
|
|
197
|
-
this.options.onHistoryTruncated?.('retained-live-overflow');
|
|
277
|
+
this.prepareRetainedLiveRebase();
|
|
278
|
+
this.recoveryRunning = false;
|
|
198
279
|
await this.runRecovery();
|
|
199
280
|
return;
|
|
200
281
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
282
|
+
const replayChunks = this.mergeRecoveryChunks(historyChunks, coveredEnd);
|
|
283
|
+
await this.writeOrdered(replayChunks, generation);
|
|
284
|
+
if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
|
|
285
|
+
return;
|
|
286
|
+
if (this.needsRebase) {
|
|
287
|
+
this.prepareRetainedLiveRebase();
|
|
288
|
+
this.recoveryRunning = false;
|
|
289
|
+
await this.runRecovery();
|
|
290
|
+
return;
|
|
210
291
|
}
|
|
292
|
+
this.coveredThroughSequence = Math.max(this.coveredThroughSequence, coveredEnd);
|
|
293
|
+
this.scheduledThroughSequence = Math.max(this.scheduledThroughSequence, coveredEnd);
|
|
294
|
+
const firstRetainedSequence = this.firstRetainedLiveSequence();
|
|
211
295
|
if (this.recoveryKind === 'catch-up'
|
|
212
|
-
&& firstRetainedSequence
|
|
213
|
-
&& firstRetainedSequence >
|
|
214
|
-
throw new
|
|
296
|
+
&& firstRetainedSequence !== undefined
|
|
297
|
+
&& firstRetainedSequence > coveredEnd + 1) {
|
|
298
|
+
throw new HistoryContractError('history_coverage_incomplete', 'terminal history coverage has not reached retained live output', firstRetainedSequence);
|
|
299
|
+
}
|
|
300
|
+
if (this.recoveryKind === 'initial' && !this.baselineReady) {
|
|
301
|
+
this.baselineReady = true;
|
|
302
|
+
this.resolveBaselineWaiters();
|
|
303
|
+
this.emitState();
|
|
215
304
|
}
|
|
216
|
-
this.pipeline.reset();
|
|
217
305
|
this.retryAttempt = 0;
|
|
218
306
|
this.lastError = null;
|
|
307
|
+
this.failure = null;
|
|
219
308
|
this.setState('live');
|
|
220
|
-
this.
|
|
221
|
-
this.
|
|
309
|
+
this.recoveryRunning = false;
|
|
310
|
+
this.drainRetainedLive();
|
|
222
311
|
}
|
|
223
312
|
catch (error) {
|
|
224
|
-
if (
|
|
313
|
+
if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
|
|
225
314
|
return;
|
|
226
315
|
this.lastError = error;
|
|
316
|
+
const contract = error instanceof HistoryContractError ? error : null;
|
|
317
|
+
this.failure = {
|
|
318
|
+
code: contract?.code ?? 'history_fetch_failed',
|
|
319
|
+
phase: this.recoveryKind === 'initial' ? 'initial' : 'catch_up',
|
|
320
|
+
retryable: contract?.code !== 'history_contract_missing' && contract?.code !== 'history_contract_invalid',
|
|
321
|
+
attempt: this.retryAttempt,
|
|
322
|
+
coveredSequence: this.coveredThroughSequence,
|
|
323
|
+
firstRetainedSequence: contract?.firstRetainedSequence,
|
|
324
|
+
attachGeneration: generation,
|
|
325
|
+
cause: error,
|
|
326
|
+
};
|
|
327
|
+
this.recoveryRunning = false;
|
|
227
328
|
this.scheduleRetry();
|
|
228
329
|
}
|
|
229
330
|
}
|
|
331
|
+
validatePage(page, previousCoverage) {
|
|
332
|
+
if (!Object.prototype.hasOwnProperty.call(page, 'coveredThroughSequence')) {
|
|
333
|
+
throw new HistoryContractError('history_contract_missing', 'coveredThroughSequence is required');
|
|
334
|
+
}
|
|
335
|
+
const coverage = normalizeSequence(page.coveredThroughSequence, 'coveredThroughSequence');
|
|
336
|
+
if (coverage < previousCoverage) {
|
|
337
|
+
throw new HistoryContractError('history_contract_invalid', 'coveredThroughSequence regressed');
|
|
338
|
+
}
|
|
339
|
+
return coverage;
|
|
340
|
+
}
|
|
341
|
+
mergeRecoveryChunks(history, coveredEnd) {
|
|
342
|
+
const selected = new Map();
|
|
343
|
+
const unsequenced = [];
|
|
344
|
+
for (const item of history) {
|
|
345
|
+
const sequence = this.chunkSequence(item);
|
|
346
|
+
if (sequence === undefined)
|
|
347
|
+
unsequenced.push(item);
|
|
348
|
+
else if (sequence > this.coveredThroughSequence && sequence <= coveredEnd)
|
|
349
|
+
selected.set(sequence, item);
|
|
350
|
+
}
|
|
351
|
+
const remaining = [];
|
|
352
|
+
let remainingBytes = 0;
|
|
353
|
+
for (const item of this.retainedLive) {
|
|
354
|
+
const sequence = this.chunkSequence(item);
|
|
355
|
+
if (sequence !== undefined && sequence <= coveredEnd) {
|
|
356
|
+
if (!selected.has(sequence))
|
|
357
|
+
selected.set(sequence, item);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
remaining.push(item);
|
|
361
|
+
remainingBytes += item.data.byteLength;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
this.retainedLive = remaining;
|
|
365
|
+
this.retainedLiveBytes = remainingBytes;
|
|
366
|
+
return [
|
|
367
|
+
...[...selected.entries()].sort(([left], [right]) => left - right).map(([, item]) => item),
|
|
368
|
+
...unsequenced,
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
async writeOrdered(chunks, generation) {
|
|
372
|
+
let batch = [];
|
|
373
|
+
let batchBytes = 0;
|
|
374
|
+
let batchSource;
|
|
375
|
+
const flush = async () => {
|
|
376
|
+
if (batch.length === 0 || !this.isCurrent(generation))
|
|
377
|
+
return;
|
|
378
|
+
const current = batch;
|
|
379
|
+
const source = batchSource;
|
|
380
|
+
batch = [];
|
|
381
|
+
batchBytes = 0;
|
|
382
|
+
batchSource = undefined;
|
|
383
|
+
const accepted = [];
|
|
384
|
+
const data = [];
|
|
385
|
+
for (const item of current) {
|
|
386
|
+
const transformed = this.options.transformChunk ? this.options.transformChunk(item) : item.data;
|
|
387
|
+
if (transformed === null)
|
|
388
|
+
continue;
|
|
389
|
+
accepted.push({ ...item, data: transformed });
|
|
390
|
+
data.push(transformed);
|
|
391
|
+
}
|
|
392
|
+
if (accepted.length === 0 || !this.isCurrent(generation))
|
|
393
|
+
return;
|
|
394
|
+
const writer = source === 'history' ? (this.options.writeHistory ?? this.options.write) : this.options.write;
|
|
395
|
+
await writer(concatData(data), accepted);
|
|
396
|
+
if (!this.isCurrent(generation))
|
|
397
|
+
return;
|
|
398
|
+
for (const item of accepted) {
|
|
399
|
+
const sequence = this.chunkSequence(item);
|
|
400
|
+
if (sequence !== undefined) {
|
|
401
|
+
this.coveredThroughSequence = Math.max(this.coveredThroughSequence, sequence);
|
|
402
|
+
this.scheduledThroughSequence = Math.max(this.scheduledThroughSequence, sequence);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
for (const item of chunks) {
|
|
407
|
+
const source = item.source ?? 'live';
|
|
408
|
+
if (batch.length > 0 && (source !== batchSource || batchBytes + item.data.byteLength > this.policy.maxWriteBatchBytes)) {
|
|
409
|
+
await flush();
|
|
410
|
+
}
|
|
411
|
+
batchSource = source;
|
|
412
|
+
batch.push(item);
|
|
413
|
+
batchBytes += item.data.byteLength;
|
|
414
|
+
}
|
|
415
|
+
await flush();
|
|
416
|
+
}
|
|
230
417
|
scheduleRetry() {
|
|
418
|
+
if (this.failure && !this.failure.retryable) {
|
|
419
|
+
this.setState('failed');
|
|
420
|
+
if (!this.baselineReady)
|
|
421
|
+
this.resolveBaselineWaiters();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
231
424
|
const delay = this.policy.retryDelaysMs[this.retryAttempt];
|
|
232
425
|
if (delay === undefined) {
|
|
233
426
|
this.setState('failed');
|
|
427
|
+
if (!this.baselineReady)
|
|
428
|
+
this.resolveBaselineWaiters();
|
|
234
429
|
return;
|
|
235
430
|
}
|
|
236
431
|
this.retryAttempt += 1;
|
|
237
432
|
this.setState('retry-wait');
|
|
433
|
+
const generation = this.generation;
|
|
238
434
|
const setTimer = this.options.scheduler?.setTimer ?? setTimeout;
|
|
239
435
|
this.retryTimer = setTimer(() => {
|
|
240
436
|
this.retryTimer = null;
|
|
437
|
+
if (!this.isCurrent(generation))
|
|
438
|
+
return;
|
|
241
439
|
void this.runRecovery();
|
|
242
440
|
}, delay);
|
|
243
441
|
this.emitState();
|
|
@@ -255,93 +453,129 @@ class PagedTerminalOutputCoordinator {
|
|
|
255
453
|
}
|
|
256
454
|
this.emitState();
|
|
257
455
|
}
|
|
456
|
+
prepareRetainedLiveRebase() {
|
|
457
|
+
this.needsRebase = false;
|
|
458
|
+
this.historyRebasePrepared = false;
|
|
459
|
+
this.recoveryKind = this.baselineReady ? 'catch-up' : 'initial';
|
|
460
|
+
this.recoveryStartSequence = 0;
|
|
461
|
+
this.coveredThroughSequence = 0;
|
|
462
|
+
this.scheduledThroughSequence = 0;
|
|
463
|
+
this.options.clear?.();
|
|
464
|
+
this.options.onHistoryTruncated?.('retained-live-overflow');
|
|
465
|
+
}
|
|
466
|
+
prepareHistoryGenerationRebase(firstRetainedSequence) {
|
|
467
|
+
this.recoveryKind = this.baselineReady ? 'catch-up' : 'initial';
|
|
468
|
+
this.recoveryStartSequence = 0;
|
|
469
|
+
this.coveredThroughSequence = 0;
|
|
470
|
+
this.scheduledThroughSequence = 0;
|
|
471
|
+
this.options.clear?.();
|
|
472
|
+
this.options.onHistoryTruncated?.('history-evicted');
|
|
473
|
+
this.historyRebasePrepared = true;
|
|
474
|
+
if (firstRetainedSequence !== undefined && firstRetainedSequence > 1) {
|
|
475
|
+
this.recoveryStartSequence = firstRetainedSequence;
|
|
476
|
+
this.coveredThroughSequence = firstRetainedSequence - 1;
|
|
477
|
+
this.scheduledThroughSequence = this.coveredThroughSequence;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
258
480
|
canRenderLive() {
|
|
259
481
|
return this.active && (this.options.isInteractive?.() ?? true);
|
|
260
482
|
}
|
|
261
483
|
acceptLive(chunk) {
|
|
262
|
-
const sequence =
|
|
263
|
-
|
|
264
|
-
: undefined;
|
|
265
|
-
if (sequence && sequence <= this.coveredThroughSequence) {
|
|
484
|
+
const sequence = this.chunkSequence(chunk);
|
|
485
|
+
if (sequence !== undefined && sequence <= this.scheduledThroughSequence)
|
|
266
486
|
return;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
this.
|
|
270
|
-
this.retainLive(
|
|
271
|
-
this.beginCatchUp(this.
|
|
487
|
+
if (sequence !== undefined
|
|
488
|
+
&& this.scheduledThroughSequence > 0
|
|
489
|
+
&& sequence > this.scheduledThroughSequence + 1) {
|
|
490
|
+
this.retainLive(chunk);
|
|
491
|
+
this.beginCatchUp(this.scheduledThroughSequence + 1);
|
|
272
492
|
return;
|
|
273
493
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const sequence = chunk.sequence;
|
|
278
|
-
const pipelineChunk = {
|
|
279
|
-
...chunk,
|
|
280
|
-
sequence: undefined,
|
|
281
|
-
coordinatorSequence: sequence,
|
|
282
|
-
};
|
|
283
|
-
this.pipeline.enqueue(pipelineChunk);
|
|
284
|
-
if (sequence) {
|
|
285
|
-
this.coveredThroughSequence = Math.max(this.coveredThroughSequence, sequence);
|
|
494
|
+
if (sequence !== undefined && this.scheduledThroughSequence === 0) {
|
|
495
|
+
this.coveredThroughSequence = sequence - 1;
|
|
496
|
+
this.scheduledThroughSequence = sequence - 1;
|
|
286
497
|
}
|
|
498
|
+
this.enqueueLiveWrite(chunk);
|
|
499
|
+
}
|
|
500
|
+
enqueueLiveWrite(chunk) {
|
|
501
|
+
const generation = this.generation;
|
|
502
|
+
const sequence = this.chunkSequence(chunk);
|
|
503
|
+
if (sequence !== undefined)
|
|
504
|
+
this.scheduledThroughSequence = Math.max(this.scheduledThroughSequence, sequence);
|
|
505
|
+
this.pendingLiveWrites.push(chunk);
|
|
506
|
+
if (this.liveWriteScheduled)
|
|
507
|
+
return;
|
|
508
|
+
this.liveWriteScheduled = true;
|
|
509
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
510
|
+
if (!this.isCurrent(generation))
|
|
511
|
+
return;
|
|
512
|
+
await Promise.resolve();
|
|
513
|
+
if (!this.isCurrent(generation))
|
|
514
|
+
return;
|
|
515
|
+
const pending = this.pendingLiveWrites.splice(0);
|
|
516
|
+
this.liveWriteScheduled = false;
|
|
517
|
+
await this.writeOrdered(pending, generation);
|
|
518
|
+
}).catch(error => {
|
|
519
|
+
this.liveWriteScheduled = false;
|
|
520
|
+
if (!this.isCurrent(generation))
|
|
521
|
+
return;
|
|
522
|
+
this.lastError = error;
|
|
523
|
+
this.failure = {
|
|
524
|
+
code: 'history_fetch_failed',
|
|
525
|
+
phase: 'catch_up',
|
|
526
|
+
retryable: true,
|
|
527
|
+
attempt: this.retryAttempt,
|
|
528
|
+
coveredSequence: this.coveredThroughSequence,
|
|
529
|
+
attachGeneration: generation,
|
|
530
|
+
cause: error,
|
|
531
|
+
};
|
|
532
|
+
this.setState('failed');
|
|
533
|
+
});
|
|
287
534
|
}
|
|
288
|
-
drainRetainedLive(
|
|
289
|
-
if (!this.canRenderLive() || this.retainedLive.length === 0)
|
|
535
|
+
drainRetainedLive() {
|
|
536
|
+
if (!this.canRenderLive() || this.retainedLive.length === 0 || this.state !== 'live')
|
|
290
537
|
return;
|
|
291
|
-
const retained = [...this.retainedLive].sort((left, right) => ((left
|
|
538
|
+
const retained = [...this.retainedLive].sort((left, right) => ((this.chunkSequence(left) ?? Number.MAX_SAFE_INTEGER)
|
|
539
|
+
- (this.chunkSequence(right) ?? Number.MAX_SAFE_INTEGER)));
|
|
292
540
|
this.retainedLive = [];
|
|
293
541
|
this.retainedLiveBytes = 0;
|
|
294
|
-
const firstSequence = retained.find(chunk => chunk.sequence)?.sequence;
|
|
295
|
-
if (this.coveredThroughSequence === 0 && firstSequence && replayedSequences.size === 0) {
|
|
296
|
-
this.coveredThroughSequence = firstSequence - 1;
|
|
297
|
-
}
|
|
298
542
|
for (let index = 0; index < retained.length; index += 1) {
|
|
299
|
-
|
|
300
|
-
if (chunk.sequence && replayedSequences.has(chunk.sequence))
|
|
301
|
-
continue;
|
|
302
|
-
if (chunk.sequence && chunk.sequence <= this.coveredThroughSequence) {
|
|
303
|
-
this.enqueueForRender(chunk);
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
this.acceptLive(chunk);
|
|
543
|
+
this.acceptLive(retained[index]);
|
|
307
544
|
if (this.state !== 'live') {
|
|
308
|
-
for (const pending of retained.slice(index + 1))
|
|
545
|
+
for (const pending of retained.slice(index + 1))
|
|
309
546
|
this.retainLive(pending);
|
|
310
|
-
}
|
|
311
547
|
break;
|
|
312
548
|
}
|
|
313
549
|
}
|
|
314
550
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
: chunk.data;
|
|
328
|
-
if (transformed === null)
|
|
329
|
-
continue;
|
|
330
|
-
acceptedChunks.push({ ...chunk, data: transformed });
|
|
331
|
-
data.push(transformed);
|
|
332
|
-
}
|
|
333
|
-
if (acceptedChunks.length > 0) {
|
|
334
|
-
this.options.write(concatData(data), acceptedChunks);
|
|
335
|
-
for (const chunk of acceptedChunks) {
|
|
336
|
-
if (chunk.sequence) {
|
|
337
|
-
this.coveredThroughSequence = Math.max(this.coveredThroughSequence, chunk.sequence);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
551
|
+
chunkSequence(chunk) {
|
|
552
|
+
const sequence = chunk.sequence;
|
|
553
|
+
return typeof sequence === 'number' && Number.isSafeInteger(sequence) && sequence > 0
|
|
554
|
+
? sequence
|
|
555
|
+
: undefined;
|
|
556
|
+
}
|
|
557
|
+
firstRetainedLiveSequence() {
|
|
558
|
+
let first;
|
|
559
|
+
for (const item of this.retainedLive) {
|
|
560
|
+
const sequence = this.chunkSequence(item);
|
|
561
|
+
if (sequence !== undefined && (first === undefined || sequence < first))
|
|
562
|
+
first = sequence;
|
|
340
563
|
}
|
|
564
|
+
return first;
|
|
565
|
+
}
|
|
566
|
+
isCurrent(generation) {
|
|
567
|
+
return !this.disposed && generation === this.generation;
|
|
568
|
+
}
|
|
569
|
+
isRecoveryCurrent(generation, recoverySerial, controller) {
|
|
570
|
+
return this.isCurrent(generation)
|
|
571
|
+
&& recoverySerial === this.recoverySerial
|
|
572
|
+
&& !controller.signal.aborted;
|
|
341
573
|
}
|
|
342
574
|
cancelRecovery(incrementGeneration = true) {
|
|
343
575
|
this.abortController?.abort();
|
|
344
576
|
this.abortController = null;
|
|
577
|
+
this.recoveryRunning = false;
|
|
578
|
+
this.recoverySerial += 1;
|
|
345
579
|
if (this.retryTimer !== null) {
|
|
346
580
|
const clearTimer = this.options.scheduler?.clearTimer ?? clearTimeout;
|
|
347
581
|
clearTimer(this.retryTimer);
|
|
@@ -350,6 +584,14 @@ class PagedTerminalOutputCoordinator {
|
|
|
350
584
|
if (incrementGeneration)
|
|
351
585
|
this.generation += 1;
|
|
352
586
|
}
|
|
587
|
+
resolveBaselineWaiters() {
|
|
588
|
+
if (this.baselineWaiters.length === 0)
|
|
589
|
+
return;
|
|
590
|
+
const snapshot = this.getSnapshot();
|
|
591
|
+
const waiters = this.baselineWaiters.splice(0);
|
|
592
|
+
for (const resolve of waiters)
|
|
593
|
+
resolve(snapshot);
|
|
594
|
+
}
|
|
353
595
|
setState(state) {
|
|
354
596
|
this.state = state;
|
|
355
597
|
this.emitState();
|