@floegence/floeterm-terminal-web 0.5.19 → 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 +31 -0
- package/dist/core/PagedTerminalOutputCoordinator.d.ts +25 -2
- package/dist/core/PagedTerminalOutputCoordinator.d.ts.map +1 -1
- package/dist/core/PagedTerminalOutputCoordinator.js +324 -10
- 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
package/README.md
CHANGED
|
@@ -51,6 +51,17 @@ const core = new TerminalCore(
|
|
|
51
51
|
await core.initialize();
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
Hosts can preload the dynamic Ghostty module, WASM, and renderer resources before a terminal surface is visible without creating a `TerminalCore`:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { preloadTerminalResources } from '@floegence/floeterm-terminal-web';
|
|
58
|
+
|
|
59
|
+
await preloadTerminalResources({ signal });
|
|
60
|
+
await core.initialize({ priority: 'interactive', signal });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Resource loading is single-flight and retryable after a real import or WASM initialization failure. Caller cancellation only stops that caller from waiting; it does not interrupt shared resource initialization. Core initialization is globally bounded, gives queued interactive work priority over background work, and shares the actual ready promise across duplicate calls.
|
|
64
|
+
|
|
54
65
|
## Notes
|
|
55
66
|
|
|
56
67
|
- You must provide a `TerminalTransport` and `TerminalEventSource` for the managed controller.
|
|
@@ -96,6 +107,26 @@ If a page reports `historyReset` or changes `historyGeneration` while catch-up i
|
|
|
96
107
|
|
|
97
108
|
Use `TerminalCore.writeHistory` for history batches. Its auto-response suppression is scoped to that parser write and ends at its completion callback, so later live output and user input use normal terminal behavior.
|
|
98
109
|
|
|
110
|
+
Running sessions can prepare bounded history before a visible terminal exists. Preparation does not create a Core, attach a session, resize a PTY, or subscribe to live output:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const preparedHistory = await preparePagedTerminalHistory({
|
|
114
|
+
fetchPage: request => transport.historyPage(sessionId, request),
|
|
115
|
+
maxBytes: 4 * 1024 * 1024,
|
|
116
|
+
signal,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const attachGeneration = output.beginAttach(0);
|
|
120
|
+
const attach = await transport.attachWithHistoryBoundary(sessionId, cols, rows);
|
|
121
|
+
await output.completeAttach(attachGeneration, attach.historyBoundarySequence, {
|
|
122
|
+
preparedHistory,
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Prepared history is renderer-neutral and pins `historyGeneration`, `firstRetainedSequence`, and `snapshotEndSequence`. The visible attach validates those boundaries, replays the seed once, and fetches only the missing delta. A generation reset, retention advance, stale fence, or malformed seed discards it and falls back to the normal full recovery path.
|
|
127
|
+
|
|
128
|
+
`preparePagedTerminalHistory` requires those three metadata fields on every prepared page and passes the remaining retained-byte budget as `request.maxBytes` so transports can bound each page as well. The helper always enforces its retained seed budget after a page returns; adapters should honor the request hint to keep peak response memory bounded too.
|
|
129
|
+
|
|
99
130
|
## Restorable In-Memory Snapshots
|
|
100
131
|
|
|
101
132
|
Adaptive host worksets can release inactive renderers without closing their PTY sessions:
|
|
@@ -5,6 +5,7 @@ export interface PagedTerminalHistoryRequest {
|
|
|
5
5
|
endSequence?: number;
|
|
6
6
|
historyGeneration?: number;
|
|
7
7
|
cursor?: string | number;
|
|
8
|
+
maxBytes?: number;
|
|
8
9
|
signal: AbortSignal;
|
|
9
10
|
}
|
|
10
11
|
export interface PagedTerminalHistoryPage {
|
|
@@ -21,6 +22,27 @@ export interface PagedTerminalHistoryPage {
|
|
|
21
22
|
coveredBytes?: number;
|
|
22
23
|
totalBytes?: number;
|
|
23
24
|
}
|
|
25
|
+
export interface PreparePagedTerminalHistoryOptions {
|
|
26
|
+
fetchPage(request: PagedTerminalHistoryRequest): Promise<PagedTerminalHistoryPage>;
|
|
27
|
+
startSequence?: number;
|
|
28
|
+
maxBytes?: number;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
yieldControl?: () => void | Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export interface PreparedPagedTerminalHistory {
|
|
33
|
+
readonly chunks: readonly Readonly<TerminalOutputPipelineChunk>[];
|
|
34
|
+
readonly requestedStartSequence: number;
|
|
35
|
+
readonly firstRetainedSequence: number;
|
|
36
|
+
readonly coveredThroughSequence: number;
|
|
37
|
+
readonly snapshotEndSequence: number;
|
|
38
|
+
readonly historyGeneration: number;
|
|
39
|
+
readonly byteLength: number;
|
|
40
|
+
readonly pageCount: number;
|
|
41
|
+
readonly complete: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface PagedTerminalCompleteAttachOptions {
|
|
44
|
+
preparedHistory?: PreparedPagedTerminalHistory;
|
|
45
|
+
}
|
|
24
46
|
export type PagedTerminalHistoryTruncationReason = 'history-evicted' | 'retained-live-overflow';
|
|
25
47
|
export type PagedTerminalOutputFailureCode = 'history_fetch_failed' | 'history_coverage_incomplete' | 'history_contract_missing' | 'history_contract_invalid' | 'retained_live_overflow' | 'history_evicted';
|
|
26
48
|
export interface PagedTerminalOutputFailure {
|
|
@@ -84,9 +106,10 @@ export interface PagedTerminalOutputCoordinatorHandle {
|
|
|
84
106
|
}
|
|
85
107
|
export interface AtomicPagedTerminalOutputCoordinatorHandle extends PagedTerminalOutputCoordinatorHandle {
|
|
86
108
|
beginAttach(startSequence?: number): number;
|
|
87
|
-
completeAttach(attachGeneration: number, snapshotEndSequence?: number): Promise<void>;
|
|
88
|
-
attach(startSequence?: number, snapshotEndSequence?: number): Promise<void>;
|
|
109
|
+
completeAttach(attachGeneration: number, snapshotEndSequence?: number, options?: PagedTerminalCompleteAttachOptions): Promise<void>;
|
|
110
|
+
attach(startSequence?: number, snapshotEndSequence?: number, options?: PagedTerminalCompleteAttachOptions): Promise<void>;
|
|
89
111
|
}
|
|
112
|
+
export declare const preparePagedTerminalHistory: (options: PreparePagedTerminalHistoryOptions) => Promise<PreparedPagedTerminalHistory>;
|
|
90
113
|
export declare const createPagedTerminalOutputCoordinator: (options: PagedTerminalOutputCoordinatorOptions) => AtomicPagedTerminalOutputCoordinatorHandle;
|
|
91
114
|
export {};
|
|
92
115
|
//# sourceMappingURL=PagedTerminalOutputCoordinator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PagedTerminalOutputCoordinator.d.ts","sourceRoot":"","sources":["../../src/core/PagedTerminalOutputCoordinator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAC3B,+BAA+B,EAChC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"PagedTerminalOutputCoordinator.d.ts","sourceRoot":"","sources":["../../src/core/PagedTerminalOutputCoordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -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;
|
|
@@ -64,6 +273,7 @@ class PagedTerminalOutputCoordinator {
|
|
|
64
273
|
this.writerQuiescenceWaiters = [];
|
|
65
274
|
this.pausedRecoveryPending = false;
|
|
66
275
|
this.droppedLiveThroughSequence = 0;
|
|
276
|
+
this.preparedHistory = null;
|
|
67
277
|
this.options = options;
|
|
68
278
|
this.policy = {
|
|
69
279
|
maxRetainedLiveChunks: normalizePositiveInteger(options.policy?.maxRetainedLiveChunks, DEFAULT_POLICY.maxRetainedLiveChunks),
|
|
@@ -74,9 +284,9 @@ class PagedTerminalOutputCoordinator {
|
|
|
74
284
|
maxWriteBatchBytes: normalizePositiveInteger(options.policy?.maxWriteBatchBytes, DEFAULT_POLICY.maxWriteBatchBytes),
|
|
75
285
|
};
|
|
76
286
|
}
|
|
77
|
-
async attach(startSequence = 1, snapshotEndSequence) {
|
|
287
|
+
async attach(startSequence = 1, snapshotEndSequence, options) {
|
|
78
288
|
const attachGeneration = this.beginAttach(startSequence);
|
|
79
|
-
await this.completeAttach(attachGeneration, snapshotEndSequence);
|
|
289
|
+
await this.completeAttach(attachGeneration, snapshotEndSequence, options);
|
|
80
290
|
}
|
|
81
291
|
beginAttach(startSequence = 1) {
|
|
82
292
|
if (this.disposed)
|
|
@@ -102,14 +312,16 @@ class PagedTerminalOutputCoordinator {
|
|
|
102
312
|
this.failure = null;
|
|
103
313
|
this.lastError = null;
|
|
104
314
|
this.historyRebasePrepared = false;
|
|
315
|
+
this.preparedHistory = null;
|
|
105
316
|
this.setState('idle');
|
|
106
317
|
return this.generation;
|
|
107
318
|
}
|
|
108
|
-
async completeAttach(attachGeneration, snapshotEndSequence) {
|
|
319
|
+
async completeAttach(attachGeneration, snapshotEndSequence, options = {}) {
|
|
109
320
|
if (this.disposed || !this.isCurrent(attachGeneration))
|
|
110
321
|
return;
|
|
111
322
|
this.explicitAttachFence = snapshotEndSequence !== undefined;
|
|
112
323
|
this.recoveryEndSequence = normalizeSequence(snapshotEndSequence, 'snapshotEndSequence', true);
|
|
324
|
+
this.preparedHistory = this.acceptPreparedHistory(options.preparedHistory);
|
|
113
325
|
if (this.explicitAttachFence && this.recoveryEndSequence === 0) {
|
|
114
326
|
if (this.needsRebase) {
|
|
115
327
|
this.prepareRetainedLiveRebase();
|
|
@@ -201,6 +413,7 @@ class PagedTerminalOutputCoordinator {
|
|
|
201
413
|
this.failure = null;
|
|
202
414
|
this.lastError = null;
|
|
203
415
|
this.historyRebasePrepared = false;
|
|
416
|
+
this.preparedHistory = null;
|
|
204
417
|
this.setState('idle');
|
|
205
418
|
this.resolveBaselineWaiters();
|
|
206
419
|
}
|
|
@@ -237,6 +450,7 @@ class PagedTerminalOutputCoordinator {
|
|
|
237
450
|
this.pendingLiveWrites = [];
|
|
238
451
|
this.liveWriteScheduled = false;
|
|
239
452
|
this.pausedRecoveryPending = false;
|
|
453
|
+
this.preparedHistory = null;
|
|
240
454
|
this.setState('disposed');
|
|
241
455
|
this.resolveBaselineWaiters();
|
|
242
456
|
}
|
|
@@ -252,6 +466,62 @@ class PagedTerminalOutputCoordinator {
|
|
|
252
466
|
this.retryAttempt = 0;
|
|
253
467
|
void this.runRecovery();
|
|
254
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
|
+
}
|
|
255
525
|
async runRecovery() {
|
|
256
526
|
if (this.disposed || this.recoveryRunning)
|
|
257
527
|
return;
|
|
@@ -268,13 +538,26 @@ class PagedTerminalOutputCoordinator {
|
|
|
268
538
|
await this.writeChain;
|
|
269
539
|
if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
|
|
270
540
|
return;
|
|
271
|
-
const
|
|
541
|
+
const preparedHistory = this.preparedHistory;
|
|
542
|
+
const historyChunks = preparedHistory
|
|
543
|
+
? preparedHistory.chunks.map(item => ({ ...item, data: new Uint8Array(item.data), source: 'history' }))
|
|
544
|
+
: [];
|
|
272
545
|
let cursor;
|
|
273
|
-
let startSequence =
|
|
546
|
+
let startSequence = preparedHistory
|
|
547
|
+
? Math.max(1, preparedHistory.coveredThroughSequence
|
|
548
|
+
+ (preparedHistory.coveredThroughSequence < (this.recoveryEndSequence ?? 0) ? 1 : 0))
|
|
549
|
+
: this.recoveryStartSequence;
|
|
274
550
|
let snapshotEnd = this.recoveryEndSequence;
|
|
275
|
-
let historyGeneration;
|
|
276
|
-
let coveredEnd = this.coveredThroughSequence;
|
|
277
|
-
let firstPage =
|
|
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
|
+
}
|
|
278
561
|
do {
|
|
279
562
|
const page = await this.options.fetchPage({
|
|
280
563
|
startSequence,
|
|
@@ -288,21 +571,32 @@ class PagedTerminalOutputCoordinator {
|
|
|
288
571
|
const pageSnapshotEnd = normalizeSequence(page.snapshotEndSequence, 'snapshotEndSequence', true);
|
|
289
572
|
const pageGeneration = normalizeSequence(page.historyGeneration, 'historyGeneration', true);
|
|
290
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
|
+
}
|
|
291
581
|
const generationChanged = historyGeneration !== undefined
|
|
292
582
|
&& pageGeneration !== undefined
|
|
293
583
|
&& pageGeneration !== historyGeneration;
|
|
294
584
|
const effectivePageStart = Math.max(1, startSequence);
|
|
295
585
|
const retentionAdvanced = firstRetained !== undefined
|
|
296
|
-
&& firstRetained > effectivePageStart
|
|
586
|
+
&& (firstRetained > effectivePageStart
|
|
587
|
+
|| (preparedHistory !== null && firstRetained > preparedHistory.firstRetainedSequence));
|
|
297
588
|
const generationNeedsRebase = page.historyReset || generationChanged;
|
|
298
589
|
const retentionNeedsRebase = page.historyTruncated || retentionAdvanced;
|
|
299
590
|
const fencedRangeFullyEvicted = this.recoveryEndSequence !== undefined
|
|
300
591
|
&& firstRetained !== undefined
|
|
301
592
|
&& firstRetained > this.recoveryEndSequence;
|
|
302
593
|
if (!this.historyRebasePrepared && (generationNeedsRebase || retentionNeedsRebase)) {
|
|
594
|
+
this.preparedHistory = null;
|
|
303
595
|
if (fencedRangeFullyEvicted) {
|
|
304
596
|
this.options.clear?.();
|
|
305
597
|
this.options.onHistoryTruncated?.('history-evicted');
|
|
598
|
+
if (preparedHistory !== null)
|
|
599
|
+
historyChunks.length = 0;
|
|
306
600
|
if (generationNeedsRebase) {
|
|
307
601
|
this.historyRebasePrepared = true;
|
|
308
602
|
this.recoveryRunning = false;
|
|
@@ -320,7 +614,19 @@ class PagedTerminalOutputCoordinator {
|
|
|
320
614
|
if (this.historyRebasePrepared && generationNeedsRebase) {
|
|
321
615
|
throw new HistoryContractError('history_contract_invalid', 'history generation reset persisted after rebase');
|
|
322
616
|
}
|
|
323
|
-
|
|
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;
|
|
625
|
+
this.prepareHistoryGenerationRebase(firstRetained);
|
|
626
|
+
this.recoveryRunning = false;
|
|
627
|
+
await this.runRecovery();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
324
630
|
if (snapshotEnd !== undefined && coverage > snapshotEnd) {
|
|
325
631
|
throw new HistoryContractError('history_contract_invalid', 'coveredThroughSequence exceeded the recovery fence');
|
|
326
632
|
}
|
|
@@ -348,6 +654,13 @@ class PagedTerminalOutputCoordinator {
|
|
|
348
654
|
}
|
|
349
655
|
else {
|
|
350
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
|
+
}
|
|
351
664
|
throw new HistoryContractError('history_contract_invalid', 'snapshotEndSequence changed during pagination');
|
|
352
665
|
}
|
|
353
666
|
}
|
|
@@ -399,6 +712,7 @@ class PagedTerminalOutputCoordinator {
|
|
|
399
712
|
this.retryAttempt = 0;
|
|
400
713
|
this.lastError = null;
|
|
401
714
|
this.failure = null;
|
|
715
|
+
this.preparedHistory = null;
|
|
402
716
|
this.setState('live');
|
|
403
717
|
this.recoveryRunning = false;
|
|
404
718
|
this.drainRetainedLive();
|