dsh-mobile-plugin 0.1.93 → 0.1.94
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/README.md +1 -1
- package/lib/desktop-view.cjs +20 -0
- package/lib/impl.js +1 -1
- package/lib/mirror/pusher.cjs +40 -0
- package/lib/mobile/archive-clean.js +17 -18
- package/lib/mobile/events.js +28 -3
- package/lib/mobile/host-bridge/endpoints.js +0 -1
- package/lib/mobile/host-bridge/event-synthesis.js +33 -3
- package/lib/mobile/host-bridge/shims.js +25 -2
- package/lib/mobile/monitor.js +135 -0
- package/lib/mobile/session-flags.js +63 -0
- package/lib/mobile/session-snapshot.js +9 -17
- package/lib/mobile/sessions.js +3 -38
- package/lib/mobile/stream.js +69 -11
- package/lib/panel/index.html +1 -1
- package/lib/reconnect-scheduler.cjs +31 -4
- package/lib/relay/lan-proxy.js +10 -2
- package/lib/relay/protocol.js +3 -3
- package/lib/relay/tunnel.js +17 -4
- package/lib/routes/mobile-api.js +18 -1
- package/lib/session-cleanup.cjs +160 -27
- package/lib/shared/desktop-view-signal.js +66 -0
- package/lib/shared/http-util.js +50 -4
- package/lib/shared/sse-write.js +52 -2
- package/lib/sidecar-facade-worker.cjs +16 -2
- package/package.json +1 -1
package/lib/shared/sse-write.js
CHANGED
|
@@ -89,6 +89,48 @@ function stableEventId(payload, frame, namespace = 'session') {
|
|
|
89
89
|
return frame && typeof frame.rpcId === 'string' && frame.rpcId ? frame.rpcId : undefined;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 每条连接上「距离上次补齐注释」的状态:上次补齐的时刻、之后写出去的字节数。
|
|
94
|
+
*
|
|
95
|
+
* 键是响应对象,连接收掉后自然被回收,不需要显式清理。
|
|
96
|
+
*/
|
|
97
|
+
const PAD_STATES = new WeakMap();
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 决定这一条记录要不要补注释、补多少。
|
|
101
|
+
*
|
|
102
|
+
* 补注释的**唯一理由**是让安卓 RN 的 XHR 攒够字节尽快把进度事件吐给界面;补多少与补
|
|
103
|
+
* 多勤都不影响这条理由,只影响代价 —— 一条记录补整份 2048 字节时,一次几十秒的流式
|
|
104
|
+
* 回复(每秒几十条增量)光注释就要几 MB。所以改成按窗口补齐:
|
|
105
|
+
* - 距离上次补齐不到 `padEveryMs` 且累计字节还没越过 `padBytes` → 先不补,攒着;
|
|
106
|
+
* - 到了窗口边界或单条记录自己就够大 → 补到「自上次补齐以来累计 ≥ padBytes」为止。
|
|
107
|
+
* 客户端看到的仍然是「每 ≥padBytes 就有一条补过注释的记录」,界面的出字节奏不变。
|
|
108
|
+
*/
|
|
109
|
+
function takePadding(res, written, padBytes, padEveryMs) {
|
|
110
|
+
let state = PAD_STATES.get(res);
|
|
111
|
+
if (!state) {
|
|
112
|
+
state = { at: 0, bytes: 0 };
|
|
113
|
+
PAD_STATES.set(res, state);
|
|
114
|
+
}
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
const pending = state.bytes + written;
|
|
117
|
+
const due = padEveryMs > 0 ? now - state.at >= padEveryMs : true;
|
|
118
|
+
if (pending >= padBytes) {
|
|
119
|
+
// 这一发自己就越过了阈值,注释是纯浪费。
|
|
120
|
+
state.bytes = 0;
|
|
121
|
+
state.at = now;
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
if (!due) {
|
|
125
|
+
state.bytes = pending;
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
const padding = ssePadding(padBytes - pending);
|
|
129
|
+
state.bytes = 0;
|
|
130
|
+
state.at = now;
|
|
131
|
+
return padding;
|
|
132
|
+
}
|
|
133
|
+
|
|
92
134
|
/**
|
|
93
135
|
* 写一条 SSE 记录。
|
|
94
136
|
*
|
|
@@ -97,7 +139,9 @@ function stableEventId(payload, frame, namespace = 'session') {
|
|
|
97
139
|
* @param {object} [options]
|
|
98
140
|
* @param {object} [options.frame] 宿主 mux 帧,只用于算稳定 id
|
|
99
141
|
* @param {string} [options.namespace='session'] 兜底 id 的前缀
|
|
100
|
-
* @param {string} [options.padding='']
|
|
142
|
+
* @param {string} [options.padding=''] 固定的 SSE 注释行(调用方自己算好时用,见 ssePadding)
|
|
143
|
+
* @param {number} [options.paddingBytes=0] 让这条流维持的最小出字节数:>0 时按窗口补齐(见 takePadding)
|
|
144
|
+
* @param {number} [options.padEveryMs=0] 补齐窗口;0 表示每条记录都补到 paddingBytes
|
|
101
145
|
* @param {number} [options.retryMs=SSE_RETRY_MS] SSE 重连间隔
|
|
102
146
|
* @param {(payload: object) => boolean} [options.shedWhenBuffered] 超过软水位时这一帧能不能丢
|
|
103
147
|
* @returns {{ok: boolean, shed?: boolean, needDrain?: boolean}}
|
|
@@ -110,6 +154,8 @@ function writeSse(res, payload, options = {}) {
|
|
|
110
154
|
frame = null,
|
|
111
155
|
namespace = 'session',
|
|
112
156
|
padding = '',
|
|
157
|
+
paddingBytes = 0,
|
|
158
|
+
padEveryMs = 0,
|
|
113
159
|
retryMs = SSE_RETRY_MS,
|
|
114
160
|
shedWhenBuffered = shedWithoutSeq,
|
|
115
161
|
} = options;
|
|
@@ -120,7 +166,11 @@ function writeSse(res, payload, options = {}) {
|
|
|
120
166
|
const fields = [`retry: ${retryMs}`];
|
|
121
167
|
if (id) fields.push(`id: ${id}`);
|
|
122
168
|
fields.push(`data: ${JSON.stringify(payload)}`);
|
|
123
|
-
|
|
169
|
+
const extra = padding || (paddingBytes > 0
|
|
170
|
+
// 按字节算:正文是中文时字符数只有字节数三分之一,按字符算会低估、补多。
|
|
171
|
+
? takePadding(res, Buffer.byteLength(`${fields.join('\n')}\n`, 'utf8'), Math.floor(paddingBytes), Math.floor(padEveryMs))
|
|
172
|
+
: '');
|
|
173
|
+
if (extra) fields.push(extra);
|
|
124
174
|
fields.push('', '');
|
|
125
175
|
try {
|
|
126
176
|
// `write()` 返回 false 才是背压的正信号:数据已经堆到高水位,继续写只会把内存
|
|
@@ -68,8 +68,18 @@ const NETWORK_POLL_MS = 5000
|
|
|
68
68
|
// 隧道重连间隔。onClose 与 start().catch 两条失败路径共用这个幂等调度器,
|
|
69
69
|
// 否则每次失败会排两个定时器(重连风暴);幂等性由
|
|
70
70
|
// test/reconnect-scheduler.test.cjs 精确验证。
|
|
71
|
+
//
|
|
72
|
+
// 抖动 2s:中转重启后所有宿主在同一刻掉线,固定间隔会让它们踩着同一个点回来。
|
|
73
|
+
// 退避到 30s:中转没起来时不该每 5 秒敲一次;注册成功由 onRegistered 里的 reset 清账。
|
|
71
74
|
const RECONNECT_DELAY_MS = 5000
|
|
72
|
-
const
|
|
75
|
+
const RECONNECT_JITTER_MS = 2000
|
|
76
|
+
const RECONNECT_MAX_DELAY_MS = 30_000
|
|
77
|
+
const reconnect = createReconnectScheduler({
|
|
78
|
+
delayMs: RECONNECT_DELAY_MS,
|
|
79
|
+
jitterMs: RECONNECT_JITTER_MS,
|
|
80
|
+
maxDelayMs: RECONNECT_MAX_DELAY_MS,
|
|
81
|
+
onFire: () => startTunnel(),
|
|
82
|
+
})
|
|
73
83
|
|
|
74
84
|
function send(res, code, value) {
|
|
75
85
|
if (res.headersSent) return res.end()
|
|
@@ -239,7 +249,11 @@ function startTunnel() {
|
|
|
239
249
|
// 是新实例、它的 ws 还停在 CONNECTING。于是 /status 报「已连接」、宿主据此放行镜像
|
|
240
250
|
// 推送,worker 却按真值回 503,镜像一条也发不出去(现场:host.log 里
|
|
241
251
|
// 「bg queue: sidecar facade request failed: 503」刷屏,而隧道其实在正常转发会话)。
|
|
242
|
-
onRegistered: () => {
|
|
252
|
+
onRegistered: () => {
|
|
253
|
+
// 注册成功 = 链路真的通了:连续失败账清零,下次掉线重新从 5s 起算。
|
|
254
|
+
if (tunnel !== current) return; reconnect.reset()
|
|
255
|
+
tunnelState = 'connected'; tunnelError = null; publishState()
|
|
256
|
+
},
|
|
243
257
|
onClose: () => { if (tunnel === current && config.enabled) { tunnelState = 'error'; tunnelError = '连接断开'; publishState(); scheduleReconnect() } },
|
|
244
258
|
onPairRequest: async (message) => {
|
|
245
259
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobile-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.94",
|
|
4
4
|
"description": "Mobile API (/api/mobile/v1) and relay client for the DSH mobile app — desktop profile build (0.2.0-rc.2 compatible, peers resolved by the host)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|