diff options
Diffstat (limited to 'packages/app/src/context/global-sync')
| -rw-r--r-- | packages/app/src/context/global-sync/child-store.ts | 100 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/queue.test.ts | 46 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/queue.ts | 15 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/utils.test.ts | 19 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/utils.ts | 1 |
5 files changed, 131 insertions, 50 deletions
diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index d3b82894a..4c3c677a7 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -17,6 +17,7 @@ import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction" import { useQueries } from "@tanstack/solid-query" import { loadPathQuery, loadProvidersQuery } from "./bootstrap" import { loadLspQuery, loadMcpQuery } from "../global-sync" +import { directoryKey, type DirectoryKey } from "./utils" export function createChildStoreManager(input: { owner: Owner @@ -36,30 +37,37 @@ export function createChildStoreManager(input: { const ownerPins = new WeakMap<object, Set<string>>() const disposers = new Map<string, () => void>() + const markKey = (key: DirectoryKey) => { + if (!key) return + lifecycle.set(key, { lastAccessAt: Date.now() }) + runEviction(key) + } + const mark = (directory: string) => { - if (!directory) return - lifecycle.set(directory, { lastAccessAt: Date.now() }) - runEviction(directory) + const key = directoryKey(directory) + markKey(key) } const pin = (directory: string) => { - if (!directory) return - pins.set(directory, (pins.get(directory) ?? 0) + 1) - mark(directory) + const key = directoryKey(directory) + if (!key) return + pins.set(key, (pins.get(key) ?? 0) + 1) + markKey(key) } const unpin = (directory: string) => { - if (!directory) return - const next = (pins.get(directory) ?? 0) - 1 + const key = directoryKey(directory) + if (!key) return + const next = (pins.get(key) ?? 0) - 1 if (next > 0) { - pins.set(directory, next) + pins.set(key, next) return } - pins.delete(directory) + pins.delete(key) runEviction() } - const pinned = (directory: string) => (pins.get(directory) ?? 0) > 0 + const pinned = (directory: string) => (pins.get(directoryKey(directory)) ?? 0) > 0 const pinForOwner = (directory: string) => { const current = getOwner() @@ -81,30 +89,31 @@ export function createChildStoreManager(input: { }) } - function disposeDirectory(directory: string) { + function disposeDirectory(directory: DirectoryKey) { + const key = directory if ( !canDisposeDirectory({ - directory, - hasStore: !!children[directory], - pinned: pinned(directory), - booting: input.isBooting(directory), - loadingSessions: input.isLoadingSessions(directory), + directory: key, + hasStore: !!children[key], + pinned: pinned(key), + booting: input.isBooting(key), + loadingSessions: input.isLoadingSessions(key), }) ) { return false } - vcsCache.delete(directory) - metaCache.delete(directory) - iconCache.delete(directory) - lifecycle.delete(directory) - const dispose = disposers.get(directory) + vcsCache.delete(key) + metaCache.delete(key) + iconCache.delete(key) + lifecycle.delete(key) + const dispose = disposers.get(key) if (dispose) { dispose() - disposers.delete(directory) + disposers.delete(key) } - delete children[directory] - input.onDispose(directory) + delete children[key] + input.onDispose(key) return true } @@ -121,13 +130,14 @@ export function createChildStoreManager(input: { }).filter((directory) => directory !== skip) if (list.length === 0) return for (const directory of list) { - if (!disposeDirectory(directory)) continue + if (!disposeDirectory(directoryKey(directory))) continue } } function ensureChild(directory: string) { - if (!directory) console.error("No directory provided") - if (!children[directory]) { + const key = directoryKey(directory) + if (!key) console.error("No directory provided") + if (!children[key]) { const vcs = runWithOwner(input.owner, () => persisted( Persist.workspace(directory, "vcs", ["vcs.v1"]), @@ -136,7 +146,7 @@ export function createChildStoreManager(input: { ) if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed")) const vcsStore = vcs[0] - vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] }) + vcsCache.set(key, { store: vcsStore, setStore: vcs[1], ready: vcs[3] }) const meta = runWithOwner(input.owner, () => persisted( @@ -145,7 +155,7 @@ export function createChildStoreManager(input: { ), ) if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed")) - metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] }) + metaCache.set(key, { store: meta[0], setStore: meta[1], ready: meta[3] }) const icon = runWithOwner(input.owner, () => persisted( @@ -154,7 +164,7 @@ export function createChildStoreManager(input: { ), ) if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed")) - iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] }) + iconCache.set(key, { store: icon[0], setStore: icon[1], ready: icon[3] }) const init = () => createRoot((dispose) => { @@ -165,10 +175,10 @@ export function createChildStoreManager(input: { const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({ queries: [ - loadPathQuery(directory, sdk), - loadMcpQuery(directory, sdk), - loadLspQuery(directory, sdk), - loadProvidersQuery(directory, sdk), + loadPathQuery(key, sdk), + loadMcpQuery(key, sdk), + loadLspQuery(key, sdk), + loadProvidersQuery(key, sdk), ], })) @@ -213,13 +223,13 @@ export function createChildStoreManager(input: { message: {}, part: {}, }) - children[directory] = child - disposers.set(directory, dispose) + children[key] = child + disposers.set(key, dispose) const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => { if (!(init instanceof Promise)) return void init.then(() => { - if (children[directory] !== child) return + if (children[key] !== child) return run() }) } @@ -243,15 +253,16 @@ export function createChildStoreManager(input: { runWithOwner(input.owner, init) } - mark(directory) - const childStore = children[directory] + markKey(key) + const childStore = children[key] if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed")) return childStore } function child(directory: string, options: ChildOptions = {}) { + const key = directoryKey(directory) const childStore = ensureChild(directory) - pinForOwner(directory) + pinForOwner(key) const shouldBootstrap = options.bootstrap ?? true if (shouldBootstrap && childStore[0].status === "loading") { input.onBootstrap(directory) @@ -260,6 +271,7 @@ export function createChildStoreManager(input: { } function peek(directory: string, options: ChildOptions = {}) { + const key = directoryKey(directory) const childStore = ensureChild(directory) const shouldBootstrap = options.bootstrap ?? true if (shouldBootstrap && childStore[0].status === "loading") { @@ -269,8 +281,9 @@ export function createChildStoreManager(input: { } function projectMeta(directory: string, patch: ProjectMeta) { + const key = directoryKey(directory) const [store, setStore] = ensureChild(directory) - const cached = metaCache.get(directory) + const cached = metaCache.get(key) if (!cached) return const previous = store.projectMeta ?? {} const icon = patch.icon ? { ...previous.icon, ...patch.icon } : previous.icon @@ -286,8 +299,9 @@ export function createChildStoreManager(input: { } function projectIcon(directory: string, value: string | undefined) { + const key = directoryKey(directory) const [store, setStore] = ensureChild(directory) - const cached = iconCache.get(directory) + const cached = iconCache.get(key) if (!cached) return if (store.icon === value) return cached.setStore("value", value) diff --git a/packages/app/src/context/global-sync/queue.test.ts b/packages/app/src/context/global-sync/queue.test.ts new file mode 100644 index 000000000..c9919855e --- /dev/null +++ b/packages/app/src/context/global-sync/queue.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "bun:test" +import { createRefreshQueue } from "./queue" +import { directoryKey } from "./utils" + +const tick = () => new Promise((resolve) => setTimeout(resolve, 10)) + +describe("createRefreshQueue", () => { + test("clears queued directories by normalized key", async () => { + const calls: string[] = [] + const queue = createRefreshQueue({ + paused: () => false, + key: directoryKey, + bootstrap: async () => {}, + bootstrapInstance: (directory) => { + calls.push(directory) + }, + }) + + queue.push("C:\\tmp\\demo") + queue.clear("C:/tmp/demo") + + await tick() + + expect(calls).toEqual([]) + queue.dispose() + }) + + test("passes the original directory to bootstrapInstance", async () => { + const calls: string[] = [] + const queue = createRefreshQueue({ + paused: () => false, + key: directoryKey, + bootstrap: async () => {}, + bootstrapInstance: (directory) => { + calls.push(directory) + }, + }) + + queue.push("C:\\tmp\\demo") + + await tick() + + expect(calls).toEqual(["C:\\tmp\\demo"]) + queue.dispose() + }) +}) diff --git a/packages/app/src/context/global-sync/queue.ts b/packages/app/src/context/global-sync/queue.ts index 5c228dac0..947e31ac9 100644 --- a/packages/app/src/context/global-sync/queue.ts +++ b/packages/app/src/context/global-sync/queue.ts @@ -2,22 +2,25 @@ type QueueInput = { paused: () => boolean bootstrap: () => Promise<void> bootstrapInstance: (directory: string) => Promise<void> | void + key?: (directory: string) => string } export function createRefreshQueue(input: QueueInput) { - const queued = new Set<string>() + const queued = new Map<string, string>() let root = false let running = false let timer: ReturnType<typeof setTimeout> | undefined + const key = input.key ?? ((directory: string) => directory) + const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0)) const take = (count: number) => { if (queued.size === 0) return [] as string[] const items: string[] = [] - for (const item of queued) { - queued.delete(item) - items.push(item) + for (const [id, directory] of queued) { + queued.delete(id) + items.push(directory) if (items.length >= count) break } return items @@ -33,7 +36,7 @@ export function createRefreshQueue(input: QueueInput) { const push = (directory: string) => { if (!directory) return - queued.add(directory) + queued.set(key(directory), directory) if (input.paused()) return schedule() } @@ -73,7 +76,7 @@ export function createRefreshQueue(input: QueueInput) { push, refresh, clear(directory: string) { - queued.delete(directory) + queued.delete(key(directory)) }, dispose() { if (!timer) return diff --git a/packages/app/src/context/global-sync/utils.test.ts b/packages/app/src/context/global-sync/utils.test.ts index 6d44ac9a8..406c0f124 100644 --- a/packages/app/src/context/global-sync/utils.test.ts +++ b/packages/app/src/context/global-sync/utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import type { Agent } from "@opencode-ai/sdk/v2/client" -import { normalizeAgentList } from "./utils" +import { directoryKey, normalizeAgentList } from "./utils" const agent = (name = "build") => ({ @@ -33,3 +33,20 @@ describe("normalizeAgentList", () => { expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")]) }) }) + +describe("directoryKey", () => { + test("normalizes slashes", () => { + expect(String(directoryKey("C:\\Repos\\sst\\opencode"))).toBe("C:/Repos/sst/opencode") + expect(String(directoryKey("C:/Repos/sst/opencode"))).toBe("C:/Repos/sst/opencode") + }) + + test("preserves backslashes in posix paths", () => { + expect(String(directoryKey("/tmp/foo\\bar"))).toBe("/tmp/foo\\bar") + }) + + test("trims trailing slashes without breaking roots", () => { + expect(String(directoryKey("C:/Repos/sst/opencode/"))).toBe("C:/Repos/sst/opencode") + expect(String(directoryKey("C:/"))).toBe("C:/") + expect(String(directoryKey("/"))).toBe("/") + }) +}) diff --git a/packages/app/src/context/global-sync/utils.ts b/packages/app/src/context/global-sync/utils.ts index cac58f317..b98299088 100644 --- a/packages/app/src/context/global-sync/utils.ts +++ b/packages/app/src/context/global-sync/utils.ts @@ -1,4 +1,5 @@ import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client" +export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key" export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) |
