Skip to content

Commit

Permalink
Add useFetch hook. Refine types in other hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Exidex committed Sep 13, 2024
1 parent 9f38e3a commit 60578e6
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 67 deletions.
2 changes: 1 addition & 1 deletion dev_plugin/gauntlet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,4 @@ os = 'windows'
[permissions]
environment = ["RUST_LOG"]
system = ["systemMemoryInfo"]
network = ["upload.wikimedia.org"]
network = ["upload.wikimedia.org", "api.github.com"]
64 changes: 63 additions & 1 deletion dev_plugin/src/hooks-view.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Icons, List } from "@project-gauntlet/api/components";
import React, { ReactElement, useRef } from "react";
import { useCachedPromise, useNavigation, usePromise } from "@project-gauntlet/api/hooks";
import { useCachedPromise, useFetch, useNavigation, usePromise } from "@project-gauntlet/api/hooks";

export default function ListView(): ReactElement {
const { pushView } = useNavigation();
Expand Down Expand Up @@ -53,6 +53,14 @@ export default function ListView(): ReactElement {
pushView(<UseCachedPromiseInitialState/>)
break;
}
case "UseFetchBasic": {
pushView(<UseFetchBasic/>)
break;
}
case "UseFetchMap": {
pushView(<UseFetchMap/>)
break;
}
}
}}
>
Expand All @@ -67,6 +75,8 @@ export default function ListView(): ReactElement {
<List.Item id="UsePromiseTestThrow" title="UsePromiseTestThrow"/>
<List.Item id="UseCachedPromiseBasic" title="UseCachedPromiseBasic"/>
<List.Item id="UseCachedPromiseInitialState" title="UseCachedPromiseInitialState"/>
<List.Item id="UseFetchBasic" title="UseFetchBasic"/>
<List.Item id="UseFetchMap" title="UseFetchMap"/>
</List>
)
}
Expand Down Expand Up @@ -355,6 +365,58 @@ function UsePromiseTestThrow(): ReactElement {
)
}

function UseFetchBasic(): ReactElement {
const { popView } = useNavigation();

interface GithubLatestRelease {

}

const { data, error, isLoading } = useFetch<GithubLatestRelease>(
"https://api.github.com/repos/project-gauntlet/gauntlet/releases/latest"
);

printState(data, error, isLoading)

return (
<List
isLoading={isLoading}
onSelectionChange={onSelectionChangeHandler(popView)}
>
<List.Section title={"Data " + data}>
<List.Item id="back" title="Go Back" icon={Icons.Clipboard}/>
</List.Section>
</List>
)
}

function UseFetchMap(): ReactElement {
interface GithubLatestRelease {
url: string
}

const { popView } = useNavigation();
const { data, error, isLoading } = useFetch<GithubLatestRelease, string>(
"https://api.github.com/repos/project-gauntlet/gauntlet/releases/latest",
{
map: result => result.url
}
);

printState(data, error, isLoading)

return (
<List
isLoading={isLoading}
onSelectionChange={onSelectionChangeHandler(popView)}
>
<List.Section title={"Data " + data}>
<List.Item id="back" title="Go Back" icon={Icons.Clipboard}/>
</List.Section>
</List>
)
}

function onSelectionChangeHandler(popView: () => void, handler?: () => void): (id: string) => void {
return (id) => {
switch (id) {
Expand Down
198 changes: 133 additions & 65 deletions js/api/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,11 @@ export function useNavigation(): { popView: () => void, pushView: (component: Re
}
}

export type AsyncStateInitial = {
isLoading: boolean; // false if options.execute is false, otherwise true
error?: undefined;
data?: undefined;
};
export type AsyncStateLoading<T> = {
isLoading: true;
error?: unknown | undefined;
export type AsyncState<T> = {
isLoading: boolean;
error?: unknown;
data?: T;
};
export type AsyncStateError = {
isLoading: false;
error: unknown;
data?: undefined;
};
export type AsyncStateSuccess<T> = {
isLoading: false;
error?: undefined;
data: T;
};

export type AsyncState<T> = AsyncStateInitial | AsyncStateLoading<T> | AsyncStateError | AsyncStateSuccess<T>;

export type MutatePromiseFn<T, R> = (
asyncUpdate: Promise<R>,
Expand All @@ -47,25 +30,23 @@ export type MutatePromiseFn<T, R> = (
},
) => Promise<R>;

export type UsePromiseOptions<T extends (...args: any[]) => Promise<any>> = {
abortable?: MutableRefObject<AbortController | undefined>;
execute?: boolean;
onError?: (error: unknown) => void;
onData?: (data: Awaited<ReturnType<T>>) => void;
onWillExecute?: (...args: Parameters<T>) => void;
}

export function usePromise<T extends (...args: any[]) => Promise<any>, R>(
fn: T,
args?: Parameters<T>,
options?: UsePromiseOptions<T>,
): AsyncState<Awaited<ReturnType<T>>> & {
export function usePromise<Return, Args extends unknown[], R = unknown>(
fn: (...args: Args) => Promise<Return>,
args?: Args,
options?: {
abortable?: MutableRefObject<AbortController | undefined>;
execute?: boolean;
onError?: (error: unknown) => void;
onData?: (data: Return) => void;
onWillExecute?: (...args: Args) => void;
},
): AsyncState<Return> & {
revalidate: () => void;
mutate: MutatePromiseFn<Awaited<ReturnType<T>>, R>;
mutate: MutatePromiseFn<Return, R>;
} {
const execute = options?.execute !== false; // execute by default

const [state, setState] = useState<AsyncState<Awaited<ReturnType<T>>>>({ isLoading: execute });
const [state, setState] = useState<AsyncState<Return>>({ isLoading: execute });

return usePromiseInternal(
fn,
Expand All @@ -80,13 +61,20 @@ export function usePromise<T extends (...args: any[]) => Promise<any>, R>(
)
}

export function useCachedPromise<T extends (...args: any[]) => Promise<any>, R>(
fn: T,
args?: Parameters<T>,
options?: UsePromiseOptions<T> & { initialState?: Awaited<ReturnType<T>> | (() => Awaited<ReturnType<T>>) },
): AsyncState<Awaited<ReturnType<T>>> & {
export function useCachedPromise<Return, Args extends unknown[], R = unknown>(
fn: (...args: Args) => Promise<Return>,
args?: Args,
options?: {
initialState?: Return | (() => Return),
abortable?: MutableRefObject<AbortController | undefined>;
execute?: boolean;
onError?: (error: unknown) => void;
onData?: (data: Return) => void;
onWillExecute?: (...args: Args) => void;
},
): AsyncState<Return> & {
revalidate: () => void;
mutate: MutatePromiseFn<Awaited<ReturnType<T>>, R>;
mutate: MutatePromiseFn<Return, R>;
} {
const execute = options?.execute !== false; // execute by default

Expand All @@ -95,7 +83,7 @@ export function useCachedPromise<T extends (...args: any[]) => Promise<any>, R>(
const { entrypointId }: { entrypointId: () => string } = useGauntletContext();

// same store is fetched and updated between command runs
const [state, setState] = useCache<AsyncState<Awaited<ReturnType<T>>>>("useCachedPromise" + entrypointId() + id, () => {
const [state, setState] = useCache<AsyncState<Return>>("useCachedPromise" + entrypointId() + id, (): AsyncState<Return> => {
const initialState = options?.initialState;
if (initialState) {
if (initialState instanceof Function) {
Expand All @@ -121,19 +109,19 @@ export function useCachedPromise<T extends (...args: any[]) => Promise<any>, R>(
)
}

function usePromiseInternal<T extends (...args: any[]) => Promise<any>, R>(
fn: T,
state: AsyncState<Awaited<ReturnType<T>>>,
setState: Dispatch<SetStateAction<AsyncState<Awaited<ReturnType<T>>>>>,
args: Parameters<T>,
function usePromiseInternal<Return, Args extends unknown[], R = unknown>(
fn: (...args: Args) => Promise<Return>,
state: AsyncState<Return>,
setState: Dispatch<SetStateAction<AsyncState<Return>>>,
args: Args,
execute: boolean,
abortable?: MutableRefObject<AbortController | undefined>,
onError?: (error: unknown) => void,
onData?: (data: Awaited<ReturnType<T>>) => void,
onWillExecute?: (...args: Parameters<T>) => void,
): AsyncState<Awaited<ReturnType<T>>> & {
onData?: (data: Return) => void,
onWillExecute?: (...args: Args) => void,
): AsyncState<Return> & {
revalidate: () => void; // will execute even if options.execute is false
mutate: MutatePromiseFn<Awaited<ReturnType<T>>, R>; // will execute even if options.execute is false
mutate: MutatePromiseFn<Return, R>; // will execute even if options.execute is false
} {

const promiseRef = useRef<Promise<any>>();
Expand All @@ -144,7 +132,7 @@ function usePromiseInternal<T extends (...args: any[]) => Promise<any>, R>(
};
}, [abortable]);

const callback = useCallback(async (...args: Parameters<T>): Promise<void> => {
const callback = useCallback(async (...args: Args): Promise<void> => {
if (abortable) {
abortable.current?.abort();
abortable.current = new AbortController()
Expand All @@ -158,7 +146,7 @@ function usePromiseInternal<T extends (...args: any[]) => Promise<any>, R>(

promiseRef.current = promise;

let promiseResult: Awaited<ReturnType<T>>;
let promiseResult: Return;
try {
promiseResult = await promise;
} catch (error) {
Expand Down Expand Up @@ -202,8 +190,8 @@ function usePromiseInternal<T extends (...args: any[]) => Promise<any>, R>(
mutate: async (
asyncUpdate: Promise<R>,
options?: {
optimisticUpdate?: (data: Awaited<ReturnType<T>> | undefined) => Awaited<ReturnType<T>>;
rollbackOnError?: boolean | ((data: Awaited<ReturnType<T>> | undefined) => Awaited<ReturnType<T>>);
optimisticUpdate?: (data: Return | undefined) => Return;
rollbackOnError?: boolean | ((data: Return | undefined) => Return);
shouldRevalidateAfter?: boolean;
},
): Promise<R> => {
Expand Down Expand Up @@ -231,20 +219,12 @@ function usePromiseInternal<T extends (...args: any[]) => Promise<any>, R>(
} catch (e) {
switch (typeof rollbackOnError) {
case "undefined": {
if (prevData === undefined) {
setState({ data: prevData, isLoading: false })
} else {
setState({ data: prevData, isLoading: false })
}
setState({ data: prevData, isLoading: false })
break;
}
case "boolean": {
if (rollbackOnError) {
if (prevData === undefined) {
setState({ data: prevData, isLoading: false })
} else {
setState({ data: prevData, isLoading: false })
}
setState({ data: prevData, isLoading: false })
}
break;
}
Expand Down Expand Up @@ -317,3 +297,91 @@ function useWebStorage<T>(

return [value, setValue]
}

export function useFetch<T, R = unknown>(
url: RequestInfo | URL,
options?: {
request?: RequestInit,
parse?: (response: Response) => T | Promise<T>;
initialState?: T | (() => T),
execute?: boolean;
onError?: (error: unknown) => void;
onData?: (data: T) => void;
onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void;
},
): AsyncState<T> & {
revalidate: () => void;
mutate: MutatePromiseFn<T, R>;
};
export function useFetch<V, T, R = unknown>(
url: RequestInfo | URL,
options: {
request?: RequestInit,
parse?: (response: Response) => V | Promise<V>;
map: (result: V) => T | Promise<T>;
initialState?: T | (() => T),
execute?: boolean;
onError?: (error: unknown) => void;
onData?: (data: T) => void;
onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void;
},
): AsyncState<T> & {
revalidate: () => void;
mutate: MutatePromiseFn<T, R>;
};
export function useFetch<V, T, R = unknown>(
url: RequestInfo | URL,
options?: {
request?: RequestInit,
parse?: (response: Response) => V | Promise<V>;
map?: (result: V) => T | Promise<T>;
initialState?: T | V | (() => T | V),
execute?: boolean;
onError?: (error: unknown) => void;
onData?: (data: T | V) => void;
onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void;
},
): AsyncState<T | V> & {
revalidate: () => void;
mutate: MutatePromiseFn<T | V, R>;
} {
const abortable = useRef<AbortController>();

return useCachedPromise(
async (inputParam: RequestInfo | URL): Promise<T | V> => {
const response = await fetch(inputParam, { ...options?.request, signal: abortable.current?.signal });

if (options?.parse) {
const parsed: V = await options?.parse(response)

if (options?.map) {
return options?.map(parsed)
} else {
return parsed
}
} else {
const content = response.headers.get("content-type");
if (!content || !content.includes("application/json")) {
throw new Error("Content-Type is not 'application/json', please specify custom options.parse")
}

const parsed: V = await response.json()

if (options?.map) {
return options?.map(parsed)
} else {
return parsed
}
}
},
[url],
{
initialState: options?.initialState,
abortable,
execute: options?.execute,
onError: options?.onError,
onData: options?.onData,
onWillExecute: options?.onWillExecute,
}
)
}

0 comments on commit 60578e6

Please sign in to comment.