aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Data
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Data')
-rw-r--r--src/lib/Data/AniList/cacheHydration.ts48
-rw-r--r--src/lib/Data/AniList/media.ts114
2 files changed, 112 insertions, 50 deletions
diff --git a/src/lib/Data/AniList/cacheHydration.ts b/src/lib/Data/AniList/cacheHydration.ts
new file mode 100644
index 00000000..434d2a82
--- /dev/null
+++ b/src/lib/Data/AniList/cacheHydration.ts
@@ -0,0 +1,48 @@
+import { browser } from "$app/environment";
+import anime from "$stores/anime";
+import lastPruneTimes from "$stores/lastPruneTimes";
+import manga from "$stores/manga";
+import localforage from "localforage";
+
+type MediaCacheKind = "anime" | "manga";
+
+interface StoredLastPruneTimes {
+ anime: number;
+ chapters: number;
+ manga: number;
+}
+
+const hydration = new Map<MediaCacheKind, Promise<void>>();
+
+const isStoredLastPruneTimes = (
+ value: unknown,
+): value is StoredLastPruneTimes =>
+ typeof value === "object" &&
+ value !== null &&
+ typeof (value as StoredLastPruneTimes).anime === "number" &&
+ typeof (value as StoredLastPruneTimes).chapters === "number" &&
+ typeof (value as StoredLastPruneTimes).manga === "number";
+
+export const hydrateMediaListCache = (kind: MediaCacheKind) => {
+ if (!browser) return Promise.resolve();
+
+ const existing = hydration.get(kind);
+
+ if (existing) return existing;
+
+ const promise = (async () => {
+ const [cache, pruneTimes] = await Promise.all([
+ localforage.getItem<string>(kind),
+ localforage.getItem<StoredLastPruneTimes>("lastPruneTimes"),
+ ]);
+
+ if (typeof cache === "string" && cache.length)
+ (kind === "anime" ? anime : manga).set(cache);
+
+ if (isStoredLastPruneTimes(pruneTimes)) lastPruneTimes.set(pruneTimes);
+ })();
+
+ hydration.set(kind, promise);
+
+ return promise;
+};
diff --git a/src/lib/Data/AniList/media.ts b/src/lib/Data/AniList/media.ts
index f2a4aad6..d8315b19 100644
--- a/src/lib/Data/AniList/media.ts
+++ b/src/lib/Data/AniList/media.ts
@@ -233,6 +233,21 @@ const assignDefaultOptions = (options: CollectionOptions) => {
return nonNullOptions;
};
+const inFlightCollections = new Map<string, Promise<Media[]>>();
+
+const collectionKey = (
+ type: Type,
+ userId: number,
+ options: CollectionOptions,
+) =>
+ JSON.stringify({
+ type,
+ userId,
+ includeCompleted: options.includeCompleted,
+ all: options.all,
+ includeRelations: options.includeRelations,
+ });
+
export const mediaListCollection = async (
anilistAuthorisation: AniListAuthorisation,
userIdentity: UserIdentity,
@@ -274,61 +289,60 @@ export const mediaListCollection = async (
if (mediaCache !== undefined && mediaCache !== "")
return parseJsonStringOrDefault<Media[]>(mediaCache, []);
- const userIdResponse = await (
- await fetch("https://graphql.anilist.co", {
- method: "POST",
- headers: {
- Authorization: `${anilistAuthorisation.tokenType} ${anilistAuthorisation.accessToken}`,
- "Content-Type": "application/json",
- Accept: "application/json",
- },
- body: JSON.stringify({
- query: collectionQueryTemplate(type, userIdentity.id, options),
- }),
- })
- ).json();
+ const key = collectionKey(type, userIdentity.id, options);
+ const existing = inFlightCollections.get(key);
- if (
- !userIdResponse["data"] ||
- !userIdResponse["data"]["MediaListCollection"] ||
- !userIdResponse["data"]["MediaListCollection"]["lists"]
- )
- return [];
+ if (existing) return existing;
- if (mediaCache === "")
- if (type === Type.Anime)
- anime.set(
- JSON.stringify(
- flattenLists(
- userIdResponse["data"]["MediaListCollection"]["lists"],
- options.all,
- ),
- ),
- );
- else
- manga.set(
- JSON.stringify(
- flattenLists(
- userIdResponse["data"]["MediaListCollection"]["lists"],
- options.all,
- ),
- ),
- );
+ const request = (async () => {
+ const userIdResponse = await (
+ await fetch("https://graphql.anilist.co", {
+ method: "POST",
+ headers: {
+ Authorization: `${anilistAuthorisation.tokenType} ${anilistAuthorisation.accessToken}`,
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify({
+ query: collectionQueryTemplate(type, userIdentity.id, options),
+ }),
+ })
+ ).json();
- if (options.addNotification)
- options.addNotification(
- getOptions({
- heading: options.notificationType
- ? options.notificationType
- : Type[type],
- description: "Re-cached media lists from AniList",
- }),
+ if (
+ !userIdResponse["data"] ||
+ !userIdResponse["data"]["MediaListCollection"] ||
+ !userIdResponse["data"]["MediaListCollection"]["lists"]
+ )
+ return [];
+
+ const flattened = flattenLists(
+ userIdResponse["data"]["MediaListCollection"]["lists"],
+ options.all,
);
- return flattenLists(
- userIdResponse["data"]["MediaListCollection"]["lists"],
- options.all,
- );
+ if (mediaCache === "")
+ if (type === Type.Anime) anime.set(JSON.stringify(flattened));
+ else manga.set(JSON.stringify(flattened));
+
+ if (options.addNotification)
+ options.addNotification(
+ getOptions({
+ heading: options.notificationType
+ ? options.notificationType
+ : Type[type],
+ description: "Re-cached media lists from AniList",
+ }),
+ );
+
+ return flattened;
+ })().finally(() => {
+ inFlightCollections.delete(key);
+ });
+
+ inFlightCollections.set(key, request);
+
+ return request;
};
export const publicMediaListCollection = async (