aboutsummaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/Data/AniList/cacheHydration.ts48
-rw-r--r--src/lib/Data/AniList/media.ts114
-rw-r--r--src/lib/List/Anime/CompletedAnimeList.svelte3
-rw-r--r--src/lib/List/Anime/DueAnimeList.svelte2
-rw-r--r--src/lib/List/Anime/UpcomingAnimeList.svelte3
-rw-r--r--src/lib/List/Manga/MangaListTemplate.svelte2
-rw-r--r--src/lib/Utility/persistentStore.ts19
7 files changed, 136 insertions, 55 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 (
diff --git a/src/lib/List/Anime/CompletedAnimeList.svelte b/src/lib/List/Anime/CompletedAnimeList.svelte
index 6bfdfdd1..9c58a311 100644
--- a/src/lib/List/Anime/CompletedAnimeList.svelte
+++ b/src/lib/List/Anime/CompletedAnimeList.svelte
@@ -1,5 +1,6 @@
<script lang="ts">
import { mediaListCollection, Type, type Media } from "$lib/Data/AniList/media";
+import { hydrateMediaListCache } from "$lib/Data/AniList/cacheHydration";
import type { AniListAuthorisation } from "$lib/Data/AniList/identity";
import { onMount } from "svelte";
import anime from "$stores/anime";
@@ -25,6 +26,8 @@ let startTime: number;
let endTime: number;
onMount(async () => {
+ await hydrateMediaListCache("anime");
+
startTime = performance.now();
if (dummy) {
diff --git a/src/lib/List/Anime/DueAnimeList.svelte b/src/lib/List/Anime/DueAnimeList.svelte
index e170a81e..2c707ffb 100644
--- a/src/lib/List/Anime/DueAnimeList.svelte
+++ b/src/lib/List/Anime/DueAnimeList.svelte
@@ -1,5 +1,6 @@
<script lang="ts">
import { mediaListCollection, Type, type Media } from "$lib/Data/AniList/media";
+import { hydrateMediaListCache } from "$lib/Data/AniList/cacheHydration";
import type { AniListAuthorisation } from "$lib/Data/AniList/identity";
import { onDestroy, onMount } from "svelte";
import anime from "$stores/anime";
@@ -48,6 +49,7 @@ const restartKeyCacher = (cacheMinutes: number) => {
};
onMount(async () => {
+ await hydrateMediaListCache("anime");
restartKeyCacher($settings.cacheMinutes);
startTime = performance.now();
diff --git a/src/lib/List/Anime/UpcomingAnimeList.svelte b/src/lib/List/Anime/UpcomingAnimeList.svelte
index 4bd7a287..d9b91122 100644
--- a/src/lib/List/Anime/UpcomingAnimeList.svelte
+++ b/src/lib/List/Anime/UpcomingAnimeList.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
import Spacer from "$lib/Layout/Spacer.svelte";
import { mediaListCollection, Type, type Media } from "$lib/Data/AniList/media";
+import { hydrateMediaListCache } from "$lib/Data/AniList/cacheHydration";
import type { AniListAuthorisation } from "$lib/Data/AniList/identity";
import { onMount } from "svelte";
import anime from "$stores/anime";
@@ -24,6 +25,8 @@ let startTime: number;
let endTime: number;
onMount(async () => {
+ await hydrateMediaListCache("anime");
+
startTime = performance.now();
animeLists = mediaListCollection(
user,
diff --git a/src/lib/List/Manga/MangaListTemplate.svelte b/src/lib/List/Manga/MangaListTemplate.svelte
index 9931d9bf..2ead1303 100644
--- a/src/lib/List/Manga/MangaListTemplate.svelte
+++ b/src/lib/List/Manga/MangaListTemplate.svelte
@@ -4,6 +4,7 @@ import { onDestroy, onMount } from "svelte";
import { browser } from "$app/environment";
import { env } from "$env/dynamic/public";
import type { AniListAuthorisation } from "$lib/Data/AniList/identity";
+import { hydrateMediaListCache } from "$lib/Data/AniList/cacheHydration";
import { type Media, mediaListCollection, Type } from "$lib/Data/AniList/media";
import { database } from "$lib/Database/IDB/chapters";
import RateLimitedError from "$lib/Error/RateLimited.svelte";
@@ -75,6 +76,7 @@ const restartKeyCacher = (cacheMinutes: number) => {
};
onMount(async () => {
+ await hydrateMediaListCache("manga");
restartKeyCacher(Math.max($settings.cacheMangaMinutes, 5));
if (browser) {
diff --git a/src/lib/Utility/persistentStore.ts b/src/lib/Utility/persistentStore.ts
index 7df3049f..a3837a5b 100644
--- a/src/lib/Utility/persistentStore.ts
+++ b/src/lib/Utility/persistentStore.ts
@@ -5,14 +5,23 @@ export const persistentStore = <T>(key: string, initial: T): Writable<T> => {
const store = writable<T>(initial);
if (browser)
- import("localforage").then((localforage) => {
- localforage.default.getItem<T>(key).then((value) => {
- if (value !== null) store.set(value);
- });
+ import("localforage").then(async (localforage) => {
+ let hydrated = false;
+ let pendingValue = initial;
store.subscribe((value) => {
- localforage.default.setItem(key, value);
+ pendingValue = value;
+
+ if (hydrated) localforage.default.setItem(key, value);
});
+
+ const value = await localforage.default.getItem<T>(key);
+
+ if (value !== null) store.set(value);
+
+ hydrated = true;
+
+ await localforage.default.setItem(key, pendingValue);
});
return store;