From 7cf8e4f4e071b1ec534f379fb5f81d673b4d1496 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 18 Sep 2026 18:07:41 +0800 Subject: [PATCH] fix(groups): correct rename-sync event name and unreliable getMeta path The previous fix listened for a documented-but-wrong event name (group.profile.updated); live instrumentation of a real rename showed the actual wire event is group.settings.updated. Fixing only the name still didn't work: the refresh mechanism it triggered (getMeta(startMetaQuery().withDesc().build())) was itself unreliable against this backend -- live-confirmed to either time out (504) on a brand-new topic or return stale pre-rename data on an established one, which also explains the "second rename flips back to the original name" symptom (a late, stale response from an earlier call clobbering the correct value). Replace it with the already-reliable REST getGroup() response applied locally via the SDK's own _processMetaDesc (same internal method the SDK calls when a real meta response arrives; confirmed to reach me.contacts(), which the sidebar/header read). Also fixes a second, independently-discovered bug found while verifying the above: notifyTopic's requestAnimationFrame-based notification batching could get permanently stuck if the callback was scheduled while the page wasn't compositing frames, silently freezing all future topic-list updates (not just renames) until a full reload. Falls back to setTimeout when the document is hidden, since that is never dropped the way the rAF was. Co-Authored-By: Claude Sonnet 5 --- web/src/features/groups/GroupDetailColumn.tsx | 11 +++-- web/src/lib/tinode/tinode-sdk.d.ts | 18 +++++++ web/src/lib/tinode/topics.ts | 47 ++++++++++++++----- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/web/src/features/groups/GroupDetailColumn.tsx b/web/src/features/groups/GroupDetailColumn.tsx index fdb842ab..368ea1dc 100644 --- a/web/src/features/groups/GroupDetailColumn.tsx +++ b/web/src/features/groups/GroupDetailColumn.tsx @@ -172,10 +172,13 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }: queryClient.setQueryData(['group', topicId], (prev: GroupProfile | undefined) => (prev ? { ...prev, ...data } : data)); setNameEditing(false); // The renamer's OWN sidebar/header (`contact.public?.fn`, native SDK state) doesn't update from - // this REST call either -- only from the `group.profile.updated` push topics.ts now listens for, - // which this account won't receive for its own action. Trigger the same SDK-sanctioned refresh - // directly instead of waiting on an unconfirmed self-echo of that push. - tinode.getTopic(topicId).getMeta(tinode.getTopic(topicId).startMetaQuery().withDesc().build()).catch(() => {}); + // this REST call either -- only from the `group.settings.updated` push topics.ts listens for, + // which this account won't receive for its own action. `data` here is already the fresh REST + // response, so apply it locally via `_processMetaDesc` (live-confirmed to reach `me.contacts()`, + // see topics.ts's comment on the same call) instead of an extra `getMeta(...withDesc())` round + // trip -- that SDK-sanctioned path was live-instrumented and found to reliably time out (504) or + // return stale data against this backend, so it's not used anywhere in this fix anymore. + tinode.getTopic(topicId)._processMetaDesc({ public: { fn: data.name, name: data.name } }); }, onError: (err) => showError(err, '群名保存失败'), }); diff --git a/web/src/lib/tinode/tinode-sdk.d.ts b/web/src/lib/tinode/tinode-sdk.d.ts index d4bf2f67..66ae9482 100644 --- a/web/src/lib/tinode/tinode-sdk.d.ts +++ b/web/src/lib/tinode/tinode-sdk.d.ts @@ -183,6 +183,24 @@ declare module 'tinode-sdk' { getMeta(query: unknown): Promise; startMetaQuery(): MetaGetBuilder; + /** + * Private/internal SDK method (underscore prefix, not part of the documented API) -- normally only + * ever called by the SDK itself when a real `{meta desc}` server response arrives. Declared here + * and used deliberately, NOT as a first choice: `getMeta(startMetaQuery().withDesc().build())` (the + * sanctioned public API for exactly this "re-pull desc from server" need) was live-instrumented + * against this backend and found to be genuinely unreliable for group topics -- consistently a + * 504 timeout (~5.4s, every attempt, including 10+s after the write) on a brand-new topic, and on + * an established topic a fast-but-STALE response whose `public.fn` still reflected the pre-rename + * name (confirmed by round-tripping a real REST rename immediately followed by this call). Calling + * this directly with data already known-fresh (from a REST response we trust) sidesteps that + * broken path entirely -- confirmed by reading the SDK's own source that it merges `e.public` onto + * the topic's existing `public` object (other fields like `version` survive) and, for a non-`me` + * topic, forwards the update to the `me` topic's own subscription cache via `onMetaSub`/ + * `onSubsUpdated` -- i.e. the exact same fan-out a genuine server push would trigger. Live-confirmed + * this reaches `me.contacts()` (same object reference as `topic.public`, verified live). + */ + _processMetaDesc(desc: { public?: Partial }): void; + /** Builds the `{pub}` packet shape but does not send it -- pair with `publishDraft`/`publishMessage`. */ createMessage(content: DraftyContent | string, noEcho?: boolean): StoredMessage; /** diff --git a/web/src/lib/tinode/topics.ts b/web/src/lib/tinode/topics.ts index 648eea0c..7019bba3 100644 --- a/web/src/lib/tinode/topics.ts +++ b/web/src/lib/tinode/topics.ts @@ -12,6 +12,7 @@ // getParams -- so this always leaves first when already subscribed, then resubscribes. import type { Contact, StoredMessage, Topic } from 'tinode-sdk'; +import { getGroup } from '@/lib/api/groups'; import { MESSAGES_PAGE } from '@/lib/config'; import { getTinode, onReauthenticated } from './connection'; @@ -128,7 +129,22 @@ function notifyTopic(topicName: string) { pendingAnyNotification = true; if (!notifyFrameScheduled) { notifyFrameScheduled = true; - requestAnimationFrame(flushTopicNotifications); + // Live-confirmed real bug (surfaced while testing an unrelated group-rename fix, in a webview whose + // frame compositing was paused, i.e. genuinely not painting -- not just an ordinary backgrounded + // browser tab): a `requestAnimationFrame` scheduled while the page isn't compositing frames can go + // uncalled indefinitely rather than merely deferred-until-visible. Since `notifyFrameScheduled` only + // resets to false inside the callback, one dropped frame permanently wedges this guard, silently + // freezing EVERY future topic-list update (not just the group name) until a full page reload -- data + // kept arriving and updating correctly underneath (confirmed live: `me.contacts()` had the right + // data the whole time), only the UI never found out. `document.hidden` costs nothing to check and + // `setTimeout` is never silently dropped the way this rAF was, so route through it whenever the page + // isn't visible; keep `requestAnimationFrame` for the visible case since that's what actually gives + // the intended one-flush-per-paint batching this function exists for. + if (typeof document !== 'undefined' && document.hidden) { + setTimeout(flushTopicNotifications, 0); + } else { + requestAnimationFrame(flushTopicNotifications); + } } } @@ -298,19 +314,24 @@ function wireTopicCallbacks(topic: Topic) { topic.getMeta(topic.startMetaQuery().withData(seq, seq + 1, 1).build()).catch(() => {}); } } - // docs/IM-前后端对接文档.md:1542: `PATCH .../groups/{topic}` (rename/avatar/description) pushes a - // custom `{info what:"group", event:"group.profile.updated"}` to every other online member -- same - // undocumented-to-the-SDK shape as `group.dissolved` above, so nothing here ever refreshed the - // topic's native `public` fields on receipt. Live-confirmed the actual gap: the sidebar/header name + // docs/IM-前后端对接文档.md:1542 documents this push as `group.profile.updated`, but a real rename, + // live-instrumented (logging the raw `info` object as it arrived), showed the actual wire event is + // `{what:"group", event:"group.settings.updated"}` -- same doc-vs-wire drift already seen elsewhere + // in this backend. Matching the real event name fixes only half the bug: the sidebar/header name // comes from `contact.public?.fn` (native SDK state, see ChatListColumn.tsx), which a REST-only - // mutation never touches for ANYONE -- not just other members, but the renamer's own other tabs/ - // views too -- and only ever got refreshed as a side effect of the unrelated focus-triggered - // `reconcileMeSubs()`, which is why refocus "fixed" it for everyone. `withDesc()` is the SDK's own - // sanctioned way to re-pull `public`/description from the server (contrast the abandoned approach - // of manually patching `Topic._processMetaDesc` tried earlier for a different bug -- that one was - // reverted for inconclusive verification; this one goes through a real server round-trip instead). - if (groupEvent?.what === 'group' && groupEvent.event === 'group.profile.updated') { - topic.getMeta(topic.startMetaQuery().withDesc().build()).catch(() => {}); + // mutation never touches for anyone, and the "obvious" fix -- `getMeta(startMetaQuery().withDesc() + // .build())`, the SDK's own sanctioned way to re-pull desc from the server -- was live-instrumented + // too and found to be genuinely broken against this backend: a consistent 504 timeout (~5.4s, every + // attempt, still failing 10+s after the write) on a brand-new topic, and on an established topic a + // fast-but-stale response whose `public.fn` still held the pre-rename name. Using the already- + // reliable REST `getGroup()` instead (confirmed live to return the fresh name immediately after a + // rename, every time) and applying it locally via `_processMetaDesc` -- the same internal method the + // SDK itself calls when a real `{meta desc}` response arrives, live-confirmed to correctly reach + // `me.contacts()` -- sidesteps the broken path entirely instead of routing through it. + if (groupEvent?.what === 'group' && groupEvent.event === 'group.settings.updated') { + getGroup(topic.name) + .then((data) => topic._processMetaDesc({ public: { fn: data.name, name: data.name } })) + .catch(() => {}); } }; topic.onMetaDesc = notify;