History

Replace useWorkspaceTools with TanStack Query + Eden, remove workspace tool sync

- Rewrite useWorkspaceTools hook to use @tanstack/react-query + Eden - Add QueryClientProvider to app layout - Remove syncWorkspaceTools/listWorkspaceToolsForContext from database layer - Remove workspace tool inventory sync from service and worker - Tools are now fetched directly from the API, not materialized to Convex

Rhys Sullivan committed 9 months ago

Commit 1bf1a351 parent c5505279 files · +78 −125Browse files

Unified · Split

9 files
  1. executor/apps/server/src/database.ts +0 −17
  2. executor/apps/server/src/service.ts +0 −27
  3. executor/apps/server/src/worker.ts +0 −41
  4. executor/apps/web/src/app/layout.tsx +6 −3
  5. executor/apps/web/src/components/dashboard-view.tsx +1 −1
  6. executor/apps/web/src/components/tools-view.tsx +8 −23
  7. executor/apps/web/src/hooks/use-workspace-tools.ts +41 −13
  8. executor/apps/web/src/lib/query-client.ts +9 −0
  9. executor/apps/web/src/lib/query-provider.tsx +13 −0
executor/apps/server/src/database.ts+0−17
@@ -27,7 +27,6 @@ type MutationName = · 26 unmodified lines
2727 | "database:upsertAccessPolicy"
2828 | "database:upsertCredential"
2929 | "database:upsertToolSource"
30− | "database:syncWorkspaceTools"
3130 | "database:deleteToolSource"
3231 | "database:createTaskEvent";
3332
@@ -46,7 +45,6 @@ type QueryName = · 12 unmodified lines
4645 | "database:resolveCredential"
4746 | "database:listToolSources"
4847 | "database:listToolSourceWorkspaceUpdates"
49− | "database:listWorkspaceToolsForContext"
5048 | "database:listTaskEvents";
5149
5250 export class ExecutorDatabase {
@@ -225,21 +223,6 @@ export class ExecutorDatabase { · 172 unmodified lines
225223 return await this.query("database:listToolSourceWorkspaceUpdates", {});
226224 }
227225
228− async syncWorkspaceTools(params: {
229− workspaceId: string;
230− tools: ToolDescriptor[];
231− }): Promise<boolean> {
232− return await this.mutation("database:syncWorkspaceTools", params);
233− }
234−
235− async listWorkspaceToolsForContext(params: {
236− workspaceId: string;
237− actorId?: string;
238− clientId?: string;
239− }): Promise<ToolDescriptor[]> {
240− return await this.query("database:listWorkspaceToolsForContext", params);
241− }
242−
243226 async deleteToolSource(workspaceId: string, sourceId: string): Promise<boolean> {
244227 return await this.mutation("database:deleteToolSource", { workspaceId, sourceId });
245228 }
executor/apps/server/src/service.ts+0−27
@@ -131,7 +131,6 @@ export class ExecutorService { · 130 unmodified lines
131131 string,
132132 { signature: string; loadedAt: number; tools: Map<string, ToolDefinition> }
133133 >();
134− private readonly workspaceToolInventorySignatures = new Map<string, string>();
135134 private readonly workspaceToolLoadWarnings = new Map<string, string[]>();
136135 private readonly inFlightTaskIds = new Set<string>();
137136 private readonly autoExecuteTasks: boolean;
@@ -491,9 +490,6 @@ export class ExecutorService { · 353 unmodified lines
491490 const signature = sourceSignature(workspaceId, sources);
492491 const cached = this.workspaceToolCache.get(workspaceId);
493492 if (cached && cached.signature === signature) {
494− if (this.workspaceToolInventorySignatures.get(workspaceId) !== signature) {
495− await this.syncWorkspaceToolInventory(workspaceId, cached.tools, signature);
496− }
497493 return cached.tools;
498494 }
499495
@@ -531,32 +527,9 @@ export class ExecutorService { · 31 unmodified lines
531527 tools: merged,
532528 });
533529
534− await this.syncWorkspaceToolInventory(workspaceId, merged, signature);
535−
536530 return merged;
537531 }
538532
539− private async syncWorkspaceToolInventory(
540− workspaceId: string,
541− tools: Map<string, ToolDefinition>,
542− signature: string,
543− ): Promise<void> {
544− const inventory = [...tools.values()].map((tool) => ({
545− path: tool.path,
546− description: tool.description,
547− approval: tool.approval,
548− source: tool.source,
549− argsType: tool.metadata?.argsType,
550− returnsType: tool.metadata?.returnsType,
551− }));
552−
553− await this.db.syncWorkspaceTools({
554− workspaceId,
555− tools: inventory,
556− });
557− this.workspaceToolInventorySignatures.set(workspaceId, signature);
558− }
559−
560533 private getToolDecision(
561534 task: TaskRecord,
562535 tool: ToolDefinition,
executor/apps/server/src/worker.ts+0−41
@@ -51,7 +51,6 @@ const convexClient = new ConvexClient(convexUrl, { · 50 unmodified lines
5151 });
5252
5353 let draining = false;
54−let syncingToolInventories = false;
5554
5655 async function drainQueue(trigger: string): Promise<void> {
5756 if (draining) {
@@ -79,30 +78,6 @@ async function drainQueue(trigger: string): Promise<void> { · 21 unmodified lines
7978 }
8079 }
8180
82−async function syncWorkspaceToolInventories(trigger: string): Promise<void> {
83− if (syncingToolInventories) {
84− return;
85− }
86−
87− syncingToolInventories = true;
88− try {
89− const updates = await service.listToolSourceWorkspaceUpdates();
90− if (updates.length === 0) {
91− return;
92− }
93−
94− console.log(`[worker] ${trigger}: syncing tool inventories for ${updates.length} workspace(s)`);
95− for (const update of updates) {
96− await service.refreshWorkspaceTools(update.workspaceId);
97− }
98− } catch (error) {
99− const message = error instanceof Error ? error.message : String(error);
100− console.error(`[worker] tool inventory sync failed: ${message}`);
101− } finally {
102− syncingToolInventories = false;
103− }
104−}
105−
10681 const queueSubscription = convexClient.onUpdate(
10782 api.database.listQueuedTaskIds,
10883 { limit: 1 },
@@ -116,31 +91,15 @@ const queueSubscription = convexClient.onUpdate( · 7 unmodified lines
11691 },
11792 );
11893
119−const toolSourceSubscription = convexClient.onUpdate(
120− api.database.listToolSourceWorkspaceUpdates,
121− {},
122− (updates) => {
123− if (updates.length > 0) {
124− void syncWorkspaceToolInventories("onUpdate");
125− }
126− },
127− (error) => {
128− console.warn(`[worker] tool source watcher error: ${error.message}`);
129− },
130−);
131−
13294 const interval = setInterval(() => {
13395 void drainQueue("interval");
134− void syncWorkspaceToolInventories("interval");
13596 }, pollMs);
13697
13798 await drainQueue("startup");
138−await syncWorkspaceToolInventories("startup");
13999
140100 function shutdown(signal: string): void {
141101 console.log(`[worker] received ${signal}, shutting down...`);
142102 queueSubscription.unsubscribe();
143− toolSourceSubscription.unsubscribe();
144103 clearInterval(interval);
145104 void convexClient.close();
146105 process.exit(0);
executor/apps/web/src/app/layout.tsx+6−3
@@ -2,6 +2,7 @@ import type { Metadata } from "next"; · 1 unmodified line
22 import { JetBrains_Mono, Inter } from "next/font/google";
33 import { Toaster } from "sonner";
44 import { AppConvexProvider } from "@/lib/convex-provider";
5+import { QueryProvider } from "@/lib/query-provider";
56 import { SessionProvider } from "@/lib/session-context";
67 import "./globals.css";
78
@@ -31,9 +32,11 @@ export default function RootLayout({ · 23 unmodified lines
3132 className={`${inter.variable} ${jetbrainsMono.variable} antialiased`}
3233 >
3334 <AppConvexProvider>
34− <SessionProvider>
35− {children}
36− </SessionProvider>
35+ <QueryProvider>
36+ <SessionProvider>
37+ {children}
38+ </SessionProvider>
39+ </QueryProvider>
3740 </AppConvexProvider>
3841 <Toaster
3942 theme="dark"
executor/apps/web/src/components/dashboard-view.tsx+1−1
@@ -251,7 +251,7 @@ export function DashboardView() { · 250 unmodified lines
251251 </div>
252252
253253 {/* Tools summary */}
254− {tools && tools.length > 0 && (
254+ {tools.length > 0 && (
255255 <Card className="bg-card border-border">
256256 <CardHeader className="pb-3">
257257 <CardTitle className="text-sm font-medium flex items-center gap-2">
executor/apps/web/src/components/tools-view.tsx+8−23
@@ -304,24 +304,13 @@ function AddSourceDialog({ · 303 unmodified lines
304304 config: Record<string, unknown>,
305305 ) => {
306306 if (!context) return;
307− // Write the tool source to the database
308307 await upsertToolSource({
309308 workspaceId: context.workspaceId,
310309 name: sourceName,
311310 type: sourceType,
312311 config,
313312 });
314− toast.success(`Source "${sourceName}" added`);
315−
316− // Trigger a server-side tool refresh so the workspace tools inventory
317− // is updated immediately (instead of waiting for the worker poll).
318− // Fetching the tools list forces the server to load external sources
319− // and sync the workspaceTools table.
320− try {
321− await fetch(`/api/tools?workspaceId=${encodeURIComponent(context.workspaceId)}`);
322− } catch {
323− // Server-side refresh is best-effort; the worker will eventually sync.
324− }
313+ toast.success(`Source "${sourceName}" added — loading tools…`);
325314 };
326315
327316 const handlePresetAdd = async (preset: ApiPreset) => {
@@ -763,11 +752,9 @@ export function ToolsView() { · 435 unmodified lines
763752 </TabsTrigger>
764753 <TabsTrigger value="inventory" className="text-xs data-[state=active]:bg-background">
765754 Inventory
766− {tools && (
767− <span className="ml-1.5 text-[10px] font-mono text-muted-foreground">
768− {tools.length}
769− </span>
770− )}
755+ <span className="ml-1.5 text-[10px] font-mono text-muted-foreground">
756+ {toolsLoading ? "…" : tools.length}
757+ </span>
771758 </TabsTrigger>
772759 </TabsList>
773760
@@ -818,11 +805,9 @@ export function ToolsView() { · 44 unmodified lines
818805 <CardTitle className="text-sm font-medium flex items-center gap-2">
819806 <Zap className="h-4 w-4 text-primary" />
820807 Available Tools
821− {tools && (
822− <span className="text-[10px] font-mono bg-primary/10 text-primary px-1.5 py-0.5 rounded">
823− {tools.length}
824− </span>
825− )}
808+ <span className="text-[10px] font-mono bg-primary/10 text-primary px-1.5 py-0.5 rounded">
809+ {toolsLoading ? "…" : tools.length}
810+ </span>
826811 </CardTitle>
827812 </CardHeader>
828813 <CardContent className="pt-0">
@@ -833,7 +818,7 @@ export function ToolsView() { · 4 unmodified lines
833818 ))}
834819 </div>
835820 ) : (
836− <ToolInventory tools={tools ?? []} />
821+ <ToolInventory tools={tools} />
837822 )}
838823 </CardContent>
839824 </Card>
executor/apps/web/src/hooks/use-workspace-tools.ts+41−13
@@ -1,7 +1,10 @@
11 "use client";
22
3−import { useQuery } from "convex/react";
4−import { convexApi } from "../lib/convex-api";
3+import { useQuery as useTanstackQuery } from "@tanstack/react-query";
4+import { useQuery as useConvexQuery } from "convex/react";
5+import { convexApi } from "@/lib/convex-api";
6+import { executor } from "@/lib/executor-client";
7+import type { ToolDescriptor } from "@/lib/types";
58
69 interface WorkspaceContext {
710 workspaceId: string;
@@ -9,20 +12,45 @@ interface WorkspaceContext { · 1 unmodified line
912 clientId?: string;
1013 }
1114
15+/**
16+ * Fetches tool metadata from the server API (`GET /api/tools`) via Eden Treaty,
17+ * cached and deduplicated by TanStack Query.
18+ *
19+ * Automatically re-fetches when the Convex `toolSources` subscription changes
20+ * (the reactive value is included in the query key).
21+ */
1222 export function useWorkspaceTools(context: WorkspaceContext | null) {
13− const tools = useQuery(
14− convexApi.database.listWorkspaceToolsForContext,
15− context
16− ? {
17− workspaceId: context.workspaceId,
18− actorId: context.actorId,
19− clientId: context.clientId,
20− }
21− : "skip",
23+ // Watch tool sources reactively so we invalidate when sources change
24+ const toolSources = useConvexQuery(
25+ convexApi.database.listToolSources,
26+ context ? { workspaceId: context.workspaceId } : "skip",
2227 );
2328
29+ const { data, isLoading } = useTanstackQuery({
30+ queryKey: [
31+ "workspace-tools",
32+ context?.workspaceId,
33+ context?.actorId,
34+ context?.clientId,
35+ toolSources,
36+ ],
37+ queryFn: async () => {
38+ if (!context) return [];
39+ const { data, error } = await executor.api.tools.get({
40+ query: {
41+ workspaceId: context.workspaceId,
42+ ...(context.actorId && { actorId: context.actorId }),
43+ ...(context.clientId && { clientId: context.clientId }),
44+ },
45+ });
46+ if (error) throw error;
47+ return data as ToolDescriptor[];
48+ },
49+ enabled: !!context,
50+ });
51+
2452 return {
25− tools,
26− loading: !!context && tools === undefined,
53+ tools: data ?? [],
54+ loading: !!context && isLoading,
2755 };
2856 }
executor/apps/web/src/lib/query-client.ts+9−0
@@ -0,0 +1,9 @@
1+import { QueryClient } from "@tanstack/react-query";
2+
3+export const queryClient = new QueryClient({
4+ defaultOptions: {
5+ queries: {
6+ staleTime: 30_000, // tools don't change that often
7+ },
8+ },
9+});
executor/apps/web/src/lib/query-provider.tsx+13−0
@@ -0,0 +1,13 @@
1+"use client";
2+
3+import { QueryClientProvider } from "@tanstack/react-query";
4+import { queryClient } from "./query-client";
5+import type { ReactNode } from "react";
6+
7+export function QueryProvider({ children }: { children: ReactNode }) {
8+ return (
9+ <QueryClientProvider client={queryClient}>
10+ {children}
11+ </QueryClientProvider>
12+ );
13+}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t