diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/Data/AniList/cacheHydration.ts | 48 | ||||
| -rw-r--r-- | src/lib/Data/AniList/media.ts | 114 | ||||
| -rw-r--r-- | src/lib/List/Anime/CompletedAnimeList.svelte | 3 | ||||
| -rw-r--r-- | src/lib/List/Anime/DueAnimeList.svelte | 2 | ||||
| -rw-r--r-- | src/lib/List/Anime/UpcomingAnimeList.svelte | 3 | ||||
| -rw-r--r-- | src/lib/List/Manga/MangaListTemplate.svelte | 2 | ||||
| -rw-r--r-- | src/lib/Utility/persistentStore.ts | 19 | ||||
| -rw-r--r-- | src/stores/lastPruneTimes.ts | 27 |
8 files changed, 153 insertions, 65 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; diff --git a/src/stores/lastPruneTimes.ts b/src/stores/lastPruneTimes.ts index 4741201e..2a770538 100644 --- a/src/stores/lastPruneTimes.ts +++ b/src/stores/lastPruneTimes.ts @@ -17,22 +17,29 @@ const defaultTimes: LastPruneTimes = { const createStore = () => { const store = writable<LastPruneTimes>(defaultTimes); let state: LastPruneTimes = defaultTimes; - - if (browser) - localforage.getItem<LastPruneTimes>("lastPruneTimes").then((value) => { - if ( - value && - Object.keys(value).length === Object.keys(defaultTimes).length - ) - store.set(value); - }); + let hydrated = !browser; store.subscribe((value) => { state = value; - if (browser) localforage.setItem("lastPruneTimes", value); + if (browser && hydrated) localforage.setItem("lastPruneTimes", value); }); + if (browser) + localforage + .getItem<LastPruneTimes>("lastPruneTimes") + .then(async (value) => { + if ( + value && + Object.keys(value).length === Object.keys(defaultTimes).length + ) + store.set(value); + + hydrated = true; + + await localforage.setItem("lastPruneTimes", state); + }); + return { subscribe: store.subscribe, set: store.set, |