@floegence/floeterm-terminal-web 0.5.13 → 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 +348 -120
- 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/core/TerminalOutputPipeline.d.ts +1 -0
- package/dist/core/TerminalOutputPipeline.d.ts.map +1 -1
- package/dist/core/TerminalOutputPipeline.js +24 -7
- package/dist/core/TerminalOutputPipeline.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,84 +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))
|
|
255
|
+
return;
|
|
256
|
+
if (this.needsRebase) {
|
|
257
|
+
this.prepareRetainedLiveRebase();
|
|
258
|
+
this.recoveryRunning = false;
|
|
259
|
+
await this.runRecovery();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const replayChunks = this.mergeRecoveryChunks(historyChunks, coveredEnd);
|
|
263
|
+
await this.writeOrdered(replayChunks, generation);
|
|
264
|
+
if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
|
|
190
265
|
return;
|
|
191
266
|
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');
|
|
267
|
+
this.prepareRetainedLiveRebase();
|
|
268
|
+
this.recoveryRunning = false;
|
|
198
269
|
await this.runRecovery();
|
|
199
270
|
return;
|
|
200
271
|
}
|
|
201
|
-
this.
|
|
272
|
+
this.coveredThroughSequence = Math.max(this.coveredThroughSequence, coveredEnd);
|
|
273
|
+
this.scheduledThroughSequence = Math.max(this.scheduledThroughSequence, coveredEnd);
|
|
274
|
+
const firstRetainedSequence = this.firstRetainedLiveSequence();
|
|
275
|
+
if (this.recoveryKind === 'catch-up'
|
|
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();
|
|
284
|
+
}
|
|
202
285
|
this.retryAttempt = 0;
|
|
203
286
|
this.lastError = null;
|
|
287
|
+
this.failure = null;
|
|
204
288
|
this.setState('live');
|
|
205
|
-
this.
|
|
206
|
-
this.
|
|
289
|
+
this.recoveryRunning = false;
|
|
290
|
+
this.drainRetainedLive();
|
|
207
291
|
}
|
|
208
292
|
catch (error) {
|
|
209
|
-
if (
|
|
293
|
+
if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
|
|
210
294
|
return;
|
|
211
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;
|
|
212
308
|
this.scheduleRetry();
|
|
213
309
|
}
|
|
214
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
|
+
}
|
|
215
397
|
scheduleRetry() {
|
|
398
|
+
if (this.failure && !this.failure.retryable) {
|
|
399
|
+
this.setState('failed');
|
|
400
|
+
if (!this.baselineReady)
|
|
401
|
+
this.resolveBaselineWaiters();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
216
404
|
const delay = this.policy.retryDelaysMs[this.retryAttempt];
|
|
217
405
|
if (delay === undefined) {
|
|
218
406
|
this.setState('failed');
|
|
407
|
+
if (!this.baselineReady)
|
|
408
|
+
this.resolveBaselineWaiters();
|
|
219
409
|
return;
|
|
220
410
|
}
|
|
221
411
|
this.retryAttempt += 1;
|
|
222
412
|
this.setState('retry-wait');
|
|
413
|
+
const generation = this.generation;
|
|
223
414
|
const setTimer = this.options.scheduler?.setTimer ?? setTimeout;
|
|
224
415
|
this.retryTimer = setTimer(() => {
|
|
225
416
|
this.retryTimer = null;
|
|
417
|
+
if (!this.isCurrent(generation))
|
|
418
|
+
return;
|
|
226
419
|
void this.runRecovery();
|
|
227
420
|
}, delay);
|
|
228
421
|
this.emitState();
|
|
@@ -240,87 +433,114 @@ class PagedTerminalOutputCoordinator {
|
|
|
240
433
|
}
|
|
241
434
|
this.emitState();
|
|
242
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
|
+
}
|
|
243
445
|
canRenderLive() {
|
|
244
446
|
return this.active && (this.options.isInteractive?.() ?? true);
|
|
245
447
|
}
|
|
246
448
|
acceptLive(chunk) {
|
|
247
|
-
const sequence =
|
|
248
|
-
|
|
249
|
-
: undefined;
|
|
250
|
-
if (sequence && sequence <= this.coveredThroughSequence) {
|
|
449
|
+
const sequence = this.chunkSequence(chunk);
|
|
450
|
+
if (sequence !== undefined && sequence <= this.scheduledThroughSequence)
|
|
251
451
|
return;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
this.
|
|
255
|
-
this.
|
|
452
|
+
if (sequence !== undefined
|
|
453
|
+
&& this.scheduledThroughSequence > 0
|
|
454
|
+
&& sequence > this.scheduledThroughSequence + 1) {
|
|
455
|
+
this.retainLive(chunk);
|
|
456
|
+
this.beginCatchUp(this.scheduledThroughSequence + 1);
|
|
256
457
|
return;
|
|
257
458
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const sequence = chunk.sequence;
|
|
262
|
-
const pipelineChunk = {
|
|
263
|
-
...chunk,
|
|
264
|
-
sequence: undefined,
|
|
265
|
-
coordinatorSequence: sequence,
|
|
266
|
-
};
|
|
267
|
-
this.pipeline.enqueue(pipelineChunk);
|
|
268
|
-
if (sequence) {
|
|
269
|
-
this.coveredThroughSequence = Math.max(this.coveredThroughSequence, sequence);
|
|
459
|
+
if (sequence !== undefined && this.scheduledThroughSequence === 0) {
|
|
460
|
+
this.coveredThroughSequence = sequence - 1;
|
|
461
|
+
this.scheduledThroughSequence = sequence - 1;
|
|
270
462
|
}
|
|
463
|
+
this.enqueueLiveWrite(chunk);
|
|
271
464
|
}
|
|
272
|
-
|
|
273
|
-
|
|
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)
|
|
274
472
|
return;
|
|
275
|
-
|
|
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)));
|
|
276
505
|
this.retainedLive = [];
|
|
277
506
|
this.retainedLiveBytes = 0;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
this.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (chunk.sequence && replayedSequences.has(chunk.sequence))
|
|
284
|
-
continue;
|
|
285
|
-
if (chunk.sequence && chunk.sequence <= this.coveredThroughSequence) {
|
|
286
|
-
this.enqueueForRender(chunk);
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
this.acceptLive(chunk);
|
|
290
|
-
if (this.state !== 'live')
|
|
507
|
+
for (let index = 0; index < retained.length; index += 1) {
|
|
508
|
+
this.acceptLive(retained[index]);
|
|
509
|
+
if (this.state !== 'live') {
|
|
510
|
+
for (const pending of retained.slice(index + 1))
|
|
511
|
+
this.retainLive(pending);
|
|
291
512
|
break;
|
|
513
|
+
}
|
|
292
514
|
}
|
|
293
515
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
: chunk.data;
|
|
307
|
-
if (transformed === null)
|
|
308
|
-
continue;
|
|
309
|
-
acceptedChunks.push({ ...chunk, data: transformed });
|
|
310
|
-
data.push(transformed);
|
|
311
|
-
}
|
|
312
|
-
if (acceptedChunks.length > 0) {
|
|
313
|
-
this.options.write(concatData(data), acceptedChunks);
|
|
314
|
-
for (const chunk of acceptedChunks) {
|
|
315
|
-
if (chunk.sequence) {
|
|
316
|
-
this.coveredThroughSequence = Math.max(this.coveredThroughSequence, chunk.sequence);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
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;
|
|
319
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;
|
|
320
538
|
}
|
|
321
539
|
cancelRecovery(incrementGeneration = true) {
|
|
322
540
|
this.abortController?.abort();
|
|
323
541
|
this.abortController = null;
|
|
542
|
+
this.recoveryRunning = false;
|
|
543
|
+
this.recoverySerial += 1;
|
|
324
544
|
if (this.retryTimer !== null) {
|
|
325
545
|
const clearTimer = this.options.scheduler?.clearTimer ?? clearTimeout;
|
|
326
546
|
clearTimer(this.retryTimer);
|
|
@@ -329,6 +549,14 @@ class PagedTerminalOutputCoordinator {
|
|
|
329
549
|
if (incrementGeneration)
|
|
330
550
|
this.generation += 1;
|
|
331
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
|
+
}
|
|
332
560
|
setState(state) {
|
|
333
561
|
this.state = state;
|
|
334
562
|
this.emitState();
|