feat: implement webhook merger

This commit is contained in:
2026-06-11 18:54:10 +07:00 Unverified
parent 1fa07d439b
commit dcd521ee3c
5 changed files with 133 additions and 3 deletions
-2
View File
@@ -1,2 +0,0 @@
PRIVATE_API_KEY=key is not needed
PUBLIC_API_KEY=key is not needed
+2 -1
View File
@@ -42,4 +42,5 @@ package-lock.json
**/*.bun
dist
/store/
/store/
.env
+1
View File
@@ -9,6 +9,7 @@ declare module "bun" {
interface Env {
PUBLIC_API_KEY: string;
PRIVATE_API_KEY: string;
WH_ROUTE_MAP: string;
}
}
+3
View File
@@ -4,10 +4,13 @@ import { t } from "elysia";
// import { endfieldCard } from "@/games/endfield";
import { card } from "@/games/genshin";
import { webhookMerger } from "./merger";
export function v1routes(app: App) {
app.group("/v1", (app) =>
app
.use(card)
.use(webhookMerger)
// .use(endfieldCard)
.get(
"/status/:status",
+127
View File
@@ -0,0 +1,127 @@
import type { App } from "@";
import { env } from "bun";
import { status } from "elysia";
import { TTLCache } from "enka.ts";
const routesCache = new TTLCache();
type Pending = {
data: unknown;
timer: ReturnType<typeof setTimeout>;
count: number;
};
const pending = new Map<string, Pending>();
export function webhookMerger(app: App) {
return app.post("/webhook/:id", async ({ body, params: { id } }) => {
const map = await getRoutes();
const cur = map.find((e) => e[0] === id);
if (!cur) return status(404);
const [, url, bufferTime] = cur;
const existing = pending.get(id);
if (existing) {
clearTimeout(existing.timer);
existing.data = merge(existing.data, body);
existing.count++;
existing.timer = setTimeout(async () => {
try {
await fetch(url, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(existing.data),
});
} finally {
pending.delete(id);
}
}, bufferTime);
return status(202);
}
const state: Pending = {
data: body,
count: 1,
timer: undefined as never,
};
state.timer = setTimeout(async () => {
try {
await fetch(url, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(state.data),
});
} finally {
pending.delete(id);
}
}, bufferTime);
pending.set(id, state);
return status(202);
});
}
type RouteMap = [string, string, number][];
async function getRoutes(): Promise<RouteMap> {
const cached = routesCache.get(".");
if (cached) return cached as RouteMap;
const res = await fetch(
env.WH_ROUTE_MAP,
).then((r) => r.ok && r.json());
if (!res) throw new Error("cant get route map");
routesCache.set(".", res, 900);
return res;
}
const CONCAT_FIELDS = new Set(["content"]);
function merge(a: unknown, b: unknown, key?: string): unknown {
if (
key &&
CONCAT_FIELDS.has(key) &&
typeof a === "string" &&
typeof b === "string"
) {
return `${a}\n${b}`;
}
if (Array.isArray(a) && Array.isArray(b)) {
return [...a, ...b];
}
if (
a &&
b &&
typeof a === "object" &&
typeof b === "object" &&
!Array.isArray(a) &&
!Array.isArray(b)
) {
const out: Record<string, unknown> = {
...(a as Record<string, unknown>),
};
for (const [k, v] of Object.entries(b)) {
out[k] = k in out ? merge(out[k], v, k) : v;
}
return out;
}
return b;
}