mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
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:
parent
3554f7e7d6
commit
884b65a040
4 changed files with 93 additions and 1 deletions
5
.changeset/web-snapshot-sync-guard.md
Normal file
5
.changeset/web-snapshot-sync-guard.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix duplicate session snapshot reloads in the bundled web UI during resync.
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
35
apps/kimi-web/src/lib/snapshotSync.ts
Normal file
35
apps/kimi-web/src/lib/snapshotSync.ts
Normal 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 };
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue