History

refactor: extract React client to packages/clients/react

- @executor/react: shared React package for plugins and apps - base-url.ts: configurable API base URL - client.ts: ExecutorApiClient via AtomHttpApi.Tag (typed, cached) - atoms.ts: query atoms (tools, secrets) + mutation atoms (invoke, set, remove) - provider.tsx: ExecutorProvider wrapping RegistryProvider - Re-exports effect-atom hooks (useAtomValue, useAtomSet, etc.) - apps/web: now imports from @executor/react instead of local api/ - Plugins can import @executor/react for typed API access and shared atoms - 10 packages typecheck clean

Rhys Sullivan committed 7 months ago

Commit 200cb6f1 parent 8babea813 files · +196 −83Browse files

Unified · Split

13 files
  1. apps/web/package.json +1 −6
  2. apps/web/src/App.tsx +8 −7
  3. apps/web/src/api/atoms.ts +0 −40
  4. apps/web/src/api/client.ts +0 −15
  5. apps/web/src/api/index.ts +0 −10
  6. bun.lock +20 −5
  7. packages/clients/react/package.json +26 −0
  8. packages/clients/react/src/atoms.ts +44 −0
  9. packages/clients/react/src/base-url.ts +12 −0
  10. packages/clients/react/src/client.ts +23 −0
  11. packages/clients/react/src/index.ts +34 −0
  12. packages/clients/react/src/provider.tsx +6 −0
  13. packages/clients/react/tsconfig.json +22 −0
apps/web/package.json+1−6
@@ -4,13 +4,8 @@ · 3 unmodified lines
44 "private": true,
55 "type": "module",
66 "dependencies": {
7− "@effect/platform": "catalog:",
8− "@effect-atom/atom": "^0.5.3",
9− "@effect-atom/atom-react": "^0.5.0",
10− "@executor/api": "workspace:*",
11− "@executor/sdk": "workspace:*",
7+ "@executor/react": "workspace:*",
128 "@executor/server": "workspace:*",
13− "effect": "catalog:",
149 "react": "^19.1.0",
1510 "react-dom": "^19.1.0"
1611 },
apps/web/src/App.tsx+8−7
@@ -1,8 +1,9 @@
1−import React, { Suspense } from "react";
2−import { RegistryProvider, useAtomValue, Result } from "@effect-atom/atom-react";
3−
4−import { ExecutorClient } from "./api/client";
5−import { toolsAtom } from "./api/atoms";
1+import React from "react";
2+import {
3+ ExecutorProvider,
4+ useAtomValue,
5+ toolsAtom,
6+} from "@executor/react";
67
78 function ToolList() {
89 const tools = useAtomValue(toolsAtom());
@@ -36,11 +37,11 @@ function ToolList() { · 27 unmodified lines
3637
3738 export function App() {
3839 return (
39− <RegistryProvider>
40+ <ExecutorProvider>
4041 <div style={{ fontFamily: "system-ui", padding: "2rem" }}>
4142 <h1>Executor</h1>
4243 <ToolList />
4344 </div>
44− </RegistryProvider>
45+ </ExecutorProvider>
4546 );
4647 }
apps/web/src/api/atoms.ts+0−40
@@ -1,40 +0,0 @@
1−import { ScopeId, ToolId, SecretId } from "@executor/sdk";
2−import { ExecutorClient } from "./client";
3−
4−// ---------------------------------------------------------------------------
5−// Query atoms — typed, cached, reactive
6−// ---------------------------------------------------------------------------
7−
8−export const toolsAtom = (scopeId: ScopeId = ScopeId.make("default")) =>
9− ExecutorClient.query("tools", "list", {
10− path: { scopeId },
11− timeToLive: "30 seconds",
12− });
13−
14−export const toolSchemaAtom = (scopeId: ScopeId, toolId: ToolId) =>
15− ExecutorClient.query("tools", "schema", {
16− path: { scopeId, toolId },
17− timeToLive: "1 minute",
18− });
19−
20−export const secretsAtom = (scopeId: ScopeId = ScopeId.make("default")) =>
21− ExecutorClient.query("secrets", "list", {
22− path: { scopeId },
23− timeToLive: "30 seconds",
24− });
25−
26−export const secretStatusAtom = (scopeId: ScopeId, secretId: SecretId) =>
27− ExecutorClient.query("secrets", "status", {
28− path: { scopeId, secretId },
29− timeToLive: "15 seconds",
30− });
31−
32−// ---------------------------------------------------------------------------
33−// Mutation atoms — fire-and-forget style
34−// ---------------------------------------------------------------------------
35−
36−export const invokeTool = ExecutorClient.mutation("tools", "invoke");
37−
38−export const setSecret = ExecutorClient.mutation("secrets", "set");
39−
40−export const removeSecret = ExecutorClient.mutation("secrets", "remove");
apps/web/src/api/client.ts+0−15
@@ -1,15 +0,0 @@
1−import { AtomHttpApi } from "@effect-atom/atom-react";
2−import { FetchHttpClient } from "@effect/platform";
3−import { ExecutorApi } from "@executor/api";
4−
5−// ---------------------------------------------------------------------------
6−// Typed HTTP API client — uses effect-atom's AtomHttpApi.Tag
7−// ---------------------------------------------------------------------------
8−
9−class ExecutorClient extends AtomHttpApi.Tag<ExecutorClient>()("ExecutorClient", {
10− api: ExecutorApi,
11− httpClient: FetchHttpClient.layer,
12− baseUrl: "/",
13−}) {}
14−
15−export { ExecutorClient };
apps/web/src/api/index.ts+0−10
@@ -1,10 +0,0 @@
1−export { ExecutorClient } from "./client";
2−export {
3− toolsAtom,
4− toolSchemaAtom,
5− secretsAtom,
6− secretStatusAtom,
7− invokeTool,
8− setSecret,
9− removeSecret,
10−} from "./atoms";
bun.lock+20−5
@@ -29,24 +29,37 @@ · 28 unmodified lines
2929 "apps/web": {
3030 "name": "@executor/web",
3131 "version": "0.0.0",
32+ "dependencies": {
33+ "@executor/react": "workspace:*",
34+ "@executor/server": "workspace:*",
35+ "react": "^19.1.0",
36+ "react-dom": "^19.1.0",
37+ },
38+ "devDependencies": {
39+ "@types/node": "catalog:",
40+ "@types/react": "^19.1.0",
41+ "@types/react-dom": "^19.1.0",
42+ "@vitejs/plugin-react": "^4.5.2",
43+ "typescript": "^5.9.3",
44+ "vite": "^6.3.5",
45+ },
46+ },
47+ "packages/clients/react": {
48+ "name": "@executor/react",
49+ "version": "0.0.0",
3250 "dependencies": {
3351 "@effect-atom/atom": "^0.5.3",
3452 "@effect-atom/atom-react": "^0.5.0",
3553 "@effect/platform": "catalog:",
3654 "@executor/api": "workspace:*",
3755 "@executor/sdk": "workspace:*",
38− "@executor/server": "workspace:*",
3956 "effect": "catalog:",
4057 "react": "^19.1.0",
41− "react-dom": "^19.1.0",
4258 },
4359 "devDependencies": {
4460 "@types/node": "catalog:",
4561 "@types/react": "^19.1.0",
46− "@types/react-dom": "^19.1.0",
47− "@vitejs/plugin-react": "^4.5.2",
4862 "typescript": "^5.9.3",
49− "vite": "^6.3.5",
5063 },
5164 },
5265 "packages/core/api": {
@@ -298,6 +311,8 @@ · 245 unmodified lines
298311
299312 "@executor/plugin-openapi": ["@executor/plugin-openapi@workspace:packages/plugins/openapi"],
300313
314+ "@executor/react": ["@executor/react@workspace:packages/clients/react"],
315+
301316 "@executor/runtime-quickjs": ["@executor/runtime-quickjs@workspace:packages/kernel/runtime-quickjs"],
302317
303318 "@executor/sdk": ["@executor/sdk@workspace:packages/core/sdk"],
packages/clients/react/package.json+26−0
@@ -0,0 +1,26 @@
1+{
2+ "name": "@executor/react",
3+ "version": "0.0.0",
4+ "private": true,
5+ "type": "module",
6+ "exports": {
7+ ".": "./src/index.ts"
8+ },
9+ "dependencies": {
10+ "@effect/platform": "catalog:",
11+ "@effect-atom/atom": "^0.5.3",
12+ "@effect-atom/atom-react": "^0.5.0",
13+ "@executor/api": "workspace:*",
14+ "@executor/sdk": "workspace:*",
15+ "effect": "catalog:",
16+ "react": "^19.1.0"
17+ },
18+ "devDependencies": {
19+ "@types/react": "^19.1.0",
20+ "@types/node": "catalog:",
21+ "typescript": "^5.9.3"
22+ },
23+ "scripts": {
24+ "typecheck": "tsc --noEmit"
25+ }
26+}
packages/clients/react/src/atoms.ts+44−0
@@ -0,0 +1,44 @@
1+import type { ScopeId, ToolId, SecretId } from "@executor/sdk";
2+import { ScopeId as ScopeIdSchema } from "@executor/sdk";
3+
4+import { getExecutorClient } from "./client";
5+
6+// ---------------------------------------------------------------------------
7+// Query atoms — typed, cached, reactive
8+// ---------------------------------------------------------------------------
9+
10+const DEFAULT_SCOPE = ScopeIdSchema.make("default");
11+
12+export const toolsAtom = (scopeId: ScopeId = DEFAULT_SCOPE) =>
13+ getExecutorClient().query("tools", "list", {
14+ path: { scopeId },
15+ timeToLive: "30 seconds",
16+ });
17+
18+export const toolSchemaAtom = (scopeId: ScopeId, toolId: ToolId) =>
19+ getExecutorClient().query("tools", "schema", {
20+ path: { scopeId, toolId },
21+ timeToLive: "1 minute",
22+ });
23+
24+export const secretsAtom = (scopeId: ScopeId = DEFAULT_SCOPE) =>
25+ getExecutorClient().query("secrets", "list", {
26+ path: { scopeId },
27+ timeToLive: "30 seconds",
28+ });
29+
30+export const secretStatusAtom = (scopeId: ScopeId, secretId: SecretId) =>
31+ getExecutorClient().query("secrets", "status", {
32+ path: { scopeId, secretId },
33+ timeToLive: "15 seconds",
34+ });
35+
36+// ---------------------------------------------------------------------------
37+// Mutation atoms
38+// ---------------------------------------------------------------------------
39+
40+export const invokeTool = getExecutorClient().mutation("tools", "invoke");
41+
42+export const setSecret = getExecutorClient().mutation("secrets", "set");
43+
44+export const removeSecret = getExecutorClient().mutation("secrets", "remove");
packages/clients/react/src/base-url.ts+12−0
@@ -0,0 +1,12 @@
1+const DEFAULT_BASE_URL = "http://127.0.0.1:4000";
2+
3+let baseUrl =
4+ typeof window !== "undefined" && typeof window.location?.origin === "string"
5+ ? window.location.origin
6+ : DEFAULT_BASE_URL;
7+
8+export const getBaseUrl = (): string => baseUrl;
9+
10+export const setBaseUrl = (url: string): void => {
11+ baseUrl = url;
12+};
packages/clients/react/src/client.ts+23−0
@@ -0,0 +1,23 @@
1+import { AtomHttpApi } from "@effect-atom/atom-react";
2+import { FetchHttpClient } from "@effect/platform";
3+import { ExecutorApi } from "@executor/api";
4+
5+import { getBaseUrl } from "./base-url";
6+
7+// ---------------------------------------------------------------------------
8+// Typed HTTP API client — cached per base URL
9+// ---------------------------------------------------------------------------
10+
11+class ExecutorApiClient extends AtomHttpApi.Tag<ExecutorApiClient>()(
12+ "ExecutorApiClient",
13+ {
14+ api: ExecutorApi,
15+ httpClient: FetchHttpClient.layer,
16+ baseUrl: getBaseUrl(),
17+ },
18+) {}
19+
20+export { ExecutorApiClient };
21+
22+export const getExecutorClient = (): typeof ExecutorApiClient =>
23+ ExecutorApiClient;
packages/clients/react/src/index.ts+34−0
@@ -0,0 +1,34 @@
1+// Re-export effect-atom essentials so consumers don't need direct deps
2+export {
3+ Atom,
4+ AtomHttpApi,
5+ Result,
6+ RegistryContext,
7+ RegistryProvider,
8+ useAtom,
9+ useAtomMount,
10+ useAtomRefresh,
11+ useAtomSet,
12+ useAtomSuspense,
13+ useAtomValue,
14+} from "@effect-atom/atom-react";
15+
16+// Base URL management
17+export { getBaseUrl, setBaseUrl } from "./base-url";
18+
19+// Typed API client
20+export { getExecutorClient } from "./client";
21+
22+// Query & mutation atoms
23+export {
24+ toolsAtom,
25+ toolSchemaAtom,
26+ secretsAtom,
27+ secretStatusAtom,
28+ invokeTool,
29+ setSecret,
30+ removeSecret,
31+} from "./atoms";
32+
33+// Provider
34+export { ExecutorProvider } from "./provider";
packages/clients/react/src/provider.tsx+6−0
@@ -0,0 +1,6 @@
1+import { RegistryProvider } from "@effect-atom/atom-react";
2+import * as React from "react";
3+
4+export const ExecutorProvider = (props: React.PropsWithChildren) => (
5+ <RegistryProvider>{props.children}</RegistryProvider>
6+);
packages/clients/react/tsconfig.json+22−0
@@ -0,0 +1,22 @@
1+{
2+ "compilerOptions": {
3+ "target": "ESNext",
4+ "module": "ESNext",
5+ "moduleResolution": "bundler",
6+ "strict": true,
7+ "esModuleInterop": true,
8+ "skipLibCheck": true,
9+ "outDir": "dist",
10+ "rootDir": "src",
11+ "declaration": true,
12+ "declarationMap": true,
13+ "sourceMap": true,
14+ "jsx": "react-jsx",
15+ "plugins": [
16+ {
17+ "name": "@effect/language-service"
18+ }
19+ ]
20+ },
21+ "include": ["src"]
22+}