Add error handling in the fetch component

This commit is contained in:
Zoe Roux 2022-12-03 22:34:50 +09:00
parent e82e515a23
commit 67de27578e
No known key found for this signature in database
GPG Key ID: B2AB52A2636E5C46
17 changed files with 146 additions and 226 deletions

View File

@ -4,7 +4,7 @@
"slug": "kyoo",
"scheme": "kyoo",
"version": "1.0.0",
"orientation": "portrait",
"orientation": "default",
"icon": "./assets/icon.png",
"entryPoint": "./index.ts",
"userInterfaceStyle": "light",

View File

@ -22,6 +22,9 @@ import { Stack } from "expo-router";
import { Avatar, ThemeSelector } from "@kyoo/primitives";
import { useTheme } from "yoshiki/native";
import { NavbarTitle } from "@kyoo/ui";
import { useState } from "react";
import { QueryClientProvider } from "@tanstack/react-query";
import { createQueryClient } from "@kyoo/models";
const ThemedStack = () => {
const theme = useTheme();
@ -44,9 +47,13 @@ const ThemedStack = () => {
};
export default function Root() {
const [queryClient] = useState(() => createQueryClient());
return (
<ThemeSelector>
<ThemedStack />
</ThemeSelector>
<QueryClientProvider client={queryClient}>
<ThemeSelector>
<ThemedStack />
</ThemeSelector>
</QueryClientProvider>
);
}

View File

@ -18,6 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Navbar } from "@kyoo/ui";
import { Text, View } from "react-native";
import { useYoshiki } from "yoshiki/native";
@ -26,7 +27,7 @@ const App = () => {
return (
<View {...css({ backgroundColor: (theme) => theme.background })}>
{/* <Navbar /> */}
<Navbar />
<Text>toto</Text>
</View>
);

View File

@ -24,7 +24,7 @@ import { useTheme, useMobileHover } from "yoshiki/web";
import { createTheme, ThemeProvider as MTheme } from "@mui/material";
import NextApp, { AppContext, type AppProps } from "next/app";
import { Hydrate, QueryClientProvider } from "@tanstack/react-query";
import { createQueryClient, fetchQuery, QueryIdentifier, QueryPage } from "~/utils/query";
import { createQueryClient, fetchQuery, QueryIdentifier, QueryPage } from "@kyoo/models";
import superjson from "superjson";
import Head from "next/head";
import { ThemeSelector as KThemeSelector } from "@kyoo/primitives";

View File

@ -43,7 +43,7 @@ import { getDisplayDate } from "@kyoo/models";
import { InfiniteScroll } from "~/utils/infinite-scroll";
import { Link } from "~/utils/link";
import { withRoute } from "~/utils/router";
import { QueryIdentifier, QueryPage, useInfiniteFetch } from "~/utils/query";
import { QueryIdentifier, QueryPage, useInfiniteFetch } from "@kyoo/models";
import { px } from "yoshiki/native";
enum SortBy {

View File

@ -1,160 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { ComponentType, ReactElement, ReactNode } from "react";
import {
dehydrate,
QueryClient,
QueryFunctionContext,
useInfiniteQuery,
useQuery,
} from "@tanstack/react-query";
import { z } from "zod";
import { KyooErrors, Page } from "~/models";
import { Paged } from "~/models";
const queryFn = async <Data>(
type: z.ZodType<Data>,
context: QueryFunctionContext,
): Promise<Data> => {
const kyoo_url = process.env.KYOO_URL ?? "http://localhost:5000";
let resp;
try {
resp = await fetch(
[typeof window === "undefined" ? kyoo_url : "/api"]
.concat(
context.pageParam ? [context.pageParam] : (context.queryKey.filter((x) => x) as string[]),
)
.join("/")
.replace("/?", "?"),
);
} catch (e) {
console.log("Fetch error", e);
throw { errors: ["Could not reach Kyoo's server."] } as KyooErrors;
}
if (resp.status === 404) {
throw { errors: ["Resource not found."] } as KyooErrors;
}
if (!resp.ok) {
const error = await resp.text();
let data;
try {
data = JSON.parse(error);
} catch (e) {
data = { errors: [error] } as KyooErrors;
}
console.log("Invalid response:", data);
throw data as KyooErrors;
}
let data;
try {
data = await resp.json();
} catch (e) {
console.error("Invald json from kyoo", e);
throw { errors: ["Invalid repsonse from kyoo"] };
}
const parsed = await type.safeParseAsync(data);
if (!parsed.success) {
console.log("Parse error: ", parsed.error);
throw { errors: parsed.error.errors.map((x) => x.message) } as KyooErrors;
}
return parsed.data;
};
export const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
},
},
});
export type QueryIdentifier<T = unknown> = {
parser: z.ZodType<T, z.ZodTypeDef, any>;
path: (string | undefined)[];
params?: { [query: string]: boolean | number | string | string[] | undefined };
infinite?: boolean;
};
export type QueryPage<Props = {}> = ComponentType<Props> & {
getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[];
getLayout?: (page: ReactElement) => ReactNode;
};
const toQueryKey = <Data>(query: QueryIdentifier<Data>) => {
if (query.params) {
return [
...query.path,
"?" +
Object.entries(query.params)
.filter(([k, v]) => v !== undefined)
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
.join("&"),
];
} else {
return query.path;
}
};
export const useFetch = <Data>(query: QueryIdentifier<Data>) => {
return useQuery<Data, KyooErrors>({
queryKey: toQueryKey(query),
queryFn: (ctx) => queryFn(query.parser, ctx),
});
};
export const useInfiniteFetch = <Data>(query: QueryIdentifier<Data>) => {
const ret = useInfiniteQuery<Page<Data>, KyooErrors>({
queryKey: toQueryKey(query),
queryFn: (ctx) => queryFn(Paged(query.parser), ctx),
getNextPageParam: (page: Page<Data>) => page?.next || undefined,
});
return { ...ret, items: ret.data?.pages.flatMap((x) => x.items) };
};
export const fetchQuery = async (queries: QueryIdentifier[]) => {
// we can't put this check in a function because we want build time optimizations
// see https://github.com/vercel/next.js/issues/5354 for details
if (typeof window !== "undefined") return {};
const client = createQueryClient();
await Promise.all(
queries.map((query) => {
if (query.infinite) {
return client.prefetchInfiniteQuery({
queryKey: toQueryKey(query),
queryFn: (ctx) => queryFn(Paged(query.parser), ctx),
});
} else {
return client.prefetchQuery({
queryKey: toQueryKey(query),
queryFn: (ctx) => queryFn(query.parser, ctx),
});
}
}),
);
return dehydrate(client);
};

View File

@ -18,10 +18,10 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./resources";
export * from "./traits";
export * from "./page";
export * from "./kyoo-errors";
export * from "./utils"
export * from "./traits";
export * from "./resources";
export * from "./query";

View File

@ -29,7 +29,6 @@ import {
import { z } from "zod";
import { KyooErrors } from "./kyoo-errors";
import { Page, Paged } from "./page";
import { Library } from "./resources";
const queryFn = async <Data,>(
type: z.ZodType<Data>,
@ -159,49 +158,3 @@ export const fetchQuery = async (queries: QueryIdentifier[]) => {
);
return dehydrate(client);
};
/* export const Fetch = <Data,>({ */
/* query, */
/* children, */
/* }: { */
/* query: QueryIdentifier<Data>; */
/* children: ( */
/* item: (Data & { isLoading: false }) | { isLoading: true }, */
/* i: number, */
/* ) => JSX.Element | null; */
/* }) => { */
/* const { data, error, isSuccess, isError } = useFetch(query); */
/* return children(isSuccess ? { ...data, isLoading: false } : { isLoading: true }, 0); */
/* }; */
type WithLoading<Item> = (Item & { isLoading: false }) | { isLoading: true };
const isPage = <T = unknown,>(obj: unknown): obj is Page<T> =>
(typeof obj === "object" && obj && "items" in obj) || false;
export const Fetch = <Data,>({
query,
placeholderCount,
children,
}: {
query: QueryIdentifier<Data>;
placeholderCount?: number;
children: (
item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>,
i: number,
) => JSX.Element | null;
}) => {
const { data, error } = useFetch(query);
if (error) throw error;
if (!isPage<object>(data))
return <> {children(data ? { ...data, isLoading: false } : ({ isLoading: true } as any), 0)}</>;
return (
<>
{data
? data.items.map((item, i) => children({ ...item, isLoading: false } as any, i))
: [...Array(placeholderCount)].map((_, i) => children({ isLoading: true } as any, i))}
</>
);
};

View File

@ -34,7 +34,7 @@ export enum ItemType {
export const LibraryItemP = z.preprocess(
(x: any) => {
x.aliases ??= [];
if (!x.aliases) x.aliases = [];
return x;
},
z.union([

View File

@ -14,7 +14,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-native",
"jsx": "react-jsx",
"incremental": true,
"baseUrl": ".",
"paths": {

View File

@ -20,12 +20,14 @@
import { ThemeBuilder } from "./theme";
// Ref: https://github.com/catppuccin/catppuccin
export const catppuccin: ThemeBuilder = {
fonts: {
heading: "Pacifico",
paragraph: "Poppins",
},
light: {
// Catppuccin latte
appbar: "#e64553",
contrast: "#cdd6f4",
subcontrast: "#bac2de",
@ -45,8 +47,17 @@ export const catppuccin: ThemeBuilder = {
paragraph: "#5c5f77",
subtext: "#6c6f85",
},
colors: {
red: "#d20f39",
green: "#40a02b",
blue: "#1e66f5",
yellow: "#df8e1d",
black: "#4c4f69",
white: "#eff1f5",
},
},
dark: {
// Catppuccin mocha
appbar: "#94e2d5",
contrast: "#cdd6f4",
subcontrast: "#bac2de",
@ -66,5 +77,13 @@ export const catppuccin: ThemeBuilder = {
paragraph: "#bac2de",
subtext: "#a6adc8",
},
colors: {
red: "#f38ba8",
green: "#a6e3a1",
blue: "#89b4fa",
yellow: "#f9e2af",
black: "#11111b",
white: "#cdd6f4",
},
},
};

View File

@ -39,6 +39,14 @@ type Mode = {
contrast: Property.Color;
subcontrast: Property.Color;
variant: Variant;
colors: {
red: Property.Color,
green: Property.Color,
blue: Property.Color,
yellow: Property.Color,
white: Property.Color,
black: Property.Color,
}
};
type Variant = {

View File

@ -14,7 +14,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-native",
"jsx": "react-jsx",
"incremental": true,
"baseUrl": ".",
"paths": {

View File

@ -0,0 +1,73 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Page, QueryIdentifier, useFetch, KyooErrors } from "@kyoo/models";
import { P } from "@kyoo/primitives";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
export type WithLoading<Item> = (Item & { isLoading: false }) | { isLoading: true };
const isPage = <T = unknown,>(obj: unknown): obj is Page<T> =>
(typeof obj === "object" && obj && "items" in obj) || false;
export const Fetch = <Data,>({
query,
placeholderCount,
children,
}: {
query: QueryIdentifier<Data>;
placeholderCount: number;
children: (
item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>,
i: number,
) => JSX.Element | null;
}): JSX.Element | null => {
const { data, error } = useFetch(query);
if (error) return <ErrorView error={error} />;
if (!data)
return (
<>{[...Array(placeholderCount)].map((_, i) => children({ isLoading: true } as any, i))}</>
);
if (!isPage<object>(data))
return children(data ? { ...data, isLoading: false } : ({ isLoading: true } as any), 0);
return <>{data.items.map((item, i) => children({ ...item, isLoading: false } as any, i))}</>;
};
export const ErrorView = ({ error }: { error: KyooErrors }) => {
const { css } = useYoshiki();
return (
<View
{...css({
backgroundColor: (theme) => theme.colors.red,
flex: 1,
alignItems: "center"
})}
>
{error.errors.map((x, i) => (
<P key={i} {...css({ color: (theme) => theme.colors.white })}>
{x}
</P>
))}
</View>
);
};

View File

@ -19,10 +19,11 @@
*/
import useTranslation from "next-translate/useTranslation";
import { Library, LibraryP, Page, Paged, Fetch, QueryIdentifier } from "@kyoo/models";
import { useYoshiki } from "yoshiki/native";
import { Library, LibraryP, Page, Paged, QueryIdentifier } from "@kyoo/models";
import { IconButton, Header, Avatar, A, ts } from "@kyoo/primitives";
import { useYoshiki } from "yoshiki/native";
import { Text, View } from "react-native";
import { Fetch } from "../fetch";
import { KyooLongLogo } from "./icon";
const tooltip = (tooltip: string): object => ({});
@ -64,12 +65,13 @@ export const Navbar = () => {
<View
{...css({
flexGrow: 1,
flexShrink: 1,
flexDirection: "row",
display: { xs: "none", sm: "flex" },
marginLeft: ts(2),
marginX: ts(2),
})}
>
<Fetch query={Navbar.query()}>
<Fetch query={Navbar.query()} placeholderCount={4}>
{(library, i) =>
!library.isLoading ? (
<A

View File

@ -15,8 +15,6 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsxImportSource": "@emotion/react",
"types": ["@emotion/react/types/css-prop"],
"incremental": true,
"baseUrl": ".",
"paths": {

View File

@ -2341,12 +2341,30 @@ __metadata:
languageName: node
linkType: hard
"@kyoo/models@workspace:^, @kyoo/models@workspace:packages/models":
version: 0.0.0-use.local
resolution: "@kyoo/models@workspace:packages/models"
dependencies:
"@tanstack/react-query": ^4.18.0
"@types/react": ^18.0.25
typescript: ^4.9.3
zod: ^3.19.1
peerDependencies:
react: "*"
react-native: "*"
peerDependenciesMeta:
react-native-web:
optional: true
languageName: unknown
linkType: soft
"@kyoo/primitives@workspace:^, @kyoo/primitives@workspace:packages/primitives":
version: 0.0.0-use.local
resolution: "@kyoo/primitives@workspace:packages/primitives"
dependencies:
"@expo/html-elements": ^0.2.2
"@expo/vector-icons": "AnonymusRaccoon/expo-vector-icons#no-prepare"
"@tanstack/react-query": ^4.18.0
"@types/react": ^18.0.25
solito: ^2.0.5
typescript: ^4.9.3
@ -2364,6 +2382,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@kyoo/ui@workspace:packages/ui"
dependencies:
"@kyoo/models": "workspace:^"
"@kyoo/primitives": "workspace:^"
"@types/react": ^18.0.25
react-native-svg: ^13.6.0
@ -12907,12 +12926,12 @@ __metadata:
"@emotion/react": ^11.9.3
"@emotion/styled": ^11.9.3
"@jellyfin/libass-wasm": ^4.1.1
"@kyoo/models": "workspace:^"
"@kyoo/primitives": "workspace:^"
"@kyoo/ui": "workspace:^"
"@mui/icons-material": ^5.8.4
"@mui/material": ^5.8.7
"@mui/system": ^5.10.10
"@tanstack/react-query": ^4.18.0
"@types/node": 18.11.9
"@types/react": 18.0.25
"@types/react-dom": 18.0.9