Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions packages/app/src/context/global-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { ProjectMeta } from "./global-sync/types"
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
import { sanitizeProject } from "./global-sync/utils"
import { formatServerError } from "@/utils/server-errors"
import { workspaceKey } from "@/pages/layout/helpers"

type GlobalStore = {
ready: boolean
Expand Down Expand Up @@ -151,39 +152,42 @@ function createGlobalSync() {

const children = createChildStoreManager({
owner,
isBooting: (directory) => booting.has(directory),
isLoadingSessions: (directory) => sessionLoads.has(directory),
isBooting: (directory) => booting.has(workspaceKey(directory)),
isLoadingSessions: (directory) => sessionLoads.has(workspaceKey(directory)),
onBootstrap: (directory) => {
void bootstrapInstance(directory)
},
onDispose: (directory) => {
const key = workspaceKey(directory)
queue.clear(directory)
sessionMeta.delete(directory)
sdkCache.delete(directory)
clearProviderRev(directory)
clearSessionPrefetchDirectory(directory)
sessionMeta.delete(key)
sdkCache.delete(key)
clearProviderRev(key)
clearSessionPrefetchDirectory(key)
},
translate: language.t,
})

const sdkFor = (directory: string) => {
const cached = sdkCache.get(directory)
const key = workspaceKey(directory)
const cached = sdkCache.get(key)
if (cached) return cached
const sdk = globalSDK.createClient({
directory,
throwOnError: true,
})
sdkCache.set(directory, sdk)
sdkCache.set(key, sdk)
return sdk
}

async function loadSessions(directory: string) {
const pending = sessionLoads.get(directory)
const key = workspaceKey(directory)
const pending = sessionLoads.get(key)
if (pending) return pending

children.pin(directory)
const [store, setStore] = children.child(directory, { bootstrap: false })
const meta = sessionMeta.get(directory)
const meta = sessionMeta.get(key)
if (meta && meta.limit >= store.limit) {
const next = trimSessions(store.session, {
limit: store.limit,
Expand Down Expand Up @@ -224,7 +228,7 @@ function createGlobalSync() {
)
setStore("session", reconcile(sessions, { key: "id" }))
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
sessionMeta.set(directory, { limit })
sessionMeta.set(key, { limit })
})
.catch((err) => {
console.error("Failed to load sessions", err)
Expand All @@ -236,23 +240,24 @@ function createGlobalSync() {
})
})

sessionLoads.set(directory, promise)
sessionLoads.set(key, promise)
promise.finally(() => {
sessionLoads.delete(directory)
sessionLoads.delete(key)
children.unpin(directory)
})
return promise
}

async function bootstrapInstance(directory: string) {
if (!directory) return
const pending = booting.get(directory)
const key = workspaceKey(directory)
const pending = booting.get(key)
if (pending) return pending

children.pin(directory)
const promise = (async () => {
const child = children.ensureChild(directory)
const cache = children.vcsCache.get(directory)
const cache = children.vcsCache.get(key)
if (!cache) return
const sdk = sdkFor(directory)
await bootstrapDirectory({
Expand All @@ -272,9 +277,9 @@ function createGlobalSync() {
})
})()

booting.set(directory, promise)
booting.set(key, promise)
promise.finally(() => {
booting.delete(directory)
booting.delete(key)
children.unpin(directory)
})
return promise
Expand All @@ -297,14 +302,15 @@ function createGlobalSync() {
})
if (event.type === "server.connected" || event.type === "global.disposed") {
if (recent) return
for (const directory of Object.keys(children.children)) {
for (const directory of children.canonicalDir.values()) {
queue.push(directory)
}
}
return
}

const existing = children.children[directory]
const key = workspaceKey(directory)
const existing = children.children[key]
if (!existing) return
children.mark(directory)
const [store, setStore] = existing
Expand All @@ -315,7 +321,7 @@ function createGlobalSync() {
setStore,
push: queue.push,
setSessionTodo,
vcsCache: children.vcsCache.get(directory),
vcsCache: children.vcsCache.get(key),
loadLsp: () => {
sdkFor(directory)
.lsp.status()
Expand Down
12 changes: 7 additions & 5 deletions packages/app/src/context/global-sync/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { State, VcsCache } from "./types"
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
import { formatServerError } from "@/utils/server-errors"
import { workspaceKey } from "@/pages/layout/helpers"

type GlobalStore = {
ready: boolean
Expand Down Expand Up @@ -58,7 +59,7 @@ function errors(list: PromiseSettledResult<unknown>[]) {
const providerRev = new Map<string, number>()

export function clearProviderRev(directory: string) {
providerRev.delete(directory)
providerRev.delete(workspaceKey(directory))
}

function runAll(list: Array<() => Promise<unknown>>) {
Expand Down Expand Up @@ -339,16 +340,17 @@ export async function bootstrapDirectory(input: {

if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")

const rev = (providerRev.get(input.directory) ?? 0) + 1
providerRev.set(input.directory, rev)
const wk = workspaceKey(input.directory)
const rev = (providerRev.get(wk) ?? 0) + 1
providerRev.set(wk, rev)
void retry(() => input.sdk.provider.list())
.then((x) => {
if (providerRev.get(input.directory) !== rev) return
if (providerRev.get(wk) !== rev) return
input.setStore("provider", normalizeProviderList(x.data!))
input.setStore("provider_ready", true)
})
.catch((err) => {
if (providerRev.get(input.directory) !== rev) return
if (providerRev.get(wk) !== rev) return
console.error("Failed to refresh provider list", err)
const project = getFilename(input.directory)
showToast({
Expand Down
71 changes: 42 additions & 29 deletions packages/app/src/context/global-sync/child-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type VcsCache,
} from "./types"
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
import { workspaceKey } from "@/pages/layout/helpers"

export function createChildStoreManager(input: {
owner: Owner
Expand All @@ -31,31 +32,35 @@ export function createChildStoreManager(input: {
const pins = new Map<string, number>()
const ownerPins = new WeakMap<object, Set<string>>()
const disposers = new Map<string, () => void>()
const canonicalDir = new Map<string, string>()

const mark = (directory: string) => {
if (!directory) return
lifecycle.set(directory, { lastAccessAt: Date.now() })
runEviction(directory)
const k = workspaceKey(directory)
lifecycle.set(k, { lastAccessAt: Date.now() })
runEviction(k)
}

const pin = (directory: string) => {
if (!directory) return
pins.set(directory, (pins.get(directory) ?? 0) + 1)
const k = workspaceKey(directory)
pins.set(k, (pins.get(k) ?? 0) + 1)
mark(directory)
}

const unpin = (directory: string) => {
if (!directory) return
const next = (pins.get(directory) ?? 0) - 1
const k = workspaceKey(directory)
const next = (pins.get(k) ?? 0) - 1
if (next > 0) {
pins.set(directory, next)
pins.set(k, next)
return
}
pins.delete(directory)
pins.delete(k)
runEviction()
}

const pinned = (directory: string) => (pins.get(directory) ?? 0) > 0
const pinned = (directory: string) => (pins.get(workspaceKey(directory)) ?? 0) > 0

const pinForOwner = (directory: string) => {
const current = getOwner()
Expand All @@ -78,29 +83,32 @@ export function createChildStoreManager(input: {
}

function disposeDirectory(directory: string) {
const key = workspaceKey(directory)
const orig = canonicalDir.get(key) ?? directory
if (
!canDisposeDirectory({
directory,
hasStore: !!children[directory],
directory: orig,
hasStore: !!children[key],
pinned: pinned(directory),
booting: input.isBooting(directory),
loadingSessions: input.isLoadingSessions(directory),
booting: input.isBooting(orig),
loadingSessions: input.isLoadingSessions(orig),
})
) {
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)
canonicalDir.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(orig)
return true
}

Expand All @@ -123,7 +131,11 @@ export function createChildStoreManager(input: {

function ensureChild(directory: string) {
if (!directory) console.error("No directory provided")
if (!children[directory]) {
const key = workspaceKey(directory)
// Prefer backslash format on Windows for API calls that need server-side exact match.
// If backslash path is available, always use it as the canonical form.
if (!canonicalDir.has(key) || directory.includes("\\")) canonicalDir.set(key, directory)
if (!children[key]) {
const vcs = runWithOwner(input.owner, () =>
persisted(
Persist.workspace(directory, "vcs", ["vcs.v1"]),
Expand All @@ -132,7 +144,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(
Expand All @@ -141,7 +153,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(
Expand All @@ -150,7 +162,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) => {
Expand Down Expand Up @@ -183,13 +195,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()
})
}
Expand All @@ -214,7 +226,7 @@ export function createChildStoreManager(input: {
runWithOwner(input.owner, init)
}
mark(directory)
const childStore = children[directory]
const childStore = children[key]
if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed"))
return childStore
}
Expand All @@ -240,7 +252,7 @@ export function createChildStoreManager(input: {

function projectMeta(directory: string, patch: ProjectMeta) {
const [store, setStore] = ensureChild(directory)
const cached = metaCache.get(directory)
const cached = metaCache.get(workspaceKey(directory))
if (!cached) return
const previous = store.projectMeta ?? {}
const icon = patch.icon ? { ...(previous.icon ?? {}), ...patch.icon } : previous.icon
Expand All @@ -257,7 +269,7 @@ export function createChildStoreManager(input: {

function projectIcon(directory: string, value: string | undefined) {
const [store, setStore] = ensureChild(directory)
const cached = iconCache.get(directory)
const cached = iconCache.get(workspaceKey(directory))
if (!cached) return
if (store.icon === value) return
cached.setStore("value", value)
Expand All @@ -266,6 +278,7 @@ export function createChildStoreManager(input: {

return {
children,
canonicalDir,
ensureChild,
child,
peek,
Expand Down
8 changes: 6 additions & 2 deletions packages/app/src/context/global-sync/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ type QueueInput = {
bootstrapInstance: (directory: string) => Promise<void> | void
}

function normalizeKey(directory: string) {
return directory.replaceAll("\\", "/").replace(/\/+$/, "")
}

export function createRefreshQueue(input: QueueInput) {
const queued = new Set<string>()
let root = false
Expand Down Expand Up @@ -33,7 +37,7 @@ export function createRefreshQueue(input: QueueInput) {

const push = (directory: string) => {
if (!directory) return
queued.add(directory)
queued.add(normalizeKey(directory))
if (input.paused()) return
schedule()
}
Expand Down Expand Up @@ -72,7 +76,7 @@ export function createRefreshQueue(input: QueueInput) {
push,
refresh,
clear(directory: string) {
queued.delete(directory)
queued.delete(normalizeKey(directory))
},
dispose() {
if (!timer) return
Expand Down
Loading
Loading