@cc-livekit/ttsignal 1.0.18 → 1.1.0
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/build/Release/ttsignal.darwin.arm64.node +0 -0
- package/build/Release/ttsignal.darwin.x64.node +0 -0
- package/build/Release/ttsignal.linux.arm64.node +0 -0
- package/build/Release/ttsignal.linux.x64.node +0 -0
- package/build/Release/ttsignal.win32.x64.node +0 -0
- package/index.js +724 -2
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.js
CHANGED
|
@@ -1009,11 +1009,69 @@ 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.
|
|
1050
|
+
* @param config.proxyUrl {String=} optional MASQUE CONNECT-UDP (RFC 9298)
|
|
1051
|
+
* proxy. When set, every connection created from this connector tunnels its
|
|
1052
|
+
* QUIC packets to the real server through an HTTP/3 CONNECT-UDP session to
|
|
1053
|
+
* this proxy (the proxy must speak MASQUE connect-udp). Accepted forms:
|
|
1054
|
+
* `masque://host:port`, `https://host:port`, `h3://host:port`, or a bare
|
|
1055
|
+
* `host:port` (defaults to MASQUE). Port defaults to 443; IPv6 must be
|
|
1056
|
+
* bracketed (`[2001:db8::1]:443`). Alias: `proxy_url`.
|
|
1057
|
+
* @param config.proxyHost {String=} optional, proxy host. Overrides the host
|
|
1058
|
+
* parsed from `proxyUrl`; setting it alone enables the MASQUE proxy.
|
|
1059
|
+
* Alias: `proxy_host`.
|
|
1060
|
+
* @param config.proxyPort {Number=} optional, proxy port (default 443).
|
|
1061
|
+
* Alias: `proxy_port`.
|
|
1062
|
+
* @param config.proxySni {String=} optional, TLS SNI presented to the proxy
|
|
1063
|
+
* (defaults to the proxy host). Alias: `proxy_sni`.
|
|
1012
1064
|
* @example
|
|
1013
1065
|
* var connector = ttsignal.createConnector({
|
|
1014
1066
|
* alpn: 'ttsignal',
|
|
1015
1067
|
* disableAutoRestart: false, // default
|
|
1016
1068
|
* });
|
|
1069
|
+
* @example
|
|
1070
|
+
* // Route all connections through a MASQUE CONNECT-UDP proxy:
|
|
1071
|
+
* var connector = ttsignal.createConnector({
|
|
1072
|
+
* alpn: 'ttsignal',
|
|
1073
|
+
* proxyUrl: 'masque://proxy.example.com:443',
|
|
1074
|
+
* });
|
|
1017
1075
|
**/
|
|
1018
1076
|
ttsignal.createConnector = function(config){
|
|
1019
1077
|
if (typeof config !== 'object') {
|
|
@@ -1022,9 +1080,32 @@ ttsignal.createConnector = function(config){
|
|
|
1022
1080
|
if (!config.alpn){
|
|
1023
1081
|
throw Error('Invalid alpn value.');
|
|
1024
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
|
+
}
|
|
1025
1092
|
if (config.caCertPem && !config.ca_cert_pem) {
|
|
1026
1093
|
config.ca_cert_pem = config.caCertPem;
|
|
1027
1094
|
}
|
|
1095
|
+
// MASQUE CONNECT-UDP proxy config: accept camelCase aliases and forward as
|
|
1096
|
+
// the snake_case keys the native layer reads (proxy_url/host/port/sni).
|
|
1097
|
+
if (config.proxyUrl && !config.proxy_url) {
|
|
1098
|
+
config.proxy_url = config.proxyUrl;
|
|
1099
|
+
}
|
|
1100
|
+
if (config.proxyHost && !config.proxy_host) {
|
|
1101
|
+
config.proxy_host = config.proxyHost;
|
|
1102
|
+
}
|
|
1103
|
+
if (config.proxyPort && !config.proxy_port) {
|
|
1104
|
+
config.proxy_port = config.proxyPort;
|
|
1105
|
+
}
|
|
1106
|
+
if (config.proxySni && !config.proxy_sni) {
|
|
1107
|
+
config.proxy_sni = config.proxySni;
|
|
1108
|
+
}
|
|
1028
1109
|
return ttsignal.__createConnector__(config);
|
|
1029
1110
|
}
|
|
1030
1111
|
|
|
@@ -1048,10 +1129,651 @@ ttsignal.createServer = function(config){
|
|
|
1048
1129
|
}
|
|
1049
1130
|
|
|
1050
1131
|
//*******************************************************************************
|
|
1051
|
-
//
|
|
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, hasPathMonitor }
|
|
1395
|
+
* hasPathMonitor 为 false = 这个产物把网卡绑定条件编译掉了,见本节文档第五条。
|
|
1396
|
+
**/
|
|
1397
|
+
ttsignal.HttpConnector.prototype.info = function () {
|
|
1398
|
+
return this.__info__();
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
} // typeof ttsignal.HttpConnector === 'function'
|
|
1402
|
+
|
|
1403
|
+
/*******************************************************************************
|
|
1404
|
+
* @class ttsignal.WsConnector / ttsignal.WsConnection
|
|
1405
|
+
*******************************************************************************/
|
|
1406
|
+
|
|
1407
|
+
if (typeof ttsignal.WsConnector === 'function' &&
|
|
1408
|
+
typeof ttsignal.WsConnection === 'function') {
|
|
1409
|
+
|
|
1410
|
+
inherits(ttsignal.WsConnector, EventEmitter);
|
|
1411
|
+
inherits(ttsignal.WsConnection, EventEmitter);
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* 把还悬着的 conn.close() Promise 收掉。重复 resolve 是无害的(Promise 幂等),
|
|
1415
|
+
* 所以同一个 resolver 同时登记在连接和连接器两处也没问题。
|
|
1416
|
+
*
|
|
1417
|
+
* @private
|
|
1418
|
+
*/
|
|
1419
|
+
function ttDrainCloseWaiters(conn) {
|
|
1420
|
+
const waiters = conn.__closeWaiters;
|
|
1421
|
+
if (!waiters || waiters.length === 0) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
conn.__closeWaiters = [];
|
|
1425
|
+
const connector = conn.__connector;
|
|
1426
|
+
for (let i = 0; i < waiters.length; i++) {
|
|
1427
|
+
if (connector && connector.__connCloseWaiters) {
|
|
1428
|
+
connector.__connCloseWaiters.delete(waiters[i]);
|
|
1429
|
+
}
|
|
1430
|
+
waiters[i]();
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* @method _internalCallback
|
|
1436
|
+
* @private
|
|
1437
|
+
**/
|
|
1438
|
+
ttsignal.WsConnector.prototype._internalCallback = function (type, arg) {
|
|
1439
|
+
switch (type) {
|
|
1440
|
+
case 'close':
|
|
1441
|
+
// 连接器收尾了 -> 不会再有任何事件进来。还悬着的 conn.close() 必须在这里
|
|
1442
|
+
// 收掉:~WSConnector 的放弃路径下 closed 事件永远不会来。
|
|
1443
|
+
if (this.__connCloseWaiters) {
|
|
1444
|
+
const waiters = Array.from(this.__connCloseWaiters);
|
|
1445
|
+
this.__connCloseWaiters.clear();
|
|
1446
|
+
for (let i = 0; i < waiters.length; i++) {
|
|
1447
|
+
waiters[i]();
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (!this.__closeEmitted) {
|
|
1451
|
+
this.__closeEmitted = true;
|
|
1452
|
+
ttEmitSafely(this, 'close');
|
|
1453
|
+
}
|
|
1454
|
+
break;
|
|
1455
|
+
case 'exception':
|
|
1456
|
+
ttEmitSafely(this, 'exception', arg);
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* 建一条连接(还没发起,需要再调 conn.connect())。
|
|
1463
|
+
*
|
|
1464
|
+
* @method createConnection
|
|
1465
|
+
* @public
|
|
1466
|
+
* @param config {Object=} 连接级配置,逐键覆盖连接器的默认值。常用键:
|
|
1467
|
+
* vpnPolicy / connectTimeoutMs / pingIntervalMs / idleTimeoutMs / maxFrameBytes /
|
|
1468
|
+
* caCerts / spkiPin / insecureSkipVerify / dnsServers / dnsTimeoutMs
|
|
1469
|
+
* @return {ttsignal.WsConnection}
|
|
1470
|
+
**/
|
|
1471
|
+
ttsignal.WsConnector.prototype.createConnection = function (config) {
|
|
1472
|
+
const conn = this.__createConnection__(
|
|
1473
|
+
config === undefined || config === null ? undefined :
|
|
1474
|
+
ttNormalizeStackConfig(config, 'createConnection'));
|
|
1475
|
+
// 连接反指连接器,用来在连接器关闭时兜底 settle 还悬着的 conn.close()。
|
|
1476
|
+
// 方向是单向的(连接 -> 连接器):连接器只存 resolver 闭包,**不存连接对象
|
|
1477
|
+
// 本身**,免得连接器反过来强引用它的连接、把 GC 挡住。
|
|
1478
|
+
Object.defineProperty(conn, '__connector', {
|
|
1479
|
+
value: this, enumerable: false, writable: false, configurable: true
|
|
1480
|
+
});
|
|
1481
|
+
return conn;
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* 关闭连接器。可以重复调,每一次的 Promise 都会 settle。
|
|
1486
|
+
*
|
|
1487
|
+
* 在途连接会被 settle(connect 的 Promise reject),已连上的连接会收到 closed。
|
|
1488
|
+
* **用完一定要显式调**:靠 GC 的话析构会在主线程上等在途回调收尾,最多阻塞
|
|
1489
|
+
* drainTimeoutMs。
|
|
1490
|
+
*
|
|
1491
|
+
* @method close
|
|
1492
|
+
* @public
|
|
1493
|
+
* @async
|
|
1494
|
+
* @return {Promise<void>}
|
|
1495
|
+
**/
|
|
1496
|
+
ttsignal.WsConnector.prototype.close = function () {
|
|
1497
|
+
const self = this;
|
|
1498
|
+
return new Promise(function (resolve) {
|
|
1499
|
+
self.__close__(resolve);
|
|
1500
|
+
});
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* 生效配置与运行期状态的只读快照。**形状恒定**,关闭之后字段一个不少。
|
|
1505
|
+
*
|
|
1506
|
+
* @method info
|
|
1507
|
+
* @public
|
|
1508
|
+
* @return {Object} { closed, connections, vpnPolicy, drainTimeoutMs,
|
|
1509
|
+
* hasPathMonitor }
|
|
1510
|
+
**/
|
|
1511
|
+
ttsignal.WsConnector.prototype.info = function () {
|
|
1512
|
+
return this.__info__();
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* 连接统计。关闭之后报的是最后一次快照(关完正是最想看统计的时候)。
|
|
1517
|
+
*
|
|
1518
|
+
* @method stats
|
|
1519
|
+
* @public
|
|
1520
|
+
* @return {Object} { allocated_conn_size, active_conn_size,
|
|
1521
|
+
* handshake_failed_size, ... }
|
|
1522
|
+
**/
|
|
1523
|
+
ttsignal.WsConnector.prototype.stats = function () {
|
|
1524
|
+
return this.__stats__();
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* @method _internalCallback
|
|
1529
|
+
* @private
|
|
1530
|
+
**/
|
|
1531
|
+
ttsignal.WsConnection.prototype._internalCallback = function (type, arg) {
|
|
1532
|
+
switch (type) {
|
|
1533
|
+
case 'text':
|
|
1534
|
+
ttEmitSafely(this, 'text', arg);
|
|
1535
|
+
break;
|
|
1536
|
+
case 'data':
|
|
1537
|
+
ttEmitSafely(this, 'data', arg);
|
|
1538
|
+
break;
|
|
1539
|
+
case 'closed':
|
|
1540
|
+
this.__closedSeen = true;
|
|
1541
|
+
// 先 emit 再收 Promise:业务的 closed 监听器应当在 await close() 返回之前
|
|
1542
|
+
// 跑完(emit 是同步的,close() 的 Promise 只能在微任务里继续)。
|
|
1543
|
+
ttEmitSafely(this, 'closed', arg);
|
|
1544
|
+
ttDrainCloseWaiters(this);
|
|
1545
|
+
break;
|
|
1546
|
+
case 'exception':
|
|
1547
|
+
ttEmitSafely(this, 'exception', arg);
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
/**
|
|
1553
|
+
* 发起连接。**一条连接只能 connect 一次**(第二次当场抛),重连 = 新建连接。
|
|
1554
|
+
*
|
|
1555
|
+
* 没有 connectResult 事件 —— 那是"恰好一次"的语义,Promise 才是自然形态。
|
|
1556
|
+
*
|
|
1557
|
+
* @method connect
|
|
1558
|
+
* @public
|
|
1559
|
+
* @async
|
|
1560
|
+
* @param url {String} ws:// 或 wss://
|
|
1561
|
+
* @param timeoutMs {Number=} DNS + connect + TLS + 握手响应的总预算,0 / 不给则
|
|
1562
|
+
* 用配置里的 connectTimeoutMs(默认 10 秒)
|
|
1563
|
+
* @return {Promise<Object>} resolve:{ headers, peerIp, boundIfIndex, pinMethod }。
|
|
1564
|
+
* reject:原生 Error,带 result / errName / errMessage。
|
|
1565
|
+
*
|
|
1566
|
+
* ⚠️ 握手失败与"连上之后被关闭"是可区分的:握手失败**只有** reject,**不会**有
|
|
1567
|
+
* closed 事件;握手成功过的连接一定先 resolve、之后某个时刻才 closed。业务的重连
|
|
1568
|
+
* 逻辑可以据此分支。
|
|
1569
|
+
*
|
|
1570
|
+
* @example
|
|
1571
|
+
* const ok = await conn.connect('wss://sfu.example.com/signal', 5000);
|
|
1572
|
+
* console.log(ok.peerIp, ok.boundIfIndex, ok.pinMethod);
|
|
1573
|
+
**/
|
|
1574
|
+
ttsignal.WsConnection.prototype.connect = function (url, timeoutMs) {
|
|
1575
|
+
const self = this;
|
|
1576
|
+
return new Promise(function (resolve, reject) {
|
|
1577
|
+
// __connect__ 的同步抛(参数校验 / 重复 connect)会被 Promise 构造器转成
|
|
1578
|
+
// reject —— 与 HttpConnector.request 一致。
|
|
1579
|
+
self.__connect__(url, timeoutMs === undefined || timeoutMs === null ?
|
|
1580
|
+
0 : timeoutMs, function (err, info) {
|
|
1581
|
+
if (err) {
|
|
1582
|
+
reject(err); // 原样透传:result / errName / errMessage 都在
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
resolve(info);
|
|
1586
|
+
});
|
|
1587
|
+
// ⚠️ resolve / reject 之后不要再写业务代码。
|
|
1588
|
+
});
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* 发送一个文本帧。
|
|
1593
|
+
*
|
|
1594
|
+
* @method sendText
|
|
1595
|
+
* @public
|
|
1596
|
+
* @sync
|
|
1597
|
+
* @param text {String}
|
|
1598
|
+
* @throws {Error} 发送失败时抛出,err.result 是原生错误码(没有 errName /
|
|
1599
|
+
* errMessage —— 原生发送接口只给码)。**刻意抛而不是返回错误码**:返回码没人看
|
|
1600
|
+
* 就是静默丢消息,而 WebSocket 标准里对未 OPEN 的连接 send 也是抛的。想避开
|
|
1601
|
+
* try/catch 就订阅 'closed' 事件、或看 conn.info().closed。
|
|
1602
|
+
**/
|
|
1603
|
+
ttsignal.WsConnection.prototype.sendText = function (text) {
|
|
1604
|
+
ttThrowIfSendFailed(this.__sendText__(text), 'sendText');
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* 发送一个二进制帧。
|
|
1609
|
+
*
|
|
1610
|
+
* @method sendData
|
|
1611
|
+
* @public
|
|
1612
|
+
* @sync
|
|
1613
|
+
* @param data {Buffer}
|
|
1614
|
+
* @throws {Error} 同 sendText
|
|
1615
|
+
**/
|
|
1616
|
+
ttsignal.WsConnection.prototype.sendData = function (data) {
|
|
1617
|
+
ttThrowIfSendFailed(this.__sendData__(data), 'sendData');
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* 发一个 ping(对端的 pong 由原生层静默吃掉,不产生事件)。
|
|
1622
|
+
* 一般不用手动调:连接级 pingIntervalMs 会自动保活。
|
|
1623
|
+
*
|
|
1624
|
+
* @method sendPing
|
|
1625
|
+
* @public
|
|
1626
|
+
* @sync
|
|
1627
|
+
* @throws {Error} 同 sendText
|
|
1628
|
+
**/
|
|
1629
|
+
ttsignal.WsConnection.prototype.sendPing = function () {
|
|
1630
|
+
ttThrowIfSendFailed(this.__sendPing__(), 'sendPing');
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
/** @private */
|
|
1634
|
+
function ttThrowIfSendFailed(result, what) {
|
|
1635
|
+
if (result === 0) {
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const err = Error(what + ' failed: result=' + result +
|
|
1639
|
+
'(连接可能已关闭或还没连上;订阅 closed 事件或看 conn.info())');
|
|
1640
|
+
err.result = result;
|
|
1641
|
+
throw err;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* 关闭这条连接。幂等;可以重复调,每一次的 Promise 都会 settle。
|
|
1646
|
+
*
|
|
1647
|
+
* @method close
|
|
1648
|
+
* @public
|
|
1649
|
+
* @async
|
|
1650
|
+
* @param reason {Number=} 可选的关闭原因码(BCRESULT),给了非法值按正常关闭处理
|
|
1651
|
+
* @return {Promise<void>} 握手成功过的连接:closed 事件到达时 resolve。从没握手
|
|
1652
|
+
* 成功过的连接(没 connect 过 / connect 失败 / 还在连接中):立刻 resolve ——
|
|
1653
|
+
* 那种连接按契约永远不会有 closed 事件,等下去就是永远悬着。连接器被关闭时,
|
|
1654
|
+
* 还悬着的 Promise 一并 resolve。
|
|
1655
|
+
**/
|
|
1656
|
+
ttsignal.WsConnection.prototype.close = function (reason) {
|
|
1657
|
+
const info = this.__info__();
|
|
1658
|
+
this.__close__(reason);
|
|
1659
|
+
if (this.__closedSeen || info.closed || !info.upgraded) {
|
|
1660
|
+
return Promise.resolve();
|
|
1661
|
+
}
|
|
1662
|
+
const self = this;
|
|
1663
|
+
return new Promise(function (resolve) {
|
|
1664
|
+
if (!self.__closeWaiters) {
|
|
1665
|
+
self.__closeWaiters = [];
|
|
1666
|
+
}
|
|
1667
|
+
self.__closeWaiters.push(resolve);
|
|
1668
|
+
// 连接器关闭时的兜底(放弃路径下 closed 永远不来)。只登记 resolver,
|
|
1669
|
+
// 不登记连接对象本身。
|
|
1670
|
+
const connector = self.__connector;
|
|
1671
|
+
if (connector) {
|
|
1672
|
+
if (!connector.__connCloseWaiters) {
|
|
1673
|
+
connector.__connCloseWaiters = new Set();
|
|
1674
|
+
}
|
|
1675
|
+
connector.__connCloseWaiters.add(resolve);
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
};
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* 这条连接的只读快照。**形状恒定**,关闭之后字段一个不少。
|
|
1682
|
+
*
|
|
1683
|
+
* @method info
|
|
1684
|
+
* @public
|
|
1685
|
+
* @return {Object} { id, attached, connecting, upgraded, closed, peerIp,
|
|
1686
|
+
* boundIfIndex, pinMethod, lastError, hasPathMonitor }
|
|
1687
|
+
* lastError 是原生给的最后一段失败现场描述(connectResult 的 err 只出现那一次,
|
|
1688
|
+
* 事后复查就看这里)。
|
|
1689
|
+
**/
|
|
1690
|
+
ttsignal.WsConnection.prototype.info = function () {
|
|
1691
|
+
return this.__info__();
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
} // typeof ttsignal.WsConnector === 'function' && ...
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* 创建 HTTP 连接器。**复用单例**,不要按请求建。
|
|
1698
|
+
*
|
|
1699
|
+
* @method createHttpConnector
|
|
1700
|
+
* @public
|
|
1701
|
+
* @param config {Object=} 配置
|
|
1702
|
+
* @param config.vpnPolicy {String=} 'os' | 'prefer-physical' | 'force-physical'。
|
|
1703
|
+
* 默认按平台(macOS / Windows / Linux 是 'prefer-physical')。
|
|
1704
|
+
* 'force-physical' = 只走物理网卡、绝不回落,用户真实 IP 出网;出不去就失败,
|
|
1705
|
+
* 错误码与现场描述见本节文档第一条。
|
|
1706
|
+
* @param config.logLevel {Number=} 1 DEBUG ... 5 FATAL
|
|
1707
|
+
* @param config.logFile {String=} 日志文件;给了它 log_callback 就不会触发
|
|
1708
|
+
* @param config.log_callback {Function=} (level, msg) => {}
|
|
1709
|
+
* @param config.caCerts {String=} 额外信任的 PEM
|
|
1710
|
+
* @param config.spkiPin {String=} SPKI pin
|
|
1711
|
+
* @param config.insecureSkipVerify {Boolean=} 跳过证书校验(别在生产用)
|
|
1712
|
+
* @param config.maxResponseBytes {Number=} 响应上限,超了报 73
|
|
1713
|
+
* @param config.drainTimeoutMs {Number=} 关闭时等在途请求的上限
|
|
1714
|
+
* @param config.dnsServers {String=} / @param config.dnsTimeoutMs {Number=}
|
|
1715
|
+
* @return {ttsignal.HttpConnector}
|
|
1716
|
+
* @example
|
|
1717
|
+
* const http = ttsignal.createHttpConnector({ vpnPolicy: 'force-physical' });
|
|
1718
|
+
* try {
|
|
1719
|
+
* const resp = await http.request({ url: 'https://ipinfo.io/ip' });
|
|
1720
|
+
* } finally {
|
|
1721
|
+
* await http.close();
|
|
1722
|
+
* }
|
|
1723
|
+
**/
|
|
1724
|
+
ttsignal.createHttpConnector = function (config) {
|
|
1725
|
+
ttRequireStack('__createHttpConnector__', 'HTTP 栈');
|
|
1726
|
+
return ttsignal.__createHttpConnector__(
|
|
1727
|
+
ttNormalizeStackConfig(config, 'createHttpConnector'));
|
|
1728
|
+
};
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
* 创建 WebSocket 连接器。**复用单例**,一个连接器可以开多条连接。
|
|
1732
|
+
*
|
|
1733
|
+
* @method createWsConnector
|
|
1734
|
+
* @public
|
|
1735
|
+
* @param config {Object=} 配置。除 createHttpConnector 那几项(vpnPolicy /
|
|
1736
|
+
* logLevel / logFile / log_callback / caCerts / spkiPin / insecureSkipVerify /
|
|
1737
|
+
* drainTimeoutMs / dnsServers / dnsTimeoutMs)之外还有:
|
|
1738
|
+
* connectTimeoutMs / pingIntervalMs / idleTimeoutMs / maxFrameBytes /
|
|
1739
|
+
* clientCertFile / clientKeyFile / clientKeyPassword。
|
|
1740
|
+
* 这些键在 createConnection(config) 里可以逐条覆盖。
|
|
1741
|
+
* @return {ttsignal.WsConnector}
|
|
1742
|
+
* @example
|
|
1743
|
+
* const ws = ttsignal.createWsConnector({ vpnPolicy: 'force-physical' });
|
|
1744
|
+
* const conn = ws.createConnection();
|
|
1745
|
+
* conn.on('data', (buf) => handle(buf));
|
|
1746
|
+
* await conn.connect('wss://sfu.example.com/signal', 5000);
|
|
1747
|
+
**/
|
|
1748
|
+
ttsignal.createWsConnector = function (config) {
|
|
1749
|
+
ttRequireStack('__createWsConnector__', 'WebSocket 栈');
|
|
1750
|
+
return ttsignal.__createWsConnector__(
|
|
1751
|
+
ttNormalizeStackConfig(config, 'createWsConnector'));
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
|
|
1755
|
+
//*******************************************************************************
|
|
1756
|
+
// Exports :
|
|
1757
|
+
//
|
|
1758
|
+
// 历史形态是 `exports.ttsignal = ttsignal`,于是 `require('ttsignal')` 拿到的是一层
|
|
1759
|
+
// 包装对象 —— `require('ttsignal').createConnector` 是 undefined。仓库里 js/client.js
|
|
1760
|
+
// / js/server.js / js/getLogFile.js / js/upload-livekit-bin.js 等示例脚本正是那么写
|
|
1761
|
+
// 的,一直跑不起来(既有问题,不是本次引入的)。这里把**模块本身**导出,并保留一个
|
|
1762
|
+
// 不可枚举的 `.ttsignal` 自引用,两种写法都成立:
|
|
1763
|
+
//
|
|
1764
|
+
// const tts = require('ttsignal'); // tts.createConnector / createHttpConnector
|
|
1765
|
+
// const { ttsignal } = require('ttsignal'); // 旧写法(js/pathChange.js),仍然可用
|
|
1766
|
+
//
|
|
1767
|
+
// 这是一次**放宽**,不是替换:旧调用方读到的 `.ttsignal` 还是同一个对象。
|
|
1768
|
+
// rtc-client 的 src/signaling/ttsignal-module.ts 做的正是 `wrapped.ttsignal ?? wrapped`,
|
|
1769
|
+
// 两种形态都吃,所以对它没有影响。自引用设为不可枚举,避免 console.dir / 遍历导出对象
|
|
1770
|
+
// 时多出一层环。
|
|
1052
1771
|
//*******************************************************************************
|
|
1053
1772
|
|
|
1054
|
-
exports
|
|
1773
|
+
module.exports = ttsignal;
|
|
1774
|
+
Object.defineProperty(module.exports, 'ttsignal', {
|
|
1775
|
+
value: ttsignal, enumerable: false, writable: true, configurable: true
|
|
1776
|
+
});
|
|
1055
1777
|
|
|
1056
1778
|
//*******************************************************************************
|
|
1057
1779
|
// End of file : ttsignal.js
|