|
|
@@ -6,6 +6,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
|
|
|
import { useGlobalSync } from "./global-sync"
|
|
|
import { useSDK } from "./sdk"
|
|
|
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
|
|
+import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
|
|
|
|
|
function sortParts(parts: Part[]) {
|
|
|
return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
|
|
|
@@ -108,6 +109,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
const inflight = new Map<string, Promise<void>>()
|
|
|
const inflightDiff = new Map<string, Promise<void>>()
|
|
|
const inflightTodo = new Map<string, Promise<void>>()
|
|
|
+ const maxDirs = 30
|
|
|
+ const seen = new Map<string, Set<string>>()
|
|
|
const [meta, setMeta] = createStore({
|
|
|
limit: {} as Record<string, number>,
|
|
|
complete: {} as Record<string, boolean>,
|
|
|
@@ -121,6 +124,62 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
return undefined
|
|
|
}
|
|
|
|
|
|
+ const seenFor = (directory: string) => {
|
|
|
+ const existing = seen.get(directory)
|
|
|
+ if (existing) {
|
|
|
+ seen.delete(directory)
|
|
|
+ seen.set(directory, existing)
|
|
|
+ return existing
|
|
|
+ }
|
|
|
+ const created = new Set<string>()
|
|
|
+ seen.set(directory, created)
|
|
|
+ while (seen.size > maxDirs) {
|
|
|
+ const first = seen.keys().next().value
|
|
|
+ if (!first) break
|
|
|
+ const stale = [...(seen.get(first) ?? [])]
|
|
|
+ seen.delete(first)
|
|
|
+ const [, setStore] = globalSync.child(first, { bootstrap: false })
|
|
|
+ evict(first, setStore, stale)
|
|
|
+ }
|
|
|
+ return created
|
|
|
+ }
|
|
|
+
|
|
|
+ const clearMeta = (directory: string, sessionIDs: string[]) => {
|
|
|
+ if (sessionIDs.length === 0) return
|
|
|
+ setMeta(
|
|
|
+ produce((draft) => {
|
|
|
+ for (const sessionID of sessionIDs) {
|
|
|
+ const key = keyFor(directory, sessionID)
|
|
|
+ delete draft.limit[key]
|
|
|
+ delete draft.complete[key]
|
|
|
+ delete draft.loading[key]
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ const evict = (directory: string, setStore: Setter, sessionIDs: string[]) => {
|
|
|
+ if (sessionIDs.length === 0) return
|
|
|
+ for (const sessionID of sessionIDs) {
|
|
|
+ globalSync.todo.set(sessionID, undefined)
|
|
|
+ }
|
|
|
+ setStore(
|
|
|
+ produce((draft) => {
|
|
|
+ dropSessionCaches(draft, sessionIDs)
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ clearMeta(directory, sessionIDs)
|
|
|
+ }
|
|
|
+
|
|
|
+ const touch = (directory: string, setStore: Setter, sessionID: string) => {
|
|
|
+ const stale = pickSessionCacheEvictions({
|
|
|
+ seen: seenFor(directory),
|
|
|
+ keep: sessionID,
|
|
|
+ limit: SESSION_CACHE_LIMIT,
|
|
|
+ })
|
|
|
+ evict(directory, setStore, stale)
|
|
|
+ }
|
|
|
+
|
|
|
const fetchMessages = async (input: { client: typeof sdk.client; sessionID: string; limit: number }) => {
|
|
|
const messages = await retry(() =>
|
|
|
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }),
|
|
|
@@ -135,6 +194,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ const tracked = (directory: string, sessionID: string) => seen.get(directory)?.has(sessionID) ?? false
|
|
|
+
|
|
|
const loadMessages = async (input: {
|
|
|
directory: string
|
|
|
client: typeof sdk.client
|
|
|
@@ -148,6 +209,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
setMeta("loading", key, true)
|
|
|
await fetchMessages(input)
|
|
|
.then((next) => {
|
|
|
+ if (!tracked(input.directory, input.sessionID)) return
|
|
|
batch(() => {
|
|
|
input.setStore("message", input.sessionID, reconcile(next.session, { key: "id" }))
|
|
|
for (const p of next.part) {
|
|
|
@@ -158,6 +220,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
})
|
|
|
})
|
|
|
.finally(() => {
|
|
|
+ if (!tracked(input.directory, input.sessionID)) return
|
|
|
setMeta("loading", key, false)
|
|
|
})
|
|
|
}
|
|
|
@@ -224,11 +287,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
const key = keyFor(directory, sessionID)
|
|
|
const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
|
|
|
|
|
|
+ touch(directory, setStore, sessionID)
|
|
|
+
|
|
|
+ if (store.message[sessionID] !== undefined && hasSession && meta.limit[key] !== undefined) return
|
|
|
+
|
|
|
const limit = meta.limit[key] ?? messagePageSize
|
|
|
|
|
|
const sessionReq = hasSession
|
|
|
? Promise.resolve()
|
|
|
: retry(() => client.session.get({ sessionID })).then((session) => {
|
|
|
+ if (!tracked(directory, sessionID)) return
|
|
|
const data = session.data
|
|
|
if (!data) return
|
|
|
setStore(
|
|
|
@@ -258,11 +326,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
const directory = sdk.directory
|
|
|
const client = sdk.client
|
|
|
const [store, setStore] = globalSync.child(directory)
|
|
|
+ touch(directory, setStore, sessionID)
|
|
|
if (store.session_diff[sessionID] !== undefined) return
|
|
|
|
|
|
const key = keyFor(directory, sessionID)
|
|
|
return runInflight(inflightDiff, key, () =>
|
|
|
retry(() => client.session.diff({ sessionID })).then((diff) => {
|
|
|
+ if (!tracked(directory, sessionID)) return
|
|
|
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
|
|
|
}),
|
|
|
)
|
|
|
@@ -271,6 +341,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
const directory = sdk.directory
|
|
|
const client = sdk.client
|
|
|
const [store, setStore] = globalSync.child(directory)
|
|
|
+ touch(directory, setStore, sessionID)
|
|
|
const existing = store.todo[sessionID]
|
|
|
const cached = globalSync.data.session_todo[sessionID]
|
|
|
if (existing !== undefined) {
|
|
|
@@ -287,6 +358,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
const key = keyFor(directory, sessionID)
|
|
|
return runInflight(inflightTodo, key, () =>
|
|
|
retry(() => client.session.todo({ sessionID })).then((todo) => {
|
|
|
+ if (!tracked(directory, sessionID)) return
|
|
|
const list = todo.data ?? []
|
|
|
setStore("todo", sessionID, reconcile(list, { key: "id" }))
|
|
|
globalSync.todo.set(sessionID, list)
|
|
|
@@ -310,6 +382,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
const directory = sdk.directory
|
|
|
const client = sdk.client
|
|
|
const [, setStore] = globalSync.child(directory)
|
|
|
+ touch(directory, setStore, sessionID)
|
|
|
const key = keyFor(directory, sessionID)
|
|
|
const step = count ?? messagePageSize
|
|
|
if (meta.loading[key]) return
|
|
|
@@ -325,6 +398,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
})
|
|
|
},
|
|
|
},
|
|
|
+ evict(sessionID: string, directory = sdk.directory) {
|
|
|
+ const [, setStore] = globalSync.child(directory)
|
|
|
+ seenFor(directory).delete(sessionID)
|
|
|
+ evict(directory, setStore, [sessionID])
|
|
|
+ },
|
|
|
fetch: async (count = 10) => {
|
|
|
const directory = sdk.directory
|
|
|
const client = sdk.client
|