From 60578e63914348fd8b5c1f8dfb53a85c879534c8 Mon Sep 17 00:00:00 2001
From: Exidex <16986685+Exidex@users.noreply.github.com>
Date: Fri, 13 Sep 2024 19:27:10 +0200
Subject: [PATCH] Add useFetch hook. Refine types in other hooks
---
dev_plugin/gauntlet.toml | 2 +-
dev_plugin/src/hooks-view.tsx | 64 ++++++++++-
js/api/src/hooks.ts | 198 +++++++++++++++++++++++-----------
3 files changed, 197 insertions(+), 67 deletions(-)
diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml
index 6741b0c..9d36add 100644
--- a/dev_plugin/gauntlet.toml
+++ b/dev_plugin/gauntlet.toml
@@ -147,4 +147,4 @@ os = 'windows'
[permissions]
environment = ["RUST_LOG"]
system = ["systemMemoryInfo"]
-network = ["upload.wikimedia.org"]
\ No newline at end of file
+network = ["upload.wikimedia.org", "api.github.com"]
\ No newline at end of file
diff --git a/dev_plugin/src/hooks-view.tsx b/dev_plugin/src/hooks-view.tsx
index 19dd1e4..babc285 100644
--- a/dev_plugin/src/hooks-view.tsx
+++ b/dev_plugin/src/hooks-view.tsx
@@ -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();
@@ -53,6 +53,14 @@ export default function ListView(): ReactElement {
pushView()
break;
}
+ case "UseFetchBasic": {
+ pushView()
+ break;
+ }
+ case "UseFetchMap": {
+ pushView()
+ break;
+ }
}
}}
>
@@ -67,6 +75,8 @@ export default function ListView(): ReactElement {
+
+
)
}
@@ -355,6 +365,58 @@ function UsePromiseTestThrow(): ReactElement {
)
}
+function UseFetchBasic(): ReactElement {
+ const { popView } = useNavigation();
+
+ interface GithubLatestRelease {
+
+ }
+
+ const { data, error, isLoading } = useFetch(
+ "https://api.github.com/repos/project-gauntlet/gauntlet/releases/latest"
+ );
+
+ printState(data, error, isLoading)
+
+ return (
+
+
+
+
+
+ )
+}
+
+function UseFetchMap(): ReactElement {
+ interface GithubLatestRelease {
+ url: string
+ }
+
+ const { popView } = useNavigation();
+ const { data, error, isLoading } = useFetch(
+ "https://api.github.com/repos/project-gauntlet/gauntlet/releases/latest",
+ {
+ map: result => result.url
+ }
+ );
+
+ printState(data, error, isLoading)
+
+ return (
+
+
+
+
+
+ )
+}
+
function onSelectionChangeHandler(popView: () => void, handler?: () => void): (id: string) => void {
return (id) => {
switch (id) {
diff --git a/js/api/src/hooks.ts b/js/api/src/hooks.ts
index 1073c9d..535ec31 100644
--- a/js/api/src/hooks.ts
+++ b/js/api/src/hooks.ts
@@ -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 = {
- isLoading: true;
- error?: unknown | undefined;
+export type AsyncState = {
+ isLoading: boolean;
+ error?: unknown;
data?: T;
};
-export type AsyncStateError = {
- isLoading: false;
- error: unknown;
- data?: undefined;
-};
-export type AsyncStateSuccess = {
- isLoading: false;
- error?: undefined;
- data: T;
-};
-
-export type AsyncState = AsyncStateInitial | AsyncStateLoading | AsyncStateError | AsyncStateSuccess;
export type MutatePromiseFn = (
asyncUpdate: Promise,
@@ -47,25 +30,23 @@ export type MutatePromiseFn = (
},
) => Promise;
-export type UsePromiseOptions Promise> = {
- abortable?: MutableRefObject;
- execute?: boolean;
- onError?: (error: unknown) => void;
- onData?: (data: Awaited>) => void;
- onWillExecute?: (...args: Parameters) => void;
-}
-
-export function usePromise Promise, R>(
- fn: T,
- args?: Parameters,
- options?: UsePromiseOptions,
-): AsyncState>> & {
+export function usePromise(
+ fn: (...args: Args) => Promise,
+ args?: Args,
+ options?: {
+ abortable?: MutableRefObject;
+ execute?: boolean;
+ onError?: (error: unknown) => void;
+ onData?: (data: Return) => void;
+ onWillExecute?: (...args: Args) => void;
+ },
+): AsyncState & {
revalidate: () => void;
- mutate: MutatePromiseFn>, R>;
+ mutate: MutatePromiseFn;
} {
const execute = options?.execute !== false; // execute by default
- const [state, setState] = useState>>>({ isLoading: execute });
+ const [state, setState] = useState>({ isLoading: execute });
return usePromiseInternal(
fn,
@@ -80,13 +61,20 @@ export function usePromise Promise, R>(
)
}
-export function useCachedPromise Promise, R>(
- fn: T,
- args?: Parameters,
- options?: UsePromiseOptions & { initialState?: Awaited> | (() => Awaited>) },
-): AsyncState>> & {
+export function useCachedPromise(
+ fn: (...args: Args) => Promise,
+ args?: Args,
+ options?: {
+ initialState?: Return | (() => Return),
+ abortable?: MutableRefObject;
+ execute?: boolean;
+ onError?: (error: unknown) => void;
+ onData?: (data: Return) => void;
+ onWillExecute?: (...args: Args) => void;
+ },
+): AsyncState & {
revalidate: () => void;
- mutate: MutatePromiseFn>, R>;
+ mutate: MutatePromiseFn;
} {
const execute = options?.execute !== false; // execute by default
@@ -95,7 +83,7 @@ export function useCachedPromise Promise, R>(
const { entrypointId }: { entrypointId: () => string } = useGauntletContext();
// same store is fetched and updated between command runs
- const [state, setState] = useCache>>>("useCachedPromise" + entrypointId() + id, () => {
+ const [state, setState] = useCache>("useCachedPromise" + entrypointId() + id, (): AsyncState => {
const initialState = options?.initialState;
if (initialState) {
if (initialState instanceof Function) {
@@ -121,19 +109,19 @@ export function useCachedPromise Promise, R>(
)
}
-function usePromiseInternal Promise, R>(
- fn: T,
- state: AsyncState>>,
- setState: Dispatch>>>>,
- args: Parameters,
+function usePromiseInternal(
+ fn: (...args: Args) => Promise,
+ state: AsyncState,
+ setState: Dispatch>>,
+ args: Args,
execute: boolean,
abortable?: MutableRefObject,
onError?: (error: unknown) => void,
- onData?: (data: Awaited>) => void,
- onWillExecute?: (...args: Parameters) => void,
-): AsyncState>> & {
+ onData?: (data: Return) => void,
+ onWillExecute?: (...args: Args) => void,
+): AsyncState & {
revalidate: () => void; // will execute even if options.execute is false
- mutate: MutatePromiseFn>, R>; // will execute even if options.execute is false
+ mutate: MutatePromiseFn; // will execute even if options.execute is false
} {
const promiseRef = useRef>();
@@ -144,7 +132,7 @@ function usePromiseInternal Promise, R>(
};
}, [abortable]);
- const callback = useCallback(async (...args: Parameters): Promise => {
+ const callback = useCallback(async (...args: Args): Promise => {
if (abortable) {
abortable.current?.abort();
abortable.current = new AbortController()
@@ -158,7 +146,7 @@ function usePromiseInternal Promise, R>(
promiseRef.current = promise;
- let promiseResult: Awaited>;
+ let promiseResult: Return;
try {
promiseResult = await promise;
} catch (error) {
@@ -202,8 +190,8 @@ function usePromiseInternal Promise, R>(
mutate: async (
asyncUpdate: Promise,
options?: {
- optimisticUpdate?: (data: Awaited> | undefined) => Awaited>;
- rollbackOnError?: boolean | ((data: Awaited> | undefined) => Awaited>);
+ optimisticUpdate?: (data: Return | undefined) => Return;
+ rollbackOnError?: boolean | ((data: Return | undefined) => Return);
shouldRevalidateAfter?: boolean;
},
): Promise => {
@@ -231,20 +219,12 @@ function usePromiseInternal Promise, 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;
}
@@ -317,3 +297,91 @@ function useWebStorage(
return [value, setValue]
}
+
+export function useFetch(
+ url: RequestInfo | URL,
+ options?: {
+ request?: RequestInit,
+ parse?: (response: Response) => T | Promise;
+ initialState?: T | (() => T),
+ execute?: boolean;
+ onError?: (error: unknown) => void;
+ onData?: (data: T) => void;
+ onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void;
+ },
+): AsyncState & {
+ revalidate: () => void;
+ mutate: MutatePromiseFn;
+};
+export function useFetch(
+ url: RequestInfo | URL,
+ options: {
+ request?: RequestInit,
+ parse?: (response: Response) => V | Promise;
+ map: (result: V) => T | Promise;
+ initialState?: T | (() => T),
+ execute?: boolean;
+ onError?: (error: unknown) => void;
+ onData?: (data: T) => void;
+ onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void;
+ },
+): AsyncState & {
+ revalidate: () => void;
+ mutate: MutatePromiseFn;
+};
+export function useFetch(
+ url: RequestInfo | URL,
+ options?: {
+ request?: RequestInit,
+ parse?: (response: Response) => V | Promise;
+ map?: (result: V) => T | Promise;
+ initialState?: T | V | (() => T | V),
+ execute?: boolean;
+ onError?: (error: unknown) => void;
+ onData?: (data: T | V) => void;
+ onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void;
+ },
+): AsyncState & {
+ revalidate: () => void;
+ mutate: MutatePromiseFn;
+} {
+ const abortable = useRef();
+
+ return useCachedPromise(
+ async (inputParam: RequestInfo | URL): Promise => {
+ 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,
+ }
+ )
+}
\ No newline at end of file