From 28283725e06857dad2fc2003016bdad0999ee110 Mon Sep 17 00:00:00 2001 From: ThaUnknown <6506529+ThaUnknown@users.noreply.github.com> Date: Sat, 26 Jul 2025 12:00:26 +0200 Subject: [PATCH] fix: load al cache before app --- package.json | 2 +- src/lib/components/ui/forums/Comment.svelte | 2 +- src/lib/components/ui/forums/Write.svelte | 2 +- src/lib/components/ui/irc/irc.svelte | 2 +- .../components/ui/sidebar/sidebarlist.svelte | 2 +- src/lib/modules/anilist/client.ts | 305 +---------------- src/lib/modules/anilist/urql-client.ts | 309 ++++++++++++++++++ src/lib/modules/auth/client.ts | 8 +- src/lib/modules/w2g/index.ts | 2 +- src/routes/app/+layout.ts | 5 +- .../anime/[id]/thread/[threadId]/+page.svelte | 2 +- src/routes/app/search/+page.svelte | 2 +- src/routes/app/settings/accounts/+page.svelte | 6 +- src/routes/app/settings/app/+page.svelte | 6 +- 14 files changed, 335 insertions(+), 320 deletions(-) diff --git a/package.json b/package.json index 4f46212..4accd91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ui", - "version": "6.4.88", + "version": "6.4.89", "license": "BUSL-1.1", "private": true, "packageManager": "pnpm@9.15.5", diff --git a/src/lib/components/ui/forums/Comment.svelte b/src/lib/components/ui/forums/Comment.svelte index ea5f947..1a9aed9 100644 --- a/src/lib/components/ui/forums/Comment.svelte +++ b/src/lib/components/ui/forums/Comment.svelte @@ -25,7 +25,7 @@ let childComments: Array> $: childComments = comment.childComments as Array> | null ?? [] - const viewer = client.viewer + const viewer = client.client.viewer
diff --git a/src/lib/components/ui/forums/Write.svelte b/src/lib/components/ui/forums/Write.svelte index e114882..47162b0 100644 --- a/src/lib/components/ui/forums/Write.svelte +++ b/src/lib/components/ui/forums/Write.svelte @@ -7,7 +7,7 @@ export let isLocked = false - const viewer = client.viewer + const viewer = client.client.viewer export let threadId: number | undefined = undefined export let parentCommentId: number | undefined = undefined diff --git a/src/lib/components/ui/irc/irc.svelte b/src/lib/components/ui/irc/irc.svelte index 337eebb..9031e80 100644 --- a/src/lib/components/ui/irc/irc.svelte +++ b/src/lib/components/ui/irc/irc.svelte @@ -10,7 +10,7 @@ diff --git a/src/lib/modules/anilist/client.ts b/src/lib/modules/anilist/client.ts index 35a11c1..cb99a74 100644 --- a/src/lib/modules/anilist/client.ts +++ b/src/lib/modules/anilist/client.ts @@ -1,37 +1,16 @@ -import { authExchange } from '@urql/exchange-auth' -import { offlineExchange } from '@urql/exchange-graphcache' -import { makeDefaultStorage } from '@urql/exchange-graphcache/default-storage' -import { Client, fetchExchange, queryStore, type OperationResultState, gql as _gql } from '@urql/svelte' -import Bottleneck from 'bottleneck' +import { queryStore, type OperationResultState, gql as _gql } from '@urql/svelte' import lavenshtein from 'js-levenshtein' -import { writable as _writable } from 'simple-store-svelte' import { derived, readable, writable, type Writable } from 'svelte/store' -import { toast } from 'svelte-sonner' -import gql from './gql' -import { CommentFrag, Comments, CustomLists, DeleteEntry, DeleteThreadComment, Entry, Following, FullMedia, FullMediaList, IDMedia, SaveThreadComment, Schedule, Search, ThreadFrag, Threads, ToggleFavourite, ToggleLike, UserLists, Viewer } from './queries' -import { refocusExchange } from './refocus' -import schema from './schema.json' with { type: 'json' } +import { Comments, DeleteEntry, DeleteThreadComment, Entry, Following, IDMedia, SaveThreadComment, Schedule, Search, Threads, ToggleFavourite, ToggleLike, UserLists } from './queries' +import urqlClient from './urql-client' import { currentSeason, currentYear, lastSeason, lastYear, nextSeason, nextYear } from './util' import type { Media } from './types' import type { ResultOf, VariablesOf } from 'gql.tada' import type { AnyVariables, OperationContext, RequestPolicy, TypedDocumentNode } from 'urql' -import { dev } from '$app/environment' -import native from '$lib/modules/native' -import { arrayEqual, safeLocalStorage, sleep } from '$lib/utils' - -class FetchError extends Error { - res - - constructor (res: Response, message?: string, opts?: ErrorOptions) { - super(message, opts) - this.res = res - } -} - -interface ViewerData { viewer: ResultOf['Viewer'], token: string, expires: string } +import { arrayEqual } from '$lib/utils' function getDistanceFromTitle (media: Media & {lavenshtein?: number}, name: string) { const titles = Object.values(media.title ?? {}).filter(v => v).map(title => lavenshtein(title?.toLowerCase() ?? '', name.toLowerCase())) @@ -43,286 +22,14 @@ function getDistanceFromTitle (media: Media & {lavenshtein?: number}, name: stri } class AnilistClient { - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - storagePromise = Promise.withResolvers() - storage = makeDefaultStorage({ - idbName: 'graphcache-v3', - onCacheHydrated: () => this.storagePromise.resolve(), - maxAge: 31 // The maximum age of the persisted data in days - }) - - client = new Client({ - url: 'https://graphql.anilist.co', - // fetch: dev ? fetch : (req: RequestInfo | URL, opts?: RequestInit) => this.handleRequest(req, opts), - fetch: (req: RequestInfo | URL, opts?: RequestInit) => this.handleRequest(req, opts), - exchanges: [ - refocusExchange(60_000), - offlineExchange({ - schema: schema as Parameters[0]['schema'], - storage: this.storage, - updates: { - Mutation: { - ToggleFavourite (result: ResultOf, args, cache) { - if (!result.ToggleFavourite?.anime?.nodes) return result - const id = args.animeId as number - - // we check if exists, because AL always returns false for isFavourite, so we need to check if it exists in the list - const exists = result.ToggleFavourite.anime.nodes.find(n => n?.id === id) - - cache.writeFragment(gql('fragment Med on Media {id, isFavourite}'), { id, isFavourite: !!exists }) - }, - DeleteMediaListEntry: (_, { id }, cache) => { - cache.writeFragment(FullMediaList, { id: id as number, progress: null, repeat: null, status: null, customLists: null, score: null }) - cache.updateQuery({ query: UserLists, variables: { id: this.viewer.value?.viewer?.id } }, data => { - if (!data?.MediaListCollection?.lists) return data - const oldLists = data.MediaListCollection.lists - - data.MediaListCollection.lists = oldLists.map(list => { - if (!list?.entries) return list - return { - ...list, - entries: list.entries.filter(entry => entry?.media?.mediaListEntry?.id !== id) - } - }) - - return data - }) - }, - SaveMediaListEntry: (result: ResultOf, { mediaId }, cache) => { - const media = gql('fragment Med on Media {id, mediaListEntry {status, progress, repeat, score, customLists }}') - - const entry = result.SaveMediaListEntry - - if (entry?.customLists) entry.customLists = (entry.customLists as string[]).map(name => ({ enabled: true, name })) - cache.writeFragment(media, { - id: mediaId as number, - mediaListEntry: entry ?? null - }) - cache.updateQuery({ query: UserLists, variables: { id: this.viewer.value?.viewer?.id } }, data => { - if (!data?.MediaListCollection?.lists) return data - const oldLists = data.MediaListCollection.lists - const oldEntry = oldLists.flatMap(list => list?.entries).find(entry => entry?.media?.id === mediaId) ?? { id: -1, media: cache.readFragment(FullMedia, { id: mediaId as number, __typename: 'Media' }) } - if (!oldEntry.media) return data - - const lists = oldLists.map(list => { - if (!list?.entries) return list - return { - ...list, - entries: list.entries.filter(entry => entry?.media?.id !== mediaId) - } - }) - - const status = result.SaveMediaListEntry?.status ?? oldEntry.media.mediaListEntry?.status ?? 'PLANNING' as const - - const fallback: NonNullable = { status, entries: [] } - let targetList = lists.find(list => list?.status === status) - if (!targetList) { - lists.push(fallback) - targetList = fallback - } - targetList.entries ??= [] - targetList.entries.push(oldEntry) - return { ...data, MediaListCollection: { ...data.MediaListCollection, lists } } - }) - }, - SaveThreadComment: (_result, args, cache, _info) => { - if (_info.variables.rootCommentId) { - const id = _info.variables.rootCommentId as number - cache.invalidate({ - __typename: 'ThreadComment', - id - }) - } else { - cache.invalidate('ThreadComment') - } - }, - DeleteThreadComment: (_result, args, cache, _info) => { - const id = (_info.variables.rootCommentId ?? args.id) as number - cache.invalidate({ - __typename: 'ThreadComment', - id - }) - } - } - }, - resolvers: { - Query: { - Media: (parent, { id }) => ({ __typename: 'Media', id }), - Thread: (parent, { id }) => ({ __typename: 'Thread', id }) - } - }, - optimistic: { - ToggleFavourite ({ animeId }, cache, info) { - const id = animeId as number - const media = cache.readFragment(FullMedia, { id, __typename: 'Media' }) - info.partial = true - - const nodes = media?.isFavourite ? [] : [{ id, __typename: 'Media' }] - return { - anime: { - nodes, - __typename: 'MediaConnection' - }, - __typename: 'Favourites' - } - }, - DeleteMediaListEntry () { - return { deleted: true, __typename: 'Deleted' } - }, - SaveMediaListEntry (args, cache, info) { - const id = args.mediaId as number - const media = cache.readFragment(FullMedia, { id, __typename: 'Media' }) - if (!media) return null - info.partial = true - - return { - status: 'PLANNING' as const, - progress: 0, - repeat: 0, - score: 0, - id: -1, - ...media.mediaListEntry, - customLists: (args.customLists as string[]).map(name => ({ enabled: true, name })), - ...args, - media, - __typename: 'MediaList' - } - }, - ToggleLikeV2 ({ id, type }, cache, info) { - const threadOrCommentId = id as number - const likable = type as 'THREAD' | 'THREAD_COMMENT' | 'ACTIVITY' | 'ACTIVITY_REPLY' - - const typename = likable === 'THREAD' ? 'Thread' : 'ThreadComment' - - const likableUnion = cache.readFragment(likable === 'THREAD' ? ThreadFrag : CommentFrag, { id: threadOrCommentId, __typename: typename }) - - if (!likableUnion) return null - - return { - id: threadOrCommentId, - isLiked: !likableUnion.isLiked, - likeCount: likableUnion.likeCount + (likableUnion.isLiked ? -1 : 1), - __typename: typename - } - } - }, - keys: { - FuzzyDate: () => null, - PageInfo: () => null, - Page: () => null, - MediaTitle: () => null, - MediaCoverImage: () => null, - AiringSchedule: () => null, - MediaListCollection: e => (e.user as {id: string | null}).id, - MediaListGroup: e => e.status as string | null, - UserAvatar: () => null, - UserOptions: () => null, - UserStatisticTypes: () => null, - UserGenreStatistic: () => null, - UserStatistics: () => null - } - }), - authExchange(async utils => { - return { - addAuthToOperation: (operation) => { - if (!this.viewer.value) return operation - return utils.appendHeaders(operation, { - Authorization: `Bearer ${this.viewer.value.token}` - }) - }, - didAuthError (error, _operation) { - return error.graphQLErrors.some(e => e.message === 'Invalid token') - }, - refreshAuth: async () => { - const oauth = this.token() - this.auth(oauth) // TODO: this should be awaited, but it utils doesnt expose query, only mutation, so need to wait for it to be added - await oauth - }, - willAuthError: () => { - if (!this.viewer.value?.expires) return false - return parseInt(this.viewer.value.expires) < Date.now() - } - } - }), - fetchExchange - ], - requestPolicy: 'cache-and-network' - }) - - limiter = new Bottleneck({ - reservoir: 90, - reservoirRefreshAmount: 90, - reservoirRefreshInterval: 60 * 1000, - maxConcurrent: 3, - minTime: 200 - }) - - rateLimitPromise: Promise | null = null - - handleRequest = this.limiter.wrap(async (req: RequestInfo | URL, opts?: RequestInit) => { - await this.rateLimitPromise - // await sleep(1000) - const res = await fetch(req, opts) - if (!res.ok && (res.status === 429 || res.status === 500)) { - throw new FetchError(res) - } - return res - }) - - async token () { - const res = await native.authAL(`https://anilist.co/api/v2/oauth/authorize?client_id=${dev ? 26159 : 3461}&response_type=token`) - const token = res.access_token - const expires = '' + (Date.now() + (parseInt(res.expires_in) * 1000)) - this.viewer.value = { viewer: this.viewer.value?.viewer ?? null, token, expires } - return { token, expires } - } - - async auth (oauth = this.token()) { - const { token, expires } = await oauth - const viewerRes = await this.client.query(Viewer, {}, { fetchOptions: { headers: { Authorization: `Bearer ${token}` } } }) - if (!viewerRes.data?.Viewer) throw new Error('Failed to fetch viewer data') - - this.viewer.value = { viewer: viewerRes.data.Viewer, token, expires } - localStorage.setItem('ALViewer', JSON.stringify(this.viewer.value)) - - const lists = viewerRes.data.Viewer.mediaListOptions?.animeList?.customLists ?? [] - if (!lists.includes('Watched using Hayase')) { - await this.client.mutation(CustomLists, { lists: [...lists, 'Watched using Hayase'] }) - } - } - - async logout () { - await this.storage.clear() - localStorage.removeItem('ALViewer') - native.restart() - } - - setRateLimit (sec: number) { - toast.error('Anilist Error', { description: 'Rate limit exceeded, retrying in ' + Math.round(sec / 1000) + ' seconds.' }) - this.rateLimitPromise ??= sleep(sec).then(() => { this.rateLimitPromise = null }) - return sec - } - + client: typeof urqlClient = urqlClient constructor () { - this.limiter.on('failed', async (error: FetchError | Error, jobInfo) => { - // urql has some weird bug that first error is always an AbortError ??? - if (error.name === 'AbortError') return undefined - if (jobInfo.retryCount > 8) return undefined - - if (error.message === 'Failed to fetch') return this.setRateLimit(60000) - if (!(error instanceof FetchError)) return 0 - if (error.res.status === 500) return 1000 - - return this.setRateLimit((parseInt(error.res.headers.get('retry-after') ?? '60') + 1) * 1000) - }) // hacky but prevents query from re-running this.userlists.subscribe(() => undefined) this.continueIDs.subscribe(() => undefined) } - viewer = _writable(safeLocalStorage('ALViewer')) - - userlists = derived>>(this.viewer, (store, set) => { + userlists = derived>>(this.client.viewer, (store, set) => { return queryStore({ client: this.client, query: UserLists, variables: { id: store?.viewer?.id } }).subscribe(set) }) diff --git a/src/lib/modules/anilist/urql-client.ts b/src/lib/modules/anilist/urql-client.ts index e69de29..6d2f839 100644 --- a/src/lib/modules/anilist/urql-client.ts +++ b/src/lib/modules/anilist/urql-client.ts @@ -0,0 +1,309 @@ +import { authExchange } from '@urql/exchange-auth' +import { offlineExchange } from '@urql/exchange-graphcache' +import { makeDefaultStorage } from '@urql/exchange-graphcache/default-storage' +import { Client, fetchExchange } from '@urql/svelte' +import Bottleneck from 'bottleneck' +import { writable as _writable } from 'simple-store-svelte' +import { toast } from 'svelte-sonner' + +import gql from './gql' +import { CommentFrag, CustomLists, type Entry, FullMedia, FullMediaList, ThreadFrag, type ToggleFavourite, UserLists, Viewer } from './queries' +import { refocusExchange } from './refocus' +import schema from './schema.json' with { type: 'json' } + +import type { ResultOf } from 'gql.tada' + +import { dev } from '$app/environment' +import native from '$lib/modules/native' +import { safeLocalStorage, sleep } from '$lib/utils' + +interface ViewerData { viewer: ResultOf['Viewer'], token: string, expires: string } + +class FetchError extends Error { + res + + constructor (res: Response, message?: string, opts?: ErrorOptions) { + super(message, opts) + this.res = res + } +} + +// eslint-disable-next-line @typescript-eslint/no-invalid-void-type +export const storagePromise = Promise.withResolvers() +export const storage = makeDefaultStorage({ + idbName: 'graphcache-v3', + onCacheHydrated: () => storagePromise.resolve(), + maxAge: 31 // The maximum age of the persisted data in days +}) + +export default new class URQLClient extends Client { + limiter = new Bottleneck({ + reservoir: 90, + reservoirRefreshAmount: 90, + reservoirRefreshInterval: 60 * 1000, + maxConcurrent: 3, + minTime: 200 + }) + + rateLimitPromise: Promise | null = null + + handleRequest = this.limiter.wrap(async (req: RequestInfo | URL, opts?: RequestInit) => { + await this.rateLimitPromise + // await sleep(1000) + const res = await fetch(req, opts) + if (!res.ok && (res.status === 429 || res.status === 500)) { + throw new FetchError(res) + } + return res + }) + + async token () { + const res = await native.authAL(`https://anilist.co/api/v2/oauth/authorize?client_id=${dev ? 26159 : 3461}&response_type=token`) + const token = res.access_token + const expires = '' + (Date.now() + (parseInt(res.expires_in) * 1000)) + this.viewer.value = { viewer: this.viewer.value?.viewer ?? null, token, expires } + return { token, expires } + } + + async auth (oauth = this.token()) { + const { token, expires } = await oauth + const viewerRes = await this.query(Viewer, {}, { fetchOptions: { headers: { Authorization: `Bearer ${token}` } } }) + if (!viewerRes.data?.Viewer) throw new Error('Failed to fetch viewer data') + + this.viewer.value = { viewer: viewerRes.data.Viewer, token, expires } + localStorage.setItem('ALViewer', JSON.stringify(this.viewer.value)) + + const lists = viewerRes.data.Viewer.mediaListOptions?.animeList?.customLists ?? [] + if (!lists.includes('Watched using Hayase')) { + await this.mutation(CustomLists, { lists: [...lists, 'Watched using Hayase'] }) + } + } + + async logout () { + await storage.clear() + localStorage.removeItem('ALViewer') + native.restart() + } + + setRateLimit (sec: number) { + toast.error('Anilist Error', { description: 'Rate limit exceeded, retrying in ' + Math.round(sec / 1000) + ' seconds.' }) + this.rateLimitPromise ??= sleep(sec).then(() => { this.rateLimitPromise = null }) + return sec + } + + viewer = _writable(safeLocalStorage('ALViewer')) + + constructor () { + super({ + url: 'https://graphql.anilist.co', + // fetch: dev ? fetch : (req: RequestInfo | URL, opts?: RequestInit) => this.handleRequest(req, opts), + fetch: (req: RequestInfo | URL, opts?: RequestInit) => this.handleRequest(req, opts), + exchanges: [ + refocusExchange(60_000), + offlineExchange({ + schema, + storage, + updates: { + Mutation: { + ToggleFavourite (result: ResultOf, args, cache) { + if (!result.ToggleFavourite?.anime?.nodes) return result + const id = args.animeId as number + + // we check if exists, because AL always returns false for isFavourite, so we need to check if it exists in the list + const exists = result.ToggleFavourite.anime.nodes.find(n => n?.id === id) + + cache.writeFragment(gql('fragment Med on Media {id, isFavourite}'), { id, isFavourite: !!exists }) + }, + DeleteMediaListEntry: (_, { id }, cache) => { + cache.writeFragment(FullMediaList, { id: id as number, progress: null, repeat: null, status: null, customLists: null, score: null }) + cache.updateQuery({ query: UserLists, variables: { id: this.viewer.value?.viewer?.id } }, data => { + if (!data?.MediaListCollection?.lists) return data + const oldLists = data.MediaListCollection.lists + + data.MediaListCollection.lists = oldLists.map(list => { + if (!list?.entries) return list + return { + ...list, + entries: list.entries.filter(entry => entry?.media?.mediaListEntry?.id !== id) + } + }) + + return data + }) + }, + SaveMediaListEntry: (result: ResultOf, { mediaId }, cache) => { + const media = gql('fragment Med on Media {id, mediaListEntry {status, progress, repeat, score, customLists }}') + + const entry = result.SaveMediaListEntry + + if (entry?.customLists) entry.customLists = (entry.customLists as string[]).map(name => ({ enabled: true, name })) + cache.writeFragment(media, { + id: mediaId as number, + mediaListEntry: entry ?? null + }) + cache.updateQuery({ query: UserLists, variables: { id: this.viewer.value?.viewer?.id } }, data => { + if (!data?.MediaListCollection?.lists) return data + const oldLists = data.MediaListCollection.lists + const oldEntry = oldLists.flatMap(list => list?.entries).find(entry => entry?.media?.id === mediaId) ?? { id: -1, media: cache.readFragment(FullMedia, { id: mediaId as number, __typename: 'Media' }) } + if (!oldEntry.media) return data + + const lists = oldLists.map(list => { + if (!list?.entries) return list + return { + ...list, + entries: list.entries.filter(entry => entry?.media?.id !== mediaId) + } + }) + + const status = result.SaveMediaListEntry?.status ?? oldEntry.media.mediaListEntry?.status ?? 'PLANNING' as const + + const fallback: NonNullable = { status, entries: [] } + let targetList = lists.find(list => list?.status === status) + if (!targetList) { + lists.push(fallback) + targetList = fallback + } + targetList.entries ??= [] + targetList.entries.push(oldEntry) + return { ...data, MediaListCollection: { ...data.MediaListCollection, lists } } + }) + }, + SaveThreadComment: (_result, args, cache, _info) => { + if (_info.variables.rootCommentId) { + const id = _info.variables.rootCommentId as number + cache.invalidate({ + __typename: 'ThreadComment', + id + }) + } else { + cache.invalidate('ThreadComment') + } + }, + DeleteThreadComment: (_result, args, cache, _info) => { + const id = (_info.variables.rootCommentId ?? args.id) as number + cache.invalidate({ + __typename: 'ThreadComment', + id + }) + } + } + }, + resolvers: { + Query: { + Media: (parent, { id }) => ({ __typename: 'Media', id }), + Thread: (parent, { id }) => ({ __typename: 'Thread', id }) + } + }, + optimistic: { + ToggleFavourite ({ animeId }, cache, info) { + const id = animeId as number + const media = cache.readFragment(FullMedia, { id, __typename: 'Media' }) + info.partial = true + + const nodes = media?.isFavourite ? [] : [{ id, __typename: 'Media' }] + return { + anime: { + nodes, + __typename: 'MediaConnection' + }, + __typename: 'Favourites' + } + }, + DeleteMediaListEntry () { + return { deleted: true, __typename: 'Deleted' } + }, + SaveMediaListEntry (args, cache, info) { + const id = args.mediaId as number + const media = cache.readFragment(FullMedia, { id, __typename: 'Media' }) + if (!media) return null + info.partial = true + + return { + status: 'PLANNING' as const, + progress: 0, + repeat: 0, + score: 0, + id: -1, + ...media.mediaListEntry, + customLists: (args.customLists as string[]).map(name => ({ enabled: true, name })), + ...args, + media, + __typename: 'MediaList' + } + }, + ToggleLikeV2 ({ id, type }, cache, info) { + const threadOrCommentId = id as number + const likable = type as 'THREAD' | 'THREAD_COMMENT' | 'ACTIVITY' | 'ACTIVITY_REPLY' + + const typename = likable === 'THREAD' ? 'Thread' : 'ThreadComment' + + const likableUnion = cache.readFragment(likable === 'THREAD' ? ThreadFrag : CommentFrag, { id: threadOrCommentId, __typename: typename }) + + if (!likableUnion) return null + + return { + id: threadOrCommentId, + isLiked: !likableUnion.isLiked, + likeCount: likableUnion.likeCount + (likableUnion.isLiked ? -1 : 1), + __typename: typename + } + } + }, + keys: { + FuzzyDate: () => null, + PageInfo: () => null, + Page: () => null, + MediaTitle: () => null, + MediaCoverImage: () => null, + AiringSchedule: () => null, + MediaListCollection: e => (e.user as {id: string | null}).id, + MediaListGroup: e => e.status as string | null, + UserAvatar: () => null, + UserOptions: () => null, + UserStatisticTypes: () => null, + UserGenreStatistic: () => null, + UserStatistics: () => null, + MediaListOptions: () => null, + MediaListTypeOptions: () => null + } + }), + authExchange(async utils => { + return { + addAuthToOperation: (operation) => { + if (!this.viewer.value) return operation + return utils.appendHeaders(operation, { + Authorization: `Bearer ${this.viewer.value.token}` + }) + }, + didAuthError (error, _operation) { + return error.graphQLErrors.some(e => e.message === 'Invalid token') + }, + refreshAuth: async () => { + const oauth = this.token() + this.auth(oauth) // TODO: this should be awaited, but it utils doesnt expose query, only mutation, so need to wait for it to be added + await oauth + }, + willAuthError: () => { + if (!this.viewer.value?.expires) return false + return parseInt(this.viewer.value.expires) < Date.now() + } + } + }), + fetchExchange + ], + requestPolicy: 'cache-and-network' + }) + + this.limiter.on('failed', async (error: FetchError | Error, jobInfo) => { + // urql has some weird bug that first error is always an AbortError ??? + if (error.name === 'AbortError') return undefined + if (jobInfo.retryCount > 8) return undefined + + if (error.message === 'Failed to fetch') return this.setRateLimit(60000) + if (!(error instanceof FetchError)) return 0 + if (error.res.status === 500) return 1000 + + return this.setRateLimit((parseInt(error.res.headers.get('retry-after') ?? '60') + 1) * 1000) + }) + } +}() diff --git a/src/lib/modules/auth/client.ts b/src/lib/modules/auth/client.ts index 0ae76b1..34098fd 100644 --- a/src/lib/modules/auth/client.ts +++ b/src/lib/modules/auth/client.ts @@ -15,7 +15,7 @@ export default new class AuthAggregator { hasAuth = readable(this.checkAuth(), set => { // add other subscriptions here for MAL, kitsu, tvdb, etc const unsub = [ - client.viewer.subscribe(() => set(this.checkAuth())), + client.client.viewer.subscribe(() => set(this.checkAuth())), kitsu.viewer.subscribe(() => set(this.checkAuth())), mal.viewer.subscribe(() => set(this.checkAuth())) ] @@ -27,7 +27,7 @@ export default new class AuthAggregator { // AUTH anilist () { - return !!client.viewer.value?.viewer?.id + return !!client.client.viewer.value?.viewer?.id } kitsu () { @@ -43,14 +43,14 @@ export default new class AuthAggregator { } id () { - if (this.anilist()) return client.viewer.value!.viewer?.id + if (this.anilist()) return client.client.viewer.value!.viewer?.id if (this.kitsu()) return kitsu.id() return -1 } profile (): ResultOf | undefined { - if (this.anilist()) return client.viewer.value?.viewer ?? undefined + if (this.anilist()) return client.client.viewer.value?.viewer ?? undefined if (this.kitsu()) return kitsu.profile() if (this.mal()) return mal.profile() } diff --git a/src/lib/modules/w2g/index.ts b/src/lib/modules/w2g/index.ts index 2616dac..ab93018 100644 --- a/src/lib/modules/w2g/index.ts +++ b/src/lib/modules/w2g/index.ts @@ -53,7 +53,7 @@ export class W2GClient extends EventEmitter<{index: [number], player: [PlayerSta messages = writable([]) - self: ChatUser = client.viewer.value?.viewer ?? { id: generateRandomHexCode(16), avatar: null, mediaListOptions: null, name: 'Guest' } + self: ChatUser = client.client.viewer.value?.viewer ?? { id: generateRandomHexCode(16), avatar: null, mediaListOptions: null, name: 'Guest' } peers = writable({ [this.self.id]: { user: this.self } }) get inviteLink () { diff --git a/src/routes/app/+layout.ts b/src/routes/app/+layout.ts index 3d038cb..2c0941c 100644 --- a/src/routes/app/+layout.ts +++ b/src/routes/app/+layout.ts @@ -2,7 +2,7 @@ import { error, redirect } from '@sveltejs/kit' import { dev } from '$app/environment' import { SETUP_VERSION } from '$lib' -import { client } from '$lib/modules/anilist' +import { storagePromise } from '$lib/modules/anilist/urql-client' import native from '$lib/modules/native' import { outdatedComponent } from '$lib/modules/update' @@ -14,6 +14,5 @@ export async function load () { // hydrating the cache re-starts all queries, it's better to wait for cache to hydrate, than waste rate limit on requests which are dumped anyways // this was previously in anilist/client but it was a top level await, which isn't a great solution, this *should* be better? - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - await client.storagePromise?.promise + await storagePromise.promise } diff --git a/src/routes/app/anime/[id]/thread/[threadId]/+page.svelte b/src/routes/app/anime/[id]/thread/[threadId]/+page.svelte index a1da2f6..5052688 100644 --- a/src/routes/app/anime/[id]/thread/[threadId]/+page.svelte +++ b/src/routes/app/anime/[id]/thread/[threadId]/+page.svelte @@ -23,7 +23,7 @@ $: anime = data.anime $: media = $anime.Media! - const viewer = client.viewer + const viewer = client.client.viewer
diff --git a/src/routes/app/search/+page.svelte b/src/routes/app/search/+page.svelte index cbc61ed..f40f22c 100644 --- a/src/routes/app/search/+page.svelte +++ b/src/routes/app/search/+page.svelte @@ -229,7 +229,7 @@ } } - const viewer = client.viewer + const viewer = client.client.viewer let pressed = false diff --git a/src/routes/app/settings/accounts/+page.svelte b/src/routes/app/settings/accounts/+page.svelte index 6a41148..bf98be9 100644 --- a/src/routes/app/settings/accounts/+page.svelte +++ b/src/routes/app/settings/accounts/+page.svelte @@ -20,7 +20,7 @@ import native from '$lib/modules/native' import { click } from '$lib/modules/navigate' - const alviewer = client.viewer + const alviewer = client.client.viewer $: anilist = $alviewer @@ -64,9 +64,9 @@
{#if anilist?.viewer?.id} - + {:else} - + {/if}
diff --git a/src/routes/app/settings/app/+page.svelte b/src/routes/app/settings/app/+page.svelte index 3f31eef..6a34669 100644 --- a/src/routes/app/settings/app/+page.svelte +++ b/src/routes/app/settings/app/+page.svelte @@ -5,7 +5,7 @@ import { Button } from '$lib/components/ui/button' import { SingleCombo } from '$lib/components/ui/combobox' import { Switch } from '$lib/components/ui/switch' - import { client } from '$lib/modules/anilist' + import { storage } from '$lib/modules/anilist/urql-client' import native from '$lib/modules/native' import { settings, SUPPORTS, debug } from '$lib/modules/settings' @@ -61,9 +61,9 @@ duration: 5000 }) } - function reset () { + async function reset () { localStorage.clear() - client.storage.clear() + await storage.clear() native.restart() }