Skip to content

Commit

Permalink
refactor(react): error handling (#264)
Browse files Browse the repository at this point in the history
  • Loading branch information
tien authored Oct 18, 2024
1 parent 5aa344f commit 42a81e7
Show file tree
Hide file tree
Showing 14 changed files with 165 additions and 145 deletions.
5 changes: 5 additions & 0 deletions .changeset/rich-apricots-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@reactive-dot/react": patch
---

Improved error resetting to cover all queries.
9 changes: 3 additions & 6 deletions packages/react/src/hooks/use-block.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
bestBlockAtomFamily,
finalizedBlockAtomFamily,
} from "../stores/block.js";
import { bestBlockAtom, finalizedBlockAtom } from "../stores/block.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useConfig } from "./use-config.js";
Expand All @@ -23,7 +20,7 @@ export function useBlock(

return useAtomValue(
tag === "finalized"
? finalizedBlockAtomFamily({ config, chainId })
: bestBlockAtomFamily({ config, chainId }),
? finalizedBlockAtom({ config, chainId })
: bestBlockAtom({ config, chainId }),
);
}
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-chain-spec-data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { chainSpecDataAtomFamily } from "../stores/client.js";
import { chainSpecDataAtom } from "../stores/client.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useConfig } from "./use-config.js";
Expand All @@ -12,7 +12,7 @@ import { useAtomValue } from "jotai";
*/
export function useChainSpecData(options?: ChainHookOptions) {
return useAtomValue(
chainSpecDataAtomFamily({
chainSpecDataAtom({
config: useConfig(),
chainId: internal_useChainId(options),
}),
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clientAtomFamily } from "../stores/client.js";
import { clientAtom } from "../stores/client.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useConfig } from "./use-config.js";
Expand All @@ -12,7 +12,7 @@ import { useAtomValue } from "jotai";
*/
export function useClient(options?: ChainHookOptions) {
return useAtomValue(
clientAtomFamily({
clientAtom({
config: useConfig(),
chainId: internal_useChainId(options),
}),
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-mutation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SignerContext } from "../contexts/index.js";
import { MutationEventSubjectContext } from "../contexts/mutation.js";
import { typedApiAtomFamily } from "../stores/client.js";
import { typedApiAtom } from "../stores/client.js";
import type { ChainHookOptions } from "./types.js";
import { useAsyncAction } from "./use-async-action.js";
import { internal_useChainId } from "./use-chain-id.js";
Expand Down Expand Up @@ -77,7 +77,7 @@ export function useMutation<

const id = globalThis.crypto.randomUUID();

return from(get(typedApiAtomFamily({ config, chainId }))).pipe(
return from(get(typedApiAtom({ config, chainId }))).pipe(
switchMap((typedApi) => {
const transaction = action(typedApi.tx);

Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-query-loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { queryPayloadAtomFamily } from "../stores/query.js";
import { queryPayloadAtom } from "../stores/query.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useConfig } from "./use-config.js";
Expand Down Expand Up @@ -39,7 +39,7 @@ export function useQueryLoader() {
const query = builder(new Query([]));

void get(
queryPayloadAtomFamily({
queryPayloadAtom({
config,
chainId: options?.chainId ?? chainId,
query,
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { queryPayloadAtomFamily } from "../stores/query.js";
import { queryPayloadAtom } from "../stores/query.js";
import type { Falsy, FalsyGuard, FlatHead } from "../types.js";
import { flatHead, stringify } from "../utils/vanilla.js";
import type { ChainHookOptions } from "./types.js";
Expand Down Expand Up @@ -55,7 +55,7 @@ export function useLazyLoadQuery<
() =>
!query
? atom(idle)
: queryPayloadAtomFamily({
: queryPayloadAtom({
config,
chainId,
query,
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-typed-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { typedApiAtomFamily } from "../stores/client.js";
import { typedApiAtom } from "../stores/client.js";
import type { ChainHookOptions } from "./types.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useConfig } from "./use-config.js";
Expand All @@ -16,7 +16,7 @@ export function useTypedApi<TChainId extends ChainId>(
options?: ChainHookOptions<TChainId>,
) {
return useAtomValue(
typedApiAtomFamily({
typedApiAtom({
config: useConfig(),
chainId: internal_useChainId(options),
}),
Expand Down
23 changes: 7 additions & 16 deletions packages/react/src/stores/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
import { withAtomFamilyErrorCatcher } from "../utils/jotai.js";
import { chainSpecDataAtomFamily } from "./client.js";
import { atomFamilyWithErrorCatcher } from "../utils/jotai.js";
import { chainSpecDataAtom } from "./client.js";
import { connectedWalletsAtom } from "./wallets.js";
import { getAccounts, type ChainId, type Config } from "@reactive-dot/core";
import type { WalletAccount } from "@reactive-dot/core/wallets.js";
import type { Atom } from "jotai";
import { atomFamily, atomWithObservable } from "jotai/utils";
import { atomWithObservable } from "jotai/utils";

export const accountsAtom = atomFamily(
(param: {
config: Config;
chainId: ChainId | undefined;
}): Atom<WalletAccount[] | Promise<WalletAccount[]>> =>
withAtomFamilyErrorCatcher(
accountsAtom,
param,
atomWithObservable,
)((get) =>
export const accountsAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId | undefined }, withErrorCatcher) =>
withErrorCatcher(atomWithObservable)((get) =>
getAccounts(
get(connectedWalletsAtom(param.config)),
param.chainId === undefined
? undefined
: // @ts-expect-error `chainId` will never be undefined
get(chainSpecDataAtomFamily(param)),
get(chainSpecDataAtom(param)),
),
),
(a, b) => a.config === b.config && a.chainId === b.chainId,
Expand Down
21 changes: 11 additions & 10 deletions packages/react/src/stores/block.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { clientAtomFamily } from "./client.js";
import { atomFamilyWithErrorCatcher } from "../utils/jotai.js";
import { clientAtom } from "./client.js";
import { type ChainId, type Config, getBlock } from "@reactive-dot/core";
import { atomFamily, atomWithObservable } from "jotai/utils";
import { atomWithObservable } from "jotai/utils";
import { from } from "rxjs";
import { switchMap } from "rxjs/operators";

export const finalizedBlockAtomFamily = atomFamily(
(param: { config: Config; chainId: ChainId }) =>
atomWithObservable((get) =>
from(get(clientAtomFamily(param))).pipe(
export const finalizedBlockAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId }, withErrorCatcher) =>
withErrorCatcher(atomWithObservable)((get) =>
from(get(clientAtom(param))).pipe(
switchMap((client) => getBlock(client, { tag: "finalized" })),
),
),
(a, b) => a.config === b.config && a.chainId === b.chainId,
);

export const bestBlockAtomFamily = atomFamily(
(param: { config: Config; chainId: ChainId }) =>
atomWithObservable((get) =>
from(get(clientAtomFamily(param))).pipe(
export const bestBlockAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId }, withErrorCatcher) =>
withErrorCatcher(atomWithObservable)((get) =>
from(get(clientAtom(param))).pipe(
switchMap((client) => getBlock(client, { tag: "best" })),
),
),
Expand Down
24 changes: 12 additions & 12 deletions packages/react/src/stores/client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { atomFamilyWithErrorCatcher } from "../utils/jotai.js";
import type { ChainId, Config } from "@reactive-dot/core";
import { getClient, ReactiveDotError } from "@reactive-dot/core";
import { atom } from "jotai";
import { atomFamily } from "jotai/utils";

export const clientAtomFamily = atomFamily(
(param: { config: Config; chainId: ChainId }) =>
atom(async () => {
export const clientAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId }, withErrorCatcher) =>
withErrorCatcher(atom)(async () => {
const chainConfig = param.config.chains[param.chainId];

if (chainConfig === undefined) {
Expand All @@ -17,19 +17,19 @@ export const clientAtomFamily = atomFamily(
(a, b) => a.config === b.config && a.chainId === b.chainId,
);

export const chainSpecDataAtomFamily = atomFamily(
(param: { config: Config; chainId: ChainId }) =>
atom(async (get) => {
const client = await get(clientAtomFamily(param));
export const chainSpecDataAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId }, withErrorCatcher) =>
withErrorCatcher(atom)(async (get) => {
const client = await get(clientAtom(param));

return client.getChainSpecData();
}),
(a, b) => a.config === b.config && a.chainId === b.chainId,
);

export const typedApiAtomFamily = atomFamily(
(param: { config: Config; chainId: ChainId }) =>
atom(async (get) => {
export const typedApiAtom = atomFamilyWithErrorCatcher(
(param: { config: Config; chainId: ChainId }, withErrorCatcher) =>
withErrorCatcher(atom)(async (get) => {
const config = param.config.chains[param.chainId];

if (config === undefined) {
Expand All @@ -38,7 +38,7 @@ export const typedApiAtomFamily = atomFamily(
);
}

const client = await get(clientAtomFamily(param));
const client = await get(clientAtom(param));

return client.getTypedApi(config.descriptor);
}),
Expand Down
66 changes: 30 additions & 36 deletions packages/react/src/stores/query.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,45 @@
import { withAtomFamilyErrorCatcher } from "../utils/jotai.js";
import { atomFamilyWithErrorCatcher } from "../utils/jotai.js";
import { stringify } from "../utils/vanilla.js";
import { typedApiAtomFamily } from "./client.js";
import { typedApiAtom } from "./client.js";
import {
type Config,
preflight,
query,
type ChainId,
type Config,
type Query,
} from "@reactive-dot/core";
import type {
MultiInstruction,
QueryInstruction,
} from "@reactive-dot/core/internal.js";
import { atom, type Atom, type WritableAtom } from "jotai";
import { atomFamily, atomWithObservable, atomWithRefresh } from "jotai/utils";
import { atomWithObservable, atomWithRefresh } from "jotai/utils";
import { from, switchMap, type Observable } from "rxjs";

export const instructionPayloadAtomFamily = atomFamily(
(param: {
config: Config;
chainId: ChainId;
instruction: Exclude<
QueryInstruction,
MultiInstruction<// @ts-expect-error need any empty object here
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
{}>
>;
}): Atom<unknown> | WritableAtom<Promise<unknown>, [], void> => {
export const instructionPayloadAtom = atomFamilyWithErrorCatcher(
(
param: {
config: Config;
chainId: ChainId;
instruction: Exclude<
QueryInstruction,
MultiInstruction<// @ts-expect-error need any empty object here
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
{}>
>;
},
withErrorCatcher,
): Atom<unknown> | WritableAtom<Promise<unknown>, [], void> => {
switch (preflight(param.instruction)) {
case "promise":
return withAtomFamilyErrorCatcher(
instructionPayloadAtomFamily,
param,
atomWithRefresh,
)(async (get, { signal }) => {
const api = await get(typedApiAtomFamily(param));
return withErrorCatcher(atomWithRefresh)(async (get, { signal }) => {
const api = await get(typedApiAtom(param));

return query(api, param.instruction, { signal });
});
case "observable":
return withAtomFamilyErrorCatcher(
instructionPayloadAtomFamily,
param,
atomWithObservable,
)((get) =>
from(get(typedApiAtomFamily(param))).pipe(
return withErrorCatcher(atomWithObservable)((get) =>
from(get(typedApiAtom(param))).pipe(
switchMap(
(api) => query(api, param.instruction) as Observable<unknown>,
),
Expand All @@ -65,7 +60,7 @@ export function getQueryInstructionPayloadAtoms(
) {
return query.instructions.map((instruction) => {
if (!("multi" in instruction)) {
return instructionPayloadAtomFamily({
return instructionPayloadAtom({
config,
chainId,
instruction,
Expand All @@ -75,7 +70,7 @@ export function getQueryInstructionPayloadAtoms(
return (instruction.args as unknown[]).map((args) => {
const { multi, ...rest } = instruction;

return instructionPayloadAtomFamily({
return instructionPayloadAtom({
config,
chainId,
instruction: { ...rest, args },
Expand All @@ -86,13 +81,12 @@ export function getQueryInstructionPayloadAtoms(

// TODO: should be memoized within render function instead
// https://github.com/pmndrs/jotai/discussions/1553
export const queryPayloadAtomFamily = atomFamily(
(param: { config: Config; chainId: ChainId; query: Query }): Atom<unknown> =>
withAtomFamilyErrorCatcher(
queryPayloadAtomFamily,
param,
atom,
)((get) => {
export const queryPayloadAtom = atomFamilyWithErrorCatcher(
(
param: { config: Config; chainId: ChainId; query: Query },
withErrorCatcher,
): Atom<unknown> =>
withErrorCatcher(atom)((get) => {
const atoms = getQueryInstructionPayloadAtoms(
param.config,
param.chainId,
Expand Down
Loading

0 comments on commit 42a81e7

Please sign in to comment.