@seamnet/client 0.13.0 → 0.13.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/lib/plugins/im/index.cjs +140 -80
- package/package.json +1 -1
package/lib/plugins/im/index.cjs
CHANGED
|
@@ -136,17 +136,35 @@ function createImPlugin() {
|
|
|
136
136
|
|
|
137
137
|
// ===== Supervisor 状态(SDK 僵尸时自动重建 chat 实例)=====
|
|
138
138
|
let credentialsRef = null;
|
|
139
|
-
|
|
140
|
-
let
|
|
139
|
+
// 状态机:scheduled(待启动)/ rebuilding(进行中);期间又来信号标记 requestedAgain
|
|
140
|
+
let rebuildScheduled = false;
|
|
141
|
+
let rebuilding = false;
|
|
142
|
+
let rebuildRequestedAgain = false;
|
|
143
|
+
let lastRebuildSuccessAt = 0;
|
|
144
|
+
let chatGeneration = 0; // chat 实例代数,每次 create 递增;listener 校验代数,老代事件忽略
|
|
141
145
|
let keepaliveTimer = null;
|
|
142
146
|
let processRejectionHandler = null;
|
|
143
147
|
const REBUILD_COOLDOWN_MS = 10_000;
|
|
144
148
|
const REBUILD_DEBOUNCE_MS = 3_000;
|
|
149
|
+
const REBUILD_WATCHDOG_MS = 30_000; // 整个重建最多 30s,超时强制失败
|
|
150
|
+
const SDK_CALL_TIMEOUT_MS = 10_000; // 单个 SDK 调用超时
|
|
151
|
+
const LOGOUT_TIMEOUT_MS = 5_000;
|
|
152
|
+
const LOGIN_TIMEOUT_MS = 15_000;
|
|
145
153
|
const KEEPALIVE_INTERVAL_MS = 3 * 60 * 1000;
|
|
146
154
|
const KEEPALIVE_TIMEOUT_MS = 10_000;
|
|
147
155
|
// SDK 错误 code 需要触发 rebuild(2801=PING 超时,WebSocket 应用层僵尸)
|
|
148
156
|
const SDK_REBUILD_ERROR_CODES = new Set([2801]);
|
|
149
157
|
|
|
158
|
+
function withTimeout(promise, ms, name) {
|
|
159
|
+
let tid = null;
|
|
160
|
+
const timeoutP = new Promise((_, reject) => {
|
|
161
|
+
tid = setTimeout(() => reject(new Error(`${name} timeout ${ms}ms`)), ms);
|
|
162
|
+
});
|
|
163
|
+
return Promise.race([promise, timeoutP]).finally(() => {
|
|
164
|
+
if (tid) clearTimeout(tid);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
150
168
|
async function prefetchDisplayName(userId) {
|
|
151
169
|
if (!chat || !userId || displayNameCache.has(userId)) return;
|
|
152
170
|
displayNameCache.set(userId, null);
|
|
@@ -339,50 +357,62 @@ function createImPlugin() {
|
|
|
339
357
|
return { ok: true };
|
|
340
358
|
}
|
|
341
359
|
|
|
342
|
-
// ===== 事件 handler
|
|
343
|
-
|
|
344
|
-
function onSdkReady() {
|
|
345
|
-
sdkReady = true;
|
|
346
|
-
log.info('sdk_ready', { userId: myUserId });
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function onSdkNotReady() {
|
|
350
|
-
sdkReady = false;
|
|
351
|
-
log.warn('sdk_not_ready');
|
|
352
|
-
}
|
|
360
|
+
// ===== 事件 handler 工厂(每次 setupAllListeners 时绑定当前 generation)=====
|
|
361
|
+
// handler 校验 generation,老实例晚到的事件直接忽略,避免污染新状态
|
|
353
362
|
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
363
|
+
function makeHandlers(gen) {
|
|
364
|
+
return {
|
|
365
|
+
onSdkReady: () => {
|
|
366
|
+
if (gen !== chatGeneration) return;
|
|
367
|
+
sdkReady = true;
|
|
368
|
+
log.info('sdk_ready', { userId: myUserId, gen });
|
|
369
|
+
},
|
|
370
|
+
onSdkNotReady: () => {
|
|
371
|
+
if (gen !== chatGeneration) return;
|
|
372
|
+
sdkReady = false;
|
|
373
|
+
log.warn('sdk_not_ready', { gen });
|
|
374
|
+
},
|
|
375
|
+
onSdkError: (event) => {
|
|
376
|
+
if (gen !== chatGeneration) return;
|
|
377
|
+
const err = event?.data || event;
|
|
378
|
+
const code = err?.code;
|
|
379
|
+
log.warn('sdk_error', {
|
|
380
|
+
code,
|
|
381
|
+
message: err?.message || String(err).slice(0, 200),
|
|
382
|
+
gen,
|
|
383
|
+
});
|
|
384
|
+
if (SDK_REBUILD_ERROR_CODES.has(code)) {
|
|
385
|
+
scheduleRebuild(`sdk_error:${code}`);
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
onKickedOut: (event) => {
|
|
389
|
+
if (gen !== chatGeneration) return;
|
|
390
|
+
log.warn('sdk_kicked_out', {
|
|
391
|
+
type: event?.data?.type,
|
|
392
|
+
message: event?.data?.message,
|
|
393
|
+
gen,
|
|
394
|
+
});
|
|
395
|
+
sdkReady = false;
|
|
396
|
+
scheduleRebuild('kicked_out');
|
|
397
|
+
},
|
|
398
|
+
onNetStateChange: (event) => {
|
|
399
|
+
if (gen !== chatGeneration) return;
|
|
400
|
+
log.info('sdk_net_state', { state: event?.data?.state, gen });
|
|
401
|
+
},
|
|
402
|
+
onFriendListUpdated: () => {
|
|
403
|
+
if (gen !== chatGeneration) return;
|
|
404
|
+
refreshContactsFromFriendList().catch((err) => {
|
|
405
|
+
log.warn('refresh_contacts_on_update_failed', { message: err.message });
|
|
406
|
+
});
|
|
407
|
+
},
|
|
408
|
+
onMessageReceived: (event) => {
|
|
409
|
+
if (gen !== chatGeneration) return;
|
|
410
|
+
handleMessageEvent(event);
|
|
411
|
+
},
|
|
412
|
+
};
|
|
383
413
|
}
|
|
384
414
|
|
|
385
|
-
function
|
|
415
|
+
function handleMessageEvent(event) {
|
|
386
416
|
for (const msg of event.data) {
|
|
387
417
|
if (msg.from === myUserId) continue;
|
|
388
418
|
|
|
@@ -500,14 +530,15 @@ function createImPlugin() {
|
|
|
500
530
|
}
|
|
501
531
|
|
|
502
532
|
function setupAllListeners() {
|
|
503
|
-
|
|
504
|
-
chat.on(TencentCloudChat.EVENT.
|
|
505
|
-
chat.on(TencentCloudChat.EVENT.
|
|
506
|
-
chat.on(TencentCloudChat.EVENT.
|
|
507
|
-
chat.on(TencentCloudChat.EVENT.
|
|
508
|
-
chat.on(TencentCloudChat.EVENT.
|
|
533
|
+
const h = makeHandlers(chatGeneration);
|
|
534
|
+
chat.on(TencentCloudChat.EVENT.SDK_READY, h.onSdkReady);
|
|
535
|
+
chat.on(TencentCloudChat.EVENT.SDK_NOT_READY, h.onSdkNotReady);
|
|
536
|
+
chat.on(TencentCloudChat.EVENT.ERROR, h.onSdkError);
|
|
537
|
+
chat.on(TencentCloudChat.EVENT.KICKED_OUT, h.onKickedOut);
|
|
538
|
+
chat.on(TencentCloudChat.EVENT.NET_STATE_CHANGE, h.onNetStateChange);
|
|
539
|
+
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, h.onMessageReceived);
|
|
509
540
|
try {
|
|
510
|
-
chat.on(TencentCloudChat.EVENT.FRIEND_LIST_UPDATED, onFriendListUpdated);
|
|
541
|
+
chat.on(TencentCloudChat.EVENT.FRIEND_LIST_UPDATED, h.onFriendListUpdated);
|
|
511
542
|
} catch (err) {
|
|
512
543
|
log.warn('friend_list_listener_register_failed', { message: err.message });
|
|
513
544
|
}
|
|
@@ -516,7 +547,11 @@ function createImPlugin() {
|
|
|
516
547
|
// ===== 重建 chat 实例(SDK 僵尸时用)=====
|
|
517
548
|
|
|
518
549
|
async function createAndLoginChat() {
|
|
550
|
+
// 每次 create 递增 generation,让老实例的 handler 自动失效
|
|
551
|
+
chatGeneration += 1;
|
|
552
|
+
// 清理 globals 让 setupSdkGlobals 能重新设置 window 等
|
|
519
553
|
delete global.window;
|
|
554
|
+
global._imSdkGlobalsSet = false;
|
|
520
555
|
chat = TencentCloudChat.create({ SDKAppID: Number(credentialsRef.sdkAppId) });
|
|
521
556
|
setupSdkGlobals();
|
|
522
557
|
try {
|
|
@@ -525,11 +560,12 @@ function createImPlugin() {
|
|
|
525
560
|
} catch {}
|
|
526
561
|
chat.setLogLevel(4);
|
|
527
562
|
setupAllListeners();
|
|
528
|
-
await
|
|
529
|
-
userID: myUserId,
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
563
|
+
await withTimeout(
|
|
564
|
+
chat.login({ userID: myUserId, userSig: credentialsRef.userSig }),
|
|
565
|
+
LOGIN_TIMEOUT_MS,
|
|
566
|
+
'chat.login'
|
|
567
|
+
);
|
|
568
|
+
// 等 SDK_READY(最多 10s)——sdkReady 可能已被 handler 置 true
|
|
533
569
|
await new Promise((resolve) => {
|
|
534
570
|
if (sdkReady) return resolve();
|
|
535
571
|
const timer = setTimeout(() => resolve(), 10000);
|
|
@@ -539,48 +575,71 @@ function createImPlugin() {
|
|
|
539
575
|
}
|
|
540
576
|
|
|
541
577
|
async function rebuildChat(reason) {
|
|
542
|
-
log.warn('chat_rebuild_start', { reason });
|
|
578
|
+
log.warn('chat_rebuild_start', { reason, prevGen: chatGeneration });
|
|
543
579
|
sdkReady = false;
|
|
544
|
-
|
|
545
|
-
|
|
580
|
+
// 每步都 timeout:老 chat 僵尸可能让 logout 永不 resolve
|
|
581
|
+
const oldChat = chat;
|
|
582
|
+
try {
|
|
583
|
+
await withTimeout(oldChat.logout().catch(() => {}), LOGOUT_TIMEOUT_MS, 'logout');
|
|
584
|
+
} catch (e) {
|
|
585
|
+
log.warn('logout_timeout', { message: e.message });
|
|
586
|
+
}
|
|
587
|
+
try { oldChat.destroy && oldChat.destroy(); } catch {}
|
|
588
|
+
// createAndLoginChat 自身会递增 chatGeneration,老 listener 自动失效
|
|
589
|
+
await createAndLoginChat();
|
|
546
590
|
try {
|
|
547
|
-
await
|
|
548
|
-
try { await refreshContactsFromFriendList(); } catch {}
|
|
549
|
-
log.info('chat_rebuild_done', { reason });
|
|
591
|
+
await withTimeout(refreshContactsFromFriendList(), SDK_CALL_TIMEOUT_MS, 'refresh_contacts');
|
|
550
592
|
} catch (e) {
|
|
551
|
-
log.
|
|
593
|
+
log.warn('refresh_contacts_after_rebuild_timeout', { message: e.message });
|
|
594
|
+
}
|
|
595
|
+
log.info('chat_rebuild_done', { reason, newGen: chatGeneration });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function doRebuild(reason) {
|
|
599
|
+
rebuildScheduled = false;
|
|
600
|
+
rebuilding = true;
|
|
601
|
+
try {
|
|
602
|
+
await withTimeout(rebuildChat(reason), REBUILD_WATCHDOG_MS, 'rebuild_watchdog');
|
|
603
|
+
lastRebuildSuccessAt = Date.now();
|
|
604
|
+
} catch (e) {
|
|
605
|
+
log.error('chat_rebuild_failed', e, { reason, message: e.message });
|
|
606
|
+
// 注意:lastRebuildSuccessAt 不更新,允许立即重试(不被 cooldown 拦)
|
|
607
|
+
} finally {
|
|
608
|
+
rebuilding = false;
|
|
609
|
+
if (rebuildRequestedAgain) {
|
|
610
|
+
rebuildRequestedAgain = false;
|
|
611
|
+
scheduleRebuild('trailing');
|
|
612
|
+
}
|
|
552
613
|
}
|
|
553
614
|
}
|
|
554
615
|
|
|
555
616
|
function scheduleRebuild(reason) {
|
|
556
|
-
|
|
617
|
+
// 已在重建中:标记 trailing,结束后再跑一轮
|
|
618
|
+
if (rebuilding) {
|
|
619
|
+
rebuildRequestedAgain = true;
|
|
620
|
+
log.info('chat_rebuild_queued_trailing', { reason });
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
// 已在等待启动:合并(debounce)
|
|
624
|
+
if (rebuildScheduled) return;
|
|
625
|
+
// Cooldown 只针对上次"成功"的重建——失败后允许立即重试
|
|
557
626
|
const now = Date.now();
|
|
558
|
-
if (now -
|
|
627
|
+
if (now - lastRebuildSuccessAt < REBUILD_COOLDOWN_MS) {
|
|
559
628
|
log.info('chat_rebuild_skipped_cooldown', { reason });
|
|
560
629
|
return;
|
|
561
630
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
lastRebuildAt = Date.now();
|
|
567
|
-
} finally {
|
|
568
|
-
rebuildInProgress = false;
|
|
569
|
-
}
|
|
570
|
-
}, REBUILD_DEBOUNCE_MS);
|
|
631
|
+
// 立即降级:阻止发送路径继续用坏 chat
|
|
632
|
+
sdkReady = false;
|
|
633
|
+
rebuildScheduled = true;
|
|
634
|
+
setTimeout(() => { doRebuild(reason).catch(() => {}); }, REBUILD_DEBOUNCE_MS);
|
|
571
635
|
}
|
|
572
636
|
|
|
573
637
|
function startKeepalive() {
|
|
574
638
|
if (keepaliveTimer) return;
|
|
575
639
|
keepaliveTimer = setInterval(async () => {
|
|
576
|
-
if (!sdkReady ||
|
|
640
|
+
if (!sdkReady || rebuilding || rebuildScheduled) return;
|
|
577
641
|
try {
|
|
578
|
-
await
|
|
579
|
-
chat.getMyProfile(),
|
|
580
|
-
new Promise((_, reject) =>
|
|
581
|
-
setTimeout(() => reject(new Error('keepalive timeout')), KEEPALIVE_TIMEOUT_MS)
|
|
582
|
-
),
|
|
583
|
-
]);
|
|
642
|
+
await withTimeout(chat.getMyProfile(), KEEPALIVE_TIMEOUT_MS, 'keepalive');
|
|
584
643
|
} catch (e) {
|
|
585
644
|
log.warn('keepalive_failed', { message: e.message });
|
|
586
645
|
scheduleRebuild(`keepalive:${e.message}`);
|
|
@@ -612,6 +671,7 @@ function createImPlugin() {
|
|
|
612
671
|
// 先 delete window 让 SDK 走 node 路径
|
|
613
672
|
delete global.window;
|
|
614
673
|
TencentCloudChat = require('@tencentcloud/chat');
|
|
674
|
+
chatGeneration += 1; // 首次也是 gen 1(listener 校验需要)
|
|
615
675
|
chat = TencentCloudChat.create({ SDKAppID: Number(credentials.sdkAppId) });
|
|
616
676
|
|
|
617
677
|
// 然后补上 SDK 运行所需的 window/document 等全局
|