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
4 "private": true,4 "private": true,
5 "type": "module",5 "type": "module",
6 "dependencies": {6 "dependencies": {
7 "@effect/platform": "catalog:",7 "@executor/react": "workspace:*",
8 "@effect-atom/atom": "^0.5.3",
9 "@effect-atom/atom-react": "^0.5.0",
10 "@executor/api": "workspace:*",
11 "@executor/sdk": "workspace:*",
12 "@executor/server": "workspace:*",8 "@executor/server": "workspace:*",
13 "effect": "catalog:",
14 "react": "^19.1.0",9 "react": "^19.1.0",
15 "react-dom": "^19.1.0"10 "react-dom": "^19.1.0"
16 },11 },
apps/web/src/App.tsx+8−7
@@ -1,8 +1,9 @@
1import React, { Suspense } from "react";1import React from "react";
2import { RegistryProvider, useAtomValue, Result } from "@effect-atom/atom-react";2import {
3 3 ExecutorProvider,
4import { ExecutorClient } from "./api/client";4 useAtomValue,
5import { toolsAtom } from "./api/atoms";5 toolsAtom,
6} from "@executor/react";
6 7
7function ToolList() {8function ToolList() {
8 const tools = useAtomValue(toolsAtom());9 const tools = useAtomValue(toolsAtom());
@@ -36,11 +37,11 @@ function ToolList() { · 27 unmodified lines
36 37
37export function App() {38export function App() {
38 return (39 return (
39 <RegistryProvider>40 <ExecutorProvider>
40 <div style={{ fontFamily: "system-ui", padding: "2rem" }}>41 <div style={{ fontFamily: "system-ui", padding: "2rem" }}>
41 <h1>Executor</h1>42 <h1>Executor</h1>
42 <ToolList />43 <ToolList />
43 </div>44 </div>
44 </RegistryProvider>45 </ExecutorProvider>
45 );46 );
46}47}
apps/web/src/api/atoms.ts+0−40
@@ -1,40 +0,0 @@
1import { ScopeId, ToolId, SecretId } from "@executor/sdk";
2import { ExecutorClient } from "./client";
3
4// ---------------------------------------------------------------------------
5// Query atoms — typed, cached, reactive
6// ---------------------------------------------------------------------------
7
8export const toolsAtom = (scopeId: ScopeId = ScopeId.make("default")) =>
9 ExecutorClient.query("tools", "list", {
10 path: { scopeId },
11 timeToLive: "30 seconds",
12 });
13
14export const toolSchemaAtom = (scopeId: ScopeId, toolId: ToolId) =>
15 ExecutorClient.query("tools", "schema", {
16 path: { scopeId, toolId },
17 timeToLive: "1 minute",
18 });
19
20export const secretsAtom = (scopeId: ScopeId = ScopeId.make("default")) =>
21 ExecutorClient.query("secrets", "list", {
22 path: { scopeId },
23 timeToLive: "30 seconds",
24 });
25
26export 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
36export const invokeTool = ExecutorClient.mutation("tools", "invoke");
37
38export const setSecret = ExecutorClient.mutation("secrets", "set");
39
40export const removeSecret = ExecutorClient.mutation("secrets", "remove");
apps/web/src/api/client.ts+0−15
@@ -1,15 +0,0 @@
1import { AtomHttpApi } from "@effect-atom/atom-react";
2import { FetchHttpClient } from "@effect/platform";
3import { ExecutorApi } from "@executor/api";
4
5// ---------------------------------------------------------------------------
6// Typed HTTP API client — uses effect-atom's AtomHttpApi.Tag
7// ---------------------------------------------------------------------------
8
9class ExecutorClient extends AtomHttpApi.Tag<ExecutorClient>()("ExecutorClient", {
10 api: ExecutorApi,
11 httpClient: FetchHttpClient.layer,
12 baseUrl: "/",
13}) {}
14
15export { ExecutorClient };
apps/web/src/api/index.ts+0−10
@@ -1,10 +0,0 @@
1export { ExecutorClient } from "./client";
2export {
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
29 "apps/web": {29 "apps/web": {
30 "name": "@executor/web",30 "name": "@executor/web",
31 "version": "0.0.0",31 "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",
32 "dependencies": {50 "dependencies": {
33 "@effect-atom/atom": "^0.5.3",51 "@effect-atom/atom": "^0.5.3",
34 "@effect-atom/atom-react": "^0.5.0",52 "@effect-atom/atom-react": "^0.5.0",
35 "@effect/platform": "catalog:",53 "@effect/platform": "catalog:",
36 "@executor/api": "workspace:*",54 "@executor/api": "workspace:*",
37 "@executor/sdk": "workspace:*",55 "@executor/sdk": "workspace:*",
38 "@executor/server": "workspace:*",
39 "effect": "catalog:",56 "effect": "catalog:",
40 "react": "^19.1.0",57 "react": "^19.1.0",
41 "react-dom": "^19.1.0",
42 },58 },
43 "devDependencies": {59 "devDependencies": {
44 "@types/node": "catalog:",60 "@types/node": "catalog:",
45 "@types/react": "^19.1.0",61 "@types/react": "^19.1.0",
46 "@types/react-dom": "^19.1.0",
47 "@vitejs/plugin-react": "^4.5.2",
48 "typescript": "^5.9.3",62 "typescript": "^5.9.3",
49 "vite": "^6.3.5",
50 },63 },
51 },64 },
52 "packages/core/api": {65 "packages/core/api": {
@@ -298,6 +311,8 @@ · 245 unmodified lines
298 311
299 "@executor/plugin-openapi": ["@executor/plugin-openapi@workspace:packages/plugins/openapi"],312 "@executor/plugin-openapi": ["@executor/plugin-openapi@workspace:packages/plugins/openapi"],
300 313
314 "@executor/react": ["@executor/react@workspace:packages/clients/react"],
315
301 "@executor/runtime-quickjs": ["@executor/runtime-quickjs@workspace:packages/kernel/runtime-quickjs"],316 "@executor/runtime-quickjs": ["@executor/runtime-quickjs@workspace:packages/kernel/runtime-quickjs"],
302 317
303 "@executor/sdk": ["@executor/sdk@workspace:packages/core/sdk"],318 "@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 @@
1import type { ScopeId, ToolId, SecretId } from "@executor/sdk";
2import { ScopeId as ScopeIdSchema } from "@executor/sdk";
3
4import { getExecutorClient } from "./client";
5
6// ---------------------------------------------------------------------------
7// Query atoms — typed, cached, reactive
8// ---------------------------------------------------------------------------
9
10const DEFAULT_SCOPE = ScopeIdSchema.make("default");
11
12export const toolsAtom = (scopeId: ScopeId = DEFAULT_SCOPE) =>
13 getExecutorClient().query("tools", "list", {
14 path: { scopeId },
15 timeToLive: "30 seconds",
16 });
17
18export const toolSchemaAtom = (scopeId: ScopeId, toolId: ToolId) =>
19 getExecutorClient().query("tools", "schema", {
20 path: { scopeId, toolId },
21 timeToLive: "1 minute",
22 });
23
24export const secretsAtom = (scopeId: ScopeId = DEFAULT_SCOPE) =>
25 getExecutorClient().query("secrets", "list", {
26 path: { scopeId },
27 timeToLive: "30 seconds",
28 });
29
30export 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
40export const invokeTool = getExecutorClient().mutation("tools", "invoke");
41
42export const setSecret = getExecutorClient().mutation("secrets", "set");
43
44export const removeSecret = getExecutorClient().mutation("secrets", "remove");
packages/clients/react/src/base-url.ts+12−0
@@ -0,0 +1,12 @@
1const DEFAULT_BASE_URL = "http://127.0.0.1:4000";
2
3let baseUrl =
4 typeof window !== "undefined" && typeof window.location?.origin === "string"
5 ? window.location.origin
6 : DEFAULT_BASE_URL;
7
8export const getBaseUrl = (): string => baseUrl;
9
10export const setBaseUrl = (url: string): void => {
11 baseUrl = url;
12};
packages/clients/react/src/client.ts+23−0
@@ -0,0 +1,23 @@
1import { AtomHttpApi } from "@effect-atom/atom-react";
2import { FetchHttpClient } from "@effect/platform";
3import { ExecutorApi } from "@executor/api";
4
5import { getBaseUrl } from "./base-url";
6
7// ---------------------------------------------------------------------------
8// Typed HTTP API client — cached per base URL
9// ---------------------------------------------------------------------------
10
11class ExecutorApiClient extends AtomHttpApi.Tag<ExecutorApiClient>()(
12 "ExecutorApiClient",
13 {
14 api: ExecutorApi,
15 httpClient: FetchHttpClient.layer,
16 baseUrl: getBaseUrl(),
17 },
18) {}
19
20export { ExecutorApiClient };
21
22export 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
2export {
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
17export { getBaseUrl, setBaseUrl } from "./base-url";
18
19// Typed API client
20export { getExecutorClient } from "./client";
21
22// Query & mutation atoms
23export {
24 toolsAtom,
25 toolSchemaAtom,
26 secretsAtom,
27 secretStatusAtom,
28 invokeTool,
29 setSecret,
30 removeSecret,
31} from "./atoms";
32
33// Provider
34export { ExecutorProvider } from "./provider";
packages/clients/react/src/provider.tsx+6−0
@@ -0,0 +1,6 @@
1import { RegistryProvider } from "@effect-atom/atom-react";
2import * as React from "react";
3
4export 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}