mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-08 18:54:22 -04:00
Rework query function
This commit is contained in:
parent
c1e3a67a4e
commit
5d6bb63ba2
@ -1,7 +1,7 @@
|
|||||||
import { Text, View } from "react-native";
|
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { LibraryItem, LibraryItemP, type News, NewsP } from "~/models";
|
import { type LibraryItem, LibraryItemP } from "~/models";
|
||||||
import type { QueryIdentifier } from "~/query/index";
|
import { P } from "~/primitives";
|
||||||
|
import { Fetch, type QueryIdentifier } from "~/query";
|
||||||
|
|
||||||
export async function loader() {
|
export async function loader() {
|
||||||
await prefetchQuery(Header.query());
|
await prefetchQuery(Header.query());
|
||||||
@ -12,15 +12,9 @@ export default function Header() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fetch
|
<Fetch
|
||||||
query={NewsList.query()}
|
query={Header.query()}
|
||||||
layout={{ ...ItemGrid.layout, layout: "horizontal" }}
|
Render={({ name }) => <P {...css({ bg: "red" })}>{name}</P>}
|
||||||
getItemType={(x, i) => (x?.kind === "movie" || (!x && i % 2) ? "movie" : "episode")}
|
Loader={() => <P>Loading</P>}
|
||||||
getItemSize={(kind) => (kind === "episode" ? 2 : 1)}
|
|
||||||
empty={t("home.none")}
|
|
||||||
Render={({ item }) => {
|
|
||||||
<Text>{item.name}</Text>;
|
|
||||||
}}
|
|
||||||
Loader={({ index }) => (index % 2 ? <EpisodeBox.Loader /> : <ItemGrid.Loader />)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,7 @@
|
|||||||
import {
|
import { QueryClient, useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||||
QueryClient,
|
import { type ComponentType, type ReactElement, useContext } from "react";
|
||||||
type QueryFunctionContext,
|
|
||||||
useInfiniteQuery,
|
|
||||||
useQuery,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
import type { ComponentType, ReactElement } from "react";
|
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { type KyooError, type Page, Paged } from "~/models";
|
import { type KyooError, type Page, Paged } from "~/models";
|
||||||
// import { getToken, getTokenWJ } from "./login";
|
|
||||||
|
|
||||||
export let lastUsedUrl: string = null!;
|
|
||||||
|
|
||||||
const cleanSlash = (str: string | null, keepFirst = false) => {
|
const cleanSlash = (str: string | null, keepFirst = false) => {
|
||||||
if (!str) return null;
|
if (!str) return null;
|
||||||
@ -17,41 +9,19 @@ const cleanSlash = (str: string | null, keepFirst = false) => {
|
|||||||
return str.replace(/^\/|\/$/g, "");
|
return str.replace(/^\/|\/$/g, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const queryFn = async <Parser extends z.ZodTypeAny>(
|
export const queryFn = async <Parser extends z.ZodTypeAny>(context: {
|
||||||
context: {
|
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||||
apiUrl?: string | null;
|
url: string;
|
||||||
authenticated?: boolean;
|
body?: object;
|
||||||
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
formData?: FormData;
|
||||||
} & (
|
plainText?: boolean;
|
||||||
| QueryFunctionContext
|
authToken: string | null;
|
||||||
| ({
|
parser?: Parser;
|
||||||
path: (string | false | undefined | null)[];
|
signal: AbortSignal;
|
||||||
body?: object;
|
}): Promise<z.infer<Parser>> => {
|
||||||
formData?: FormData;
|
|
||||||
plainText?: boolean;
|
|
||||||
} & Partial<QueryFunctionContext>)
|
|
||||||
),
|
|
||||||
type?: Parser,
|
|
||||||
iToken?: string | null,
|
|
||||||
): Promise<z.infer<Parser>> => {
|
|
||||||
const url = context.apiUrl && context.apiUrl.length > 0 ? context.apiUrl : getCurrentApiUrl();
|
|
||||||
lastUsedUrl = url!;
|
|
||||||
|
|
||||||
const token = iToken === undefined && context.authenticated !== false ? await getToken() : iToken;
|
|
||||||
const path = [cleanSlash(url, true)]
|
|
||||||
.concat(
|
|
||||||
"path" in context
|
|
||||||
? (context.path as string[])
|
|
||||||
: "pageParam" in context && context.pageParam
|
|
||||||
? [cleanSlash(context.pageParam as string)]
|
|
||||||
: (context.queryKey as string[]),
|
|
||||||
)
|
|
||||||
.filter((x) => x)
|
|
||||||
.join("/")
|
|
||||||
.replace("/?", "?");
|
|
||||||
let resp: Response;
|
let resp: Response;
|
||||||
try {
|
try {
|
||||||
resp = await fetch(path, {
|
resp = await fetch(context.url, {
|
||||||
method: context.method,
|
method: context.method,
|
||||||
body:
|
body:
|
||||||
"body" in context && context.body
|
"body" in context && context.body
|
||||||
@ -60,7 +30,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
? context.formData
|
? context.formData
|
||||||
: undefined,
|
: undefined,
|
||||||
headers: {
|
headers: {
|
||||||
...(token ? { Authorization: token } : {}),
|
...(context.authToken ? { Authorization: context.authToken } : {}),
|
||||||
...("body" in context ? { "Content-Type": "application/json" } : {}),
|
...("body" in context ? { "Content-Type": "application/json" } : {}),
|
||||||
},
|
},
|
||||||
signal: context.signal,
|
signal: context.signal,
|
||||||
@ -68,20 +38,12 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (typeof e === "object" && e && "name" in e && e.name === "AbortError")
|
if (typeof e === "object" && e && "name" in e && e.name === "AbortError")
|
||||||
throw { message: "Aborted", status: "aborted" } as KyooError;
|
throw { message: "Aborted", status: "aborted" } as KyooError;
|
||||||
console.log("Fetch error", e, path);
|
console.log("Fetch error", e, context.url);
|
||||||
throw { message: "Could not reach Kyoo's server.", status: "aborted" } as KyooError;
|
throw { message: "Could not reach Kyoo's server.", status: "aborted" } as KyooError;
|
||||||
}
|
}
|
||||||
if (resp.status === 404) {
|
if (resp.status === 404) {
|
||||||
throw { message: "Resource not found.", status: 404 } as KyooError;
|
throw { message: "Resource not found.", status: 404 } as KyooError;
|
||||||
}
|
}
|
||||||
// If we got a forbidden, try to refresh the token
|
|
||||||
// if we got a token as an argument, it either means we already retried or we go one provided that's fresh
|
|
||||||
// so we can't retry either ways.
|
|
||||||
if (resp.status === 403 && iToken === undefined && token) {
|
|
||||||
const [newToken, _, error] = await getTokenWJ(undefined, true);
|
|
||||||
if (newToken) return await queryFn(context, type, newToken);
|
|
||||||
console.error("refresh error while retrying a forbidden", error);
|
|
||||||
}
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const error = await resp.text();
|
const error = await resp.text();
|
||||||
let data: Record<string, any>;
|
let data: Record<string, any>;
|
||||||
@ -92,9 +54,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
}
|
}
|
||||||
data.status = resp.status;
|
data.status = resp.status;
|
||||||
console.trace(
|
console.trace(
|
||||||
`Invalid response (${
|
`Invalid response (${context.method ?? "GET"} ${context.url}):`,
|
||||||
"method" in context && context.method ? context.method : "GET"
|
|
||||||
} ${path}):`,
|
|
||||||
data,
|
data,
|
||||||
resp.status,
|
resp.status,
|
||||||
);
|
);
|
||||||
@ -103,7 +63,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
|
|
||||||
if (resp.status === 204) return null;
|
if (resp.status === 204) return null;
|
||||||
|
|
||||||
if ("plainText" in context && context.plainText) return (await resp.text()) as unknown;
|
if (context.plainText) return (await resp.text()) as unknown;
|
||||||
|
|
||||||
let data: Record<string, any>;
|
let data: Record<string, any>;
|
||||||
try {
|
try {
|
||||||
@ -112,10 +72,10 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(
|
|||||||
console.error("Invalid json from kyoo", e);
|
console.error("Invalid json from kyoo", e);
|
||||||
throw { message: "Invalid response from kyoo", status: "json" } as KyooError;
|
throw { message: "Invalid response from kyoo", status: "json" } as KyooError;
|
||||||
}
|
}
|
||||||
if (!type) return data;
|
if (!context.parser) return data;
|
||||||
const parsed = await type.safeParseAsync(data);
|
const parsed = await context.parser.safeParseAsync(data);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
console.log("Path: ", path, " Response: ", resp.status, " Parse error: ", parsed.error);
|
console.log("Url: ", context.url, " Response: ", resp.status, " Parse error: ", parsed.error);
|
||||||
throw {
|
throw {
|
||||||
status: "parse",
|
status: "parse",
|
||||||
message:
|
message:
|
||||||
@ -178,12 +138,12 @@ export type QueryPage<Props = {}, Items = unknown> = ComponentType<
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const toQueryKey = (query: {
|
export const toQueryKey = (query: {
|
||||||
|
apiUrl: string;
|
||||||
path: (string | undefined)[];
|
path: (string | undefined)[];
|
||||||
params?: { [query: string]: boolean | number | string | string[] | undefined };
|
params?: { [query: string]: boolean | number | string | string[] | undefined };
|
||||||
options?: { apiUrl?: string | null };
|
|
||||||
}) => {
|
}) => {
|
||||||
return [
|
return [
|
||||||
query.options?.apiUrl,
|
cleanSlash(query.apiUrl, true),
|
||||||
...query.path,
|
...query.path,
|
||||||
query.params
|
query.params
|
||||||
? `?${Object.entries(query.params)
|
? `?${Object.entries(query.params)
|
||||||
@ -195,30 +155,38 @@ export const toQueryKey = (query: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
||||||
|
const { apiUrl, authToken } = useContext(QueryContext);
|
||||||
|
const key = toQueryKey({ apiUrl, path: query.path, params: query.params });
|
||||||
|
|
||||||
return useQuery<Data, KyooError>({
|
return useQuery<Data, KyooError>({
|
||||||
queryKey: toQueryKey(query),
|
queryKey: key,
|
||||||
queryFn: (ctx) =>
|
queryFn: (ctx) =>
|
||||||
queryFn(
|
queryFn({
|
||||||
{
|
url: key.join("/").replace("/?", "?"),
|
||||||
...ctx,
|
parser: query.parser,
|
||||||
queryKey: toQueryKey({ ...query, options: {} }),
|
signal: ctx.signal,
|
||||||
...query.options,
|
authToken,
|
||||||
},
|
...query.options,
|
||||||
query.parser,
|
}),
|
||||||
),
|
|
||||||
placeholderData: query.placeholderData as any,
|
placeholderData: query.placeholderData as any,
|
||||||
enabled: query.enabled,
|
enabled: query.enabled,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInfiniteFetch = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => {
|
export const useInfiniteFetch = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => {
|
||||||
|
const { apiUrl, authToken } = useContext(QueryContext);
|
||||||
|
const key = toQueryKey({ apiUrl, path: query.path, params: query.params });
|
||||||
|
|
||||||
const ret = useInfiniteQuery<Page<Data>, KyooError>({
|
const ret = useInfiniteQuery<Page<Data>, KyooError>({
|
||||||
queryKey: toQueryKey(query),
|
queryKey: key,
|
||||||
queryFn: (ctx) =>
|
queryFn: (ctx) =>
|
||||||
queryFn(
|
queryFn({
|
||||||
{ ...ctx, queryKey: toQueryKey({ ...query, options: {} }), ...query.options },
|
url: (ctx.pageParam as string) ?? key.join("/").replace("/?", "?"),
|
||||||
Paged(query.parser),
|
parser: Paged(query.parser),
|
||||||
),
|
signal: ctx.signal,
|
||||||
|
authToken,
|
||||||
|
...query.options,
|
||||||
|
}),
|
||||||
getNextPageParam: (page: Page<Data>) => page?.next || undefined,
|
getNextPageParam: (page: Page<Data>) => page?.next || undefined,
|
||||||
initialPageParam: undefined,
|
initialPageParam: undefined,
|
||||||
placeholderData: query.placeholderData as any,
|
placeholderData: query.placeholderData as any,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user