166 lines
4.8 KiB
TypeScript
166 lines
4.8 KiB
TypeScript
import { swaggerUI } from "@hono/swagger-ui";
|
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
import { Duration, type DurationLike } from "luxon";
|
|
|
|
import { onNewEpisode } from "~/controllers/internal/new-episode";
|
|
import { AnilistUpdateType } from "~/libs/anilist/updateType";
|
|
import { calculateExponentialBackoff } from "~/libs/calculateExponentialBackoff";
|
|
import type { QueueName } from "~/libs/tasks/queueName.ts";
|
|
import {
|
|
MAX_QUEUE_DELAY_SECONDS,
|
|
type QueueBody,
|
|
} from "~/libs/tasks/queueTask";
|
|
import { maybeUpdateLastConnectedAt } from "~/middleware/maybeUpdateLastConnectedAt";
|
|
|
|
export const app = new OpenAPIHono<{ Bindings: Env }>();
|
|
|
|
app.use(maybeUpdateLastConnectedAt);
|
|
|
|
app.route(
|
|
"/",
|
|
await import("~/controllers/health-check").then(
|
|
(controller) => controller.default,
|
|
),
|
|
);
|
|
app.route(
|
|
"/title",
|
|
await import("~/controllers/title").then((controller) => controller.default),
|
|
);
|
|
app.route(
|
|
"/episodes",
|
|
await import("~/controllers/episodes").then(
|
|
(controller) => controller.default,
|
|
),
|
|
);
|
|
app.route(
|
|
"/search",
|
|
await import("~/controllers/search").then((controller) => controller.default),
|
|
);
|
|
app.route(
|
|
"/watch-status",
|
|
await import("~/controllers/watch-status").then(
|
|
(controller) => controller.default,
|
|
),
|
|
);
|
|
app.route(
|
|
"/token",
|
|
await import("~/controllers/token").then((controller) => controller.default),
|
|
);
|
|
app.route(
|
|
"/auth",
|
|
await import("~/controllers/auth").then((controller) => controller.default),
|
|
);
|
|
app.route(
|
|
"/popular",
|
|
await import("~/controllers/popular").then(
|
|
(controller) => controller.default,
|
|
),
|
|
);
|
|
app.route(
|
|
"/internal",
|
|
await import("~/controllers/internal").then(
|
|
(controller) => controller.default,
|
|
),
|
|
);
|
|
|
|
// The OpenAPI documentation will be available at /doc
|
|
app.doc("/openapi.json", {
|
|
openapi: "3.0.0",
|
|
info: {
|
|
version: "1.0.0",
|
|
title: "Aniplay API",
|
|
},
|
|
});
|
|
|
|
app.get("/docs", swaggerUI({ url: "/openapi.json" }));
|
|
|
|
export default {
|
|
fetch: app.fetch,
|
|
async queue(batch) {
|
|
onMessageQueue(batch, async (message, queueName) => {
|
|
switch (queueName) {
|
|
case "ANILIST_UPDATES":
|
|
const anilistUpdateBody =
|
|
message.body as QueueBody["ANILIST_UPDATES"];
|
|
switch (anilistUpdateBody.updateType) {
|
|
case AnilistUpdateType.UpdateWatchStatus:
|
|
if (!anilistUpdateBody[AnilistUpdateType.UpdateWatchStatus]) {
|
|
console.error(
|
|
`Discarding update, unknown body ${JSON.stringify(message.body)}`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const { updateWatchStatusOnAnilist } =
|
|
await import("~/controllers/watch-status/anilist");
|
|
const payload =
|
|
anilistUpdateBody[AnilistUpdateType.UpdateWatchStatus];
|
|
await updateWatchStatusOnAnilist(
|
|
payload.titleId,
|
|
payload.watchStatus,
|
|
payload.aniListToken,
|
|
);
|
|
break;
|
|
default:
|
|
throw new Error(
|
|
`Unhandled update type: ${anilistUpdateBody.updateType}`,
|
|
);
|
|
}
|
|
break;
|
|
case "NEW_EPISODE":
|
|
const newEpisodeBody = message.body as QueueBody["NEW_EPISODE"];
|
|
await onNewEpisode(
|
|
newEpisodeBody.aniListId,
|
|
newEpisodeBody.episodeNumber,
|
|
);
|
|
break;
|
|
default:
|
|
throw new Error(`Unhandled queue name: ${queueName}`);
|
|
}
|
|
});
|
|
},
|
|
async scheduled(event, env, ctx) {
|
|
const { processDelayedTasks } =
|
|
await import("~/libs/tasks/processDelayedTasks");
|
|
await processDelayedTasks(env);
|
|
},
|
|
} satisfies ExportedHandler<Env>;
|
|
|
|
const retryDelayConfig: Partial<
|
|
Record<QueueName, { min: DurationLike; max: DurationLike }>
|
|
> = {
|
|
NEW_EPISODE: {
|
|
min: Duration.fromObject({ hours: 1 }),
|
|
max: Duration.fromObject({ hours: 12 }),
|
|
},
|
|
};
|
|
|
|
function onMessageQueue<QN extends QueueName>(
|
|
messageBatch: MessageBatch<unknown>,
|
|
callback: (message: Message<QueueBody[QN]>, queueName: QN) => void,
|
|
) {
|
|
for (const message of messageBatch.messages) {
|
|
try {
|
|
callback(message as Message<QueueBody[QN]>, messageBatch.queue as QN);
|
|
message.ack();
|
|
} catch (error) {
|
|
console.error(
|
|
`Failed to process message ${message.id} for queue ${messageBatch.queue} with body ${JSON.stringify(message.body)}`,
|
|
);
|
|
console.error(error);
|
|
message.retry({
|
|
delaySeconds: Math.min(
|
|
calculateExponentialBackoff({
|
|
attempt: message.attempts,
|
|
baseMin: retryDelayConfig[messageBatch.queue as QN]?.min,
|
|
absCap: retryDelayConfig[messageBatch.queue as QN]?.max,
|
|
}),
|
|
MAX_QUEUE_DELAY_SECONDS,
|
|
),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export { AnilistDurableObject as AnilistDo } from "~/libs/anilist/anilist-do.ts";
|