From e9864f42b1a4fd9c6089f02b8fb8e91d06c9863d Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Thu, 18 Dec 2025 08:51:53 -0500 Subject: [PATCH] feat: use luxon for TTL --- src/controllers/watch-status/index.ts | 1 - src/libs/anilist/anilist-do.ts | 53 +++++++++++++++------------ 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/controllers/watch-status/index.ts b/src/controllers/watch-status/index.ts index 8c87f42..c1b7047 100644 --- a/src/controllers/watch-status/index.ts +++ b/src/controllers/watch-status/index.ts @@ -1,5 +1,4 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; -import type { HonoRequest } from "hono"; import { AnilistUpdateType } from "~/libs/anilist/updateType.ts"; import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; diff --git a/src/libs/anilist/anilist-do.ts b/src/libs/anilist/anilist-do.ts index aa55a6b..9ed1a06 100644 --- a/src/libs/anilist/anilist-do.ts +++ b/src/libs/anilist/anilist-do.ts @@ -1,6 +1,7 @@ import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { DurableObject } from "cloudflare:workers"; import { print } from "graphql"; +import { DateTime } from "luxon"; import { z } from "zod"; import { @@ -99,7 +100,7 @@ export class AnilistDurableObject extends DurableObject { }); return data?.Media; }, - 60 * 60 * 1000, + DateTime.now().plus({ hours: 1 }), ); } @@ -114,7 +115,7 @@ export class AnilistDurableObject extends DurableObject { }); return data?.Page; }, - 60 * 60 * 1000, + DateTime.now().plus({ hours: 1 }), ); } @@ -156,7 +157,7 @@ export class AnilistDurableObject extends DurableObject { limit, }); }, - 24 * 60 * 60 * 1000, + DateTime.now().plus({ days: 1 }), ); } @@ -169,15 +170,14 @@ export class AnilistDurableObject extends DurableObject { return this.handleCachedRequest( `popular:${JSON.stringify({ page, limit, season, seasonYear })}`, async () => { - const data = await this.fetchFromAnilist(GetPopularTitlesQuery, { + return this.fetchFromAnilist(GetPopularTitlesQuery, { page, limit, season, seasonYear, - }); - return data?.Page; + }).then((data) => data?.Page); }, - 24 * 60 * 60 * 1000, + DateTime.now().plus({ days: 1 }), ); } @@ -191,7 +191,7 @@ export class AnilistDurableObject extends DurableObject { }); return data?.Page; }, - 24 * 60 * 60 * 1000, + DateTime.now().plus({ days: 1 }), ); } @@ -210,7 +210,7 @@ export class AnilistDurableObject extends DurableObject { }); return data?.Page; }, - 24 * 60 * 60 * 1000, + DateTime.now().plus({ days: 1 }), ); } @@ -218,10 +218,10 @@ export class AnilistDurableObject extends DurableObject { return this.handleCachedRequest( `user:${token}`, async () => { - const data = await this.fetchFromAnilist(GetUserQuery, {}, token); + const data = await this.fetchFromAnilist(GetUserQuery, {}, { token }); return data?.Viewer; }, - 60 * 60 * 24 * 30 * 1000, + DateTime.now().plus({ days: 30 }), ); } @@ -232,11 +232,11 @@ export class AnilistDurableObject extends DurableObject { const data = await this.fetchFromAnilist( GetUserProfileQuery, { token }, - token, + { token }, ); return data?.Viewer; }, - 60 * 60 * 24 * 30 * 1000, + DateTime.now().plus({ days: 30 }), ); } @@ -248,7 +248,7 @@ export class AnilistDurableObject extends DurableObject { const data = await this.fetchFromAnilist( MarkEpisodeAsWatchedMutation, { titleId, episodeNumber }, - token, + { token }, ); return data?.SaveMediaListEntry; } @@ -257,7 +257,7 @@ export class AnilistDurableObject extends DurableObject { const data = await this.fetchFromAnilist( MarkTitleAsWatchedMutation, { titleId }, - token, + { token }, ); return data?.SaveMediaListEntry; } @@ -266,7 +266,7 @@ export class AnilistDurableObject extends DurableObject { async handleCachedRequest( key: string, fetcher: () => Promise, - ttl?: number | ((data: T) => number | undefined), + ttl?: DateTime | ((data: T) => DateTime | undefined), ) { const cache = await this.state.storage.get(key); console.debug(`Retrieving request ${key} from cache:`, cache != null); @@ -279,9 +279,8 @@ export class AnilistDurableObject extends DurableObject { console.debug(`Retrieved alarms from cache:`, Object.entries(alarms)); const calculatedTtl = typeof ttl === "function" ? ttl(result) : ttl; - - if (calculatedTtl && calculatedTtl > 0) { - const alarmTime = Date.now() + calculatedTtl; + if (calculatedTtl) { + const alarmTime = calculatedTtl.toMillis(); await this.state.storage.setAlarm(alarmTime); console.debug(`Deleting storage key ${storageKey} & alarm ${key}`); await this.state.storage.put(`alarm:${key}`, alarmTime); @@ -307,8 +306,11 @@ export class AnilistDurableObject extends DurableObject { async fetchFromAnilist( query: TypedDocumentNode, variables: Variables, - token?: string | undefined, - ): Promise { + { + token, + shouldRetryOnRateLimit = true, + }: { token?: string | undefined; shouldRetryOnRateLimit?: boolean } = {}, + ): Promise { const headers: any = { "Content-Type": "application/json", }; @@ -338,14 +340,17 @@ export class AnilistDurableObject extends DurableObject { }); // 1. Handle Rate Limiting (429) - if (response.status === 429) { + if (shouldRetryOnRateLimit && response.status === 429) { const retryAfter = await response - .json() + .json<{ headers: Record }>() .then(({ headers }) => new Headers(headers).get("Retry-After")); console.log("429, retrying in", retryAfter); await sleep(Number(retryAfter || 1) * 1000); // specific fallback or ensure logic - return this.fetchFromAnilist(query, variables, token); + return this.fetchFromAnilist(query, variables, { + token, + shouldRetryOnRateLimit: false, + }); } // 2. Handle HTTP Errors (like 404 or 500)