Skip to content

Commit 449c649

Browse files
authored
fix(app): 78x faster Home cold loading (anomalyco#36214)
1 parent 35c88c3 commit 449c649

9 files changed

Lines changed: 524 additions & 38 deletions

File tree

packages/app/e2e/utils/mock-server.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
5656
const path = url.pathname
5757
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
5858
if (path === "/global/health") return json(route, { healthy: true })
59+
if (path === "/api/session")
60+
return json(route, {
61+
data: config.sessions.map((session) => v2Session(session, config.directory)),
62+
cursor: {},
63+
})
5964
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: false })
6065
if (path === "/permission")
6166
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
@@ -132,6 +137,30 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
132137
})
133138
}
134139

140+
function v2Session(session: { id: string } & Record<string, unknown>, fallbackDirectory: string) {
141+
const time = session.time && typeof session.time === "object" ? session.time : {}
142+
return {
143+
id: session.id,
144+
parentID: session.parentID,
145+
projectID: session.projectID ?? "project",
146+
cost: session.cost ?? 0,
147+
tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
148+
time: {
149+
created: "created" in time && typeof time.created === "number" ? time.created : 0,
150+
updated: "updated" in time && typeof time.updated === "number" ? time.updated : 0,
151+
...(session.time && typeof session.time === "object" && "archived" in session.time
152+
? { archived: session.time.archived }
153+
: {}),
154+
},
155+
title: session.title ?? session.id,
156+
location: {
157+
directory: typeof session.directory === "string" ? session.directory : fallbackDirectory,
158+
...(typeof session.workspaceID === "string" ? { workspaceID: session.workspaceID } : {}),
159+
},
160+
...(typeof session.path === "string" ? { subpath: session.path } : {}),
161+
}
162+
}
163+
135164
function json(route: Route, body: unknown, headers?: Record<string, string>, status = 200) {
136165
return route.fulfill({
137166
status,

packages/app/src/context/global-sync/child-store.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,56 @@ describe("createChildStoreManager", () => {
221221
dispose()
222222
}
223223
})
224+
225+
test("keeps non-bootstrapping children passive until a real directory access", () => {
226+
let manager: ReturnType<typeof createChildStoreManager> | undefined
227+
const offset = querySingles.length
228+
const bootstraps: string[] = []
229+
230+
const dispose = createOwner((owner) => {
231+
manager = createChildStoreManager({
232+
owner,
233+
scope: ServerScope.local,
234+
persist,
235+
isBooting: () => false,
236+
isLoadingSessions: () => false,
237+
onBootstrap(directory) {
238+
bootstraps.push(directory)
239+
},
240+
onMcp() {},
241+
onDispose() {},
242+
translate: (key) => key,
243+
queryOptions: queryOptionsApi,
244+
global: { provider },
245+
})
246+
})
247+
248+
try {
249+
if (!manager) throw new Error("manager required")
250+
const [store] = manager.child("/project", { bootstrap: false })
251+
const queries = querySingles.slice(offset)
252+
253+
expect(queries).toHaveLength(6)
254+
expect(queries[0]?.().enabled).toBe(false)
255+
expect(queries[3]?.().enabled).toBe(false)
256+
expect(queries[4]?.().enabled).toBe(false)
257+
expect(queries[5]?.().enabled).toBe(false)
258+
expect(store.path.directory).toBe("/project")
259+
expect(store.provider_ready).toBe(false)
260+
expect(store.lsp_ready).toBe(false)
261+
expect(bootstraps).toEqual([])
262+
263+
manager.child("/project")
264+
expect(queries[0]?.().enabled).toBe(true)
265+
expect(queries[3]?.().enabled).toBe(true)
266+
expect(queries[4]?.().enabled).toBe(true)
267+
expect(queries[5]?.().enabled).toBe(true)
268+
expect(bootstraps).toEqual(["/project"])
269+
270+
manager.child("/project", { bootstrap: false })
271+
expect(queries[0]?.().enabled).toBe(true)
272+
} finally {
273+
dispose()
274+
}
275+
})
224276
})

packages/app/src/context/global-sync/child-store.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export function createChildStoreManager(input: {
4545
const disposers = new Map<string, () => void>()
4646
const mcpDirectories = new Set<string>()
4747
const mcpToggles = new Map<string, (enabled: boolean) => void>()
48+
const activeDirectories = new Set<string>()
49+
const activationToggles = new Map<string, (enabled: boolean) => void>()
4850

4951
const markKey = (key: DirectoryKey) => {
5052
if (!key) return
@@ -118,6 +120,8 @@ export function createChildStoreManager(input: {
118120
lifecycle.delete(key)
119121
mcpDirectories.delete(key)
120122
mcpToggles.delete(key)
123+
activeDirectories.delete(key)
124+
activationToggles.delete(key)
121125
const dispose = disposers.get(key)
122126
if (dispose) {
123127
dispose()
@@ -182,20 +186,27 @@ export function createChildStoreManager(input: {
182186
const initialMeta = meta[0].value
183187
const initialIcon = icon[0].value
184188
const [mcpEnabled, setMcpEnabled] = createSignal(false)
189+
const [instanceQueriesEnabled, setInstanceQueriesEnabled] = createSignal(false)
185190

186-
const pathQuery = useQuery(() => input.queryOptions.path(key))
191+
const pathQuery = useQuery(() => ({ ...input.queryOptions.path(key), enabled: instanceQueriesEnabled() }))
187192
const mcpQuery = useQuery(() => ({ ...input.queryOptions.mcp(key), enabled: mcpEnabled() }))
188193
const mcpResourceQuery = useQuery(() => ({ ...input.queryOptions.mcpResources(key), enabled: mcpEnabled() }))
189-
const lspQuery = useQuery(() => input.queryOptions.lsp(key))
190-
const providerQuery = useQuery(() => input.queryOptions.providers(key))
191-
const referenceQuery = useQuery(() => input.queryOptions.references(key))
194+
const lspQuery = useQuery(() => ({ ...input.queryOptions.lsp(key), enabled: instanceQueriesEnabled() }))
195+
const providerQuery = useQuery(() => ({
196+
...input.queryOptions.providers(key),
197+
enabled: instanceQueriesEnabled(),
198+
}))
199+
const referenceQuery = useQuery(() => ({
200+
...input.queryOptions.references(key),
201+
enabled: instanceQueriesEnabled(),
202+
}))
192203

193204
const child = createStore<State>({
194205
project: "",
195206
projectMeta: initialMeta,
196207
icon: initialIcon,
197208
get provider_ready() {
198-
return !providerQuery.isLoading
209+
return instanceQueriesEnabled() && !providerQuery.isLoading
199210
},
200211
get provider() {
201212
const EMPTY = { all: new Map(), connected: [], default: {} }
@@ -236,7 +247,7 @@ export function createChildStoreManager(input: {
236247
return mcpResourceQuery.isLoading ? {} : (mcpResourceQuery.data ?? {})
237248
},
238249
get lsp_ready() {
239-
return !lspQuery.isLoading
250+
return instanceQueriesEnabled() && !lspQuery.isLoading
240251
},
241252
get lsp() {
242253
return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
@@ -250,6 +261,7 @@ export function createChildStoreManager(input: {
250261
children[key] = child
251262
disposers.set(key, dispose)
252263
mcpToggles.set(key, setMcpEnabled)
264+
activationToggles.set(key, setInstanceQueriesEnabled)
253265

254266
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
255267
if (!(init instanceof Promise)) return
@@ -290,6 +302,7 @@ export function createChildStoreManager(input: {
290302
pinForOwner(key)
291303
if (options.mcp) enableMcp(directory, key, childStore)
292304
const shouldBootstrap = options.bootstrap ?? true
305+
if (shouldBootstrap) activate(key)
293306
if (shouldBootstrap && childStore[0].status === "loading") {
294307
input.onBootstrap(directory)
295308
}
@@ -301,6 +314,7 @@ export function createChildStoreManager(input: {
301314
const childStore = ensureChild(directory)
302315
if (options.mcp) enableMcp(directory, key, childStore)
303316
const shouldBootstrap = options.bootstrap ?? true
317+
if (shouldBootstrap) activate(key)
304318
if (shouldBootstrap && childStore[0].status === "loading") {
305319
input.onBootstrap(directory)
306320
}
@@ -314,6 +328,16 @@ export function createChildStoreManager(input: {
314328
if (childStore[0].status !== "loading") input.onMcp(directory, childStore[1])
315329
}
316330

331+
// Passive Home/project metadata reads must not initialize the directory.
332+
// A real directory access enables these queries once for the store lifetime.
333+
// TODO(v2): After Home switches to v2.project.list and root-filtered,
334+
// updated-time v2.session.list, remove any Home-only passive child creation.
335+
function activate(key: DirectoryKey) {
336+
if (activeDirectories.has(key)) return
337+
activeDirectories.add(key)
338+
activationToggles.get(key)?.(true)
339+
}
340+
317341
function disableMcp(directory: string) {
318342
const key = directoryKey(directory)
319343
if (!mcpDirectories.delete(key)) return
@@ -360,6 +384,7 @@ export function createChildStoreManager(input: {
360384
unpin,
361385
pinned,
362386
mcp: (directory: string) => mcpDirectories.has(directoryKey(directory)),
387+
active: (directory: string) => activeDirectories.has(directoryKey(directory)),
363388
disableMcp,
364389
disposeDirectory,
365390
runEviction,

packages/app/src/context/global-sync/event-reducer.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,22 @@ describe("applyDirectoryEvent", () => {
243243
expect(store.session_status.ses_1).toBeUndefined()
244244
})
245245

246+
test("ignores an archived session absent from a passive directory store", () => {
247+
const [store, setStore] = createStore(baseState({ session: [], sessionTotal: 0 }))
248+
249+
applyDirectoryEvent({
250+
event: { type: "session.updated", properties: { info: rootSession({ id: "missing", archived: 10 }) } },
251+
store,
252+
setStore,
253+
push() {},
254+
directory: "/tmp",
255+
loadLsp() {},
256+
})
257+
258+
expect(store.session).toEqual([])
259+
expect(store.sessionTotal).toBe(0)
260+
})
261+
246262
test("cleans session caches when deleted and decrements only root totals", () => {
247263
const cases = [
248264
{ info: rootSession({ id: "ses_1" }), expectedTotal: 1 },

packages/app/src/context/global-sync/event-reducer.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,14 @@ export function applyDirectoryEvent(input: {
146146
const info = (event.properties as { info: Session }).info
147147
const result = Binary.search(input.store.session, info.id, (s) => s.id)
148148
if (info.time.archived) {
149+
if (!result.found) break
149150
if (input.store.session[result.index]!.time.archived === info.time.archived) break
150-
if (result.found) {
151-
input.setStore(
152-
"session",
153-
produce((draft) => {
154-
draft.splice(result.index, 1)
155-
}),
156-
)
157-
}
151+
input.setStore(
152+
"session",
153+
produce((draft) => {
154+
draft.splice(result.index, 1)
155+
}),
156+
)
158157
cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo)
159158
if (info.parentID) break
160159
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { describe, expect, test } from "bun:test"
2+
import {
3+
applyHomeSessionEvent,
4+
appendHomeSessionEvent,
5+
HOME_V2_SESSION_PAGE_LIMIT,
6+
loadHomeSessionIndex,
7+
homeSessionIndexSessions,
8+
homeSessionIndexRefresh,
9+
parseHomeSessionIndex,
10+
retainHomeSessions,
11+
} from "./home-session-index"
12+
13+
const session = (input: {
14+
id: string
15+
directory?: string
16+
parentID?: string
17+
archived?: number
18+
updated?: number
19+
}) => ({
20+
id: input.id,
21+
parentID: input.parentID,
22+
projectID: "project",
23+
cost: 0,
24+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
25+
time: { created: 1, updated: input.updated ?? 1, archived: input.archived },
26+
title: input.id,
27+
location: { directory: input.directory ?? "/project" },
28+
})
29+
30+
describe("Home V2 session index", () => {
31+
test("loads the Home index with one global V2 request", async () => {
32+
const calls: unknown[] = []
33+
const result = await loadHomeSessionIndex(async (input) => {
34+
calls.push(input)
35+
return { data: { data: [session({ id: "root" })], cursor: {} } }
36+
})
37+
38+
expect(result.sessions).toHaveLength(1)
39+
expect(calls).toEqual([{ limit: HOME_V2_SESSION_PAGE_LIMIT, order: "desc" }])
40+
})
41+
42+
test("loads subsequent pages until the session index is complete", async () => {
43+
const calls: unknown[] = []
44+
const controller = new AbortController()
45+
const result = await loadHomeSessionIndex(
46+
async (input, options) => {
47+
calls.push({ input, signal: options.signal })
48+
if (!("cursor" in input)) {
49+
return {
50+
data: {
51+
data: Array.from({ length: HOME_V2_SESSION_PAGE_LIMIT }, (_, index) =>
52+
session({ id: `page-1-${index}` }),
53+
),
54+
cursor: { next: "next-page" },
55+
},
56+
}
57+
}
58+
return { data: { data: [session({ id: "page-2" })], cursor: {} } }
59+
},
60+
0,
61+
controller.signal,
62+
)
63+
64+
expect(result.sessions).toHaveLength(HOME_V2_SESSION_PAGE_LIMIT + 1)
65+
expect(calls).toEqual([
66+
{ input: { limit: HOME_V2_SESSION_PAGE_LIMIT, order: "desc" }, signal: controller.signal },
67+
{
68+
input: { limit: HOME_V2_SESSION_PAGE_LIMIT, order: "desc", cursor: "next-page" },
69+
signal: controller.signal,
70+
},
71+
])
72+
})
73+
74+
test("maps visible roots to Home session summaries", () => {
75+
const result = parseHomeSessionIndex([
76+
session({ id: "root", updated: 30 }),
77+
session({ id: "child", parentID: "root", updated: 40 }),
78+
session({ id: "archived", archived: 50, updated: 50 }),
79+
])
80+
81+
expect(result).toEqual([
82+
expect.objectContaining({
83+
id: "root",
84+
slug: "root",
85+
version: "",
86+
directory: "/project",
87+
projectID: "project",
88+
title: "root",
89+
time: { created: 1, updated: 30 },
90+
}),
91+
])
92+
})
93+
94+
test("preserves the per-directory Home retention limit", () => {
95+
const now = 10 * 60 * 60 * 1000
96+
const sessions = Array.from({ length: 80 }, (_, index) => ({
97+
...parseHomeSessionIndex([session({ id: `session-${index}`, updated: index + 1 })])[0],
98+
directory: index % 2 === 0 ? "/one" : "/two",
99+
}))
100+
101+
const retained = retainHomeSessions(sessions, 10, now)
102+
expect(retained.filter((item) => item.directory === "/one")).toHaveLength(10)
103+
expect(retained.filter((item) => item.directory === "/two")).toHaveLength(10)
104+
})
105+
106+
test("replays session events over the loaded index", () => {
107+
const initial = parseHomeSessionIndex([session({ id: "old" })])
108+
const created = { ...initial[0], id: "new", slug: "new", title: "new", time: { created: 2, updated: 2 } }
109+
110+
const afterCreate = applyHomeSessionEvent(initial, {
111+
type: "session.created",
112+
properties: { sessionID: created.id, info: created },
113+
})
114+
expect(
115+
applyHomeSessionEvent(afterCreate, {
116+
type: "session.deleted",
117+
properties: { sessionID: initial[0]!.id, info: initial[0]! },
118+
}),
119+
).toEqual([created])
120+
})
121+
122+
test("applies only events newer than the index baseline", () => {
123+
const initial = parseHomeSessionIndex([session({ id: "old" })])
124+
const stale = { ...initial[0], title: "stale" }
125+
const current = { ...initial[0], title: "current" }
126+
const first = appendHomeSessionEvent(undefined, {
127+
type: "session.updated",
128+
properties: { sessionID: stale.id, info: stale },
129+
})
130+
const events = appendHomeSessionEvent(first, {
131+
type: "session.updated",
132+
properties: { sessionID: current.id, info: current },
133+
})
134+
135+
expect(homeSessionIndexSessions({ sessions: initial, eventSequence: 1 }, events)[0]?.title).toBe("current")
136+
})
137+
138+
test("refetches after reconnect, disposal, and session moves", () => {
139+
expect(homeSessionIndexRefresh("server.connected", false)).toEqual({ connected: true, refetch: false })
140+
expect(homeSessionIndexRefresh("server.connected", true)).toEqual({ connected: true, refetch: true })
141+
expect(homeSessionIndexRefresh("global.disposed", true).refetch).toBe(true)
142+
expect(homeSessionIndexRefresh("session.next.moved", true).refetch).toBe(true)
143+
})
144+
})

0 commit comments

Comments
 (0)