fix(web): code review round 3 -- surviving races and a shape bug
Round 2's dedup fixes for connect()/openConversation left two gaps that
only showed up under closer scrutiny (and, for the first one, actual
live reproduction):
- closeConversation() awaited the shared in-flight subscribe promise
before calling leave(), but didn't check whether a NEWER
openConversation() had since claimed the same topic while it was
waiting -- under React 19 StrictMode's mount->cleanup->remount, the
stale cleanup's leave() would detach the subscription the surviving
mount had just established, silently killing live updates for
whatever conversation you'd just opened. Fixed with a per-topic
generation counter: closeConversation only actually leaves if no
newer open has since claimed the topic.
- prefetchPreview (chat-list preview subscribe) never joined the same
in-flight tracking as openConversation, so opening a conversation
while its own preview subscribe was still resolving could still send
two overlapping {sub} packets at the same topic.
- resetTinode() didn't clear connectPromise/restorePromise or
disconnect a connection that was still mid-handshake (only guarded on
isConnected()) -- switching tenants while the old connection was slow
to respond could leave a new login silently waiting on the abandoned
old connection's outcome.
- subscribeMeTopic() got the same in-flight dedup as the others, for
consistency (no concrete concurrent caller today, cheap insurance
against a future one).
Also:
- getBlocks()'s response type was a flat Friend shape; the real
contract (confirmed against both the protocol doc's JSON example and
the old app's own consumer) nests the user under `.user`, so "already
blocked" state in DetailColumn was silently always false.
- useBootstrap() had no tenant in its query key and staleTime:Infinity,
so switching tenants without a full reload kept serving the PREVIOUS
tenant's message_policy/features/permissions/security forever.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c9f4615c40
commit
8ec74dffcc
@@ -165,7 +165,7 @@ export async function imRequest<T = unknown>(
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!response.ok || (payload?.code && payload.code !== 0)) {
|
||||
if (!response.ok || payload?.code) {
|
||||
const known = translateReason(payload.reason, payload.details);
|
||||
throw new ImApiError(
|
||||
known || payload.message || payload.reason || response.statusText || '请求失败',
|
||||
@@ -196,16 +196,22 @@ export const imDelete = <T = unknown>(path: string, opts: RequestOptions = {}) =
|
||||
* only ever fetch page 1 (e.g. "am I this group's owner/admin" going undetected because the real
|
||||
* answer was on page 2). Empty/repeated next_cursor means done.
|
||||
*/
|
||||
// Guards against a server-side cursor bug (e.g. two values that cycle back and forth) turning this
|
||||
// into an infinite fetch loop -- 200 pages at a 20-50 item page size is already far more than any
|
||||
// realistic friends/blocks/search list, so hitting this is itself a signal something's wrong server-
|
||||
// side, not a real user's data legitimately needing more.
|
||||
const MAX_PAGES = 200;
|
||||
|
||||
export async function fetchAllPages<T>(
|
||||
fetchPage: (cursor: string) => Promise<{ items?: T[]; next_cursor?: string }>,
|
||||
startCursor = '',
|
||||
): Promise<T[]> {
|
||||
let cursor = startCursor;
|
||||
let items: T[] = [];
|
||||
for (;;) {
|
||||
const page = await fetchPage(cursor);
|
||||
items = items.concat(page?.items || []);
|
||||
const nextCursor = page?.next_cursor || '';
|
||||
for (let page = 0; page < MAX_PAGES; page++) {
|
||||
const result = await fetchPage(cursor);
|
||||
items = items.concat(result?.items || []);
|
||||
const nextCursor = result?.next_cursor || '';
|
||||
if (!nextCursor || nextCursor === cursor) break;
|
||||
cursor = nextCursor;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user