This commit is contained in:
Austin
2026-09-18 18:34:21 +08:00
3 changed files with 59 additions and 17 deletions
@@ -172,10 +172,13 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }:
queryClient.setQueryData(['group', topicId], (prev: GroupProfile | undefined) => (prev ? { ...prev, ...data } : data)); queryClient.setQueryData(['group', topicId], (prev: GroupProfile | undefined) => (prev ? { ...prev, ...data } : data));
setNameEditing(false); setNameEditing(false);
// The renamer's OWN sidebar/header (`contact.public?.fn`, native SDK state) doesn't update from // 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, // 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. Trigger the same SDK-sanctioned refresh // which this account won't receive for its own action. `data` here is already the fresh REST
// directly instead of waiting on an unconfirmed self-echo of that push. // response, so apply it locally via `_processMetaDesc` (live-confirmed to reach `me.contacts()`,
tinode.getTopic(topicId).getMeta(tinode.getTopic(topicId).startMetaQuery().withDesc().build()).catch(() => {}); // 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, '群名保存失败'), onError: (err) => showError(err, '群名保存失败'),
}); });
+18
View File
@@ -183,6 +183,24 @@ declare module 'tinode-sdk' {
getMeta(query: unknown): Promise<unknown>; getMeta(query: unknown): Promise<unknown>;
startMetaQuery(): MetaGetBuilder; 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<TopicDescPublic> }): void;
/** Builds the `{pub}` packet shape but does not send it -- pair with `publishDraft`/`publishMessage`. */ /** Builds the `{pub}` packet shape but does not send it -- pair with `publishDraft`/`publishMessage`. */
createMessage(content: DraftyContent | string, noEcho?: boolean): StoredMessage; createMessage(content: DraftyContent | string, noEcho?: boolean): StoredMessage;
/** /**
+34 -13
View File
@@ -12,6 +12,7 @@
// getParams -- so this always leaves first when already subscribed, then resubscribes. // getParams -- so this always leaves first when already subscribed, then resubscribes.
import type { Contact, StoredMessage, Topic } from 'tinode-sdk'; import type { Contact, StoredMessage, Topic } from 'tinode-sdk';
import { getGroup } from '@/lib/api/groups';
import { MESSAGES_PAGE } from '@/lib/config'; import { MESSAGES_PAGE } from '@/lib/config';
import { getTinode, onReauthenticated } from './connection'; import { getTinode, onReauthenticated } from './connection';
@@ -128,7 +129,22 @@ function notifyTopic(topicName: string) {
pendingAnyNotification = true; pendingAnyNotification = true;
if (!notifyFrameScheduled) { if (!notifyFrameScheduled) {
notifyFrameScheduled = true; 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);
}
} }
} }
@@ -327,19 +343,24 @@ function wireTopicCallbacks(topic: Topic) {
topic.getMeta(topic.startMetaQuery().withData(seq, seq + 1, 1).build()).catch(() => {}); topic.getMeta(topic.startMetaQuery().withData(seq, seq + 1, 1).build()).catch(() => {});
} }
} }
// docs/IM-前后端对接文档.md:1542: `PATCH .../groups/{topic}` (rename/avatar/description) pushes a // docs/IM-前后端对接文档.md:1542 documents this push as `group.profile.updated`, but a real rename,
// custom `{info what:"group", event:"group.profile.updated"}` to every other online member -- same // live-instrumented (logging the raw `info` object as it arrived), showed the actual wire event is
// undocumented-to-the-SDK shape as `group.dissolved` above, so nothing here ever refreshed the // `{what:"group", event:"group.settings.updated"}` -- same doc-vs-wire drift already seen elsewhere
// topic's native `public` fields on receipt. Live-confirmed the actual gap: the sidebar/header name // 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 // 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/ // mutation never touches for anyone, and the "obvious" fix -- `getMeta(startMetaQuery().withDesc()
// views too -- and only ever got refreshed as a side effect of the unrelated focus-triggered // .build())`, the SDK's own sanctioned way to re-pull desc from the server -- was live-instrumented
// `reconcileMeSubs()`, which is why refocus "fixed" it for everyone. `withDesc()` is the SDK's own // too and found to be genuinely broken against this backend: a consistent 504 timeout (~5.4s, every
// sanctioned way to re-pull `public`/description from the server (contrast the abandoned approach // attempt, still failing 10+s after the write) on a brand-new topic, and on an established topic a
// of manually patching `Topic._processMetaDesc` tried earlier for a different bug -- that one was // fast-but-stale response whose `public.fn` still held the pre-rename name. Using the already-
// reverted for inconclusive verification; this one goes through a real server round-trip instead). // reliable REST `getGroup()` instead (confirmed live to return the fresh name immediately after a
if (groupEvent?.what === 'group' && groupEvent.event === 'group.profile.updated') { // rename, every time) and applying it locally via `_processMetaDesc` -- the same internal method the
topic.getMeta(topic.startMetaQuery().withDesc().build()).catch(() => {}); // 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; topic.onMetaDesc = notify;