@ntdgy/dsh-remote-bridge-plugin 0.0.0-stage → 0.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/cordis.patch.yml +28 -0
- package/lib/client.js +336 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +106 -0
- package/lib/index.js +535 -0
- package/lib/index.js.map +1 -0
- package/lib/local/sessions.d.ts +52 -0
- package/lib/local/sessions.js +25 -0
- package/lib/local/sessions.js.map +1 -0
- package/lib/relay/client.d.ts +262 -0
- package/lib/relay/client.js +825 -0
- package/lib/relay/client.js.map +1 -0
- package/lib/relay/dispatcher.d.ts +74 -0
- package/lib/relay/dispatcher.js +192 -0
- package/lib/relay/dispatcher.js.map +1 -0
- package/lib/relay/errors.d.ts +37 -0
- package/lib/relay/errors.js +71 -0
- package/lib/relay/errors.js.map +1 -0
- package/lib/relay/follow.d.ts +176 -0
- package/lib/relay/follow.js +307 -0
- package/lib/relay/follow.js.map +1 -0
- package/lib/relay/http-forward.d.ts +30 -0
- package/lib/relay/http-forward.js +183 -0
- package/lib/relay/http-forward.js.map +1 -0
- package/lib/relay/route-observer.d.ts +66 -0
- package/lib/relay/route-observer.js +122 -0
- package/lib/relay/route-observer.js.map +1 -0
- package/lib/relay/ws-forward.d.ts +28 -0
- package/lib/relay/ws-forward.js +138 -0
- package/lib/relay/ws-forward.js.map +1 -0
- package/lib/types/client/StatusPanel.d.ts +36 -0
- package/lib/types/client/index.d.ts +27 -0
- package/lib/types/client/types.d.ts +40 -0
- package/package.json +68 -4
- package/README.md +0 -3
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { type HttpRequestFrame, type WsOpenFrame, type RpcErrorCode, type RpcFrame, type RpcFrameEnd, type RpcRequest } from '@ntdgy/dsh-remote-proto';
|
|
2
|
+
import type { UpstreamResult } from './http-forward.ts';
|
|
3
|
+
/**
|
|
4
|
+
* 校验 relayUrl 的 scheme。
|
|
5
|
+
*
|
|
6
|
+
* 返回两种**不同性质**的结论,调用方据此采取不同动作 —— 这是刻意分开的:
|
|
7
|
+
* · fatalReason 配置根本不可用(非 ws/wss、URL 畸形)→ 重连没有意义
|
|
8
|
+
* · insecureReason 可用但不安全(ws://)→ 照常连接,只提醒一次
|
|
9
|
+
*
|
|
10
|
+
* 混在一起的后果实测过:ws:// 每次重连都打一条同样的 stderr(8 条刷屏),
|
|
11
|
+
* 而且把「本机联调」这种合法配置当成错误。日志噪音会让人对真正的告警麻木。
|
|
12
|
+
*
|
|
13
|
+
* 为什么单独成函数而不是内联:这是纯函数,可以直接单测,
|
|
14
|
+
* 而"配了个 http:// 的地址"这种事只会在部署现场出现一次,测不到就等于没有。
|
|
15
|
+
*/
|
|
16
|
+
export interface SchemeVerdict {
|
|
17
|
+
/** 不可用:调用方应放弃本次连接尝试。 */
|
|
18
|
+
readonly fatalReason?: string;
|
|
19
|
+
/** 可用但不安全:调用方应连接,并(只)提醒一次。 */
|
|
20
|
+
readonly insecureReason?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function checkRelayUrlScheme(url: string): SchemeVerdict;
|
|
23
|
+
export interface RelayClientOptions {
|
|
24
|
+
url: string;
|
|
25
|
+
instanceId: string;
|
|
26
|
+
instanceKey: string;
|
|
27
|
+
reconnectBaseMs: number;
|
|
28
|
+
reconnectMaxMs: number;
|
|
29
|
+
/** 首次心跳间隔;网关 hello 回执携带权威值,收到后以网关为准。 */
|
|
30
|
+
heartbeatIntervalMs: number;
|
|
31
|
+
onRequest: (req: RpcRequest) => Promise<unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* 订阅控制面(网关 → 插件的退订指令)。
|
|
34
|
+
*
|
|
35
|
+
* 这是**唯一**允许由网关发起、作用于插件侧订阅的消息;它不是业务 RPC,
|
|
36
|
+
* 所以不走 onRequest,也不产生 RpcResponse(见 proto 的 wire-sub-control 提案)。
|
|
37
|
+
*/
|
|
38
|
+
onSubscriptionControl: (subscriptionId: string, reason: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* ADR-005:HTTP 转发。返回的 body 以二进制帧紧随控制帧回传。
|
|
41
|
+
* 缺省时收到 http-request 会回 upstream_unreachable,而不是静默丢弃。
|
|
42
|
+
*/
|
|
43
|
+
onHttpRequest?: (frame: HttpRequestFrame, body?: Uint8Array) => Promise<UpstreamResult>;
|
|
44
|
+
/** ADR-005 T34:网关要求发起 WS 升级。 */
|
|
45
|
+
onWsOpen?: (frame: WsOpenFrame) => void;
|
|
46
|
+
/** 网关流量控制:暂停/恢复从上游读取。 */
|
|
47
|
+
onWsFlow?: (id: string, action: 'pause' | 'resume') => void;
|
|
48
|
+
/** 网关要求关闭上游 WS。 */
|
|
49
|
+
onWsClose?: (id: string, code: number, reason: string) => void;
|
|
50
|
+
/** 网关转来的 WS 数据帧(写入上游)。 */
|
|
51
|
+
onWsData?: (id: string, opcode: number, payload: Uint8Array) => void;
|
|
52
|
+
/**
|
|
53
|
+
* 取本实例在 DSH webServer 上实际注册的路径前缀(ADR-005 运行时自声明)。
|
|
54
|
+
*
|
|
55
|
+
* 每次 hello 都会调用一次,实现方应该现读路由表而不是缓存 ——
|
|
56
|
+
* 插件可能在两次重连之间注册了新路由。
|
|
57
|
+
*/
|
|
58
|
+
collectHttpPrefixes?: () => string[] | Promise<string[]>;
|
|
59
|
+
/**
|
|
60
|
+
* 上报本实例的已认证应用 URL(含 DSH 的 launch token)。
|
|
61
|
+
*
|
|
62
|
+
* 由 DSH 的 `connection.authenticatedUrl()` 生成;网关用它代浏览器完成
|
|
63
|
+
* DSH 登录,用户从此不必手输 DSH token。
|
|
64
|
+
*
|
|
65
|
+
* 每次 hello 都现取:**DSH 重启后 token 会变**,缓存会让网关拿到过期值。
|
|
66
|
+
* 取不到就返回 undefined —— 网关退化为原有行为,不做任何代持。
|
|
67
|
+
*/
|
|
68
|
+
collectLaunchUrl?: () => string | undefined;
|
|
69
|
+
onOpen: (connectionId: string) => void;
|
|
70
|
+
/** 带 code:网关主动踢线(如 4408 心跳超时)时可据 code 区分,不改变退避策略。 */
|
|
71
|
+
onClose: (reason: string, code: number) => void;
|
|
72
|
+
/** 协议告警(如收不到 hello 回执的裸帧),用于观测而不是静默。 */
|
|
73
|
+
onProtocolWarning?: (detail: string) => void;
|
|
74
|
+
/**
|
|
75
|
+
* 认证被网关拒绝。
|
|
76
|
+
*
|
|
77
|
+
* 与普通断连分开上报:密钥配错是**运维事故**,不是网络抖动,
|
|
78
|
+
* 混在"disconnected"里会让人一直去查网络。文案里绝不带 key。
|
|
79
|
+
*/
|
|
80
|
+
onAuthFailure?: (code: RpcErrorCode, message: string) => void;
|
|
81
|
+
/**
|
|
82
|
+
* 发送缓冲越过硬上限、本端主动断开连接时回调。
|
|
83
|
+
*
|
|
84
|
+
* 与普通断连分开上报:这是**本端主动止损**,不是对端掉线,
|
|
85
|
+
* 混在一起会让人去查网关的网络,而真正该查的是"网关为什么不读了"。
|
|
86
|
+
*/
|
|
87
|
+
onBackpressure?: (bufferedAmount: number) => void;
|
|
88
|
+
}
|
|
89
|
+
export declare class RelayClient {
|
|
90
|
+
private socket?;
|
|
91
|
+
private timer?;
|
|
92
|
+
private heartbeatTimer?;
|
|
93
|
+
/** WS ping/pong 存活探测的定时器。 */
|
|
94
|
+
private pingTimer?;
|
|
95
|
+
/** 上一轮 ping 是否尚未收到 pong。 */
|
|
96
|
+
private awaitingPong;
|
|
97
|
+
private delay;
|
|
98
|
+
private stopped;
|
|
99
|
+
private connectionId?;
|
|
100
|
+
/** 是否已收到 hello 回执。未确认前收到的业务请求一律拒绝。 */
|
|
101
|
+
private registered;
|
|
102
|
+
private heartbeatIntervalMs;
|
|
103
|
+
/** 被丢弃的反向帧计数,供调试读取(ADR-004 纪律 1 的可观测位)。 */
|
|
104
|
+
private droppedFrames;
|
|
105
|
+
/** 认证被拒次数。持续增长 = 密钥配错,而不是网络问题。 */
|
|
106
|
+
private authFailures;
|
|
107
|
+
/**
|
|
108
|
+
* 本次连接上 hello 的请求 id。
|
|
109
|
+
*
|
|
110
|
+
* 用来校验 hello-challenge 是不是**这一次**握手的挑战:断线重连时旧连接与新连接
|
|
111
|
+
* 可能短暂共存,迟到的挑战若不校验,会拿过期 nonce 去签名,
|
|
112
|
+
* 表现为"握手失败但看不出原因"。
|
|
113
|
+
*/
|
|
114
|
+
private pendingHelloId?;
|
|
115
|
+
/** 在途的入站 HTTP 请求数,与 MAX_INFLIGHT_HTTP_REQUESTS 对照。 */
|
|
116
|
+
private inflightHttpRequests;
|
|
117
|
+
private readonly opts;
|
|
118
|
+
constructor(opts: RelayClientOptions);
|
|
119
|
+
get connected(): boolean;
|
|
120
|
+
/** 已注册(收到 hello 回执)才算真正可用。 */
|
|
121
|
+
get ready(): boolean;
|
|
122
|
+
get stats(): {
|
|
123
|
+
droppedFrames: number;
|
|
124
|
+
heartbeatIntervalMs: number;
|
|
125
|
+
authFailures: number;
|
|
126
|
+
};
|
|
127
|
+
start(): void;
|
|
128
|
+
stop(): void;
|
|
129
|
+
private connect;
|
|
130
|
+
/**
|
|
131
|
+
* ADR-005 HTTP 转发:先回控制帧,再回二进制 body 帧。
|
|
132
|
+
*
|
|
133
|
+
* 顺序是协议约定(body 帧必须紧随其控制帧);这里用 void 并发调度,
|
|
134
|
+
* 多个请求互不阻塞。
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* 在途 HTTP 请求已满:回明确的错误帧,不排队。
|
|
138
|
+
*
|
|
139
|
+
* 复用既有的 http-response-error 通道而不是硬塞一个 503 响应:
|
|
140
|
+
* - HttpResponseFrame 没有 reason 字段,伪造一个能让网关解析失败;
|
|
141
|
+
* - 该通道的语义正是"请求根本没被完整处理",与本场景一致,前端能区分。
|
|
142
|
+
*
|
|
143
|
+
* 排队不是选项:排队只是把内存压力推迟到队列里,而且让请求方白等到超时,
|
|
144
|
+
* 错误更不可读。明确拒绝比慢慢耗尽内存负责。
|
|
145
|
+
*/
|
|
146
|
+
private rejectTooBusy;
|
|
147
|
+
private onHttpRequest;
|
|
148
|
+
/**
|
|
149
|
+
* 底层发送:JSON 走文本帧,Uint8Array 走二进制帧。带**发送水位**检查。
|
|
150
|
+
*
|
|
151
|
+
* 为什么需要:ws 的 send() 只是把数据塞进内核/userland 缓冲**立即返回**。
|
|
152
|
+
* 网关不读(挂住、限速、被暂停)时缓冲只增不减,本进程内存随之线性增长;
|
|
153
|
+
* 单个 HTTP 响应体上限是 32MB(HTTP_BODY_MAX_BYTES),几十个在途响应就足以吃光内存。
|
|
154
|
+
* 故障网关或恶意网关都能远程触发这件事——私网 DSH 不该被公网对端拖垮。
|
|
155
|
+
*
|
|
156
|
+
* 策略:**不丢帧**(丢帧会让上面的多路复用协议失去同步),而是当水位超过硬上限时
|
|
157
|
+
* 主动断开本条连接。断开是安全的收口:上层走既有退避重连,网关侧 dropInstance
|
|
158
|
+
* 会作废名下订阅,本端重建状态后重新开始。
|
|
159
|
+
*
|
|
160
|
+
* @returns 是否真的写出去了。调用方目前都不依赖返回值,保留以便将来分流。
|
|
161
|
+
*/
|
|
162
|
+
private sendRaw;
|
|
163
|
+
/** 发一条控制帧(JSON 文本帧)。ADR-005 T34 的 ws-open-ack / ws-close 走这里。 */
|
|
164
|
+
sendControl(frame: unknown): void;
|
|
165
|
+
/** 发一条二进制帧(WS 数据帧)。与 HTTP body 帧共用通道,靠 id 前缀区分。 */
|
|
166
|
+
sendBinary(payload: Uint8Array): void;
|
|
167
|
+
/**
|
|
168
|
+
* 入站分发。
|
|
169
|
+
*
|
|
170
|
+
* 修复点(原 client.ts:101):旧判定把**响应**当 hello 请求判,
|
|
171
|
+
* 而网关 hello 回执是 `{ id, ok: true, result }`,没有 method 字段,
|
|
172
|
+
* 因此 onOpen 永不触发、退避永不重置。现在用 proto 的唯一判别函数
|
|
173
|
+
* classifyWireMessage 区分顶层消息(request / response / frame / frame-end /
|
|
174
|
+
* sub-control / hello-challenge)。"挑战"因此是**类型级**判别,不靠约定。
|
|
175
|
+
*/
|
|
176
|
+
/** 待配对的 HTTP 请求体:id → {body, resolve}。 */
|
|
177
|
+
private readonly pendingBodies;
|
|
178
|
+
/**
|
|
179
|
+
* 等请求体二进制帧。超时返回 undefined,由调用方报明确错误而不是挂死。
|
|
180
|
+
*
|
|
181
|
+
* 用 Promise + 定时器而不是轮询:这是纯粹的「等一条消息」语义。
|
|
182
|
+
*/
|
|
183
|
+
private awaitRequestBody;
|
|
184
|
+
/** 二进制帧入口:区分 HTTP body 与 WS 数据帧(按 id 前缀 'h' / 'w')。 */
|
|
185
|
+
private onBinary;
|
|
186
|
+
private onMessage;
|
|
187
|
+
/**
|
|
188
|
+
* 订阅控制帧(网关 → 插件)。
|
|
189
|
+
*
|
|
190
|
+
* 控制面事务:不产生 RpcResponse,失败只告警。畸形消息不计入统计,
|
|
191
|
+
* 否则网关侧一个 bug 会让插件的协议告警计数失真。
|
|
192
|
+
*/
|
|
193
|
+
private onSubscriptionControl;
|
|
194
|
+
/**
|
|
195
|
+
* 网关的认证挑战(ADR-003 挑战应答第二跳)。
|
|
196
|
+
*
|
|
197
|
+
* 挑战是独立顶层控制帧 `kind:'hello-challenge'`(网关 → 插件,与 sub-control 同向),
|
|
198
|
+
* **不是**失败响应里附带的字段——"挑战"与"真拒"的区分是类型级的,
|
|
199
|
+
* 不靠"失败里有没有 nonce"这种约定。
|
|
200
|
+
*/
|
|
201
|
+
private onHelloChallenge;
|
|
202
|
+
/** 网关对本插件的响应(hello 回执 / 心跳回执 / 认证失败)。 */
|
|
203
|
+
private onResponse;
|
|
204
|
+
/** 浏览器 → 插件方向:只接受 RpcRequest(ADR-004 纪律 1)。 */
|
|
205
|
+
private onInboundRequest;
|
|
206
|
+
/**
|
|
207
|
+
* 主动心跳。
|
|
208
|
+
*
|
|
209
|
+
* 用 setTimeout 链而非 setInterval:发送本身不阻塞消息循环(ws.send 是
|
|
210
|
+
* 非阻塞入队),但排程必须与连接生命周期同生共死,避免 stop 后残留 tick。
|
|
211
|
+
* 心跳不等待回执(回执只是网关时间戳),因此不存在"心跳阻塞消息循环"。
|
|
212
|
+
*/
|
|
213
|
+
private scheduleHeartbeat;
|
|
214
|
+
private clearHeartbeat;
|
|
215
|
+
/**
|
|
216
|
+
* WS ping/pong 存活探测 —— **应用层心跳发现不了对端进程死亡**。
|
|
217
|
+
*
|
|
218
|
+
* 为什么必须用协议层 ping:relay 被 kill -9 后,ws.send() 仍会成功
|
|
219
|
+
* (写进内核发送缓冲),应用层 heartbeat 永远收不到回复也不报错,
|
|
220
|
+
* 连接半开挂起、重连永不触发。实测就是这么卡住的。
|
|
221
|
+
*
|
|
222
|
+
* 规则:每 pingIntervalMs 发一次 ping;若**上一轮 ping 尚未收到 pong**,
|
|
223
|
+
* 就判定连接已死 → terminate()(不是 close():半开连接调 close() 会等握手,
|
|
224
|
+
* 等不到就卡住),随后走既有退避重连。
|
|
225
|
+
*/
|
|
226
|
+
private startLivenessProbe;
|
|
227
|
+
private stopLivenessProbe;
|
|
228
|
+
/** 上一次**成功发送**的 launchUrl,用于变化时才重发(省流量)。 */
|
|
229
|
+
private lastSentLaunchUrl;
|
|
230
|
+
private sendHeartbeat;
|
|
231
|
+
/**
|
|
232
|
+
* 投递一条订阅帧(ADR-004)。
|
|
233
|
+
*
|
|
234
|
+
* 未就绪(socket 未开或 hello 未回执)时返回 false 而不抛:帧流是异步泵,
|
|
235
|
+
* 投递失败只会让浏览器少几帧,正确的是让订阅侧收到 frame-end 而不是炸掉泵循环。
|
|
236
|
+
*/
|
|
237
|
+
sendFrame(frame: RpcFrame): boolean;
|
|
238
|
+
/** 投递帧流终止信号。返回 false 的含义同 sendFrame。 */
|
|
239
|
+
sendFrameEnd(end: RpcFrameEnd): boolean;
|
|
240
|
+
private send;
|
|
241
|
+
/**
|
|
242
|
+
* 上报一条"配置层面不可恢复、但每次重连都应可见"的问题。
|
|
243
|
+
*
|
|
244
|
+
* 走 stderr 而不是 ctx.logger:配置错误通常发生在插件还没激活时,
|
|
245
|
+
* 这时 logger 可能不可用;而运维排查的第一现场是进程启动输出。
|
|
246
|
+
* 文案里绝不带 instanceKey。
|
|
247
|
+
*/
|
|
248
|
+
private reportFatalConfig;
|
|
249
|
+
/**
|
|
250
|
+
* 只提醒一次「链路不安全」。
|
|
251
|
+
*
|
|
252
|
+
* 为什么不能走 reportFatalConfig:那条路径每次重连都会执行,
|
|
253
|
+
* 实测 ws:// 配置重连 8 次就打 8 条一模一样的 stderr。
|
|
254
|
+
* 同一个已知情况反复刷屏,会让人对真正的告警麻木 —— 比不告警更糟。
|
|
255
|
+
*
|
|
256
|
+
* 用实例字段记状态而不是模块级标志:多实例(不同 relayUrl)各自独立判断。
|
|
257
|
+
*/
|
|
258
|
+
private warnInsecureSchemeOnce;
|
|
259
|
+
/** ws:// 告警是否已打过(只提醒一次,避免重连刷屏)。 */
|
|
260
|
+
private warnedInsecureScheme;
|
|
261
|
+
private scheduleReconnect;
|
|
262
|
+
}
|