farming-code 2.2.7 → 2.2.11
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 +110 -199
- package/README.zh_cn.md +106 -237
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1496 -737
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +45 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +30 -0
- package/backend/control-api.js +192 -17
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +294 -86
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +81 -4
- package/backend/workspace-directory.js +152 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-CHYpgECZ.js +208 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
- package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
- package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
- package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
- package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
- package/dist/assets/code-dark-B1pAtO_P.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-B8SHldPE.js → index-miO2r9IZ.js} +3 -3
- package/dist/assets/main-DNe0jPw-.css +1 -0
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
- package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1976 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-iA2BU9P6.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-DpYuBonn.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const TerminalScreenWorker = require('./terminal-screen-worker');
|
|
2
2
|
|
|
3
3
|
const DEFAULT_POOL_SIZE = 3;
|
|
4
|
+
const DEFAULT_RETRY_DELAY_MS = 250;
|
|
5
|
+
const MAX_RETRY_DELAY_MS = 5000;
|
|
4
6
|
|
|
5
7
|
function normalizePoolSize(value) {
|
|
6
8
|
const parsed = Number(value);
|
|
@@ -21,6 +23,11 @@ class TerminalScreenWorkerPool {
|
|
|
21
23
|
this.waiters = [];
|
|
22
24
|
this.readyWaiters = [];
|
|
23
25
|
this.pendingStarts = 0;
|
|
26
|
+
this.consecutiveStartFailures = 0;
|
|
27
|
+
this.retryDelayMs = Number.isFinite(options.retryDelayMs)
|
|
28
|
+
? Math.max(0, Number(options.retryDelayMs))
|
|
29
|
+
: DEFAULT_RETRY_DELAY_MS;
|
|
30
|
+
this.retryTimer = null;
|
|
24
31
|
this.disposed = false;
|
|
25
32
|
|
|
26
33
|
this.ensureCapacity();
|
|
@@ -32,6 +39,7 @@ class TerminalScreenWorkerPool {
|
|
|
32
39
|
idle: this.idle.length,
|
|
33
40
|
pendingStarts: this.pendingStarts,
|
|
34
41
|
waiters: this.waiters.length,
|
|
42
|
+
consecutiveStartFailures: this.consecutiveStartFailures,
|
|
35
43
|
};
|
|
36
44
|
}
|
|
37
45
|
|
|
@@ -60,7 +68,7 @@ class TerminalScreenWorkerPool {
|
|
|
60
68
|
const worker = this.idle.shift();
|
|
61
69
|
if (worker) {
|
|
62
70
|
this.ensureCapacity();
|
|
63
|
-
return this.
|
|
71
|
+
return this.prepareCheckedWorker(worker, options);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
return new Promise((resolve, reject) => {
|
|
@@ -70,7 +78,7 @@ class TerminalScreenWorkerPool {
|
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
ensureCapacity() {
|
|
73
|
-
if (this.disposed || this.size <= 0) {
|
|
81
|
+
if (this.disposed || this.size <= 0 || this.retryTimer) {
|
|
74
82
|
return;
|
|
75
83
|
}
|
|
76
84
|
|
|
@@ -82,9 +90,11 @@ class TerminalScreenWorkerPool {
|
|
|
82
90
|
|
|
83
91
|
startWorker() {
|
|
84
92
|
this.pendingStarts += 1;
|
|
93
|
+
let failed = false;
|
|
85
94
|
this.createReadyWorker()
|
|
86
95
|
.then((worker) => {
|
|
87
96
|
this.pendingStarts -= 1;
|
|
97
|
+
this.consecutiveStartFailures = 0;
|
|
88
98
|
if (this.disposed) {
|
|
89
99
|
worker.dispose().catch(() => {});
|
|
90
100
|
return;
|
|
@@ -92,18 +102,41 @@ class TerminalScreenWorkerPool {
|
|
|
92
102
|
this.deliverWorker(worker);
|
|
93
103
|
})
|
|
94
104
|
.catch((error) => {
|
|
105
|
+
failed = true;
|
|
95
106
|
this.pendingStarts -= 1;
|
|
107
|
+
this.consecutiveStartFailures += 1;
|
|
96
108
|
const waiter = this.waiters.shift();
|
|
97
109
|
if (waiter) {
|
|
98
110
|
waiter.reject(error);
|
|
99
111
|
}
|
|
112
|
+
if (this.pendingStarts === 0 && this.idle.length < this.size) {
|
|
113
|
+
const readyWaiters = this.readyWaiters.splice(0);
|
|
114
|
+
readyWaiters.forEach(({ reject }) => reject(error));
|
|
115
|
+
}
|
|
100
116
|
})
|
|
101
117
|
.finally(() => {
|
|
102
118
|
this.notifyReadyWaiters();
|
|
103
|
-
|
|
119
|
+
if (failed) {
|
|
120
|
+
this.scheduleCapacityRetry();
|
|
121
|
+
} else {
|
|
122
|
+
this.ensureCapacity();
|
|
123
|
+
}
|
|
104
124
|
});
|
|
105
125
|
}
|
|
106
126
|
|
|
127
|
+
scheduleCapacityRetry() {
|
|
128
|
+
if (this.disposed || this.size <= 0 || this.retryTimer) return;
|
|
129
|
+
const delayMs = Math.min(
|
|
130
|
+
MAX_RETRY_DELAY_MS,
|
|
131
|
+
this.retryDelayMs * Math.max(1, 2 ** Math.min(this.consecutiveStartFailures - 1, 5)),
|
|
132
|
+
);
|
|
133
|
+
this.retryTimer = setTimeout(() => {
|
|
134
|
+
this.retryTimer = null;
|
|
135
|
+
this.ensureCapacity();
|
|
136
|
+
}, delayMs);
|
|
137
|
+
this.retryTimer.unref?.();
|
|
138
|
+
}
|
|
139
|
+
|
|
107
140
|
async createReadyWorker() {
|
|
108
141
|
const worker = new this.WorkerClass(this.workerOptions);
|
|
109
142
|
try {
|
|
@@ -122,12 +155,28 @@ class TerminalScreenWorkerPool {
|
|
|
122
155
|
async prepareWorker(worker, options = {}) {
|
|
123
156
|
const cols = Number(options.cols || this.workerOptions.cols || 80);
|
|
124
157
|
const rows = Number(options.rows || this.workerOptions.rows || 30);
|
|
125
|
-
if (
|
|
126
|
-
await worker.
|
|
158
|
+
if (typeof options.runtimeEpoch === 'string') {
|
|
159
|
+
await worker.setRuntimeEpoch(options.runtimeEpoch, cols, rows);
|
|
160
|
+
} else if (Number.isFinite(cols) && cols > 0 && Number.isFinite(rows) && rows > 0) {
|
|
161
|
+
await worker.resize(cols, rows, 1);
|
|
127
162
|
}
|
|
128
163
|
return worker;
|
|
129
164
|
}
|
|
130
165
|
|
|
166
|
+
async prepareCheckedWorker(worker, options = {}) {
|
|
167
|
+
try {
|
|
168
|
+
return await this.prepareWorker(worker, options);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
try {
|
|
171
|
+
await worker.dispose();
|
|
172
|
+
} catch {
|
|
173
|
+
// best effort
|
|
174
|
+
}
|
|
175
|
+
this.ensureCapacity();
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
131
180
|
deliverWorker(worker) {
|
|
132
181
|
const waiter = this.waiters.shift();
|
|
133
182
|
if (!waiter) {
|
|
@@ -136,7 +185,7 @@ class TerminalScreenWorkerPool {
|
|
|
136
185
|
return;
|
|
137
186
|
}
|
|
138
187
|
|
|
139
|
-
this.
|
|
188
|
+
this.prepareCheckedWorker(worker, waiter.options).then(waiter.resolve, waiter.reject);
|
|
140
189
|
}
|
|
141
190
|
|
|
142
191
|
notifyReadyWaiters() {
|
|
@@ -153,6 +202,10 @@ class TerminalScreenWorkerPool {
|
|
|
153
202
|
}
|
|
154
203
|
|
|
155
204
|
this.disposed = true;
|
|
205
|
+
if (this.retryTimer) {
|
|
206
|
+
clearTimeout(this.retryTimer);
|
|
207
|
+
this.retryTimer = null;
|
|
208
|
+
}
|
|
156
209
|
const error = new Error('Terminal screen worker pool is disposed');
|
|
157
210
|
this.waiters.splice(0).forEach(({ reject }) => reject(error));
|
|
158
211
|
this.readyWaiters.splice(0).forEach(({ reject }) => reject(error));
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
const { parentPort, workerData } = require('worker_threads');
|
|
2
2
|
const TerminalScreenState = require('./terminal-screen-state');
|
|
3
3
|
|
|
4
|
-
const PREVIEW_FLUSH_INTERVAL_MS =
|
|
5
|
-
const MAX_PENDING_DATA_BYTES = 128 * 1024;
|
|
4
|
+
const PREVIEW_FLUSH_INTERVAL_MS = 50;
|
|
6
5
|
|
|
7
6
|
const screenState = new TerminalScreenState(workerData || {});
|
|
8
|
-
let
|
|
9
|
-
let
|
|
7
|
+
let runtimeEpoch = typeof workerData.runtimeEpoch === 'string' ? workerData.runtimeEpoch : '';
|
|
8
|
+
let appliedOutputSeq = 0;
|
|
9
|
+
let appliedStateRevision = 0;
|
|
10
10
|
let lastPreviewText = '';
|
|
11
11
|
let lastTitle = '';
|
|
12
12
|
let lastSnapshotFingerprint = '';
|
|
13
13
|
let messageQueue = Promise.resolve();
|
|
14
|
-
|
|
15
|
-
function byteLength(value) {
|
|
16
|
-
return Buffer.byteLength(String(value || ''), 'utf8');
|
|
17
|
-
}
|
|
14
|
+
let previewFlushTimer = null;
|
|
18
15
|
|
|
19
16
|
function postPreview(state) {
|
|
20
17
|
const previewText = state.previewText || '';
|
|
@@ -39,86 +36,129 @@ function postPreview(state) {
|
|
|
39
36
|
});
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
pendingData = '';
|
|
51
|
-
const state = await screenState.write(data);
|
|
52
|
-
if (options.emitPreview !== false) {
|
|
53
|
-
postPreview(state);
|
|
54
|
-
}
|
|
55
|
-
return screenState.getState({ includeRenderOutput: options.includeRenderOutput });
|
|
56
|
-
}
|
|
39
|
+
function withOutputSeq(state) {
|
|
40
|
+
return {
|
|
41
|
+
...state,
|
|
42
|
+
runtimeEpoch,
|
|
43
|
+
outputSeq: appliedOutputSeq,
|
|
44
|
+
stateRevision: appliedStateRevision,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
57
47
|
|
|
58
|
-
|
|
48
|
+
function currentState(options = {}) {
|
|
49
|
+
const state = screenState.getState({
|
|
50
|
+
includeRenderOutput: options.includeRenderOutput,
|
|
51
|
+
refreshPreview: options.refreshPreview,
|
|
52
|
+
});
|
|
59
53
|
if (options.emitPreview) {
|
|
60
54
|
postPreview(state);
|
|
61
55
|
}
|
|
62
|
-
return state;
|
|
56
|
+
return withOutputSeq(state);
|
|
63
57
|
}
|
|
64
58
|
|
|
65
|
-
function
|
|
66
|
-
if (previewFlushTimer)
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
59
|
+
function schedulePreview() {
|
|
60
|
+
if (previewFlushTimer) return;
|
|
70
61
|
previewFlushTimer = setTimeout(() => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
62
|
+
previewFlushTimer = null;
|
|
63
|
+
postPreview(currentState({
|
|
64
|
+
includeRenderOutput: false,
|
|
65
|
+
refreshPreview: true,
|
|
66
|
+
}));
|
|
77
67
|
}, PREVIEW_FLUSH_INTERVAL_MS);
|
|
68
|
+
if (typeof previewFlushTimer.unref === 'function') previewFlushTimer.unref();
|
|
78
69
|
}
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
await flushPending({ includeRenderOutput: false, emitPreview: false });
|
|
71
|
+
function assertNextRevision(stateRevision, transition) {
|
|
72
|
+
if (!Number.isFinite(stateRevision) || stateRevision !== appliedStateRevision + 1) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Terminal screen ${transition} revision gap: expected ${appliedStateRevision + 1}, received ${stateRevision}`,
|
|
75
|
+
);
|
|
86
76
|
}
|
|
77
|
+
}
|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
79
|
+
async function appendEntries(rawEntries) {
|
|
80
|
+
const entries = Array.isArray(rawEntries) ? rawEntries : [];
|
|
81
|
+
if (entries.length === 0) return;
|
|
82
|
+
|
|
83
|
+
let expectedRevision = appliedStateRevision + 1;
|
|
84
|
+
let nextOutputSeq = appliedOutputSeq;
|
|
85
|
+
let data = '';
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const stateRevision = Number(entry && entry.stateRevision);
|
|
88
|
+
if (!Number.isFinite(stateRevision) || stateRevision !== expectedRevision) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Terminal screen append revision gap: expected ${expectedRevision}, received ${entry && entry.stateRevision}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const text = String((entry && entry.data) || '');
|
|
94
|
+
if (!text) {
|
|
95
|
+
throw new Error(`Terminal screen append revision ${stateRevision} has no data`);
|
|
96
|
+
}
|
|
97
|
+
const outputSeq = Number(entry && entry.outputSeq);
|
|
98
|
+
if (!Number.isFinite(outputSeq) || outputSeq !== nextOutputSeq + 1) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Terminal screen output sequence gap: expected ${nextOutputSeq + 1}, received ${entry && entry.outputSeq}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
data += text;
|
|
104
|
+
nextOutputSeq = outputSeq;
|
|
105
|
+
expectedRevision += 1;
|
|
92
106
|
}
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
const state = await screenState.write(data);
|
|
109
|
+
appliedStateRevision = expectedRevision - 1;
|
|
110
|
+
appliedOutputSeq = nextOutputSeq;
|
|
111
|
+
schedulePreview();
|
|
112
|
+
return state;
|
|
96
113
|
}
|
|
97
114
|
|
|
98
115
|
async function handleRequest(message) {
|
|
99
116
|
switch (message.type) {
|
|
100
117
|
case 'append':
|
|
101
|
-
await
|
|
102
|
-
return
|
|
118
|
+
await appendEntries(message.entries);
|
|
119
|
+
return currentState({
|
|
120
|
+
includeRenderOutput: false,
|
|
121
|
+
refreshPreview: false,
|
|
122
|
+
});
|
|
123
|
+
case 'set-runtime-epoch':
|
|
124
|
+
runtimeEpoch = typeof message.runtimeEpoch === 'string' ? message.runtimeEpoch : '';
|
|
125
|
+
appliedOutputSeq = 0;
|
|
126
|
+
appliedStateRevision = 0;
|
|
127
|
+
if (Number.isFinite(message.cols) && Number.isFinite(message.rows)) {
|
|
128
|
+
screenState.resize(message.cols, message.rows);
|
|
129
|
+
}
|
|
130
|
+
return currentState({ includeRenderOutput: false });
|
|
103
131
|
case 'resize': {
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
assertNextRevision(message.stateRevision, 'resize');
|
|
133
|
+
screenState.resize(message.cols, message.rows);
|
|
134
|
+
appliedStateRevision = message.stateRevision;
|
|
135
|
+
const state = screenState.getState({ includeRenderOutput: true });
|
|
106
136
|
postPreview(state);
|
|
107
|
-
return state;
|
|
137
|
+
return withOutputSeq(state);
|
|
108
138
|
}
|
|
109
139
|
case 'clear': {
|
|
110
|
-
|
|
140
|
+
assertNextRevision(message.stateRevision, 'clear');
|
|
111
141
|
const state = await screenState.clearBuffer();
|
|
142
|
+
const outputSeq = Number(message.outputSeq);
|
|
143
|
+
if (!Number.isFinite(outputSeq) || outputSeq !== appliedOutputSeq) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Terminal screen clear output sequence mismatch: expected ${appliedOutputSeq}, received ${message.outputSeq}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
appliedStateRevision = message.stateRevision;
|
|
112
149
|
postPreview(state);
|
|
113
|
-
return state;
|
|
150
|
+
return withOutputSeq(state);
|
|
114
151
|
}
|
|
115
152
|
case 'get-state':
|
|
116
|
-
return
|
|
153
|
+
return currentState({
|
|
117
154
|
includeRenderOutput: message.includeRenderOutput !== false,
|
|
118
155
|
emitPreview: false,
|
|
119
156
|
});
|
|
120
157
|
case 'dispose':
|
|
121
|
-
|
|
158
|
+
if (previewFlushTimer) {
|
|
159
|
+
clearTimeout(previewFlushTimer);
|
|
160
|
+
previewFlushTimer = null;
|
|
161
|
+
}
|
|
122
162
|
screenState.dispose();
|
|
123
163
|
return { disposed: true };
|
|
124
164
|
default:
|
|
@@ -6,6 +6,7 @@ const { Worker } = require('worker_threads');
|
|
|
6
6
|
const APPEND_FLUSH_INTERVAL_MS = 16;
|
|
7
7
|
const MAX_PENDING_APPEND_BYTES = 128 * 1024;
|
|
8
8
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
9
|
+
const DEFAULT_STATE_REQUEST_HARD_TIMEOUT_MS = 5000;
|
|
9
10
|
const PACKAGED_WORKER_FILE = 'terminal-screen-worker-thread.pkg.js';
|
|
10
11
|
const SOURCE_WORKER_FILE = 'terminal-screen-worker-thread.js';
|
|
11
12
|
|
|
@@ -28,11 +29,17 @@ class TerminalScreenWorker extends EventEmitter {
|
|
|
28
29
|
super();
|
|
29
30
|
this.nextRequestId = 1;
|
|
30
31
|
this.pendingRequests = new Map();
|
|
31
|
-
this.
|
|
32
|
+
this.pendingAppendEntries = [];
|
|
33
|
+
this.pendingAppendBytes = 0;
|
|
34
|
+
this.pendingAppendWaiters = [];
|
|
32
35
|
this.appendFlushTimer = null;
|
|
36
|
+
this.stateRequestInFlight = null;
|
|
33
37
|
this.requestTimeoutMs = Number.isFinite(options.requestTimeoutMs)
|
|
34
38
|
? Math.max(1, Math.floor(options.requestTimeoutMs))
|
|
35
39
|
: DEFAULT_REQUEST_TIMEOUT_MS;
|
|
40
|
+
this.stateRequestHardTimeoutMs = Number.isFinite(options.stateRequestHardTimeoutMs)
|
|
41
|
+
? Math.max(1, Math.floor(options.stateRequestHardTimeoutMs))
|
|
42
|
+
: Math.min(this.requestTimeoutMs, DEFAULT_STATE_REQUEST_HARD_TIMEOUT_MS);
|
|
36
43
|
this.failed = false;
|
|
37
44
|
this.disposed = false;
|
|
38
45
|
const workerFile = resolveWorkerFile();
|
|
@@ -40,6 +47,7 @@ class TerminalScreenWorker extends EventEmitter {
|
|
|
40
47
|
const workerData = { ...options };
|
|
41
48
|
delete workerData.WorkerClass;
|
|
42
49
|
delete workerData.requestTimeoutMs;
|
|
50
|
+
delete workerData.stateRequestHardTimeoutMs;
|
|
43
51
|
this.worker = new WorkerClass(path.join(__dirname, workerFile), {
|
|
44
52
|
workerData,
|
|
45
53
|
});
|
|
@@ -79,7 +87,7 @@ class TerminalScreenWorker extends EventEmitter {
|
|
|
79
87
|
});
|
|
80
88
|
|
|
81
89
|
this.worker.on('error', (error) => {
|
|
82
|
-
this.
|
|
90
|
+
this.handleWorkerFailure(error);
|
|
83
91
|
});
|
|
84
92
|
|
|
85
93
|
this.worker.on('exit', (code) => {
|
|
@@ -87,26 +95,34 @@ class TerminalScreenWorker extends EventEmitter {
|
|
|
87
95
|
return;
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
this.failed = true;
|
|
92
|
-
this.pendingRequests.forEach(({ reject, timer }) => {
|
|
93
|
-
clearTimeout(timer);
|
|
94
|
-
reject(error);
|
|
95
|
-
});
|
|
96
|
-
this.pendingRequests.clear();
|
|
97
|
-
this.emit('error', error);
|
|
98
|
+
this.handleWorkerFailure(new Error(`Terminal screen worker exited unexpectedly with code ${code}`));
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
+
handleWorkerFailure(error) {
|
|
102
103
|
if (this.disposed) return;
|
|
104
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
105
|
+
const shouldEmit = !this.failed;
|
|
103
106
|
this.failed = true;
|
|
104
107
|
if (this.appendFlushTimer) {
|
|
105
108
|
clearTimeout(this.appendFlushTimer);
|
|
106
109
|
this.appendFlushTimer = null;
|
|
107
110
|
}
|
|
108
|
-
this.
|
|
109
|
-
this.
|
|
111
|
+
this.pendingAppendEntries = [];
|
|
112
|
+
this.pendingAppendBytes = 0;
|
|
113
|
+
this.pendingAppendWaiters.splice(0).forEach(({ reject }) => reject(failure));
|
|
114
|
+
this.pendingRequests.forEach(({ reject, timer }) => {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
reject(failure);
|
|
117
|
+
});
|
|
118
|
+
this.pendingRequests.clear();
|
|
119
|
+
if (shouldEmit && this.listenerCount('error') > 0) {
|
|
120
|
+
this.emit('error', failure);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
handlePostMessageFailure(error) {
|
|
125
|
+
this.handleWorkerFailure(error);
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
postWorkerMessage(message, options = {}) {
|
|
@@ -125,20 +141,24 @@ class TerminalScreenWorker extends EventEmitter {
|
|
|
125
141
|
clearTimeout(this.appendFlushTimer);
|
|
126
142
|
this.appendFlushTimer = null;
|
|
127
143
|
}
|
|
128
|
-
if (this.disposed || this.failed ||
|
|
129
|
-
this.
|
|
144
|
+
if (this.disposed || this.failed || this.pendingAppendEntries.length === 0) {
|
|
145
|
+
this.pendingAppendEntries = [];
|
|
146
|
+
this.pendingAppendBytes = 0;
|
|
130
147
|
return;
|
|
131
148
|
}
|
|
132
149
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
this.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
})
|
|
150
|
+
const entries = this.pendingAppendEntries;
|
|
151
|
+
const waiters = this.pendingAppendWaiters;
|
|
152
|
+
this.pendingAppendEntries = [];
|
|
153
|
+
this.pendingAppendBytes = 0;
|
|
154
|
+
this.pendingAppendWaiters = [];
|
|
155
|
+
this.request('append', { entries }, { flushAppend: false }).then(
|
|
156
|
+
state => waiters.forEach(({ resolve }) => resolve(state)),
|
|
157
|
+
error => waiters.forEach(({ reject }) => reject(error)),
|
|
158
|
+
);
|
|
139
159
|
}
|
|
140
160
|
|
|
141
|
-
request(type, payload = {}) {
|
|
161
|
+
request(type, payload = {}, options = {}) {
|
|
142
162
|
if (this.disposed) {
|
|
143
163
|
return Promise.reject(new Error('Terminal screen worker is disposed'));
|
|
144
164
|
}
|
|
@@ -146,7 +166,9 @@ class TerminalScreenWorker extends EventEmitter {
|
|
|
146
166
|
return Promise.reject(new Error('Terminal screen worker is not available'));
|
|
147
167
|
}
|
|
148
168
|
|
|
149
|
-
|
|
169
|
+
if (options.flushAppend !== false) {
|
|
170
|
+
this.flushAppend();
|
|
171
|
+
}
|
|
150
172
|
if (this.failed) {
|
|
151
173
|
return Promise.reject(new Error('Terminal screen worker is not available'));
|
|
152
174
|
}
|
|
@@ -154,12 +176,15 @@ class TerminalScreenWorker extends EventEmitter {
|
|
|
154
176
|
this.nextRequestId += 1;
|
|
155
177
|
|
|
156
178
|
return new Promise((resolve, reject) => {
|
|
179
|
+
const requestTimeoutMs = Number.isFinite(options.timeoutMs)
|
|
180
|
+
? Math.max(1, Math.floor(options.timeoutMs))
|
|
181
|
+
: this.requestTimeoutMs;
|
|
157
182
|
const timer = setTimeout(() => {
|
|
158
183
|
this.pendingRequests.delete(requestId);
|
|
159
184
|
const error = new Error(`Terminal screen worker request timed out: ${type}`);
|
|
160
185
|
error.code = 'ETIMEDOUT';
|
|
161
186
|
reject(error);
|
|
162
|
-
},
|
|
187
|
+
}, requestTimeoutMs);
|
|
163
188
|
if (typeof timer.unref === 'function') timer.unref();
|
|
164
189
|
|
|
165
190
|
this.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
@@ -172,56 +197,108 @@ class TerminalScreenWorker extends EventEmitter {
|
|
|
172
197
|
} catch (error) {
|
|
173
198
|
this.pendingRequests.delete(requestId);
|
|
174
199
|
clearTimeout(timer);
|
|
175
|
-
this.
|
|
200
|
+
this.handlePostMessageFailure(error);
|
|
176
201
|
reject(error);
|
|
177
202
|
}
|
|
178
203
|
});
|
|
179
204
|
}
|
|
180
205
|
|
|
181
|
-
append(data) {
|
|
206
|
+
append(data, stateRevision, outputSeq = null) {
|
|
182
207
|
if (this.disposed || this.failed) {
|
|
183
|
-
return;
|
|
208
|
+
return Promise.reject(new Error('Terminal screen worker is not available'));
|
|
184
209
|
}
|
|
185
210
|
|
|
186
211
|
const text = String(data || '');
|
|
187
212
|
if (!text) return;
|
|
213
|
+
if (!Number.isFinite(stateRevision)) {
|
|
214
|
+
this.handlePostMessageFailure(new Error('Terminal screen append requires a finite state revision'));
|
|
215
|
+
return Promise.reject(new Error('Terminal screen append requires a finite state revision'));
|
|
216
|
+
}
|
|
188
217
|
|
|
189
218
|
if (
|
|
190
|
-
this.
|
|
191
|
-
|
|
219
|
+
this.pendingAppendEntries.length > 0 &&
|
|
220
|
+
this.pendingAppendBytes + byteLength(text) > MAX_PENDING_APPEND_BYTES
|
|
192
221
|
) {
|
|
193
222
|
this.flushAppend();
|
|
194
223
|
}
|
|
195
224
|
|
|
196
|
-
|
|
197
|
-
this.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
this.pendingAppendEntries.push({ data: text, stateRevision, outputSeq });
|
|
227
|
+
this.pendingAppendBytes += byteLength(text);
|
|
228
|
+
this.pendingAppendWaiters.push({ resolve, reject });
|
|
229
|
+
if (byteLength(text) > MAX_PENDING_APPEND_BYTES) {
|
|
230
|
+
this.flushAppend();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (this.appendFlushTimer) return;
|
|
234
|
+
this.appendFlushTimer = setTimeout(() => {
|
|
235
|
+
this.flushAppend();
|
|
236
|
+
}, APPEND_FLUSH_INTERVAL_MS);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
208
239
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}, APPEND_FLUSH_INTERVAL_MS);
|
|
240
|
+
resize(cols, rows, stateRevision) {
|
|
241
|
+
return this.request('resize', { cols, rows, stateRevision });
|
|
212
242
|
}
|
|
213
243
|
|
|
214
|
-
|
|
215
|
-
return this.request('
|
|
244
|
+
setRuntimeEpoch(runtimeEpoch, cols, rows) {
|
|
245
|
+
return this.request('set-runtime-epoch', { runtimeEpoch, cols, rows });
|
|
216
246
|
}
|
|
217
247
|
|
|
218
|
-
clear() {
|
|
219
|
-
return this.request('clear');
|
|
248
|
+
clear(stateRevision, outputSeq = null) {
|
|
249
|
+
return this.request('clear', { stateRevision, outputSeq });
|
|
220
250
|
}
|
|
221
251
|
|
|
222
252
|
getState(options = {}) {
|
|
223
|
-
|
|
224
|
-
|
|
253
|
+
if (!this.stateRequestInFlight) {
|
|
254
|
+
const request = this.request('get-state', {
|
|
255
|
+
// A full checkpoint can satisfy callers that only need metadata too,
|
|
256
|
+
// while the reverse would make coalescing unsafe.
|
|
257
|
+
includeRenderOutput: true,
|
|
258
|
+
}, {
|
|
259
|
+
// Caller deadlines are deliberately softer than this shared deadline.
|
|
260
|
+
// A timed-out caller may stop waiting, but the single-flight itself
|
|
261
|
+
// must never poison every later checkpoint for the generic 30s worker
|
|
262
|
+
// request timeout. Crossing this hard deadline means the authoritative
|
|
263
|
+
// reducer can no longer prove progress, so fail it closed.
|
|
264
|
+
timeoutMs: this.stateRequestHardTimeoutMs,
|
|
265
|
+
});
|
|
266
|
+
const sharedRequest = request.catch((error) => {
|
|
267
|
+
if (error && error.code === 'ETIMEDOUT') {
|
|
268
|
+
this.handleWorkerFailure(error);
|
|
269
|
+
}
|
|
270
|
+
throw error;
|
|
271
|
+
}).finally(() => {
|
|
272
|
+
if (this.stateRequestInFlight === sharedRequest) {
|
|
273
|
+
this.stateRequestInFlight = null;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
this.stateRequestInFlight = sharedRequest;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const sharedRequest = this.stateRequestInFlight;
|
|
280
|
+
const timeoutMs = Number.isFinite(options.timeoutMs)
|
|
281
|
+
? Math.max(1, Math.floor(options.timeoutMs))
|
|
282
|
+
: null;
|
|
283
|
+
if (timeoutMs === null) return sharedRequest;
|
|
284
|
+
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
const timer = setTimeout(() => {
|
|
287
|
+
const error = new Error('Terminal screen worker request timed out: get-state');
|
|
288
|
+
error.code = 'ETIMEDOUT';
|
|
289
|
+
reject(error);
|
|
290
|
+
}, timeoutMs);
|
|
291
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
292
|
+
sharedRequest.then(
|
|
293
|
+
(state) => {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
resolve(state);
|
|
296
|
+
},
|
|
297
|
+
(error) => {
|
|
298
|
+
clearTimeout(timer);
|
|
299
|
+
reject(error);
|
|
300
|
+
},
|
|
301
|
+
);
|
|
225
302
|
});
|
|
226
303
|
}
|
|
227
304
|
|
|
@@ -239,7 +316,11 @@ class TerminalScreenWorker extends EventEmitter {
|
|
|
239
316
|
clearTimeout(this.appendFlushTimer);
|
|
240
317
|
this.appendFlushTimer = null;
|
|
241
318
|
}
|
|
242
|
-
this.
|
|
319
|
+
this.pendingAppendEntries = [];
|
|
320
|
+
this.pendingAppendBytes = 0;
|
|
321
|
+
this.pendingAppendWaiters.splice(0).forEach(({ reject }) => {
|
|
322
|
+
reject(new Error('Terminal screen worker is disposed'));
|
|
323
|
+
});
|
|
243
324
|
this.disposed = true;
|
|
244
325
|
this.pendingRequests.forEach(({ reject, timer }) => {
|
|
245
326
|
clearTimeout(timer);
|
|
@@ -253,3 +334,4 @@ class TerminalScreenWorker extends EventEmitter {
|
|
|
253
334
|
module.exports = TerminalScreenWorker;
|
|
254
335
|
module.exports.resolveWorkerFile = resolveWorkerFile;
|
|
255
336
|
module.exports.DEFAULT_REQUEST_TIMEOUT_MS = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
337
|
+
module.exports.DEFAULT_STATE_REQUEST_HARD_TIMEOUT_MS = DEFAULT_STATE_REQUEST_HARD_TIMEOUT_MS;
|