shellbase 0.13.2 → 0.13.4
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 +54 -14
- 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;
|
|
@@ -288,10 +294,13 @@ export async function runAgent(options) {
|
|
|
288
294
|
// (세션을 전환할 때마다 검은 화면이 몇 초씩 남다가 뒤늦게 돌아오던 원인).
|
|
289
295
|
// 통로는 부친 순서대로 내보내므로, 한꺼번에 부치고 결과만 모아서 확인한다.
|
|
290
296
|
const sendParts = async (session, parts) => {
|
|
291
|
-
const results = await Promise.allSettled(parts.map((part) => sendFrame({
|
|
297
|
+
const results = await Promise.allSettled(parts.map((part, index) => sendFrame({
|
|
292
298
|
kind: 'output',
|
|
293
299
|
to: session.deviceId,
|
|
294
300
|
data: encryptFrame(part, session.frameKey),
|
|
301
|
+
// 복원 조각임을 표시해서, 폰이 다 모은 뒤 한 번에 그리게 한다 (검은 화면 방지)
|
|
302
|
+
seq: index,
|
|
303
|
+
total: parts.length,
|
|
295
304
|
})));
|
|
296
305
|
let sent = 0;
|
|
297
306
|
while (sent < results.length && results[sent].status === 'fulfilled')
|
|
@@ -325,23 +334,49 @@ export async function runAgent(options) {
|
|
|
325
334
|
}
|
|
326
335
|
session.flushing = true;
|
|
327
336
|
session.lastSentAt = Date.now();
|
|
337
|
+
// 부치는 *순서*만 이 줄에서 지키고, **서버 답(ack)은 기다리지 않는다**.
|
|
338
|
+
// 예전에는 조각 하나를 부치고 답이 올 때까지(왕복 276ms) 다음 조각을 붙들고 있었다.
|
|
339
|
+
// 그래서 출력이 열 조각으로 나뉘면 2.7초 뒤에야 마지막이 도착했고, 화면에는 한참 아무것도
|
|
340
|
+
// 없다가 한꺼번에 쏟아지는 것처럼 보였다 — "타자가 2초 뒤에 몰아서 보인다" 의 정체.
|
|
328
341
|
await enqueueSend(session, async () => {
|
|
329
342
|
try {
|
|
330
343
|
// 프레임 한도를 넘지 않도록 나눠 보낸다 (한 덩이로 보내면 통째로 거부돼 화면이 멈춘다)
|
|
331
344
|
const parts = chunkByBytes(pending, frameBudget);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
345
|
+
session.inFlight += parts.length;
|
|
346
|
+
// send() 를 부르는 순간 이미 통로로 나간다. 부른 순서 = 나가는 순서라서(실측 확인)
|
|
347
|
+
// 답을 기다리지 않아도 조각이 뒤섞이지 않는다.
|
|
348
|
+
const results = parts.map((part) => sendFrame({
|
|
349
|
+
kind: 'output',
|
|
350
|
+
to: session.deviceId,
|
|
351
|
+
data: encryptFrame(part, session.frameKey),
|
|
352
|
+
}));
|
|
353
|
+
void Promise.allSettled(results)
|
|
354
|
+
.then((settled) => {
|
|
355
|
+
let sent = 0;
|
|
356
|
+
while (sent < settled.length && settled[sent].status === 'fulfilled')
|
|
357
|
+
sent += 1;
|
|
358
|
+
if (sent >= settled.length)
|
|
359
|
+
return;
|
|
360
|
+
const reason = settled[sent].reason;
|
|
361
|
+
const message = String(reason?.message ?? reason);
|
|
362
|
+
console.error(`출력 전송 실패 (${sent}/${parts.length} 조각만 나감):`, message);
|
|
363
|
+
if (/too large|TOO_LARGE|size|크기/i.test(message))
|
|
364
|
+
shrinkFrameBudget(message);
|
|
365
|
+
session.sendPenaltyUntil = Date.now() + SEND_PENALTY_MS;
|
|
366
|
+
// 못 간 것을 다시 보내는 대신 **화면을 통째로 다시 보낸다**.
|
|
367
|
+
// 답을 기다리지 않고 잇달아 부치기 때문에, 실패를 알게 된 시점에는 그 뒤의 출력이
|
|
368
|
+
// 이미 나가 있을 수 있다 — 거기에 옛 조각을 뒤늦게 끼워 넣으면 화면이 뒤섞인다.
|
|
369
|
+
// 지금 화면이 어떻게 생겼는지는 session.screen 이 정확히 들고 있어서,
|
|
370
|
+
// 통째로 다시 보내면 빠지지도 겹치지도 않는다.
|
|
371
|
+
if (!session.closing)
|
|
342
372
|
void sendRestore(session);
|
|
343
|
-
|
|
344
|
-
|
|
373
|
+
})
|
|
374
|
+
.finally(() => {
|
|
375
|
+
session.inFlight -= parts.length;
|
|
376
|
+
// 답을 기다리느라 모아둔 것이 있으면 이어서 보낸다
|
|
377
|
+
if (session.outBuffer && !session.closing && !session.restoring)
|
|
378
|
+
scheduleFlush(session);
|
|
379
|
+
});
|
|
345
380
|
}
|
|
346
381
|
catch (err) {
|
|
347
382
|
// 여기서 새어 나간 오류는 아무도 받아주지 않아 프로세스가 통째로 죽는다 — 반드시 잡는다
|
|
@@ -351,7 +386,7 @@ export async function runAgent(options) {
|
|
|
351
386
|
// 무슨 일이 있어도 표시는 반드시 푼다 — 여기 걸린 채로 남으면 그 세션은 그 뒤로
|
|
352
387
|
// 아무 화면도 못 보낸다 (검은 화면의 원인 중 하나)
|
|
353
388
|
session.flushing = false;
|
|
354
|
-
//
|
|
389
|
+
// 보내는 사이에 새로 나온 것이 있으면 곧바로 이어서 보낸다
|
|
355
390
|
if (session.outBuffer && !session.closing && !session.restoring)
|
|
356
391
|
scheduleFlush(session);
|
|
357
392
|
}
|
|
@@ -458,6 +493,10 @@ export async function runAgent(options) {
|
|
|
458
493
|
const scheduleFlush = (session) => {
|
|
459
494
|
if (session.pendingFlush !== null || session.flushing || session.restoring)
|
|
460
495
|
return;
|
|
496
|
+
// 답을 못 받은 조각이 너무 많이 쌓였으면 잠시 모아둔다 — 하나가 돌아오면 그때 이어서 보낸다
|
|
497
|
+
// (그 사이 나온 출력은 한 덩이로 합쳐져서, 막힌 상황에서도 조각 수가 늘지 않는다)
|
|
498
|
+
if (session.inFlight >= MAX_FRAMES_IN_FLIGHT)
|
|
499
|
+
return;
|
|
461
500
|
const interval = Date.now() < session.sendPenaltyUntil ? PENALTY_INTERVAL_MS : MIN_SEND_INTERVAL_MS;
|
|
462
501
|
const wait = Math.max(0, interval - (Date.now() - session.lastSentAt));
|
|
463
502
|
session.pendingFlush = setTimeout(() => void flush(session), wait);
|
|
@@ -578,6 +617,7 @@ export async function runAgent(options) {
|
|
|
578
617
|
approvalInFlight: false,
|
|
579
618
|
outBuffer: '',
|
|
580
619
|
flushing: false,
|
|
620
|
+
inFlight: 0,
|
|
581
621
|
sendChain: Promise.resolve(),
|
|
582
622
|
restoring: false,
|
|
583
623
|
restoreAgain: false,
|