/*
* 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);
};