@floegence/floeterm-terminal-web 0.14.0 → 0.15.0

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.
Files changed (156) hide show
  1. package/README.md +78 -445
  2. package/dist/core/TerminalInputBridge.d.ts +7 -3
  3. package/dist/core/TerminalInputBridge.d.ts.map +1 -1
  4. package/dist/core/TerminalInputBridge.js +63 -4
  5. package/dist/core/TerminalInputBridge.js.map +1 -1
  6. package/dist/entries/live.d.ts +4 -4
  7. package/dist/entries/live.d.ts.map +1 -1
  8. package/dist/entries/live.js +2 -2
  9. package/dist/entries/live.js.map +1 -1
  10. package/dist/entries/semantic.d.ts +9 -0
  11. package/dist/entries/semantic.d.ts.map +1 -0
  12. package/dist/entries/semantic.js +5 -0
  13. package/dist/entries/semantic.js.map +1 -0
  14. package/dist/entries/sessions.d.ts +1 -1
  15. package/dist/entries/sessions.d.ts.map +1 -1
  16. package/dist/index.d.ts +4 -26
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -16
  19. package/dist/index.js.map +1 -1
  20. package/dist/live/client.d.ts +4 -6
  21. package/dist/live/client.d.ts.map +1 -1
  22. package/dist/live/client.js +19 -72
  23. package/dist/live/client.js.map +1 -1
  24. package/dist/live/codec.d.ts +8 -21
  25. package/dist/live/codec.d.ts.map +1 -1
  26. package/dist/live/codec.js +79 -111
  27. package/dist/live/codec.js.map +1 -1
  28. package/dist/live/transport.d.ts +38 -18
  29. package/dist/live/transport.d.ts.map +1 -1
  30. package/dist/live/transport.js +59 -133
  31. package/dist/live/transport.js.map +1 -1
  32. package/dist/semantic/RendererSurface.d.ts +65 -0
  33. package/dist/semantic/RendererSurface.d.ts.map +1 -0
  34. package/dist/semantic/RendererSurface.js +566 -0
  35. package/dist/semantic/RendererSurface.js.map +1 -0
  36. package/dist/semantic/presentation.d.ts +97 -0
  37. package/dist/semantic/presentation.d.ts.map +1 -0
  38. package/dist/semantic/presentation.js +102 -0
  39. package/dist/semantic/presentation.js.map +1 -0
  40. package/dist/types.d.ts +13 -494
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js +1 -37
  43. package/dist/types.js.map +1 -1
  44. package/package.json +11 -19
  45. package/dist/checkpoint/GhosttyCheckpoint.worker.d.ts +0 -2
  46. package/dist/checkpoint/GhosttyCheckpoint.worker.d.ts.map +0 -1
  47. package/dist/checkpoint/GhosttyCheckpoint.worker.js +0 -9
  48. package/dist/checkpoint/GhosttyCheckpoint.worker.js.map +0 -1
  49. package/dist/checkpoint/GhosttyCheckpointActor.d.ts +0 -44
  50. package/dist/checkpoint/GhosttyCheckpointActor.d.ts.map +0 -1
  51. package/dist/checkpoint/GhosttyCheckpointActor.js +0 -284
  52. package/dist/checkpoint/GhosttyCheckpointActor.js.map +0 -1
  53. package/dist/checkpoint/GhosttyCheckpointEngine.d.ts +0 -62
  54. package/dist/checkpoint/GhosttyCheckpointEngine.d.ts.map +0 -1
  55. package/dist/checkpoint/GhosttyCheckpointEngine.js +0 -175
  56. package/dist/checkpoint/GhosttyCheckpointEngine.js.map +0 -1
  57. package/dist/checkpoint/GhosttyCheckpointWorkerRuntime.d.ts +0 -31
  58. package/dist/checkpoint/GhosttyCheckpointWorkerRuntime.d.ts.map +0 -1
  59. package/dist/checkpoint/GhosttyCheckpointWorkerRuntime.js +0 -112
  60. package/dist/checkpoint/GhosttyCheckpointWorkerRuntime.js.map +0 -1
  61. package/dist/core/PagedTerminalOutputCoordinator.d.ts +0 -130
  62. package/dist/core/PagedTerminalOutputCoordinator.d.ts.map +0 -1
  63. package/dist/core/PagedTerminalOutputCoordinator.js +0 -1197
  64. package/dist/core/PagedTerminalOutputCoordinator.js.map +0 -1
  65. package/dist/core/TerminalCore.d.ts +0 -286
  66. package/dist/core/TerminalCore.d.ts.map +0 -1
  67. package/dist/core/TerminalCore.js +0 -3566
  68. package/dist/core/TerminalCore.js.map +0 -1
  69. package/dist/core/TerminalOutputPipeline.d.ts +0 -86
  70. package/dist/core/TerminalOutputPipeline.d.ts.map +0 -1
  71. package/dist/core/TerminalOutputPipeline.js +0 -511
  72. package/dist/core/TerminalOutputPipeline.js.map +0 -1
  73. package/dist/core/TerminalRenderScheduler.d.ts +0 -45
  74. package/dist/core/TerminalRenderScheduler.d.ts.map +0 -1
  75. package/dist/core/TerminalRenderScheduler.js +0 -145
  76. package/dist/core/TerminalRenderScheduler.js.map +0 -1
  77. package/dist/core/TerminalScrollbarOverlay.d.ts +0 -63
  78. package/dist/core/TerminalScrollbarOverlay.d.ts.map +0 -1
  79. package/dist/core/TerminalScrollbarOverlay.js +0 -493
  80. package/dist/core/TerminalScrollbarOverlay.js.map +0 -1
  81. package/dist/entries/history.d.ts +0 -4
  82. package/dist/entries/history.d.ts.map +0 -1
  83. package/dist/entries/history.js +0 -2
  84. package/dist/entries/history.js.map +0 -1
  85. package/dist/entries/preload.d.ts +0 -3
  86. package/dist/entries/preload.d.ts.map +0 -1
  87. package/dist/entries/preload.js +0 -2
  88. package/dist/entries/preload.js.map +0 -1
  89. package/dist/fabric/BeamtermFabricRenderer.d.ts +0 -67
  90. package/dist/fabric/BeamtermFabricRenderer.d.ts.map +0 -1
  91. package/dist/fabric/BeamtermFabricRenderer.js +0 -659
  92. package/dist/fabric/BeamtermFabricRenderer.js.map +0 -1
  93. package/dist/fabric/TerminalFabricCoordinator.d.ts +0 -40
  94. package/dist/fabric/TerminalFabricCoordinator.d.ts.map +0 -1
  95. package/dist/fabric/TerminalFabricCoordinator.js +0 -141
  96. package/dist/fabric/TerminalFabricCoordinator.js.map +0 -1
  97. package/dist/fabric/TerminalLiveFabric.d.ts +0 -44
  98. package/dist/fabric/TerminalLiveFabric.d.ts.map +0 -1
  99. package/dist/fabric/TerminalLiveFabric.js +0 -117
  100. package/dist/fabric/TerminalLiveFabric.js.map +0 -1
  101. package/dist/fabric/types.d.ts +0 -152
  102. package/dist/fabric/types.d.ts.map +0 -1
  103. package/dist/fabric/types.js +0 -2
  104. package/dist/fabric/types.js.map +0 -1
  105. package/dist/internal/BeamtermResourceLoader.d.ts +0 -3
  106. package/dist/internal/BeamtermResourceLoader.d.ts.map +0 -1
  107. package/dist/internal/BeamtermResourceLoader.js +0 -14
  108. package/dist/internal/BeamtermResourceLoader.js.map +0 -1
  109. package/dist/internal/GhosttyCompatibilityVersion.d.ts +0 -2
  110. package/dist/internal/GhosttyCompatibilityVersion.d.ts.map +0 -1
  111. package/dist/internal/GhosttyCompatibilityVersion.js +0 -2
  112. package/dist/internal/GhosttyCompatibilityVersion.js.map +0 -1
  113. package/dist/internal/GhosttyScrollbackCompat.d.ts +0 -16
  114. package/dist/internal/GhosttyScrollbackCompat.d.ts.map +0 -1
  115. package/dist/internal/GhosttyScrollbackCompat.js +0 -43
  116. package/dist/internal/GhosttyScrollbackCompat.js.map +0 -1
  117. package/dist/internal/GhosttyScrollbarCompat.d.ts +0 -12
  118. package/dist/internal/GhosttyScrollbarCompat.d.ts.map +0 -1
  119. package/dist/internal/GhosttyScrollbarCompat.js +0 -160
  120. package/dist/internal/GhosttyScrollbarCompat.js.map +0 -1
  121. package/dist/internal/SequenceBuffer.d.ts +0 -19
  122. package/dist/internal/SequenceBuffer.d.ts.map +0 -1
  123. package/dist/internal/SequenceBuffer.js +0 -83
  124. package/dist/internal/SequenceBuffer.js.map +0 -1
  125. package/dist/internal/TerminalInitializationScheduler.d.ts +0 -38
  126. package/dist/internal/TerminalInitializationScheduler.d.ts.map +0 -1
  127. package/dist/internal/TerminalInitializationScheduler.js +0 -112
  128. package/dist/internal/TerminalInitializationScheduler.js.map +0 -1
  129. package/dist/internal/TerminalResourceLoader.d.ts +0 -40
  130. package/dist/internal/TerminalResourceLoader.d.ts.map +0 -1
  131. package/dist/internal/TerminalResourceLoader.js +0 -165
  132. package/dist/internal/TerminalResourceLoader.js.map +0 -1
  133. package/dist/internal/UnicodeSafeUrlProvider.d.ts +0 -17
  134. package/dist/internal/UnicodeSafeUrlProvider.d.ts.map +0 -1
  135. package/dist/internal/UnicodeSafeUrlProvider.js +0 -32
  136. package/dist/internal/UnicodeSafeUrlProvider.js.map +0 -1
  137. package/dist/internal/scheduleUiTurn.d.ts +0 -5
  138. package/dist/internal/scheduleUiTurn.d.ts.map +0 -1
  139. package/dist/internal/scheduleUiTurn.js +0 -37
  140. package/dist/internal/scheduleUiTurn.js.map +0 -1
  141. package/dist/manager/TerminalInstanceController.d.ts +0 -88
  142. package/dist/manager/TerminalInstanceController.d.ts.map +0 -1
  143. package/dist/manager/TerminalInstanceController.js +0 -1076
  144. package/dist/manager/TerminalInstanceController.js.map +0 -1
  145. package/dist/utils/errors.d.ts +0 -3
  146. package/dist/utils/errors.d.ts.map +0 -1
  147. package/dist/utils/errors.js +0 -11
  148. package/dist/utils/errors.js.map +0 -1
  149. package/dist/utils/history.d.ts +0 -2
  150. package/dist/utils/history.d.ts.map +0 -1
  151. package/dist/utils/history.js +0 -12
  152. package/dist/utils/history.js.map +0 -1
  153. package/dist/utils/xtermAutoResponseFilter.d.ts +0 -2
  154. package/dist/utils/xtermAutoResponseFilter.d.ts.map +0 -1
  155. package/dist/utils/xtermAutoResponseFilter.js +0 -109
  156. package/dist/utils/xtermAutoResponseFilter.js.map +0 -1
@@ -1,1197 +0,0 @@
1
- import { scheduleUiTurn } from '../internal/scheduleUiTurn.js';
2
- const DEFAULT_POLICY = {
3
- maxRetainedLiveChunks: 2048,
4
- maxRetainedLiveBytes: 8 * 1024 * 1024,
5
- retryDelaysMs: [250, 1000, 4000],
6
- maxWriteBatchBytes: 512 * 1024,
7
- };
8
- const DEFAULT_PREPARED_HISTORY_MAX_BYTES = 32 * 1024 * 1024;
9
- const normalizePositiveInteger = (value, fallback) => (typeof value === 'number' && Number.isSafeInteger(value) && value > 0
10
- ? value
11
- : fallback);
12
- const normalizeSequence = (value, field, optional = false) => {
13
- if (value === undefined && optional)
14
- return undefined;
15
- if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
16
- throw new HistoryContractError('history_contract_invalid', `${field} must be a non-negative safe integer`);
17
- }
18
- return value;
19
- };
20
- const concatData = (chunks) => {
21
- const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
22
- const result = new Uint8Array(total);
23
- let offset = 0;
24
- for (const chunk of chunks) {
25
- result.set(chunk, offset);
26
- offset += chunk.byteLength;
27
- }
28
- return result;
29
- };
30
- class HistoryContractError extends Error {
31
- constructor(code, message, firstRetainedSequence) {
32
- super(message);
33
- this.name = 'HistoryContractError';
34
- this.code = code;
35
- this.firstRetainedSequence = firstRetainedSequence;
36
- }
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
- ...(chunk.geometryGeneration !== undefined ? { geometryGeneration: chunk.geometryGeneration } : {}),
94
- ...(chunk.cols !== undefined ? { cols: chunk.cols } : {}),
95
- ...(chunk.rows !== undefined ? { rows: chunk.rows } : {}),
96
- });
97
- };
98
- const historyGeometryForChunk = (chunk) => {
99
- if (chunk.geometryGeneration === undefined || chunk.cols === undefined || chunk.rows === undefined) {
100
- throw new HistoryContractError('history_contract_missing', 'history chunk geometry is required');
101
- }
102
- if (!Number.isSafeInteger(chunk.geometryGeneration) || chunk.geometryGeneration <= 0
103
- || !Number.isSafeInteger(chunk.cols) || chunk.cols <= 0
104
- || !Number.isSafeInteger(chunk.rows) || chunk.rows <= 0) {
105
- throw new HistoryContractError('history_contract_invalid', 'history chunk geometry is invalid');
106
- }
107
- return { generation: chunk.geometryGeneration, cols: chunk.cols, rows: chunk.rows };
108
- };
109
- const validateHistoryGeometryOrder = (previous, current) => {
110
- if (previous && current.generation < previous.generation) {
111
- throw new HistoryContractError('history_contract_invalid', 'history geometry generation regressed');
112
- }
113
- if (previous && current.generation === previous.generation
114
- && (current.cols !== previous.cols || current.rows !== previous.rows)) {
115
- throw new HistoryContractError('history_contract_invalid', 'history geometry dimensions conflict within a generation');
116
- }
117
- return current;
118
- };
119
- const sameHistoryGeometry = (left, right) => (left.generation === right.generation && left.cols === right.cols && left.rows === right.rows);
120
- const validateCheckpointForPage = (checkpoint, page, requestedStart, snapshotEnd, firstRetained) => {
121
- if (checkpoint === undefined)
122
- return undefined;
123
- if (checkpoint.formatVersion !== 1
124
- || checkpoint.engineId !== 'floegence-ghostty-web'
125
- || !Number.isSafeInteger(checkpoint.coveredThroughSequence)
126
- || checkpoint.coveredThroughSequence < Math.max(0, requestedStart - 1)
127
- || (snapshotEnd !== undefined && checkpoint.coveredThroughSequence > snapshotEnd)
128
- || !Number.isSafeInteger(checkpoint.geometryGeneration)
129
- || checkpoint.geometryGeneration <= 0
130
- || !Number.isSafeInteger(checkpoint.parserEpoch)
131
- || checkpoint.parserEpoch <= 0
132
- || !Number.isSafeInteger(checkpoint.cols)
133
- || checkpoint.cols <= 0
134
- || !Number.isSafeInteger(checkpoint.rows)
135
- || checkpoint.rows <= 0
136
- || !(checkpoint.bytes instanceof Uint8Array)
137
- || checkpoint.bytes.byteLength === 0
138
- || !/^[0-9a-f]{64}$/.test(checkpoint.checksumSha256)
139
- || !/^[0-9a-f]{64}$/.test(checkpoint.stateDigestSha256)) {
140
- throw new HistoryContractError('history_checkpoint_invalid', 'terminal history checkpoint metadata is invalid');
141
- }
142
- const expectedDeltaStart = checkpoint.coveredThroughSequence + 1;
143
- if (page.deltaStartSequence !== expectedDeltaStart) {
144
- throw new HistoryContractError('history_checkpoint_invalid', 'terminal history checkpoint delta boundary is invalid');
145
- }
146
- if (firstRetained !== undefined && firstRetained !== expectedDeltaStart) {
147
- throw new HistoryContractError('history_checkpoint_invalid', 'terminal history checkpoint retention floor is invalid', firstRetained);
148
- }
149
- return { ...checkpoint, bytes: checkpoint.bytes.slice() };
150
- };
151
- export const preparePagedTerminalHistory = async (options) => {
152
- const requestedStartSequence = normalizeSequence(options.startSequence ?? 1, 'startSequence');
153
- const maxBytes = normalizePositiveInteger(options.maxBytes, DEFAULT_PREPARED_HISTORY_MAX_BYTES);
154
- let startSequence = Math.max(1, requestedStartSequence);
155
- let cursor;
156
- let historyGeneration;
157
- let snapshotEndSequence;
158
- let firstRetainedSequence;
159
- let coveredThroughSequence = Math.max(0, startSequence - 1);
160
- let byteLength = 0;
161
- let pageCount = 0;
162
- let chunks = [];
163
- let rebaseAttempts = 0;
164
- let previousHistoryGeometry;
165
- const requestSignal = options.signal ?? new AbortController().signal;
166
- while (true) {
167
- throwIfAborted(options.signal);
168
- const page = await waitForPreparedPage(options.fetchPage({
169
- startSequence,
170
- endSequence: snapshotEndSequence,
171
- historyGeneration,
172
- cursor,
173
- maxBytes: Math.max(1, maxBytes - byteLength),
174
- signal: requestSignal,
175
- }), options.signal);
176
- throwIfAborted(options.signal);
177
- pageCount += 1;
178
- if (!Object.prototype.hasOwnProperty.call(page, 'coveredThroughSequence')) {
179
- throw new HistoryContractError('history_contract_missing', 'coveredThroughSequence is required');
180
- }
181
- if (!Object.prototype.hasOwnProperty.call(page, 'snapshotEndSequence')) {
182
- throw new HistoryContractError('history_contract_missing', 'snapshotEndSequence is required');
183
- }
184
- if (!Object.prototype.hasOwnProperty.call(page, 'historyGeneration')) {
185
- throw new HistoryContractError('history_contract_missing', 'historyGeneration is required');
186
- }
187
- if (!Object.prototype.hasOwnProperty.call(page, 'firstRetainedSequence')
188
- && !Object.prototype.hasOwnProperty.call(page, 'firstAvailableSequence')) {
189
- throw new HistoryContractError('history_contract_missing', 'firstRetainedSequence is required');
190
- }
191
- const pageCoverage = normalizeSequence(page.coveredThroughSequence, 'coveredThroughSequence');
192
- const pageSnapshotEnd = normalizeSequence(page.snapshotEndSequence, 'snapshotEndSequence');
193
- const pageGeneration = normalizeSequence(page.historyGeneration, 'historyGeneration');
194
- const pageFirstRetained = normalizeSequence(page.firstRetainedSequence ?? page.firstAvailableSequence, 'firstRetainedSequence');
195
- const generationChanged = historyGeneration !== undefined && pageGeneration !== historyGeneration;
196
- const snapshotChanged = snapshotEndSequence !== undefined && pageSnapshotEnd !== snapshotEndSequence;
197
- const retentionAdvanced = firstRetainedSequence !== undefined
198
- && pageFirstRetained > firstRetainedSequence;
199
- if (historyGeneration !== undefined
200
- && (page.historyReset || page.historyTruncated || generationChanged || snapshotChanged || retentionAdvanced)) {
201
- if (rebaseAttempts >= 2) {
202
- throw new HistoryContractError('history_contract_invalid', 'terminal history changed repeatedly while preparing a stable snapshot');
203
- }
204
- rebaseAttempts += 1;
205
- startSequence = Math.max(1, pageFirstRetained);
206
- cursor = undefined;
207
- historyGeneration = undefined;
208
- snapshotEndSequence = undefined;
209
- firstRetainedSequence = undefined;
210
- coveredThroughSequence = startSequence - 1;
211
- byteLength = 0;
212
- chunks = [];
213
- previousHistoryGeometry = undefined;
214
- await yieldHistoryPreparation(options.signal, options.yieldControl);
215
- continue;
216
- }
217
- if (pageCoverage < coveredThroughSequence) {
218
- throw new HistoryContractError('history_contract_invalid', 'coveredThroughSequence regressed');
219
- }
220
- if (pageCoverage > pageSnapshotEnd) {
221
- throw new HistoryContractError('history_contract_invalid', 'coveredThroughSequence exceeded snapshotEndSequence');
222
- }
223
- let previousChunkSequence = coveredThroughSequence;
224
- for (const chunk of page.chunks) {
225
- const sequence = normalizeSequence(chunk.sequence, 'chunk.sequence');
226
- if (sequence <= previousChunkSequence || sequence > pageCoverage) {
227
- throw new HistoryContractError('history_contract_invalid', 'prepared history chunks must have strictly increasing sequences within page coverage');
228
- }
229
- previousChunkSequence = sequence;
230
- previousHistoryGeometry = validateHistoryGeometryOrder(previousHistoryGeometry, historyGeometryForChunk(chunk));
231
- }
232
- if (pageCoverage > Math.max(0, pageFirstRetained - 1)
233
- && previousChunkSequence !== pageCoverage) {
234
- throw new HistoryContractError('history_contract_invalid', 'prepared history page did not include its covered terminal sequence');
235
- }
236
- historyGeneration = pageGeneration;
237
- snapshotEndSequence = pageSnapshotEnd;
238
- firstRetainedSequence = pageFirstRetained;
239
- if (pageFirstRetained > startSequence) {
240
- coveredThroughSequence = Math.max(coveredThroughSequence, pageFirstRetained - 1);
241
- }
242
- const pageBytes = page.chunks.reduce((sum, chunk) => sum + chunk.data.byteLength, 0);
243
- const remainingBytes = maxBytes - byteLength;
244
- if (pageBytes <= remainingBytes) {
245
- chunks.push(...page.chunks.map(clonePreparedChunk));
246
- byteLength += pageBytes;
247
- coveredThroughSequence = Math.max(coveredThroughSequence, pageCoverage);
248
- }
249
- else {
250
- for (const chunk of page.chunks) {
251
- const sequence = normalizeSequence(chunk.sequence, 'chunk.sequence');
252
- if (byteLength + chunk.data.byteLength > maxBytes)
253
- break;
254
- chunks.push(clonePreparedChunk(chunk));
255
- byteLength += chunk.data.byteLength;
256
- coveredThroughSequence = Math.max(coveredThroughSequence, sequence);
257
- }
258
- return Object.freeze({
259
- chunks: Object.freeze(chunks),
260
- requestedStartSequence,
261
- firstRetainedSequence,
262
- coveredThroughSequence,
263
- snapshotEndSequence,
264
- historyGeneration,
265
- byteLength,
266
- pageCount,
267
- complete: false,
268
- });
269
- }
270
- if (!page.hasMore) {
271
- return Object.freeze({
272
- chunks: Object.freeze(chunks),
273
- requestedStartSequence,
274
- firstRetainedSequence,
275
- coveredThroughSequence,
276
- snapshotEndSequence,
277
- historyGeneration,
278
- byteLength,
279
- pageCount,
280
- complete: coveredThroughSequence >= snapshotEndSequence,
281
- });
282
- }
283
- if (byteLength >= maxBytes) {
284
- return Object.freeze({
285
- chunks: Object.freeze(chunks),
286
- requestedStartSequence,
287
- firstRetainedSequence,
288
- coveredThroughSequence,
289
- snapshotEndSequence,
290
- historyGeneration,
291
- byteLength,
292
- pageCount,
293
- complete: false,
294
- });
295
- }
296
- if (page.nextCursor === undefined) {
297
- throw new HistoryContractError('history_contract_invalid', 'nextCursor is required when hasMore is true');
298
- }
299
- cursor = page.nextCursor;
300
- startSequence = pageCoverage + 1;
301
- await yieldHistoryPreparation(options.signal, options.yieldControl);
302
- }
303
- };
304
- class PagedTerminalOutputCoordinator {
305
- constructor(options) {
306
- this.active = true;
307
- this.state = 'idle';
308
- this.baselineReady = false;
309
- this.coveredThroughSequence = 0;
310
- this.scheduledThroughSequence = 0;
311
- this.retainedLive = [];
312
- this.retainedLiveBytes = 0;
313
- this.retryAttempt = 0;
314
- this.retryTimer = null;
315
- this.abortController = null;
316
- this.generation = 0;
317
- this.recoverySerial = 0;
318
- this.disposed = false;
319
- this.lastError = null;
320
- this.failure = null;
321
- this.recoveryStartSequence = 1;
322
- this.explicitAttachFence = false;
323
- this.recoveryKind = 'initial';
324
- this.recoveryRunning = false;
325
- this.needsRebase = false;
326
- this.historyRebasePrepared = false;
327
- this.writeChain = Promise.resolve();
328
- this.pendingLiveWrites = [];
329
- this.liveWriteScheduled = false;
330
- this.baselineWaiters = [];
331
- this.activeWriters = 0;
332
- this.writerQuiescenceWaiters = [];
333
- this.pausedRecoveryPending = false;
334
- this.droppedLiveThroughSequence = 0;
335
- this.preparedHistory = null;
336
- this.preparedHistoryOutcome = {
337
- status: 'not-provided',
338
- rebased: false,
339
- };
340
- this.options = options;
341
- this.policy = {
342
- maxRetainedLiveChunks: normalizePositiveInteger(options.policy?.maxRetainedLiveChunks, DEFAULT_POLICY.maxRetainedLiveChunks),
343
- maxRetainedLiveBytes: normalizePositiveInteger(options.policy?.maxRetainedLiveBytes, DEFAULT_POLICY.maxRetainedLiveBytes),
344
- retryDelaysMs: options.policy?.retryDelaysMs !== undefined
345
- ? options.policy.retryDelaysMs.map(delay => Math.max(0, Math.floor(delay)))
346
- : DEFAULT_POLICY.retryDelaysMs,
347
- maxWriteBatchBytes: normalizePositiveInteger(options.policy?.maxWriteBatchBytes, DEFAULT_POLICY.maxWriteBatchBytes),
348
- };
349
- }
350
- async attach(startSequence = 1, snapshotEndSequence, options) {
351
- const attachGeneration = this.beginAttach(startSequence);
352
- await this.completeAttach(attachGeneration, snapshotEndSequence, options);
353
- }
354
- beginAttach(startSequence = 1) {
355
- if (this.disposed)
356
- return this.generation;
357
- this.cancelRecovery();
358
- this.resolveBaselineWaiters();
359
- this.retainedLive = [];
360
- this.retainedLiveBytes = 0;
361
- this.pendingLiveWrites = [];
362
- this.liveWriteScheduled = false;
363
- this.pausedRecoveryPending = false;
364
- this.writeChain = Promise.resolve();
365
- this.baselineReady = false;
366
- this.coveredThroughSequence = Math.max(0, Math.floor(startSequence) - 1);
367
- this.scheduledThroughSequence = this.coveredThroughSequence;
368
- this.recoveryKind = 'initial';
369
- this.recoveryStartSequence = Math.max(0, Math.floor(startSequence));
370
- this.recoveryEndSequence = undefined;
371
- this.explicitAttachFence = false;
372
- this.droppedLiveThroughSequence = 0;
373
- this.needsRebase = false;
374
- this.retryAttempt = 0;
375
- this.failure = null;
376
- this.lastError = null;
377
- this.historyRebasePrepared = false;
378
- this.preparedHistory = null;
379
- this.preparedHistoryOutcome = { status: 'not-provided', rebased: false };
380
- this.setState('idle');
381
- return this.generation;
382
- }
383
- async completeAttach(attachGeneration, snapshotEndSequence, options = {}) {
384
- if (this.disposed || !this.isCurrent(attachGeneration))
385
- return;
386
- this.explicitAttachFence = snapshotEndSequence !== undefined;
387
- this.recoveryEndSequence = normalizeSequence(snapshotEndSequence, 'snapshotEndSequence', true);
388
- this.preparedHistoryOutcome = options.preparedHistory
389
- ? { status: 'rejected', rebased: false }
390
- : { status: 'not-provided', rebased: false };
391
- this.preparedHistory = this.acceptPreparedHistory(options.preparedHistory);
392
- if (this.preparedHistory) {
393
- this.preparedHistoryOutcome = {
394
- ...this.preparedHistoryOutcome,
395
- status: 'accepted',
396
- };
397
- }
398
- if (this.explicitAttachFence && this.recoveryEndSequence === 0) {
399
- if (this.needsRebase) {
400
- this.prepareRetainedLiveRebase();
401
- await this.runRecovery();
402
- return;
403
- }
404
- this.baselineReady = true;
405
- this.resolveBaselineWaiters();
406
- this.emitState();
407
- this.setState('live');
408
- this.drainRetainedLive();
409
- await this.writeChain;
410
- return;
411
- }
412
- await this.runRecovery();
413
- }
414
- waitForBaseline() {
415
- const snapshot = this.getSnapshot();
416
- if (snapshot.baselineReady || snapshot.state === 'failed' || snapshot.disposed) {
417
- return Promise.resolve(snapshot);
418
- }
419
- return new Promise(resolve => this.baselineWaiters.push(resolve));
420
- }
421
- async pause() {
422
- if (this.disposed)
423
- return this.getSnapshot();
424
- this.active = false;
425
- const recoveryPending = this.recoveryRunning
426
- || this.state === 'initial-replay'
427
- || this.state === 'catching-up'
428
- || this.state === 'retry-wait';
429
- if (recoveryPending) {
430
- this.pausedRecoveryPending = true;
431
- this.cancelRecovery(false);
432
- }
433
- this.emitState();
434
- await this.writeChain;
435
- await this.waitForWriterQuiescence();
436
- return this.getSnapshot();
437
- }
438
- pushLive(chunk) {
439
- if (this.disposed)
440
- return;
441
- const tagged = { ...chunk, source: 'live' };
442
- if (this.state !== 'live' || !this.canRenderLive()) {
443
- this.retainLive(tagged);
444
- return;
445
- }
446
- this.acceptLive(tagged);
447
- }
448
- setActive(active) {
449
- if (this.disposed)
450
- return;
451
- this.active = active;
452
- if (active) {
453
- if (this.pausedRecoveryPending) {
454
- this.pausedRecoveryPending = false;
455
- void this.runRecovery();
456
- }
457
- else if (this.needsRebase) {
458
- if (!this.recoveryRunning) {
459
- this.prepareRetainedLiveRebase();
460
- void this.runRecovery();
461
- }
462
- }
463
- else if (this.state === 'live') {
464
- this.drainRetainedLive();
465
- }
466
- }
467
- this.emitState();
468
- }
469
- clear(startSequence = 1) {
470
- if (this.disposed)
471
- return;
472
- this.cancelRecovery();
473
- this.options.clear?.();
474
- this.retainedLive = [];
475
- this.retainedLiveBytes = 0;
476
- this.pendingLiveWrites = [];
477
- this.liveWriteScheduled = false;
478
- this.pausedRecoveryPending = false;
479
- this.writeChain = Promise.resolve();
480
- this.baselineReady = false;
481
- this.coveredThroughSequence = Math.max(0, Math.floor(startSequence) - 1);
482
- this.scheduledThroughSequence = this.coveredThroughSequence;
483
- this.recoveryEndSequence = undefined;
484
- this.explicitAttachFence = false;
485
- this.droppedLiveThroughSequence = 0;
486
- this.retryAttempt = 0;
487
- this.failure = null;
488
- this.lastError = null;
489
- this.historyRebasePrepared = false;
490
- this.preparedHistory = null;
491
- this.preparedHistoryOutcome = { status: 'not-provided', rebased: false };
492
- this.setState('idle');
493
- this.resolveBaselineWaiters();
494
- }
495
- retry() {
496
- if (this.disposed || this.state !== 'failed')
497
- return;
498
- this.retryAttempt = 0;
499
- this.failure = null;
500
- void this.runRecovery();
501
- }
502
- getSnapshot() {
503
- return {
504
- state: this.state,
505
- active: this.active,
506
- baselineReady: this.baselineReady,
507
- coveredThroughSequence: this.coveredThroughSequence,
508
- retainedLiveChunks: this.retainedLive.length,
509
- retainedLiveBytes: this.retainedLiveBytes,
510
- retryAttempt: this.retryAttempt,
511
- retryScheduled: this.retryTimer !== null,
512
- failure: this.failure,
513
- lastError: this.lastError,
514
- attachGeneration: this.generation,
515
- disposed: this.disposed,
516
- preparedHistoryOutcome: { ...this.preparedHistoryOutcome },
517
- };
518
- }
519
- dispose() {
520
- if (this.disposed)
521
- return;
522
- this.disposed = true;
523
- this.cancelRecovery();
524
- this.retainedLive = [];
525
- this.retainedLiveBytes = 0;
526
- this.pendingLiveWrites = [];
527
- this.liveWriteScheduled = false;
528
- this.pausedRecoveryPending = false;
529
- this.preparedHistory = null;
530
- this.setState('disposed');
531
- this.resolveBaselineWaiters();
532
- }
533
- beginCatchUp(startSequence) {
534
- if (this.disposed || this.recoveryRunning || this.state === 'initial-replay')
535
- return;
536
- this.recoveryKind = 'catch-up';
537
- this.recoveryStartSequence = Math.max(0, Math.floor(startSequence));
538
- const firstRetainedSequence = this.firstRetainedLiveSequence();
539
- this.recoveryEndSequence = firstRetainedSequence === undefined
540
- ? undefined
541
- : Math.max(0, firstRetainedSequence - 1);
542
- this.retryAttempt = 0;
543
- void this.runRecovery();
544
- }
545
- acceptPreparedHistory(preparedHistory) {
546
- if (!preparedHistory || this.recoveryEndSequence === undefined)
547
- return null;
548
- try {
549
- const requestedStart = normalizeSequence(preparedHistory.requestedStartSequence, 'preparedHistory.requestedStartSequence');
550
- const firstRetained = normalizeSequence(preparedHistory.firstRetainedSequence, 'preparedHistory.firstRetainedSequence');
551
- const coveredThrough = normalizeSequence(preparedHistory.coveredThroughSequence, 'preparedHistory.coveredThroughSequence');
552
- const snapshotEnd = normalizeSequence(preparedHistory.snapshotEndSequence, 'preparedHistory.snapshotEndSequence');
553
- normalizeSequence(preparedHistory.historyGeneration, 'preparedHistory.historyGeneration');
554
- normalizeSequence(preparedHistory.byteLength, 'preparedHistory.byteLength');
555
- normalizeSequence(preparedHistory.pageCount, 'preparedHistory.pageCount');
556
- const expectedStart = Math.max(1, this.recoveryStartSequence);
557
- if (requestedStart > expectedStart)
558
- return null;
559
- if (firstRetained > coveredThrough + 1)
560
- return null;
561
- if (coveredThrough < expectedStart - 1 || coveredThrough > snapshotEnd)
562
- return null;
563
- if (snapshotEnd > this.recoveryEndSequence || coveredThrough > this.recoveryEndSequence)
564
- return null;
565
- if (!Array.isArray(preparedHistory.chunks))
566
- return null;
567
- let previousChunkSequence = Math.max(0, firstRetained - 1);
568
- let previousGeometry;
569
- let preparedBytes = 0;
570
- for (const chunk of preparedHistory.chunks) {
571
- if (!(chunk.data instanceof Uint8Array))
572
- return null;
573
- const sequence = normalizeSequence(chunk.sequence, 'preparedHistory.chunk.sequence');
574
- if (sequence <= previousChunkSequence || sequence > coveredThrough)
575
- return null;
576
- previousChunkSequence = sequence;
577
- try {
578
- previousGeometry = validateHistoryGeometryOrder(previousGeometry, historyGeometryForChunk(chunk));
579
- }
580
- catch {
581
- return null;
582
- }
583
- preparedBytes += chunk.data.byteLength;
584
- }
585
- if (preparedBytes !== preparedHistory.byteLength)
586
- return null;
587
- if (coveredThrough >= Math.max(1, firstRetained)
588
- && previousChunkSequence !== coveredThrough)
589
- return null;
590
- if (preparedHistory.complete !== (coveredThrough >= snapshotEnd))
591
- return null;
592
- if (firstRetained > expectedStart) {
593
- this.markPreparedHistoryRebased(false);
594
- this.options.clear?.();
595
- this.options.onHistoryTruncated?.('history-evicted');
596
- }
597
- return preparedHistory;
598
- }
599
- catch {
600
- return null;
601
- }
602
- }
603
- discardPreparedHistoryForFullRecovery() {
604
- this.markPreparedHistoryRebased(true);
605
- this.preparedHistory = null;
606
- this.coveredThroughSequence = Math.max(0, this.recoveryStartSequence - 1);
607
- this.scheduledThroughSequence = this.coveredThroughSequence;
608
- this.historyRebasePrepared = false;
609
- }
610
- async runRecovery() {
611
- if (this.disposed || this.recoveryRunning)
612
- return;
613
- this.cancelRecovery(false);
614
- const generation = this.generation;
615
- const recoverySerial = ++this.recoverySerial;
616
- const controller = new AbortController();
617
- this.abortController = controller;
618
- this.lastError = null;
619
- this.failure = null;
620
- this.recoveryRunning = true;
621
- this.setState(this.recoveryKind === 'initial' ? 'initial-replay' : 'catching-up');
622
- try {
623
- await this.writeChain;
624
- if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
625
- return;
626
- const preparedHistory = this.preparedHistory;
627
- const historyChunks = preparedHistory
628
- ? preparedHistory.chunks.map(item => ({ ...item, data: new Uint8Array(item.data), source: 'history' }))
629
- : [];
630
- let cursor;
631
- let startSequence = preparedHistory
632
- ? Math.max(1, preparedHistory.coveredThroughSequence
633
- + (preparedHistory.coveredThroughSequence < (this.recoveryEndSequence ?? 0) ? 1 : 0))
634
- : this.recoveryStartSequence;
635
- let snapshotEnd = this.recoveryEndSequence;
636
- let historyGeneration = preparedHistory?.historyGeneration;
637
- let coveredEnd = preparedHistory?.coveredThroughSequence ?? this.coveredThroughSequence;
638
- let firstPage = preparedHistory === null;
639
- let restoredCheckpointSequence;
640
- const preparedBaseline = preparedHistory === null
641
- ? undefined
642
- : Math.max(Math.max(1, this.recoveryStartSequence) - 1, preparedHistory.firstRetainedSequence - 1);
643
- if (preparedBaseline !== undefined) {
644
- this.coveredThroughSequence = preparedBaseline;
645
- this.scheduledThroughSequence = preparedBaseline;
646
- }
647
- do {
648
- const page = await this.options.fetchPage({
649
- startSequence,
650
- endSequence: snapshotEnd,
651
- historyGeneration,
652
- cursor,
653
- signal: controller.signal,
654
- });
655
- if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
656
- return;
657
- const pageSnapshotEnd = normalizeSequence(page.snapshotEndSequence, 'snapshotEndSequence', true);
658
- const pageGeneration = normalizeSequence(page.historyGeneration, 'historyGeneration', true);
659
- const firstRetained = normalizeSequence(page.firstRetainedSequence ?? page.firstAvailableSequence, 'firstRetainedSequence', true);
660
- const pageCheckpoint = validateCheckpointForPage(page.checkpoint, page, Math.max(1, startSequence), pageSnapshotEnd, firstRetained);
661
- let restoredCheckpointThisPage = false;
662
- if (pageCheckpoint) {
663
- if (restoredCheckpointSequence !== undefined) {
664
- throw new HistoryContractError('history_checkpoint_invalid', 'terminal history checkpoint was repeated during pagination');
665
- }
666
- if (!this.options.restoreCheckpoint) {
667
- throw new HistoryContractError('history_checkpoint_missing', 'terminal history checkpoint restore is not configured');
668
- }
669
- try {
670
- await this.options.restoreCheckpoint(pageCheckpoint);
671
- }
672
- catch (error) {
673
- throw new HistoryContractError('history_checkpoint_restore_failed', 'terminal history checkpoint restore failed', pageCheckpoint.coveredThroughSequence);
674
- }
675
- restoredCheckpointSequence = pageCheckpoint.coveredThroughSequence;
676
- restoredCheckpointThisPage = true;
677
- this.coveredThroughSequence = pageCheckpoint.coveredThroughSequence;
678
- this.scheduledThroughSequence = pageCheckpoint.coveredThroughSequence;
679
- coveredEnd = Math.max(coveredEnd, pageCheckpoint.coveredThroughSequence);
680
- if (preparedHistory !== null)
681
- historyChunks.length = 0;
682
- }
683
- if (preparedHistory !== null
684
- && (pageSnapshotEnd === undefined || pageGeneration === undefined || firstRetained === undefined)) {
685
- this.discardPreparedHistoryForFullRecovery();
686
- this.recoveryRunning = false;
687
- await this.runRecovery();
688
- return;
689
- }
690
- const generationChanged = historyGeneration !== undefined
691
- && pageGeneration !== undefined
692
- && pageGeneration !== historyGeneration;
693
- const effectivePageStart = Math.max(1, startSequence);
694
- const retentionAdvanced = firstRetained !== undefined
695
- && (firstRetained > effectivePageStart
696
- || (preparedHistory !== null && firstRetained > preparedHistory.firstRetainedSequence));
697
- const generationNeedsRebase = !restoredCheckpointThisPage && (page.historyReset || generationChanged);
698
- const retentionNeedsRebase = !restoredCheckpointThisPage && (page.historyTruncated || retentionAdvanced);
699
- if (retentionNeedsRebase && !pageCheckpoint && !this.historyRebasePrepared) {
700
- throw new HistoryContractError('history_checkpoint_missing', 'truncated terminal history requires a validated checkpoint', firstRetained);
701
- }
702
- const fencedRangeFullyEvicted = this.recoveryEndSequence !== undefined
703
- && firstRetained !== undefined
704
- && firstRetained > this.recoveryEndSequence;
705
- if (!this.historyRebasePrepared && (generationNeedsRebase || retentionNeedsRebase)) {
706
- this.markPreparedHistoryRebased(preparedHistory !== null);
707
- this.preparedHistory = null;
708
- if (fencedRangeFullyEvicted) {
709
- this.options.clear?.();
710
- this.options.onHistoryTruncated?.('history-evicted');
711
- if (preparedHistory !== null)
712
- historyChunks.length = 0;
713
- if (generationNeedsRebase) {
714
- this.historyRebasePrepared = true;
715
- this.recoveryRunning = false;
716
- await this.runRecovery();
717
- return;
718
- }
719
- }
720
- else {
721
- this.prepareHistoryGenerationRebase(firstRetained);
722
- this.recoveryRunning = false;
723
- await this.runRecovery();
724
- return;
725
- }
726
- }
727
- if (this.historyRebasePrepared && generationNeedsRebase) {
728
- throw new HistoryContractError('history_contract_invalid', 'history generation reset persisted after rebase');
729
- }
730
- let coverage;
731
- try {
732
- coverage = this.validatePage(page, coveredEnd);
733
- }
734
- catch (error) {
735
- if (preparedHistory === null)
736
- throw error;
737
- this.markPreparedHistoryRebased(true);
738
- this.preparedHistory = null;
739
- this.prepareHistoryGenerationRebase(firstRetained);
740
- this.recoveryRunning = false;
741
- await this.runRecovery();
742
- return;
743
- }
744
- if (snapshotEnd !== undefined && coverage > snapshotEnd) {
745
- throw new HistoryContractError('history_contract_invalid', 'coveredThroughSequence exceeded the recovery fence');
746
- }
747
- if (firstPage) {
748
- if (this.recoveryEndSequence !== undefined) {
749
- if (pageSnapshotEnd !== undefined && pageSnapshotEnd !== this.recoveryEndSequence) {
750
- throw new HistoryContractError('history_contract_invalid', 'snapshotEndSequence does not match the requested recovery fence');
751
- }
752
- snapshotEnd = this.recoveryEndSequence;
753
- }
754
- else {
755
- snapshotEnd = pageSnapshotEnd;
756
- }
757
- historyGeneration = pageGeneration;
758
- const effectiveStart = Math.max(1, startSequence);
759
- if (!restoredCheckpointThisPage && !fencedRangeFullyEvicted && firstRetained !== undefined && firstRetained > effectiveStart) {
760
- if (!this.historyRebasePrepared) {
761
- this.options.clear?.();
762
- this.options.onHistoryTruncated?.('history-evicted');
763
- }
764
- this.coveredThroughSequence = Math.max(0, (firstRetained ?? effectiveStart) - 1);
765
- this.scheduledThroughSequence = this.coveredThroughSequence;
766
- }
767
- this.historyRebasePrepared = false;
768
- }
769
- else {
770
- if (snapshotEnd !== undefined && pageSnapshotEnd !== undefined && pageSnapshotEnd !== snapshotEnd) {
771
- if (preparedHistory !== null) {
772
- this.markPreparedHistoryRebased(true);
773
- this.preparedHistory = null;
774
- this.prepareHistoryGenerationRebase(firstRetained);
775
- this.recoveryRunning = false;
776
- await this.runRecovery();
777
- return;
778
- }
779
- throw new HistoryContractError('history_contract_invalid', 'snapshotEndSequence changed during pagination');
780
- }
781
- }
782
- firstPage = false;
783
- historyChunks.push(...page.chunks.map(item => ({ ...item, source: 'history' })));
784
- coveredEnd = Math.max(coveredEnd, coverage);
785
- cursor = page.nextCursor;
786
- if (!page.hasMore)
787
- break;
788
- if (cursor === undefined) {
789
- throw new HistoryContractError('history_contract_invalid', 'nextCursor is required when hasMore is true');
790
- }
791
- startSequence = coverage + 1;
792
- } while (!controller.signal.aborted);
793
- if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
794
- return;
795
- if (this.recoveryEndSequence !== undefined && coveredEnd < this.recoveryEndSequence) {
796
- throw new HistoryContractError('history_coverage_incomplete', 'terminal history coverage did not reach the recovery fence');
797
- }
798
- if (this.needsRebase) {
799
- this.prepareRetainedLiveRebase();
800
- this.recoveryRunning = false;
801
- await this.runRecovery();
802
- return;
803
- }
804
- const replayChunks = this.mergeRecoveryChunks(historyChunks, coveredEnd);
805
- await this.writeOrdered(replayChunks, generation, () => this.isRecoveryCurrent(generation, recoverySerial, controller));
806
- if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
807
- return;
808
- if (this.needsRebase) {
809
- this.prepareRetainedLiveRebase();
810
- this.recoveryRunning = false;
811
- await this.runRecovery();
812
- return;
813
- }
814
- this.coveredThroughSequence = Math.max(this.coveredThroughSequence, coveredEnd);
815
- this.scheduledThroughSequence = Math.max(this.scheduledThroughSequence, coveredEnd);
816
- const firstRetainedSequence = this.firstRetainedLiveSequence();
817
- if (this.recoveryKind === 'catch-up'
818
- && firstRetainedSequence !== undefined
819
- && firstRetainedSequence > coveredEnd + 1) {
820
- throw new HistoryContractError('history_coverage_incomplete', 'terminal history coverage has not reached retained live output', firstRetainedSequence);
821
- }
822
- if (this.recoveryKind === 'initial' && !this.baselineReady) {
823
- this.baselineReady = true;
824
- this.resolveBaselineWaiters();
825
- this.emitState();
826
- }
827
- this.retryAttempt = 0;
828
- this.lastError = null;
829
- this.failure = null;
830
- this.preparedHistory = null;
831
- this.setState('live');
832
- this.recoveryRunning = false;
833
- this.drainRetainedLive();
834
- await this.writeChain;
835
- }
836
- catch (error) {
837
- if (!this.isRecoveryCurrent(generation, recoverySerial, controller))
838
- return;
839
- this.lastError = error;
840
- const contract = error instanceof HistoryContractError ? error : null;
841
- this.failure = {
842
- code: contract?.code ?? 'history_fetch_failed',
843
- phase: this.recoveryKind === 'initial' ? 'initial' : 'catch_up',
844
- retryable: contract?.code !== 'history_contract_missing'
845
- && contract?.code !== 'history_contract_invalid'
846
- && contract?.code !== 'history_checkpoint_missing'
847
- && contract?.code !== 'history_checkpoint_invalid'
848
- && contract?.code !== 'history_checkpoint_restore_failed',
849
- attempt: this.retryAttempt,
850
- coveredSequence: this.coveredThroughSequence,
851
- firstRetainedSequence: contract?.firstRetainedSequence,
852
- attachGeneration: generation,
853
- cause: error,
854
- };
855
- this.recoveryRunning = false;
856
- this.scheduleRetry();
857
- }
858
- }
859
- validatePage(page, previousCoverage) {
860
- if (!Object.prototype.hasOwnProperty.call(page, 'coveredThroughSequence')) {
861
- throw new HistoryContractError('history_contract_missing', 'coveredThroughSequence is required');
862
- }
863
- const coverage = normalizeSequence(page.coveredThroughSequence, 'coveredThroughSequence');
864
- if (coverage < previousCoverage) {
865
- throw new HistoryContractError('history_contract_invalid', 'coveredThroughSequence regressed');
866
- }
867
- return coverage;
868
- }
869
- mergeRecoveryChunks(history, coveredEnd) {
870
- const selected = new Map();
871
- const unsequenced = [];
872
- for (const item of history) {
873
- const sequence = this.chunkSequence(item);
874
- if (sequence === undefined)
875
- unsequenced.push(item);
876
- else if (sequence > this.coveredThroughSequence && sequence <= coveredEnd)
877
- selected.set(sequence, item);
878
- }
879
- const remaining = [];
880
- let remainingBytes = 0;
881
- for (const item of this.retainedLive) {
882
- const sequence = this.chunkSequence(item);
883
- if (sequence !== undefined && sequence <= coveredEnd) {
884
- // A live copy preserves terminal query/response semantics that history replay suppresses.
885
- selected.set(sequence, item);
886
- }
887
- else {
888
- remaining.push(item);
889
- remainingBytes += item.data.byteLength;
890
- }
891
- }
892
- this.retainedLive = remaining;
893
- this.retainedLiveBytes = remainingBytes;
894
- return [
895
- ...[...selected.entries()].sort(([left], [right]) => left - right).map(([, item]) => item),
896
- ...unsequenced,
897
- ];
898
- }
899
- async writeOrdered(chunks, generation, canStartBatch = () => this.isCurrent(generation)) {
900
- let batch = [];
901
- let batchBytes = 0;
902
- let batchSource;
903
- let batchGeometry;
904
- let previousHistoryGeometry;
905
- const flush = async () => {
906
- if (batch.length === 0 || !this.isCurrent(generation) || !canStartBatch())
907
- return;
908
- const current = batch;
909
- const source = batchSource;
910
- const geometry = batchGeometry;
911
- batch = [];
912
- batchBytes = 0;
913
- batchSource = undefined;
914
- batchGeometry = undefined;
915
- const accepted = [];
916
- const data = [];
917
- for (const item of current) {
918
- const transformed = this.options.transformChunk ? this.options.transformChunk(item) : item.data;
919
- if (transformed === null)
920
- continue;
921
- accepted.push({ ...item, data: transformed });
922
- data.push(transformed);
923
- }
924
- if (accepted.length === 0 || !this.isCurrent(generation))
925
- return;
926
- const writer = source === 'history' ? (this.options.writeHistory ?? this.options.write) : this.options.write;
927
- if (source === 'history') {
928
- if (!geometry) {
929
- throw new HistoryContractError('history_contract_missing', 'history batch geometry is required');
930
- }
931
- await this.options.applyHistoryGeometry?.(geometry);
932
- }
933
- await this.invokeWriter(writer, concatData(data), accepted, generation);
934
- };
935
- for (const item of chunks) {
936
- const source = item.source ?? 'live';
937
- let itemGeometry;
938
- if (source === 'history') {
939
- itemGeometry = historyGeometryForChunk(item);
940
- previousHistoryGeometry = validateHistoryGeometryOrder(previousHistoryGeometry, itemGeometry);
941
- }
942
- else if (item.geometryGeneration !== undefined
943
- && item.cols !== undefined
944
- && item.rows !== undefined) {
945
- // Live batches carry the PTY geometry that produced their bytes. Keep
946
- // geometry transitions as writer boundaries so relative cursor output
947
- // is never concatenated across grids.
948
- itemGeometry = historyGeometryForChunk(item);
949
- }
950
- if (batch.length > 0 && (source !== batchSource || batchBytes + item.data.byteLength > this.policy.maxWriteBatchBytes
951
- || (batchGeometry && itemGeometry && !sameHistoryGeometry(batchGeometry, itemGeometry)))) {
952
- await flush();
953
- }
954
- batchSource = source;
955
- batchGeometry = itemGeometry;
956
- batch.push(item);
957
- batchBytes += item.data.byteLength;
958
- }
959
- await flush();
960
- }
961
- scheduleRetry() {
962
- if (this.failure && !this.failure.retryable) {
963
- this.setState('failed');
964
- if (!this.baselineReady)
965
- this.resolveBaselineWaiters();
966
- return;
967
- }
968
- const delay = this.policy.retryDelaysMs[this.retryAttempt];
969
- if (delay === undefined) {
970
- this.setState('failed');
971
- if (!this.baselineReady)
972
- this.resolveBaselineWaiters();
973
- return;
974
- }
975
- this.retryAttempt += 1;
976
- this.setState('retry-wait');
977
- const generation = this.generation;
978
- const setTimer = this.options.scheduler?.setTimer ?? setTimeout;
979
- this.retryTimer = setTimer(() => {
980
- this.retryTimer = null;
981
- if (!this.isCurrent(generation))
982
- return;
983
- void this.runRecovery();
984
- }, delay);
985
- this.emitState();
986
- }
987
- retainLive(chunk) {
988
- this.retainedLive.push(chunk);
989
- this.retainedLiveBytes += chunk.data.byteLength;
990
- while (this.retainedLive.length > this.policy.maxRetainedLiveChunks
991
- || this.retainedLiveBytes > this.policy.maxRetainedLiveBytes) {
992
- const removed = this.retainedLive.shift();
993
- if (!removed)
994
- break;
995
- this.retainedLiveBytes -= removed.data.byteLength;
996
- const removedSequence = this.chunkSequence(removed);
997
- if (removedSequence !== undefined) {
998
- this.droppedLiveThroughSequence = Math.max(this.droppedLiveThroughSequence, removedSequence);
999
- }
1000
- this.needsRebase = true;
1001
- }
1002
- this.emitState();
1003
- }
1004
- prepareRetainedLiveRebase() {
1005
- this.markPreparedHistoryRebased(false);
1006
- this.needsRebase = false;
1007
- this.historyRebasePrepared = false;
1008
- this.recoveryKind = this.baselineReady ? 'catch-up' : 'initial';
1009
- this.recoveryStartSequence = 0;
1010
- const firstRetainedSequence = this.firstRetainedLiveSequence();
1011
- this.recoveryEndSequence = Math.max(this.droppedLiveThroughSequence, firstRetainedSequence === undefined ? 0 : firstRetainedSequence - 1) || undefined;
1012
- this.droppedLiveThroughSequence = 0;
1013
- this.coveredThroughSequence = 0;
1014
- this.scheduledThroughSequence = 0;
1015
- this.options.clear?.();
1016
- this.options.onHistoryTruncated?.('retained-live-overflow');
1017
- }
1018
- prepareHistoryGenerationRebase(firstRetainedSequence) {
1019
- this.recoveryKind = this.baselineReady ? 'catch-up' : 'initial';
1020
- this.recoveryStartSequence = 0;
1021
- this.coveredThroughSequence = 0;
1022
- this.scheduledThroughSequence = 0;
1023
- this.options.clear?.();
1024
- this.options.onHistoryTruncated?.('history-evicted');
1025
- this.historyRebasePrepared = true;
1026
- if (firstRetainedSequence !== undefined && firstRetainedSequence > 1) {
1027
- this.recoveryStartSequence = firstRetainedSequence;
1028
- this.coveredThroughSequence = firstRetainedSequence - 1;
1029
- this.scheduledThroughSequence = this.coveredThroughSequence;
1030
- }
1031
- }
1032
- markPreparedHistoryRebased(discard) {
1033
- if (this.preparedHistoryOutcome.status === 'not-provided')
1034
- return;
1035
- this.preparedHistoryOutcome = {
1036
- status: discard ? 'rejected' : this.preparedHistoryOutcome.status,
1037
- rebased: true,
1038
- };
1039
- }
1040
- canRenderLive() {
1041
- return this.active && (this.options.isInteractive?.() ?? true);
1042
- }
1043
- acceptLive(chunk) {
1044
- const sequence = this.chunkSequence(chunk);
1045
- if (sequence !== undefined && sequence <= this.scheduledThroughSequence)
1046
- return;
1047
- if (sequence !== undefined
1048
- && this.explicitAttachFence
1049
- && this.scheduledThroughSequence === 0
1050
- && sequence > 1) {
1051
- this.retainLive(chunk);
1052
- this.beginCatchUp(1);
1053
- return;
1054
- }
1055
- if (sequence !== undefined
1056
- && this.scheduledThroughSequence > 0
1057
- && sequence > this.scheduledThroughSequence + 1) {
1058
- this.retainLive(chunk);
1059
- this.beginCatchUp(this.scheduledThroughSequence + 1);
1060
- return;
1061
- }
1062
- if (sequence !== undefined && this.scheduledThroughSequence === 0) {
1063
- this.coveredThroughSequence = sequence - 1;
1064
- this.scheduledThroughSequence = sequence - 1;
1065
- }
1066
- this.enqueueLiveWrite(chunk);
1067
- }
1068
- enqueueLiveWrite(chunk) {
1069
- const generation = this.generation;
1070
- const sequence = this.chunkSequence(chunk);
1071
- if (sequence !== undefined)
1072
- this.scheduledThroughSequence = Math.max(this.scheduledThroughSequence, sequence);
1073
- this.pendingLiveWrites.push(chunk);
1074
- if (this.liveWriteScheduled)
1075
- return;
1076
- this.liveWriteScheduled = true;
1077
- this.writeChain = this.writeChain.then(async () => {
1078
- if (!this.isCurrent(generation))
1079
- return;
1080
- const pending = this.pendingLiveWrites.splice(0);
1081
- this.liveWriteScheduled = false;
1082
- await this.writeOrdered(pending, generation);
1083
- }).catch(error => {
1084
- this.liveWriteScheduled = false;
1085
- if (!this.isCurrent(generation))
1086
- return;
1087
- this.lastError = error;
1088
- this.failure = {
1089
- code: 'history_fetch_failed',
1090
- phase: 'catch_up',
1091
- retryable: true,
1092
- attempt: this.retryAttempt,
1093
- coveredSequence: this.coveredThroughSequence,
1094
- attachGeneration: generation,
1095
- cause: error,
1096
- };
1097
- this.setState('failed');
1098
- });
1099
- }
1100
- drainRetainedLive() {
1101
- if (!this.canRenderLive() || this.retainedLive.length === 0 || this.state !== 'live')
1102
- return;
1103
- const retained = [...this.retainedLive].sort((left, right) => ((this.chunkSequence(left) ?? Number.MAX_SAFE_INTEGER)
1104
- - (this.chunkSequence(right) ?? Number.MAX_SAFE_INTEGER)));
1105
- this.retainedLive = [];
1106
- this.retainedLiveBytes = 0;
1107
- for (let index = 0; index < retained.length; index += 1) {
1108
- this.acceptLive(retained[index]);
1109
- if (this.state !== 'live') {
1110
- for (const pending of retained.slice(index + 1))
1111
- this.retainLive(pending);
1112
- break;
1113
- }
1114
- }
1115
- }
1116
- async invokeWriter(writer, data, chunks, generation) {
1117
- this.activeWriters += 1;
1118
- try {
1119
- await writer(data, chunks);
1120
- if (!this.isCurrent(generation))
1121
- return;
1122
- for (const item of chunks) {
1123
- const sequence = this.chunkSequence(item);
1124
- if (sequence !== undefined) {
1125
- this.coveredThroughSequence = Math.max(this.coveredThroughSequence, sequence);
1126
- this.scheduledThroughSequence = Math.max(this.scheduledThroughSequence, sequence);
1127
- }
1128
- }
1129
- }
1130
- finally {
1131
- this.activeWriters -= 1;
1132
- if (this.activeWriters === 0) {
1133
- const waiters = this.writerQuiescenceWaiters.splice(0);
1134
- for (const resolve of waiters)
1135
- resolve();
1136
- }
1137
- }
1138
- }
1139
- waitForWriterQuiescence() {
1140
- if (this.activeWriters === 0)
1141
- return Promise.resolve();
1142
- return new Promise(resolve => this.writerQuiescenceWaiters.push(resolve));
1143
- }
1144
- chunkSequence(chunk) {
1145
- const sequence = chunk.sequence;
1146
- return typeof sequence === 'number' && Number.isSafeInteger(sequence) && sequence > 0
1147
- ? sequence
1148
- : undefined;
1149
- }
1150
- firstRetainedLiveSequence() {
1151
- let first;
1152
- for (const item of this.retainedLive) {
1153
- const sequence = this.chunkSequence(item);
1154
- if (sequence !== undefined && (first === undefined || sequence < first))
1155
- first = sequence;
1156
- }
1157
- return first;
1158
- }
1159
- isCurrent(generation) {
1160
- return !this.disposed && generation === this.generation;
1161
- }
1162
- isRecoveryCurrent(generation, recoverySerial, controller) {
1163
- return this.isCurrent(generation)
1164
- && recoverySerial === this.recoverySerial
1165
- && !controller.signal.aborted;
1166
- }
1167
- cancelRecovery(incrementGeneration = true) {
1168
- this.abortController?.abort();
1169
- this.abortController = null;
1170
- this.recoveryRunning = false;
1171
- this.recoverySerial += 1;
1172
- if (this.retryTimer !== null) {
1173
- const clearTimer = this.options.scheduler?.clearTimer ?? clearTimeout;
1174
- clearTimer(this.retryTimer);
1175
- this.retryTimer = null;
1176
- }
1177
- if (incrementGeneration)
1178
- this.generation += 1;
1179
- }
1180
- resolveBaselineWaiters() {
1181
- if (this.baselineWaiters.length === 0)
1182
- return;
1183
- const snapshot = this.getSnapshot();
1184
- const waiters = this.baselineWaiters.splice(0);
1185
- for (const resolve of waiters)
1186
- resolve(snapshot);
1187
- }
1188
- setState(state) {
1189
- this.state = state;
1190
- this.emitState();
1191
- }
1192
- emitState() {
1193
- this.options.onStateChange?.(this.getSnapshot());
1194
- }
1195
- }
1196
- export const createPagedTerminalOutputCoordinator = (options) => new PagedTerminalOutputCoordinator(options);
1197
- //# sourceMappingURL=PagedTerminalOutputCoordinator.js.map