@seamnet/client 0.13.1 → 0.13.2
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 +113 -81
- package/package.json +1 -1
package/lib/plugins/im/index.cjs
CHANGED
|
@@ -144,14 +144,20 @@ function createImPlugin() {
|
|
|
144
144
|
let chatGeneration = 0; // chat 实例代数,每次 create 递增;listener 校验代数,老代事件忽略
|
|
145
145
|
let keepaliveTimer = null;
|
|
146
146
|
let processRejectionHandler = null;
|
|
147
|
+
// 重建失败后自驱重试:指数退避 + jitter;成功后重置
|
|
148
|
+
let rebuildRetryAttempt = 0;
|
|
149
|
+
let rebuildRetryTimer = null;
|
|
147
150
|
const REBUILD_COOLDOWN_MS = 10_000;
|
|
148
151
|
const REBUILD_DEBOUNCE_MS = 3_000;
|
|
149
152
|
const REBUILD_WATCHDOG_MS = 30_000; // 整个重建最多 30s,超时强制失败
|
|
150
153
|
const SDK_CALL_TIMEOUT_MS = 10_000; // 单个 SDK 调用超时
|
|
151
154
|
const LOGOUT_TIMEOUT_MS = 5_000;
|
|
152
155
|
const LOGIN_TIMEOUT_MS = 15_000;
|
|
156
|
+
const SDK_READY_TIMEOUT_MS = 10_000; // 等 SDK_READY 最长 10s,超时 reject
|
|
153
157
|
const KEEPALIVE_INTERVAL_MS = 3 * 60 * 1000;
|
|
154
158
|
const KEEPALIVE_TIMEOUT_MS = 10_000;
|
|
159
|
+
// 重建失败退避表(最后一档持续)
|
|
160
|
+
const REBUILD_RETRY_BACKOFF_MS = [3_000, 10_000, 30_000, 60_000];
|
|
155
161
|
// SDK 错误 code 需要触发 rebuild(2801=PING 超时,WebSocket 应用层僵尸)
|
|
156
162
|
const SDK_REBUILD_ERROR_CODES = new Set([2801]);
|
|
157
163
|
|
|
@@ -357,14 +363,14 @@ function createImPlugin() {
|
|
|
357
363
|
return { ok: true };
|
|
358
364
|
}
|
|
359
365
|
|
|
360
|
-
// ===== 事件 handler 工厂(每次
|
|
366
|
+
// ===== 事件 handler 工厂(每次 attachListenersTo 时绑定当前 generation)=====
|
|
361
367
|
// handler 校验 generation,老实例晚到的事件直接忽略,避免污染新状态
|
|
362
368
|
|
|
363
369
|
function makeHandlers(gen) {
|
|
364
370
|
return {
|
|
365
371
|
onSdkReady: () => {
|
|
366
372
|
if (gen !== chatGeneration) return;
|
|
367
|
-
sdkReady
|
|
373
|
+
// sdkReady 的置 true 统一在 rebuildChat/init 成功 swap 后做,handler 只记录
|
|
368
374
|
log.info('sdk_ready', { userId: myUserId, gen });
|
|
369
375
|
},
|
|
370
376
|
onSdkNotReady: () => {
|
|
@@ -529,70 +535,110 @@ function createImPlugin() {
|
|
|
529
535
|
}
|
|
530
536
|
}
|
|
531
537
|
|
|
532
|
-
function
|
|
533
|
-
const h = makeHandlers(
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
538
|
+
function attachListenersTo(chatInstance, gen) {
|
|
539
|
+
const h = makeHandlers(gen);
|
|
540
|
+
chatInstance.on(TencentCloudChat.EVENT.SDK_READY, h.onSdkReady);
|
|
541
|
+
chatInstance.on(TencentCloudChat.EVENT.SDK_NOT_READY, h.onSdkNotReady);
|
|
542
|
+
chatInstance.on(TencentCloudChat.EVENT.ERROR, h.onSdkError);
|
|
543
|
+
chatInstance.on(TencentCloudChat.EVENT.KICKED_OUT, h.onKickedOut);
|
|
544
|
+
chatInstance.on(TencentCloudChat.EVENT.NET_STATE_CHANGE, h.onNetStateChange);
|
|
545
|
+
chatInstance.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, h.onMessageReceived);
|
|
540
546
|
try {
|
|
541
|
-
|
|
547
|
+
chatInstance.on(TencentCloudChat.EVENT.FRIEND_LIST_UPDATED, h.onFriendListUpdated);
|
|
542
548
|
} catch (err) {
|
|
543
549
|
log.warn('friend_list_listener_register_failed', { message: err.message });
|
|
544
550
|
}
|
|
545
551
|
}
|
|
546
552
|
|
|
547
|
-
|
|
553
|
+
function waitForSdkReadyStrict(chatInstance, ms) {
|
|
554
|
+
return new Promise((resolve, reject) => {
|
|
555
|
+
const timer = setTimeout(
|
|
556
|
+
() => reject(new Error(`SDK_READY timeout ${ms}ms`)),
|
|
557
|
+
ms
|
|
558
|
+
);
|
|
559
|
+
chatInstance.on(TencentCloudChat.EVENT.SDK_READY, () => {
|
|
560
|
+
clearTimeout(timer);
|
|
561
|
+
resolve();
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
}
|
|
548
565
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
//
|
|
566
|
+
// ===== 候选模式构建新 chat(失败则隔离销毁,不污染 active chat)=====
|
|
567
|
+
async function buildChatCandidate() {
|
|
568
|
+
const myGen = ++chatGeneration;
|
|
569
|
+
// 清 globals,让 setupSdkGlobals 重新铺 window 等
|
|
553
570
|
delete global.window;
|
|
554
571
|
global._imSdkGlobalsSet = false;
|
|
555
|
-
|
|
572
|
+
const nextChat = TencentCloudChat.create({ SDKAppID: Number(credentialsRef.sdkAppId) });
|
|
556
573
|
setupSdkGlobals();
|
|
557
574
|
try {
|
|
558
575
|
const TIMUploadPlugin = require('tim-upload-plugin');
|
|
559
|
-
|
|
576
|
+
nextChat.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin });
|
|
560
577
|
} catch {}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
578
|
+
nextChat.setLogLevel(4);
|
|
579
|
+
attachListenersTo(nextChat, myGen);
|
|
580
|
+
try {
|
|
581
|
+
await withTimeout(
|
|
582
|
+
nextChat.login({ userID: myUserId, userSig: credentialsRef.userSig }),
|
|
583
|
+
LOGIN_TIMEOUT_MS,
|
|
584
|
+
'chat.login'
|
|
585
|
+
);
|
|
586
|
+
// 严格等 SDK_READY:超时 reject,不再假成功
|
|
587
|
+
await waitForSdkReadyStrict(nextChat, SDK_READY_TIMEOUT_MS);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
// 失败的 candidate 必须隔离销毁,避免 late event 污染新状态
|
|
590
|
+
try {
|
|
591
|
+
await withTimeout(
|
|
592
|
+
(nextChat.logout && nextChat.logout().catch(() => {})) || Promise.resolve(),
|
|
593
|
+
LOGOUT_TIMEOUT_MS,
|
|
594
|
+
'cand_logout'
|
|
595
|
+
);
|
|
596
|
+
} catch {}
|
|
597
|
+
try { nextChat.destroy && nextChat.destroy(); } catch {}
|
|
598
|
+
throw err;
|
|
599
|
+
}
|
|
600
|
+
return { nextChat, gen: myGen };
|
|
575
601
|
}
|
|
576
602
|
|
|
603
|
+
// ===== 重建 chat 实例(SDK 僵尸时用)=====
|
|
577
604
|
async function rebuildChat(reason) {
|
|
578
605
|
log.warn('chat_rebuild_start', { reason, prevGen: chatGeneration });
|
|
579
606
|
sdkReady = false;
|
|
580
|
-
// 每步都 timeout:老 chat 僵尸可能让 logout 永不 resolve
|
|
581
607
|
const oldChat = chat;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
608
|
+
|
|
609
|
+
// 候选模式:先把新 chat 完整拉起来(login + SDK_READY),失败则 throw,老 chat 保持不变
|
|
610
|
+
const { nextChat, gen: newGen } = await buildChatCandidate();
|
|
611
|
+
|
|
612
|
+
// 成功:swap 到新 chat;sdkReady 只在 swap 完成后置 true
|
|
613
|
+
chat = nextChat;
|
|
614
|
+
sdkReady = true;
|
|
615
|
+
log.info('chat_rebuild_done', { reason, newGen });
|
|
616
|
+
|
|
617
|
+
// 老 chat 后台销毁,不阻塞新 chat 服务
|
|
618
|
+
(async () => {
|
|
619
|
+
try {
|
|
620
|
+
if (oldChat && oldChat.logout) {
|
|
621
|
+
await withTimeout(oldChat.logout().catch(() => {}), LOGOUT_TIMEOUT_MS, 'old_logout');
|
|
622
|
+
}
|
|
623
|
+
} catch (e) {
|
|
624
|
+
log.warn('old_logout_timeout', { message: e.message });
|
|
625
|
+
}
|
|
626
|
+
try { oldChat && oldChat.destroy && oldChat.destroy(); } catch {}
|
|
627
|
+
})();
|
|
628
|
+
|
|
590
629
|
try {
|
|
591
630
|
await withTimeout(refreshContactsFromFriendList(), SDK_CALL_TIMEOUT_MS, 'refresh_contacts');
|
|
592
631
|
} catch (e) {
|
|
593
632
|
log.warn('refresh_contacts_after_rebuild_timeout', { message: e.message });
|
|
594
633
|
}
|
|
595
|
-
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function scheduleRebuildRetry(reason, delayMs) {
|
|
637
|
+
if (rebuildRetryTimer) clearTimeout(rebuildRetryTimer);
|
|
638
|
+
rebuildRetryTimer = setTimeout(() => {
|
|
639
|
+
rebuildRetryTimer = null;
|
|
640
|
+
scheduleRebuild(reason);
|
|
641
|
+
}, delayMs);
|
|
596
642
|
}
|
|
597
643
|
|
|
598
644
|
async function doRebuild(reason) {
|
|
@@ -601,9 +647,18 @@ function createImPlugin() {
|
|
|
601
647
|
try {
|
|
602
648
|
await withTimeout(rebuildChat(reason), REBUILD_WATCHDOG_MS, 'rebuild_watchdog');
|
|
603
649
|
lastRebuildSuccessAt = Date.now();
|
|
650
|
+
rebuildRetryAttempt = 0;
|
|
651
|
+
if (rebuildRetryTimer) { clearTimeout(rebuildRetryTimer); rebuildRetryTimer = null; }
|
|
604
652
|
} catch (e) {
|
|
605
|
-
log.error('chat_rebuild_failed', e, { reason, message: e.message });
|
|
606
|
-
//
|
|
653
|
+
log.error('chat_rebuild_failed', e, { reason, message: e.message, attempt: rebuildRetryAttempt });
|
|
654
|
+
// 自驱重试:指数退避 + jitter;不依赖外部信号
|
|
655
|
+
const idx = Math.min(rebuildRetryAttempt, REBUILD_RETRY_BACKOFF_MS.length - 1);
|
|
656
|
+
const base = REBUILD_RETRY_BACKOFF_MS[idx];
|
|
657
|
+
const jitter = Math.floor(Math.random() * 1000);
|
|
658
|
+
const delay = base + jitter;
|
|
659
|
+
rebuildRetryAttempt += 1;
|
|
660
|
+
log.info('chat_rebuild_retry_scheduled', { attempt: rebuildRetryAttempt, delayMs: delay });
|
|
661
|
+
scheduleRebuildRetry(`auto_retry:${reason}`, delay);
|
|
607
662
|
} finally {
|
|
608
663
|
rebuilding = false;
|
|
609
664
|
if (rebuildRequestedAgain) {
|
|
@@ -668,55 +723,26 @@ function createImPlugin() {
|
|
|
668
723
|
myUserId = credentials.userId;
|
|
669
724
|
credentialsRef = credentials;
|
|
670
725
|
|
|
671
|
-
// 先 delete window 让 SDK 走 node 路径
|
|
672
|
-
delete global.window;
|
|
673
726
|
TencentCloudChat = require('@tencentcloud/chat');
|
|
674
|
-
chatGeneration += 1; // 首次也是 gen 1(listener 校验需要)
|
|
675
|
-
chat = TencentCloudChat.create({ SDKAppID: Number(credentials.sdkAppId) });
|
|
676
|
-
|
|
677
|
-
// 然后补上 SDK 运行所需的 window/document 等全局
|
|
678
|
-
setupSdkGlobals();
|
|
679
727
|
|
|
728
|
+
// 首次走 candidate 模式:严格 login + SDK_READY,失败直接 throw
|
|
680
729
|
try {
|
|
681
|
-
const
|
|
682
|
-
chat
|
|
730
|
+
const built = await buildChatCandidate();
|
|
731
|
+
chat = built.nextChat;
|
|
732
|
+
sdkReady = true;
|
|
733
|
+
log.info('login_ok', { userId: myUserId, gen: built.gen });
|
|
683
734
|
log.info('upload_plugin_registered');
|
|
684
|
-
} catch (e) {
|
|
685
|
-
log.warn('upload_plugin_unavailable', { message: e.message });
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
chat.setLogLevel(4);
|
|
689
|
-
|
|
690
|
-
// 所有事件监听通过 setupAllListeners 统一注册(便于 rebuild 时重注册)
|
|
691
|
-
setupAllListeners();
|
|
692
|
-
|
|
693
|
-
try {
|
|
694
|
-
await chat.login({ userID: myUserId, userSig: credentials.userSig });
|
|
695
|
-
log.info('login_ok', { userId: myUserId });
|
|
696
735
|
} catch (err) {
|
|
697
736
|
log.error('login_failed', err);
|
|
698
737
|
throw wrap(err, {
|
|
699
738
|
code: 'IM_LOGIN_FAILED',
|
|
700
|
-
hint: '检查 userSig 是否过期、sdkAppId
|
|
739
|
+
hint: '检查 userSig 是否过期、sdkAppId 是否正确、网络到 my-imcloud.com',
|
|
701
740
|
docs: 'docs/maintainer-guide.md#IM_LOGIN_FAILED',
|
|
702
741
|
});
|
|
703
742
|
}
|
|
704
743
|
|
|
705
|
-
// 等 SDK_READY(最多 10s)
|
|
706
|
-
await new Promise((resolve) => {
|
|
707
|
-
if (sdkReady) return resolve();
|
|
708
|
-
const timer = setTimeout(() => {
|
|
709
|
-
log.warn('sdk_ready_wait_timeout');
|
|
710
|
-
resolve();
|
|
711
|
-
}, 10000);
|
|
712
|
-
chat.on(TencentCloudChat.EVENT.SDK_READY, () => {
|
|
713
|
-
clearTimeout(timer);
|
|
714
|
-
resolve();
|
|
715
|
-
});
|
|
716
|
-
});
|
|
717
|
-
|
|
718
744
|
// 拉一次好友列表建立关系表 contacts.json
|
|
719
|
-
// FRIEND_LIST_UPDATED 监听已在
|
|
745
|
+
// FRIEND_LIST_UPDATED 监听已在 buildChatCandidate 里注册
|
|
720
746
|
seedDisplayNameFromContacts();
|
|
721
747
|
await refreshContactsFromFriendList();
|
|
722
748
|
|
|
@@ -790,17 +816,23 @@ function createImPlugin() {
|
|
|
790
816
|
clearInterval(keepaliveTimer);
|
|
791
817
|
keepaliveTimer = null;
|
|
792
818
|
}
|
|
819
|
+
if (rebuildRetryTimer) {
|
|
820
|
+
clearTimeout(rebuildRetryTimer);
|
|
821
|
+
rebuildRetryTimer = null;
|
|
822
|
+
}
|
|
793
823
|
if (processRejectionHandler) {
|
|
794
824
|
try { process.off('unhandledRejection', processRejectionHandler); } catch {}
|
|
795
825
|
processRejectionHandler = null;
|
|
796
826
|
}
|
|
797
827
|
if (chat) {
|
|
798
828
|
try {
|
|
799
|
-
await chat.logout();
|
|
829
|
+
await withTimeout(chat.logout().catch(() => {}), LOGOUT_TIMEOUT_MS, 'destroy_logout');
|
|
800
830
|
if (log) log.info('logged_out');
|
|
801
831
|
} catch (e) {
|
|
802
832
|
if (log) log.warn('logout_failed', { message: e.message });
|
|
803
833
|
}
|
|
834
|
+
try { chat.destroy && chat.destroy(); } catch {}
|
|
835
|
+
chat = null;
|
|
804
836
|
}
|
|
805
837
|
}
|
|
806
838
|
|