群聊生命周期事件对被动成员实时不可见——前端兜底同步(真正的根因在后端,无法单靠前端代码彻底解决)

读im_be的server/im_group_events.go确认:群聊创建/加人/踢人/解散/转让群主等全部事件都走
publishIMGroupRealtimeEvent,这个函数只会广播给"当前已订阅该群topic的会话"——对比P2P场景专门
有一条deliverIMConversationSystemMessageToP2PSessions,会无视订阅状态强制投递给双方的session,
群聊从未做过同等实现。实机验证:一个全新创建、成员从未打开过的群,即使对方账号完全在线、页面就
停留在/messages上,群条目和未读角标都不会出现,等多久都不会自愈,必须手动刷新页面。

由于这个topic客户端本来就不知道存在,没有任何东西可以"响应",前端无法真正修复,只能缓解:复用
subscribeMeTopic()/onReauthenticated()已经在用的withLaterSub()增量语义,改用getMeta()(不像
subscribe()那样在_attached为true时直接短路)定时(30秒)+ 切回标签页时主动拉一次"me"话题的最新
订阅列表。

实机验证:创建一个全新群、只邀请一个从未见过这个群的被动成员,在其页面保持被动、不做任何操作/
刷新的情况下——模拟切换标签页触发的focus/visibilitychange事件后约十几秒内,群条目正确出现在
列表顶部;另一次未受干预的测试中,纯靠30秒定时器本身也确认能自愈。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-16 18:55:04 +08:00
co-authored by Claude Sonnet 5
parent d1f3aea26e
commit ab3843dbdb
2 changed files with 61 additions and 1 deletions
+10 -1
View File
@@ -5,7 +5,7 @@ import { Navigate, Outlet } from 'react-router-dom';
import { Toast } from '@/app/Toast';
import { restoreSession } from '@/lib/tinode/connection';
import { isTenantMismatch } from '@/lib/tinode/errors';
import { subscribeMeTopic } from '@/lib/tinode/topics';
import { startMeReconciliation, subscribeMeTopic } from '@/lib/tinode/topics';
import { useSessionStore } from '@/stores/session';
/** No connect-timeout exists anywhere in the SDK (its WS promise only settles on the underlying
@@ -81,6 +81,15 @@ export function RequireAuth() {
subscribeMeTopic().catch(() => setContactsError('会话列表加载失败,请刷新页面重试'));
}, [myUserId]);
// Separate from the one-time subscribeMeTopic effect above on purpose: this one has real
// start/stop semantics (a timer + listeners, not a non-idempotent WS packet) and needs to
// restart cleanly across StrictMode's mount->cleanup->remount, not be guarded against it. See
// startMeReconciliation's own comment for what backend gap this is mitigating.
useEffect(() => {
if (!myUserId) return undefined;
return startMeReconciliation();
}, [myUserId]);
if (!tenant) return <Navigate to="/tenant" replace />;
if (myUserId) {
return (