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
27 | "database:upsertAccessPolicy"27 | "database:upsertAccessPolicy"
28 | "database:upsertCredential"28 | "database:upsertCredential"
29 | "database:upsertToolSource"29 | "database:upsertToolSource"
30 | "database:syncWorkspaceTools"
31 | "database:deleteToolSource"30 | "database:deleteToolSource"
32 | "database:createTaskEvent";31 | "database:createTaskEvent";
33 32
@@ -46,7 +45,6 @@ type QueryName = · 12 unmodified lines
46 | "database:resolveCredential"45 | "database:resolveCredential"
47 | "database:listToolSources"46 | "database:listToolSources"
48 | "database:listToolSourceWorkspaceUpdates"47 | "database:listToolSourceWorkspaceUpdates"
49 | "database:listWorkspaceToolsForContext"
50 | "database:listTaskEvents";48 | "database:listTaskEvents";
51 49
52export class ExecutorDatabase {50export class ExecutorDatabase {
@@ -225,21 +223,6 @@ export class ExecutorDatabase { · 172 unmodified lines
225 return await this.query("database:listToolSourceWorkspaceUpdates", {});223 return await this.query("database:listToolSourceWorkspaceUpdates", {});
226 }224 }
227 225
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
243 async deleteToolSource(workspaceId: string, sourceId: string): Promise<boolean> {226 async deleteToolSource(workspaceId: string, sourceId: string): Promise<boolean> {
244 return await this.mutation("database:deleteToolSource", { workspaceId, sourceId });227 return await this.mutation("database:deleteToolSource", { workspaceId, sourceId });
245 }228 }
executor/apps/server/src/service.ts+0−27
@@ -131,7 +131,6 @@ export class ExecutorService { · 130 unmodified lines
131 string,131 string,
132 { signature: string; loadedAt: number; tools: Map<string, ToolDefinition> }132 { signature: string; loadedAt: number; tools: Map<string, ToolDefinition> }
133 >();133 >();
134 private readonly workspaceToolInventorySignatures = new Map<string, string>();
135 private readonly workspaceToolLoadWarnings = new Map<string, string[]>();134 private readonly workspaceToolLoadWarnings = new Map<string, string[]>();
136 private readonly inFlightTaskIds = new Set<string>();135 private readonly inFlightTaskIds = new Set<string>();
137 private readonly autoExecuteTasks: boolean;136 private readonly autoExecuteTasks: boolean;
@@ -491,9 +490,6 @@ export class ExecutorService { · 353 unmodified lines
491 const signature = sourceSignature(workspaceId, sources);490 const signature = sourceSignature(workspaceId, sources);
492 const cached = this.workspaceToolCache.get(workspaceId);491 const cached = this.workspaceToolCache.get(workspaceId);
493 if (cached && cached.signature === signature) {492 if (cached && cached.signature === signature) {
494 if (this.workspaceToolInventorySignatures.get(workspaceId) !== signature) {
495 await this.syncWorkspaceToolInventory(workspaceId, cached.tools, signature);
496 }
497 return cached.tools;493 return cached.tools;
498 }494 }
499 495
@@ -531,32 +527,9 @@ export class ExecutorService { · 31 unmodified lines
531 tools: merged,527 tools: merged,
532 });528 });
533 529
534 await this.syncWorkspaceToolInventory(workspaceId, merged, signature);
535
536 return merged;530 return merged;
537 }531 }
538 532
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
560 private getToolDecision(533 private getToolDecision(
561 task: TaskRecord,534 task: TaskRecord,
562 tool: ToolDefinition,535 tool: ToolDefinition,
executor/apps/server/src/worker.ts+0−41
@@ -51,7 +51,6 @@ const convexClient = new ConvexClient(convexUrl, { · 50 unmodified lines
51});51});
52 52
53let draining = false;53let draining = false;
54let syncingToolInventories = false;
55 54
56async function drainQueue(trigger: string): Promise<void> {55async function drainQueue(trigger: string): Promise<void> {
57 if (draining) {56 if (draining) {
@@ -79,30 +78,6 @@ async function drainQueue(trigger: string): Promise<void> { · 21 unmodified lines
79 }78 }
80}79}
81 80
82async 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
106const queueSubscription = convexClient.onUpdate(81const queueSubscription = convexClient.onUpdate(
107 api.database.listQueuedTaskIds,82 api.database.listQueuedTaskIds,
108 { limit: 1 },83 { limit: 1 },
@@ -116,31 +91,15 @@ const queueSubscription = convexClient.onUpdate( · 7 unmodified lines
116 },91 },
117);92);
118 93
119const 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
132const interval = setInterval(() => {94const interval = setInterval(() => {
133 void drainQueue("interval");95 void drainQueue("interval");
134 void syncWorkspaceToolInventories("interval");
135}, pollMs);96}, pollMs);
136 97
137await drainQueue("startup");98await drainQueue("startup");
138await syncWorkspaceToolInventories("startup");
139 99
140function shutdown(signal: string): void {100function shutdown(signal: string): void {
141 console.log(`[worker] received ${signal}, shutting down...`);101 console.log(`[worker] received ${signal}, shutting down...`);
142 queueSubscription.unsubscribe();102 queueSubscription.unsubscribe();
143 toolSourceSubscription.unsubscribe();
144 clearInterval(interval);103 clearInterval(interval);
145 void convexClient.close();104 void convexClient.close();
146 process.exit(0);105 process.exit(0);
executor/apps/web/src/app/layout.tsx+6−3
@@ -2,6 +2,7 @@ import type { Metadata } from "next"; · 1 unmodified line
2import { JetBrains_Mono, Inter } from "next/font/google";2import { JetBrains_Mono, Inter } from "next/font/google";
3import { Toaster } from "sonner";3import { Toaster } from "sonner";
4import { AppConvexProvider } from "@/lib/convex-provider";4import { AppConvexProvider } from "@/lib/convex-provider";
5import { QueryProvider } from "@/lib/query-provider";
5import { SessionProvider } from "@/lib/session-context";6import { SessionProvider } from "@/lib/session-context";
6import "./globals.css";7import "./globals.css";
7 8
@@ -31,9 +32,11 @@ export default function RootLayout({ · 23 unmodified lines
31 className={`${inter.variable} ${jetbrainsMono.variable} antialiased`}32 className={`${inter.variable} ${jetbrainsMono.variable} antialiased`}
32 >33 >
33 <AppConvexProvider>34 <AppConvexProvider>
34 <SessionProvider>35 <QueryProvider>
35 {children}36 <SessionProvider>
36 </SessionProvider>37 {children}
38 </SessionProvider>
39 </QueryProvider>
37 </AppConvexProvider>40 </AppConvexProvider>
38 <Toaster41 <Toaster
39 theme="dark"42 theme="dark"
executor/apps/web/src/components/dashboard-view.tsx+1−1
@@ -251,7 +251,7 @@ export function DashboardView() { · 250 unmodified lines
251 </div>251 </div>
252 252
253 {/* Tools summary */}253 {/* Tools summary */}
254 {tools && tools.length > 0 && (254 {tools.length > 0 && (
255 <Card className="bg-card border-border">255 <Card className="bg-card border-border">
256 <CardHeader className="pb-3">256 <CardHeader className="pb-3">
257 <CardTitle className="text-sm font-medium flex items-center gap-2">257 <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
304 config: Record<string, unknown>,304 config: Record<string, unknown>,
305 ) => {305 ) => {
306 if (!context) return;306 if (!context) return;
307 // Write the tool source to the database
308 await upsertToolSource({307 await upsertToolSource({
309 workspaceId: context.workspaceId,308 workspaceId: context.workspaceId,
310 name: sourceName,309 name: sourceName,
311 type: sourceType,310 type: sourceType,
312 config,311 config,
313 });312 });
314 toast.success(`Source "${sourceName}" added`);313 toast.success(`Source "${sourceName}" added — loading tools…`);
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 }
325 };314 };
326 315
327 const handlePresetAdd = async (preset: ApiPreset) => {316 const handlePresetAdd = async (preset: ApiPreset) => {
@@ -763,11 +752,9 @@ export function ToolsView() { · 435 unmodified lines
763 </TabsTrigger>752 </TabsTrigger>
764 <TabsTrigger value="inventory" className="text-xs data-[state=active]:bg-background">753 <TabsTrigger value="inventory" className="text-xs data-[state=active]:bg-background">
765 Inventory754 Inventory
766 {tools && (755 <span className="ml-1.5 text-[10px] font-mono text-muted-foreground">
767 <span className="ml-1.5 text-[10px] font-mono text-muted-foreground">756 {toolsLoading ? "…" : tools.length}
768 {tools.length}757 </span>
769 </span>
770 )}
771 </TabsTrigger>758 </TabsTrigger>
772 </TabsList>759 </TabsList>
773 760
@@ -818,11 +805,9 @@ export function ToolsView() { · 44 unmodified lines
818 <CardTitle className="text-sm font-medium flex items-center gap-2">805 <CardTitle className="text-sm font-medium flex items-center gap-2">
819 <Zap className="h-4 w-4 text-primary" />806 <Zap className="h-4 w-4 text-primary" />
820 Available Tools807 Available Tools
821 {tools && (808 <span className="text-[10px] font-mono bg-primary/10 text-primary px-1.5 py-0.5 rounded">
822 <span className="text-[10px] font-mono bg-primary/10 text-primary px-1.5 py-0.5 rounded">809 {toolsLoading ? "…" : tools.length}
823 {tools.length}810 </span>
824 </span>
825 )}
826 </CardTitle>811 </CardTitle>
827 </CardHeader>812 </CardHeader>
828 <CardContent className="pt-0">813 <CardContent className="pt-0">
@@ -833,7 +818,7 @@ export function ToolsView() { · 4 unmodified lines
833 ))}818 ))}
834 </div>819 </div>
835 ) : (820 ) : (
836 <ToolInventory tools={tools ?? []} />821 <ToolInventory tools={tools} />
837 )}822 )}
838 </CardContent>823 </CardContent>
839 </Card>824 </Card>
executor/apps/web/src/hooks/use-workspace-tools.ts+41−13
@@ -1,7 +1,10 @@
1"use client";1"use client";
2 2
3import { useQuery } from "convex/react";3import { useQuery as useTanstackQuery } from "@tanstack/react-query";
4import { convexApi } from "../lib/convex-api";4import { useQuery as useConvexQuery } from "convex/react";
5import { convexApi } from "@/lib/convex-api";
6import { executor } from "@/lib/executor-client";
7import type { ToolDescriptor } from "@/lib/types";
5 8
6interface WorkspaceContext {9interface WorkspaceContext {
7 workspaceId: string;10 workspaceId: string;
@@ -9,20 +12,45 @@ interface WorkspaceContext { · 1 unmodified line
9 clientId?: string;12 clientId?: string;
10}13}
11 14
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 */
12export function useWorkspaceTools(context: WorkspaceContext | null) {22export function useWorkspaceTools(context: WorkspaceContext | null) {
13 const tools = useQuery(23 // Watch tool sources reactively so we invalidate when sources change
14 convexApi.database.listWorkspaceToolsForContext,24 const toolSources = useConvexQuery(
15 context25 convexApi.database.listToolSources,
16 ? {26 context ? { workspaceId: context.workspaceId } : "skip",
17 workspaceId: context.workspaceId,
18 actorId: context.actorId,
19 clientId: context.clientId,
20 }
21 : "skip",
22 );27 );
23 28
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
24 return {52 return {
25 tools,53 tools: data ?? [],
26 loading: !!context && tools === undefined,54 loading: !!context && isLoading,
27 };55 };
28}56}
executor/apps/web/src/lib/query-client.ts+9−0
@@ -0,0 +1,9 @@
1import { QueryClient } from "@tanstack/react-query";
2
3export 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
3import { QueryClientProvider } from "@tanstack/react-query";
4import { queryClient } from "./query-client";
5import type { ReactNode } from "react";
6
7export 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