shellbase 0.13.2 → 0.13.3
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/dist/agent.js +50 -13
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -57,6 +57,12 @@ const MAX_PENDING_OUTPUT = 256 * 1024;
|
|
|
57
57
|
// 서버가 40회 연속 전송도 거부 없이 받아줬고 이 값이 타이핑 체감 지연의 주범이었음(입력·출력 양쪽에서
|
|
58
58
|
// 각각 최대 120ms). 30ms 로 낮춰 체감을 줄이고, 출력이 몰릴 때는 여전히 초당 33회로 묶인다.
|
|
59
59
|
const MIN_SEND_INTERVAL_MS = 30;
|
|
60
|
+
// 서버 답(ack)을 안 기다리고 잇달아 부칠 수 있는 조각 수. 답을 하나씩 기다리면 조각마다 왕복
|
|
61
|
+
// 한 번(실측 276ms)이 통째로 지연이 되어, 출력이 조금만 나뉘어도 몇 초 뒤에 한꺼번에 나타났다
|
|
62
|
+
// (10조각 = 2.7초. 같은 것을 잇달아 부치면 0.29초 — 순서도 그대로 지켜지는 것을 실측 확인).
|
|
63
|
+
// 그래도 무한정 부치면 통로가 막혔을 때 밀린 조각이 계속 쌓이므로, 이만큼만 부쳐두고
|
|
64
|
+
// 답이 하나 돌아올 때마다 이어서 보낸다 (막히면 저절로 모아 보내는 효과도 있다).
|
|
65
|
+
const MAX_FRAMES_IN_FLIGHT = 6;
|
|
60
66
|
// 그래도 서버가 거부하면(RATE_LIMITED 등) 잠시 간격을 넉넉히 벌려서 재시도한다
|
|
61
67
|
const SEND_PENALTY_MS = 500;
|
|
62
68
|
const PENALTY_INTERVAL_MS = 250;
|
|
@@ -325,23 +331,49 @@ export async function runAgent(options) {
|
|
|
325
331
|
}
|
|
326
332
|
session.flushing = true;
|
|
327
333
|
session.lastSentAt = Date.now();
|
|
334
|
+
// 부치는 *순서*만 이 줄에서 지키고, **서버 답(ack)은 기다리지 않는다**.
|
|
335
|
+
// 예전에는 조각 하나를 부치고 답이 올 때까지(왕복 276ms) 다음 조각을 붙들고 있었다.
|
|
336
|
+
// 그래서 출력이 열 조각으로 나뉘면 2.7초 뒤에야 마지막이 도착했고, 화면에는 한참 아무것도
|
|
337
|
+
// 없다가 한꺼번에 쏟아지는 것처럼 보였다 — "타자가 2초 뒤에 몰아서 보인다" 의 정체.
|
|
328
338
|
await enqueueSend(session, async () => {
|
|
329
339
|
try {
|
|
330
340
|
// 프레임 한도를 넘지 않도록 나눠 보낸다 (한 덩이로 보내면 통째로 거부돼 화면이 멈춘다)
|
|
331
341
|
const parts = chunkByBytes(pending, frameBudget);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
+
session.inFlight += parts.length;
|
|
343
|
+
// send() 를 부르는 순간 이미 통로로 나간다. 부른 순서 = 나가는 순서라서(실측 확인)
|
|
344
|
+
// 답을 기다리지 않아도 조각이 뒤섞이지 않는다.
|
|
345
|
+
const results = parts.map((part) => sendFrame({
|
|
346
|
+
kind: 'output',
|
|
347
|
+
to: session.deviceId,
|
|
348
|
+
data: encryptFrame(part, session.frameKey),
|
|
349
|
+
}));
|
|
350
|
+
void Promise.allSettled(results)
|
|
351
|
+
.then((settled) => {
|
|
352
|
+
let sent = 0;
|
|
353
|
+
while (sent < settled.length && settled[sent].status === 'fulfilled')
|
|
354
|
+
sent += 1;
|
|
355
|
+
if (sent >= settled.length)
|
|
356
|
+
return;
|
|
357
|
+
const reason = settled[sent].reason;
|
|
358
|
+
const message = String(reason?.message ?? reason);
|
|
359
|
+
console.error(`출력 전송 실패 (${sent}/${parts.length} 조각만 나감):`, message);
|
|
360
|
+
if (/too large|TOO_LARGE|size|크기/i.test(message))
|
|
361
|
+
shrinkFrameBudget(message);
|
|
362
|
+
session.sendPenaltyUntil = Date.now() + SEND_PENALTY_MS;
|
|
363
|
+
// 못 간 것을 다시 보내는 대신 **화면을 통째로 다시 보낸다**.
|
|
364
|
+
// 답을 기다리지 않고 잇달아 부치기 때문에, 실패를 알게 된 시점에는 그 뒤의 출력이
|
|
365
|
+
// 이미 나가 있을 수 있다 — 거기에 옛 조각을 뒤늦게 끼워 넣으면 화면이 뒤섞인다.
|
|
366
|
+
// 지금 화면이 어떻게 생겼는지는 session.screen 이 정확히 들고 있어서,
|
|
367
|
+
// 통째로 다시 보내면 빠지지도 겹치지도 않는다.
|
|
368
|
+
if (!session.closing)
|
|
342
369
|
void sendRestore(session);
|
|
343
|
-
|
|
344
|
-
|
|
370
|
+
})
|
|
371
|
+
.finally(() => {
|
|
372
|
+
session.inFlight -= parts.length;
|
|
373
|
+
// 답을 기다리느라 모아둔 것이 있으면 이어서 보낸다
|
|
374
|
+
if (session.outBuffer && !session.closing && !session.restoring)
|
|
375
|
+
scheduleFlush(session);
|
|
376
|
+
});
|
|
345
377
|
}
|
|
346
378
|
catch (err) {
|
|
347
379
|
// 여기서 새어 나간 오류는 아무도 받아주지 않아 프로세스가 통째로 죽는다 — 반드시 잡는다
|
|
@@ -351,7 +383,7 @@ export async function runAgent(options) {
|
|
|
351
383
|
// 무슨 일이 있어도 표시는 반드시 푼다 — 여기 걸린 채로 남으면 그 세션은 그 뒤로
|
|
352
384
|
// 아무 화면도 못 보낸다 (검은 화면의 원인 중 하나)
|
|
353
385
|
session.flushing = false;
|
|
354
|
-
//
|
|
386
|
+
// 보내는 사이에 새로 나온 것이 있으면 곧바로 이어서 보낸다
|
|
355
387
|
if (session.outBuffer && !session.closing && !session.restoring)
|
|
356
388
|
scheduleFlush(session);
|
|
357
389
|
}
|
|
@@ -458,6 +490,10 @@ export async function runAgent(options) {
|
|
|
458
490
|
const scheduleFlush = (session) => {
|
|
459
491
|
if (session.pendingFlush !== null || session.flushing || session.restoring)
|
|
460
492
|
return;
|
|
493
|
+
// 답을 못 받은 조각이 너무 많이 쌓였으면 잠시 모아둔다 — 하나가 돌아오면 그때 이어서 보낸다
|
|
494
|
+
// (그 사이 나온 출력은 한 덩이로 합쳐져서, 막힌 상황에서도 조각 수가 늘지 않는다)
|
|
495
|
+
if (session.inFlight >= MAX_FRAMES_IN_FLIGHT)
|
|
496
|
+
return;
|
|
461
497
|
const interval = Date.now() < session.sendPenaltyUntil ? PENALTY_INTERVAL_MS : MIN_SEND_INTERVAL_MS;
|
|
462
498
|
const wait = Math.max(0, interval - (Date.now() - session.lastSentAt));
|
|
463
499
|
session.pendingFlush = setTimeout(() => void flush(session), wait);
|
|
@@ -578,6 +614,7 @@ export async function runAgent(options) {
|
|
|
578
614
|
approvalInFlight: false,
|
|
579
615
|
outBuffer: '',
|
|
580
616
|
flushing: false,
|
|
617
|
+
inFlight: 0,
|
|
581
618
|
sendChain: Promise.resolve(),
|
|
582
619
|
restoring: false,
|
|
583
620
|
restoreAgain: false,
|