fix(web): coalesce snapshot reloads on resync (#1087)

Avoid concurrent session snapshot requests when resync_required fires repeatedly, while still allowing one queued rerun after the in-flight reload settles.
This commit is contained in:
qer 2026-06-25 11:49:05 +08:00 committed by GitHub
parent 3554f7e7d6
commit 884b65a040
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 93 additions and 1 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix duplicate session snapshot reloads in the bundled web UI during resync.

View file

@ -7,6 +7,7 @@ import { i18n } from '../i18n';
import { getKimiWebApi } from '../api';
import { isDaemonApiError, isDaemonNetworkError } from '../api/errors';
import { reconcileWorkspaceOrder, sortByWorkspaceOrder } from '../lib/workspaceOrder';
import { createCoalescedAsyncRunner } from '../lib/snapshotSync';
import {
loadUnread,
loadWorkspaceOrder,
@ -747,7 +748,7 @@ function connectEventsIfNeeded(): void {
// returns the authoritative {asOfSeq, epoch} and re-subscribes.
if (epoch !== undefined) epochBySession[sessionId] = epoch;
void currentSeq;
void syncSessionFromSnapshot(sessionId);
snapshotSyncRunner.request(sessionId);
},
onError(_code: number, msg: string, _fatal: boolean) {
@ -1009,6 +1010,8 @@ async function syncSessionFromSnapshot(sessionId: string): Promise<SyncSessionRe
}
}
const snapshotSyncRunner = createCoalescedAsyncRunner(syncSessionFromSnapshot);
function hasLoadedMessages(sessionId: string): boolean {
return Object.prototype.hasOwnProperty.call(rawState.messagesBySession, sessionId);
}

View file

@ -0,0 +1,35 @@
export interface CoalescedAsyncRunner<T> {
run(key: string): Promise<T>;
request(key: string): void;
}
export function createCoalescedAsyncRunner<T>(
fn: (key: string) => Promise<T>,
): CoalescedAsyncRunner<T> {
const inFlight = new Map<string, Promise<T>>();
const queued = new Set<string>();
function run(key: string): Promise<T> {
const existing = inFlight.get(key);
if (existing !== undefined) return existing;
const promise = (async () => fn(key))().finally(() => {
inFlight.delete(key);
if (queued.delete(key)) {
void run(key);
}
});
inFlight.set(key, promise);
return promise;
}
function request(key: string): void {
if (inFlight.has(key)) {
queued.add(key);
return;
}
void run(key);
}
return { run, request };
}

View file

@ -5,6 +5,7 @@ import {
parseFilePathLinkCandidate,
} from '../src/lib/filePathLinks';
import { parseDiff } from '../src/lib/parseDiff';
import { createCoalescedAsyncRunner } from '../src/lib/snapshotSync';
import { normalizeToolName, toolSummary } from '../src/lib/toolMeta';
describe('parseDiff', () => {
@ -76,3 +77,51 @@ describe('toolMeta', () => {
).toBe('example.com/path');
});
});
describe('createCoalescedAsyncRunner', () => {
it('reuses the in-flight promise for the same key', async () => {
let runs = 0;
let resolveRun!: () => void;
const runner = createCoalescedAsyncRunner(async (_key: string) => {
runs += 1;
await new Promise<void>((resolve) => {
resolveRun = resolve;
});
return runs;
});
const first = runner.run('session-a');
const second = runner.run('session-a');
expect(runs).toBe(1);
resolveRun();
await expect(Promise.all([first, second])).resolves.toEqual([1, 1]);
expect(runs).toBe(1);
});
it('queues at most one rerun requested while a run is in flight', async () => {
let runs = 0;
const resolvers: Array<() => void> = [];
const runner = createCoalescedAsyncRunner(async (_key: string) => {
runs += 1;
await new Promise<void>((resolve) => {
resolvers.push(resolve);
});
return runs;
});
const first = runner.run('session-a');
runner.request('session-a');
runner.request('session-a');
expect(runs).toBe(1);
resolvers[0]!();
await first;
await Promise.resolve();
expect(runs).toBe(2);
resolvers[1]!();
await Promise.resolve();
expect(runs).toBe(2);
});
});