global-sdk.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
  2. import { createSimpleContext } from "@opencode-ai/ui/context"
  3. import { createGlobalEmitter } from "@solid-primitives/event-bus"
  4. import { batch, onCleanup } from "solid-js"
  5. import { usePlatform } from "./platform"
  6. import { useServer } from "./server"
  7. export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
  8. name: "GlobalSDK",
  9. init: () => {
  10. const server = useServer()
  11. const platform = usePlatform()
  12. const abort = new AbortController()
  13. const eventSdk = createOpencodeClient({
  14. baseUrl: server.url,
  15. signal: abort.signal,
  16. fetch: platform.fetch,
  17. })
  18. const emitter = createGlobalEmitter<{
  19. [key: string]: Event
  20. }>()
  21. type Queued = { directory: string; payload: Event }
  22. let queue: Array<Queued | undefined> = []
  23. let buffer: Array<Queued | undefined> = []
  24. const coalesced = new Map<string, number>()
  25. let timer: ReturnType<typeof setTimeout> | undefined
  26. let last = 0
  27. const key = (directory: string, payload: Event) => {
  28. if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
  29. if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
  30. if (payload.type === "message.part.updated") {
  31. const part = payload.properties.part
  32. return `message.part.updated:${directory}:${part.messageID}:${part.id}`
  33. }
  34. }
  35. const flush = () => {
  36. if (timer) clearTimeout(timer)
  37. timer = undefined
  38. if (queue.length === 0) return
  39. const events = queue
  40. queue = buffer
  41. buffer = events
  42. queue.length = 0
  43. coalesced.clear()
  44. last = Date.now()
  45. batch(() => {
  46. for (const event of events) {
  47. if (!event) continue
  48. emitter.emit(event.directory, event.payload)
  49. }
  50. })
  51. buffer.length = 0
  52. }
  53. const schedule = () => {
  54. if (timer) return
  55. const elapsed = Date.now() - last
  56. timer = setTimeout(flush, Math.max(0, 16 - elapsed))
  57. }
  58. void (async () => {
  59. const events = await eventSdk.global.event()
  60. let yielded = Date.now()
  61. for await (const event of events.stream) {
  62. const directory = event.directory ?? "global"
  63. const payload = event.payload
  64. const k = key(directory, payload)
  65. if (k) {
  66. const i = coalesced.get(k)
  67. if (i !== undefined) {
  68. queue[i] = undefined
  69. }
  70. coalesced.set(k, queue.length)
  71. }
  72. queue.push({ directory, payload })
  73. schedule()
  74. if (Date.now() - yielded < 8) continue
  75. yielded = Date.now()
  76. await new Promise<void>((resolve) => setTimeout(resolve, 0))
  77. }
  78. })()
  79. .finally(flush)
  80. .catch(() => undefined)
  81. onCleanup(() => {
  82. abort.abort()
  83. flush()
  84. })
  85. const sdk = createOpencodeClient({
  86. baseUrl: server.url,
  87. fetch: platform.fetch,
  88. throwOnError: true,
  89. })
  90. return { url: server.url, client: sdk, event: emitter }
  91. },
  92. })