Skip to content

Commit 50cb48d

Browse files
authored
[codex] Use effect-atom optimistic resources (#531)
* Add remove in-use toasts * Use effect-atom optimistic resources
1 parent 7265745 commit 50cb48d

17 files changed

Lines changed: 435 additions & 500 deletions

File tree

apps/cloud/src/web/shell.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
33
import { useAtomValue, useAtomSet } from "@effect/atom-react";
44
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
55
import * as Exit from "effect/Exit";
6-
import { useSourcesWithPending } from "@executor-js/react/api/optimistic";
6+
import { sourcesOptimisticAtom } from "@executor-js/react/api/atoms";
77
import { useScope } from "@executor-js/react/api/scope-context";
88
import { Button } from "@executor-js/react/components/button";
99
import { Skeleton } from "@executor-js/react/components/skeleton";
@@ -60,7 +60,7 @@ function NavItem(props: { to: string; label: string; active: boolean; onNavigate
6060

6161
function SourceList(props: { pathname: string; onNavigate?: () => void }) {
6262
const scopeId = useScope();
63-
const sources = useSourcesWithPending(scopeId);
63+
const sources = useAtomValue(sourcesOptimisticAtom(scopeId));
6464

6565
return AsyncResult.match(sources, {
6666
onInitial: () => (

apps/local/src/web/shell.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { Link, Outlet, useLocation } from "@tanstack/react-router";
22
import { useCallback, useEffect, useRef, useState } from "react";
3-
import { useAtomRefresh } from "@effect/atom-react";
3+
import { useAtomRefresh, useAtomValue } from "@effect/atom-react";
44
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5-
import { sourcesAtom, toolsAtom } from "@executor-js/react/api/atoms";
6-
import { useSourcesWithPending } from "@executor-js/react/api/optimistic";
5+
import { sourcesAtom, sourcesOptimisticAtom, toolsAtom } from "@executor-js/react/api/atoms";
76
import { useScope, useScopeInfo } from "@executor-js/react/api/scope-context";
87
import { Button } from "@executor-js/react/components/button";
98
import { SourceFavicon } from "@executor-js/react/components/source-favicon";
@@ -261,7 +260,7 @@ function PluginNav(props: { pathname: string; onNavigate?: () => void }) {
261260

262261
function SourceList(props: { pathname: string; onNavigate?: () => void }) {
263262
const scopeId = useScope();
264-
const sources = useSourcesWithPending(scopeId);
263+
const sources = useAtomValue(sourcesOptimisticAtom(scopeId));
265264

266265
return AsyncResult.match(sources, {
267266
onInitial: () => <div className="px-2.5 py-2 text-xs text-muted-foreground">Loading…</div>,

packages/plugins/google-discovery/src/react/AddGoogleDiscoverySource.tsx

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
22
import { useAtomSet } from "@effect/atom-react";
3+
import * as Exit from "effect/Exit";
4+
import * as Option from "effect/Option";
35

4-
import { usePendingSources } from "@executor-js/react/api/optimistic";
56
import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";
67
import { useScope } from "@executor-js/react/api/scope-context";
78
import type { SecretPickerSecret } from "@executor-js/react/plugins/secret-picker";
@@ -44,7 +45,7 @@ import { FloatActions } from "@executor-js/react/components/float-actions";
4445
import { Input } from "@executor-js/react/components/input";
4546
import { RadioGroup, RadioGroupItem } from "@executor-js/react/components/radio-group";
4647
import { IOSSpinner, Spinner } from "@executor-js/react/components/spinner";
47-
import { addGoogleDiscoverySource, probeGoogleDiscovery } from "./atoms";
48+
import { addGoogleDiscoverySourceOptimistic, probeGoogleDiscovery } from "./atoms";
4849
import { GOOGLE_DISCOVERY_OAUTH_POPUP_NAME, googleDiscoveryOAuthStrategy } from "./oauth";
4950
import { googleDiscoveryPresets, type GoogleDiscoveryPreset } from "../sdk/presets";
5051

@@ -203,8 +204,9 @@ export default function AddGoogleDiscoverySource(props: {
203204

204205
const scopeId = useScope();
205206
const doProbe = useAtomSet(probeGoogleDiscovery, { mode: "promise" });
206-
const doAdd = useAtomSet(addGoogleDiscoverySource, { mode: "promise" });
207-
const { beginAdd } = usePendingSources();
207+
const doAdd = useAtomSet(addGoogleDiscoverySourceOptimistic(scopeId), {
208+
mode: "promiseExit",
209+
});
208210
const secretList = useSecretPickerSecrets();
209211
const oauth = useOAuthPopupFlow({
210212
popupName: GOOGLE_DISCOVERY_OAUTH_POPUP_NAME,
@@ -326,38 +328,36 @@ export default function AddGoogleDiscoverySource(props: {
326328
setError(null);
327329
const displayName = identity.name.trim() || probe.name;
328330
const namespace = resolvedNamespace;
329-
const placeholder = beginAdd({
330-
id: namespace,
331-
name: displayName,
332-
kind: "google-discovery",
331+
const exit = await doAdd({
332+
params: { scopeId },
333+
payload: {
334+
name: displayName,
335+
discoveryUrl: discoveryUrl.trim(),
336+
namespace,
337+
auth:
338+
authKind === "oauth2" && oauthAuth
339+
? {
340+
kind: "oauth2" as const,
341+
connectionId: oauthAuth.connectionId,
342+
clientIdSecretId: oauthAuth.clientIdSecretId,
343+
clientSecretSecretId: oauthAuth.clientSecretSecretId,
344+
scopes: oauthAuth.scopes,
345+
}
346+
: { kind: "none" as const },
347+
},
348+
reactivityKeys: [...sourceWriteKeys],
333349
});
334-
try {
335-
await doAdd({
336-
params: { scopeId },
337-
payload: {
338-
name: displayName,
339-
discoveryUrl: discoveryUrl.trim(),
340-
namespace,
341-
auth:
342-
authKind === "oauth2" && oauthAuth
343-
? {
344-
kind: "oauth2" as const,
345-
connectionId: oauthAuth.connectionId,
346-
clientIdSecretId: oauthAuth.clientIdSecretId,
347-
clientSecretSecretId: oauthAuth.clientSecretSecretId,
348-
scopes: oauthAuth.scopes,
349-
}
350-
: { kind: "none" as const },
351-
},
352-
reactivityKeys: [...sourceWriteKeys],
353-
});
354-
props.onComplete();
355-
} catch (e) {
356-
setError(e instanceof Error ? e.message : "Failed to add source");
350+
if (Exit.isFailure(exit)) {
351+
const error = Exit.findErrorOption(exit);
352+
setError(
353+
Option.isSome(error) && error.value instanceof Error
354+
? error.value.message
355+
: "Failed to add source",
356+
);
357357
setAdding(false);
358-
} finally {
359-
placeholder.done();
358+
return;
360359
}
360+
props.onComplete();
361361
}, [
362362
probe,
363363
doAdd,
@@ -367,7 +367,6 @@ export default function AddGoogleDiscoverySource(props: {
367367
oauthAuth,
368368
props,
369369
scopeId,
370-
beginAdd,
371370
resolvedNamespace,
372371
]);
373372

packages/plugins/google-discovery/src/react/atoms.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import type { ScopeId } from "@executor-js/sdk/core";
2+
import * as Atom from "effect/unstable/reactivity/Atom";
3+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4+
import { sourcesOptimisticAtom } from "@executor-js/react/api/atoms";
25
import { ReactivityKey } from "@executor-js/react/api/reactivity-keys";
36
import { GoogleDiscoveryClient } from "./client";
47

@@ -25,6 +28,32 @@ export const addGoogleDiscoverySource = GoogleDiscoveryClient.mutation(
2528
"googleDiscovery",
2629
"addSource",
2730
);
31+
export const addGoogleDiscoverySourceOptimistic = Atom.family((scopeId: ScopeId) =>
32+
sourcesOptimisticAtom(scopeId).pipe(
33+
Atom.optimisticFn({
34+
reducer: (current, arg) =>
35+
AsyncResult.map(current, (rows) => {
36+
const id = arg.payload.namespace ?? `pending-${Math.random().toString(36).slice(2)}`;
37+
const source = {
38+
id,
39+
scopeId,
40+
kind: "googleDiscovery",
41+
pluginId: "google-discovery",
42+
name: arg.payload.name,
43+
url: arg.payload.discoveryUrl,
44+
canRemove: false,
45+
canRefresh: false,
46+
canEdit: false,
47+
runtime: false,
48+
};
49+
return [source, ...rows.filter((row) => row.id !== id)].sort((a, b) =>
50+
a.name.localeCompare(b.name),
51+
);
52+
}),
53+
fn: addGoogleDiscoverySource,
54+
}),
55+
),
56+
);
2857
export const updateGoogleDiscoverySource = GoogleDiscoveryClient.mutation(
2958
"googleDiscovery",
3059
"updateSource",

packages/plugins/graphql/src/react/AddGraphqlSource.tsx

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { useCallback, useState } from "react";
22
import { useAtomSet } from "@effect/atom-react";
3+
import * as Exit from "effect/Exit";
4+
import * as Option from "effect/Option";
35

46
import { useScope } from "@executor-js/react/api/scope-context";
57
import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys";
6-
import { usePendingSources } from "@executor-js/react/api/optimistic";
78
import {
89
HttpCredentialsEditor,
910
httpCredentialsValid,
@@ -33,7 +34,7 @@ import {
3334
import { FloatActions } from "@executor-js/react/components/float-actions";
3435
import { Input } from "@executor-js/react/components/input";
3536
import { Spinner } from "@executor-js/react/components/spinner";
36-
import { addGraphqlSource } from "./atoms";
37+
import { addGraphqlSourceOptimistic } from "./atoms";
3738
import { initialGraphqlCredentials } from "./defaults";
3839
import type { HeaderValue } from "../sdk/types";
3940

@@ -55,8 +56,7 @@ export default function AddGraphqlSource(props: {
5556
const [tokens, setTokens] = useState<OAuthCompletionPayload | null>(null);
5657

5758
const scopeId = useScope();
58-
const doAdd = useAtomSet(addGraphqlSource, { mode: "promise" });
59-
const { beginAdd } = usePendingSources();
59+
const doAdd = useAtomSet(addGraphqlSourceOptimistic(scopeId), { mode: "promiseExit" });
6060
const secretList = useSecretPickerSecrets();
6161
const oauth = useOAuthPopupFlow({
6262
popupName: "graphql-oauth",
@@ -112,41 +112,38 @@ export default function AddGraphqlSource(props: {
112112
const { headers: headerMap, queryParams } = serializeHttpCredentials(credentials);
113113

114114
const { trimmedEndpoint, namespace, displayName } = sourceIdentity();
115-
const placeholder = beginAdd({
116-
id: namespace,
117-
name: displayName,
118-
kind: "graphql",
119-
url: trimmedEndpoint || undefined,
115+
const exit = await doAdd({
116+
params: { scopeId },
117+
payload: {
118+
endpoint: trimmedEndpoint,
119+
name: displayName,
120+
namespace,
121+
...(Object.keys(headerMap).length > 0 ? { headers: headerMap } : {}),
122+
...(Object.keys(queryParams).length > 0
123+
? { queryParams: queryParams as Record<string, HeaderValue> }
124+
: {}),
125+
...(authMode === "oauth2" && tokens
126+
? {
127+
auth: {
128+
kind: "oauth2" as const,
129+
connectionId: tokens.connectionId,
130+
},
131+
}
132+
: {}),
133+
},
134+
reactivityKeys: sourceWriteKeys,
120135
});
121-
try {
122-
await doAdd({
123-
params: { scopeId },
124-
payload: {
125-
endpoint: trimmedEndpoint,
126-
name: identity.name.trim() || undefined,
127-
namespace: slugifyNamespace(identity.namespace) || undefined,
128-
...(Object.keys(headerMap).length > 0 ? { headers: headerMap } : {}),
129-
...(Object.keys(queryParams).length > 0
130-
? { queryParams: queryParams as Record<string, HeaderValue> }
131-
: {}),
132-
...(authMode === "oauth2" && tokens
133-
? {
134-
auth: {
135-
kind: "oauth2" as const,
136-
connectionId: tokens.connectionId,
137-
},
138-
}
139-
: {}),
140-
},
141-
reactivityKeys: sourceWriteKeys,
142-
});
143-
props.onComplete();
144-
} catch (e) {
145-
setAddError(e instanceof Error ? e.message : "Failed to add source");
136+
if (Exit.isFailure(exit)) {
137+
const error = Exit.findErrorOption(exit);
138+
setAddError(
139+
Option.isSome(error) && error.value instanceof Error
140+
? error.value.message
141+
: "Failed to add source",
142+
);
146143
setAdding(false);
147-
} finally {
148-
placeholder.done();
144+
return;
149145
}
146+
props.onComplete();
150147
};
151148

152149
return (

packages/plugins/graphql/src/react/atoms.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import type { ScopeId } from "@executor-js/sdk/core";
2+
import * as Atom from "effect/unstable/reactivity/Atom";
3+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4+
import { sourcesOptimisticAtom } from "@executor-js/react/api/atoms";
25
import { ReactivityKey } from "@executor-js/react/api/reactivity-keys";
36
import { GraphqlClient } from "./client";
47

@@ -19,4 +22,31 @@ export const graphqlSourceAtom = (scopeId: ScopeId, namespace: string) =>
1922

2023
export const addGraphqlSource = GraphqlClient.mutation("graphql", "addSource");
2124

25+
export const addGraphqlSourceOptimistic = Atom.family((scopeId: ScopeId) =>
26+
sourcesOptimisticAtom(scopeId).pipe(
27+
Atom.optimisticFn({
28+
reducer: (current, arg) =>
29+
AsyncResult.map(current, (rows) => {
30+
const id = arg.payload.namespace ?? `pending-${Math.random().toString(36).slice(2)}`;
31+
const source = {
32+
id,
33+
scopeId,
34+
kind: "graphql",
35+
pluginId: "graphql",
36+
name: arg.payload.name ?? id,
37+
url: arg.payload.endpoint,
38+
canRemove: false,
39+
canRefresh: false,
40+
canEdit: false,
41+
runtime: false,
42+
};
43+
return [source, ...rows.filter((row) => row.id !== id)].sort((a, b) =>
44+
a.name.localeCompare(b.name),
45+
);
46+
}),
47+
fn: addGraphqlSource,
48+
}),
49+
),
50+
);
51+
2252
export const updateGraphqlSource = GraphqlClient.mutation("graphql", "updateSource");

0 commit comments

Comments
 (0)