Skip to content

Commit

Permalink
refactor: simplify error handing
Browse files Browse the repository at this point in the history
  • Loading branch information
tien committed Mar 6, 2025
1 parent 2142e7a commit 52624b0
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 135 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-candies-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@reactive-dot/react": patch
---

Refactored error handling.
10 changes: 6 additions & 4 deletions packages/react/src/hooks/use-chain-spec-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ export function useChainSpecData(options?: ChainHookOptions) {
*/
export const chainSpecDataAtom = atomFamilyWithErrorCatcher(
(withErrorCatcher, config: Config, chainId: ChainId) =>
withErrorCatcher(atom)(async (get) => {
const client = await get(clientAtom(config, chainId));
withErrorCatcher(
atom(async (get) => {
const client = await get(clientAtom(config, chainId));

Check warning on line 29 in packages/react/src/hooks/use-chain-spec-data.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-chain-spec-data.ts#L27-L29

Added lines #L27 - L29 were not covered by tests

return client.getChainSpecData();
}),
return client.getChainSpecData();
}),
),

Check warning on line 33 in packages/react/src/hooks/use-chain-spec-data.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-chain-spec-data.ts#L31-L33

Added lines #L31 - L33 were not covered by tests
);
16 changes: 9 additions & 7 deletions packages/react/src/hooks/use-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ export function useClient(options?: ChainHookOptions) {
*/
export const clientAtom = atomFamilyWithErrorCatcher(
(withErrorCatcher, config: Config, chainId: ChainId) =>
withErrorCatcher(atom)(() => {
const chainConfig = config.chains[chainId];
withErrorCatcher(
atom(() => {
const chainConfig = config.chains[chainId];

Check warning on line 28 in packages/react/src/hooks/use-client.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-client.ts#L26-L28

Added lines #L26 - L28 were not covered by tests

if (chainConfig === undefined) {
throw new ReactiveDotError(`No config provided for ${chainId}`);
}
if (chainConfig === undefined) {
throw new ReactiveDotError(`No config provided for ${chainId}`);
}

Check warning on line 32 in packages/react/src/hooks/use-client.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-client.ts#L30-L32

Added lines #L30 - L32 were not covered by tests

return getClient(chainConfig);
}),
return getClient(chainConfig);
}),
),

Check warning on line 36 in packages/react/src/hooks/use-client.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-client.ts#L34-L36

Added lines #L34 - L36 were not covered by tests
);
55 changes: 28 additions & 27 deletions packages/react/src/hooks/use-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,13 @@ const instructionPayloadAtom = atomFamilyWithErrorCatcher(
) => {
switch (preflight(instruction)) {
case "promise": {
const atom = withErrorCatcher(atomWithRefresh)(async (
get,
{ signal },
) => {
const api = await get(typedApiAtom(config, chainId));
const atom = withErrorCatcher(
atomWithRefresh(async (get, { signal }) => {
const api = await get(typedApiAtom(config, chainId));

Check warning on line 189 in packages/react/src/hooks/use-query.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-query.ts#L187-L189

Added lines #L187 - L189 were not covered by tests

return query(api, instruction, { signal });
});
return query(api, instruction, { signal });
}),
);

Check warning on line 193 in packages/react/src/hooks/use-query.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-query.ts#L191-L193

Added lines #L191 - L193 were not covered by tests

return {
observableAtom: atom,
Expand Down Expand Up @@ -256,27 +255,29 @@ export const queryPayloadAtom = atomFamilyWithErrorCatcher(
) => (asObservable ? atoms.observableAtom : atoms.promiseAtom);

const createAtom = (asObservable: boolean) =>
withErrorCatcher(atom)((get) => {
return Promise.all(
atoms.map((atomOrAtoms) =>
!Array.isArray(atomOrAtoms)
? atomOrAtoms
: Promise.all(
atomOrAtoms.map((atomOrAtoms) => {
if (Array.isArray(atomOrAtoms)) {
return Promise.all(
atomOrAtoms.map((atom) =>
get(unwrap(atom, asObservable)),
),
);
}
withErrorCatcher(
atom((get) => {
return Promise.all(
atoms.map((atomOrAtoms) =>
!Array.isArray(atomOrAtoms)
? atomOrAtoms
: Promise.all(
atomOrAtoms.map((atomOrAtoms) => {
if (Array.isArray(atomOrAtoms)) {
return Promise.all(
atomOrAtoms.map((atom) =>
get(unwrap(atom, asObservable)),
),
);
}

Check warning on line 272 in packages/react/src/hooks/use-query.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-query.ts#L258-L272

Added lines #L258 - L272 were not covered by tests

return get(unwrap(atomOrAtoms, asObservable));
}),
).then(flatHead),
),
);
});
return get(unwrap(atomOrAtoms, asObservable));
}),
).then(flatHead),
),
);
}),
);

Check warning on line 280 in packages/react/src/hooks/use-query.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-query.ts#L274-L280

Added lines #L274 - L280 were not covered by tests

return { promiseAtom: createAtom(false), observableAtom: createAtom(true) };
},
Expand Down
18 changes: 10 additions & 8 deletions packages/react/src/hooks/use-typed-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,17 @@ export function useTypedApi<TChainId extends ChainId | undefined>(
*/
export const typedApiAtom = atomFamilyWithErrorCatcher(
(withErrorCatcher, config: Config, chainId: ChainId) =>
withErrorCatcher(atom)(async (get) => {
const chainConfig = config.chains[chainId];
withErrorCatcher(
atom(async (get) => {
const chainConfig = config.chains[chainId];

Check warning on line 37 in packages/react/src/hooks/use-typed-api.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-typed-api.ts#L35-L37

Added lines #L35 - L37 were not covered by tests

if (chainConfig === undefined) {
throw new ReactiveDotError(`No config provided for chain ${chainId}`);
}
if (chainConfig === undefined) {
throw new ReactiveDotError(`No config provided for chain ${chainId}`);
}

Check warning on line 41 in packages/react/src/hooks/use-typed-api.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-typed-api.ts#L39-L41

Added lines #L39 - L41 were not covered by tests

const client = await get(clientAtom(config, chainId));
const client = await get(clientAtom(config, chainId));

Check warning on line 43 in packages/react/src/hooks/use-typed-api.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-typed-api.ts#L43

Added line #L43 was not covered by tests

return client.getTypedApi(chainConfig.descriptor);
}),
return client.getTypedApi(chainConfig.descriptor);
}),
),

Check warning on line 47 in packages/react/src/hooks/use-typed-api.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-typed-api.ts#L45-L47

Added lines #L45 - L47 were not covered by tests
);
2 changes: 1 addition & 1 deletion packages/react/src/hooks/use-wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function useConnectedWallets() {
*/
export const walletsAtom = atomFamilyWithErrorCatcher(
(withErrorCatcher, config: Config) =>
withErrorCatcher(atom)(() => aggregateWallets(config.wallets ?? [])),
withErrorCatcher(atom(() => aggregateWallets(config.wallets ?? []))),

Check warning on line 36 in packages/react/src/hooks/use-wallets.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/hooks/use-wallets.ts#L36

Added line #L36 was not covered by tests
);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ beforeEach(() => {
it("should create an atom family", () => {
const myAtomFamily = atomFamilyWithErrorCatcher(
(withErrorCatcher, param: string) => {
return withErrorCatcher(atom)(`Hello ${param}`);
return withErrorCatcher(atom(`Hello ${param}`));
},
);

Expand All @@ -26,7 +26,7 @@ it("should create an atom family", () => {
it("should return different atoms for different params", () => {
const myAtomFamily = atomFamilyWithErrorCatcher(
(withErrorCatcher, param: string) => {
return withErrorCatcher(atom)(`Hello ${param}`);
return withErrorCatcher(atom(`Hello ${param}`));
},
);

Expand All @@ -39,13 +39,15 @@ it("should return different atoms for different params", () => {
it("should catch errors in synchronous reads", () => {
const myAtomFamily = atomFamilyWithErrorCatcher(
(withErrorCatcher, param: string) => {
return withErrorCatcher(atom)(() => {
if (param === "Error") {
throw new Error("Intentional Error");
}

return `Hello ${param}`;
});
return withErrorCatcher(
atom(() => {
if (param === "Error") {
throw new Error("Intentional Error");
}

return `Hello ${param}`;
}),
);
},
);

Expand All @@ -56,13 +58,15 @@ it("should catch errors in synchronous reads", () => {
it("should catch errors in Promise reads", async () => {
const myAtomFamily = atomFamilyWithErrorCatcher(
(withErrorCatcher, param: string) => {
return withErrorCatcher(atom)(async () => {
if (param === "Error") {
throw new Error("Intentional Promise Error");
}

return `Hello ${param}`;
});
return withErrorCatcher(
atom(async () => {
if (param === "Error") {
throw new Error("Intentional Promise Error");
}

return `Hello ${param}`;
}),
);
},
);

Expand All @@ -75,10 +79,12 @@ it("should catch errors in Promise reads", async () => {
it("should catch errors in Observable reads", async () => {
const myAtomFamily = atomFamilyWithErrorCatcher(
(withErrorCatcher, param: string) => {
return withErrorCatcher(atomWithObservable)(() =>
param === "Error"
? throwError(() => new Error("Intentional Observable Error"))
: of(`Hello ${param}`),
return withErrorCatcher(
atomWithObservable(() =>
param === "Error"
? throwError(() => new Error("Intentional Observable Error"))
: of(`Hello ${param}`),
),
);
},
);
Expand Down
90 changes: 38 additions & 52 deletions packages/react/src/utils/jotai/atom-family-with-error-catcher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { type AtomFamily, atomFamily } from "./atom-family.js";
import { atom, type Getter } from "jotai";
import { Observable } from "rxjs";
import { catchError } from "rxjs/operators";
import { type Atom, atom, type Getter, type WritableAtom } from "jotai";

export const atomFamilyErrorsAtom = atom(
() =>
Expand All @@ -18,70 +16,58 @@ export const atomFamilyErrorsAtom = atom(
export function atomFamilyWithErrorCatcher<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TArguments extends any[],
TAtomType,
TCached,
>(
initializeAtom: (
withErrorCatcher: <TAtomCreator>(atomCreator: TAtomCreator) => TAtomCreator,
withErrorCatcher: <TAtomType extends Atom<unknown>>(
atom: TAtomType,
) => TAtomType,
...args: TArguments
) => TAtomType,
) => TCached,
getKey?: (...args: TArguments) => unknown,
): AtomFamily<TArguments, TAtomType> {
): AtomFamily<TArguments, TCached> {
const baseAtomFamily = atomFamily((...args: TArguments) => {
const withErrorCatcher: <
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TRead extends (get: Getter, ...args: unknown[]) => any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TAtomCreator extends (read: TRead, ...args: any[]) => unknown,
>(
atomCreator: TAtomCreator,
) => TAtomCreator = (atomCreator) => {
// @ts-expect-error complex sub-type
const atomCatching: TAtomCreator = (read, ...args) => {
// @ts-expect-error complex sub-type
const readCatching: TRead = (...readArgs) => {
const addError = <T>(error: T) => {
const get = readArgs[0] as Getter;
const withErrorCatcher = <TAtomType extends Atom<unknown>>(
childAtom: TAtomType,
) => {
const read = (get: Getter) => {
try {
const value = get(childAtom);

if (!(value instanceof Promise)) {
return value;
}

return value.catch((error) => {
get(atomFamilyErrorsAtom).add({
atomFamily: baseAtomFamily,
args,
});
return error;
};

try {
const value = read(...readArgs);

if (value instanceof Promise) {
return value.catch((error) => {
throw addError(error);
});
}

if (value instanceof Observable) {
return value.pipe(
catchError((error) => {
throw addError(error);
}),
);
}

return value;
} catch (error) {
throw addError(error);
}
};
throw error;
});
} catch (error) {
get(atomFamilyErrorsAtom).add({
atomFamily: baseAtomFamily,
args,
});

return atomCreator(readCatching, ...args);
throw error;
}
};

return atomCatching;
return "write" in childAtom
? atom(read, (_, set, ...args: unknown[]) =>
set(
childAtom as unknown as WritableAtom<unknown, unknown[], unknown>,
...args,
),

Check warning on line 64 in packages/react/src/utils/jotai/atom-family-with-error-catcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/react/src/utils/jotai/atom-family-with-error-catcher.ts#L61-L64

Added lines #L61 - L64 were not covered by tests
)
: atom(read);
};

return initializeAtom(
// @ts-expect-error complex type
withErrorCatcher,
...args,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return initializeAtom(withErrorCatcher as any, ...args);
}, getKey);

return baseAtomFamily;
Expand Down
32 changes: 16 additions & 16 deletions packages/react/src/utils/jotai/atom-with-observable-and-promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ type Data<T> = { value: T | Promise<T> } | { error: unknown };

export function atomWithObservableAndPromise<
TValue,
TAtomEnhancer extends <
TAtomCreator extends (...args: never[]) => Atom<unknown>,
>(
atomCreator: TAtomCreator,
) => TAtomCreator,
TAtomEnhancer extends <TAtomType extends Atom<unknown>>(
atomType: TAtomType,
) => TAtomType,
>(
getObservable: (get: Getter) => Observable<TValue>,
enhanceAtom: TAtomEnhancer = ((atomCreator) => atomCreator) as TAtomEnhancer,
Expand All @@ -25,22 +23,24 @@ export function atomWithObservableAndPromise<

const dataAtom = atom<Data<TValue>>({ value: initialPromise });

const promiseAtom = enhanceAtom(atom)((get) => {
const data = get(dataAtom);
const promiseAtom = enhanceAtom(
atom((get) => {
const data = get(dataAtom);

if ("error" in data) {
throw data.error;
}
if ("error" in data) {
throw data.error;
}

if ("value" in data && data.value !== initialPromise) {
return data.value;
}
if ("value" in data && data.value !== initialPromise) {
return data.value;
}

return firstValueFrom(get(rawObservableAtom));
});
return firstValueFrom(get(rawObservableAtom));
}),
);

const observableAtom = withAtomEffect(
enhanceAtom(atomWithObservable)((get) => get(rawObservableAtom)),
enhanceAtom(atomWithObservable((get) => get(rawObservableAtom))),
(get, set) => {
try {
set(dataAtom, { value: get(observableAtom) });
Expand Down

0 comments on commit 52624b0

Please sign in to comment.