1a734adb4d
Moves effect logging, observability, runtime utilities, flags, installation version info, and process utilities from opencode to core package. This enables better code sharing across packages and establishes core as the single source of truth for foundational utilities. All internal imports updated to use @opencode-ai/core paths for consistency.
143 lines
3.8 KiB
TypeScript
143 lines
3.8 KiB
TypeScript
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
|
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
|
import { createSimpleContext } from "./helper"
|
|
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
import { batch, onCleanup, onMount } from "solid-js"
|
|
|
|
export type EventSource = {
|
|
subscribe: (handler: (event: GlobalEvent) => void) => Promise<() => void>
|
|
}
|
|
|
|
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|
name: "SDK",
|
|
init: (props: {
|
|
url: string
|
|
directory?: string
|
|
fetch?: typeof fetch
|
|
headers?: RequestInit["headers"]
|
|
events?: EventSource
|
|
}) => {
|
|
const abort = new AbortController()
|
|
let sse: AbortController | undefined
|
|
|
|
function createSDK() {
|
|
return createOpencodeClient({
|
|
baseUrl: props.url,
|
|
signal: abort.signal,
|
|
directory: props.directory,
|
|
fetch: props.fetch,
|
|
headers: props.headers,
|
|
})
|
|
}
|
|
|
|
let sdk = createSDK()
|
|
|
|
const emitter = createGlobalEmitter<{
|
|
event: GlobalEvent
|
|
}>()
|
|
|
|
let queue: GlobalEvent[] = []
|
|
let timer: Timer | undefined
|
|
let last = 0
|
|
const retryDelay = 1000
|
|
const maxRetryDelay = 30000
|
|
|
|
const flush = () => {
|
|
if (queue.length === 0) return
|
|
const events = queue
|
|
queue = []
|
|
timer = undefined
|
|
last = Date.now()
|
|
// Batch all event emissions so all store updates result in a single render
|
|
batch(() => {
|
|
for (const event of events) {
|
|
emitter.emit("event", event)
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleEvent = (event: GlobalEvent) => {
|
|
queue.push(event)
|
|
const elapsed = Date.now() - last
|
|
|
|
if (timer) return
|
|
// If we just flushed recently (within 16ms), batch this with future events
|
|
// Otherwise, process immediately to avoid latency
|
|
if (elapsed < 16) {
|
|
timer = setTimeout(flush, 16)
|
|
return
|
|
}
|
|
flush()
|
|
}
|
|
|
|
function startSSE() {
|
|
sse?.abort()
|
|
const ctrl = new AbortController()
|
|
sse = ctrl
|
|
;(async () => {
|
|
let attempt = 0
|
|
while (true) {
|
|
if (abort.signal.aborted || ctrl.signal.aborted) break
|
|
|
|
const events = await sdk.global.event({
|
|
signal: ctrl.signal,
|
|
sseMaxRetryAttempts: 0,
|
|
})
|
|
|
|
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
|
|
// Start syncing workspaces, it's important to do this after
|
|
// we've started listening to events
|
|
await sdk.sync.start().catch(() => {})
|
|
}
|
|
|
|
for await (const event of events.stream) {
|
|
if (ctrl.signal.aborted) break
|
|
handleEvent(event)
|
|
}
|
|
|
|
if (timer) clearTimeout(timer)
|
|
if (queue.length > 0) flush()
|
|
attempt += 1
|
|
if (abort.signal.aborted || ctrl.signal.aborted) break
|
|
|
|
// Exponential backoff
|
|
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), maxRetryDelay)
|
|
await new Promise((resolve) => setTimeout(resolve, backoff))
|
|
}
|
|
})().catch(() => {})
|
|
}
|
|
|
|
onMount(async () => {
|
|
if (props.events) {
|
|
const unsub = await props.events.subscribe(handleEvent)
|
|
onCleanup(unsub)
|
|
|
|
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
|
|
// Start syncing workspaces, it's important to do this after
|
|
// we've started listening to events
|
|
await sdk.sync.start().catch(() => {})
|
|
}
|
|
} else {
|
|
startSSE()
|
|
}
|
|
})
|
|
|
|
onCleanup(() => {
|
|
abort.abort()
|
|
sse?.abort()
|
|
if (timer) clearTimeout(timer)
|
|
})
|
|
|
|
return {
|
|
get client() {
|
|
return sdk
|
|
},
|
|
directory: props.directory,
|
|
event: emitter,
|
|
fetch: props.fetch ?? fetch,
|
|
url: props.url,
|
|
}
|
|
},
|
|
})
|