修复新会话首次打开时消息历史被静默截断为1条、对方已读回执永不刷新
根因:tinode-web.jsx的prefetchLatestMessagePreviews()(会话列表"最后一条消息"预览)
会在会话列表一渲染就对每个有动态但本地无缓存摘要的话题调用
topic.subscribe(startMetaQuery().withLaterData(1)...),只为预览目的抓取"最新1条"
消息——这常常发生在用户真正点开这个会话之前。这次订阅会把tinode-sdk的
Topic._attached置为true,且把_maxSeq设成那一条消息的seq。
用户随后点开会话时,messages-view.jsx自己的subscribe()发现topic.isSubscribed()
已经是true就直接return,从未真正发出过完整的{sub}请求。即使去掉这个短路,
SDK的Topic.subscribe()本身在_attached为true时也会无条件短路成
Promise.resolve(this),完全不看传入的getParams——所以无论如何都不会把
withLaterSub()/withAux()等真正需要的查询发给服务端。
现场复现:全新注册的两个互为好友账号之间发4条真实消息,全新session下第一次点开
该会话,只渲染出最新1条,其余3条静默丢失;对方的已读回执(应变蓝色双勾)永远
不刷新——这两个现象都会在离开会话再重新打开后自愈(因为离开会清掉_attached),
但用户在此之前看到的是一个残缺、状态错误的会话。
修复分两处:
1. subscribe()内,若topic已经isSubscribed(),先topic.leave(false)(只清客户端和
服务端的订阅态,不清本地已缓存消息),再执行真正的subscribe(),确保这次一定
是货真价实的、服务端会认真处理getParams的订阅请求。
2. 把消息分页查询从withLaterData(MESSAGES_PAGE)("给我比本地_maxSeq更新的消息")
改成withData(undefined, undefined, MESSAGES_PAGE)("给我最新的N条消息",不依赖
本地缓存游标)。前者在_maxSeq被上述预览订阅污染后会查询"since:被污染的seq+1",
服务端自然返回空;后者是绝对查询,对全新话题(_maxSeq为0,两者退化成同一个
since:undefined查询)行为不变,对被污染的话题能正确要回完整历史。
第14+轮最小粒度测试 Ch3-4 发现(P2P单聊消息状态机场景)。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8087fd7e97
commit
ddf23d40b2
@@ -665,7 +665,7 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribe(topic) {
|
subscribe(topic) {
|
||||||
if (topic.isSubscribed() || !this.props.ready) {
|
if (!this.props.ready) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,7 +676,24 @@ class MessagesView extends React.Component {
|
|||||||
let getQuery = topic.startMetaQuery().withLaterDesc().withLaterSub().withAux();
|
let getQuery = topic.startMetaQuery().withLaterDesc().withLaterSub().withAux();
|
||||||
if (this.state.isReader || newTopic) {
|
if (this.state.isReader || newTopic) {
|
||||||
// Reading is either permitted or we don't know because it's a new topic. Ask for messages.
|
// Reading is either permitted or we don't know because it's a new topic. Ask for messages.
|
||||||
getQuery = getQuery.withLaterData(MESSAGES_PAGE);
|
//
|
||||||
|
// withData(undefined, undefined, MESSAGES_PAGE) instead of the seemingly-more-natural
|
||||||
|
// withLaterData(MESSAGES_PAGE): the latter asks the server for "whatever is newer than
|
||||||
|
// topic._maxSeq", which is only correct when _maxSeq reflects a real, contiguous, locally-cached
|
||||||
|
// history. But tinode-web.jsx's prefetchLatestMessagePreviews() (conversation list "last
|
||||||
|
// message" preview) may have already called topic.subscribe(startMetaQuery().withLaterData(1))
|
||||||
|
// for this exact topic the moment the chat list rendered -- often *before* this view ever mounts,
|
||||||
|
// e.g. the very first time a brand-new conversation is opened in a fresh session -- which sets
|
||||||
|
// _maxSeq to that one prefetched message's seq without caching anything before it. From that
|
||||||
|
// point on withLaterData(MESSAGES_PAGE) asks for "later than the one message I already have" and
|
||||||
|
// gets back nothing: confirmed live, a brand-new 4-message P2P conversation opened this way
|
||||||
|
// rendered only the single latest message, silently missing the rest of the history.
|
||||||
|
// withData(undefined, undefined, limit) has no such since-cursor -- it always asks for "the
|
||||||
|
// latest `limit` messages" regardless of what the client thinks it already has, which is
|
||||||
|
// correct both in this poisoned-prefetch case and in the plain fresh-topic case (there
|
||||||
|
// topic._maxSeq is 0, so withLaterData(limit) already degrades to the exact same
|
||||||
|
// since:undefined query -- this is a strict fix, not a behavior change for the common path).
|
||||||
|
getQuery = getQuery.withData(undefined, undefined, MESSAGES_PAGE);
|
||||||
if (this.state.isReader) {
|
if (this.state.isReader) {
|
||||||
getQuery = getQuery.withLaterDel();
|
getQuery = getQuery.withLaterDel();
|
||||||
}
|
}
|
||||||
@@ -685,7 +702,7 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setQuery = newTopic ? this.props.newTopicParams : undefined;
|
const setQuery = newTopic ? this.props.newTopicParams : undefined;
|
||||||
topic.subscribe(getQuery.build(), setQuery)
|
const doSubscribe = () => topic.subscribe(getQuery.build(), setQuery)
|
||||||
.then(ctrl => {
|
.then(ctrl => {
|
||||||
if (ctrl.code == 303) {
|
if (ctrl.code == 303) {
|
||||||
// Redirect to another topic requested.
|
// Redirect to another topic requested.
|
||||||
@@ -726,6 +743,24 @@ class MessagesView extends React.Component {
|
|||||||
blankState.title = this.props.intl.formatMessage(messages.not_found);
|
blankState.title = this.props.intl.formatMessage(messages.not_found);
|
||||||
this.setState(blankState);
|
this.setState(blankState);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (topic.isSubscribed()) {
|
||||||
|
// Same prefetch race as the comment on the data query above: the topic may already be
|
||||||
|
// "attached" server-side from prefetchLatestMessagePreviews' single-message subscribe, before
|
||||||
|
// this view ever asked for anything. The SDK's own Topic.subscribe() unconditionally
|
||||||
|
// short-circuits to `Promise.resolve(this)` once `_attached` is true (tinode.dev.js)
|
||||||
|
// *without even looking at the getParams passed in* -- calling topic.subscribe(getQuery...)
|
||||||
|
// directly in that state would silently no-op and this view would never receive the real
|
||||||
|
// history/pinned-messages/peer read-receipts it just asked for. leave(false) clears _attached
|
||||||
|
// (both client-side via _resetSub() and server-side, since it's a real round trip) without
|
||||||
|
// discarding already-cached messages (that only happens for leave(true), i.e. unsubscribe) --
|
||||||
|
// once it resolves, the follow-up doSubscribe() is guaranteed to be a genuine, un-shortcut
|
||||||
|
// subscribe that gets the server to actually answer getQuery instead of silently dropping it.
|
||||||
|
topic.leave(false).catch(_ => { /* best-effort; doSubscribe() below still needs to run */ }).then(doSubscribe);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
doSubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
leave(oldTopicName) {
|
leave(oldTopicName) {
|
||||||
|
|||||||
Reference in New Issue
Block a user