@dev-anywhere/relay 0.5.45 → 0.5.47

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.
@@ -1045,7 +1045,7 @@ var relayControlDefinitions = [
1045
1045
  }))
1046
1046
  }),
1047
1047
  // PTY 会话订阅,client -> proxy,触发 terminal serialize() 返回当前状态
1048
- control("session_subscribe", { sessionId: IdSchema, requestId: IdSchema.optional() }, "client_to_proxy"),
1048
+ control("session_subscribe", { sessionId: IdSchema, requestId: IdSchema }, "client_to_proxy"),
1049
1049
  // PTY 会话快照,proxy -> client,serialize() 的全量终端状态
1050
1050
  control("session_snapshot", {
1051
1051
  sessionId: IdSchema,
@@ -1053,7 +1053,7 @@ var relayControlDefinitions = [
1053
1053
  rows: z9.number().int().positive(),
1054
1054
  data: z9.string(),
1055
1055
  outputSeq: z9.number().int().nonnegative(),
1056
- requestId: IdSchema.optional()
1056
+ requestId: IdSchema
1057
1057
  }, "proxy_to_client")
1058
1058
  ];
1059
1059
  var relayControlSchemas = relayControlDefinitions.map((definition) => definition.schema);
@@ -1776,7 +1776,7 @@ function rejectNotRegistered(ws) {
1776
1776
  })
1777
1777
  );
1778
1778
  }
1779
- function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1779
+ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, remoteFileBridge) {
1780
1780
  const proxyWs = ws;
1781
1781
  proxyWs.isAlive = true;
1782
1782
  proxyWs.on("pong", () => {
@@ -1806,7 +1806,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1806
1806
  const clients = registry.getClientsForProxy(proxyWs.proxyId);
1807
1807
  for (const clientWs of clients) {
1808
1808
  if (clientWs.readyState === WebSocket4.OPEN) {
1809
- clientWs.send(data);
1809
+ clientWs.send(data, { binary: true, compress: false });
1810
1810
  }
1811
1811
  }
1812
1812
  return;
@@ -1824,6 +1824,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1824
1824
  const { proxyId, name } = result.message;
1825
1825
  const status = registry.registerProxy(proxyId, proxyWs, name);
1826
1826
  proxyWs.proxyId = proxyId;
1827
+ if (status === "reconnected") ptySnapshotRoutes.clearProxy(proxyId);
1827
1828
  logger.info({ proxyId, status }, "Proxy registered");
1828
1829
  proxyWs.send(
1829
1830
  serializeControl({
@@ -1839,6 +1840,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1839
1840
  }
1840
1841
  if (result.kind === "control" && result.message.type === "proxy_disconnect") {
1841
1842
  if (proxyWs.proxyId) {
1843
+ ptySnapshotRoutes.clearProxy(proxyWs.proxyId);
1842
1844
  notifyClientsProxyOffline(proxyWs.proxyId, registry, logger, chaos);
1843
1845
  registry.unregisterProxy(proxyWs.proxyId);
1844
1846
  logger.info(
@@ -1895,6 +1897,35 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1895
1897
  remoteFileBridge?.handleProxyControl(proxyWs.proxyId, result.message);
1896
1898
  return;
1897
1899
  }
1900
+ if (proxyWs.proxyId && result.message.type === "session_snapshot") {
1901
+ const route = ptySnapshotRoutes.resolve(
1902
+ proxyWs.proxyId,
1903
+ result.message.sessionId,
1904
+ result.message.requestId,
1905
+ proxyWs
1906
+ );
1907
+ if (route.kind === "matched" && route.clientWs.readyState === WebSocket4.OPEN) {
1908
+ if (chaos) {
1909
+ chaos.send(route.clientWs, raw, {
1910
+ direction: "proxy_to_client",
1911
+ type: result.message.type
1912
+ });
1913
+ } else {
1914
+ route.clientWs.send(raw);
1915
+ }
1916
+ return;
1917
+ }
1918
+ logger.debug(
1919
+ {
1920
+ proxyId: proxyWs.proxyId,
1921
+ sessionId: result.message.sessionId,
1922
+ requestId: result.message.requestId,
1923
+ route: route.kind
1924
+ },
1925
+ "Unmatched PTY snapshot dropped"
1926
+ );
1927
+ return;
1928
+ }
1898
1929
  if (isProxyToClientRelayControlType(result.message.type)) {
1899
1930
  if (!proxyWs.proxyId) {
1900
1931
  rejectNotRegistered(proxyWs);
@@ -1953,6 +1984,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1953
1984
  );
1954
1985
  return;
1955
1986
  }
1987
+ ptySnapshotRoutes.clearProxy(proxyWs.proxyId);
1956
1988
  notifyClientsProxyOffline(proxyWs.proxyId, registry, logger, chaos);
1957
1989
  try {
1958
1990
  registry.transitionProxy(proxyWs.proxyId, "online", "offline");
@@ -2873,7 +2905,7 @@ function sendRemoteFileUploadUrlFailure(ws, requestId, sessionId, error, errorCo
2873
2905
  })
2874
2906
  );
2875
2907
  }
2876
- function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
2908
+ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
2877
2909
  const clientWs = ws;
2878
2910
  clientWs.isAlive = true;
2879
2911
  registry.addClientWs(clientWs, connectionInfo);
@@ -3113,6 +3145,48 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
3113
3145
  }
3114
3146
  const proxyWs = registry.getProxy(targetProxyId);
3115
3147
  if (proxyWs && proxyWs.readyState === WebSocket7.OPEN) {
3148
+ if (msg.type === "session_subscribe") {
3149
+ const registration = ptySnapshotRoutes.register(
3150
+ targetProxyId,
3151
+ msg.sessionId,
3152
+ msg.requestId,
3153
+ clientWs,
3154
+ proxyWs
3155
+ );
3156
+ if (registration === "duplicate") {
3157
+ logger.debug(
3158
+ { proxyId: targetProxyId, sessionId: msg.sessionId, requestId: msg.requestId },
3159
+ "Duplicate PTY snapshot subscribe suppressed"
3160
+ );
3161
+ return;
3162
+ }
3163
+ if (registration !== "registered" && registration !== "retry_due") {
3164
+ logger.warn(
3165
+ {
3166
+ proxyId: targetProxyId,
3167
+ sessionId: msg.sessionId,
3168
+ requestId: msg.requestId,
3169
+ registration
3170
+ },
3171
+ "PTY snapshot subscribe route rejected"
3172
+ );
3173
+ clientWs.send(
3174
+ JSON.stringify({
3175
+ type: "relay_error",
3176
+ requestId: msg.requestId,
3177
+ code: RelayErrorCode.INVALID_MESSAGE,
3178
+ message: registration === "collision" ? "PTY snapshot requestId is already in use" : "Too many pending PTY snapshot requests"
3179
+ })
3180
+ );
3181
+ return;
3182
+ }
3183
+ if (registration === "retry_due") {
3184
+ logger.debug(
3185
+ { proxyId: targetProxyId, sessionId: msg.sessionId, requestId: msg.requestId },
3186
+ "Retrying unanswered PTY snapshot subscribe"
3187
+ );
3188
+ }
3189
+ }
3116
3190
  if (chaos) chaos.send(proxyWs, raw, { direction: "client_to_proxy", type: msg.type });
3117
3191
  else proxyWs.send(raw);
3118
3192
  } else {
@@ -3189,6 +3263,7 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
3189
3263
  }
3190
3264
  });
3191
3265
  clientWs.on("close", (code, reason) => {
3266
+ ptySnapshotRoutes.abandonSocket(clientWs);
3192
3267
  registry.removeClientWs(clientWs);
3193
3268
  if (clientWs.clientId) {
3194
3269
  registry.unbindClientSocket(clientWs.clientId, clientWs);
@@ -4095,7 +4170,7 @@ var RemoteFileBridge = class {
4095
4170
  req.pause();
4096
4171
  const frame = encodeFileStreamFrame(uploadId, pending.chunkSeq, Buffer.from(chunk));
4097
4172
  pending.chunkSeq += 1;
4098
- proxyWs.send(frame, { binary: true }, (err) => {
4173
+ proxyWs.send(frame, { binary: true, compress: false }, (err) => {
4099
4174
  if (err) {
4100
4175
  this.failUpload(uploadId, 502, err.message);
4101
4176
  return;
@@ -4411,10 +4486,138 @@ function mountWebApp(app, options) {
4411
4486
  return true;
4412
4487
  }
4413
4488
 
4489
+ // src/pty-snapshot-route-registry.ts
4490
+ var DEFAULT_MAX_ENTRIES = 4096;
4491
+ var DEFAULT_MAX_PENDING_PER_CLIENT = 64;
4492
+ var DEFAULT_RETRY_FORWARD_INTERVAL_MS = 25e3;
4493
+ var DEFAULT_PENDING_TTL_MS = 5 * 6e4;
4494
+ var DEFAULT_TOMBSTONE_TTL_MS = 12e4;
4495
+ function routeKey(proxyId, sessionId, requestId) {
4496
+ return JSON.stringify([proxyId, sessionId, requestId]);
4497
+ }
4498
+ var PtySnapshotRouteRegistry = class {
4499
+ routes = /* @__PURE__ */ new Map();
4500
+ maxEntries;
4501
+ maxPendingPerClient;
4502
+ retryForwardIntervalMs;
4503
+ pendingTtlMs;
4504
+ tombstoneTtlMs;
4505
+ now;
4506
+ constructor(options = {}) {
4507
+ this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_ENTRIES);
4508
+ this.maxPendingPerClient = Math.max(
4509
+ 1,
4510
+ options.maxPendingPerClient ?? DEFAULT_MAX_PENDING_PER_CLIENT
4511
+ );
4512
+ this.retryForwardIntervalMs = Math.max(
4513
+ 1,
4514
+ options.retryForwardIntervalMs ?? DEFAULT_RETRY_FORWARD_INTERVAL_MS
4515
+ );
4516
+ this.pendingTtlMs = Math.max(1, options.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS);
4517
+ this.tombstoneTtlMs = Math.max(1, options.tombstoneTtlMs ?? DEFAULT_TOMBSTONE_TTL_MS);
4518
+ this.now = options.now ?? Date.now;
4519
+ }
4520
+ register(proxyId, sessionId, requestId, clientWs, proxyWs) {
4521
+ const now = this.now();
4522
+ this.pruneExpired(now);
4523
+ const key = routeKey(proxyId, sessionId, requestId);
4524
+ const existing = this.routes.get(key);
4525
+ if (existing) {
4526
+ if (existing.state === "pending" && existing.clientWs === clientWs && existing.proxyWs === proxyWs) {
4527
+ if (now - existing.lastForwardedAt >= this.retryForwardIntervalMs) {
4528
+ existing.lastForwardedAt = now;
4529
+ return "retry_due";
4530
+ }
4531
+ return "duplicate";
4532
+ }
4533
+ return "collision";
4534
+ }
4535
+ let clientPendingCount = 0;
4536
+ for (const route of this.routes.values()) {
4537
+ if (route.state === "pending" && route.clientWs === clientWs) {
4538
+ clientPendingCount += 1;
4539
+ if (clientPendingCount >= this.maxPendingPerClient) {
4540
+ return "client_capacity_exceeded";
4541
+ }
4542
+ }
4543
+ }
4544
+ if (this.routes.size >= this.maxEntries) {
4545
+ let oldestTombstoneKey;
4546
+ let oldestTombstoneExpiry = Number.POSITIVE_INFINITY;
4547
+ for (const [candidateKey, route] of this.routes) {
4548
+ if (route.state === "tombstone" && route.expiresAt < oldestTombstoneExpiry) {
4549
+ oldestTombstoneKey = candidateKey;
4550
+ oldestTombstoneExpiry = route.expiresAt;
4551
+ }
4552
+ }
4553
+ if (oldestTombstoneKey) this.routes.delete(oldestTombstoneKey);
4554
+ else return "capacity_exceeded";
4555
+ }
4556
+ this.routes.set(key, {
4557
+ state: "pending",
4558
+ proxyId,
4559
+ proxyWs,
4560
+ clientWs,
4561
+ lastForwardedAt: now,
4562
+ expiresAt: now + this.pendingTtlMs
4563
+ });
4564
+ return "registered";
4565
+ }
4566
+ resolve(proxyId, sessionId, requestId, proxyWs) {
4567
+ const now = this.now();
4568
+ this.pruneExpired(now);
4569
+ const key = routeKey(proxyId, sessionId, requestId);
4570
+ const route = this.routes.get(key);
4571
+ if (!route) return { kind: "unmatched" };
4572
+ if (route.state === "tombstone") return { kind: "tombstone" };
4573
+ if (route.proxyWs !== proxyWs) return { kind: "stale_proxy" };
4574
+ this.routes.set(key, {
4575
+ state: "tombstone",
4576
+ proxyId,
4577
+ expiresAt: now + this.tombstoneTtlMs
4578
+ });
4579
+ return { kind: "matched", clientWs: route.clientWs };
4580
+ }
4581
+ abandonSocket(clientWs) {
4582
+ const now = this.now();
4583
+ this.pruneExpired(now);
4584
+ for (const [key, route] of this.routes) {
4585
+ if (route.state === "pending" && route.clientWs === clientWs) {
4586
+ this.routes.set(key, {
4587
+ state: "tombstone",
4588
+ proxyId: route.proxyId,
4589
+ expiresAt: now + this.tombstoneTtlMs
4590
+ });
4591
+ }
4592
+ }
4593
+ }
4594
+ clearProxy(proxyId) {
4595
+ for (const [key, route] of this.routes) {
4596
+ if (route.proxyId === proxyId) this.routes.delete(key);
4597
+ }
4598
+ }
4599
+ pruneExpired(now) {
4600
+ for (const [key, route] of this.routes) {
4601
+ if (route.expiresAt <= now) this.routes.delete(key);
4602
+ }
4603
+ }
4604
+ };
4605
+
4414
4606
  // src/server.ts
4415
4607
  var MODULE_DIR = dirname3(fileURLToPath2(import.meta.url));
4416
4608
  var PACKAGED_FONTS_DIR = resolve(MODULE_DIR, "../assets/fonts");
4417
4609
  var PACKAGED_WEB_DIR = resolve(MODULE_DIR, "../assets/web");
4610
+ var DATA_CHANNEL_MAX_PAYLOAD_BYTES = 10 * 1024 * 1024;
4611
+ var DATA_CHANNEL_COMPRESSION = {
4612
+ clientNoContextTakeover: true,
4613
+ serverNoContextTakeover: true,
4614
+ threshold: 32 * 1024,
4615
+ concurrencyLimit: 4,
4616
+ zlibDeflateOptions: {
4617
+ level: 3,
4618
+ memLevel: 7
4619
+ }
4620
+ };
4418
4621
  function firstHeaderValue(value) {
4419
4622
  if (Array.isArray(value)) return value[0];
4420
4623
  return value;
@@ -4444,6 +4647,7 @@ function createRelayServer(options) {
4444
4647
  );
4445
4648
  }
4446
4649
  const registry = new RelayRegistry();
4650
+ const ptySnapshotRoutes = new PtySnapshotRouteRegistry();
4447
4651
  const remoteFileBridge = new RemoteFileBridge({ registry, logger });
4448
4652
  const voiceConfigStore = createVoiceConfigStore({
4449
4653
  dataDir,
@@ -4513,8 +4717,16 @@ function createRelayServer(options) {
4513
4717
  mountWebApp(app, { webAssetDir, logger });
4514
4718
  }
4515
4719
  const httpServer = createServer(app);
4516
- const proxyWss = new WebSocketServer({ noServer: true });
4517
- const clientWss = new WebSocketServer({ noServer: true });
4720
+ const proxyWss = new WebSocketServer({
4721
+ noServer: true,
4722
+ maxPayload: DATA_CHANNEL_MAX_PAYLOAD_BYTES,
4723
+ perMessageDeflate: DATA_CHANNEL_COMPRESSION
4724
+ });
4725
+ const clientWss = new WebSocketServer({
4726
+ noServer: true,
4727
+ maxPayload: DATA_CHANNEL_MAX_PAYLOAD_BYTES,
4728
+ perMessageDeflate: DATA_CHANNEL_COMPRESSION
4729
+ });
4518
4730
  const voiceAsrWss = new WebSocketServer({ noServer: true });
4519
4731
  const voiceTtsWss = new WebSocketServer({ noServer: true });
4520
4732
  httpServer.on("upgrade", (request, socket, head) => {
@@ -4579,13 +4791,14 @@ function createRelayServer(options) {
4579
4791
  socket.destroy();
4580
4792
  });
4581
4793
  proxyWss.on("connection", (ws) => {
4582
- handleProxyConnection(ws, registry, logger, relayChaos, remoteFileBridge);
4794
+ handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, relayChaos, remoteFileBridge);
4583
4795
  });
4584
4796
  clientWss.on("connection", (ws, request) => {
4585
4797
  handleClientConnection(
4586
4798
  ws,
4587
4799
  registry,
4588
4800
  logger,
4801
+ ptySnapshotRoutes,
4589
4802
  relayChaos,
4590
4803
  voiceConfigStore,
4591
4804
  voiceProviders,
@@ -4681,4 +4894,4 @@ export {
4681
4894
  parseRelayChaosFromEnv,
4682
4895
  createRelayServer
4683
4896
  };
4684
- //# sourceMappingURL=chunk-7KWO37WD.js.map
4897
+ //# sourceMappingURL=chunk-4Q4EAZP6.js.map