@myagentroam/marctl 0.9.120 → 0.9.122
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/package.json +1 -1
- package/src/direct.mjs +21 -2
- package/src/scp.mjs +29 -3
- package/src/terminal-relay-sender.d.mts +17 -0
- package/src/terminal-relay-sender.mjs +129 -0
- package/src/terminal.mjs +58 -16
package/package.json
CHANGED
package/src/direct.mjs
CHANGED
|
@@ -27,14 +27,17 @@ export async function openDirectTerminal({
|
|
|
27
27
|
WebSocketClass,
|
|
28
28
|
nodeId,
|
|
29
29
|
terminalId,
|
|
30
|
+
manifest,
|
|
30
31
|
afterSequence = 0,
|
|
31
32
|
timeoutMs = DIRECT_SETUP_TIMEOUT_MS,
|
|
32
33
|
heartbeatMs = DIRECT_HEARTBEAT_MS,
|
|
33
34
|
peerConnectionFactory,
|
|
35
|
+
onPrepared,
|
|
36
|
+
onIceConnected,
|
|
34
37
|
onTransportReady,
|
|
35
38
|
signal
|
|
36
39
|
}) {
|
|
37
|
-
if (typeof client.getTerminalManifest !== 'function')
|
|
40
|
+
if (manifest === undefined && typeof client.getTerminalManifest !== 'function')
|
|
38
41
|
throw new DirectFallbackError('DIRECT_MANIFEST_UNAVAILABLE');
|
|
39
42
|
|
|
40
43
|
let live;
|
|
@@ -49,7 +52,7 @@ export async function openDirectTerminal({
|
|
|
49
52
|
let transportNegotiationActive = true;
|
|
50
53
|
let cleanupNative;
|
|
51
54
|
try {
|
|
52
|
-
|
|
55
|
+
manifest ??= await client.getTerminalManifest(nodeId);
|
|
53
56
|
if (
|
|
54
57
|
typeof manifest?.nodeGeneration !== 'string' ||
|
|
55
58
|
manifest.nodeGeneration.length < 8 ||
|
|
@@ -84,7 +87,12 @@ export async function openDirectTerminal({
|
|
|
84
87
|
peerConnectionFactory ?? ((name, options) => new native.PeerConnection(name, options));
|
|
85
88
|
peer = createPeer(`marctl-${transportSessionId}`, { iceServers: [] });
|
|
86
89
|
|
|
90
|
+
// Start negotiation only after both paths finish their control-plane preparation.
|
|
91
|
+
if (onPrepared) await onPrepared();
|
|
92
|
+
if (signal?.aborted) throw new DirectFallbackError('DIRECT_RACE_CANCELLED');
|
|
93
|
+
|
|
87
94
|
const transportReady = new Promise((resolve, reject) => {
|
|
95
|
+
let iceConnected = false;
|
|
88
96
|
let connected = false;
|
|
89
97
|
let serverOpened = false;
|
|
90
98
|
let settled = false;
|
|
@@ -162,6 +170,17 @@ export async function openDirectTerminal({
|
|
|
162
170
|
)
|
|
163
171
|
finish(new DirectFallbackError('DIRECT_CONTROL_UNAVAILABLE'));
|
|
164
172
|
});
|
|
173
|
+
peer.onIceStateChange((state) => {
|
|
174
|
+
if (!transportNegotiationActive || settled || iceConnected) return;
|
|
175
|
+
const normalizedState = String(state).toLowerCase();
|
|
176
|
+
if (normalizedState !== 'connected' && normalizedState !== 'completed') return;
|
|
177
|
+
iceConnected = true;
|
|
178
|
+
try {
|
|
179
|
+
onIceConnected?.();
|
|
180
|
+
} catch (error) {
|
|
181
|
+
finish(error);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
165
184
|
peer.onStateChange((state) => {
|
|
166
185
|
if (!transportNegotiationActive || settled) return;
|
|
167
186
|
const normalizedState = String(state).toLowerCase();
|
package/src/scp.mjs
CHANGED
|
@@ -61,13 +61,29 @@ function workerCommand(mode, file, size, metadata, preserve, sourceBasename) {
|
|
|
61
61
|
class ByteChannel {
|
|
62
62
|
#buffer = Buffer.alloc(0);
|
|
63
63
|
#waiting = [];
|
|
64
|
+
#sending = new Map();
|
|
64
65
|
#ended;
|
|
65
66
|
#failure;
|
|
66
67
|
|
|
67
68
|
constructor(send, signal) {
|
|
68
69
|
this.send = (bytes) => {
|
|
69
70
|
if (this.#failure) throw this.#failure;
|
|
70
|
-
|
|
71
|
+
if (this.#ended !== undefined) throw new MarctlError('Copy stream ended unexpectedly.');
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
this.#sending.set(reject, resolve);
|
|
74
|
+
const settle = (complete, value) => {
|
|
75
|
+
this.#sending.delete(reject);
|
|
76
|
+
complete(value);
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
Promise.resolve(send(bytes)).then(
|
|
80
|
+
(value) => settle(resolve, value),
|
|
81
|
+
(error) => settle(reject, error)
|
|
82
|
+
);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
settle(reject, error);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
71
87
|
};
|
|
72
88
|
if (signal) {
|
|
73
89
|
if (signal.aborted) this.fail(new MarctlError('Copy was interrupted.'));
|
|
@@ -92,11 +108,21 @@ class ByteChannel {
|
|
|
92
108
|
|
|
93
109
|
finish(code) {
|
|
94
110
|
this.#ended = code;
|
|
111
|
+
if (code !== 0) return this.fail(new MarctlError('Remote copy command failed.'));
|
|
112
|
+
// A successful exit confirms the worker consumed its final protocol input.
|
|
113
|
+
for (const resolve of this.#sending.values()) resolve();
|
|
114
|
+
this.#sending.clear();
|
|
95
115
|
this.#wake();
|
|
96
116
|
}
|
|
97
117
|
|
|
118
|
+
connectionEnded(error) {
|
|
119
|
+
if (this.#ended === undefined) this.fail(error);
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
fail(error) {
|
|
99
123
|
this.#failure ??= error;
|
|
124
|
+
for (const reject of this.#sending.keys()) reject(this.#failure);
|
|
125
|
+
this.#sending.clear();
|
|
100
126
|
this.#wake();
|
|
101
127
|
}
|
|
102
128
|
|
|
@@ -231,9 +257,9 @@ async function withRemoteWorker(
|
|
|
231
257
|
}
|
|
232
258
|
};
|
|
233
259
|
const onError = () =>
|
|
234
|
-
channel.
|
|
260
|
+
channel.connectionEnded(new MarctlError(translate('errors.terminalConnectionFailed')));
|
|
235
261
|
const onClose = () =>
|
|
236
|
-
channel.
|
|
262
|
+
channel.connectionEnded(new MarctlError(translate('errors.terminalConnectionLost')));
|
|
237
263
|
transport.on('message', onMessage);
|
|
238
264
|
transport.on('error', onError);
|
|
239
265
|
transport.on('close', onClose);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface RelaySocket {
|
|
2
|
+
readonly readyState: number;
|
|
3
|
+
readonly bufferedAmount?: number;
|
|
4
|
+
send(value: string, callback: (error?: Error) => void): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class TerminalRelaySender {
|
|
8
|
+
constructor(
|
|
9
|
+
socket: RelaySocket,
|
|
10
|
+
options?: {
|
|
11
|
+
readonly onPendingChange?: (keys: ReadonlySet<string>) => void;
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
send(value: string, key?: string): Promise<void>;
|
|
15
|
+
cancel(key: string): void;
|
|
16
|
+
close(error?: Error): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const MAX_PENDING_BYTES = 2 * 1024 * 1024;
|
|
2
|
+
const MAX_SOCKET_BYTES = 256 * 1024;
|
|
3
|
+
|
|
4
|
+
/** Shared sender for CLI input and the Node's aggregate Terminal relay output. */
|
|
5
|
+
export class TerminalRelaySender {
|
|
6
|
+
#socket;
|
|
7
|
+
#onPendingChange;
|
|
8
|
+
#pending = [];
|
|
9
|
+
#pendingBytes = 0;
|
|
10
|
+
#inFlight;
|
|
11
|
+
#timer;
|
|
12
|
+
#immediate;
|
|
13
|
+
#closed = false;
|
|
14
|
+
|
|
15
|
+
constructor(socket, { onPendingChange = () => {} } = {}) {
|
|
16
|
+
this.#socket = socket;
|
|
17
|
+
this.#onPendingChange = onPendingChange;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
send(value, key = '') {
|
|
21
|
+
if (this.#closed || this.#socket.readyState !== 1)
|
|
22
|
+
return Promise.reject(new Error('TERMINAL_CONNECTION_LOST'));
|
|
23
|
+
const bytes = Buffer.byteLength(value, 'utf8');
|
|
24
|
+
if (this.#pendingBytes + bytes > MAX_PENDING_BYTES)
|
|
25
|
+
return Promise.reject(
|
|
26
|
+
Object.assign(new Error('TERMINAL_RELAY_BUFFER_EXCEEDED'), {
|
|
27
|
+
code: 'TERMINAL_RELAY_BUFFER_EXCEEDED'
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
const result = new Promise((resolve, reject) => {
|
|
31
|
+
this.#pending.push({ value, key, bytes, resolve, reject });
|
|
32
|
+
this.#pendingBytes += bytes;
|
|
33
|
+
});
|
|
34
|
+
this.#pump();
|
|
35
|
+
this.#notify();
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
cancel(key) {
|
|
40
|
+
if (this.#inFlight?.key === key) {
|
|
41
|
+
this.#inFlight.cancelled = true;
|
|
42
|
+
this.#inFlight.reject(new Error('TERMINAL_ATTACH_INVALID'));
|
|
43
|
+
}
|
|
44
|
+
const kept = [];
|
|
45
|
+
for (const item of this.#pending) {
|
|
46
|
+
if (item.key !== key) kept.push(item);
|
|
47
|
+
else {
|
|
48
|
+
this.#pendingBytes -= item.bytes;
|
|
49
|
+
item.reject(new Error('TERMINAL_ATTACH_INVALID'));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.#pending = kept;
|
|
53
|
+
if (kept.length === 0) this.#clearScheduled();
|
|
54
|
+
this.#notify();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
close(error = new Error('TERMINAL_CONNECTION_LOST')) {
|
|
58
|
+
if (this.#closed) return;
|
|
59
|
+
this.#closed = true;
|
|
60
|
+
this.#clearScheduled();
|
|
61
|
+
this.#inFlight?.reject(error);
|
|
62
|
+
this.#inFlight = undefined;
|
|
63
|
+
for (const item of this.#pending) item.reject(error);
|
|
64
|
+
this.#pending = [];
|
|
65
|
+
this.#pendingBytes = 0;
|
|
66
|
+
this.#notify();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#pump() {
|
|
70
|
+
if (
|
|
71
|
+
this.#closed ||
|
|
72
|
+
this.#inFlight !== undefined ||
|
|
73
|
+
this.#timer !== undefined ||
|
|
74
|
+
this.#immediate !== undefined ||
|
|
75
|
+
this.#pending.length === 0
|
|
76
|
+
)
|
|
77
|
+
return;
|
|
78
|
+
if (this.#socket.readyState !== 1) {
|
|
79
|
+
this.close();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const buffered = this.#socket.bufferedAmount ?? 0;
|
|
83
|
+
if (buffered > 0 && buffered + this.#pending[0].bytes > MAX_SOCKET_BYTES) {
|
|
84
|
+
// Poll only while the transport is congested; there is no throughput timer.
|
|
85
|
+
this.#timer = setTimeout(() => {
|
|
86
|
+
this.#timer = undefined;
|
|
87
|
+
this.#pump();
|
|
88
|
+
this.#notify();
|
|
89
|
+
}, 20);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const item = this.#pending.shift();
|
|
93
|
+
this.#inFlight = item;
|
|
94
|
+
try {
|
|
95
|
+
this.#socket.send(item.value, (error) => {
|
|
96
|
+
if (this.#inFlight !== item) return;
|
|
97
|
+
if (error) {
|
|
98
|
+
this.close(error);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.#inFlight = undefined;
|
|
102
|
+
this.#pendingBytes -= item.bytes;
|
|
103
|
+
// Even a synchronous write callback must yield before the next frame.
|
|
104
|
+
this.#immediate = setImmediate(() => {
|
|
105
|
+
this.#immediate = undefined;
|
|
106
|
+
this.#pump();
|
|
107
|
+
this.#notify();
|
|
108
|
+
});
|
|
109
|
+
item.resolve();
|
|
110
|
+
this.#notify();
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.close(error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#notify() {
|
|
118
|
+
const keys = new Set(this.#pending.map((item) => item.key));
|
|
119
|
+
if (this.#inFlight !== undefined && !this.#inFlight.cancelled) keys.add(this.#inFlight.key);
|
|
120
|
+
this.#onPendingChange(keys);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#clearScheduled() {
|
|
124
|
+
clearTimeout(this.#timer);
|
|
125
|
+
this.#timer = undefined;
|
|
126
|
+
clearImmediate(this.#immediate);
|
|
127
|
+
this.#immediate = undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/terminal.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { MarctlError } from './config.mjs';
|
|
3
3
|
import { createTranslator } from './i18n/index.mjs';
|
|
4
|
+
import { TerminalRelaySender } from './terminal-relay-sender.mjs';
|
|
4
5
|
|
|
5
6
|
const MAX_INPUT_BYTES = 65536;
|
|
6
7
|
const DEFAULT_ROWS = 24;
|
|
@@ -27,13 +28,16 @@ export async function runTerminalSession({
|
|
|
27
28
|
let transport;
|
|
28
29
|
let result;
|
|
29
30
|
let primaryError;
|
|
31
|
+
let manifest;
|
|
30
32
|
|
|
31
33
|
try {
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
if (binary) {
|
|
35
|
+
if (command === undefined)
|
|
36
|
+
throw new MarctlError(translate('errors.terminalBinaryUnavailable'));
|
|
37
|
+
manifest = await client.getTerminalManifest(nodeId);
|
|
38
|
+
if (manifest?.binaryTerminal !== true)
|
|
39
|
+
throw new MarctlError(translate('errors.terminalBinaryUnavailable'));
|
|
40
|
+
}
|
|
37
41
|
const created = await client.createTerminal({
|
|
38
42
|
nodeId,
|
|
39
43
|
...dimensions,
|
|
@@ -46,17 +50,18 @@ export async function runTerminalSession({
|
|
|
46
50
|
try {
|
|
47
51
|
const openDirectTerminal =
|
|
48
52
|
openDirectTerminalImpl ?? (await import('./direct.mjs')).openDirectTerminal;
|
|
53
|
+
if (raceConnections && manifest === undefined)
|
|
54
|
+
manifest = await client.getTerminalManifest(nodeId).catch(() => undefined);
|
|
49
55
|
const directOptions = {
|
|
50
56
|
client,
|
|
51
57
|
WebSocketClass,
|
|
52
58
|
nodeId,
|
|
53
59
|
terminalId,
|
|
60
|
+
manifest,
|
|
54
61
|
afterSequence: 0,
|
|
55
62
|
timeoutMs: connectionTimeoutMs
|
|
56
63
|
};
|
|
57
|
-
const canRace =
|
|
58
|
-
raceConnections &&
|
|
59
|
-
(await client.getTerminalManifest(nodeId).catch(() => undefined))?.deferredRelay === true;
|
|
64
|
+
const canRace = raceConnections && manifest?.deferredRelay === true;
|
|
60
65
|
transport = canRace
|
|
61
66
|
? await raceTerminalConnections({
|
|
62
67
|
openDirectTerminal,
|
|
@@ -136,6 +141,16 @@ async function raceTerminalConnections({
|
|
|
136
141
|
}) {
|
|
137
142
|
const abort = new AbortController();
|
|
138
143
|
const relayAbort = new AbortController();
|
|
144
|
+
const preparing = new Set(['direct', 'relay']);
|
|
145
|
+
let start;
|
|
146
|
+
const preparation = new Promise((resolve) => {
|
|
147
|
+
start = resolve;
|
|
148
|
+
});
|
|
149
|
+
const prepared = (candidate) => {
|
|
150
|
+
preparing.delete(candidate);
|
|
151
|
+
if (preparing.size === 0) start();
|
|
152
|
+
return preparation;
|
|
153
|
+
};
|
|
139
154
|
let selected;
|
|
140
155
|
let announce;
|
|
141
156
|
const first = new Promise((resolve) => {
|
|
@@ -152,12 +167,19 @@ async function raceTerminalConnections({
|
|
|
152
167
|
const direct = openDirectTerminal({
|
|
153
168
|
...directOptions,
|
|
154
169
|
signal: abort.signal,
|
|
155
|
-
|
|
170
|
+
onPrepared: () => prepared('direct'),
|
|
171
|
+
onIceConnected: () => {
|
|
156
172
|
if (!select('direct')) throw new Error('DIRECT_RACE_CANCELLED');
|
|
173
|
+
},
|
|
174
|
+
onTransportReady: () => {
|
|
175
|
+
if (selected !== 'direct') throw new Error('DIRECT_RACE_CANCELLED');
|
|
157
176
|
}
|
|
158
177
|
}).then(
|
|
159
178
|
(value) => ({ value }),
|
|
160
|
-
(error) =>
|
|
179
|
+
(error) => {
|
|
180
|
+
prepared('direct');
|
|
181
|
+
return { error };
|
|
182
|
+
}
|
|
161
183
|
);
|
|
162
184
|
const relay = prepareRelayTransport({
|
|
163
185
|
client,
|
|
@@ -166,13 +188,17 @@ async function raceTerminalConnections({
|
|
|
166
188
|
terminalId,
|
|
167
189
|
translate,
|
|
168
190
|
connectionTimeoutMs,
|
|
169
|
-
signal: relayAbort.signal
|
|
191
|
+
signal: relayAbort.signal,
|
|
192
|
+
onPrepared: () => prepared('relay')
|
|
170
193
|
}).then(
|
|
171
194
|
(value) => {
|
|
172
195
|
if (!select('relay')) value.close();
|
|
173
196
|
return { value };
|
|
174
197
|
},
|
|
175
|
-
(error) =>
|
|
198
|
+
(error) => {
|
|
199
|
+
prepared('relay');
|
|
200
|
+
return { error };
|
|
201
|
+
}
|
|
176
202
|
);
|
|
177
203
|
const winner = await Promise.race([
|
|
178
204
|
first,
|
|
@@ -252,9 +278,11 @@ async function prepareRelayTransport({
|
|
|
252
278
|
terminalId,
|
|
253
279
|
translate,
|
|
254
280
|
connectionTimeoutMs,
|
|
255
|
-
signal
|
|
281
|
+
signal,
|
|
282
|
+
onPrepared
|
|
256
283
|
}) {
|
|
257
284
|
const ticket = await client.createAttachTicket({ nodeId, terminalId });
|
|
285
|
+
if (onPrepared) await onPrepared();
|
|
258
286
|
if (signal?.aborted) throw new MarctlError(translate('errors.terminalAttachConnection'));
|
|
259
287
|
const socket = client.openTerminalWebSocket(WebSocketClass, {
|
|
260
288
|
terminalId,
|
|
@@ -328,6 +356,7 @@ async function prepareRelayTransport({
|
|
|
328
356
|
|
|
329
357
|
class BufferedRelayTransport extends EventEmitter {
|
|
330
358
|
#socket;
|
|
359
|
+
#sender;
|
|
331
360
|
#terminalId;
|
|
332
361
|
#translate;
|
|
333
362
|
#attached = false;
|
|
@@ -346,6 +375,7 @@ class BufferedRelayTransport extends EventEmitter {
|
|
|
346
375
|
constructor({ socket, terminalId, translate, connectionTimeoutMs }) {
|
|
347
376
|
super();
|
|
348
377
|
this.#socket = socket;
|
|
378
|
+
this.#sender = new TerminalRelaySender(socket);
|
|
349
379
|
this.#terminalId = terminalId;
|
|
350
380
|
this.#translate = translate;
|
|
351
381
|
this.#onOpen = () => {
|
|
@@ -388,6 +418,7 @@ class BufferedRelayTransport extends EventEmitter {
|
|
|
388
418
|
else this.#fail(error);
|
|
389
419
|
};
|
|
390
420
|
this.#onClose = () => {
|
|
421
|
+
this.#sender.close();
|
|
391
422
|
const error = new MarctlError(
|
|
392
423
|
this.#translate(
|
|
393
424
|
this.#attached ? 'errors.terminalConnectionLost' : 'errors.terminalAttachLost'
|
|
@@ -439,12 +470,14 @@ class BufferedRelayTransport extends EventEmitter {
|
|
|
439
470
|
|
|
440
471
|
send(value) {
|
|
441
472
|
if (this.#closed) throw new Error('TERMINAL_CONNECTION_LOST');
|
|
473
|
+
if (JSON.parse(value).type === 'input') return this.#sender.send(value);
|
|
442
474
|
this.#socket.send(value);
|
|
443
475
|
}
|
|
444
476
|
|
|
445
477
|
close() {
|
|
446
478
|
if (this.#closed) return;
|
|
447
479
|
this.#closed = true;
|
|
480
|
+
this.#sender.close();
|
|
448
481
|
clearTimeout(this.#openTimer);
|
|
449
482
|
clearTimeout(this.#attachTimer);
|
|
450
483
|
this.#socket.removeListener?.('open', this.#onOpen);
|
|
@@ -499,6 +532,7 @@ function runAttachedTerminal({
|
|
|
499
532
|
let settled = false;
|
|
500
533
|
let inputEnabled = false;
|
|
501
534
|
let rawModeEnabled = false;
|
|
535
|
+
let inputTasks = 0;
|
|
502
536
|
|
|
503
537
|
const cleanup = () => {
|
|
504
538
|
transport.removeListener?.('message', onMessage);
|
|
@@ -529,13 +563,21 @@ function runAttachedTerminal({
|
|
|
529
563
|
};
|
|
530
564
|
const fail = (key) => finish(new MarctlError(translate(key)));
|
|
531
565
|
const send = (frame) => {
|
|
532
|
-
transport.send(JSON.stringify(frame));
|
|
566
|
+
return transport.send(JSON.stringify(frame));
|
|
533
567
|
};
|
|
534
|
-
const onInput = (data) => {
|
|
568
|
+
const onInput = async (data) => {
|
|
569
|
+
inputTasks += 1;
|
|
570
|
+
stdin.pause?.();
|
|
535
571
|
try {
|
|
536
|
-
for (const chunk of splitInput(data))
|
|
572
|
+
for (const chunk of splitInput(data)) {
|
|
573
|
+
if (settled) return;
|
|
574
|
+
await send({ type: 'input', terminalId, data: chunk });
|
|
575
|
+
}
|
|
537
576
|
} catch {
|
|
538
577
|
fail('errors.terminalInputFailed');
|
|
578
|
+
} finally {
|
|
579
|
+
inputTasks -= 1;
|
|
580
|
+
if (!settled && inputTasks === 0) stdin.resume?.();
|
|
539
581
|
}
|
|
540
582
|
};
|
|
541
583
|
const onResize = () => {
|