Skip to content

Commit be9b2ba

Browse files
committed
feat(app): cache session-scoped stores, optional context gating
1 parent c949e5b commit be9b2ba

File tree

6 files changed

+467
-229
lines changed

6 files changed

+467
-229
lines changed

packages/app/src/app.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,16 @@ export function AppInterface() {
108108
<Route path="/" component={() => <Navigate href="session" />} />
109109
<Route
110110
path="/session/:id?"
111-
component={(p) => (
112-
<Show when={p.params.id ?? "new"} keyed>
113-
<TerminalProvider>
114-
<FileProvider>
115-
<PromptProvider>
116-
<Suspense fallback={<Loading />}>
117-
<Session />
118-
</Suspense>
119-
</PromptProvider>
120-
</FileProvider>
121-
</TerminalProvider>
122-
</Show>
111+
component={() => (
112+
<TerminalProvider>
113+
<FileProvider>
114+
<PromptProvider>
115+
<Suspense fallback={<Loading />}>
116+
<Session />
117+
</Suspense>
118+
</PromptProvider>
119+
</FileProvider>
120+
</TerminalProvider>
123121
)}
124122
/>
125123
</Route>

packages/app/src/context/file.tsx

Lines changed: 142 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createEffect, createMemo, onCleanup } from "solid-js"
1+
import { createEffect, createMemo, createRoot, onCleanup } from "solid-js"
22
import { createStore, produce } from "solid-js/store"
33
import { createSimpleContext } from "@opencode-ai/ui/context"
44
import type { FileContent } from "@opencode-ai/sdk/v2"
@@ -82,8 +82,106 @@ function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange {
8282
}
8383
}
8484

85+
const WORKSPACE_KEY = "__workspace__"
86+
const MAX_FILE_VIEW_SESSIONS = 20
87+
const MAX_VIEW_FILES = 500
88+
89+
type ViewSession = ReturnType<typeof createViewSession>
90+
91+
type ViewCacheEntry = {
92+
value: ViewSession
93+
dispose: VoidFunction
94+
}
95+
96+
function createViewSession(dir: string, id: string | undefined) {
97+
const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1`
98+
99+
const [view, setView, _, ready] = persisted(
100+
Persist.scoped(dir, id, "file-view", [legacyViewKey]),
101+
createStore<{
102+
file: Record<string, FileViewState>
103+
}>({
104+
file: {},
105+
}),
106+
)
107+
108+
const meta = { pruned: false }
109+
110+
const pruneView = (keep?: string) => {
111+
const keys = Object.keys(view.file)
112+
if (keys.length <= MAX_VIEW_FILES) return
113+
114+
const drop = keys.filter((key) => key !== keep).slice(0, keys.length - MAX_VIEW_FILES)
115+
if (drop.length === 0) return
116+
117+
setView(
118+
produce((draft) => {
119+
for (const key of drop) {
120+
delete draft.file[key]
121+
}
122+
}),
123+
)
124+
}
125+
126+
createEffect(() => {
127+
if (!ready()) return
128+
if (meta.pruned) return
129+
meta.pruned = true
130+
pruneView()
131+
})
132+
133+
const scrollTop = (path: string) => view.file[path]?.scrollTop
134+
const scrollLeft = (path: string) => view.file[path]?.scrollLeft
135+
const selectedLines = (path: string) => view.file[path]?.selectedLines
136+
137+
const setScrollTop = (path: string, top: number) => {
138+
setView("file", path, (current) => {
139+
if (current?.scrollTop === top) return current
140+
return {
141+
...(current ?? {}),
142+
scrollTop: top,
143+
}
144+
})
145+
pruneView(path)
146+
}
147+
148+
const setScrollLeft = (path: string, left: number) => {
149+
setView("file", path, (current) => {
150+
if (current?.scrollLeft === left) return current
151+
return {
152+
...(current ?? {}),
153+
scrollLeft: left,
154+
}
155+
})
156+
pruneView(path)
157+
}
158+
159+
const setSelectedLines = (path: string, range: SelectedLineRange | null) => {
160+
const next = range ? normalizeSelectedLines(range) : null
161+
setView("file", path, (current) => {
162+
if (current?.selectedLines === next) return current
163+
return {
164+
...(current ?? {}),
165+
selectedLines: next,
166+
}
167+
})
168+
pruneView(path)
169+
}
170+
171+
return {
172+
ready,
173+
scrollTop,
174+
scrollLeft,
175+
selectedLines,
176+
setScrollTop,
177+
setScrollLeft,
178+
setSelectedLines,
179+
}
180+
}
181+
85182
export const { use: useFile, provider: FileProvider } = createSimpleContext({
86183
name: "File",
184+
gate: false,
87185
init: () => {
88186
const sdk = useSDK()
89187
const sync = useSync()
@@ -134,42 +232,45 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
134232
file: {},
135233
})
136234

137-
const legacyViewKey = createMemo(() => `${params.dir}/file${params.id ? "/" + params.id : ""}.v1`)
235+
const viewCache = new Map<string, ViewCacheEntry>()
138236

139-
const [view, setView, _, ready] = persisted(
140-
Persist.scoped(params.dir!, params.id, "file-view", [legacyViewKey()]),
141-
createStore<{
142-
file: Record<string, FileViewState>
143-
}>({
144-
file: {},
145-
}),
146-
)
237+
const disposeViews = () => {
238+
for (const entry of viewCache.values()) {
239+
entry.dispose()
240+
}
241+
viewCache.clear()
242+
}
147243

148-
const MAX_VIEW_FILES = 500
149-
const viewMeta = { pruned: false }
244+
const pruneViews = () => {
245+
while (viewCache.size > MAX_FILE_VIEW_SESSIONS) {
246+
const first = viewCache.keys().next().value
247+
if (!first) return
248+
const entry = viewCache.get(first)
249+
entry?.dispose()
250+
viewCache.delete(first)
251+
}
252+
}
150253

151-
const pruneView = (keep?: string) => {
152-
const keys = Object.keys(view.file)
153-
if (keys.length <= MAX_VIEW_FILES) return
254+
const loadView = (dir: string, id: string | undefined) => {
255+
const key = `${dir}:${id ?? WORKSPACE_KEY}`
256+
const existing = viewCache.get(key)
257+
if (existing) {
258+
viewCache.delete(key)
259+
viewCache.set(key, existing)
260+
return existing.value
261+
}
154262

155-
const drop = keys.filter((key) => key !== keep).slice(0, keys.length - MAX_VIEW_FILES)
156-
if (drop.length === 0) return
263+
const entry = createRoot((dispose) => ({
264+
value: createViewSession(dir, id),
265+
dispose,
266+
}))
157267

158-
setView(
159-
produce((draft) => {
160-
for (const key of drop) {
161-
delete draft.file[key]
162-
}
163-
}),
164-
)
268+
viewCache.set(key, entry)
269+
pruneViews()
270+
return entry.value
165271
}
166272

167-
createEffect(() => {
168-
if (!ready()) return
169-
if (viewMeta.pruned) return
170-
viewMeta.pruned = true
171-
pruneView()
172-
})
273+
const view = createMemo(() => loadView(params.dir!, params.id))
173274

174275
function ensure(path: string) {
175276
if (!path) return
@@ -246,51 +347,32 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
246347

247348
const get = (input: string) => store.file[normalize(input)]
248349

249-
const scrollTop = (input: string) => view.file[normalize(input)]?.scrollTop
250-
const scrollLeft = (input: string) => view.file[normalize(input)]?.scrollLeft
251-
const selectedLines = (input: string) => view.file[normalize(input)]?.selectedLines
350+
const scrollTop = (input: string) => view().scrollTop(normalize(input))
351+
const scrollLeft = (input: string) => view().scrollLeft(normalize(input))
352+
const selectedLines = (input: string) => view().selectedLines(normalize(input))
252353

253354
const setScrollTop = (input: string, top: number) => {
254355
const path = normalize(input)
255-
setView("file", path, (current) => {
256-
if (current?.scrollTop === top) return current
257-
return {
258-
...(current ?? {}),
259-
scrollTop: top,
260-
}
261-
})
262-
pruneView(path)
356+
view().setScrollTop(path, top)
263357
}
264358

265359
const setScrollLeft = (input: string, left: number) => {
266360
const path = normalize(input)
267-
setView("file", path, (current) => {
268-
if (current?.scrollLeft === left) return current
269-
return {
270-
...(current ?? {}),
271-
scrollLeft: left,
272-
}
273-
})
274-
pruneView(path)
361+
view().setScrollLeft(path, left)
275362
}
276363

277364
const setSelectedLines = (input: string, range: SelectedLineRange | null) => {
278365
const path = normalize(input)
279-
const next = range ? normalizeSelectedLines(range) : null
280-
setView("file", path, (current) => {
281-
if (current?.selectedLines === next) return current
282-
return {
283-
...(current ?? {}),
284-
selectedLines: next,
285-
}
286-
})
287-
pruneView(path)
366+
view().setSelectedLines(path, range)
288367
}
289368

290-
onCleanup(() => stop())
369+
onCleanup(() => {
370+
stop()
371+
disposeViews()
372+
})
291373

292374
return {
293-
ready,
375+
ready: () => view().ready(),
294376
normalize,
295377
tab,
296378
pathFromTab,

0 commit comments

Comments
 (0)