refactor: decouple Anilist watch status updates from API endpoint to an asynchronous queue worker.

This commit is contained in:
2025-12-16 08:28:33 -05:00
parent 1501aff3b6
commit 80a6f67ead
5 changed files with 43 additions and 80 deletions

View File

@@ -30,19 +30,11 @@ const DeleteMediaListEntryMutation = graphql(`
}
`);
/** Updates the watch status for a title on Anilist. If the token is null, the watch status will not be updated.
*
* @returns true if the watch status was updated or if the token was null, false if it was not
*/
export async function maybeUpdateWatchStatusOnAnilist(
export async function updateWatchStatusOnAnilist(
titleId: number,
watchStatus: WatchStatus | null,
aniListToken: string | undefined,
aniListToken: string,
) {
if (!aniListToken) {
return true;
}
const client = new GraphQLClient("https://graphql.anilist.co/");
const headers = new Headers({ Authorization: `Bearer ${aniListToken}` });

View File

@@ -22,8 +22,6 @@ vi.mock("~/mocks", () => ({
describe("requests the /watch-status route", () => {
const db = getTestDb(env);
let app: typeof import("../../../src/index").app;
let maybeUpdateWatchStatusOnAnilist: any;
let queueTask: any;
let maybeScheduleNextAiringEpisode: any;
let removeTask: any;
@@ -31,10 +29,6 @@ describe("requests the /watch-status route", () => {
await resetTestDb(db);
vi.resetModules();
vi.doMock("./anilist", () => ({
maybeUpdateWatchStatusOnAnilist: vi.fn().mockResolvedValue(undefined),
}));
vi.doMock("~/libs/tasks/queueTask", () => ({
queueTask: vi.fn().mockResolvedValue(undefined),
}));
@@ -52,10 +46,6 @@ describe("requests the /watch-status route", () => {
}));
app = (await import("~/index")).app;
maybeUpdateWatchStatusOnAnilist = (
await import("~/controllers/watch-status/anilist")
).maybeUpdateWatchStatusOnAnilist;
queueTask = (await import("~/libs/tasks/queueTask")).queueTask;
removeTask = (await import("~/libs/tasks/removeTask")).removeTask;
maybeScheduleNextAiringEpisode = (
await import("~/libs/maybeScheduleNextAiringEpisode")
@@ -119,34 +109,6 @@ describe("requests the /watch-status route", () => {
expect(res.status).toBe(500);
});
it("saving title, Anilist request fails, should succeed", async () => {
vi.mocked(maybeUpdateWatchStatusOnAnilist).mockRejectedValue(
new Error("Anilist failed"),
);
const res = await app.request(
"/watch-status",
{
method: "POST",
headers: new Headers({
"x-anilist-token": "asd",
"Content-Type": "application/json",
}),
body: JSON.stringify({
deviceId: "123",
watchStatus: "CURRENT",
titleId: -1,
}),
},
env,
);
await expect(res.json()).resolves.toEqual({ success: true });
expect(res.status).toBe(200);
// Should queue task if direct update fails
expect(queueTask).toHaveBeenCalled();
});
it("watch status is null, should succeed", async () => {
const res = await app.request(
"/watch-status",

View File

@@ -16,8 +16,6 @@ import {
} from "~/types/schema";
import { WatchStatus } from "~/types/title/watchStatus";
import { maybeUpdateWatchStatusOnAnilist } from "./anilist";
const app = new OpenAPIHono<Cloudflare.Env>();
const UpdateWatchStatusRequest = z.object({
@@ -109,19 +107,6 @@ app.openapi(route, async (c) => {
}
}
try {
await maybeUpdateWatchStatusOnAnilist(
Number(titleId),
watchStatus,
aniListToken,
);
} catch (error) {
console.error("Failed to update watch status on Anilist");
console.error(error);
if (isRetrying) {
return c.json(ErrorResponse, { status: 500 });
}
await queueTask(
"ANILIST_UPDATES",
{
@@ -132,7 +117,6 @@ app.openapi(route, async (c) => {
},
{ req: c.req, scheduleConfig: { delay: { minute: 1 } } },
);
}
return c.json(SuccessResponse, { status: 200 });
});

View File

@@ -5,6 +5,7 @@ import { maybeUpdateLastConnectedAt } from "~/controllers/maybeUpdateLastConnect
import type { QueueName } from "~/libs/tasks/queueName.ts";
import { onNewEpisode } from "./controllers/internal/new-episode";
import { AnilistUpdateType } from "./libs/anilist/updateType";
import type { QueueBody } from "./libs/tasks/queueTask";
export const app = new OpenAPIHono<{ Bindings: Env }>();
@@ -74,7 +75,30 @@ export default {
async queue(batch) {
switch (batch.queue as QueueName) {
case "ANILIST_UPDATES":
batch.retryAll();
for (const message of (
batch as MessageBatch<QueueBody["ANILIST_UPDATES"]>
).messages) {
switch (message.body.updateType) {
case AnilistUpdateType.UpdateWatchStatus:
if (!message.body[AnilistUpdateType.UpdateWatchStatus]) {
throw new Error(
`Discarding update, unknown body ${JSON.stringify(message.body)}`,
);
}
const { updateWatchStatusOnAnilist } =
await import("~/controllers/watch-status/anilist");
const payload = message.body[AnilistUpdateType.UpdateWatchStatus];
await updateWatchStatusOnAnilist(
payload.titleId,
payload.watchStatus,
payload.aniListToken,
);
break;
}
message.ack();
}
break;
case "NEW_EPISODE":
for (const message of (batch as MessageBatch<QueueBody["NEW_EPISODE"]>)
@@ -89,9 +113,8 @@ export default {
}
},
async scheduled(event, env, ctx) {
const { processDelayedTasks } = await import(
"~/libs/tasks/processDelayedTasks"
);
const { processDelayedTasks } =
await import("~/libs/tasks/processDelayedTasks");
await processDelayedTasks(env, ctx);
},
} satisfies ExportedHandler<Env>;

View File

@@ -9,9 +9,11 @@ import type { QueueName } from "./queueName";
export type QueueBody = {
ANILIST_UPDATES: {
deviceId: string;
watchStatus: WatchStatus | null;
[AnilistUpdateType.UpdateWatchStatus]: {
titleId: number;
watchStatus: WatchStatus | null;
aniListToken: string;
};
updateType: AnilistUpdateType;
};
NEW_EPISODE: { aniListId: number; episodeNumber: number };