Cache available editor discovery
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import equal from "fast-deep-equal";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { TTLCache } from "@isaacs/ttlcache";
|
||||
import pMemoize from "p-memoize";
|
||||
import type { FSWatcher } from "node:fs";
|
||||
import { resolve, sep } from "path";
|
||||
import { homedir } from "node:os";
|
||||
@@ -448,6 +450,8 @@ const MIN_STREAMING_SEGMENT_BYTES = Math.round(
|
||||
);
|
||||
const AgentIdSchema = z.string().uuid();
|
||||
const VOICE_INTERRUPT_CONFIRMATION_MS = 500;
|
||||
const AVAILABLE_EDITOR_TARGETS_CACHE_TTL_MS = 60_000;
|
||||
const AVAILABLE_EDITOR_TARGETS_CACHE_KEY = "available";
|
||||
|
||||
type VoiceModeBaseConfig = {
|
||||
systemPrompt?: string;
|
||||
@@ -705,6 +709,21 @@ export class Session {
|
||||
private nextTerminalSlot = 0;
|
||||
private inflightRequests = 0;
|
||||
private peakInflightRequests = 0;
|
||||
private readonly availableEditorTargetsCache = new TTLCache<
|
||||
string,
|
||||
EditorTargetDescriptorPayload[]
|
||||
>({
|
||||
ttl: AVAILABLE_EDITOR_TARGETS_CACHE_TTL_MS,
|
||||
max: 1,
|
||||
checkAgeOnGet: true,
|
||||
});
|
||||
private readonly getMemoizedAvailableEditorTargets = pMemoize(
|
||||
async () => this.resolveAvailableEditorTargets(),
|
||||
{
|
||||
cache: this.availableEditorTargetsCache,
|
||||
cacheKey: () => AVAILABLE_EDITOR_TARGETS_CACHE_KEY,
|
||||
},
|
||||
);
|
||||
private readonly checkoutDiffSubscriptions = new Map<string, () => void>();
|
||||
private readonly workspaceGitWatchTargets = new Map<string, WorkspaceGitWatchTarget>();
|
||||
private readonly workspaceSetupSnapshots = new Map<string, WorkspaceSetupSnapshot>();
|
||||
@@ -6590,8 +6609,12 @@ export class Session {
|
||||
});
|
||||
}
|
||||
|
||||
async resolveAvailableEditorTargets(): Promise<EditorTargetDescriptorPayload[]> {
|
||||
return listAvailableEditorTargets();
|
||||
}
|
||||
|
||||
async getAvailableEditorTargets() {
|
||||
return this.filterEditorsForClient(await listAvailableEditorTargets());
|
||||
return this.filterEditorsForClient(await this.getMemoizedAvailableEditorTargets());
|
||||
}
|
||||
|
||||
async openEditorTarget(options: { editorId: EditorTargetId; path: string }): Promise<void> {
|
||||
|
||||
@@ -4,7 +4,11 @@ import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { Session } from "./session.js";
|
||||
import type { AgentSnapshotPayload } from "../shared/messages.js";
|
||||
import type {
|
||||
AgentSnapshotPayload,
|
||||
EditorTargetDescriptorPayload,
|
||||
SessionOutboundMessage,
|
||||
} from "../shared/messages.js";
|
||||
import type { WorkspaceGitRuntimeSnapshot } from "./workspace-git-service.js";
|
||||
import {
|
||||
createPersistedProjectRecord,
|
||||
@@ -188,6 +192,7 @@ function createWorkspaceRuntimeSnapshot(
|
||||
function createSessionForWorkspaceTests(
|
||||
options: {
|
||||
appVersion?: string | null;
|
||||
onMessage?: (message: SessionOutboundMessage) => void;
|
||||
workspaceGitService?: ReturnType<typeof createNoopWorkspaceGitService>;
|
||||
} = {},
|
||||
): Session {
|
||||
@@ -203,7 +208,7 @@ function createSessionForWorkspaceTests(
|
||||
const session = new Session({
|
||||
clientId: "test-client",
|
||||
appVersion: options.appVersion ?? null,
|
||||
onMessage: vi.fn(),
|
||||
onMessage: options.onMessage ?? vi.fn(),
|
||||
logger: logger as any,
|
||||
downloadTokenStore: {} as any,
|
||||
pushTokenStore: {} as any,
|
||||
@@ -1555,6 +1560,61 @@ describe("workspace aggregation", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("list_available_editors_request coalesces concurrent discovery", async () => {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const session = createSessionForWorkspaceTests({
|
||||
appVersion: "0.1.50",
|
||||
onMessage: (message) => emitted.push(message),
|
||||
});
|
||||
let resolveDiscovery: (editors: EditorTargetDescriptorPayload[]) => void = () => {};
|
||||
let discoveryCalls = 0;
|
||||
|
||||
vi.spyOn(session, "resolveAvailableEditorTargets").mockImplementation(async () => {
|
||||
discoveryCalls += 1;
|
||||
return await new Promise<EditorTargetDescriptorPayload[]>((resolve) => {
|
||||
resolveDiscovery = resolve;
|
||||
});
|
||||
});
|
||||
|
||||
const first = session.handleMessage({
|
||||
type: "list_available_editors_request",
|
||||
requestId: "req-editors-1",
|
||||
});
|
||||
const second = session.handleMessage({
|
||||
type: "list_available_editors_request",
|
||||
requestId: "req-editors-2",
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
expect(discoveryCalls).toBe(1);
|
||||
|
||||
resolveDiscovery([{ id: "cursor", label: "Cursor" }]);
|
||||
await Promise.all([first, second]);
|
||||
|
||||
const responses = emitted.filter(
|
||||
(
|
||||
message,
|
||||
): message is Extract<SessionOutboundMessage, { type: "list_available_editors_response" }> =>
|
||||
message.type === "list_available_editors_response",
|
||||
);
|
||||
expect(responses).toHaveLength(2);
|
||||
expect(responses.map((response) => response.payload.requestId)).toEqual([
|
||||
"req-editors-1",
|
||||
"req-editors-2",
|
||||
]);
|
||||
expect(responses.map((response) => response.payload.editors)).toEqual([
|
||||
[{ id: "cursor", label: "Cursor" }],
|
||||
[{ id: "cursor", label: "Cursor" }],
|
||||
]);
|
||||
|
||||
await session.handleMessage({
|
||||
type: "list_available_editors_request",
|
||||
requestId: "req-editors-3",
|
||||
});
|
||||
|
||||
expect(discoveryCalls).toBe(1);
|
||||
});
|
||||
|
||||
test("list_available_editors_request filters unsupported ids for legacy clients", async () => {
|
||||
const emitted: Array<{ type: string; payload: unknown }> = [];
|
||||
const session = createSessionForWorkspaceTests({ appVersion: "0.1.49" }) as any;
|
||||
|
||||
Reference in New Issue
Block a user