@rynx-ai/remote-runtime-client 0.1.11-beta.4 → 0.1.11-beta.41

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/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type RemoteRuntimeRpcMethod, type RemoteRuntimeRpcParams, type RemoteRuntimeRpcResultFor, type RemoteRuntimeSequencedSessionEvent, type RemoteRuntimeSessionTerminalClosedFrame, type RemoteRuntimeSessionTerminalRole } from "@rynx-ai/protocol/remote-runtime-rpc";
1
+ import { type RemoteRuntimeRpcMethod, type RemoteRuntimeRpcParams, type RemoteRuntimeRpcResultFor, type RemoteRuntimeSequencedSessionEvent, type RemoteRuntimeSessionTerminalRole } from "@rynx-ai/protocol/remote-runtime-rpc";
2
2
  import { type DaemonStatus } from "@rynx-ai/protocol/remote-runtime";
3
3
  import { type RuntimeEmulatorSurfaceClosedFrame, type RuntimeEmulatorSurfaceImageFrame, type RuntimeEmulatorSurfaceReadyFrame } from "@rynx-ai/protocol/runtime-emulator-surface";
4
4
  import { type DirectRuntimeClientIdentity, type DirectRuntimeCredential } from "./credential.js";
@@ -12,8 +12,9 @@ export interface DirectRuntimeClientOptions {
12
12
  };
13
13
  }
14
14
  export interface DirectRuntimeCallOptions {
15
- /** Per-call override for operations whose expected duration differs from the handshake. */
16
- timeoutMs?: number;
15
+ /** Per-call override for operations whose expected duration differs from the
16
+ * handshake. `null` leaves the operation to its own lifecycle deadlines. */
17
+ timeoutMs?: number | null;
17
18
  }
18
19
  export interface DirectRuntimeSessionEventsOptions {
19
20
  /** Timeout for the accepted ready barrier and cancellation acknowledgement. */
@@ -33,11 +34,34 @@ export interface DirectRuntimeSessionTerminalOptions {
33
34
  /** Timeout for the opened barrier and close acknowledgement. */
34
35
  timeoutMs?: number;
35
36
  }
36
- export interface DirectRuntimeSessionTerminal extends AsyncIterable<Uint8Array> {
37
+ export interface DirectRuntimeSessionTerminalRead {
38
+ data: Uint8Array;
39
+ nextOffset: number;
40
+ done: boolean;
41
+ finalOffset?: number;
42
+ }
43
+ export interface DirectRuntimeSessionTerminalCloseInfo {
44
+ reason: "client_closed" | "terminal_exited" | "not_live" | "unauthorized" | "capacity" | "backpressure" | "internal";
45
+ exitCode?: number;
46
+ finalOffset?: number;
47
+ }
48
+ export interface DirectRuntimeSessionTerminal {
37
49
  readonly attachmentId: string;
38
50
  readonly sessionId: string;
39
51
  readonly role: RemoteRuntimeSessionTerminalRole;
40
- readonly closed: Promise<RemoteRuntimeSessionTerminalClosedFrame>;
52
+ readonly seedBytes: number;
53
+ readonly dimensions?: {
54
+ cols: number;
55
+ rows: number;
56
+ };
57
+ readonly closed: Promise<DirectRuntimeSessionTerminalCloseInfo>;
58
+ readSeed(offset: number, maxBytes: number): Promise<DirectRuntimeSessionTerminalRead>;
59
+ start(): Promise<void>;
60
+ read(offset: number, maxBytes: number): Promise<DirectRuntimeSessionTerminalRead>;
61
+ onResize(listener: (dimensions: {
62
+ cols: number;
63
+ rows: number;
64
+ }) => void): void;
41
65
  write(data: Uint8Array): Promise<void>;
42
66
  resize(cols: number, rows: number): Promise<void>;
43
67
  /** Idempotently detach and wait for the remote close acknowledgement. */
package/dist/client.js CHANGED
@@ -25,8 +25,6 @@ const MAX_PENDING_OUTBOUND_BYTES = 1024 * 1024;
25
25
  const MAX_E2EE_ENVELOPE_OVERHEAD_BYTES = 256;
26
26
  const MAX_SUBSCRIPTION_QUEUED_EVENTS = 128;
27
27
  const MAX_SUBSCRIPTION_QUEUED_BYTES = 256 * 1024;
28
- const MAX_TERMINAL_QUEUED_OUTPUT_FRAMES = 128;
29
- const MAX_TERMINAL_QUEUED_OUTPUT_BYTES = 512 * 1024;
30
28
  const establishedTextDecoder = new TextDecoder("utf-8", { fatal: true });
31
29
  /** Enroll a client identity, authenticate the issued grant, and probe status. */
32
30
  export async function pairDirectRuntime(untrustedOffer, options = {}) {
@@ -208,20 +206,25 @@ class DirectRuntimeConnectionImpl {
208
206
  outcome: "not_started",
209
207
  });
210
208
  }
211
- const timeoutMs = parseTimeout(options.timeoutMs ?? this.timeoutMs);
209
+ const timeoutMs = options.timeoutMs === null
210
+ ? null
211
+ : parseTimeout(options.timeoutMs ?? this.timeoutMs);
212
212
  return await new Promise((resolve, reject) => {
213
- const timer = setTimeout(() => {
214
- this.pending.delete(id);
215
- reject(callTransportError(clientError("deadline_exceeded", "Direct Runtime RPC timed out"), method, id));
216
- }, timeoutMs);
217
- timer.unref?.();
213
+ const timer = timeoutMs === null
214
+ ? undefined
215
+ : setTimeout(() => {
216
+ this.pending.delete(id);
217
+ reject(callTransportError(clientError("deadline_exceeded", "Direct Runtime RPC timed out"), method, id));
218
+ }, timeoutMs);
219
+ timer?.unref?.();
218
220
  this.pending.set(id, { method, resolve, reject, timer });
219
221
  void this.channel.socket.sendText(frame).catch((error) => {
220
222
  const pending = this.pending.get(id);
221
223
  if (!pending)
222
224
  return;
223
225
  this.pending.delete(id);
224
- clearTimeout(pending.timer);
226
+ if (pending.timer)
227
+ clearTimeout(pending.timer);
225
228
  pending.reject(error instanceof DirectRuntimeClientCapacityError
226
229
  ? error
227
230
  : callTransportError(error, pending.method, id));
@@ -232,7 +235,11 @@ class DirectRuntimeConnectionImpl {
232
235
  if (this.statusSnapshot)
233
236
  return Promise.resolve(this.statusSnapshot);
234
237
  if (!this.statusProbe) {
235
- this.statusProbe = this.sendCall("status.get", {}, options).finally(() => {
238
+ // An application operation may deliberately have no outer deadline, but
239
+ // capability preflight is transport metadata and retains the connection
240
+ // default rather than inheriting that unbounded lifecycle.
241
+ const statusOptions = options.timeoutMs === null ? {} : options;
242
+ this.statusProbe = this.sendCall("status.get", {}, statusOptions).finally(() => {
236
243
  this.statusProbe = undefined;
237
244
  });
238
245
  }
@@ -469,8 +476,14 @@ class DirectRuntimeConnectionImpl {
469
476
  case "session.events.closed":
470
477
  this.receiveSessionFrame(value, frame.byteLength);
471
478
  return;
472
- case "session.terminal.opened":
473
- case "session.terminal.output":
479
+ case "session.terminal.prepared":
480
+ case "session.terminal.seed.chunk":
481
+ case "session.terminal.started":
482
+ case "session.terminal.chunk":
483
+ case "session.terminal.input.ack":
484
+ case "session.terminal.resize.ack":
485
+ case "session.terminal.reader.done":
486
+ case "session.terminal.dimensions":
474
487
  case "session.terminal.closed":
475
488
  this.receiveTerminalFrame(value);
476
489
  return;
@@ -498,7 +511,8 @@ class DirectRuntimeConnectionImpl {
498
511
  return;
499
512
  }
500
513
  this.pending.delete(envelope.id);
501
- clearTimeout(pending.timer);
514
+ if (pending.timer)
515
+ clearTimeout(pending.timer);
502
516
  let response;
503
517
  try {
504
518
  response = parseRemoteRuntimeRpcResponseForMethod(envelope, pending.method);
@@ -615,16 +629,30 @@ class DirectRuntimeConnectionImpl {
615
629
  return;
616
630
  }
617
631
  switch (frame.type) {
618
- case "session.terminal.opened":
632
+ case "session.terminal.prepared":
619
633
  this.acceptTerminal(frame);
620
634
  return;
621
- case "session.terminal.output": {
635
+ case "session.terminal.seed.chunk":
636
+ case "session.terminal.chunk":
637
+ case "session.terminal.started":
638
+ case "session.terminal.input.ack":
639
+ case "session.terminal.resize.ack":
640
+ case "session.terminal.reader.done": {
622
641
  const terminal = this.terminal;
623
642
  if (!terminal || terminal.attachmentId !== frame.attachmentId) {
624
- this.fail(clientError("protocol_error", "Terminal output has no active attachment"));
643
+ this.fail(clientError("protocol_error", "Terminal response has no active attachment"));
625
644
  return;
626
645
  }
627
- terminal.push(Uint8Array.from(Buffer.from(frame.data, "base64")));
646
+ terminal.receive(frame);
647
+ return;
648
+ }
649
+ case "session.terminal.dimensions": {
650
+ const terminal = this.terminal;
651
+ if (!terminal || terminal.attachmentId !== frame.attachmentId) {
652
+ this.fail(clientError("protocol_error", "Terminal dimensions have no active attachment"));
653
+ return;
654
+ }
655
+ terminal.updateDimensions(frame.cols, frame.rows);
628
656
  return;
629
657
  }
630
658
  case "session.terminal.closed":
@@ -643,7 +671,7 @@ class DirectRuntimeConnectionImpl {
643
671
  }
644
672
  this.pendingTerminal = undefined;
645
673
  clearTimeout(pending.timer);
646
- const terminal = new SessionTerminalImpl(frame, pending.timeoutMs, (data) => this.writeTerminal(frame.attachmentId, data), (cols, rows) => this.resizeTerminal(frame.attachmentId, cols, rows), () => this.closeTerminal(frame.attachmentId), (error) => this.fail(error));
674
+ const terminal = new SessionTerminalImpl(frame, pending.timeoutMs, (value) => this.channel.socket.sendText(encodeRemoteRuntimeSessionTerminalClientFrame(value)), () => this.closeTerminal(frame.attachmentId), (error) => this.fail(error));
647
675
  this.terminal = terminal;
648
676
  pending.resolve(terminal);
649
677
  }
@@ -746,28 +774,6 @@ class DirectRuntimeConnectionImpl {
746
774
  ...point,
747
775
  }));
748
776
  }
749
- async writeTerminal(attachmentId, data) {
750
- if (data.byteLength === 0)
751
- return;
752
- for (let offset = 0; offset < data.byteLength; offset += REMOTE_RUNTIME_SESSION_TERMINAL_MAX_DATA_BYTES) {
753
- const chunk = data.subarray(offset, Math.min(offset + REMOTE_RUNTIME_SESSION_TERMINAL_MAX_DATA_BYTES, data.byteLength));
754
- const frame = encodeRemoteRuntimeSessionTerminalClientFrame({
755
- type: "session.terminal.input",
756
- attachmentId,
757
- data: Buffer.from(chunk).toString("base64"),
758
- });
759
- await this.channel.socket.sendText(frame);
760
- }
761
- }
762
- async resizeTerminal(attachmentId, cols, rows) {
763
- const frame = encodeRemoteRuntimeSessionTerminalClientFrame({
764
- type: "session.terminal.resize",
765
- attachmentId,
766
- cols,
767
- rows,
768
- });
769
- await this.channel.socket.sendText(frame);
770
- }
771
777
  async closeTerminal(attachmentId) {
772
778
  const terminal = this.terminal;
773
779
  if (!terminal || terminal.attachmentId !== attachmentId)
@@ -847,7 +853,8 @@ class DirectRuntimeConnectionImpl {
847
853
  }
848
854
  rejectPending(error) {
849
855
  for (const [id, pending] of this.pending) {
850
- clearTimeout(pending.timer);
856
+ if (pending.timer)
857
+ clearTimeout(pending.timer);
851
858
  pending.reject(callTransportError(error, pending.method, id));
852
859
  }
853
860
  this.pending.clear();
@@ -1176,31 +1183,39 @@ function sessionEventsError(frame, sessionId) {
1176
1183
  }
1177
1184
  class SessionTerminalImpl {
1178
1185
  timeoutMs;
1179
- sendInput;
1180
- sendResize;
1186
+ sendFrame;
1181
1187
  requestClose;
1182
1188
  reportBackpressure;
1183
- queue = [];
1184
- queuedBytes = 0;
1185
- waiter;
1186
- terminalError;
1187
- ended = false;
1188
- closing = false;
1189
+ state = "prepared";
1190
+ seedOffset = 0;
1191
+ liveOffset = 0;
1192
+ closedSettled = false;
1193
+ detachedSettled = false;
1194
+ pending = new Map();
1189
1195
  resolveClosed;
1190
1196
  rejectClosed;
1197
+ resolveDetached;
1198
+ rejectDetached;
1191
1199
  closed;
1200
+ detached;
1192
1201
  attachmentId;
1193
1202
  sessionId;
1194
1203
  role;
1195
- constructor(opened, timeoutMs, sendInput, sendResize, requestClose, reportBackpressure) {
1204
+ seedBytes;
1205
+ dimensions;
1206
+ resizeListener;
1207
+ constructor(opened, timeoutMs, sendFrame, requestClose, reportBackpressure) {
1196
1208
  this.timeoutMs = timeoutMs;
1197
- this.sendInput = sendInput;
1198
- this.sendResize = sendResize;
1209
+ this.sendFrame = sendFrame;
1199
1210
  this.requestClose = requestClose;
1200
1211
  this.reportBackpressure = reportBackpressure;
1201
1212
  this.attachmentId = opened.attachmentId;
1202
1213
  this.sessionId = opened.sessionId;
1203
1214
  this.role = opened.role;
1215
+ this.seedBytes = opened.seedBytes;
1216
+ this.dimensions = opened.cols === undefined || opened.rows === undefined
1217
+ ? undefined
1218
+ : { cols: opened.cols, rows: opened.rows };
1204
1219
  let resolveClosed;
1205
1220
  let rejectClosed;
1206
1221
  this.closed = new Promise((resolve, reject) => {
@@ -1209,111 +1224,225 @@ class SessionTerminalImpl {
1209
1224
  });
1210
1225
  this.resolveClosed = resolveClosed;
1211
1226
  this.rejectClosed = rejectClosed;
1227
+ let resolveDetached;
1228
+ let rejectDetached;
1229
+ this.detached = new Promise((resolve, reject) => {
1230
+ resolveDetached = resolve;
1231
+ rejectDetached = reject;
1232
+ });
1233
+ this.resolveDetached = resolveDetached;
1234
+ this.rejectDetached = rejectDetached;
1212
1235
  void this.closed.catch(() => undefined);
1236
+ void this.detached.catch(() => undefined);
1213
1237
  }
1214
- [Symbol.asyncIterator]() {
1215
- return this;
1238
+ onResize(listener) {
1239
+ this.resizeListener = listener;
1240
+ if (this.dimensions)
1241
+ queueMicrotask(() => listener(this.dimensions));
1216
1242
  }
1217
- next() {
1218
- const queued = this.queue.shift();
1219
- if (queued) {
1220
- this.queuedBytes -= queued.bytes;
1221
- return Promise.resolve({ done: false, value: queued.data });
1222
- }
1223
- if (this.terminalError !== undefined)
1224
- return Promise.reject(this.terminalError);
1225
- if (this.ended)
1226
- return Promise.resolve({ done: true, value: undefined });
1227
- if (this.waiter) {
1228
- return Promise.reject(clientError("protocol_error", "concurrent reads from one Session terminal are not allowed"));
1229
- }
1230
- return new Promise((resolve, reject) => {
1231
- this.waiter = { resolve, reject };
1243
+ updateDimensions(cols, rows) {
1244
+ if (this.state === "ended")
1245
+ return;
1246
+ this.dimensions = { cols, rows };
1247
+ this.resizeListener?.(this.dimensions);
1248
+ }
1249
+ readSeed(offset, maxBytes) {
1250
+ if (this.state !== "prepared" || offset !== this.seedOffset) {
1251
+ return Promise.reject(clientError("protocol_error", "Session terminal seed read is out of order"));
1252
+ }
1253
+ return this.requestRead("session.terminal.seed.chunk", {
1254
+ type: "session.terminal.seed.read",
1255
+ attachmentId: this.attachmentId,
1256
+ requestId: this.nextRequestId(),
1257
+ offset,
1258
+ maxBytes: Math.min(maxBytes, REMOTE_RUNTIME_SESSION_TERMINAL_MAX_DATA_BYTES),
1259
+ }).then((result) => {
1260
+ this.seedOffset = result.nextOffset;
1261
+ if (result.done && result.nextOffset !== this.seedBytes) {
1262
+ throw clientError("protocol_error", "Session terminal seed length changed");
1263
+ }
1264
+ return result;
1232
1265
  });
1233
1266
  }
1234
- async return() {
1235
- await this.close();
1236
- return { done: true, value: undefined };
1267
+ async start() {
1268
+ if (this.state !== "prepared" || this.seedOffset !== this.seedBytes) {
1269
+ throw clientError("protocol_error", "Session terminal history must be consumed before start");
1270
+ }
1271
+ this.state = "starting";
1272
+ await this.requestVoid("session.terminal.started", {
1273
+ type: "session.terminal.start",
1274
+ attachmentId: this.attachmentId,
1275
+ requestId: this.nextRequestId(),
1276
+ });
1277
+ if (this.state === "starting")
1278
+ this.state = "started";
1279
+ }
1280
+ read(offset, maxBytes) {
1281
+ if (this.state !== "started" || offset !== this.liveOffset) {
1282
+ return Promise.reject(clientError("protocol_error", "Session terminal read is out of order"));
1283
+ }
1284
+ return this.requestRead("session.terminal.chunk", {
1285
+ type: "session.terminal.read",
1286
+ attachmentId: this.attachmentId,
1287
+ requestId: this.nextRequestId(),
1288
+ offset,
1289
+ maxBytes: Math.min(maxBytes, REMOTE_RUNTIME_SESSION_TERMINAL_MAX_DATA_BYTES),
1290
+ }).then((result) => {
1291
+ this.liveOffset = result.nextOffset;
1292
+ return result;
1293
+ });
1237
1294
  }
1238
1295
  async write(data) {
1239
- if (this.ended || this.closing || this.role !== "owner" || data.byteLength === 0)
1296
+ if (this.state !== "started" || this.role !== "owner" || data.byteLength === 0)
1240
1297
  return;
1241
- await this.sendInput(Uint8Array.from(data));
1298
+ for (let offset = 0; offset < data.byteLength; offset += REMOTE_RUNTIME_SESSION_TERMINAL_MAX_DATA_BYTES) {
1299
+ const chunk = data.subarray(offset, Math.min(offset + REMOTE_RUNTIME_SESSION_TERMINAL_MAX_DATA_BYTES, data.byteLength));
1300
+ await this.requestVoid("session.terminal.input.ack", {
1301
+ type: "session.terminal.input",
1302
+ attachmentId: this.attachmentId,
1303
+ requestId: this.nextRequestId(),
1304
+ data: Buffer.from(chunk).toString("base64"),
1305
+ });
1306
+ }
1242
1307
  }
1243
1308
  async resize(cols, rows) {
1244
- if (this.ended || this.closing)
1309
+ if (this.state !== "started")
1245
1310
  return;
1246
- await this.sendResize(cols, rows);
1311
+ await this.requestVoid("session.terminal.resize.ack", {
1312
+ type: "session.terminal.resize",
1313
+ attachmentId: this.attachmentId,
1314
+ requestId: this.nextRequestId(),
1315
+ cols,
1316
+ rows,
1317
+ });
1247
1318
  }
1248
1319
  async close() {
1249
- if (this.ended)
1320
+ if (this.state === "ended")
1250
1321
  return;
1251
1322
  await this.requestClose();
1252
1323
  }
1253
- push(data) {
1254
- if (this.ended || this.closing || data.byteLength === 0)
1255
- return;
1256
- if (this.waiter) {
1257
- const waiter = this.waiter;
1258
- this.waiter = undefined;
1259
- waiter.resolve({ done: false, value: data });
1324
+ receive(frame) {
1325
+ if (frame.type === "session.terminal.reader.done") {
1326
+ if (!this.closedSettled) {
1327
+ this.closedSettled = true;
1328
+ this.resolveClosed({
1329
+ reason: frame.reason,
1330
+ exitCode: frame.exitCode,
1331
+ finalOffset: frame.finalOffset,
1332
+ });
1333
+ }
1260
1334
  return;
1261
1335
  }
1262
- if (this.queue.length >= MAX_TERMINAL_QUEUED_OUTPUT_FRAMES ||
1263
- this.queuedBytes + data.byteLength > MAX_TERMINAL_QUEUED_OUTPUT_BYTES) {
1264
- this.reportBackpressure(clientError("unreachable", "Remote Session terminal output exceeded its bounded client queue"));
1336
+ const pending = this.pending.get(frame.requestId);
1337
+ if (!pending || pending.expectedType !== frame.type) {
1338
+ this.reportBackpressure(clientError("protocol_error", "Remote Session terminal response is duplicate or out of order"));
1265
1339
  return;
1266
1340
  }
1267
- const copy = Uint8Array.from(data);
1268
- this.queue.push({ data: copy, bytes: copy.byteLength });
1269
- this.queuedBytes += copy.byteLength;
1341
+ this.pending.delete(frame.requestId);
1342
+ clearTimeout(pending.timer);
1343
+ if (frame.type === "session.terminal.seed.chunk" || frame.type === "session.terminal.chunk") {
1344
+ pending.resolve({
1345
+ data: Uint8Array.from(Buffer.from(frame.data, "base64")),
1346
+ nextOffset: frame.nextOffset,
1347
+ done: frame.done,
1348
+ ...(frame.finalOffset === undefined ? {} : { finalOffset: frame.finalOffset }),
1349
+ });
1350
+ }
1351
+ else
1352
+ pending.resolve(undefined);
1270
1353
  }
1271
1354
  beginClose() {
1272
- if (this.ended || this.closing)
1355
+ if (this.state === "ended" || this.state === "closing")
1273
1356
  return false;
1274
- this.closing = true;
1275
- this.clearQueue();
1357
+ this.state = "closing";
1358
+ this.rejectPending(clientError("closed", "Remote Session terminal is closing"));
1276
1359
  return true;
1277
1360
  }
1278
1361
  remoteClose(frame) {
1279
- if (this.ended)
1362
+ if (this.state === "ended")
1280
1363
  return;
1364
+ this.state = "ended";
1281
1365
  const error = frame.reason === "client_closed" || frame.reason === "terminal_exited"
1282
1366
  ? undefined
1283
1367
  : sessionTerminalError(frame, this.sessionId);
1284
- this.finish(frame, error, frame.reason !== "terminal_exited");
1368
+ this.rejectPending(error ?? clientError("closed", "Remote Session terminal detached"));
1369
+ if (!this.closedSettled) {
1370
+ this.closedSettled = true;
1371
+ if (error)
1372
+ this.rejectClosed(error);
1373
+ else
1374
+ this.resolveClosed({ reason: frame.reason, ...(frame.exitCode === undefined ? {} : { exitCode: frame.exitCode }) });
1375
+ }
1376
+ if (!this.detachedSettled) {
1377
+ this.detachedSettled = true;
1378
+ if (error)
1379
+ this.rejectDetached(error);
1380
+ else
1381
+ this.resolveDetached();
1382
+ }
1285
1383
  }
1286
1384
  terminate(error) {
1287
- if (this.ended)
1385
+ if (this.state === "ended")
1288
1386
  return;
1289
- this.finish(undefined, error, true);
1387
+ this.state = "ended";
1388
+ this.rejectPending(error);
1389
+ if (!this.closedSettled) {
1390
+ this.closedSettled = true;
1391
+ this.rejectClosed(error);
1392
+ }
1393
+ if (!this.detachedSettled) {
1394
+ this.detachedSettled = true;
1395
+ this.rejectDetached(error);
1396
+ }
1290
1397
  }
1291
1398
  waitClosed() {
1292
- return this.closed.then(() => undefined);
1399
+ return this.detached;
1293
1400
  }
1294
- finish(frame, error, discardQueue = false) {
1295
- if (this.ended)
1296
- return;
1297
- this.ended = true;
1298
- this.terminalError = error;
1299
- if (discardQueue)
1300
- this.clearQueue();
1301
- const waiter = this.waiter;
1302
- this.waiter = undefined;
1303
- if (waiter) {
1304
- if (error === undefined)
1305
- waiter.resolve({ done: true, value: undefined });
1306
- else
1307
- waiter.reject(error);
1401
+ requestRead(expectedType, frame) {
1402
+ return this.request(expectedType, frame);
1403
+ }
1404
+ requestVoid(expectedType, frame) {
1405
+ return this.request(expectedType, frame).then(() => undefined);
1406
+ }
1407
+ request(expectedType, frame) {
1408
+ if (!("requestId" in frame)) {
1409
+ return Promise.reject(clientError("protocol_error", "terminal request id is missing"));
1308
1410
  }
1309
- if (frame)
1310
- this.resolveClosed(frame);
1311
- else
1312
- this.rejectClosed(error ?? clientError("closed", "Remote Session terminal closed"));
1411
+ const pull = expectedType === "session.terminal.seed.chunk" || expectedType === "session.terminal.chunk";
1412
+ const sameClassPending = [...this.pending.values()].some((pending) => {
1413
+ const pendingPull = pending.expectedType === "session.terminal.seed.chunk" ||
1414
+ pending.expectedType === "session.terminal.chunk";
1415
+ return pendingPull === pull;
1416
+ });
1417
+ if (sameClassPending) {
1418
+ return Promise.reject(clientError("protocol_error", pull ? "concurrent terminal pulls are not allowed" : "concurrent terminal controls are not allowed"));
1419
+ }
1420
+ return new Promise((resolve, reject) => {
1421
+ const timer = setTimeout(() => {
1422
+ this.pending.delete(frame.requestId);
1423
+ reject(clientError("deadline_exceeded", "Remote Session terminal operation timed out"));
1424
+ }, this.timeoutMs);
1425
+ timer.unref?.();
1426
+ this.pending.set(frame.requestId, { expectedType, resolve, reject, timer });
1427
+ void this.sendFrame(frame).catch((error) => {
1428
+ const pending = this.pending.get(frame.requestId);
1429
+ if (!pending)
1430
+ return;
1431
+ this.pending.delete(frame.requestId);
1432
+ clearTimeout(pending.timer);
1433
+ pending.reject(error);
1434
+ });
1435
+ });
1313
1436
  }
1314
- clearQueue() {
1315
- this.queue.length = 0;
1316
- this.queuedBytes = 0;
1437
+ rejectPending(error) {
1438
+ for (const pending of this.pending.values()) {
1439
+ clearTimeout(pending.timer);
1440
+ pending.reject(error);
1441
+ }
1442
+ this.pending.clear();
1443
+ }
1444
+ nextRequestId() {
1445
+ return `treq_${randomBytes(12).toString("base64url")}`;
1317
1446
  }
1318
1447
  }
1319
1448
  function sessionTerminalError(frame, sessionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/remote-runtime-client",
3
- "version": "0.1.11-beta.4",
3
+ "version": "0.1.11-beta.41",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -25,8 +25,8 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "ws": "^8.21.0",
28
- "@rynx-ai/protocol": "0.1.11-beta.4",
29
- "@rynx-ai/remote-runtime-e2ee": "0.1.11-beta.4"
28
+ "@rynx-ai/protocol": "0.1.11-beta.41",
29
+ "@rynx-ai/remote-runtime-e2ee": "0.1.11-beta.41"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/ws": "^8.18.1"