From c04a2f8d1109efee53ea615da1e2706c078be6d5 Mon Sep 17 00:00:00 2001 From: bq Date: Sat, 18 Jul 2026 08:56:08 +0200 Subject: [PATCH 1/5] add back temporal polyfill(-lite) --- package.json | 1 + pnpm-lock.yaml | 8 ++++++++ src/index.ts | 4 ++++ tsconfig.json | 6 +++++- vitest.config.ts | 3 ++- wrangler.json | 2 +- 6 files changed, 21 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 43a7bf25..b380fbeb 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "p-queue": "9.1.0", "prettier": "3.9.5", "simple-git-hooks": "2.13.1", + "temporal-polyfill-lite": "0.4.2", "tsdown": "0.22.9", "typescript": "6.0.3", "ultrahtml": "1.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f314bf5..30687356 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,6 +99,9 @@ importers: simple-git-hooks: specifier: 2.13.1 version: 2.13.1 + temporal-polyfill-lite: + specifier: 0.4.2 + version: 0.4.2 tsdown: specifier: 0.22.9 version: 0.22.9(typescript@6.0.3) @@ -1996,6 +1999,9 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + temporal-polyfill-lite@0.4.2: + resolution: {integrity: sha512-AguA4sTLtAnpoJ62dk/kjssR5Tt1JmodrjS4r1o9IDkmxWJsqLJNsyicAZTpMY/Dt5AKF1tbkL9P84wGsXPhAQ==} + tiny-lru@11.4.7: resolution: {integrity: sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==} engines: {node: '>=12'} @@ -3833,6 +3839,8 @@ snapshots: tapable@2.3.3: {} + temporal-polyfill-lite@0.4.2: {} + tiny-lru@11.4.7: {} tinybench@2.9.0: {} diff --git a/src/index.ts b/src/index.ts index b004cee1..67efc765 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,10 @@ import { createApp } from "./app.ts" import { registerEnv } from "./db0-dialect/d1-register" import { notifier } from "./notify.ts" +if (!("Temporal" in globalThis)) { + await import("temporal-polyfill-lite/global") +} + const app = createApp() export default { diff --git a/tsconfig.json b/tsconfig.json index 5af4c631..7ad821f5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,11 @@ "noUncheckedIndexedAccess": false, "exactOptionalPropertyTypes": false, // Extra types - "types": ["@total-typescript/ts-reset", "@cloudflare/vitest-pool-workers/types"], + "types": [ + "@total-typescript/ts-reset", + "@cloudflare/vitest-pool-workers/types", + "temporal-polyfill-lite/global" + ], "lib": ["esnext"] }, "include": ["./", "./worker-configuration.d.ts"] diff --git a/vitest.config.ts b/vitest.config.ts index 5cb36750..42ae572a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -19,6 +19,7 @@ export default defineConfig({ test: { name: "node", include: ["src/**/*.node.test.ts"], + setupFiles: ["temporal-polyfill-lite/global"], mockReset: true, clearMocks: true, }, @@ -42,7 +43,7 @@ export default defineConfig({ name: "cf-worker", include: ["src/**/*.test.ts"], exclude: ["src/**/*.node.test.ts"], - setupFiles: ["./vitest.setup.ts"], + setupFiles: ["temporal-polyfill-lite/global", "./vitest.setup.ts"], mockReset: true, clearMocks: true, diff --git a/wrangler.json b/wrangler.json index efaa16d7..ff1498fd 100644 --- a/wrangler.json +++ b/wrangler.json @@ -2,7 +2,7 @@ "$schema": "./node_modules/wrangler/config-schema.json", "name": "dota-matches-api", "account_id": "a2ae71949fd5eaebc354f81fb3b1deab", - "compatibility_date": "2026-03-01", + "compatibility_date": "2026-07-17", "compatibility_flags": ["no_nodejs_compat_v2"], "main": "src/index.ts", From 87fc1ac6c1bbc4990ae375d7f7f1cffd4834b36a Mon Sep 17 00:00:00 2001 From: bq Date: Sat, 18 Jul 2026 09:48:19 +0200 Subject: [PATCH 2/5] move from hono to h3 --- package.json | 2 +- pnpm-lock.yaml | 35 ++++++++++++++++++++++++------ src/app.ts | 28 +++++++++++------------- src/discord/commands.ts | 43 ++++++++++++++----------------------- src/discord/index.ts | 30 ++++++++++++++------------ src/http-errors.ts | 5 +++-- src/index.ts | 4 ++-- src/notify.ts | 7 ++---- src/routes/v1/discord.ts | 46 ++++++++++++++++++---------------------- src/routes/v1/index.ts | 46 ++++++++++++++++++++++------------------ src/utils.ts | 14 +++++++----- tsdown.config.ts | 3 ++- 12 files changed, 138 insertions(+), 125 deletions(-) diff --git a/package.json b/package.json index b380fbeb..4a136065 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "discord-interactions": "4.4.0", "es-toolkit": "1.49.0", "eslint": "10.7.0", - "hono": "4.12.25", + "h3": "2.0.1-rc.25", "kysely": "0.29.4", "mentoss": "0.13.0", "milli": "4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30687356..f8e77416 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,9 +69,9 @@ importers: eslint: specifier: 10.7.0 version: 10.7.0 - hono: - specifier: 4.12.25 - version: 4.12.25 + h3: + specifier: 2.0.1-rc.25 + version: 2.0.1-rc.25 kysely: specifier: 0.29.4 version: 0.29.4 @@ -1651,9 +1651,15 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - hono@4.12.25: - resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==} - engines: {node: '>=16.9.0'} + h3@2.0.1-rc.25: + resolution: {integrity: sha512-uIK++Up28xDt4n0+bSWfVssRivNXpUmMmA6CCiK6j9+utLcBMcFK97Gn9PlSZwAPW9oLy3CjGJ4ag40CY9YYtQ==} + engines: {node: '>=20.11.1'} + hasBin: true + peerDependencies: + crossws: ^0.4.9 + peerDependenciesMeta: + crossws: + optional: true hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} @@ -1937,6 +1943,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rou3@0.9.1: + resolution: {integrity: sha512-z/sSmzvtwMDDnxsPVhfWMuG6F6mbmhFDXoVqLmMfbpDD9qfV3GDmSQpf0+W296/ZDIpW2wcMmBfpVFzcnOi/nA==} + scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} @@ -1981,6 +1990,11 @@ packages: spdx-license-ids@3.0.23: resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + srvx@0.11.22: + resolution: {integrity: sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==} + engines: {node: '>=20.16.0'} + hasBin: true + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -3490,7 +3504,10 @@ snapshots: graceful-fs@4.2.11: {} - hono@4.12.25: {} + h3@2.0.1-rc.25: + dependencies: + rou3: 0.9.1 + srvx: 0.11.22 hookable@6.1.1: {} @@ -3767,6 +3784,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + rou3@0.9.1: {} + scslre@0.3.0: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -3829,6 +3848,8 @@ snapshots: spdx-license-ids@3.0.23: {} + srvx@0.11.22: {} + stackback@0.0.2: {} std-env@4.2.0: {} diff --git a/src/app.ts b/src/app.ts index 6fcefd36..6d7e450e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,27 +1,23 @@ -import { Hono } from "hono" -import { cors } from "hono/cors" -import { showRoutes } from "hono/dev" +import { H3, handleCors, redirect } from "h3" import { v1Router } from "./routes/v1/index.ts" export const createApp = () => { - const app = new Hono<{ Bindings: Env }>() + const app = new H3() - app.use( - cors({ - origin: ["*"], - allowMethods: ["GET"], + app.all("/", (event) => { + const result = handleCors(event, { + origin: "*", + methods: ["GET"], credentials: false, - }), - ) + preflight: { statusCode: 204 }, + }) + if (result !== false) return result + }) - app.route("/v1", v1Router) + app.mount("/v1", v1Router) - app.get("/", (c) => c.redirect("https://github.com/BeeeQueue/dota-matches-api", 302)) - - if (process.env.NODE_ENV === "production") { - showRoutes(app) - } + app.get("/", () => redirect("https://github.com/BeeeQueue/dota-matches-api", 302)) return app } diff --git a/src/discord/commands.ts b/src/discord/commands.ts index d62606d8..64f26904 100644 --- a/src/discord/commands.ts +++ b/src/discord/commands.ts @@ -8,7 +8,7 @@ import { InteractionResponseType, MessageFlags, } from "discord-api-types/v10" -import type { Context } from "hono" +import type { H3Event } from "h3" import { db } from "../db.ts" import { createDotaClient } from "../dota.ts" @@ -33,9 +33,8 @@ const createCommandResponse = ( }) export const handleFollowCommand = async ( - c: Context<{ Bindings: Env }>, body: APIChatInputApplicationCommandInteraction, -) => { +): Promise => { const guildId = body.guild_id! const teamNames = body.data .options!.map((option) => @@ -45,7 +44,7 @@ export const handleFollowCommand = async ( const data: Array = teamNames.map((teamName) => ({ guildId, - channel: body.channel_id, + channel: body.channel.id, teamName, })) await db @@ -54,15 +53,12 @@ export const handleFollowCommand = async ( .onConflict((c) => c.doNothing()) .execute() - return c.json( - createCommandResponse(`Okay, I will now notify you those teams' matches.`), - ) + return createCommandResponse(`Okay, I will now notify you those teams' matches.`) } export const handleUnfollowCommand = async ( - c: Context<{ Bindings: Env }>, body: APIChatInputApplicationCommandInteraction, -) => { +): Promise => { const guildId = body.guild_id! const teamOption = body.data.options!.find( (option): option is APIApplicationCommandInteractionDataStringOption => @@ -85,17 +81,14 @@ export const handleUnfollowCommand = async ( throw badRequest("Guild not registered.") } - return c.json( - createCommandResponse( - `Okay, you will no longer receive notifications for ${teamOption.value} anymore.`, - ), + return createCommandResponse( + `Okay, you will no longer receive notifications for ${teamOption.value} anymore.`, ) } export const handleListCommand = async ( - c: Context<{ Bindings: Env }>, body: APIChatInputApplicationCommandInteraction, -) => { +): Promise => { const subscriptions = await db .selectFrom("subscription") .select(["teamName"]) @@ -104,43 +97,39 @@ export const handleListCommand = async ( .execute() if (subscriptions.length === 0) { - const response: APIInteractionResponse = { + return { type: InteractionResponseType.ChannelMessageWithSource, data: { flags: MessageFlags.Ephemeral, content: `This channel is not following any teams. Follow some with \`/follow \`!`, }, } - - return c.json(response) } - return c.json( - createCommandResponse( - `This channel is following: + return createCommandResponse( + `This channel is following: \`\`\` ${subscriptions.map((sub) => sub.teamName).join("\n")} \`\`\``, - ), ) } export const handleAutocompleteCommand = async ( - c: Context<{ Bindings: Env }>, + event: H3Event, country: string, value: string, -) => { - const dotaClient = createDotaClient(c.env) +): Promise => { + const dotaClient = createDotaClient(event) const teams = await dotaClient.getTeams(country) const fuzzy = new Fuzzy({ intraIns: 2, interIns: 5 }) const [idxs] = fuzzy.search(teams, value) - return c.json({ + return { type: InteractionResponseType.ApplicationCommandAutocompleteResult, data: { choices: idxs?.map((idx) => teams[idx]).map((team) => ({ name: team, value: team })) ?? [], }, - } satisfies APIApplicationCommandAutocompleteResponse) + } } diff --git a/src/discord/index.ts b/src/discord/index.ts index 43a95875..417da2c2 100644 --- a/src/discord/index.ts +++ b/src/discord/index.ts @@ -11,7 +11,7 @@ import { Routes, ThreadAutoArchiveDuration, } from "discord-api-types/v10" -import type { Context } from "hono" +import type { EventHandlerResponse } from "h3" import { Xior, type XiorError } from "xior" import { badRequest } from "../http-errors.ts" @@ -36,14 +36,18 @@ type RegisterGuildOptions = { } const registerGuild = - (c: Context<{ Bindings: Env }>) => - async ({ code, guildId, permissions }: RegisterGuildOptions): Promise => { + (env: Env) => + async ({ + code, + guildId, + permissions, + }: RegisterGuildOptions): Promise => { const urlEncodedBody = { grant_type: "authorization_code", - client_id: c.env.DISCORD_CLIENT_ID, - client_secret: c.env.DISCORD_CLIENT_SECRET, + client_id: env.DISCORD_CLIENT_ID, + client_secret: env.DISCORD_CLIENT_SECRET, code, - redirect_uri: getRedirectUri(c.env), + redirect_uri: getRedirectUri(env), } satisfies RESTPostOAuth2AccessTokenURLEncodedData const response = await fetch(`${baseUrl}${Routes.oauth2TokenExchange()}`, { method: "POST", @@ -64,12 +68,12 @@ const registerGuild = if (permissions !== BOT_PERMISSIONS) { console.log(`Got invalid permissions: ${permissions}`) - await leaveGuild(c.env, guildId) + await leaveGuild(env, guildId) throw badRequest("All the required permissions were not given.") } - return c.text("Ok") + return "Ok" } const getRedirectUri = (env: Env) => `${env.API_BASE}/v1/discord/callback` @@ -154,10 +158,10 @@ const sendMessage = } } -export const createDiscordClient = (c: { env: Env } | Context<{ Bindings: Env }>) => ({ - getAuthorizeUrl: getAuthorizeUrl(c.env), - registerGuild: registerGuild(c as Context<{ Bindings: Env }>), - createThread: createThread(c.env), - sendMessage: sendMessage(c.env), +export const createDiscordClient = (env: Env) => ({ + getAuthorizeUrl: getAuthorizeUrl(env), + registerGuild: registerGuild(env), + createThread: createThread(env), + sendMessage: sendMessage(env), leaveGuild, }) diff --git a/src/http-errors.ts b/src/http-errors.ts index e9237883..e2a7d15f 100644 --- a/src/http-errors.ts +++ b/src/http-errors.ts @@ -1,3 +1,4 @@ -import { HTTPException } from "hono/http-exception" +import { HTTPError } from "h3" -export const badRequest = (message = "Bad request") => new HTTPException(400, { message }) +export const badRequest = (message = "Bad request") => + new HTTPError(message, { status: 400 }) diff --git a/src/index.ts b/src/index.ts index 67efc765..bb2bf629 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,10 +9,10 @@ if (!("Temporal" in globalThis)) { const app = createApp() export default { - fetch: async (request, env, ctx) => { + fetch: async (request, env) => { registerEnv(env) - return app.fetch(request, env, ctx) + return app.fetch(request) }, scheduled: notifier, } satisfies ExportedHandler diff --git a/src/notify.ts b/src/notify.ts index 3b9ce818..7624773a 100644 --- a/src/notify.ts +++ b/src/notify.ts @@ -51,10 +51,7 @@ export const formatMatchToEmbedField = ( } } -export const notifier: ExportedHandlerScheduledHandler = async ( - _controller, - env: Env, -) => { +export const notifier: ExportedHandlerScheduledHandler = async (_, env: Env) => { registerEnv(env) const now = Temporal.Now.instant().toString() @@ -103,7 +100,7 @@ export const notifier: ExportedHandlerScheduledHandler = async ( return [channelId, embed] as const }) - const discordClient = createDiscordClient({ env }) + const discordClient = createDiscordClient(env) await Promise.all( messages.map(async ([channelId, embed]) => { await discordClient.sendMessage(channelId, embed) diff --git a/src/routes/v1/discord.ts b/src/routes/v1/discord.ts index f8ffb1ad..d36caa2d 100644 --- a/src/routes/v1/discord.ts +++ b/src/routes/v1/discord.ts @@ -6,7 +6,7 @@ import { InteractionType, } from "discord-api-types/v10" import { verifyKey } from "discord-interactions" -import { Hono } from "hono" +import { getQuery, H3, redirect } from "h3" import { handleAutocompleteCommand, @@ -16,26 +16,27 @@ import { } from "../../discord/commands.ts" import { createDiscordClient } from "../../discord/index.ts" import { badRequest } from "../../http-errors.ts" -import { getCountry } from "../../utils.ts" +import { getCountry, getEnv } from "../../utils.ts" -export const discordRouter = new Hono<{ Bindings: Env }>() +export const discordRouter = new H3() -discordRouter.get("/", (c) => - c.redirect(createDiscordClient(c).getAuthorizeUrl().toString(), 302), +discordRouter.get("/", (event) => + redirect(createDiscordClient(getEnv(event)).getAuthorizeUrl().toString(), 302), ) if (process.env.NODE_ENV !== "production") { - discordRouter.get("/autocomplete/teams", async (c) => { - return handleAutocompleteCommand(c, "main", c.req.query().query ?? "") + discordRouter.get("/autocomplete/teams", async (event) => { + const query = getQuery(event) + return handleAutocompleteCommand(event, "main", query.query ?? "") }) } const isValidCallback = (params: URLSearchParams) => params.has("code") && params.has("guild_id") && params.has("permissions") -discordRouter.get("/callback", async (c) => { - const discordClient = createDiscordClient(c) - const url = new URL(c.req.url) +discordRouter.get("/callback", async (event) => { + const discordClient = createDiscordClient(getEnv(event)) + const url = new URL(event.req.url) if (!isValidCallback(url.searchParams)) { throw badRequest("Missing or invalid callback parameters") @@ -48,11 +49,11 @@ discordRouter.get("/callback", async (c) => { }) }) -discordRouter.post("/interactions", async (c) => { - const body = await c.req.text() - const signature = c.req.header("x-signature-ed25519")! - const timestamp = c.req.header("x-signature-timestamp")! - if (!(await verifyKey(body, signature, timestamp, c.env.DISCORD_PUBLIC_KEY))) { +discordRouter.post("/interactions", async (event) => { + const body = await event.req.text() + const signature = event.req.headers.get("x-signature-ed25519")! + const timestamp = event.req.headers.get("x-signature-timestamp")! + if (!(await verifyKey(body, signature, timestamp, getEnv(event).DISCORD_PUBLIC_KEY))) { throw badRequest("Invalid c.req signature") } @@ -60,11 +61,11 @@ discordRouter.post("/interactions", async (c) => { const { type, data } = parsedBody if (type === InteractionType.Ping) { - return c.json({ type: InteractionResponseType.Pong }) + return { type: InteractionResponseType.Pong } } if (type === InteractionType.ApplicationCommandAutocomplete) { - const country = getCountry(c.req) + const country = getCountry(event) const { value } = data.options.find( (option): option is APIApplicationCommandInteractionDataStringOption => @@ -73,35 +74,30 @@ discordRouter.post("/interactions", async (c) => { if (value == null) throw badRequest() - return handleAutocompleteCommand(c, country, value) + return handleAutocompleteCommand(event, country, value) } if (type === InteractionType.ApplicationCommand && data != null) { switch (data.name) { case "follow": { return handleFollowCommand( - c, parsedBody as APIChatInputApplicationCommandInteraction, ) } case "unfollow": { return handleUnfollowCommand( - c, parsedBody as APIChatInputApplicationCommandInteraction, ) } case "follows": { - return handleListCommand( - c, - parsedBody as APIChatInputApplicationCommandInteraction, - ) + return handleListCommand(parsedBody as APIChatInputApplicationCommandInteraction) } } throw badRequest("Invalid command") } - return c.text("Ok") + return "Ok" }) diff --git a/src/routes/v1/index.ts b/src/routes/v1/index.ts index 6be50b99..b960e7fa 100644 --- a/src/routes/v1/index.ts +++ b/src/routes/v1/index.ts @@ -1,4 +1,4 @@ -import { Hono } from "hono" +import { H3 } from "h3" import { createDotaClient } from "../../dota.ts" import { @@ -6,46 +6,50 @@ import { getBrowserCacheTtl, getCacheHeaders, getCountry, + getEnv, getTtl, MetaKey, + setCacheHeaders, } from "../../utils.ts" import { discordRouter } from "./discord.ts" -export const v1Router = new Hono<{ Bindings: Env }>() +export const v1Router = new H3() -v1Router.route("/discord", discordRouter) +v1Router.mount("/discord", discordRouter) -v1Router.get("/matches", async (c) => { - const cached = await caches.default.match(c.req.url) +v1Router.get("/matches", async (event) => { + const env = getEnv(event) + + const cached = await caches.default.match(event.req.url) if (cached != null) { - const lastFetched = Number(await c.env.META.get(MetaKey.MATCHES_LAST_FETCHED)) - - return c.json((await cached.json()) as Record, 200, { - ...cached.headers, - "Cache-Control": `public, max-age=${getBrowserCacheTtl( - getTtl(lastFetched, EDGE_CACHE_TIMEOUT), - )}`, - }) + const lastFetched = Number(await env.META.get(MetaKey.MATCHES_LAST_FETCHED)) + + // TODO: test handleCacheHeaders + event.res.headers.set( + "Cache-Control", + `public, max-age=${getBrowserCacheTtl(getTtl(lastFetched, EDGE_CACHE_TIMEOUT))}`, + ) + return cached.json() } - const dota = createDotaClient(c.env) + const dota = createDotaClient(env) - const country = getCountry(c.req) + const country = getCountry(event) const { matches, lastFetched } = await dota.getMatches(country) - c.executionCtx.waitUntil( + event.waitUntil( caches.default .put( - c.req.url, - c.json(matches, { + event.req.url, + Response.json(matches, { headers: getCacheHeaders(lastFetched), }), ) .catch((error: Error) => console.log(`Failed to cache response: ${error.message}`)), ) - return c.json(matches, { - headers: getCacheHeaders(lastFetched), - }) + setCacheHeaders(event, lastFetched) + + return matches }) diff --git a/src/utils.ts b/src/utils.ts index 3722dd5d..33020242 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,12 @@ -import type { HonoRequest } from "hono" +import type { H3Event } from "h3" import { ms, type StringValue } from "milli" import { ELEMENT_NODE, type ElementNode, parse, TEXT_NODE } from "ultrahtml" import { querySelector, querySelectorAll } from "ultrahtml/selector" import type { Team } from "./dota" +export const getEnv = (event: H3Event) => event.req.runtime!.cloudflare!.env + export const MetaKey = { MATCHES_LAST_FETCHED: "MATCHES_LAST_FETCHED", TEAMS_LAST_FETCHED: "TEAMS_LAST_FETCHED", @@ -32,10 +34,8 @@ export const getNodeText = (node: ElementNode): string => { .trim() } -export const getCountry = (request: HonoRequest): string => { - if (request.raw.cf?.country != null) { - return request.raw.cf.country as string - } +export const getCountry = (event: H3Event): string => { + if (event.req.cf?.country != null) return event.req.cf.country as string return "UNKNOWN" } @@ -61,6 +61,10 @@ export const getCacheHeaders = (lastFetched: number) => { } } +export const setCacheHeaders = (event: H3Event, lastFetched: number) => { + event.res.headers.set("Cache-Control", getCacheHeaders(lastFetched)["Cache-Control"]) +} + export const parseTeamsPage = (html: string): Team[] => { const root = parse(html) as ElementNode diff --git a/tsdown.config.ts b/tsdown.config.ts index dca6a14d..64eec255 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -3,12 +3,13 @@ import { defineConfig } from "tsdown" export default defineConfig({ entry: ["src/index.ts"], outDir: "dist", + deps: { onlyBundle: false }, env: { NODE_ENV: process.env.NODE_ENV === "development" ? "development" : "production", }, - target: "node25", + target: "esnext", platform: "browser", format: "esm", fixedExtension: true, From 2cd5b5350096eb48130290b9d4e67edffeb60fd5 Mon Sep 17 00:00:00 2001 From: bq Date: Sat, 18 Jul 2026 09:54:42 +0200 Subject: [PATCH 3/5] update renovate config, CI workflows --- .github/renovate.json5 | 24 +-------- .github/workflows/cicd.yml | 96 +++--------------------------------- .github/workflows/e2e.yml | 16 ++---- .github/workflows/zizmor.yml | 37 ++++++++++++++ .github/zizmor.yml | 3 ++ 5 files changed, 54 insertions(+), 122 deletions(-) create mode 100644 .github/workflows/zizmor.yml create mode 100644 .github/zizmor.yml diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 71f1d5ec..fa694c35 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,24 +1,4 @@ { - extends: [ - "config:js-app", - "helpers:disableTypesNodeMajor", - "schedule:earlyMondays", - "group:allNonMajor", - ], - prHourlyLimit: 5, - prConcurrentLimit: 5, - branchConcurrentLimit: 5, - labels: ["dependencies"], - baseBranches: ["main"], - packageRules: [ - { - matchUpdateTypes: ["patch", "minor"], - matchManagers: ["npm"], - automerge: true, - }, - { - packagePatterns: ["lint", "!lint-staged"], - groupName: "Linting", - }, - ], + $schema: "https://docs.renovatebot.com/renovate-schema.json", + extends: ["github>BeeeQueue/renovate-config:js-lib.json"], } diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 66f4b6e2..5bae6870 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -22,21 +22,12 @@ jobs: with: persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 26 - - - name: find pnpm cache path - id: cache - run: echo "path=$(pnpm store path)" >> "$GITHUB_OUTPUT" - - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: - path: ${{ steps.cache.outputs.path }} - key: v1-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - v1-pnpm- + cache: true - name: Install dependencies run: pnpm install --frozen-lockfile @@ -51,21 +42,12 @@ jobs: with: persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 26 - - - name: find pnpm cache path - id: cache - run: echo "path=$(pnpm store path)" >> "$GITHUB_OUTPUT" - - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: - path: ${{ steps.cache.outputs.path }} - key: v1-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - v1-pnpm- + cache: true - name: Install dependencies run: pnpm install --frozen-lockfile @@ -81,21 +63,12 @@ jobs: with: persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 26 - - - name: find pnpm cache path - id: cache - run: echo "path=$(pnpm store path)" >> "$GITHUB_OUTPUT" - - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: - path: ${{ steps.cache.outputs.path }} - key: v1-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - v1-pnpm- + cache: true - name: Install dependencies run: pnpm install --frozen-lockfile @@ -111,68 +84,15 @@ jobs: with: persist-credentials: false - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 26 - - - name: find pnpm cache path - id: cache - run: echo "path=$(pnpm store path)" >> "$GITHUB_OUTPUT" - - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: - path: ${{ steps.cache.outputs.path }} - key: v1-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - v1-pnpm- + cache: true - name: Install dependencies run: pnpm install --frozen-lockfile - run: node --run build - run: node --run test - - deploy: - if: ${{ github.ref == 'refs/heads/main' }} - runs-on: ubuntu-latest - - environment: - name: live - url: https://dota.haglund.dev/v1/matches - concurrency: - group: live - - needs: - - build - - lint - - test - - typecheck - - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 26 - - - name: find pnpm cache path - id: cache - run: echo "path=$(pnpm store path)" >> "$GITHUB_OUTPUT" - - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ${{ steps.cache.outputs.path }} - key: v1-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - v1-pnpm- - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - run: pnpm wrangler deploy - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index df834584..8b783a78 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -27,23 +27,15 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 26 - - - run: corepack enable - - - name: find pnpm cache path - id: cache - run: echo "path=$(pnpm store path)" >> $GITHUB_OUTPUT - - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: - path: ${{ steps.cache.outputs.path }} - key: v1-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - v1-pnpm- + cache: true - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 00000000..f0706d3a --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,37 @@ +name: zizmor + +on: + push: + branches: [main] + paths: [.github/workflows/*] + pull_request: + paths: [.github/workflows/*] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + lint-workflows: + runs-on: ubuntu-latest + + permissions: + contents: read # only required in private repos + actions: read # only required in private repos + security-events: write # allow writing security events + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Run zizmor 🌈 + uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 + with: + persona: pedantic + annotations: true + advanced-security: false diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000..74cea038 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,3 @@ +rules: + anonymous-definition: + disable: true From c86d128facfa3f51f1305dcfb80889ad46cd829f Mon Sep 17 00:00:00 2001 From: bq Date: Sat, 18 Jul 2026 10:11:39 +0200 Subject: [PATCH 4/5] fix type error and missing extensions --- src/discord/commands.ts | 5 +++-- src/dota.test.ts | 2 +- src/dota.ts | 2 +- src/index.ts | 2 +- src/notify.ts | 6 +++--- src/parser.node.test.ts | 2 +- src/parser.ts | 4 ++-- src/test-utils.ts | 2 +- src/utils.ts | 2 +- 9 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/discord/commands.ts b/src/discord/commands.ts index 64f26904..8a417e68 100644 --- a/src/discord/commands.ts +++ b/src/discord/commands.ts @@ -13,7 +13,8 @@ import type { H3Event } from "h3" import { db } from "../db.ts" import { createDotaClient } from "../dota.ts" import { badRequest } from "../http-errors.ts" -import type { Subscription$ } from "../schema" +import type { Subscription$ } from "../schema.ts" +import { getEnv } from "../utils.ts" export const Command = { Follow: "follow", @@ -119,7 +120,7 @@ export const handleAutocompleteCommand = async ( country: string, value: string, ): Promise => { - const dotaClient = createDotaClient(event) + const dotaClient = createDotaClient(getEnv(event)) const teams = await dotaClient.getTeams(country) const fuzzy = new Fuzzy({ intraIns: 2, interIns: 5 }) diff --git a/src/dota.test.ts b/src/dota.test.ts index 2a1ad467..ff92cd6d 100644 --- a/src/dota.test.ts +++ b/src/dota.test.ts @@ -3,7 +3,7 @@ import { FetchMocker, MockServer } from "mentoss" import { ms } from "milli" import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest" -import { registerEnv } from "./db0-dialect/d1-register" +import { registerEnv } from "./db0-dialect/d1-register.ts" import { db } from "./db.ts" import { getTeams, type LiquipediaBody } from "./dota.ts" import { MetaKey } from "./utils.ts" diff --git a/src/dota.ts b/src/dota.ts index 45c53309..eb753926 100644 --- a/src/dota.ts +++ b/src/dota.ts @@ -8,7 +8,7 @@ import { upsertMatchData, upsertTeamsData, } from "./db.ts" -import { parseMatchesPage } from "./parser" +import { parseMatchesPage } from "./parser.ts" import { EDGE_CACHE_TIMEOUT, MetaKey, parseTeamsPage, seconds } from "./utils.ts" export type Team = { diff --git a/src/index.ts b/src/index.ts index bb2bf629..5c5534c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { createApp } from "./app.ts" -import { registerEnv } from "./db0-dialect/d1-register" +import { registerEnv } from "./db0-dialect/d1-register.ts" import { notifier } from "./notify.ts" if (!("Temporal" in globalThis)) { diff --git a/src/notify.ts b/src/notify.ts index 7624773a..8ba1ef9e 100644 --- a/src/notify.ts +++ b/src/notify.ts @@ -2,11 +2,11 @@ import type { APIEmbed, APIEmbedField } from "discord-api-types/v10" import { groupBy } from "es-toolkit" import { sql } from "kysely" -import { registerEnv } from "./db0-dialect/d1-register" +import { registerEnv } from "./db0-dialect/d1-register.ts" import { db } from "./db.ts" import { createDiscordClient } from "./discord/index.ts" -import type { Match$, Subscription$ } from "./schema" -import { ms2s } from "./utils" +import type { Match$, Subscription$ } from "./schema.ts" +import { ms2s } from "./utils.ts" const orEmpty = (check: T | null | undefined, value: string) => check != null ? value : "" diff --git a/src/parser.node.test.ts b/src/parser.node.test.ts index 82e0696d..2bbe1419 100644 --- a/src/parser.node.test.ts +++ b/src/parser.node.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest" import teamsPageFixture from "./fixtures/matches.html?raw" -import { parseMatchesPage } from "./parser" +import { parseMatchesPage } from "./parser.ts" describe("parseMatchesPage", () => { it("correctly parses the body", async () => { diff --git a/src/parser.ts b/src/parser.ts index e5634b57..346e62f7 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -2,8 +2,8 @@ import { nanoid } from "nanoid/non-secure" import { type ElementNode, parse } from "ultrahtml" import { querySelector, querySelectorAll } from "ultrahtml/selector" -import type { Match, Team } from "./dota" -import { getNodeText } from "./utils" +import type { Match, Team } from "./dota.ts" +import { getNodeText } from "./utils.ts" const extractTeam = (team$: ElementNode): Team => { if (getNodeText(team$).trim() === "TBD") { diff --git a/src/test-utils.ts b/src/test-utils.ts index af9a8205..eca87278 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -1,4 +1,4 @@ -import type { Subscription$, Team$ } from "./schema" +import type { Subscription$, Team$ } from "./schema.ts" export const GUILD_ID = "987613986523" export const CHANNEL_ID = "0986526095326812" diff --git a/src/utils.ts b/src/utils.ts index 33020242..c73fe4c6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,7 +3,7 @@ import { ms, type StringValue } from "milli" import { ELEMENT_NODE, type ElementNode, parse, TEXT_NODE } from "ultrahtml" import { querySelector, querySelectorAll } from "ultrahtml/selector" -import type { Team } from "./dota" +import type { Team } from "./dota.ts" export const getEnv = (event: H3Event) => event.req.runtime!.cloudflare!.env From ff609987c65d35bbd56fd40318b9c5d60fae6af3 Mon Sep 17 00:00:00 2001 From: bq Date: Sat, 18 Jul 2026 10:25:14 +0200 Subject: [PATCH 5/5] fix exported handler --- src/index.ts | 13 +++++-------- tsdown.config.ts | 3 +++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5c5534c8..892f44a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,15 @@ +import { serve } from "h3" + import { createApp } from "./app.ts" -import { registerEnv } from "./db0-dialect/d1-register.ts" import { notifier } from "./notify.ts" if (!("Temporal" in globalThis)) { await import("temporal-polyfill-lite/global") } -const app = createApp() - export default { - fetch: async (request, env) => { - registerEnv(env) - - return app.fetch(request) - }, + ...serve(createApp(), { + trustProxy: true, + }), scheduled: notifier, } satisfies ExportedHandler diff --git a/tsdown.config.ts b/tsdown.config.ts index 64eec255..cc0198a4 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -8,6 +8,9 @@ export default defineConfig({ env: { NODE_ENV: process.env.NODE_ENV === "development" ? "development" : "production", }, + alias: { + h3: "h3/cloudflare", + }, target: "esnext", platform: "browser",