@dev-anywhere/relay 0.4.2 → 0.4.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.
@@ -536,6 +536,7 @@ var SessionHistoryMessageSchema = z8.object({
536
536
  cursor: z8.string().optional()
537
537
  });
538
538
  var RequestIdShape = { requestId: IdSchema.optional() };
539
+ var RequiredRequestIdShape = { requestId: IdSchema };
539
540
  var ControlErrorCodeSchema = z8.enum(Object.values(ControlErrorCode));
540
541
  var RequestErrorShape = {
541
542
  error: z8.string().optional(),
@@ -618,6 +619,30 @@ var relayControlDefinitions = [
618
619
  ...RequestErrorShape,
619
620
  capabilities: VoiceCapabilitiesSchema.optional()
620
621
  }),
622
+ // Lightweight latency probes. These measure synthetic round-trip latency for the transport
623
+ // segments and intentionally stay separate from PTY input echo tracing.
624
+ control("latency_web_relay_ping", RequiredRequestIdShape),
625
+ control("latency_web_relay_pong", {
626
+ ...RequiredRequestIdShape,
627
+ relayNow: z8.number().optional()
628
+ }),
629
+ control("latency_relay_proxy_request", RequiredRequestIdShape),
630
+ control("latency_relay_proxy_response", {
631
+ ...RequiredRequestIdShape,
632
+ success: z8.boolean(),
633
+ rttMs: z8.number().nonnegative().optional(),
634
+ error: z8.string().optional()
635
+ }),
636
+ control("latency_relay_proxy_ping", {
637
+ ...RequiredRequestIdShape,
638
+ relayNow: z8.number().optional()
639
+ }),
640
+ control("latency_relay_proxy_pong", {
641
+ ...RequiredRequestIdShape,
642
+ proxyNow: z8.number().optional()
643
+ }),
644
+ control("latency_web_proxy_ping", RequiredRequestIdShape, "client_to_proxy"),
645
+ control("latency_web_proxy_pong", { ...RequiredRequestIdShape, proxyNow: z8.number().optional() }, "proxy_to_client"),
621
646
  // 客户端注册协议
622
647
  control("client_register", {
623
648
  clientId: IdSchema
@@ -1244,7 +1269,7 @@ function healthRouter(registry, options = {}) {
1244
1269
  }
1245
1270
 
1246
1271
  // src/handlers/proxy.ts
1247
- import { WebSocket as WebSocket3 } from "ws";
1272
+ import { WebSocket as WebSocket4 } from "ws";
1248
1273
 
1249
1274
  // src/router.ts
1250
1275
  import { WebSocket as WebSocket2 } from "ws";
@@ -1331,6 +1356,100 @@ function routeClientMessage(raw, proxyId, clientWs, registry, logger, chaos) {
1331
1356
  else proxyWs.send(raw);
1332
1357
  }
1333
1358
 
1359
+ // src/latency-probes.ts
1360
+ import { performance } from "perf_hooks";
1361
+ import { WebSocket as WebSocket3 } from "ws";
1362
+ var DEFAULT_RELAY_PROXY_PROBE_TIMEOUT_MS = 3e3;
1363
+ var pendingRelayProxyProbes = /* @__PURE__ */ new Map();
1364
+ function keyFor(proxyId, requestId) {
1365
+ return `${proxyId}:${requestId}`;
1366
+ }
1367
+ function sendClientResponse(clientWs, response, chaos) {
1368
+ if (clientWs.readyState !== WebSocket3.OPEN) return;
1369
+ const raw = serializeControl(response);
1370
+ if (chaos) {
1371
+ chaos.send(clientWs, raw, {
1372
+ direction: "proxy_to_client",
1373
+ type: "latency_relay_proxy_response"
1374
+ });
1375
+ return;
1376
+ }
1377
+ clientWs.send(raw);
1378
+ }
1379
+ function startRelayProxyLatencyProbe({
1380
+ requestId,
1381
+ proxyId,
1382
+ proxyWs,
1383
+ clientWs,
1384
+ logger,
1385
+ chaos,
1386
+ timeoutMs = DEFAULT_RELAY_PROXY_PROBE_TIMEOUT_MS
1387
+ }) {
1388
+ const key = keyFor(proxyId, requestId);
1389
+ const existing = pendingRelayProxyProbes.get(key);
1390
+ if (existing) {
1391
+ clearTimeout(existing.timer);
1392
+ pendingRelayProxyProbes.delete(key);
1393
+ }
1394
+ const startedAt = performance.now();
1395
+ const timer = setTimeout(() => {
1396
+ pendingRelayProxyProbes.delete(key);
1397
+ sendClientResponse(
1398
+ clientWs,
1399
+ {
1400
+ type: "latency_relay_proxy_response",
1401
+ requestId,
1402
+ success: false,
1403
+ error: "Relay \u5230\u5F00\u53D1\u673A\u6D4B\u901F\u8D85\u65F6"
1404
+ },
1405
+ chaos
1406
+ );
1407
+ logger.warn({ proxyId, requestId }, "Relay-proxy latency probe timed out");
1408
+ }, timeoutMs);
1409
+ pendingRelayProxyProbes.set(key, {
1410
+ proxyId,
1411
+ requestId,
1412
+ clientWs,
1413
+ startedAt,
1414
+ timer,
1415
+ chaos
1416
+ });
1417
+ const raw = serializeControl({
1418
+ type: "latency_relay_proxy_ping",
1419
+ requestId,
1420
+ relayNow: Date.now()
1421
+ });
1422
+ if (chaos) {
1423
+ chaos.send(proxyWs, raw, { direction: "client_to_proxy", type: "latency_relay_proxy_ping" });
1424
+ return;
1425
+ }
1426
+ proxyWs.send(raw);
1427
+ }
1428
+ function completeRelayProxyLatencyProbe({
1429
+ proxyId,
1430
+ requestId,
1431
+ logger
1432
+ }) {
1433
+ const key = keyFor(proxyId, requestId);
1434
+ const pending = pendingRelayProxyProbes.get(key);
1435
+ if (!pending) return false;
1436
+ pendingRelayProxyProbes.delete(key);
1437
+ clearTimeout(pending.timer);
1438
+ const rttMs = performance.now() - pending.startedAt;
1439
+ sendClientResponse(
1440
+ pending.clientWs,
1441
+ {
1442
+ type: "latency_relay_proxy_response",
1443
+ requestId,
1444
+ success: true,
1445
+ rttMs
1446
+ },
1447
+ pending.chaos
1448
+ );
1449
+ logger.debug({ proxyId, requestId, rttMs }, "Relay-proxy latency probe completed");
1450
+ return true;
1451
+ }
1452
+
1334
1453
  // src/handlers/proxy.ts
1335
1454
  var MAX_BINARY_FRAME_SIZE = 10 * 1024 * 1024;
1336
1455
  var MAX_JSON_MESSAGE_SIZE = 1 * 1024 * 1024;
@@ -1399,7 +1518,7 @@ function handleProxyConnection(ws, registry, logger, chaos) {
1399
1518
  }
1400
1519
  const clients = registry.getClientsForProxy(proxyWs.proxyId);
1401
1520
  for (const clientWs of clients) {
1402
- if (clientWs.readyState === WebSocket3.OPEN) {
1521
+ if (clientWs.readyState === WebSocket4.OPEN) {
1403
1522
  clientWs.send(data);
1404
1523
  }
1405
1524
  }
@@ -1453,6 +1572,24 @@ function handleProxyConnection(ws, registry, logger, chaos) {
1453
1572
  logger.info({ proxyId: proxyWs.proxyId, count: sessions.length }, "Session sync received");
1454
1573
  return;
1455
1574
  }
1575
+ if (result.kind === "control" && result.message.type === "latency_relay_proxy_pong") {
1576
+ if (!proxyWs.proxyId) {
1577
+ rejectNotRegistered(proxyWs);
1578
+ return;
1579
+ }
1580
+ const completed = completeRelayProxyLatencyProbe({
1581
+ proxyId: proxyWs.proxyId,
1582
+ requestId: result.message.requestId,
1583
+ logger
1584
+ });
1585
+ if (!completed) {
1586
+ logger.debug(
1587
+ { proxyId: proxyWs.proxyId, requestId: result.message.requestId },
1588
+ "Unmatched relay-proxy latency pong ignored"
1589
+ );
1590
+ }
1591
+ return;
1592
+ }
1456
1593
  if (result.kind === "control") {
1457
1594
  if (isProxyToClientRelayControlType(result.message.type)) {
1458
1595
  if (!proxyWs.proxyId) {
@@ -1461,7 +1598,7 @@ function handleProxyConnection(ws, registry, logger, chaos) {
1461
1598
  }
1462
1599
  const clients = registry.getClientsForProxy(proxyWs.proxyId);
1463
1600
  for (const clientWs of clients) {
1464
- if (clientWs.readyState === WebSocket3.OPEN) {
1601
+ if (clientWs.readyState === WebSocket4.OPEN) {
1465
1602
  if (chaos) {
1466
1603
  chaos.send(clientWs, raw, {
1467
1604
  direction: "proxy_to_client",
@@ -1533,13 +1670,13 @@ function handleProxyConnection(ws, registry, logger, chaos) {
1533
1670
  }
1534
1671
 
1535
1672
  // src/handlers/client.ts
1536
- import { WebSocket as WebSocket6 } from "ws";
1673
+ import { WebSocket as WebSocket7 } from "ws";
1537
1674
  import { nanoid as nanoid3 } from "nanoid";
1538
1675
 
1539
1676
  // src/voice/bailian-asr.ts
1540
1677
  import { EventEmitter } from "events";
1541
1678
  import { nanoid } from "nanoid";
1542
- import { WebSocket as WebSocket4 } from "ws";
1679
+ import { WebSocket as WebSocket5 } from "ws";
1543
1680
 
1544
1681
  // src/voice/bailian-endpoints.ts
1545
1682
  var BAILIAN_HOSTS = {
@@ -1558,7 +1695,7 @@ function bailianInferenceUrl(region) {
1558
1695
  var OPEN = 1;
1559
1696
  var END_OF_SPEECH_SILENCE_MS = 1200;
1560
1697
  function defaultSocketFactory(url, options) {
1561
- return new WebSocket4(url, options);
1698
+ return new WebSocket5(url, options);
1562
1699
  }
1563
1700
  function extractRealtimePreview(payload) {
1564
1701
  if (!payload || typeof payload !== "object") return null;
@@ -1735,10 +1872,10 @@ function createBailianAsrClient(config, options = {}) {
1735
1872
  // src/voice/bailian-tts.ts
1736
1873
  import { EventEmitter as EventEmitter2 } from "events";
1737
1874
  import { nanoid as nanoid2 } from "nanoid";
1738
- import { WebSocket as WebSocket5 } from "ws";
1875
+ import { WebSocket as WebSocket6 } from "ws";
1739
1876
  var OPEN2 = 1;
1740
1877
  function defaultSocketFactory2(url, options) {
1741
- return new WebSocket5(url, options);
1878
+ return new WebSocket6(url, options);
1742
1879
  }
1743
1880
  function errorFromPayload(payload) {
1744
1881
  if (!payload || typeof payload !== "object") return new Error("Bailian TTS error");
@@ -2240,6 +2377,22 @@ function rejectProxySelect(ws, requestId, proxyId) {
2240
2377
  })
2241
2378
  );
2242
2379
  }
2380
+ function sendRelayProxyProbeFailure(ws, requestId, error, chaos) {
2381
+ const response = serializeControl({
2382
+ type: "latency_relay_proxy_response",
2383
+ requestId,
2384
+ success: false,
2385
+ error
2386
+ });
2387
+ if (chaos) {
2388
+ chaos.send(ws, response, {
2389
+ direction: "proxy_to_client",
2390
+ type: "latency_relay_proxy_response"
2391
+ });
2392
+ return;
2393
+ }
2394
+ ws.send(response);
2395
+ }
2243
2396
  function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, voiceProviders) {
2244
2397
  const clientWs = ws;
2245
2398
  clientWs.isAlive = true;
@@ -2290,6 +2443,48 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
2290
2443
  }
2291
2444
  return;
2292
2445
  }
2446
+ if (msg.type === "latency_web_relay_ping") {
2447
+ const response = serializeControl({
2448
+ type: "latency_web_relay_pong",
2449
+ requestId: msg.requestId,
2450
+ relayNow: Date.now()
2451
+ });
2452
+ if (chaos) {
2453
+ chaos.send(clientWs, response, {
2454
+ direction: "proxy_to_client",
2455
+ type: "latency_web_relay_pong"
2456
+ });
2457
+ } else {
2458
+ clientWs.send(response);
2459
+ }
2460
+ return;
2461
+ }
2462
+ if (msg.type === "latency_relay_proxy_request") {
2463
+ const targetProxyId = clientWs.boundProxyId;
2464
+ if (!targetProxyId) {
2465
+ sendRelayProxyProbeFailure(clientWs, msg.requestId, "\u5F53\u524D\u672A\u8FDE\u63A5\u5F00\u53D1\u673A", chaos);
2466
+ return;
2467
+ }
2468
+ const proxyWs = registry.getProxy(targetProxyId);
2469
+ if (!proxyWs || proxyWs.readyState !== WebSocket7.OPEN) {
2470
+ sendRelayProxyProbeFailure(
2471
+ clientWs,
2472
+ msg.requestId,
2473
+ `\u5F00\u53D1\u673A ${targetProxyId} \u4E0D\u5728\u7EBF`,
2474
+ chaos
2475
+ );
2476
+ return;
2477
+ }
2478
+ startRelayProxyLatencyProbe({
2479
+ requestId: msg.requestId,
2480
+ proxyId: targetProxyId,
2481
+ proxyWs,
2482
+ clientWs,
2483
+ logger,
2484
+ chaos
2485
+ });
2486
+ return;
2487
+ }
2293
2488
  if (voiceConfigStore && handleVoiceConfigControl(msg, clientWs, voiceConfigStore, logger, voiceProviders)) {
2294
2489
  return;
2295
2490
  }
@@ -2300,7 +2495,7 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
2300
2495
  return;
2301
2496
  }
2302
2497
  const proxyWs = registry.getProxy(targetProxyId);
2303
- if (proxyWs && proxyWs.readyState === WebSocket6.OPEN) {
2498
+ if (proxyWs && proxyWs.readyState === WebSocket7.OPEN) {
2304
2499
  if (chaos) chaos.send(proxyWs, raw, { direction: "client_to_proxy", type: msg.type });
2305
2500
  else proxyWs.send(raw);
2306
2501
  } else {
@@ -2408,7 +2603,7 @@ function setupHeartbeat(wss, interval = 3e4) {
2408
2603
  }
2409
2604
 
2410
2605
  // src/chaos.ts
2411
- import { WebSocket as WebSocket7 } from "ws";
2606
+ import { WebSocket as WebSocket8 } from "ws";
2412
2607
  function parseRelayChaosFromEnv(env) {
2413
2608
  const enabled = env.DEV_ANYWHERE_RELAY_CHAOS === "1";
2414
2609
  const types = env.DEV_ANYWHERE_RELAY_CHAOS_TYPES?.split(",").map((type) => type.trim()).filter(Boolean);
@@ -2430,7 +2625,7 @@ function createRelayChaos(options, logger) {
2430
2625
  return true;
2431
2626
  }
2432
2627
  function sendNow(ws, data) {
2433
- if (ws.readyState === WebSocket7.OPEN) {
2628
+ if (ws.readyState === WebSocket8.OPEN) {
2434
2629
  ws.send(data);
2435
2630
  }
2436
2631
  }
@@ -2547,9 +2742,9 @@ function createVoiceConfigStore(options = {}) {
2547
2742
  }
2548
2743
 
2549
2744
  // src/voice/asr-ws.ts
2550
- import { WebSocket as WebSocket8 } from "ws";
2745
+ import { WebSocket as WebSocket9 } from "ws";
2551
2746
  function sendJson(ws, payload) {
2552
- if (ws.readyState === WebSocket8.OPEN) {
2747
+ if (ws.readyState === WebSocket9.OPEN) {
2553
2748
  ws.send(JSON.stringify(payload));
2554
2749
  }
2555
2750
  }
@@ -2622,9 +2817,9 @@ function handleVoiceAsrConnection(ws, store, logger, providers) {
2622
2817
  }
2623
2818
 
2624
2819
  // src/voice/tts-ws.ts
2625
- import { WebSocket as WebSocket9 } from "ws";
2820
+ import { WebSocket as WebSocket10 } from "ws";
2626
2821
  function sendJson2(ws, payload) {
2627
- if (ws.readyState === WebSocket9.OPEN) {
2822
+ if (ws.readyState === WebSocket10.OPEN) {
2628
2823
  ws.send(JSON.stringify(payload));
2629
2824
  }
2630
2825
  }
@@ -2707,7 +2902,7 @@ function handleVoiceTtsConnection(ws, store, logger, providers) {
2707
2902
  activeStats.audioBytes += chunk.byteLength;
2708
2903
  activeStats.audioChunks += 1;
2709
2904
  }
2710
- if (ws.readyState === WebSocket9.OPEN) ws.send(chunk);
2905
+ if (ws.readyState === WebSocket10.OPEN) ws.send(chunk);
2711
2906
  });
2712
2907
  provider.on("finished", () => {
2713
2908
  if (activeStats) logger.info(statsLogFields(activeStats), "Voice TTS finished");
@@ -3161,4 +3356,4 @@ export {
3161
3356
  parseRelayChaosFromEnv,
3162
3357
  createRelayServer
3163
3358
  };
3164
- //# sourceMappingURL=chunk-ERH2EO6I.js.map
3359
+ //# sourceMappingURL=chunk-OJABHE5C.js.map