@jhh96/light-app-bridge 1.1.0 → 1.2.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/README.md +21 -0
- package/dist/index.d.ts +1 -1
- package/dist/lightAppBridge.d.ts +86 -3
- package/dist/lightAppBridge.js +299 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,27 @@ bridge.onMessage((data, id) => {
|
|
|
54
54
|
})
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## 动态绑定与 ping(1.1.0+,仅 plugin 角色生效)
|
|
58
|
+
|
|
59
|
+
默认 `bindMode: 'fixed'`(首绑后固定对端,失联自动向原对端重连)。
|
|
60
|
+
多轻应用窗口场景用 `dynamic`:插件轮询 `getContentContext()` 感知用户切换窗口,向新窗口探测握手——成功即切换绑定(`onRebind`),失败(新窗口未集成 SDK)保持旧绑定。
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const bridge = new LightAppBridge('plugin', {
|
|
64
|
+
bindMode: 'dynamic', // 'fixed'(默认)| 'dynamic'
|
|
65
|
+
dynamicPollInterval: 2000, // 轮询当前轻应用间隔(ms)
|
|
66
|
+
migrateProbeTimeout: 3000, // 新窗口 hello 探测窗口(ms)
|
|
67
|
+
pingInterval: 10000, // 存活探测周期(ms),0=关闭
|
|
68
|
+
pingTimeout: 5000, // 单次 pong 等待(ms)
|
|
69
|
+
pingFailLimit: 3 // 连续失败阈值,达阈值后 hello/ack 兜底探测
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
bridge.onRebind((newPeerId, oldPeerId) => { /* 动态绑定已切换 */ })
|
|
73
|
+
bridge.onDisconnect((peerId) => { /* 对端失联(ping 与兜底探测均无响应) */ })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
ping 兼容性:对端 ≤1.0.6 不会回 pong,但握手逻辑仍在——连续 ping 失败后自动发 hello 兜底探测,收到 ack 即判定存活,不会误断;新旧版本可混布。
|
|
77
|
+
|
|
57
78
|
## 消息信封
|
|
58
79
|
|
|
59
80
|
```js
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { default } from './lightAppBridge';
|
|
2
2
|
export { createBridge } from './lightAppBridge';
|
|
3
|
-
export type { BridgeRole, EnvelopeType, Envelope, BridgeOptions, SendOptions, MessageHandler } from './lightAppBridge';
|
|
3
|
+
export type { BridgeRole, BindMode, EnvelopeType, Envelope, BridgeOptions, SendOptions, MessageHandler } from './lightAppBridge';
|
package/dist/lightAppBridge.d.ts
CHANGED
|
@@ -23,8 +23,15 @@
|
|
|
23
23
|
*/
|
|
24
24
|
/** 角色:plugin=插件窗口(发起方),lightapp=轻应用窗口(接收方) */
|
|
25
25
|
export type BridgeRole = 'plugin' | 'lightapp';
|
|
26
|
+
/**
|
|
27
|
+
* 绑定模式(仅 plugin 角色生效):
|
|
28
|
+
* - fixed 默认。首次握手绑定后固定对端,失联后向原对端重连。
|
|
29
|
+
* - dynamic 跟随当前激活的轻应用窗口:轮询 getContentContext 感知切换,
|
|
30
|
+
* 向新窗口探测握手(成功即切换绑定;失败——如未集成 SDK——保持旧绑定)。
|
|
31
|
+
*/
|
|
32
|
+
export type BindMode = 'fixed' | 'dynamic';
|
|
26
33
|
/** 信封类型 */
|
|
27
|
-
export type EnvelopeType = 'hello' | 'ack' | 'req' | 'res';
|
|
34
|
+
export type EnvelopeType = 'hello' | 'ack' | 'req' | 'res' | 'ping' | 'pong';
|
|
28
35
|
/** 消息信封:所有跨窗口通信的统一包装 */
|
|
29
36
|
export interface Envelope {
|
|
30
37
|
__sdk: string;
|
|
@@ -39,13 +46,33 @@ export interface BridgeOptions {
|
|
|
39
46
|
timeout?: number;
|
|
40
47
|
/** 握手超时(ms),默认 30000。仅 plugin 角色生效;lightapp 端持续等待,不超时 */
|
|
41
48
|
handshakeTimeout?: number;
|
|
49
|
+
/**
|
|
50
|
+
* 绑定模式(仅 plugin 生效),默认 'fixed'。
|
|
51
|
+
* dynamic:轮询 getContentContext 跟随当前激活的轻应用窗口切换绑定。
|
|
52
|
+
*/
|
|
53
|
+
bindMode?: BindMode;
|
|
54
|
+
/** dynamic 模式轮询 getContentContext 的间隔(ms),默认 2000 */
|
|
55
|
+
dynamicPollInterval?: number;
|
|
56
|
+
/** dynamic 模式迁移探测窗口:向新窗口发 hello 等 ack 的时长(ms),默认 3000 */
|
|
57
|
+
migrateProbeTimeout?: number;
|
|
58
|
+
/**
|
|
59
|
+
* ping 周期(ms),默认 10000;设 0 关闭 ping。
|
|
60
|
+
* plugin 角色在绑定成功后周期 ping 对端(lightapp 自动回 pong),
|
|
61
|
+
* 连续 pingTimeout 无 pong 且 hello/ack 兜底探测也失败时判定失联并触发
|
|
62
|
+
* onDisconnect。
|
|
63
|
+
*/
|
|
64
|
+
pingInterval?: number;
|
|
65
|
+
/** 单次 pong 等待(ms),默认 5000 */
|
|
66
|
+
pingTimeout?: number;
|
|
67
|
+
/** 连续 ping 失败阈值(次),默认 3。达到后进入 hello/ack 兜底探测 */
|
|
68
|
+
pingFailLimit?: number;
|
|
42
69
|
/**
|
|
43
70
|
* 调试模式:开启后在控制台打印完整通信轨迹,便于排查 plugin ↔ lightapp
|
|
44
71
|
* 联调问题。覆盖:
|
|
45
72
|
* - 构造(角色/配置)、握手(hello/ack/连接建立/超时/重连)
|
|
46
73
|
* - 发送请求(send data + id)、底层投递(目标 webContentsId + 信封)
|
|
47
74
|
* - 收到消息(type/id/from/data)、响应返回(res 数据/超时)
|
|
48
|
-
* - handler 异常、销毁
|
|
75
|
+
* - ping/pong、动态绑定迁移、handler 异常、销毁
|
|
49
76
|
* 大 payload 截断到 500 字符防刷屏。
|
|
50
77
|
*/
|
|
51
78
|
debug?: boolean;
|
|
@@ -66,6 +93,12 @@ export default class LightAppBridge {
|
|
|
66
93
|
role: BridgeRole;
|
|
67
94
|
timeout: number;
|
|
68
95
|
handshakeTimeout: number;
|
|
96
|
+
bindMode: BindMode;
|
|
97
|
+
dynamicPollInterval: number;
|
|
98
|
+
migrateProbeTimeout: number;
|
|
99
|
+
pingInterval: number;
|
|
100
|
+
pingTimeout: number;
|
|
101
|
+
pingFailLimit: number;
|
|
69
102
|
private _pending;
|
|
70
103
|
private _messageHandlers;
|
|
71
104
|
private _target;
|
|
@@ -84,6 +117,20 @@ export default class LightAppBridge {
|
|
|
84
117
|
private _destroyed;
|
|
85
118
|
private _debug;
|
|
86
119
|
private _helloCount;
|
|
120
|
+
private _dynamicTimer;
|
|
121
|
+
private _migrating;
|
|
122
|
+
private _migrateTargetId;
|
|
123
|
+
private _migrateTimer;
|
|
124
|
+
private _migrateRetryTimer;
|
|
125
|
+
private _rebindCbs;
|
|
126
|
+
private _pingTimer;
|
|
127
|
+
private _pingWaiting;
|
|
128
|
+
private _pingFails;
|
|
129
|
+
private _pingResolve;
|
|
130
|
+
private _probeActive;
|
|
131
|
+
private _probeOk;
|
|
132
|
+
private _probeTimers;
|
|
133
|
+
private _disconnectCbs;
|
|
87
134
|
/**
|
|
88
135
|
* @param role 角色,省略时按 window.worklink.plugin 是否存在自动探测
|
|
89
136
|
* @param options 超时配置
|
|
@@ -127,6 +174,42 @@ export default class LightAppBridge {
|
|
|
127
174
|
private _clearHelloTimer;
|
|
128
175
|
/** 监听连接建立(握手完成)。若已连接则立即触发 */
|
|
129
176
|
onConnect(cb: (peerId: number | null) => void): this;
|
|
177
|
+
/** 连接建立后启动目标轮询(幂等;非 dynamic / 非 plugin 不启动) */
|
|
178
|
+
private _startDynamicPoll;
|
|
179
|
+
private _stopDynamicPoll;
|
|
180
|
+
/**
|
|
181
|
+
* 解析当前激活的轻应用 webContentsId;变化则发起迁移探测。
|
|
182
|
+
* 失联恢复(onDisconnect 后)也复用此入口强制解析。
|
|
183
|
+
*/
|
|
184
|
+
private _pollTarget;
|
|
185
|
+
/**
|
|
186
|
+
* 向新目标发 hello 探测(HELLO_RETRY_INTERVAL 重试)。
|
|
187
|
+
* - 探测窗口内收到 ack(_onAck → _rebindTo):切换绑定
|
|
188
|
+
* - 超时(新窗口未集成 SDK):放弃,保持旧绑定,等下一轮轮询再探
|
|
189
|
+
*/
|
|
190
|
+
private _startMigrate;
|
|
191
|
+
private _endMigrate;
|
|
192
|
+
/** 迁移成功:切换绑定目标并通知(连接语义连续,ready() 不受影响) */
|
|
193
|
+
private _rebindTo;
|
|
194
|
+
/** 监听动态绑定切换(bindMode='dynamic' 时目标窗口变化并成功迁移后触发一次) */
|
|
195
|
+
onRebind(cb: (newPeerId: number | null, oldPeerId: number | null) => void): this;
|
|
196
|
+
/** 绑定成功后启动周期 ping(幂等;pingInterval=0 或非 plugin 不启动) */
|
|
197
|
+
private _startPing;
|
|
198
|
+
private _stopPing;
|
|
199
|
+
/** 发一次 ping:等 pong → 失败计数 → 达阈值后 hello/ack 兜底探测 → 仍失败判定失联 */
|
|
200
|
+
private _pingOnce;
|
|
201
|
+
/** 收到 pong:唤醒在途 ping */
|
|
202
|
+
private _onPong;
|
|
203
|
+
/**
|
|
204
|
+
* ping 连续失败后的兜底探测:向对端发 hello 等 ack。
|
|
205
|
+
* 旧版对端(无 pong 能力)握手逻辑仍在会回 ack —— 收到即判定存活,避免误杀。
|
|
206
|
+
* ack 也无响应才判定真失联。
|
|
207
|
+
*/
|
|
208
|
+
private _probeAlive;
|
|
209
|
+
/** 判定失联:触发 onDisconnect,并按绑定模式恢复 */
|
|
210
|
+
private _handleDisconnect;
|
|
211
|
+
/** 监听对端失联(ping 与 hello 兜底探测均无响应时触发一次) */
|
|
212
|
+
onDisconnect(cb: (peerId: number | null) => void): this;
|
|
130
213
|
/**
|
|
131
214
|
* 等待连接建立(握手完成)。构造时已自动发起握手,直接 await 即可。
|
|
132
215
|
* @returns 对端 webContentsId
|
|
@@ -156,7 +239,7 @@ export default class LightAppBridge {
|
|
|
156
239
|
* @returns 调用以取消监听
|
|
157
240
|
*/
|
|
158
241
|
onMessage(handler: MessageHandler): () => void;
|
|
159
|
-
/**
|
|
242
|
+
/** 销毁实例:解绑消息监听、清空待处理请求、停止握手/轮询/迁移/ping。销毁后实例不可再用 */
|
|
160
243
|
destroy(): void;
|
|
161
244
|
}
|
|
162
245
|
/**
|
package/dist/lightAppBridge.js
CHANGED
|
@@ -27,6 +27,12 @@ const DEFAULT_TIMEOUT = 3000; // send 默认超时(ms)
|
|
|
27
27
|
const DEFAULT_HANDSHAKE_TIMEOUT = 30000; // 握手超时(ms)
|
|
28
28
|
const HELLO_RETRY_INTERVAL = 1000; // 插件 hello 重试间隔(ms)
|
|
29
29
|
const DEBUG_PAYLOAD_LIMIT = 500; // debug 日志 payload 截断长度
|
|
30
|
+
const DEFAULT_BIND_MODE = 'fixed'; // 绑定模式默认值
|
|
31
|
+
const DEFAULT_DYNAMIC_POLL_INTERVAL = 2000; // dynamic 轮询间隔(ms)
|
|
32
|
+
const DEFAULT_MIGRATE_PROBE_TIMEOUT = 3000; // 迁移探测窗口(ms)
|
|
33
|
+
const DEFAULT_PING_INTERVAL = 10000; // ping 周期(ms)
|
|
34
|
+
const DEFAULT_PING_TIMEOUT = 5000; // 单次 pong 等待(ms)
|
|
35
|
+
const DEFAULT_PING_FAIL_LIMIT = 3; // 连续 ping 失败阈值(次)
|
|
30
36
|
const genId = () => 'r_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 7);
|
|
31
37
|
/** debug 日志用:安全序列化 payload,超长截断 */
|
|
32
38
|
function debugPayload(data) {
|
|
@@ -180,6 +186,22 @@ export default class LightAppBridge {
|
|
|
180
186
|
this._connectFailed = false;
|
|
181
187
|
this._destroyed = false;
|
|
182
188
|
this._helloCount = 0;
|
|
189
|
+
// —— 动态绑定(plugin + bindMode='dynamic')——
|
|
190
|
+
this._dynamicTimer = null;
|
|
191
|
+
this._migrating = false;
|
|
192
|
+
this._migrateTargetId = null;
|
|
193
|
+
this._migrateTimer = null;
|
|
194
|
+
this._migrateRetryTimer = null;
|
|
195
|
+
this._rebindCbs = [];
|
|
196
|
+
// —— ping/pong 存活探测(plugin 主动)——
|
|
197
|
+
this._pingTimer = null;
|
|
198
|
+
this._pingWaiting = false; // 一次 ping 在途(等 pong)
|
|
199
|
+
this._pingFails = 0; // 连续 pong 超时计数
|
|
200
|
+
this._pingResolve = null; // 当前在途 ping 的完成回调
|
|
201
|
+
this._probeActive = false; // hello/ack 兜底探测进行中
|
|
202
|
+
this._probeOk = false; // 兜底探测是否收到 ack
|
|
203
|
+
this._probeTimers = []; // 防 destroy 后悬挂
|
|
204
|
+
this._disconnectCbs = [];
|
|
183
205
|
let r = role === 'plugin' || role === 'lightapp' ? role : 'lightapp';
|
|
184
206
|
if (role !== 'plugin' && role !== 'lightapp') {
|
|
185
207
|
const wl = window.worklink;
|
|
@@ -188,9 +210,18 @@ export default class LightAppBridge {
|
|
|
188
210
|
this.role = r;
|
|
189
211
|
this.timeout = options.timeout != null ? options.timeout : DEFAULT_TIMEOUT;
|
|
190
212
|
this.handshakeTimeout = options.handshakeTimeout != null ? options.handshakeTimeout : DEFAULT_HANDSHAKE_TIMEOUT;
|
|
213
|
+
this.bindMode = options.bindMode === 'dynamic' ? 'dynamic' : DEFAULT_BIND_MODE;
|
|
214
|
+
this.dynamicPollInterval = options.dynamicPollInterval != null ? options.dynamicPollInterval : DEFAULT_DYNAMIC_POLL_INTERVAL;
|
|
215
|
+
this.migrateProbeTimeout = options.migrateProbeTimeout != null ? options.migrateProbeTimeout : DEFAULT_MIGRATE_PROBE_TIMEOUT;
|
|
216
|
+
this.pingInterval = options.pingInterval != null ? options.pingInterval : DEFAULT_PING_INTERVAL;
|
|
217
|
+
this.pingTimeout = options.pingTimeout != null ? options.pingTimeout : DEFAULT_PING_TIMEOUT;
|
|
218
|
+
this.pingFailLimit = options.pingFailLimit != null ? options.pingFailLimit : DEFAULT_PING_FAIL_LIMIT;
|
|
191
219
|
this._debug = !!options.debug;
|
|
192
220
|
this._log('构造: role=' + r + ', timeout=' + this.timeout +
|
|
193
221
|
', handshakeTimeout=' + this.handshakeTimeout +
|
|
222
|
+
', bindMode=' + this.bindMode +
|
|
223
|
+
(this.bindMode === 'dynamic' ? '(poll=' + this.dynamicPollInterval + 'ms)' : '') +
|
|
224
|
+
', ping=' + (this.pingInterval > 0 ? this.pingInterval + 'ms' : 'off') +
|
|
194
225
|
', 监听后端=' + (hasWaltz() ? 'waltz' : 'handleMessageFromNative') +
|
|
195
226
|
', 自身id=' + this.getId());
|
|
196
227
|
// 连接 deferred:构造时即建立,保证轻应用收到 hello 也能 resolve(无需显式 connect)
|
|
@@ -264,6 +295,13 @@ export default class LightAppBridge {
|
|
|
264
295
|
case 'res':
|
|
265
296
|
this._serveRes(envelope);
|
|
266
297
|
break;
|
|
298
|
+
case 'ping':
|
|
299
|
+
// 存活探测:立即回 pong(data 原样带回),无论何种角色/模式都应答
|
|
300
|
+
this._send(fromId, this._envelope('pong', envelope.data, envelope.id));
|
|
301
|
+
break;
|
|
302
|
+
case 'pong':
|
|
303
|
+
this._onPong(envelope);
|
|
304
|
+
break;
|
|
267
305
|
}
|
|
268
306
|
}
|
|
269
307
|
// —— 连接 / 握手 —— //
|
|
@@ -307,7 +345,19 @@ export default class LightAppBridge {
|
|
|
307
345
|
if (this._connected)
|
|
308
346
|
return;
|
|
309
347
|
if (this.role === 'plugin') {
|
|
310
|
-
//
|
|
348
|
+
// 重连场景(失联后恢复):已有原目标时不再重新解析 getContentContext——
|
|
349
|
+
// 失联期间用户可能已切换轻应用窗口,重新解析会让 fixed 模式"漂移"到新窗口,
|
|
350
|
+
// 违背固定对端语义。固定模式坚持向原对端重连(对端彻底没了则超时失败)。
|
|
351
|
+
// dynamic 模式的窗口跟随走独立的迁移状态机(_pollTarget),不经此路径。
|
|
352
|
+
if (this._target != null) {
|
|
353
|
+
this._sendHello();
|
|
354
|
+
this._helloTimer = setInterval(() => {
|
|
355
|
+
if (!this._connected)
|
|
356
|
+
this._sendHello();
|
|
357
|
+
}, HELLO_RETRY_INTERVAL);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// 首次连接:解析目标后发 hello,并重试直到收到 ack
|
|
311
361
|
return window.worklink.plugin.getContentContext()
|
|
312
362
|
.then((ctx) => {
|
|
313
363
|
if (!ctx || ctx.webContentsId == null) {
|
|
@@ -371,6 +421,17 @@ export default class LightAppBridge {
|
|
|
371
421
|
_onAck(fromId) {
|
|
372
422
|
if (fromId == null)
|
|
373
423
|
return; // 无法定位对端则忽略
|
|
424
|
+
// ping 失败后的 hello/ack 兜底探测:收到 ack 即对端存活(旧版对端不会回 pong
|
|
425
|
+
// 但握手逻辑仍在),标记探测成功
|
|
426
|
+
if (this._probeActive) {
|
|
427
|
+
this._probeOk = true;
|
|
428
|
+
this._log('✓ ping兜底探测: 对端存活(ack) from=' + fromId);
|
|
429
|
+
}
|
|
430
|
+
// 动态绑定迁移中:仅当 ack 来自迁移目标才完成切换
|
|
431
|
+
if (this._migrating && fromId === this._migrateTargetId) {
|
|
432
|
+
this._rebindTo(fromId);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
374
435
|
this._recordPeer(fromId);
|
|
375
436
|
this._connectDeferred.resolve(fromId);
|
|
376
437
|
}
|
|
@@ -386,6 +447,9 @@ export default class LightAppBridge {
|
|
|
386
447
|
clearTimeout(this._handshakeTimer);
|
|
387
448
|
this._handshakeTimer = null;
|
|
388
449
|
}
|
|
450
|
+
// 绑定成功后启动存活探测与(dynamic 模式)目标轮询
|
|
451
|
+
this._startPing();
|
|
452
|
+
this._startDynamicPoll();
|
|
389
453
|
// 触发 onConnect 回调
|
|
390
454
|
const cbs = this._connectCbs.splice(0);
|
|
391
455
|
cbs.forEach((cb) => {
|
|
@@ -409,6 +473,230 @@ export default class LightAppBridge {
|
|
|
409
473
|
this._connectCbs.push(cb);
|
|
410
474
|
return this;
|
|
411
475
|
}
|
|
476
|
+
// —— 动态绑定(bindMode='dynamic',仅 plugin)—— //
|
|
477
|
+
/** 连接建立后启动目标轮询(幂等;非 dynamic / 非 plugin 不启动) */
|
|
478
|
+
_startDynamicPoll() {
|
|
479
|
+
if (this.role !== 'plugin' || this.bindMode !== 'dynamic')
|
|
480
|
+
return;
|
|
481
|
+
if (this._dynamicTimer || this._destroyed)
|
|
482
|
+
return;
|
|
483
|
+
this._log('dynamic: 启动目标轮询 (interval=' + this.dynamicPollInterval + 'ms)');
|
|
484
|
+
this._dynamicTimer = setInterval(() => {
|
|
485
|
+
this._pollTarget().catch(() => { });
|
|
486
|
+
}, this.dynamicPollInterval);
|
|
487
|
+
}
|
|
488
|
+
_stopDynamicPoll() {
|
|
489
|
+
if (this._dynamicTimer) {
|
|
490
|
+
clearInterval(this._dynamicTimer);
|
|
491
|
+
this._dynamicTimer = null;
|
|
492
|
+
}
|
|
493
|
+
this._endMigrate();
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* 解析当前激活的轻应用 webContentsId;变化则发起迁移探测。
|
|
497
|
+
* 失联恢复(onDisconnect 后)也复用此入口强制解析。
|
|
498
|
+
*/
|
|
499
|
+
async _pollTarget() {
|
|
500
|
+
if (this._destroyed || this.role !== 'plugin')
|
|
501
|
+
return;
|
|
502
|
+
const ctx = await window.worklink.plugin.getContentContext();
|
|
503
|
+
const ctxId = ctx && ctx.webContentsId != null ? ctx.webContentsId : null;
|
|
504
|
+
if (ctxId == null || ctxId === this._target)
|
|
505
|
+
return;
|
|
506
|
+
if (this._migrating)
|
|
507
|
+
return; // 上一轮迁移探测进行中,等它结束
|
|
508
|
+
this._startMigrate(ctxId);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* 向新目标发 hello 探测(HELLO_RETRY_INTERVAL 重试)。
|
|
512
|
+
* - 探测窗口内收到 ack(_onAck → _rebindTo):切换绑定
|
|
513
|
+
* - 超时(新窗口未集成 SDK):放弃,保持旧绑定,等下一轮轮询再探
|
|
514
|
+
*/
|
|
515
|
+
_startMigrate(newTargetId) {
|
|
516
|
+
const oldId = this._peer;
|
|
517
|
+
this._migrating = true;
|
|
518
|
+
this._migrateTargetId = newTargetId;
|
|
519
|
+
this._log('dynamic: 目标变化 ' + oldId + ' → ' + newTargetId + ',迁移探测开始');
|
|
520
|
+
const probe = () => {
|
|
521
|
+
if (!this._migrating)
|
|
522
|
+
return;
|
|
523
|
+
this._send(newTargetId, this._envelope('hello', { from: this.getId() }));
|
|
524
|
+
};
|
|
525
|
+
probe();
|
|
526
|
+
this._migrateRetryTimer = setInterval(probe, HELLO_RETRY_INTERVAL);
|
|
527
|
+
this._migrateTimer = setTimeout(() => {
|
|
528
|
+
if (this._migrating) {
|
|
529
|
+
this._log('dynamic: 迁移探测超时(' + this.migrateProbeTimeout + 'ms),目标 ' +
|
|
530
|
+
newTargetId + ' 未集成 SDK,保持原绑定 ' + oldId);
|
|
531
|
+
this._endMigrate();
|
|
532
|
+
}
|
|
533
|
+
}, this.migrateProbeTimeout);
|
|
534
|
+
}
|
|
535
|
+
_endMigrate() {
|
|
536
|
+
this._migrating = false;
|
|
537
|
+
this._migrateTargetId = null;
|
|
538
|
+
if (this._migrateTimer) {
|
|
539
|
+
clearTimeout(this._migrateTimer);
|
|
540
|
+
this._migrateTimer = null;
|
|
541
|
+
}
|
|
542
|
+
if (this._migrateRetryTimer) {
|
|
543
|
+
clearInterval(this._migrateRetryTimer);
|
|
544
|
+
this._migrateRetryTimer = null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/** 迁移成功:切换绑定目标并通知(连接语义连续,ready() 不受影响) */
|
|
548
|
+
_rebindTo(newPeerId) {
|
|
549
|
+
const oldId = this._peer;
|
|
550
|
+
this._endMigrate();
|
|
551
|
+
this._target = newPeerId;
|
|
552
|
+
this._peer = newPeerId;
|
|
553
|
+
// 失联恢复场景(onDisconnect 后迁移成功):恢复连接态并重启 ping;
|
|
554
|
+
// 常规迁移(本来就连接着)时这些操作幂等。
|
|
555
|
+
this._connected = true;
|
|
556
|
+
this._startPing();
|
|
557
|
+
this._startDynamicPoll();
|
|
558
|
+
this._log('✓ dynamic: 重新绑定 ' + oldId + ' → ' + newPeerId);
|
|
559
|
+
// ping 计数清零并立即探测新对端(新窗口刚握手成功,顺带验证 pong 能力)
|
|
560
|
+
this._pingFails = 0;
|
|
561
|
+
this._pingOnce();
|
|
562
|
+
const cbs = this._rebindCbs.splice(0);
|
|
563
|
+
cbs.forEach((cb) => {
|
|
564
|
+
try {
|
|
565
|
+
cb(newPeerId, oldId);
|
|
566
|
+
}
|
|
567
|
+
catch { /* ignore */ }
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/** 监听动态绑定切换(bindMode='dynamic' 时目标窗口变化并成功迁移后触发一次) */
|
|
571
|
+
onRebind(cb) {
|
|
572
|
+
this._rebindCbs.push(cb);
|
|
573
|
+
return this;
|
|
574
|
+
}
|
|
575
|
+
// —— ping/pong 存活探测(plugin 主动发,lightapp 自动回 pong)—— //
|
|
576
|
+
/** 绑定成功后启动周期 ping(幂等;pingInterval=0 或非 plugin 不启动) */
|
|
577
|
+
_startPing() {
|
|
578
|
+
if (this.role !== 'plugin' || this.pingInterval <= 0)
|
|
579
|
+
return;
|
|
580
|
+
if (this._pingTimer || this._destroyed)
|
|
581
|
+
return;
|
|
582
|
+
this._pingTimer = setInterval(() => this._pingOnce(), this.pingInterval);
|
|
583
|
+
}
|
|
584
|
+
_stopPing() {
|
|
585
|
+
if (this._pingTimer) {
|
|
586
|
+
clearInterval(this._pingTimer);
|
|
587
|
+
this._pingTimer = null;
|
|
588
|
+
}
|
|
589
|
+
this._pingWaiting = false;
|
|
590
|
+
this._pingFails = 0;
|
|
591
|
+
}
|
|
592
|
+
/** 发一次 ping:等 pong → 失败计数 → 达阈值后 hello/ack 兜底探测 → 仍失败判定失联 */
|
|
593
|
+
_pingOnce() {
|
|
594
|
+
if (this._destroyed || !this._connected || this._pingWaiting)
|
|
595
|
+
return;
|
|
596
|
+
const id = genId();
|
|
597
|
+
this._pingWaiting = true;
|
|
598
|
+
let settled = false;
|
|
599
|
+
const finish = (ok) => {
|
|
600
|
+
if (settled)
|
|
601
|
+
return;
|
|
602
|
+
settled = true;
|
|
603
|
+
this._pingWaiting = false;
|
|
604
|
+
this._pingResolve = null;
|
|
605
|
+
if (ok) {
|
|
606
|
+
if (this._pingFails > 0)
|
|
607
|
+
this._log('✓ pong 恢复 (此前失败' + this._pingFails + '次)');
|
|
608
|
+
this._pingFails = 0;
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
this._pingFails++;
|
|
612
|
+
this._log('✗ pong 超时 ' + this._pingFails + '/' + this.pingFailLimit);
|
|
613
|
+
if (this._pingFails >= this.pingFailLimit) {
|
|
614
|
+
this._probeAlive();
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
// pong 到达回调(_onPong 触发)
|
|
618
|
+
this._pingResolve = () => finish(true);
|
|
619
|
+
setTimeout(() => finish(false), this.pingTimeout);
|
|
620
|
+
try {
|
|
621
|
+
this._send(this._peer, this._envelope('ping', { t: Date.now() }, id));
|
|
622
|
+
}
|
|
623
|
+
catch (e) {
|
|
624
|
+
// 底层投递异常(invoke 瞬时不可用 / peer 为空)按本次失败计,不冒泡进 interval
|
|
625
|
+
this._log('✗ ping 发送异常: ' + (e instanceof Error ? e.message : String(e)));
|
|
626
|
+
finish(false);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/** 收到 pong:唤醒在途 ping */
|
|
630
|
+
_onPong(envelope) {
|
|
631
|
+
if (this._pingResolve)
|
|
632
|
+
this._pingResolve();
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* ping 连续失败后的兜底探测:向对端发 hello 等 ack。
|
|
636
|
+
* 旧版对端(无 pong 能力)握手逻辑仍在会回 ack —— 收到即判定存活,避免误杀。
|
|
637
|
+
* ack 也无响应才判定真失联。
|
|
638
|
+
*/
|
|
639
|
+
_probeAlive() {
|
|
640
|
+
if (this._destroyed || !this._connected)
|
|
641
|
+
return;
|
|
642
|
+
this._log('ping 兜底探测: 发 hello → ' + this._peer);
|
|
643
|
+
this._probeActive = true;
|
|
644
|
+
this._probeOk = false;
|
|
645
|
+
// 真发一条 hello:旧版对端(无 pong 能力)的握手逻辑仍在,会回 ack 证明存活。
|
|
646
|
+
// 发送异常按探测失败处理(timer 到期自然判定),不冒泡。
|
|
647
|
+
try {
|
|
648
|
+
this._send(this._peer, this._envelope('hello', { from: this.getId() }));
|
|
649
|
+
}
|
|
650
|
+
catch { /* timer 到期后按无响应处理 */ }
|
|
651
|
+
const timer = setTimeout(() => {
|
|
652
|
+
const active = this._probeActive;
|
|
653
|
+
const ok = this._probeOk;
|
|
654
|
+
this._probeActive = false;
|
|
655
|
+
if (!active)
|
|
656
|
+
return;
|
|
657
|
+
if (ok) {
|
|
658
|
+
// 旧版对端存活(无 pong 能力):清计数继续常规 ping(下轮失败仍会兜底,无害)
|
|
659
|
+
this._log('✓ ping 兜底探测: 对端存活(旧版无pong)');
|
|
660
|
+
this._pingFails = 0;
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
this._handleDisconnect();
|
|
664
|
+
}
|
|
665
|
+
}, this.pingTimeout);
|
|
666
|
+
// 防 destroy 后悬挂 timer
|
|
667
|
+
this._probeTimers.push(timer);
|
|
668
|
+
}
|
|
669
|
+
/** 判定失联:触发 onDisconnect,并按绑定模式恢复 */
|
|
670
|
+
_handleDisconnect() {
|
|
671
|
+
const deadId = this._peer;
|
|
672
|
+
this._log('✗ 对端失联: ' + deadId + '(ping + hello 探测均无响应)');
|
|
673
|
+
this._connected = false;
|
|
674
|
+
this._stopPing();
|
|
675
|
+
const cbs = this._disconnectCbs.splice(0);
|
|
676
|
+
cbs.forEach((cb) => {
|
|
677
|
+
try {
|
|
678
|
+
cb(deadId);
|
|
679
|
+
}
|
|
680
|
+
catch { /* ignore */ }
|
|
681
|
+
});
|
|
682
|
+
if (this._destroyed)
|
|
683
|
+
return;
|
|
684
|
+
if (this.bindMode === 'dynamic') {
|
|
685
|
+
// dynamic:立即解析当前激活轻应用并尝试迁移(找不到可绑目标则等下轮轮询)
|
|
686
|
+
this._startDynamicPoll();
|
|
687
|
+
this._pollTarget().catch(() => { });
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
// fixed:自动向原对端重新握手(复用失败重连逻辑)
|
|
691
|
+
this._resetConnectState();
|
|
692
|
+
this.connect().catch(() => { });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/** 监听对端失联(ping 与 hello 兜底探测均无响应时触发一次) */
|
|
696
|
+
onDisconnect(cb) {
|
|
697
|
+
this._disconnectCbs.push(cb);
|
|
698
|
+
return this;
|
|
699
|
+
}
|
|
412
700
|
/**
|
|
413
701
|
* 等待连接建立(握手完成)。构造时已自动发起握手,直接 await 即可。
|
|
414
702
|
* @returns 对端 webContentsId
|
|
@@ -481,7 +769,10 @@ export default class LightAppBridge {
|
|
|
481
769
|
if (this._destroyed)
|
|
482
770
|
return Promise.reject(new Error('实例已销毁'));
|
|
483
771
|
const timeout = options.timeout != null ? options.timeout : this.timeout;
|
|
484
|
-
return this.ready().then((
|
|
772
|
+
return this.ready().then(() => {
|
|
773
|
+
// 发送目标取当前 _peer(而非 ready() resolve 的快照):
|
|
774
|
+
// dynamic 模式迁移重绑后,后续 send 立即发往新对端。
|
|
775
|
+
const targetId = this._peer;
|
|
485
776
|
const id = genId();
|
|
486
777
|
const envelope = this._envelope('req', data, id);
|
|
487
778
|
this._log('send→ id=' + id + ' 目标=' + targetId + ' data=' + debugPayload(data));
|
|
@@ -517,7 +808,7 @@ export default class LightAppBridge {
|
|
|
517
808
|
this._messageHandlers.splice(i, 1);
|
|
518
809
|
};
|
|
519
810
|
}
|
|
520
|
-
/**
|
|
811
|
+
/** 销毁实例:解绑消息监听、清空待处理请求、停止握手/轮询/迁移/ping。销毁后实例不可再用 */
|
|
521
812
|
destroy() {
|
|
522
813
|
if (this._destroyed)
|
|
523
814
|
return;
|
|
@@ -531,6 +822,11 @@ export default class LightAppBridge {
|
|
|
531
822
|
this._unlisten = null;
|
|
532
823
|
}
|
|
533
824
|
this._clearHelloTimer();
|
|
825
|
+
this._stopDynamicPoll();
|
|
826
|
+
this._stopPing();
|
|
827
|
+
this._probeTimers.forEach((t) => clearTimeout(t));
|
|
828
|
+
this._probeTimers.length = 0;
|
|
829
|
+
this._probeActive = false;
|
|
534
830
|
if (this._handshakeTimer) {
|
|
535
831
|
clearTimeout(this._handshakeTimer);
|
|
536
832
|
this._handshakeTimer = null;
|