@seamnet/client 0.13.1 → 0.13.3
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 +123 -84
- package/package.json +1 -1
package/lib/plugins/im/index.cjs
CHANGED
|
@@ -144,16 +144,23 @@ function createImPlugin() {
|
|
|
144
144
|
let chatGeneration = 0; // chat 实例代数,每次 create 递增;listener 校验代数,老代事件忽略
|
|
145
145
|
let keepaliveTimer = null;
|
|
146
146
|
let processRejectionHandler = null;
|
|
147
|
-
|
|
147
|
+
// 重建失败后自驱重试:指数退避 + jitter;成功后重置
|
|
148
|
+
let rebuildRetryAttempt = 0;
|
|
149
|
+
let rebuildRetryTimer = null;
|
|
150
|
+
const REBUILD_COOLDOWN_MS = 60_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;
|
|
155
|
-
//
|
|
156
|
-
const
|
|
159
|
+
// 重建失败退避表(最后一档持续)
|
|
160
|
+
const REBUILD_RETRY_BACKOFF_MS = [3_000, 10_000, 30_000, 60_000];
|
|
161
|
+
// 不再主动 rebuild 2801——SDK 内部有自己的重连,0.12.7 证明十几天不断。
|
|
162
|
+
// 0.13.0 加 rebuild 反而制造僵尸循环。只靠 keepalive(3min)和 kicked_out 兜底。
|
|
163
|
+
const SDK_REBUILD_ERROR_CODES = new Set();
|
|
157
164
|
|
|
158
165
|
function withTimeout(promise, ms, name) {
|
|
159
166
|
let tid = null;
|
|
@@ -357,14 +364,14 @@ function createImPlugin() {
|
|
|
357
364
|
return { ok: true };
|
|
358
365
|
}
|
|
359
366
|
|
|
360
|
-
// ===== 事件 handler 工厂(每次
|
|
367
|
+
// ===== 事件 handler 工厂(每次 attachListenersTo 时绑定当前 generation)=====
|
|
361
368
|
// handler 校验 generation,老实例晚到的事件直接忽略,避免污染新状态
|
|
362
369
|
|
|
363
370
|
function makeHandlers(gen) {
|
|
364
371
|
return {
|
|
365
372
|
onSdkReady: () => {
|
|
366
373
|
if (gen !== chatGeneration) return;
|
|
367
|
-
sdkReady
|
|
374
|
+
// sdkReady 的置 true 统一在 rebuildChat/init 成功 swap 后做,handler 只记录
|
|
368
375
|
log.info('sdk_ready', { userId: myUserId, gen });
|
|
369
376
|
},
|
|
370
377
|
onSdkNotReady: () => {
|
|
@@ -529,70 +536,110 @@ function createImPlugin() {
|
|
|
529
536
|
}
|
|
530
537
|
}
|
|
531
538
|
|
|
532
|
-
function
|
|
533
|
-
const h = makeHandlers(
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
539
|
+
function attachListenersTo(chatInstance, gen) {
|
|
540
|
+
const h = makeHandlers(gen);
|
|
541
|
+
chatInstance.on(TencentCloudChat.EVENT.SDK_READY, h.onSdkReady);
|
|
542
|
+
chatInstance.on(TencentCloudChat.EVENT.SDK_NOT_READY, h.onSdkNotReady);
|
|
543
|
+
chatInstance.on(TencentCloudChat.EVENT.ERROR, h.onSdkError);
|
|
544
|
+
chatInstance.on(TencentCloudChat.EVENT.KICKED_OUT, h.onKickedOut);
|
|
545
|
+
chatInstance.on(TencentCloudChat.EVENT.NET_STATE_CHANGE, h.onNetStateChange);
|
|
546
|
+
chatInstance.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, h.onMessageReceived);
|
|
540
547
|
try {
|
|
541
|
-
|
|
548
|
+
chatInstance.on(TencentCloudChat.EVENT.FRIEND_LIST_UPDATED, h.onFriendListUpdated);
|
|
542
549
|
} catch (err) {
|
|
543
550
|
log.warn('friend_list_listener_register_failed', { message: err.message });
|
|
544
551
|
}
|
|
545
552
|
}
|
|
546
553
|
|
|
547
|
-
|
|
554
|
+
function waitForSdkReadyStrict(chatInstance, ms) {
|
|
555
|
+
return new Promise((resolve, reject) => {
|
|
556
|
+
const timer = setTimeout(
|
|
557
|
+
() => reject(new Error(`SDK_READY timeout ${ms}ms`)),
|
|
558
|
+
ms
|
|
559
|
+
);
|
|
560
|
+
chatInstance.on(TencentCloudChat.EVENT.SDK_READY, () => {
|
|
561
|
+
clearTimeout(timer);
|
|
562
|
+
resolve();
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
}
|
|
548
566
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
//
|
|
567
|
+
// ===== 候选模式构建新 chat(失败则隔离销毁,不污染 active chat)=====
|
|
568
|
+
async function buildChatCandidate() {
|
|
569
|
+
const myGen = ++chatGeneration;
|
|
570
|
+
// 清 globals,让 setupSdkGlobals 重新铺 window 等
|
|
553
571
|
delete global.window;
|
|
554
572
|
global._imSdkGlobalsSet = false;
|
|
555
|
-
|
|
573
|
+
const nextChat = TencentCloudChat.create({ SDKAppID: Number(credentialsRef.sdkAppId) });
|
|
556
574
|
setupSdkGlobals();
|
|
557
575
|
try {
|
|
558
576
|
const TIMUploadPlugin = require('tim-upload-plugin');
|
|
559
|
-
|
|
577
|
+
nextChat.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin });
|
|
560
578
|
} catch {}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
579
|
+
nextChat.setLogLevel(4);
|
|
580
|
+
attachListenersTo(nextChat, myGen);
|
|
581
|
+
try {
|
|
582
|
+
await withTimeout(
|
|
583
|
+
nextChat.login({ userID: myUserId, userSig: credentialsRef.userSig }),
|
|
584
|
+
LOGIN_TIMEOUT_MS,
|
|
585
|
+
'chat.login'
|
|
586
|
+
);
|
|
587
|
+
// 严格等 SDK_READY:超时 reject,不再假成功
|
|
588
|
+
await waitForSdkReadyStrict(nextChat, SDK_READY_TIMEOUT_MS);
|
|
589
|
+
} catch (err) {
|
|
590
|
+
// 失败的 candidate 必须隔离销毁,避免 late event 污染新状态
|
|
591
|
+
try {
|
|
592
|
+
await withTimeout(
|
|
593
|
+
(nextChat.logout && nextChat.logout().catch(() => {})) || Promise.resolve(),
|
|
594
|
+
LOGOUT_TIMEOUT_MS,
|
|
595
|
+
'cand_logout'
|
|
596
|
+
);
|
|
597
|
+
} catch {}
|
|
598
|
+
try { nextChat.destroy && nextChat.destroy(); } catch {}
|
|
599
|
+
throw err;
|
|
600
|
+
}
|
|
601
|
+
return { nextChat, gen: myGen };
|
|
575
602
|
}
|
|
576
603
|
|
|
604
|
+
// ===== 重建 chat 实例(SDK 僵尸时用)=====
|
|
577
605
|
async function rebuildChat(reason) {
|
|
578
606
|
log.warn('chat_rebuild_start', { reason, prevGen: chatGeneration });
|
|
579
607
|
sdkReady = false;
|
|
580
|
-
// 每步都 timeout:老 chat 僵尸可能让 logout 永不 resolve
|
|
581
608
|
const oldChat = chat;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
609
|
+
|
|
610
|
+
// 候选模式:先把新 chat 完整拉起来(login + SDK_READY),失败则 throw,老 chat 保持不变
|
|
611
|
+
const { nextChat, gen: newGen } = await buildChatCandidate();
|
|
612
|
+
|
|
613
|
+
// 成功:swap 到新 chat;sdkReady 只在 swap 完成后置 true
|
|
614
|
+
chat = nextChat;
|
|
615
|
+
sdkReady = true;
|
|
616
|
+
log.info('chat_rebuild_done', { reason, newGen });
|
|
617
|
+
|
|
618
|
+
// 老 chat 后台销毁,不阻塞新 chat 服务
|
|
619
|
+
(async () => {
|
|
620
|
+
try {
|
|
621
|
+
if (oldChat && oldChat.logout) {
|
|
622
|
+
await withTimeout(oldChat.logout().catch(() => {}), LOGOUT_TIMEOUT_MS, 'old_logout');
|
|
623
|
+
}
|
|
624
|
+
} catch (e) {
|
|
625
|
+
log.warn('old_logout_timeout', { message: e.message });
|
|
626
|
+
}
|
|
627
|
+
try { oldChat && oldChat.destroy && oldChat.destroy(); } catch {}
|
|
628
|
+
})();
|
|
629
|
+
|
|
590
630
|
try {
|
|
591
631
|
await withTimeout(refreshContactsFromFriendList(), SDK_CALL_TIMEOUT_MS, 'refresh_contacts');
|
|
592
632
|
} catch (e) {
|
|
593
633
|
log.warn('refresh_contacts_after_rebuild_timeout', { message: e.message });
|
|
594
634
|
}
|
|
595
|
-
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function scheduleRebuildRetry(reason, delayMs) {
|
|
638
|
+
if (rebuildRetryTimer) clearTimeout(rebuildRetryTimer);
|
|
639
|
+
rebuildRetryTimer = setTimeout(() => {
|
|
640
|
+
rebuildRetryTimer = null;
|
|
641
|
+
scheduleRebuild(reason);
|
|
642
|
+
}, delayMs);
|
|
596
643
|
}
|
|
597
644
|
|
|
598
645
|
async function doRebuild(reason) {
|
|
@@ -601,9 +648,18 @@ function createImPlugin() {
|
|
|
601
648
|
try {
|
|
602
649
|
await withTimeout(rebuildChat(reason), REBUILD_WATCHDOG_MS, 'rebuild_watchdog');
|
|
603
650
|
lastRebuildSuccessAt = Date.now();
|
|
651
|
+
rebuildRetryAttempt = 0;
|
|
652
|
+
if (rebuildRetryTimer) { clearTimeout(rebuildRetryTimer); rebuildRetryTimer = null; }
|
|
604
653
|
} catch (e) {
|
|
605
|
-
log.error('chat_rebuild_failed', e, { reason, message: e.message });
|
|
606
|
-
//
|
|
654
|
+
log.error('chat_rebuild_failed', e, { reason, message: e.message, attempt: rebuildRetryAttempt });
|
|
655
|
+
// 自驱重试:指数退避 + jitter;不依赖外部信号
|
|
656
|
+
const idx = Math.min(rebuildRetryAttempt, REBUILD_RETRY_BACKOFF_MS.length - 1);
|
|
657
|
+
const base = REBUILD_RETRY_BACKOFF_MS[idx];
|
|
658
|
+
const jitter = Math.floor(Math.random() * 1000);
|
|
659
|
+
const delay = base + jitter;
|
|
660
|
+
rebuildRetryAttempt += 1;
|
|
661
|
+
log.info('chat_rebuild_retry_scheduled', { attempt: rebuildRetryAttempt, delayMs: delay });
|
|
662
|
+
scheduleRebuildRetry(`auto_retry:${reason}`, delay);
|
|
607
663
|
} finally {
|
|
608
664
|
rebuilding = false;
|
|
609
665
|
if (rebuildRequestedAgain) {
|
|
@@ -668,55 +724,26 @@ function createImPlugin() {
|
|
|
668
724
|
myUserId = credentials.userId;
|
|
669
725
|
credentialsRef = credentials;
|
|
670
726
|
|
|
671
|
-
// 先 delete window 让 SDK 走 node 路径
|
|
672
|
-
delete global.window;
|
|
673
727
|
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
728
|
|
|
729
|
+
// 首次走 candidate 模式:严格 login + SDK_READY,失败直接 throw
|
|
680
730
|
try {
|
|
681
|
-
const
|
|
682
|
-
chat
|
|
731
|
+
const built = await buildChatCandidate();
|
|
732
|
+
chat = built.nextChat;
|
|
733
|
+
sdkReady = true;
|
|
734
|
+
log.info('login_ok', { userId: myUserId, gen: built.gen });
|
|
683
735
|
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
736
|
} catch (err) {
|
|
697
737
|
log.error('login_failed', err);
|
|
698
738
|
throw wrap(err, {
|
|
699
739
|
code: 'IM_LOGIN_FAILED',
|
|
700
|
-
hint: '检查 userSig 是否过期、sdkAppId
|
|
740
|
+
hint: '检查 userSig 是否过期、sdkAppId 是否正确、网络到 my-imcloud.com',
|
|
701
741
|
docs: 'docs/maintainer-guide.md#IM_LOGIN_FAILED',
|
|
702
742
|
});
|
|
703
743
|
}
|
|
704
744
|
|
|
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
745
|
// 拉一次好友列表建立关系表 contacts.json
|
|
719
|
-
// FRIEND_LIST_UPDATED 监听已在
|
|
746
|
+
// FRIEND_LIST_UPDATED 监听已在 buildChatCandidate 里注册
|
|
720
747
|
seedDisplayNameFromContacts();
|
|
721
748
|
await refreshContactsFromFriendList();
|
|
722
749
|
|
|
@@ -743,6 +770,12 @@ function createImPlugin() {
|
|
|
743
770
|
processRejectionHandler = (reason) => {
|
|
744
771
|
const code = reason?.code;
|
|
745
772
|
if (code && SDK_REBUILD_ERROR_CODES.has(code)) {
|
|
773
|
+
// sdkReady=true 说明当前 session 健康,2801 来自被 destroy 但没清干净的老实例
|
|
774
|
+
// 真正断了会被 keepalive(3min)或 onSdkError(generation-scoped)兜住
|
|
775
|
+
if (sdkReady) {
|
|
776
|
+
log.info('stale_rejection_ignored', { code });
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
746
779
|
scheduleRebuild(`unhandledRejection:${code}`);
|
|
747
780
|
}
|
|
748
781
|
};
|
|
@@ -790,17 +823,23 @@ function createImPlugin() {
|
|
|
790
823
|
clearInterval(keepaliveTimer);
|
|
791
824
|
keepaliveTimer = null;
|
|
792
825
|
}
|
|
826
|
+
if (rebuildRetryTimer) {
|
|
827
|
+
clearTimeout(rebuildRetryTimer);
|
|
828
|
+
rebuildRetryTimer = null;
|
|
829
|
+
}
|
|
793
830
|
if (processRejectionHandler) {
|
|
794
831
|
try { process.off('unhandledRejection', processRejectionHandler); } catch {}
|
|
795
832
|
processRejectionHandler = null;
|
|
796
833
|
}
|
|
797
834
|
if (chat) {
|
|
798
835
|
try {
|
|
799
|
-
await chat.logout();
|
|
836
|
+
await withTimeout(chat.logout().catch(() => {}), LOGOUT_TIMEOUT_MS, 'destroy_logout');
|
|
800
837
|
if (log) log.info('logged_out');
|
|
801
838
|
} catch (e) {
|
|
802
839
|
if (log) log.warn('logout_failed', { message: e.message });
|
|
803
840
|
}
|
|
841
|
+
try { chat.destroy && chat.destroy(); } catch {}
|
|
842
|
+
chat = null;
|
|
804
843
|
}
|
|
805
844
|
}
|
|
806
845
|
|