mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-04 03:27:14 -05: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: {
 | 
					 | 
				
			||||||
		apiUrl?: string | null;
 | 
					 | 
				
			||||||
		authenticated?: boolean;
 | 
					 | 
				
			||||||
	method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
 | 
						method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
 | 
				
			||||||
	} & (
 | 
						url: string;
 | 
				
			||||||
		| QueryFunctionContext
 | 
					 | 
				
			||||||
		| ({
 | 
					 | 
				
			||||||
				path: (string | false | undefined | null)[];
 | 
					 | 
				
			||||||
	body?: object;
 | 
						body?: object;
 | 
				
			||||||
	formData?: FormData;
 | 
						formData?: FormData;
 | 
				
			||||||
	plainText?: boolean;
 | 
						plainText?: boolean;
 | 
				
			||||||
		  } & Partial<QueryFunctionContext>)
 | 
						authToken: string | null;
 | 
				
			||||||
	),
 | 
						parser?: Parser;
 | 
				
			||||||
	type?: Parser,
 | 
						signal: AbortSignal;
 | 
				
			||||||
	iToken?: string | null,
 | 
					}): Promise<z.infer<Parser>> => {
 | 
				
			||||||
): 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,
 | 
				
			||||||
 | 
									authToken,
 | 
				
			||||||
				...query.options,
 | 
									...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