/* * 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 . */ 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 ( type: z.ZodType, context: QueryFunctionContext, ): Promise => { 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 = { parser: z.ZodType; path: (string | undefined)[]; params?: { [query: string]: boolean | number | string | string[] | undefined }; infinite?: boolean; }; export type QueryPage = ComponentType & { getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[]; getLayout?: (page: ReactElement) => ReactNode; }; const toQueryKey = (query: QueryIdentifier) => { 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 = (query: QueryIdentifier) => { return useQuery({ queryKey: toQueryKey(query), queryFn: (ctx) => queryFn(query.parser, ctx), }); }; export const useInfiniteFetch = (query: QueryIdentifier) => { const ret = useInfiniteQuery, KyooErrors>({ queryKey: toQueryKey(query), queryFn: (ctx) => queryFn(Paged(query.parser), ctx), getNextPageParam: (page: Page) => 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); };