History

add discovery, connect, and OAuth hooks to the React SDK

Rename useConnectMcpSource to useStartSourceOAuth and add new hooks: useDiscoverSource, useConnectSource, useRefreshSecrets. Enhance the mutation system with OptimisticMutationResult type and add dev-mode error logging for failed API calls.

Rhys Sullivan committed 8 months ago

Commit 049bed41 parent af725aa1 file · +155 −41Browse files

Unified · Split

packages/react/src/index.ts+155−41
@@ -3,20 +3,25 @@ import type * as Registry from "@effect-atom/atom/Registry"; · 2 unmodified lines
3import { RegistryContext, RegistryProvider, useAtomValue } from "@effect-atom/atom-react";3import { RegistryContext, RegistryProvider, useAtomValue } from "@effect-atom/atom-react";
4import { createControlPlaneClient } from "@executor-v3/control-plane/client";4import { createControlPlaneClient } from "@executor-v3/control-plane/client";
5import type {5import type {
6 ConnectMcpSourcePayload,6 CompleteSourceOAuthResult,
7 ConnectMcpSourceResult,7 ConnectSourcePayload,
8 ConnectSourceResult,
8 ControlPlaneClient,9 ControlPlaneClient,
9 CreateSecretPayload,10 CreateSecretPayload,
10 CreateSecretResult,11 CreateSecretResult,
11 CreateSourcePayload,12 CreateSourcePayload,
12 DeleteSecretResult,13 DeleteSecretResult,
14 DiscoverSourcePayload,
13 InstanceConfig,15 InstanceConfig,
14 LocalInstallation,16 LocalInstallation,
15 SecretListItem,17 SecretListItem,
16 Source,18 Source,
19 SourceDiscoveryResult,
17 SourceInspection,20 SourceInspection,
18 SourceInspectionDiscoverResult,21 SourceInspectionDiscoverResult,
19 SourceInspectionToolDetail,22 SourceInspectionToolDetail,
23 StartSourceOAuthPayload,
24 StartSourceOAuthResult,
20 UpdateSecretPayload,25 UpdateSecretPayload,
21 UpdateSecretResult,26 UpdateSecretResult,
22 UpdateSourcePayload,27 UpdateSourcePayload,
@@ -87,9 +92,25 @@ type MutationExecutionContext = { · 64 unmodified lines
87 invalidateQueries: (target?: InvalidationTarget) => void;92 invalidateQueries: (target?: InvalidationTarget) => void;
88};93};
89 94
90type MutationOptions<TInput, TOutput> = {95type OptimisticMutationResult<T> =
91 optimisticUpdate?: (context: MutationExecutionContext, payload: TInput) => void | (() => void);96 | void
92 onSuccess?: (context: MutationExecutionContext, payload: TInput, data: TOutput) => void;97 | (() => void)
98 | {
99 rollback?: () => void;
100 value?: T;
101 };
102
103type MutationOptions<TInput, TOutput, TOptimistic = never> = {
104 optimisticUpdate?: (
105 context: MutationExecutionContext,
106 payload: TInput,
107 ) => OptimisticMutationResult<TOptimistic>;
108 onSuccess?: (
109 context: MutationExecutionContext,
110 payload: TInput,
111 data: TOutput,
112 optimisticValue: TOptimistic | undefined,
113 ) => void;
93};114};
94 115
95type InternalNode<A> = {116type InternalNode<A> = {
@@ -146,6 +167,37 @@ const causeMessage = (cause: Cause.Cause<unknown>): Error => · 50 unmodified lines
146const toError = (cause: unknown): Error =>167const toError = (cause: unknown): Error =>
147 cause instanceof Error ? cause : new Error(String(cause));168 cause instanceof Error ? cause : new Error(String(cause));
148 169
170const shouldLogExecutorDevErrors = (): boolean => {
171 if (typeof window === "undefined") {
172 return false;
173 }
174
175 const hostname = window.location.hostname;
176 return hostname === "localhost" || hostname === "127.0.0.1";
177};
178
179const describeExecutorDevError = (cause: unknown): Record<string, unknown> => {
180 if (cause instanceof Error) {
181 return {
182 name: cause.name,
183 message: cause.message,
184 stack: cause.stack,
185 };
186 }
187
188 return {
189 message: String(cause),
190 };
191};
192
193const logExecutorDevError = (label: string, details: Record<string, unknown>): void => {
194 if (!shouldLogExecutorDevErrors()) {
195 return;
196 }
197
198 console.error(`[executor react] ${label}`, details);
199};
200
149const runControlPlane = <A>(input: {201const runControlPlane = <A>(input: {
150 accountId?: string;202 accountId?: string;
151 execute: (client: ControlPlaneClient) => Effect.Effect<A, unknown, never>;203 execute: (client: ControlPlaneClient) => Effect.Effect<A, unknown, never>;
@@ -157,7 +209,15 @@ const runControlPlane = <A>(input: { · 5 unmodified lines
157 baseUrl: apiBaseUrl,209 baseUrl: apiBaseUrl,
158 ...(accountId !== undefined ? { accountId } : {}),210 ...(accountId !== undefined ? { accountId } : {}),
159 }).pipe(Effect.flatMap(input.execute)),211 }).pipe(Effect.flatMap(input.execute)),
160 );212 ).catch((cause) => {
213 logExecutorDevError("control-plane request failed", {
214 baseUrl: apiBaseUrl,
215 accountId,
216 error: describeExecutorDevError(cause),
217 cause,
218 });
219 throw cause;
220 });
161};221};
162 222
163const controlPlaneRequest = <A>(input: {223const controlPlaneRequest = <A>(input: {
@@ -576,13 +636,13 @@ const applyUpdatePayloadToSource = (source: Source, payload: UpdateSourcePayload · 412 unmodified lines
576 updatedAt: Date.now(),636 updatedAt: Date.now(),
577});637});
578 638
579const useSourceMutation = <TInput, TOutput>(639const useSourceMutation = <TInput, TOutput, TOptimistic = never>(
580 execute: (input: {640 execute: (input: {
581 workspaceId: Source["workspaceId"];641 workspaceId: Source["workspaceId"];
582 accountId: string;642 accountId: string;
583 payload: TInput;643 payload: TInput;
584 }) => Promise<TOutput>,644 }) => Promise<TOutput>,
585 options?: MutationOptions<TInput, TOutput>,645 options?: MutationOptions<TInput, TOutput, TOptimistic>,
586) => {646) => {
587 const workspace = useWorkspaceRequestContext();647 const workspace = useWorkspaceRequestContext();
588 const { registry, invalidateQueries } = useExecutorQueryContext();648 const { registry, invalidateQueries } = useExecutorQueryContext();
@@ -612,7 +672,13 @@ const useSourceMutation = <TInput, TOutput>( · 23 unmodified lines
612 error: null,672 error: null,
613 }));673 }));
614 674
615 const rollback = options?.optimisticUpdate?.(executionContext, payload);675 const optimistic = options?.optimisticUpdate?.(executionContext, payload);
676 const rollback = typeof optimistic === "function"
677 ? optimistic
678 : optimistic?.rollback;
679 const optimisticValue = typeof optimistic === "function"
680 ? undefined
681 : optimistic?.value;
616 682
617 try {683 try {
618 const data = await execute({684 const data = await execute({
@@ -620,11 +686,18 @@ const useSourceMutation = <TInput, TOutput>( · 1 unmodified line
620 accountId: workspace.accountId,686 accountId: workspace.accountId,
621 payload,687 payload,
622 });688 });
623 options?.onSuccess?.(executionContext, payload, data);689 options?.onSuccess?.(executionContext, payload, data, optimisticValue);
624 setState({ status: "success", data, error: null });690 setState({ status: "success", data, error: null });
625 return data;691 return data;
626 } catch (cause) {692 } catch (cause) {
627 rollback?.();693 rollback?.();
694 logExecutorDevError("source mutation failed", {
695 workspaceId: workspace.workspaceId,
696 accountId: workspace.accountId,
697 payload,
698 error: describeExecutorDevError(cause),
699 cause,
700 });
628 const error = cause instanceof Error ? cause : new Error(String(cause));701 const error = cause instanceof Error ? cause : new Error(String(cause));
629 setState({ status: "error", data: null, error });702 setState({ status: "error", data: null, error });
630 throw error;703 throw error;
@@ -895,7 +968,7 @@ export const usePrefetchToolDetail = () => { · 264 unmodified lines
895};968};
896 969
897export const useCreateSource = () =>970export const useCreateSource = () =>
898 useSourceMutation<CreateSourcePayload, Source>(971 useSourceMutation<CreateSourcePayload, Source, Source>(
899 React.useCallback(972 React.useCallback(
900 ({ workspaceId, accountId, payload }) =>973 ({ workspaceId, accountId, payload }) =>
901 runControlPlane({974 runControlPlane({
@@ -922,15 +995,21 @@ export const useCreateSource = () => · 20 unmodified lines
922 payload,995 payload,
923 });996 });
924 setCachedAtomValue(context.registry, listAtom, [optimisticSource, ...previousList]);997 setCachedAtomValue(context.registry, listAtom, [optimisticSource, ...previousList]);
925 return () => {998 return {
926 setCachedAtomValue(context.registry, listAtom, previousList);999 value: optimisticSource,
1000 rollback: () => {
1001 setCachedAtomValue(context.registry, listAtom, previousList);
1002 },
927 };1003 };
928 },1004 },
929 onSuccess: (context, _payload, source) => {1005 onSuccess: (context, _payload, source, optimisticSource) => {
930 const listAtom = sourcesAtom(encodeSourcesKey(true, context.workspaceId, context.accountId));1006 const listAtom = sourcesAtom(encodeSourcesKey(true, context.workspaceId, context.accountId));
931 const currentList = getCachedAtomValue(context.registry, listAtom);1007 const currentList = getCachedAtomValue(context.registry, listAtom);
932 if (currentList !== undefined) {1008 if (currentList !== undefined) {
933 setCachedAtomValue(context.registry, listAtom, upsertSourceInList(currentList, source));1009 const withoutOptimistic = optimisticSource
1010 ? currentList.filter((candidate) => candidate.id !== optimisticSource.id)
1011 : currentList;
1012 setCachedAtomValue(context.registry, listAtom, upsertSourceInList(withoutOptimistic, source));
934 }1013 }
935 1014
936 setCachedAtomValue(1015 setCachedAtomValue(
@@ -1003,13 +1082,13 @@ export const useUpdateSource = () => · 66 unmodified lines
1003 },1082 },
1004 );1083 );
1005 1084
1006export const useConnectMcpSource = () =>1085export const useStartSourceOAuth = () =>
1007 useSourceMutation<ConnectMcpSourcePayload, ConnectMcpSourceResult>(1086 useSourceMutation<StartSourceOAuthPayload, StartSourceOAuthResult>(
1008 React.useCallback(1087 React.useCallback(
1009 ({ workspaceId, accountId, payload }) =>1088 ({ workspaceId, accountId, payload }) =>
1010 runControlPlane({1089 runControlPlane({
1011 accountId,1090 accountId,
1012 execute: (client) => client.sources.connectMcp({1091 execute: (client) => client.oauth.startSourceAuth({
1013 path: {1092 path: {
1014 workspaceId,1093 workspaceId,
1015 },1094 },
@@ -1018,27 +1097,6 @@ export const useConnectMcpSource = () => · 2 unmodified lines
1018 }),1097 }),
1019 [],1098 [],
1020 ),1099 ),
1021 {
1022 onSuccess: (context, _payload, result) => {
1023 const listAtom = sourcesAtom(encodeSourcesKey(true, context.workspaceId, context.accountId));
1024 const currentList = getCachedAtomValue(context.registry, listAtom);
1025 if (currentList !== undefined) {
1026 setCachedAtomValue(context.registry, listAtom, upsertSourceInList(currentList, result.source));
1027 }
1028
1029 setCachedAtomValue(
1030 context.registry,
1031 sourceAtom(encodeSourceKey(true, context.workspaceId, context.accountId, result.source.id)),
1032 result.source,
1033 );
1034
1035 context.invalidateQueries({
1036 workspaceId: context.workspaceId,
1037 accountId: context.accountId,
1038 sourceId: result.source.id,
1039 });
1040 },
1041 },
1042 );1100 );
1043 1101
1044export const useRemoveSource = () =>1102export const useRemoveSource = () =>
@@ -1079,20 +1137,76 @@ export const useRemoveSource = () => · 34 unmodified lines
1079 },1137 },
1080 );1138 );
1081 1139
1140export const useDiscoverSource = () =>
1141 useSourceMutation<DiscoverSourcePayload, SourceDiscoveryResult>(
1142 React.useCallback(
1143 ({ accountId, payload }) =>
1144 runControlPlane({
1145 accountId,
1146 execute: (client) => client.sources.discover({
1147 payload,
1148 }),
1149 }),
1150 [],
1151 ),
1152 );
1153
1154export const useConnectSource = () =>
1155 useSourceMutation<ConnectSourcePayload, ConnectSourceResult, Source>(
1156 React.useCallback(
1157 ({ workspaceId, accountId, payload }) =>
1158 runControlPlane({
1159 accountId,
1160 execute: (client) => client.sources.connect({
1161 path: {
1162 workspaceId,
1163 },
1164 payload,
1165 } as any),
1166 }),
1167 [],
1168 ),
1169 {
1170 onSuccess: (context, _payload, result) => {
1171 const listAtom = sourcesAtom(encodeSourcesKey(true, context.workspaceId, context.accountId));
1172 const currentList = getCachedAtomValue(context.registry, listAtom);
1173 if (currentList !== undefined) {
1174 setCachedAtomValue(context.registry, listAtom, upsertSourceInList(currentList, result.source));
1175 }
1176
1177 setCachedAtomValue(
1178 context.registry,
1179 sourceAtom(encodeSourceKey(true, context.workspaceId, context.accountId, result.source.id)),
1180 result.source,
1181 );
1182 context.invalidateQueries({
1183 workspaceId: context.workspaceId,
1184 accountId: context.accountId,
1185 });
1186 },
1187 },
1188 );
1189
1082export type {1190export type {
1083 ConnectMcpSourcePayload,1191 CompleteSourceOAuthResult,
1084 ConnectMcpSourceResult,1192 ConnectSourcePayload,
1193 ConnectSourceResult,
1085 CreateSecretPayload,1194 CreateSecretPayload,
1086 CreateSecretResult,1195 CreateSecretResult,
1087 CreateSourcePayload,1196 CreateSourcePayload,
1088 DeleteSecretResult,1197 DeleteSecretResult,
1198 DiscoverSourcePayload,
1089 InstanceConfig,1199 InstanceConfig,
1090 LocalInstallation,1200 LocalInstallation,
1201
1091 SecretListItem,1202 SecretListItem,
1092 Source,1203 Source,
1204 SourceDiscoveryResult,
1093 SourceInspection,1205 SourceInspection,
1094 SourceInspectionDiscoverResult,1206 SourceInspectionDiscoverResult,
1095 SourceInspectionToolDetail,1207 SourceInspectionToolDetail,
1208 StartSourceOAuthPayload,
1209 StartSourceOAuthResult,
1096 UpdateSecretPayload,1210 UpdateSecretPayload,
1097 UpdateSecretResult,1211 UpdateSecretResult,
1098 UpdateSourcePayload,1212 UpdateSourcePayload,