feat: add global sdk event pipeline

This commit is contained in:
Benjamin Shafii
2026-01-20 14:21:42 -08:00
parent 8077426041
commit 0fa4a415ae
3 changed files with 145 additions and 0 deletions

View File

@@ -27,6 +27,7 @@
"dependencies": {
"@opencode-ai/sdk": "^1.1.19",
"@radix-ui/colors": "^3.0.0",
"@solid-primitives/event-bus": "^1.1.2",
"@solid-primitives/storage": "^4.3.3",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "~2.6.0",

13
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@radix-ui/colors':
specifier: ^3.0.0
version: 3.0.0
'@solid-primitives/event-bus':
specifier: ^1.1.2
version: 1.1.2(solid-js@1.9.10)
'@solid-primitives/storage':
specifier: ^4.3.3
version: 4.3.3(solid-js@1.9.10)
@@ -447,6 +450,11 @@ packages:
cpu: [x64]
os: [win32]
'@solid-primitives/event-bus@1.1.2':
resolution: {integrity: sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA==}
peerDependencies:
solid-js: ^1.6.12
'@solid-primitives/storage@4.3.3':
resolution: {integrity: sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw==}
peerDependencies:
@@ -1281,6 +1289,11 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.55.1':
optional: true
'@solid-primitives/event-bus@1.1.2(solid-js@1.9.10)':
dependencies:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
solid-js: 1.9.10
'@solid-primitives/storage@4.3.3(solid-js@1.9.10)':
dependencies:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)

View File

@@ -0,0 +1,131 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client";
import { createGlobalEmitter } from "@solid-primitives/event-bus";
import { batch, createContext, onCleanup, useContext, type ParentProps } from "solid-js";
import { usePlatform } from "./platform";
import { useServer } from "./server";
type GlobalSDKContextValue = {
url: string;
client: ReturnType<typeof createOpencodeClient>;
event: ReturnType<typeof createGlobalEmitter<{ [key: string]: Event }>>;
};
const GlobalSDKContext = createContext<GlobalSDKContextValue | undefined>(undefined);
export function GlobalSDKProvider(props: ParentProps) {
const server = useServer();
const platform = usePlatform();
const abort = new AbortController();
const eventClient = createOpencodeClient({
baseUrl: server.url,
signal: abort.signal,
fetch: platform.fetch,
});
const emitter = createGlobalEmitter<{ [key: string]: Event }>();
type Queued = { directory: string; payload: Event };
let queue: Array<Queued | undefined> = [];
const coalesced = new Map<string, number>();
let timer: ReturnType<typeof setTimeout> | undefined;
let last = 0;
const keyForEvent = (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 === "todo.updated") return `todo.updated:${directory}:${payload.properties.sessionID}`;
if (payload.type === "mcp.tools.changed") return `mcp.tools.changed:${directory}:${payload.properties.server}`;
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;
const events = queue;
queue = [];
coalesced.clear();
if (events.length === 0) return;
last = Date.now();
batch(() => {
for (const entry of events) {
if (!entry) continue;
emitter.emit(entry.directory, entry.payload);
}
});
};
const schedule = () => {
if (timer) return;
const elapsed = Date.now() - last;
timer = setTimeout(flush, Math.max(0, 16 - elapsed));
};
const stop = () => {
flush();
};
void (async () => {
const subscription = await eventClient.event.subscribe(undefined, { signal: abort.signal });
let yielded = Date.now();
for await (const event of subscription.stream as AsyncIterable<unknown>) {
const record = event as Event & { directory?: string; payload?: Event };
const payload = record.payload ?? record;
if (!payload?.type) continue;
const directory = typeof record.directory === "string" ? record.directory : "global";
const key = keyForEvent(directory, payload);
if (key) {
const index = coalesced.get(key);
if (index !== undefined) {
queue[index] = undefined;
}
coalesced.set(key, 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(stop)
.catch(() => undefined);
onCleanup(() => {
abort.abort();
stop();
});
const client = createOpencodeClient({
baseUrl: server.url,
fetch: platform.fetch,
throwOnError: true,
});
const value: GlobalSDKContextValue = {
url: server.url,
client,
event: emitter,
};
return <GlobalSDKContext.Provider value={value}>{props.children}</GlobalSDKContext.Provider>;
}
export function useGlobalSDK() {
const context = useContext(GlobalSDKContext);
if (!context) {
throw new Error("Global SDK context is missing");
}
return context;
}