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
33 import { RegistryContext, RegistryProvider, useAtomValue } from "@effect-atom/atom-react";
44 import { createControlPlaneClient } from "@executor-v3/control-plane/client";
55 import type {
6− ConnectMcpSourcePayload,
7− ConnectMcpSourceResult,
6+ CompleteSourceOAuthResult,
7+ ConnectSourcePayload,
8+ ConnectSourceResult,
89 ControlPlaneClient,
910 CreateSecretPayload,
1011 CreateSecretResult,
1112 CreateSourcePayload,
1213 DeleteSecretResult,
14+ DiscoverSourcePayload,
1315 InstanceConfig,
1416 LocalInstallation,
1517 SecretListItem,
1618 Source,
19+ SourceDiscoveryResult,
1720 SourceInspection,
1821 SourceInspectionDiscoverResult,
1922 SourceInspectionToolDetail,
23+ StartSourceOAuthPayload,
24+ StartSourceOAuthResult,
2025 UpdateSecretPayload,
2126 UpdateSecretResult,
2227 UpdateSourcePayload,
@@ -87,9 +92,25 @@ type MutationExecutionContext = { · 64 unmodified lines
8792 invalidateQueries: (target?: InvalidationTarget) => void;
8893 };
8994
90−type MutationOptions<TInput, TOutput> = {
91− optimisticUpdate?: (context: MutationExecutionContext, payload: TInput) => void | (() => void);
92− onSuccess?: (context: MutationExecutionContext, payload: TInput, data: TOutput) => void;
95+type OptimisticMutationResult<T> =
96+ | void
97+ | (() => void)
98+ | {
99+ rollback?: () => void;
100+ value?: T;
101+ };
102+
103+type 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;
93114 };
94115
95116 type InternalNode<A> = {
@@ -146,6 +167,37 @@ const causeMessage = (cause: Cause.Cause<unknown>): Error => · 50 unmodified lines
146167 const toError = (cause: unknown): Error =>
147168 cause instanceof Error ? cause : new Error(String(cause));
148169
170+const 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+
179+const 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+
193+const logExecutorDevError = (label: string, details: Record<string, unknown>): void => {
194+ if (!shouldLogExecutorDevErrors()) {
195+ return;
196+ }
197+
198+ console.error(`[executor react] ${label}`, details);
199+};
200+
149201 const runControlPlane = <A>(input: {
150202 accountId?: string;
151203 execute: (client: ControlPlaneClient) => Effect.Effect<A, unknown, never>;
@@ -157,7 +209,15 @@ const runControlPlane = <A>(input: { · 5 unmodified lines
157209 baseUrl: apiBaseUrl,
158210 ...(accountId !== undefined ? { accountId } : {}),
159211 }).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+ });
161221 };
162222
163223 const controlPlaneRequest = <A>(input: {
@@ -576,13 +636,13 @@ const applyUpdatePayloadToSource = (source: Source, payload: UpdateSourcePayload · 412 unmodified lines
576636 updatedAt: Date.now(),
577637 });
578638
579−const useSourceMutation = <TInput, TOutput>(
639+const useSourceMutation = <TInput, TOutput, TOptimistic = never>(
580640 execute: (input: {
581641 workspaceId: Source["workspaceId"];
582642 accountId: string;
583643 payload: TInput;
584644 }) => Promise<TOutput>,
585− options?: MutationOptions<TInput, TOutput>,
645+ options?: MutationOptions<TInput, TOutput, TOptimistic>,
586646 ) => {
587647 const workspace = useWorkspaceRequestContext();
588648 const { registry, invalidateQueries } = useExecutorQueryContext();
@@ -612,7 +672,13 @@ const useSourceMutation = <TInput, TOutput>( · 23 unmodified lines
612672 error: null,
613673 }));
614674
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;
616682
617683 try {
618684 const data = await execute({
@@ -620,11 +686,18 @@ const useSourceMutation = <TInput, TOutput>( · 1 unmodified line
620686 accountId: workspace.accountId,
621687 payload,
622688 });
623− options?.onSuccess?.(executionContext, payload, data);
689+ options?.onSuccess?.(executionContext, payload, data, optimisticValue);
624690 setState({ status: "success", data, error: null });
625691 return data;
626692 } catch (cause) {
627693 rollback?.();
694+ logExecutorDevError("source mutation failed", {
695+ workspaceId: workspace.workspaceId,
696+ accountId: workspace.accountId,
697+ payload,
698+ error: describeExecutorDevError(cause),
699+ cause,
700+ });
628701 const error = cause instanceof Error ? cause : new Error(String(cause));
629702 setState({ status: "error", data: null, error });
630703 throw error;
@@ -895,7 +968,7 @@ export const usePrefetchToolDetail = () => { · 264 unmodified lines
895968 };
896969
897970 export const useCreateSource = () =>
898− useSourceMutation<CreateSourcePayload, Source>(
971+ useSourceMutation<CreateSourcePayload, Source, Source>(
899972 React.useCallback(
900973 ({ workspaceId, accountId, payload }) =>
901974 runControlPlane({
@@ -922,15 +995,21 @@ export const useCreateSource = () => · 20 unmodified lines
922995 payload,
923996 });
924997 setCachedAtomValue(context.registry, listAtom, [optimisticSource, ...previousList]);
925− return () => {
926− setCachedAtomValue(context.registry, listAtom, previousList);
998+ return {
999+ value: optimisticSource,
1000+ rollback: () => {
1001+ setCachedAtomValue(context.registry, listAtom, previousList);
1002+ },
9271003 };
9281004 },
929− onSuccess: (context, _payload, source) => {
1005+ onSuccess: (context, _payload, source, optimisticSource) => {
9301006 const listAtom = sourcesAtom(encodeSourcesKey(true, context.workspaceId, context.accountId));
9311007 const currentList = getCachedAtomValue(context.registry, listAtom);
9321008 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));
9341013 }
9351014
9361015 setCachedAtomValue(
@@ -1003,13 +1082,13 @@ export const useUpdateSource = () => · 66 unmodified lines
10031082 },
10041083 );
10051084
1006−export const useConnectMcpSource = () =>
1007− useSourceMutation<ConnectMcpSourcePayload, ConnectMcpSourceResult>(
1085+export const useStartSourceOAuth = () =>
1086+ useSourceMutation<StartSourceOAuthPayload, StartSourceOAuthResult>(
10081087 React.useCallback(
10091088 ({ workspaceId, accountId, payload }) =>
10101089 runControlPlane({
10111090 accountId,
1012− execute: (client) => client.sources.connectMcp({
1091+ execute: (client) => client.oauth.startSourceAuth({
10131092 path: {
10141093 workspaceId,
10151094 },
@@ -1018,27 +1097,6 @@ export const useConnectMcpSource = () => · 2 unmodified lines
10181097 }),
10191098 [],
10201099 ),
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− },
10421100 );
10431101
10441102 export const useRemoveSource = () =>
@@ -1079,20 +1137,76 @@ export const useRemoveSource = () => · 34 unmodified lines
10791137 },
10801138 );
10811139
1140+export 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+
1154+export 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+
10821190 export type {
1083− ConnectMcpSourcePayload,
1084− ConnectMcpSourceResult,
1191+ CompleteSourceOAuthResult,
1192+ ConnectSourcePayload,
1193+ ConnectSourceResult,
10851194 CreateSecretPayload,
10861195 CreateSecretResult,
10871196 CreateSourcePayload,
10881197 DeleteSecretResult,
1198+ DiscoverSourcePayload,
10891199 InstanceConfig,
10901200 LocalInstallation,
1201+
10911202 SecretListItem,
10921203 Source,
1204+ SourceDiscoveryResult,
10931205 SourceInspection,
10941206 SourceInspectionDiscoverResult,
10951207 SourceInspectionToolDetail,
1208+ StartSourceOAuthPayload,
1209+ StartSourceOAuthResult,
10961210 UpdateSecretPayload,
10971211 UpdateSecretResult,
10981212 UpdateSourcePayload,