@cc-livekit/ttsignal 1.0.19 → 1.1.1

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/index.js CHANGED
@@ -1009,6 +1009,44 @@ ttsignal.ServerConnection.prototype.close = function(){
1009
1009
  * so application code does NOT need to call `connection.restart()`
1010
1010
  * manually. Set `disableAutoRestart: true` for long-lived server
1011
1011
  * deployments where the box never roams.
1012
+ * @param config.vpnPolicy {String=} optional, one of `'os'`,
1013
+ * `'prefer-physical'`, `'force-physical'`. Controls whether QUIC traffic is
1014
+ * allowed to ride a VPN / virtual interface (utun on macOS/iOS, tun on
1015
+ * Linux, wintun/TAP on Windows).
1016
+ *
1017
+ * - `'os'` — follow the kernel's routing decision, including VPN tunnels.
1018
+ * No interface pin is installed at all.
1019
+ * - `'prefer-physical'` — prefer wifi/wired/cellular. At startup, fall back
1020
+ * to a tunnel if no physical interface exists; while running, refuse to
1021
+ * migrate onto a tunnel (keep the current socket instead).
1022
+ * - `'force-physical'` — physical interfaces only. Never fall back, and
1023
+ * never undo the interface pin even when the kernel routing table says
1024
+ * the peer is reachable only through the tunnel. Use this when the server
1025
+ * must observe the client's real IP rather than the VPN egress IP.
1026
+ *
1027
+ * Defaults: `'prefer-physical'` on macOS / Windows / Linux, `'os'` on iOS.
1028
+ *
1029
+ * ⚠️ Windows behaviour change: before this option existed, Windows
1030
+ * effectively behaved as `'os'` (GetBestInterfaceEx does no interface-type
1031
+ * filtering). It now defaults to `'prefer-physical'` like the other desktop
1032
+ * platforms. Pass `vpnPolicy: 'os'` explicitly to keep the old behaviour.
1033
+ *
1034
+ * With `'force-physical'`, `connection.connect()` fails immediately with
1035
+ * `BC_R_NO_PHYSICAL_INTERFACE` (64) when no physical interface is
1036
+ * available — it does NOT wait for the connect timeout. Handle that error
1037
+ * by either reconnecting with `'prefer-physical'` or telling the user to
1038
+ * turn off their VPN.
1039
+ *
1040
+ * Linux caveat: only `SO_BINDTODEVICE` can truly guarantee packets leave
1041
+ * via the physical NIC, and it requires `CAP_NET_RAW` (or root). Without
1042
+ * that capability the addon falls back to `IP_UNICAST_IF`, which is
1043
+ * silently ignored on kernels older than 6.0.16 / 6.1.2 / 6.2 for connected
1044
+ * UDP sockets. On such hosts under a full-tunnel proxy, `'force-physical'`
1045
+ * guarantees only that failures are visible, not that the real IP gets
1046
+ * through. A warning is logged when this fallback happens.
1047
+ * @param config.bypassVpn {Boolean=} **deprecated**, use `vpnPolicy` instead.
1048
+ * Mapped as `false` -> `'os'`, `true` -> `'prefer-physical'`. When both are
1049
+ * given, `vpnPolicy` wins and a warning is logged.
1012
1050
  * @param config.proxyUrl {String=} optional MASQUE CONNECT-UDP (RFC 9298)
1013
1051
  * proxy. When set, every connection created from this connector tunnels its
1014
1052
  * QUIC packets to the real server through an HTTP/3 CONNECT-UDP session to
@@ -1042,6 +1080,15 @@ ttsignal.createConnector = function(config){
1042
1080
  if (!config.alpn){
1043
1081
  throw Error('Invalid alpn value.');
1044
1082
  }
1083
+ // 在 JS 层就对非法取值抛错,比让原生层告警后静默回落平台默认值更容易
1084
+ // 发现拼写错误——后者只会在日志里留一行 WARN,业务多半看不到。
1085
+ if (config.vpnPolicy !== undefined) {
1086
+ var VALID_VPN_POLICIES = ['os', 'prefer-physical', 'force-physical'];
1087
+ if (VALID_VPN_POLICIES.indexOf(config.vpnPolicy) === -1) {
1088
+ throw Error('Invalid vpnPolicy: ' + config.vpnPolicy +
1089
+ '. Expected one of ' + VALID_VPN_POLICIES.join(', ') + '.');
1090
+ }
1091
+ }
1045
1092
  if (config.caCertPem && !config.ca_cert_pem) {
1046
1093
  config.ca_cert_pem = config.caCertPem;
1047
1094
  }
@@ -1082,10 +1129,683 @@ ttsignal.createServer = function(config){
1082
1129
  }
1083
1130
 
1084
1131
  //*******************************************************************************
1085
- // Exports :
1132
+ // HTTP / WebSocket 栈(真实 IP 出网)的胶水层
1133
+ //
1134
+ // 原生层导出的是带双下划线的原始接口(__createHttpConnector__ / __request__ /
1135
+ // __createWsConnector__ / __connect__ ...),本节把它们包成惯用的 JS 形态。
1136
+ //
1137
+ // HTTP —— 一次性请求,用 Promise
1138
+ // const http = ttsignal.createHttpConnector({ vpnPolicy: 'force-physical' });
1139
+ // const resp = await http.request({ method: 'GET', url: 'https://host/path',
1140
+ // headers: { authorization: token },
1141
+ // timeoutMs: 5000 });
1142
+ // resp.status / resp.reason / resp.headers / resp.body(Buffer)
1143
+ // resp.peerIp / resp.boundIfIndex / resp.pinMethod
1144
+ // await http.close();
1145
+ //
1146
+ // WS —— 长连接,用 EventEmitter(与 SMP 的 Connection 一致)
1147
+ // const ws = ttsignal.createWsConnector({ vpnPolicy: 'force-physical' });
1148
+ // const conn = ws.createConnection();
1149
+ // conn.on('text', (s) => {}); conn.on('data', (buf) => {});
1150
+ // conn.on('closed', (reason) => {}); conn.on('exception', (msg) => {});
1151
+ // await conn.connect('wss://host/path', 5000);
1152
+ // conn.sendText('hello'); conn.sendData(buf);
1153
+ // await conn.close();
1154
+ // await ws.close();
1155
+ //
1156
+ // `vpnPolicy: 'force-physical'` 的意义:绕开 VPN / 虚拟网卡,用**用户真实 IP**
1157
+ // 出网(业务侧最典型的用途是请求接入点接口,避免在 VPN 环境下被调度到错误地域
1158
+ // 的节点)。是否真的绑上了物理网卡,看 `resp.boundIfIndex` /
1159
+ // `connect()` 的返回值 / `conn.info().boundIfIndex`:非 0 就是真的绑了,
1160
+ // `pinMethod` 说明绑法(macOS/iOS 是 IP_BOUND_IF,Linux 是 SO_BINDTODEVICE),
1161
+ // 不用翻日志。
1162
+ //
1163
+ // ------------------------------------------------------------------------------
1164
+ // 一、错误对象:result / errName / errMessage 一个都不能丢
1165
+ // ------------------------------------------------------------------------------
1166
+ // reject 出来的就是原生层给的那个 Error,**原样透传**,不重新包装:
1167
+ // err.result 数字错误码(BCRESULT,跨语言契约值)
1168
+ // err.errName 错误码符号名,如 'BC_R_ROUTE_MISMATCH'
1169
+ // err.errMessage 原生层的现场描述 —— force-physical 失败时这段写得很细,
1170
+ // 包含对端 IP、peerClass、内核把包判给了哪块网卡、以及该怎么
1171
+ // 改配置。**排查 force-physical 只能看这一段**,通用提示给不出
1172
+ // 这些细节。
1173
+ // 常用取值(见 CONST 里的 TTS_R_* 常量):
1174
+ // 64 BC_R_NO_PHYSICAL_INTERFACE 当下没有可用物理网卡
1175
+ // 69 BC_R_PIN_FAILED 绑定网卡失败
1176
+ // 70 BC_R_ROUTE_MISMATCH 对端从物理网卡出不去(连环回/内网即是这个)
1177
+ // 71 BC_R_DNS_FAILED DNS 失败
1178
+ // 72 BC_R_TLS_VERIFY_FAILED 证书校验失败
1179
+ // 73 BC_R_RESPONSE_TOO_LARGE 响应超过 maxResponseBytes(仅 HTTP)
1180
+ // 74 BC_R_WS_HANDSHAKE_FAILED WebSocket 握手失败(仅 WS)
1181
+ // ⚠️ 发送失败(sendText / sendData / sendPing)抛出的 Error 只有 result,没有
1182
+ // errName / errMessage —— 原生的发送接口只给错误码,不给文案。
1183
+ //
1184
+ // ------------------------------------------------------------------------------
1185
+ // 二、监听器 / Promise 回调里抛异常
1186
+ // ------------------------------------------------------------------------------
1187
+ // `emit` 是**同步**调用监听器的,而这些 emit 发生在原生回调(uv_async)栈里。
1188
+ // 监听器抛出的异常若原样冒回 C++,会在 env 里留下 pending exception,同一批
1189
+ // uv_async 的下一个事件就会让 C++ 异常逃出事件泵 -> `libc++abi: terminating`,
1190
+ // **打死整个 node 进程**。原生层已经用 DrainPendingException() 兜住了这一层
1191
+ // (会报成 uncaughtException),本胶水层再兜一道:所有 emit 都走 ttEmitSafely,
1192
+ // 监听器抛出的异常被就地捕获、挪到 process.nextTick 再抛出去。效果是:
1193
+ // * 进程不死,异常照常出现在 process 的 'uncaughtException' 上(不会被吞掉);
1194
+ // * 同一批里的其余事件、以及后续事件,都照常交付。
1195
+ // 同理,Promise 的 resolve / reject 之后本胶水层**不再执行任何业务代码** ——
1196
+ // then/catch 里的用户代码同样是同步跑在原生回调栈上的。
1197
+ //
1198
+ // ------------------------------------------------------------------------------
1199
+ // 三、close()
1200
+ // ------------------------------------------------------------------------------
1201
+ // * `connector.close()` 可以重复调,**每一次**返回的 Promise 都会 settle
1202
+ // (原生层把 close 回调收成列表,且已关闭后仍会补一次信号)。
1203
+ // * `conn.close()` 在"握手成功过"的连接上等 closed 事件;对**从没握手成功过**
1204
+ // 的连接(没 connect 过、connect 失败、还在连接中)立刻 resolve —— 那种连接
1205
+ // 按 WSConnector.h 契约第 2 条永远不会有 closed 事件,等下去就是永远悬着。
1206
+ // * 连接器关闭时,还悬着的 `conn.close()` 一并 resolve(~WSConnector 的放弃路径
1207
+ // 下 closed 永远不会来)。
1208
+ //
1209
+ // ------------------------------------------------------------------------------
1210
+ // 四、生命周期:用完**显式** close(),别指望 GC
1211
+ // ------------------------------------------------------------------------------
1212
+ // * HttpConnector / WsConnector 的析构会等在途回调跑完,**最多阻塞主线程
1213
+ // drainTimeoutMs**(默认 30 秒)。显式 close() 是异步等待,不占主线程。
1214
+ // * 业务应当**复用连接器单例**,不要按请求建连接器:每个连接器都会装一个日志
1215
+ // appender,还各自带一份 DNS / TLS 上下文。
1216
+ // * 一条 WsConnection 只能 connect 一次(第二次当场抛)。重连 = 新建连接。
1217
+ // * `logFile` 与 `log_callback` 同时给时 **logFile 优先,log_callback 一条都不会
1218
+ // 触发**(原生层给了 logFile 就只装文件 appender)。这里会 emitWarning 提醒。
1219
+ //
1220
+ // ------------------------------------------------------------------------------
1221
+ // 五、hasPathMonitor
1222
+ // ------------------------------------------------------------------------------
1223
+ // `connector.info().hasPathMonitor` 报的是**编译期**有没有 TT_HAS_PATH_MONITOR。
1224
+ // 为 false 说明这个 .node 把网卡绑定整段条件编译掉了:`prefer-physical` 会静默
1225
+ // 回落系统路由(= VPN 隧道),`force-physical` 则硬失败 64。它就是为了让"构建
1226
+ // 配置悄悄把绑定关掉"这类缺陷在任何跑得起来的产物上当场露馅 —— 生产前建议断言
1227
+ // 它为 true。
1228
+ //*******************************************************************************
1229
+
1230
+ const VPN_POLICIES = ['os', 'prefer-physical', 'force-physical'];
1231
+
1232
+ // 供业务比对 err.result 用。原生层原样透传 BCRESULT,这里只是给几个常用码起个名,
1233
+ // 免得业务里到处写魔法数字。
1234
+ const HTTP_WS_ERRORS = {
1235
+ TTS_R_NO_PHYSICAL_INTERFACE : 64,
1236
+ TTS_R_PIN_FAILED : 69,
1237
+ TTS_R_ROUTE_MISMATCH : 70,
1238
+ TTS_R_DNS_FAILED : 71,
1239
+ TTS_R_TLS_VERIFY_FAILED : 72,
1240
+ TTS_R_RESPONSE_TOO_LARGE : 73,
1241
+ TTS_R_WS_HANDSHAKE_FAILED : 74
1242
+ };
1243
+ copyProperties(ttsignal, HTTP_WS_ERRORS);
1244
+
1245
+ /**
1246
+ * 安全 emit:监听器抛出的异常挪到下一个 tick 再抛。
1247
+ *
1248
+ * emit 是同步的,而这些 emit 跑在原生回调栈上;异常原样冒回 C++ 会留下 pending
1249
+ * exception,同一批 uv_async 的下一个事件就会把整个进程打死。挪到 nextTick 抛的
1250
+ * 效果是:异常照常出现在 'uncaughtException'(不吞),但不经过 C++ 栈。
1251
+ *
1252
+ * @private
1253
+ */
1254
+ function ttEmitSafely(target, type) {
1255
+ const args = Array.prototype.slice.call(arguments, 1);
1256
+ try {
1257
+ target.emit.apply(target, args);
1258
+ } catch (err) {
1259
+ process.nextTick(function () { throw err; });
1260
+ }
1261
+ }
1262
+
1263
+ /**
1264
+ * 校验并返回一份可以交给原生层的 config。
1265
+ *
1266
+ * vpnPolicy 拼错在 JS 层就抛:原生层虽然也会抛,但在这里抛能把"合法取值"直接
1267
+ * 写进异常文案,且与 createConnector 的既有行为一致。
1268
+ *
1269
+ * @private
1270
+ */
1271
+ function ttNormalizeStackConfig(config, what) {
1272
+ if (config === undefined || config === null) {
1273
+ config = {};
1274
+ }
1275
+ if (typeof config !== 'object') {
1276
+ throw Error('Invalid config for ' + what + ': expected an object.');
1277
+ }
1278
+ if (config.vpnPolicy !== undefined && config.vpnPolicy !== null) {
1279
+ if (VPN_POLICIES.indexOf(config.vpnPolicy) === -1) {
1280
+ throw Error('Invalid vpnPolicy: ' + config.vpnPolicy +
1281
+ '. Expected one of ' + VPN_POLICIES.join(', ') + '.');
1282
+ }
1283
+ }
1284
+ // logFile 与 log_callback 互斥且 logFile 优先。静默丢掉 log_callback 是很难
1285
+ // 察觉的(业务只会看到"一条日志都没来"),所以出个警告。
1286
+ if (config.logFile && typeof config.log_callback === 'function') {
1287
+ process.emitWarning(
1288
+ '[ttsignal] ' + what + ': logFile 与 log_callback 同时给了,' +
1289
+ 'logFile 优先,log_callback 一条都不会触发。', 'TTSignalConfigWarning');
1290
+ }
1291
+ return config;
1292
+ }
1293
+
1294
+ /** 原生产物不含 HTTP/WS 栈时给一句能看懂的话,而不是 "xxx is not a function"。 */
1295
+ function ttRequireStack(factoryName, what) {
1296
+ if (typeof ttsignal[factoryName] !== 'function') {
1297
+ throw Error('当前 ttsignal 原生产物不含 ' + what + '(缺少 ' +
1298
+ factoryName + ')。请重新构建 addon(见 CLAUDE.md 的 Build Commands)。');
1299
+ }
1300
+ }
1301
+
1302
+ /*******************************************************************************
1303
+ * @class ttsignal.HttpConnector
1304
+ *
1305
+ * 一次性 HTTP/HTTPS 请求。**复用单例**,用完 await close()。
1306
+ *******************************************************************************/
1307
+
1308
+ if (typeof ttsignal.HttpConnector === 'function') {
1309
+
1310
+ inherits(ttsignal.HttpConnector, EventEmitter);
1311
+
1312
+ /**
1313
+ * @method _internalCallback
1314
+ * @private
1315
+ **/
1316
+ ttsignal.HttpConnector.prototype._internalCallback = function (type) {
1317
+ switch (type) {
1318
+ case 'close':
1319
+ // 原生层每次 __close__ 都会给一次信号(已关闭时补投一个事件),但对外的
1320
+ // 'close' 事件按 Node 惯例只发一次。
1321
+ if (this.__closeEmitted) {
1322
+ break;
1323
+ }
1324
+ this.__closeEmitted = true;
1325
+ ttEmitSafely(this, 'close');
1326
+ break;
1327
+ }
1328
+ };
1329
+
1330
+ /**
1331
+ * 发起一次请求。
1332
+ *
1333
+ * @method request
1334
+ * @public
1335
+ * @async
1336
+ * @param options {Object} 请求参数
1337
+ * @param options.url {String} 必填,http:// 或 https://
1338
+ * @param options.method {String=} 默认 GET
1339
+ * @param options.headers {Object=} 头,值必须是字符串;含 CRLF 会被当注入拦下
1340
+ * @param options.body {String|Buffer=} 请求体
1341
+ * @param options.timeoutMs {Number=} 覆盖 DNS + connect + TLS + 收响应的总预算
1342
+ * @param options.resolvedIp {String=} 跳过 DNS,直接连这个 IP
1343
+ * @return {Promise<Object>} resolve:{ status, reason, headers, body(Buffer),
1344
+ * peerIp, boundIfIndex, pinMethod };4xx / 5xx 也算正常响应(resolve)。
1345
+ * reject:原生 Error,带 result / errName / errMessage。
1346
+ *
1347
+ * ⚠️ 参数用错(缺 url、类型不对)同样是 **reject**,不是同步抛 —— 一个既可能
1348
+ * 同步抛又可能异步 reject 的 async 接口是经典的坑。那类错误没有 result 字段。
1349
+ * ⚠️ resolve 之后不要在 then 里做耗时的事:then 的回调是同步跑在原生回调栈上的。
1350
+ *
1351
+ * @example
1352
+ * const resp = await http.request({ url: 'https://ipinfo.io/ip' });
1353
+ * console.log(resp.status, resp.body.toString('utf8'), resp.boundIfIndex);
1354
+ **/
1355
+ ttsignal.HttpConnector.prototype.request = function (options) {
1356
+ const self = this;
1357
+ return new Promise(function (resolve, reject) {
1358
+ // __request__ 的同步抛(参数校验)会被 Promise 构造器转成 reject。
1359
+ self.__request__(options, function (err, resp) {
1360
+ if (err) {
1361
+ reject(err); // 原样透传:result / errName / errMessage 都在
1362
+ return;
1363
+ }
1364
+ resolve(resp);
1365
+ });
1366
+ // ⚠️ resolve / reject 之后不要再写业务代码。
1367
+ });
1368
+ };
1369
+
1370
+ /**
1371
+ * 关闭连接器。可以重复调,每一次的 Promise 都会 settle。
1372
+ *
1373
+ * 在途请求会被 reject(不会悬着)。**用完一定要显式调**:靠 GC 的话析构会在主
1374
+ * 线程上等在途回调收尾,最多阻塞 drainTimeoutMs。
1375
+ *
1376
+ * @method close
1377
+ * @public
1378
+ * @async
1379
+ * @return {Promise<void>}
1380
+ **/
1381
+ ttsignal.HttpConnector.prototype.close = function () {
1382
+ const self = this;
1383
+ return new Promise(function (resolve) {
1384
+ self.__close__(resolve);
1385
+ });
1386
+ };
1387
+
1388
+ /**
1389
+ * 生效配置与运行期状态的只读快照。**形状恒定**,关闭之后字段一个不少。
1390
+ *
1391
+ * @method info
1392
+ * @public
1393
+ * @return {Object} { closed, vpnPolicy, pendingRequests, maxResponseBytes,
1394
+ * drainTimeoutMs, maxIdleConnections, idleTimeoutMs, hasPathMonitor }
1395
+ * hasPathMonitor 为 false = 这个产物把网卡绑定条件编译掉了,见本节文档第五条。
1396
+ **/
1397
+ ttsignal.HttpConnector.prototype.info = function () {
1398
+ return this.__info__();
1399
+ };
1400
+
1401
+ /**
1402
+ * 运行期调整 keep-alive 空闲连接的存活上限。
1403
+ *
1404
+ * 0 = 不设本地超时,完全听服务端的。上限 24 小时(越界夹住并打 WARN)。
1405
+ * 连接器已关闭时静默忽略(与 close() 的幂等语义一致)。
1406
+ *
1407
+ * ⚠️ 只影响**此后**归还入池的连接:已经躺在池里的那些仍按各自入池时的时长
1408
+ * 计时。要让新值立刻对全部连接生效,改完再重建连接器。
1409
+ *
1410
+ * @method setIdleTimeoutMs
1411
+ * @public
1412
+ * @param ms {Number} 毫秒,0 .. 4294967295。非数字抛 TypeError,越界抛 RangeError
1413
+ * @example
1414
+ * const http = ttsignal.createHttpConnector();
1415
+ * http.setIdleTimeoutMs(5 * 60 * 1000); // 空闲 5 分钟就收
1416
+ * console.log(http.info().idleTimeoutMs); // 300000
1417
+ **/
1418
+ ttsignal.HttpConnector.prototype.setIdleTimeoutMs = function (ms) {
1419
+ this.__setIdleTimeoutMs__(ms);
1420
+ };
1421
+
1422
+ } // typeof ttsignal.HttpConnector === 'function'
1423
+
1424
+ /*******************************************************************************
1425
+ * @class ttsignal.WsConnector / ttsignal.WsConnection
1426
+ *******************************************************************************/
1427
+
1428
+ if (typeof ttsignal.WsConnector === 'function' &&
1429
+ typeof ttsignal.WsConnection === 'function') {
1430
+
1431
+ inherits(ttsignal.WsConnector, EventEmitter);
1432
+ inherits(ttsignal.WsConnection, EventEmitter);
1433
+
1434
+ /**
1435
+ * 把还悬着的 conn.close() Promise 收掉。重复 resolve 是无害的(Promise 幂等),
1436
+ * 所以同一个 resolver 同时登记在连接和连接器两处也没问题。
1437
+ *
1438
+ * @private
1439
+ */
1440
+ function ttDrainCloseWaiters(conn) {
1441
+ const waiters = conn.__closeWaiters;
1442
+ if (!waiters || waiters.length === 0) {
1443
+ return;
1444
+ }
1445
+ conn.__closeWaiters = [];
1446
+ const connector = conn.__connector;
1447
+ for (let i = 0; i < waiters.length; i++) {
1448
+ if (connector && connector.__connCloseWaiters) {
1449
+ connector.__connCloseWaiters.delete(waiters[i]);
1450
+ }
1451
+ waiters[i]();
1452
+ }
1453
+ }
1454
+
1455
+ /**
1456
+ * @method _internalCallback
1457
+ * @private
1458
+ **/
1459
+ ttsignal.WsConnector.prototype._internalCallback = function (type, arg) {
1460
+ switch (type) {
1461
+ case 'close':
1462
+ // 连接器收尾了 -> 不会再有任何事件进来。还悬着的 conn.close() 必须在这里
1463
+ // 收掉:~WSConnector 的放弃路径下 closed 事件永远不会来。
1464
+ if (this.__connCloseWaiters) {
1465
+ const waiters = Array.from(this.__connCloseWaiters);
1466
+ this.__connCloseWaiters.clear();
1467
+ for (let i = 0; i < waiters.length; i++) {
1468
+ waiters[i]();
1469
+ }
1470
+ }
1471
+ if (!this.__closeEmitted) {
1472
+ this.__closeEmitted = true;
1473
+ ttEmitSafely(this, 'close');
1474
+ }
1475
+ break;
1476
+ case 'exception':
1477
+ ttEmitSafely(this, 'exception', arg);
1478
+ break;
1479
+ }
1480
+ };
1481
+
1482
+ /**
1483
+ * 建一条连接(还没发起,需要再调 conn.connect())。
1484
+ *
1485
+ * @method createConnection
1486
+ * @public
1487
+ * @param config {Object=} 连接级配置,逐键覆盖连接器的默认值。常用键:
1488
+ * vpnPolicy / connectTimeoutMs / pingIntervalMs / idleTimeoutMs / maxFrameBytes /
1489
+ * caCerts / spkiPin / insecureSkipVerify / dnsServers / dnsTimeoutMs
1490
+ * @return {ttsignal.WsConnection}
1491
+ **/
1492
+ ttsignal.WsConnector.prototype.createConnection = function (config) {
1493
+ const conn = this.__createConnection__(
1494
+ config === undefined || config === null ? undefined :
1495
+ ttNormalizeStackConfig(config, 'createConnection'));
1496
+ // 连接反指连接器,用来在连接器关闭时兜底 settle 还悬着的 conn.close()。
1497
+ // 方向是单向的(连接 -> 连接器):连接器只存 resolver 闭包,**不存连接对象
1498
+ // 本身**,免得连接器反过来强引用它的连接、把 GC 挡住。
1499
+ Object.defineProperty(conn, '__connector', {
1500
+ value: this, enumerable: false, writable: false, configurable: true
1501
+ });
1502
+ return conn;
1503
+ };
1504
+
1505
+ /**
1506
+ * 关闭连接器。可以重复调,每一次的 Promise 都会 settle。
1507
+ *
1508
+ * 在途连接会被 settle(connect 的 Promise reject),已连上的连接会收到 closed。
1509
+ * **用完一定要显式调**:靠 GC 的话析构会在主线程上等在途回调收尾,最多阻塞
1510
+ * drainTimeoutMs。
1511
+ *
1512
+ * @method close
1513
+ * @public
1514
+ * @async
1515
+ * @return {Promise<void>}
1516
+ **/
1517
+ ttsignal.WsConnector.prototype.close = function () {
1518
+ const self = this;
1519
+ return new Promise(function (resolve) {
1520
+ self.__close__(resolve);
1521
+ });
1522
+ };
1523
+
1524
+ /**
1525
+ * 生效配置与运行期状态的只读快照。**形状恒定**,关闭之后字段一个不少。
1526
+ *
1527
+ * @method info
1528
+ * @public
1529
+ * @return {Object} { closed, connections, vpnPolicy, drainTimeoutMs,
1530
+ * hasPathMonitor }
1531
+ **/
1532
+ ttsignal.WsConnector.prototype.info = function () {
1533
+ return this.__info__();
1534
+ };
1535
+
1536
+ /**
1537
+ * 连接统计。关闭之后报的是最后一次快照(关完正是最想看统计的时候)。
1538
+ *
1539
+ * @method stats
1540
+ * @public
1541
+ * @return {Object} { allocated_conn_size, active_conn_size,
1542
+ * handshake_failed_size, ... }
1543
+ **/
1544
+ ttsignal.WsConnector.prototype.stats = function () {
1545
+ return this.__stats__();
1546
+ };
1547
+
1548
+ /**
1549
+ * @method _internalCallback
1550
+ * @private
1551
+ **/
1552
+ ttsignal.WsConnection.prototype._internalCallback = function (type, arg) {
1553
+ switch (type) {
1554
+ case 'text':
1555
+ ttEmitSafely(this, 'text', arg);
1556
+ break;
1557
+ case 'data':
1558
+ ttEmitSafely(this, 'data', arg);
1559
+ break;
1560
+ case 'closed':
1561
+ this.__closedSeen = true;
1562
+ // 先 emit 再收 Promise:业务的 closed 监听器应当在 await close() 返回之前
1563
+ // 跑完(emit 是同步的,close() 的 Promise 只能在微任务里继续)。
1564
+ ttEmitSafely(this, 'closed', arg);
1565
+ ttDrainCloseWaiters(this);
1566
+ break;
1567
+ case 'exception':
1568
+ ttEmitSafely(this, 'exception', arg);
1569
+ break;
1570
+ }
1571
+ };
1572
+
1573
+ /**
1574
+ * 发起连接。**一条连接只能 connect 一次**(第二次当场抛),重连 = 新建连接。
1575
+ *
1576
+ * 没有 connectResult 事件 —— 那是"恰好一次"的语义,Promise 才是自然形态。
1577
+ *
1578
+ * @method connect
1579
+ * @public
1580
+ * @async
1581
+ * @param url {String} ws:// 或 wss://
1582
+ * @param timeoutMs {Number=} DNS + connect + TLS + 握手响应的总预算,0 / 不给则
1583
+ * 用配置里的 connectTimeoutMs(默认 10 秒)
1584
+ * @return {Promise<Object>} resolve:{ headers, peerIp, boundIfIndex, pinMethod }。
1585
+ * reject:原生 Error,带 result / errName / errMessage。
1586
+ *
1587
+ * ⚠️ 握手失败与"连上之后被关闭"是可区分的:握手失败**只有** reject,**不会**有
1588
+ * closed 事件;握手成功过的连接一定先 resolve、之后某个时刻才 closed。业务的重连
1589
+ * 逻辑可以据此分支。
1590
+ *
1591
+ * @example
1592
+ * const ok = await conn.connect('wss://sfu.example.com/signal', 5000);
1593
+ * console.log(ok.peerIp, ok.boundIfIndex, ok.pinMethod);
1594
+ **/
1595
+ ttsignal.WsConnection.prototype.connect = function (url, timeoutMs) {
1596
+ const self = this;
1597
+ return new Promise(function (resolve, reject) {
1598
+ // __connect__ 的同步抛(参数校验 / 重复 connect)会被 Promise 构造器转成
1599
+ // reject —— 与 HttpConnector.request 一致。
1600
+ self.__connect__(url, timeoutMs === undefined || timeoutMs === null ?
1601
+ 0 : timeoutMs, function (err, info) {
1602
+ if (err) {
1603
+ reject(err); // 原样透传:result / errName / errMessage 都在
1604
+ return;
1605
+ }
1606
+ resolve(info);
1607
+ });
1608
+ // ⚠️ resolve / reject 之后不要再写业务代码。
1609
+ });
1610
+ };
1611
+
1612
+ /**
1613
+ * 发送一个文本帧。
1614
+ *
1615
+ * @method sendText
1616
+ * @public
1617
+ * @sync
1618
+ * @param text {String}
1619
+ * @throws {Error} 发送失败时抛出,err.result 是原生错误码(没有 errName /
1620
+ * errMessage —— 原生发送接口只给码)。**刻意抛而不是返回错误码**:返回码没人看
1621
+ * 就是静默丢消息,而 WebSocket 标准里对未 OPEN 的连接 send 也是抛的。想避开
1622
+ * try/catch 就订阅 'closed' 事件、或看 conn.info().closed。
1623
+ **/
1624
+ ttsignal.WsConnection.prototype.sendText = function (text) {
1625
+ ttThrowIfSendFailed(this.__sendText__(text), 'sendText');
1626
+ };
1627
+
1628
+ /**
1629
+ * 发送一个二进制帧。
1630
+ *
1631
+ * @method sendData
1632
+ * @public
1633
+ * @sync
1634
+ * @param data {Buffer}
1635
+ * @throws {Error} 同 sendText
1636
+ **/
1637
+ ttsignal.WsConnection.prototype.sendData = function (data) {
1638
+ ttThrowIfSendFailed(this.__sendData__(data), 'sendData');
1639
+ };
1640
+
1641
+ /**
1642
+ * 发一个 ping(对端的 pong 由原生层静默吃掉,不产生事件)。
1643
+ * 一般不用手动调:连接级 pingIntervalMs 会自动保活。
1644
+ *
1645
+ * @method sendPing
1646
+ * @public
1647
+ * @sync
1648
+ * @throws {Error} 同 sendText
1649
+ **/
1650
+ ttsignal.WsConnection.prototype.sendPing = function () {
1651
+ ttThrowIfSendFailed(this.__sendPing__(), 'sendPing');
1652
+ };
1653
+
1654
+ /** @private */
1655
+ function ttThrowIfSendFailed(result, what) {
1656
+ if (result === 0) {
1657
+ return;
1658
+ }
1659
+ const err = Error(what + ' failed: result=' + result +
1660
+ '(连接可能已关闭或还没连上;订阅 closed 事件或看 conn.info())');
1661
+ err.result = result;
1662
+ throw err;
1663
+ }
1664
+
1665
+ /**
1666
+ * 关闭这条连接。幂等;可以重复调,每一次的 Promise 都会 settle。
1667
+ *
1668
+ * @method close
1669
+ * @public
1670
+ * @async
1671
+ * @param reason {Number=} 可选的关闭原因码(BCRESULT),给了非法值按正常关闭处理
1672
+ * @return {Promise<void>} 握手成功过的连接:closed 事件到达时 resolve。从没握手
1673
+ * 成功过的连接(没 connect 过 / connect 失败 / 还在连接中):立刻 resolve ——
1674
+ * 那种连接按契约永远不会有 closed 事件,等下去就是永远悬着。连接器被关闭时,
1675
+ * 还悬着的 Promise 一并 resolve。
1676
+ **/
1677
+ ttsignal.WsConnection.prototype.close = function (reason) {
1678
+ const info = this.__info__();
1679
+ this.__close__(reason);
1680
+ if (this.__closedSeen || info.closed || !info.upgraded) {
1681
+ return Promise.resolve();
1682
+ }
1683
+ const self = this;
1684
+ return new Promise(function (resolve) {
1685
+ if (!self.__closeWaiters) {
1686
+ self.__closeWaiters = [];
1687
+ }
1688
+ self.__closeWaiters.push(resolve);
1689
+ // 连接器关闭时的兜底(放弃路径下 closed 永远不来)。只登记 resolver,
1690
+ // 不登记连接对象本身。
1691
+ const connector = self.__connector;
1692
+ if (connector) {
1693
+ if (!connector.__connCloseWaiters) {
1694
+ connector.__connCloseWaiters = new Set();
1695
+ }
1696
+ connector.__connCloseWaiters.add(resolve);
1697
+ }
1698
+ });
1699
+ };
1700
+
1701
+ /**
1702
+ * 这条连接的只读快照。**形状恒定**,关闭之后字段一个不少。
1703
+ *
1704
+ * @method info
1705
+ * @public
1706
+ * @return {Object} { id, attached, connecting, upgraded, closed, peerIp,
1707
+ * boundIfIndex, pinMethod, lastError, hasPathMonitor }
1708
+ * lastError 是原生给的最后一段失败现场描述(connectResult 的 err 只出现那一次,
1709
+ * 事后复查就看这里)。
1710
+ **/
1711
+ ttsignal.WsConnection.prototype.info = function () {
1712
+ return this.__info__();
1713
+ };
1714
+
1715
+ } // typeof ttsignal.WsConnector === 'function' && ...
1716
+
1717
+ /**
1718
+ * 创建 HTTP 连接器。**复用单例**,不要按请求建。
1719
+ *
1720
+ * @method createHttpConnector
1721
+ * @public
1722
+ * @param config {Object=} 配置
1723
+ * @param config.vpnPolicy {String=} 'os' | 'prefer-physical' | 'force-physical'。
1724
+ * 默认按平台(macOS / Windows / Linux 是 'prefer-physical')。
1725
+ * 'force-physical' = 只走物理网卡、绝不回落,用户真实 IP 出网;出不去就失败,
1726
+ * 错误码与现场描述见本节文档第一条。
1727
+ * @param config.logLevel {Number=} 1 DEBUG ... 5 FATAL
1728
+ * @param config.logFile {String=} 日志文件;给了它 log_callback 就不会触发
1729
+ * @param config.log_callback {Function=} (level, msg) => {}
1730
+ * @param config.caCerts {String=} 额外信任的 PEM
1731
+ * @param config.spkiPin {String=} SPKI pin
1732
+ * @param config.insecureSkipVerify {Boolean=} 跳过证书校验(别在生产用)
1733
+ * @param config.maxResponseBytes {Number=} 响应上限,超了报 73
1734
+ * @param config.drainTimeoutMs {Number=} 关闭时等在途请求的上限
1735
+ * @param config.dnsServers {String=} / @param config.dnsTimeoutMs {Number=}
1736
+ * @param config.idleTimeoutMs {Number=} 空闲连接在池里躺多久没人用就自行关掉,
1737
+ * 默认 3600000(60 分钟),上限 86400000(24 小时)。每次归还重新计时。
1738
+ * **0 = 不设本地超时**,完全听服务端的。运行期可用 `setIdleTimeoutMs()` 改。
1739
+ * 它是一道兜底:正常情况下服务端自己的 keep-alive 超时会先把连接收走,这里
1740
+ * 只负责挡住「永不主动关连接」的服务端把 fd 长期占住。
1741
+ * @param config.maxIdleConnections {Number=} keep-alive 连接池里每个目标
1742
+ * (host+port+tls+网卡策略)最多囤几条空闲连接,默认 4,上限 64。
1743
+ * **0 = 关掉复用**,退回「一次请求一条连接」并显式发 Connection: close。
1744
+ * 默认开着复用:同一个 connector 连发多条请求时,第二条起省掉 TCP 握手和
1745
+ * 整套 TLS 握手(实测同一 https 端点 280ms -> 95ms)。要吃到这个收益就得
1746
+ * **复用同一个 connector**,每条请求现建一个等于没开。
1747
+ * @return {ttsignal.HttpConnector}
1748
+ * @example
1749
+ * const http = ttsignal.createHttpConnector({ vpnPolicy: 'force-physical' });
1750
+ * try {
1751
+ * const resp = await http.request({ url: 'https://ipinfo.io/ip' });
1752
+ * } finally {
1753
+ * await http.close();
1754
+ * }
1755
+ **/
1756
+ ttsignal.createHttpConnector = function (config) {
1757
+ ttRequireStack('__createHttpConnector__', 'HTTP 栈');
1758
+ return ttsignal.__createHttpConnector__(
1759
+ ttNormalizeStackConfig(config, 'createHttpConnector'));
1760
+ };
1761
+
1762
+ /**
1763
+ * 创建 WebSocket 连接器。**复用单例**,一个连接器可以开多条连接。
1764
+ *
1765
+ * @method createWsConnector
1766
+ * @public
1767
+ * @param config {Object=} 配置。除 createHttpConnector 那几项(vpnPolicy /
1768
+ * logLevel / logFile / log_callback / caCerts / spkiPin / insecureSkipVerify /
1769
+ * drainTimeoutMs / dnsServers / dnsTimeoutMs)之外还有:
1770
+ * connectTimeoutMs / pingIntervalMs / idleTimeoutMs / maxFrameBytes /
1771
+ * clientCertFile / clientKeyFile / clientKeyPassword。
1772
+ * 这些键在 createConnection(config) 里可以逐条覆盖。
1773
+ * @return {ttsignal.WsConnector}
1774
+ * @example
1775
+ * const ws = ttsignal.createWsConnector({ vpnPolicy: 'force-physical' });
1776
+ * const conn = ws.createConnection();
1777
+ * conn.on('data', (buf) => handle(buf));
1778
+ * await conn.connect('wss://sfu.example.com/signal', 5000);
1779
+ **/
1780
+ ttsignal.createWsConnector = function (config) {
1781
+ ttRequireStack('__createWsConnector__', 'WebSocket 栈');
1782
+ return ttsignal.__createWsConnector__(
1783
+ ttNormalizeStackConfig(config, 'createWsConnector'));
1784
+ };
1785
+
1786
+
1787
+ //*******************************************************************************
1788
+ // Exports :
1789
+ //
1790
+ // 历史形态是 `exports.ttsignal = ttsignal`,于是 `require('ttsignal')` 拿到的是一层
1791
+ // 包装对象 —— `require('ttsignal').createConnector` 是 undefined。仓库里 js/client.js
1792
+ // / js/server.js / js/getLogFile.js / js/upload-livekit-bin.js 等示例脚本正是那么写
1793
+ // 的,一直跑不起来(既有问题,不是本次引入的)。这里把**模块本身**导出,并保留一个
1794
+ // 不可枚举的 `.ttsignal` 自引用,两种写法都成立:
1795
+ //
1796
+ // const tts = require('ttsignal'); // tts.createConnector / createHttpConnector
1797
+ // const { ttsignal } = require('ttsignal'); // 旧写法(js/pathChange.js),仍然可用
1798
+ //
1799
+ // 这是一次**放宽**,不是替换:旧调用方读到的 `.ttsignal` 还是同一个对象。
1800
+ // rtc-client 的 src/signaling/ttsignal-module.ts 做的正是 `wrapped.ttsignal ?? wrapped`,
1801
+ // 两种形态都吃,所以对它没有影响。自引用设为不可枚举,避免 console.dir / 遍历导出对象
1802
+ // 时多出一层环。
1086
1803
  //*******************************************************************************
1087
1804
 
1088
- exports.ttsignal = ttsignal;
1805
+ module.exports = ttsignal;
1806
+ Object.defineProperty(module.exports, 'ttsignal', {
1807
+ value: ttsignal, enumerable: false, writable: true, configurable: true
1808
+ });
1089
1809
 
1090
1810
  //*******************************************************************************
1091
1811
  // End of file : ttsignal.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cc-livekit/ttsignal",
3
- "version": "1.0.19",
3
+ "version": "1.1.1",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"