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