refactor(app): extract refcount utility and clean up server sdk context (#29155)
This commit is contained in:
parent
03bb53c389
commit
9495ecd536
5 changed files with 266 additions and 280 deletions
|
|
@ -8,10 +8,11 @@ import {
|
||||||
getSessionPrefetchPromise,
|
getSessionPrefetchPromise,
|
||||||
setSessionPrefetch,
|
setSessionPrefetch,
|
||||||
} from "./global-sync/session-prefetch"
|
} from "./global-sync/session-prefetch"
|
||||||
import { useServerSync } from "./server-sync"
|
import { createServerSyncContext, useServerSync } from "./server-sync"
|
||||||
import type { Message, OpencodeClient, Part } from "@opencode-ai/sdk/v2/client"
|
import type { Message, OpencodeClient, Part } from "@opencode-ai/sdk/v2/client"
|
||||||
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
||||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||||
|
import { useServerSDK } from "./server-sdk"
|
||||||
|
|
||||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||||
|
|
||||||
|
|
@ -164,8 +165,9 @@ function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: Opti
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createDirSyncContext = (client: OpencodeClient, directory: string) => {
|
export const createDirSyncContext = (directory: string, serverSync: ReturnType<typeof createServerSyncContext>) => {
|
||||||
const serverSync = useServerSync()
|
const serverSDK = useServerSDK()
|
||||||
|
const client = serverSDK.createClient({ directory, throwOnError: true })
|
||||||
|
|
||||||
type Child = ReturnType<(typeof serverSync)["child"]>
|
type Child = ReturnType<(typeof serverSync)["child"]>
|
||||||
type Setter = Child[1]
|
type Setter = Child[1]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
init: (props: { directory: string }) => {
|
init: (props: { directory: string }) => {
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
|
|
||||||
return serverSDK.createDirSyncContext(props.directory)
|
return serverSDK.createDirSdkContext(props.directory)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,272 +6,255 @@ import { batch, onCleanup, onMount } from "solid-js"
|
||||||
import { createSdkForServer } from "@/utils/server"
|
import { createSdkForServer } from "@/utils/server"
|
||||||
import { useLanguage } from "./language"
|
import { useLanguage } from "./language"
|
||||||
import { usePlatform } from "./platform"
|
import { usePlatform } from "./platform"
|
||||||
import { useServer } from "./server"
|
import { ServerConnection, useServer } from "./server"
|
||||||
|
import { createRefCountMap } from "@/utils/refcount"
|
||||||
|
|
||||||
const isAbortError = (error: unknown) =>
|
const isAbortError = (error: unknown) =>
|
||||||
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
|
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
|
||||||
|
|
||||||
export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleContext({
|
function createServerSdkContext(server: ServerConnection.Any) {
|
||||||
name: "GlobalSDK",
|
const platform = usePlatform()
|
||||||
init: () => {
|
const abort = new AbortController()
|
||||||
const language = useLanguage()
|
|
||||||
const server = useServer()
|
|
||||||
const platform = usePlatform()
|
|
||||||
const abort = new AbortController()
|
|
||||||
|
|
||||||
const eventFetch = (() => {
|
const eventFetch = (() => {
|
||||||
if (!platform.fetch || !server.current) return
|
if (!platform.fetch || !server) return
|
||||||
try {
|
try {
|
||||||
const url = new URL(server.current.http.url)
|
const url = new URL(server.http.url)
|
||||||
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1"
|
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1"
|
||||||
if (url.protocol === "http:" && !loopback) return platform.fetch
|
if (url.protocol === "http:" && !loopback) return platform.fetch
|
||||||
} catch {
|
} catch {
|
||||||
return
|
return
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const currentServer = server.current
|
|
||||||
if (!currentServer) throw new Error(language.t("error.serverSDK.noServerAvailable"))
|
|
||||||
|
|
||||||
const eventSdk = createSdkForServer({
|
|
||||||
signal: abort.signal,
|
|
||||||
fetch: eventFetch,
|
|
||||||
server: currentServer.http,
|
|
||||||
})
|
|
||||||
const emitter = createGlobalEmitter<{
|
|
||||||
[key: string]: Event
|
|
||||||
}>()
|
|
||||||
|
|
||||||
type Queued = { directory: string; payload: Event }
|
|
||||||
const FLUSH_FRAME_MS = 16
|
|
||||||
const STREAM_YIELD_MS = 8
|
|
||||||
const RECONNECT_DELAY_MS = 250
|
|
||||||
|
|
||||||
let queue: Queued[] = []
|
|
||||||
let buffer: Queued[] = []
|
|
||||||
const coalesced = new Map<string, number>()
|
|
||||||
const staleDeltas = new Set<string>()
|
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined
|
|
||||||
let last = 0
|
|
||||||
|
|
||||||
const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
|
|
||||||
|
|
||||||
const key = (directory: string, payload: Event) => {
|
|
||||||
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
|
|
||||||
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
|
|
||||||
if (payload.type === "message.part.updated") {
|
|
||||||
const part = payload.properties.part
|
|
||||||
return `message.part.updated:${directory}:${part.messageID}:${part.id}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
const flush = () => {
|
const eventSdk = createSdkForServer({
|
||||||
if (timer) clearTimeout(timer)
|
signal: abort.signal,
|
||||||
timer = undefined
|
fetch: eventFetch,
|
||||||
|
server: server.http,
|
||||||
|
})
|
||||||
|
const emitter = createGlobalEmitter<{
|
||||||
|
[key: string]: Event
|
||||||
|
}>()
|
||||||
|
|
||||||
if (queue.length === 0) return
|
type Queued = { directory: string; payload: Event }
|
||||||
|
const FLUSH_FRAME_MS = 16
|
||||||
|
const STREAM_YIELD_MS = 8
|
||||||
|
const RECONNECT_DELAY_MS = 250
|
||||||
|
|
||||||
const events = queue
|
let queue: Queued[] = []
|
||||||
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
|
let buffer: Queued[] = []
|
||||||
queue = buffer
|
const coalesced = new Map<string, number>()
|
||||||
buffer = events
|
const staleDeltas = new Set<string>()
|
||||||
queue.length = 0
|
let timer: ReturnType<typeof setTimeout> | undefined
|
||||||
coalesced.clear()
|
let last = 0
|
||||||
staleDeltas.clear()
|
|
||||||
|
|
||||||
last = Date.now()
|
const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
|
||||||
batch(() => {
|
|
||||||
for (const event of events) {
|
const key = (directory: string, payload: Event) => {
|
||||||
if (skip && event.payload.type === "message.part.delta") {
|
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
|
||||||
const props = event.payload.properties
|
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
|
||||||
if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue
|
if (payload.type === "message.part.updated") {
|
||||||
}
|
const part = payload.properties.part
|
||||||
emitter.emit(event.directory, event.payload)
|
return `message.part.updated:${directory}:${part.messageID}:${part.id}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
timer = undefined
|
||||||
|
|
||||||
|
if (queue.length === 0) return
|
||||||
|
|
||||||
|
const events = queue
|
||||||
|
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
|
||||||
|
queue = buffer
|
||||||
|
buffer = events
|
||||||
|
queue.length = 0
|
||||||
|
coalesced.clear()
|
||||||
|
staleDeltas.clear()
|
||||||
|
|
||||||
|
last = Date.now()
|
||||||
|
batch(() => {
|
||||||
|
for (const event of events) {
|
||||||
|
if (skip && event.payload.type === "message.part.delta") {
|
||||||
|
const props = event.payload.properties
|
||||||
|
if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue
|
||||||
}
|
}
|
||||||
})
|
emitter.emit(event.directory, event.payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
buffer.length = 0
|
buffer.length = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const schedule = () => {
|
const schedule = () => {
|
||||||
if (timer) return
|
if (timer) return
|
||||||
const elapsed = Date.now() - last
|
const elapsed = Date.now() - last
|
||||||
timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed))
|
timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed))
|
||||||
}
|
}
|
||||||
|
|
||||||
let streamErrorLogged = false
|
let streamErrorLogged = false
|
||||||
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||||
const aborted = isAbortError
|
const aborted = isAbortError
|
||||||
|
|
||||||
let attempt: AbortController | undefined
|
let attempt: AbortController | undefined
|
||||||
let run: Promise<void> | undefined
|
let run: Promise<void> | undefined
|
||||||
let started = false
|
let started = false
|
||||||
const HEARTBEAT_TIMEOUT_MS = 15_000
|
const HEARTBEAT_TIMEOUT_MS = 15_000
|
||||||
let lastEventAt = Date.now()
|
let lastEventAt = Date.now()
|
||||||
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
||||||
const resetHeartbeat = () => {
|
const resetHeartbeat = () => {
|
||||||
lastEventAt = Date.now()
|
lastEventAt = Date.now()
|
||||||
if (heartbeat) clearTimeout(heartbeat)
|
if (heartbeat) clearTimeout(heartbeat)
|
||||||
heartbeat = setTimeout(() => {
|
heartbeat = setTimeout(() => {
|
||||||
attempt?.abort()
|
attempt?.abort()
|
||||||
}, HEARTBEAT_TIMEOUT_MS)
|
}, HEARTBEAT_TIMEOUT_MS)
|
||||||
}
|
}
|
||||||
const clearHeartbeat = () => {
|
const clearHeartbeat = () => {
|
||||||
if (!heartbeat) return
|
if (!heartbeat) return
|
||||||
clearTimeout(heartbeat)
|
clearTimeout(heartbeat)
|
||||||
heartbeat = undefined
|
heartbeat = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = () => {
|
const start = () => {
|
||||||
if (started) return run
|
if (started) return run
|
||||||
started = true
|
started = true
|
||||||
run = (async () => {
|
run = (async () => {
|
||||||
// oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
|
// oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
|
||||||
while (!abort.signal.aborted && started) {
|
while (!abort.signal.aborted && started) {
|
||||||
attempt = new AbortController()
|
attempt = new AbortController()
|
||||||
lastEventAt = Date.now()
|
lastEventAt = Date.now()
|
||||||
const onAbort = () => {
|
const onAbort = () => {
|
||||||
attempt?.abort()
|
attempt?.abort()
|
||||||
}
|
}
|
||||||
abort.signal.addEventListener("abort", onAbort)
|
abort.signal.addEventListener("abort", onAbort)
|
||||||
try {
|
try {
|
||||||
const events = await eventSdk.global.event({
|
const events = await eventSdk.global.event({
|
||||||
signal: attempt.signal,
|
signal: attempt.signal,
|
||||||
onSseError: (error) => {
|
onSseError: (error) => {
|
||||||
if (aborted(error)) return
|
if (aborted(error)) return
|
||||||
if (streamErrorLogged) return
|
if (streamErrorLogged) return
|
||||||
streamErrorLogged = true
|
|
||||||
console.error("[global-sdk] event stream error", {
|
|
||||||
url: currentServer.http.url,
|
|
||||||
fetch: eventFetch ? "platform" : "webview",
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
let yielded = Date.now()
|
|
||||||
resetHeartbeat()
|
|
||||||
for await (const event of events.stream) {
|
|
||||||
resetHeartbeat()
|
|
||||||
streamErrorLogged = false
|
|
||||||
const directory = event.directory ?? "global"
|
|
||||||
if (event.payload.type === "sync") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = event.payload as Event
|
|
||||||
|
|
||||||
const k = key(directory, payload)
|
|
||||||
if (k) {
|
|
||||||
const i = coalesced.get(k)
|
|
||||||
if (i !== undefined) {
|
|
||||||
queue[i] = { directory, payload }
|
|
||||||
if (payload.type === "message.part.updated") {
|
|
||||||
const part = payload.properties.part
|
|
||||||
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
coalesced.set(k, queue.length)
|
|
||||||
}
|
|
||||||
queue.push({ directory, payload })
|
|
||||||
schedule()
|
|
||||||
|
|
||||||
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
|
||||||
yielded = Date.now()
|
|
||||||
await wait(0)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (!aborted(error) && !streamErrorLogged) {
|
|
||||||
streamErrorLogged = true
|
streamErrorLogged = true
|
||||||
console.error("[global-sdk] event stream failed", {
|
console.error("[global-sdk] event stream error", {
|
||||||
url: currentServer.http.url,
|
url: server.http.url,
|
||||||
fetch: eventFetch ? "platform" : "webview",
|
fetch: eventFetch ? "platform" : "webview",
|
||||||
error,
|
error,
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
let yielded = Date.now()
|
||||||
|
resetHeartbeat()
|
||||||
|
for await (const event of events.stream) {
|
||||||
|
resetHeartbeat()
|
||||||
|
streamErrorLogged = false
|
||||||
|
const directory = event.directory ?? "global"
|
||||||
|
if (event.payload.type === "sync") {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
abort.signal.removeEventListener("abort", onAbort)
|
const payload = event.payload as Event
|
||||||
attempt = undefined
|
|
||||||
clearHeartbeat()
|
const k = key(directory, payload)
|
||||||
|
if (k) {
|
||||||
|
const i = coalesced.get(k)
|
||||||
|
if (i !== undefined) {
|
||||||
|
queue[i] = { directory, payload }
|
||||||
|
if (payload.type === "message.part.updated") {
|
||||||
|
const part = payload.properties.part
|
||||||
|
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
coalesced.set(k, queue.length)
|
||||||
|
}
|
||||||
|
queue.push({ directory, payload })
|
||||||
|
schedule()
|
||||||
|
|
||||||
|
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
||||||
|
yielded = Date.now()
|
||||||
|
await wait(0)
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
if (abort.signal.aborted || !started) return
|
if (!aborted(error) && !streamErrorLogged) {
|
||||||
await wait(RECONNECT_DELAY_MS)
|
streamErrorLogged = true
|
||||||
|
console.error("[global-sdk] event stream failed", {
|
||||||
|
url: server.http.url,
|
||||||
|
fetch: eventFetch ? "platform" : "webview",
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
abort.signal.removeEventListener("abort", onAbort)
|
||||||
|
attempt = undefined
|
||||||
|
clearHeartbeat()
|
||||||
}
|
}
|
||||||
})().finally(() => {
|
|
||||||
run = undefined
|
|
||||||
flush()
|
|
||||||
})
|
|
||||||
return run
|
|
||||||
}
|
|
||||||
|
|
||||||
const stop = () => {
|
if (abort.signal.aborted || !started) return
|
||||||
started = false
|
await wait(RECONNECT_DELAY_MS)
|
||||||
attempt?.abort()
|
}
|
||||||
clearHeartbeat()
|
})().finally(() => {
|
||||||
}
|
run = undefined
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
makeEventListener(document, "visibilitychange", () => {
|
|
||||||
if (document.visibilityState !== "visible") return
|
|
||||||
if (!started) return
|
|
||||||
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
|
|
||||||
attempt?.abort()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
stop()
|
|
||||||
abort.abort()
|
|
||||||
flush()
|
flush()
|
||||||
})
|
})
|
||||||
|
return run
|
||||||
|
}
|
||||||
|
|
||||||
const sdk = createSdkForServer({
|
const stop = () => {
|
||||||
server: server.current.http,
|
started = false
|
||||||
fetch: platform.fetch,
|
attempt?.abort()
|
||||||
throwOnError: true,
|
clearHeartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
makeEventListener(document, "visibilitychange", () => {
|
||||||
|
if (document.visibilityState !== "visible") return
|
||||||
|
if (!started) return
|
||||||
|
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
|
||||||
|
attempt?.abort()
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const dirSyncContexts = new Map<string, ReturnType<typeof createDirSdkContext>>()
|
onCleanup(() => {
|
||||||
const dirSdkContextRefCounts = new Map<string, number>()
|
stop()
|
||||||
|
abort.abort()
|
||||||
|
flush()
|
||||||
|
})
|
||||||
|
|
||||||
|
const sdk = createSdkForServer({
|
||||||
|
server: server.http,
|
||||||
|
fetch: platform.fetch,
|
||||||
|
throwOnError: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: server.http.url,
|
||||||
|
client: sdk,
|
||||||
|
event: {
|
||||||
|
on: emitter.on.bind(emitter),
|
||||||
|
listen: emitter.listen.bind(emitter),
|
||||||
|
start,
|
||||||
|
},
|
||||||
|
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
||||||
|
return createSdkForServer({
|
||||||
|
server: server.http,
|
||||||
|
fetch: platform.fetch,
|
||||||
|
...opts,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleContext({
|
||||||
|
name: "ServerSDK",
|
||||||
|
init: () => {
|
||||||
|
const language = useLanguage()
|
||||||
|
const server = useServer()
|
||||||
|
|
||||||
|
if (!server.current) throw new Error(language.t("error.serverSDK.noServerAvailable"))
|
||||||
|
const sdk = createServerSdkContext(server.current)
|
||||||
return {
|
return {
|
||||||
url: currentServer.http.url,
|
...sdk,
|
||||||
client: sdk,
|
createDirSdkContext: createRefCountMap((dir) => createDirSdkContext(dir, sdk)),
|
||||||
event: {
|
|
||||||
on: emitter.on.bind(emitter),
|
|
||||||
listen: emitter.listen.bind(emitter),
|
|
||||||
start,
|
|
||||||
},
|
|
||||||
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
|
||||||
const s = server.current
|
|
||||||
if (!s) throw new Error(language.t("error.serverSDK.serverNotAvailable"))
|
|
||||||
return createSdkForServer({
|
|
||||||
server: s.http,
|
|
||||||
fetch: platform.fetch,
|
|
||||||
...opts,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
createDirSyncContext: (directory: string) => {
|
|
||||||
onCleanup(() => {
|
|
||||||
dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) - 1)
|
|
||||||
if (dirSdkContextRefCounts.get(directory) === 0) {
|
|
||||||
dirSyncContexts.delete(directory)
|
|
||||||
dirSdkContextRefCounts.delete(directory)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const cached = dirSyncContexts.get(directory)
|
|
||||||
if (cached) {
|
|
||||||
dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) + 1)
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
const ctx = createDirSdkContext(directory)
|
|
||||||
dirSyncContexts.set(directory, ctx)
|
|
||||||
dirSdkContextRefCounts.set(directory, 1)
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -280,9 +263,7 @@ type SDKEventMap = {
|
||||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDirSdkContext(directory: string) {
|
function createDirSdkContext(directory: string, serverSDK: ReturnType<typeof createServerSdkContext>) {
|
||||||
const serverSDK = useServerSDK()
|
|
||||||
|
|
||||||
const client = serverSDK.createClient({
|
const client = serverSDK.createClient({
|
||||||
directory,
|
directory,
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ import { createRefreshQueue } from "./global-sync/queue"
|
||||||
import { directoryKey } from "./global-sync/utils"
|
import { directoryKey } from "./global-sync/utils"
|
||||||
import { PathKey } from "@/utils/path-key"
|
import { PathKey } from "@/utils/path-key"
|
||||||
import { createDirSyncContext } from "./directory-sync"
|
import { createDirSyncContext } from "./directory-sync"
|
||||||
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
import { createSimpleContext, NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||||
|
import { createRefCountMap } from "@/utils/refcount"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
|
|
@ -72,7 +73,7 @@ function makeQueryOptionsApi(serverSDK: () => OpencodeClient, sdkFor: (dir: Path
|
||||||
}
|
}
|
||||||
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
|
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
|
||||||
|
|
||||||
function createServerSyncContext() {
|
export function createServerSyncContext() {
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const owner = getOwner()
|
const owner = getOwner()
|
||||||
|
|
@ -425,9 +426,6 @@ function createServerSyncContext() {
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const dirSyncContexts = new Map<string, ReturnType<typeof createDirSyncContext>>()
|
|
||||||
const dirSyncContextRefCounts = new Map<string, number>()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: globalStore,
|
data: globalStore,
|
||||||
set,
|
set,
|
||||||
|
|
@ -446,41 +444,20 @@ function createServerSyncContext() {
|
||||||
todo: {
|
todo: {
|
||||||
set: setSessionTodo,
|
set: setSessionTodo,
|
||||||
},
|
},
|
||||||
createDirSyncContext: (directory: string) => {
|
|
||||||
onCleanup(() => {
|
|
||||||
dirSyncContextRefCounts.set(directory, (dirSyncContextRefCounts.get(directory) ?? 0) - 1)
|
|
||||||
if (dirSyncContextRefCounts.get(directory) === 0) {
|
|
||||||
dirSyncContexts.delete(directory)
|
|
||||||
dirSyncContextRefCounts.delete(directory)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const cached = dirSyncContexts.get(directory)
|
|
||||||
if (cached) {
|
|
||||||
dirSyncContextRefCounts.set(directory, (dirSyncContextRefCounts.get(directory) ?? 0) + 1)
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
const ctx = createDirSyncContext(serverSDK.createClient({ directory, throwOnError: true }), directory)
|
|
||||||
dirSyncContexts.set(directory, ctx)
|
|
||||||
dirSyncContextRefCounts.set(directory, 1)
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ServerSyncContext = createContext<ReturnType<typeof createServerSyncContext>>()
|
export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({
|
||||||
|
name: "ServerSync",
|
||||||
|
init: () => {
|
||||||
|
const sync = createServerSyncContext()
|
||||||
|
|
||||||
export function ServerSyncProvider(props: ParentProps) {
|
return {
|
||||||
const value = createServerSyncContext()
|
...sync,
|
||||||
return <ServerSyncContext.Provider value={value}>{props.children}</ServerSyncContext.Provider>
|
createDirSyncContext: createRefCountMap((dir) => createDirSyncContext(dir, sync)),
|
||||||
}
|
}
|
||||||
|
},
|
||||||
export function useServerSync() {
|
})
|
||||||
const context = useContext(ServerSyncContext)
|
|
||||||
if (!context) throw new Error("useServerSync must be used within ServerSyncProvider")
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useQueryOptions() {
|
export function useQueryOptions() {
|
||||||
return useServerSync().queryOptions
|
return useServerSync().queryOptions
|
||||||
|
|
|
||||||
26
packages/app/src/utils/refcount.ts
Normal file
26
packages/app/src/utils/refcount.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { onCleanup } from "solid-js"
|
||||||
|
|
||||||
|
export function createRefCountMap<T>(create: (key: string) => T) {
|
||||||
|
const items = new Map<string, T>()
|
||||||
|
const refCounts = new Map<string, number>()
|
||||||
|
|
||||||
|
return (key: string) => {
|
||||||
|
onCleanup(() => {
|
||||||
|
refCounts.set(key, (refCounts.get(key) ?? 0) - 1)
|
||||||
|
if (refCounts.get(key) === 0) {
|
||||||
|
items.delete(key)
|
||||||
|
refCounts.delete(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const cached = items.get(key)
|
||||||
|
if (cached) {
|
||||||
|
refCounts.set(key, (refCounts.get(key) ?? 0) + 1)
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
const item = create(key)
|
||||||
|
items.set(key, item)
|
||||||
|
refCounts.set(key, 1)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue