Files
tf_code/packages/app/src/context/global-sdk.tsx
2026-02-10 11:30:58 -06:00

134 lines
4.2 KiB
TypeScript

import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { batch, onCleanup } from "solid-js"
import { usePlatform } from "./platform"
import { useServer } from "./server"
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
name: "GlobalSDK",
init: () => {
const server = useServer()
const platform = usePlatform()
const abort = new AbortController()
// Prefer the WebView fetch implementation for streaming responses.
// @tauri-apps/plugin-http 2.5.x has known issues with streaming/cancellation that can
// retain native resources in the Rust process.
const base = platform.platform === "desktop" ? globalThis.fetch : (platform.fetch ?? globalThis.fetch)
const eventFetch = Object.assign(
(input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
const password = (globalThis as { __OPENCODE__?: { serverPassword?: string } }).__OPENCODE__?.serverPassword
const header = password ? `Basic ${btoa(`opencode:${password}`)}` : undefined
const headers = new Headers(input instanceof Request ? input.headers : undefined)
if (init?.headers) {
new Headers(init.headers).forEach((value, key) => {
headers.set(key, value)
})
}
if (header) headers.set("Authorization", header)
return base(input, { ...(init ?? {}), headers })
},
{
preconnect: (...args: Parameters<typeof fetch.preconnect>) => (base as any).preconnect?.(...args),
},
) satisfies typeof fetch
const eventSdk = createOpencodeClient({
baseUrl: server.url,
signal: abort.signal,
fetch: eventFetch,
})
const emitter = createGlobalEmitter<{
[key: string]: Event
}>()
type Queued = { directory: string; payload: Event }
let queue: Array<Queued | undefined> = []
let buffer: Array<Queued | undefined> = []
const coalesced = new Map<string, number>()
let timer: ReturnType<typeof setTimeout> | undefined
let last = 0
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 = () => {
if (timer) clearTimeout(timer)
timer = undefined
if (queue.length === 0) return
const events = queue
queue = buffer
buffer = events
queue.length = 0
coalesced.clear()
last = Date.now()
batch(() => {
for (const event of events) {
if (!event) continue
emitter.emit(event.directory, event.payload)
}
})
buffer.length = 0
}
const schedule = () => {
if (timer) return
const elapsed = Date.now() - last
timer = setTimeout(flush, Math.max(0, 16 - elapsed))
}
void (async () => {
const events = await eventSdk.global.event()
let yielded = Date.now()
for await (const event of events.stream) {
const directory = event.directory ?? "global"
const payload = event.payload
const k = key(directory, payload)
if (k) {
const i = coalesced.get(k)
if (i !== undefined) {
queue[i] = undefined
}
coalesced.set(k, queue.length)
}
queue.push({ directory, payload })
schedule()
if (Date.now() - yielded < 8) continue
yielded = Date.now()
await new Promise<void>((resolve) => setTimeout(resolve, 0))
}
})()
.finally(flush)
.catch(() => undefined)
onCleanup(() => {
abort.abort()
flush()
})
const sdk = createOpencodeClient({
baseUrl: server.url,
fetch: platform.fetch,
throwOnError: true,
})
return { url: server.url, client: sdk, event: emitter }
},
})