diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md index d22d8e0..ea8df0e 100644 --- a/docs/guide/introduction.md +++ b/docs/guide/introduction.md @@ -18,11 +18,11 @@ GitHub / Gitea Webhook → Cloudflare Worker (Nuxt 4 / Nitro) ### Components -| Component | Role | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **Cloudflare Worker** | HTTP ingress, signature verification, delivery dedup, event parsing, route matching, platform dispatch | -| **Interactions Endpoint** | Verifies Ed25519 signatures and handles `/gh` interactions (slash commands, context-menu commands, buttons, modals) | -| **KV** | Token storage (`token:{userId}`), OAuth state (`state:{hex}`), admin sessions, per-group secrets, config cache, delivery dedup/state/message-tracking fallback when D1 is unavailable (`delivery:*`, `delivery-state:*`, `msg:*`), message-update locks (`msg:lock:*`) | +| Component | Role | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Cloudflare Worker** | HTTP ingress, signature verification, delivery dedup, event parsing, route matching, platform dispatch | +| **Interactions Endpoint** | Verifies Ed25519 signatures and handles `/gh` interactions (slash commands, context-menu commands, buttons, modals) | +| **KV** | Token storage (`token:{userId}`), OAuth state (`state:{hex}`), admin sessions, per-group secrets, config cache, delivery dedup/state/message-tracking fallback when D1 is unavailable (`delivery:*`, `delivery-state:*`, `msg:*`), message-update locks (`msg:lock:*`) | | **D1** | Routes/groups (`d1_routes`/`d1_groups`), send logs (`send_logs`), audit logs (`audit_logs`), dedup (`dedup_keys`), delivery state (`delivery_state`), message tracking (`message_tracking`), Discord↔GitHub links (`discord_links`), Telegram↔GitHub links (`telegram_links`), with R2 optionally parking oversized payloads | ### Data Flow diff --git a/docs/guide/storage.md b/docs/guide/storage.md index 6c89cb9..83d0505 100644 --- a/docs/guide/storage.md +++ b/docs/guide/storage.md @@ -4,42 +4,42 @@ KV keeps only cache data and short-lived/ephemeral state. High-frequency writes (webhook dedup, delivery state, message tracking) live in D1 and only fall back to KV when D1 is unavailable or not yet migrated (see [Storage decisions](./storage#storage-decisions)). -| Key Pattern | Value | TTL | -| ------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------ | -| `config:routes` | Route config cache (D1 `d1_routes` is authoritative) | 1 hour | -| `config:groups` | Group config cache (D1 `d1_groups` is authoritative) | 1 hour | -| `session:{id}` | Admin session `{ userId, login }` | 7 days | -| `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × token expiry | -| `token-reverse:{sha256}` | User id for reverse lookup by token | 0.9 × token expiry | -| `state:{hex}` | `{ redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? }` | 600 seconds | -| `invite:{token}` | `{ groupId, role, expiresAt, createdBy, note? }` | 7 days | -| `invite:group:{id}` | Token index per group (keeps invite listing consistent) | 7 days | -| `delivery:{provider}:{groupId}:{id}` | Webhook delivery dedup fallback (D1 `dedup_keys` is primary) | 7 days | -| `delivery-state:{provider}:{groupId}:{id}` | Queue delivery state fallback (D1 `delivery_state` is primary) | 1 hour | -| `queue:payload:{provider}:{groupId}:{id}` | Oversized webhook payload parked for the queue consumer (R2 is primary) | 1 hour | -| `nonce:{nonce}` | Custom-webhook replay protection nonce (single use) | 600 seconds | -| `tenant:{groupId}` | Per-group webhook secret (64-char hex, generated from the console) | Permanent | -| `msg:{routeId}:{key}:{target}` | Message id tracking fallback (D1 `message_tracking` is primary) | 1 day | -| `cmd:guild:{id}` | Guild id whose commands were registered (dedup) | Permanent | -| `cmd:registered:global` | Global command registration marker (dedup) | 1 day | -| `config:discord-app-id` | Cached Discord application id | Permanent | -| `i18n:{lang}` | Translation overrides merged on top of English | Permanent | +| Key Pattern | Value | TTL | +| ------------------------------------------ | ----------------------------------------------------------------------------- | ------------------ | +| `config:routes` | Route config cache (D1 `d1_routes` is authoritative) | 1 hour | +| `config:groups` | Group config cache (D1 `d1_groups` is authoritative) | 1 hour | +| `session:{id}` | Admin session `{ userId, login }` | 7 days | +| `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × token expiry | +| `token-reverse:{sha256}` | User id for reverse lookup by token | 0.9 × token expiry | +| `state:{hex}` | `{ redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? }` | 600 seconds | +| `invite:{token}` | `{ groupId, role, expiresAt, createdBy, note? }` | 7 days | +| `invite:group:{id}` | Token index per group (keeps invite listing consistent) | 7 days | +| `delivery:{provider}:{groupId}:{id}` | Webhook delivery dedup fallback (D1 `dedup_keys` is primary) | 7 days | +| `delivery-state:{provider}:{groupId}:{id}` | Queue delivery state fallback (D1 `delivery_state` is primary) | 1 hour | +| `queue:payload:{provider}:{groupId}:{id}` | Oversized webhook payload parked for the queue consumer (R2 is primary) | 1 hour | +| `nonce:{nonce}` | Custom-webhook replay protection nonce (single use) | 600 seconds | +| `tenant:{groupId}` | Per-group webhook secret (64-char hex, generated from the console) | Permanent | +| `msg:{routeId}:{key}:{target}` | Message id tracking fallback (D1 `message_tracking` is primary) | 1 day | +| `cmd:guild:{id}` | Guild id whose commands were registered (dedup) | Permanent | +| `cmd:registered:global` | Global command registration marker (dedup) | 1 day | +| `config:discord-app-id` | Cached Discord application id | Permanent | +| `i18n:{lang}` | Translation overrides merged on top of English | Permanent | ## D1 Storage Layout The D1 database (`DB` binding, database `webhooker`) holds the source of truth for configuration, delivery logs and high-frequency ephemeral state: -| Table | Purpose | -| ------------------ | ---------------------------------------------------------------------------------------------------- | -| `d1_groups` | Groups (authoritative config, seeded from legacy KV `config:groups`) | -| `d1_routes` | Routes per group (authoritative config, seeded from legacy KV `config:routes`) | -| `send_logs` | One row per dispatch attempt (route id, event, target, ok/error, duration, error code, detail) | -| `audit_logs` | One row per admin operation (login/logout, group/route/member/invite changes) | -| `dedup_keys` | Webhook delivery dedup (atomic `INSERT ... ON CONFLICT` UPSERT, key + expiry) | -| `delivery_state` | Queue delivery state (`pending`/`processing`/`delivered`/`retrying`/`failed`/`dead`) | -| `message_tracking` | Message id tracking for in-place updates (`event_id` + `target_id` → `message_id`) | -| `discord_links` | Maps `discord_user_id` → `github_user_id` for `/gh` Discord commands | -| `telegram_links` | Maps `telegram_user_id` → `github_user_id` for `/gh` Telegram commands | +| Table | Purpose | +| ------------------ | ---------------------------------------------------------------------------------------------- | +| `d1_groups` | Groups (authoritative config, seeded from legacy KV `config:groups`) | +| `d1_routes` | Routes per group (authoritative config, seeded from legacy KV `config:routes`) | +| `send_logs` | One row per dispatch attempt (route id, event, target, ok/error, duration, error code, detail) | +| `audit_logs` | One row per admin operation (login/logout, group/route/member/invite changes) | +| `dedup_keys` | Webhook delivery dedup (atomic `INSERT ... ON CONFLICT` UPSERT, key + expiry) | +| `delivery_state` | Queue delivery state (`pending`/`processing`/`delivered`/`retrying`/`failed`/`dead`) | +| `message_tracking` | Message id tracking for in-place updates (`event_id` + `target_id` → `message_id`) | +| `discord_links` | Maps `discord_user_id` → `github_user_id` for `/gh` Discord commands | +| `telegram_links` | Maps `telegram_user_id` → `github_user_id` for `/gh` Telegram commands | `audit_logs` is pruned by the scheduled trigger after `AUDIT_RETENTION_DAYS` (default 90). The `storage-prune` task removes expired `dedup_keys`, `delivery_state` rows older than 7 days and `message_tracking` rows older than 30 days. See [Logs](./logs) for the log row fields. @@ -47,8 +47,8 @@ The D1 database (`DB` binding, database `webhooker`) holds the source of truth f R2 (`PAYLOAD` binding, bucket `webhooker-payloads`) stores oversized webhook payloads that are too large for a queue message or KV: -| Object Pattern | Purpose | Retention | -| ----------------------------- | ---------------------------------------------- | --------- | +| Object Pattern | Purpose | Retention | +| --------------------------------- | ----------------------------------------------- | ---------------------- | | `webhooks/YYYY/MM/DD/.json` | Oversized payload parked for the queue consumer | deleted after dispatch | When the `PAYLOAD` binding is absent, oversized payloads fall back to the KV key `queue:payload:{provider}:{groupId}:{id}` (1 hour TTL). diff --git a/docs/guide/tasks.md b/docs/guide/tasks.md index 41cf1a9..bdadac5 100644 --- a/docs/guide/tasks.md +++ b/docs/guide/tasks.md @@ -2,11 +2,11 @@ WebHooker runs four maintenance tasks on the scheduled trigger (`*/5 * * * *`, every 5 minutes). They only run on the deployed worker (Cloudflare cron); local `wrangler dev` runs them when triggered via `wrangler dev --test-scheduled`. -| Task | Purpose | -| --------------- | ---------------------------------------------------------------------------------------------------------------- | -| `discord-sync` | Registers the Discord slash/context-menu commands: per-guild (instant) and globally (24h dedup, ~1h propagation) | -| `telegram-sync` | Calls `setWebhook` to `{BASE_URL}/telegram/webhook` (with `TELEGRAM_WEBHOOK_SECRET` as `secret_token` when set) | -| `audit-prune` | Deletes `audit_logs` entries older than `AUDIT_RETENTION_DAYS` (default 90) | +| Task | Purpose | +| --------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `discord-sync` | Registers the Discord slash/context-menu commands: per-guild (instant) and globally (24h dedup, ~1h propagation) | +| `telegram-sync` | Calls `setWebhook` to `{BASE_URL}/telegram/webhook` (with `TELEGRAM_WEBHOOK_SECRET` as `secret_token` when set) | +| `audit-prune` | Deletes `audit_logs` entries older than `AUDIT_RETENTION_DAYS` (default 90) | | `storage-prune` | Deletes expired `dedup_keys` rows, `delivery_state` rows older than 7 days and `message_tracking` rows older than 30 days | There is nothing to configure beyond the secrets the tasks use (`DISCORD_TOKEN`, `DISCORD_APPLICATION_ID`, `TELEGRAM_TOKEN`, `BASE_URL`, `AUDIT_RETENTION_DAYS`). diff --git a/docs/zh/guide/introduction.md b/docs/zh/guide/introduction.md index 4e75bea..0669fbb 100644 --- a/docs/zh/guide/introduction.md +++ b/docs/zh/guide/introduction.md @@ -18,11 +18,11 @@ GitHub / Gitea Webhook → Cloudflare Worker (Nuxt 4 / Nitro) ### 组件 -| 组件 | 职责 | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Cloudflare Worker** | HTTP 入口、签名验证、投递去重、事件解析、路由匹配、平台分发 | -| **Interactions Endpoint** | 验证 Ed25519 签名并处理 `/gh` 交互(斜杠命令、右键菜单、按钮、modal) | -| **KV** | Token 存储 (`token:{userId}`)、OAuth 状态 (`state:{hex}`)、管理员会话、分组级 secret、配置缓存、投递去重/投递状态/消息更新追踪的回退(`delivery:*`、`delivery-state:*`、`msg:*`,仅在 D1 不可用时使用)、消息更新锁 (`msg:lock:*`) | +| 组件 | 职责 | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Cloudflare Worker** | HTTP 入口、签名验证、投递去重、事件解析、路由匹配、平台分发 | +| **Interactions Endpoint** | 验证 Ed25519 签名并处理 `/gh` 交互(斜杠命令、右键菜单、按钮、modal) | +| **KV** | Token 存储 (`token:{userId}`)、OAuth 状态 (`state:{hex}`)、管理员会话、分组级 secret、配置缓存、投递去重/投递状态/消息更新追踪的回退(`delivery:*`、`delivery-state:*`、`msg:*`,仅在 D1 不可用时使用)、消息更新锁 (`msg:lock:*`) | | **D1** | 路由/分组 (`d1_routes`/`d1_groups`)、发送日志 (`send_logs`)、审计日志 (`audit_logs`)、去重 (`dedup_keys`)、投递状态 (`delivery_state`)、消息追踪 (`message_tracking`)、Discord↔GitHub 绑定 (`discord_links`)、Telegram↔GitHub 绑定 (`telegram_links`),超大负载可选的 R2 存储 | ### 数据流 diff --git a/docs/zh/guide/storage.md b/docs/zh/guide/storage.md index 5683d74..7b4fe0c 100644 --- a/docs/zh/guide/storage.md +++ b/docs/zh/guide/storage.md @@ -29,17 +29,17 @@ KV 只保留缓存数据和短期/临时状态。高频写入(webhook 去重 D1 数据库(`DB` 绑定,数据库 `webhooker`)保存配置、投递日志和高频临时状态的权威数据源: -| 表 | 用途 | -| ------------------ | ---------------------------------------------------------------------- | -| `d1_groups` | 分组(权威配置,从旧版 KV `config:groups` 播种) | -| `d1_routes` | 每组分组的路由(权威配置,从旧版 KV `config:routes` 播种) | -| `send_logs` | 每次分发尝试一行(路由 id、事件、目标、ok/error、耗时、错误码、详情) | -| `audit_logs` | 每次管理员操作一行(登录/登出、分组/路由/成员/邀请变更) | -| `dedup_keys` | Webhook 投递去重(原子 `INSERT ... ON CONFLICT` UPSERT,键 + 过期时间) | +| 表 | 用途 | +| ------------------ | ----------------------------------------------------------------------------- | +| `d1_groups` | 分组(权威配置,从旧版 KV `config:groups` 播种) | +| `d1_routes` | 每组分组的路由(权威配置,从旧版 KV `config:routes` 播种) | +| `send_logs` | 每次分发尝试一行(路由 id、事件、目标、ok/error、耗时、错误码、详情) | +| `audit_logs` | 每次管理员操作一行(登录/登出、分组/路由/成员/邀请变更) | +| `dedup_keys` | Webhook 投递去重(原子 `INSERT ... ON CONFLICT` UPSERT,键 + 过期时间) | | `delivery_state` | 队列投递状态(`pending`/`processing`/`delivered`/`retrying`/`failed`/`dead`) | -| `message_tracking` | 原地更新用消息 id 追踪(`event_id` + `target_id` → `message_id`) | -| `discord_links` | 映射 `discord_user_id` → `github_user_id`,供 `/gh` Discord 命令使用 | -| `telegram_links` | 映射 `telegram_user_id` → `github_user_id`,供 `/gh` Telegram 命令使用 | +| `message_tracking` | 原地更新用消息 id 追踪(`event_id` + `target_id` → `message_id`) | +| `discord_links` | 映射 `discord_user_id` → `github_user_id`,供 `/gh` Discord 命令使用 | +| `telegram_links` | 映射 `telegram_user_id` → `github_user_id`,供 `/gh` Telegram 命令使用 | `audit_logs` 由定时任务在 `AUDIT_RETENTION_DAYS`(默认 90)后自动清理。`storage-prune` 任务会清理过期的 `dedup_keys`、超过 7 天的 `delivery_state` 行以及超过 30 天的 `message_tracking` 行。日志行字段说明见[日志](./logs)。 @@ -47,9 +47,9 @@ D1 数据库(`DB` 绑定,数据库 `webhooker`)保存配置、投递日志 R2(`PAYLOAD` 绑定,bucket `webhooker-payloads`)存储对队列消息或 KV 来说过大的 webhook 负载: -| 对象模式 | 用途 | 保留期 | -| ------------------------------- | ----------------------------------------------- | ------------ | -| `webhooks/YYYY/MM/DD/.json` | 暂存供队列消费者读取的超大负载 | 分发后删除 | +| 对象模式 | 用途 | 保留期 | +| --------------------------------- | ------------------------------ | ---------- | +| `webhooks/YYYY/MM/DD/.json` | 暂存供队列消费者读取的超大负载 | 分发后删除 | 当缺少 `PAYLOAD` 绑定时,超大负载回退到 KV 键 `queue:payload:{provider}:{groupId}:{id}`(1 小时 TTL)。 diff --git a/docs/zh/guide/tasks.md b/docs/zh/guide/tasks.md index 284cfba..a01372f 100644 --- a/docs/zh/guide/tasks.md +++ b/docs/zh/guide/tasks.md @@ -7,6 +7,6 @@ WebHooker 通过定时触发器(`*/5 * * * *`,每 5 分钟)运行四个维 | `discord-sync` | 注册 Discord 斜杠/右键菜单命令:按服务器即时注册,并全局注册(24h 去重,约 1 小时传播) | | `telegram-sync` | 调用 `setWebhook` 指向 `{BASE_URL}/telegram/webhook`(设置了 `TELEGRAM_WEBHOOK_SECRET` 时作为 `secret_token` 传入) | | `audit-prune` | 删除早于 `AUDIT_RETENTION_DAYS`(默认 90)天的 `audit_logs` 记录 | -| `storage-prune` | 删除已过期的 `dedup_keys` 记录、超过 7 天的 `delivery_state` 记录和超过 30 天的 `message_tracking` 记录 | +| `storage-prune` | 删除已过期的 `dedup_keys` 记录、超过 7 天的 `delivery_state` 记录和超过 30 天的 `message_tracking` 记录 | 除任务用到的密钥(`DISCORD_TOKEN`、`DISCORD_APPLICATION_ID`、`TELEGRAM_TOKEN`、`BASE_URL`、`AUDIT_RETENTION_DAYS`)外无需其他配置。 diff --git a/server/lib/config.ts b/server/lib/config.ts index 85f1577..9c6fc30 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -85,4 +85,4 @@ export async function loadConfig(env: Env): Promise { configCache = { config, expiresAt: Date.now() + CONFIG_CACHE_TTL }; return config; -} \ No newline at end of file +} diff --git a/server/lib/lib/idempotency.ts b/server/lib/lib/idempotency.ts index 41debba..c76cb87 100644 --- a/server/lib/lib/idempotency.ts +++ b/server/lib/lib/idempotency.ts @@ -66,4 +66,4 @@ export function deliveryKey( deliveryId: string, ): string { return `delivery:${provider}:${groupId ?? "global"}:${deliveryId}`; -} \ No newline at end of file +} diff --git a/server/lib/lib/message-tracker.ts b/server/lib/lib/message-tracker.ts index 765fa8d..ac282f1 100644 --- a/server/lib/lib/message-tracker.ts +++ b/server/lib/lib/message-tracker.ts @@ -56,4 +56,4 @@ export function d1MessageTracker(db: D1Database): MessageTracker { export function messageTracker(db: D1Database, kv: KVNamespace): MessageTracker { return canUseD1(db) ? d1MessageTracker(db) : kvMessageTracker(kv); -} \ No newline at end of file +} diff --git a/server/lib/lib/send-log-batch.ts b/server/lib/lib/send-log-batch.ts index 4203616..9cc344d 100644 --- a/server/lib/lib/send-log-batch.ts +++ b/server/lib/lib/send-log-batch.ts @@ -1,10 +1,7 @@ import type { SendRecord } from "./send-log"; import { log } from "./log"; -export async function recordSendBatch( - db: D1Database, - records: SendRecord[], -): Promise { +export async function recordSendBatch(db: D1Database, records: SendRecord[]): Promise { if (records.length === 0) return; try { const stmts = records.map((r) => @@ -38,4 +35,4 @@ export async function recordSendBatch( } catch (err) { log.warn({ err, count: records.length }, "Failed to record send log batch"); } -} \ No newline at end of file +} diff --git a/server/lib/queue/delivery.ts b/server/lib/queue/delivery.ts index ae856f2..0a20c6e 100644 --- a/server/lib/queue/delivery.ts +++ b/server/lib/queue/delivery.ts @@ -3,7 +3,7 @@ import { r2PayloadStore } from "../storage/payload"; import { canUseD1 } from "../storage/d1"; export type DeliveryStatus = - | "pending" | "processing" | "delivered" | "retrying" | "failed" | "dead"; + "pending" | "processing" | "delivered" | "retrying" | "failed" | "dead"; export interface DeliveryMessage { deliveryId: string; diff --git a/server/lib/storage/config-store.ts b/server/lib/storage/config-store.ts index 985e3d1..61e9da4 100644 --- a/server/lib/storage/config-store.ts +++ b/server/lib/storage/config-store.ts @@ -239,4 +239,4 @@ export function d1ConfigStore(db: D1Database, kv: KVNamespace): ConfigStore { groupsCache = null; }, }; -} \ No newline at end of file +} diff --git a/server/lib/storage/d1.ts b/server/lib/storage/d1.ts index 47094de..6f60855 100644 --- a/server/lib/storage/d1.ts +++ b/server/lib/storage/d1.ts @@ -7,8 +7,5 @@ * while production eagerly routes through D1. */ export function canUseD1(db: D1Database | undefined | null): boolean { - return ( - typeof db?.prepare === "function" && - typeof (db as D1Database).batch === "function" - ); -} \ No newline at end of file + return typeof db?.prepare === "function" && typeof (db as D1Database).batch === "function"; +} diff --git a/server/lib/storage/payload.ts b/server/lib/storage/payload.ts index 5630d4b..4dc1dce 100644 --- a/server/lib/storage/payload.ts +++ b/server/lib/storage/payload.ts @@ -41,9 +41,7 @@ export function r2PayloadStore(env: Env): PayloadStore { return { async put(payload: string, ttl?: number): Promise { const key = generateKey(); - const expires = new Date( - Date.now() + (ttl ?? DEFAULT_TTL_SECONDS) * 1000, - ); + const expires = new Date(Date.now() + (ttl ?? DEFAULT_TTL_SECONDS) * 1000); await bucket.put(key, payload, { httpMetadata: { contentType: "application/json" }, customMetadata: { expires: expires.toISOString() }, @@ -61,4 +59,4 @@ export function r2PayloadStore(env: Env): PayloadStore { await bucket.delete(key); }, }; -} \ No newline at end of file +} diff --git a/server/tasks/storage-prune.ts b/server/tasks/storage-prune.ts index eb0bc34..d5381c8 100644 --- a/server/tasks/storage-prune.ts +++ b/server/tasks/storage-prune.ts @@ -24,9 +24,7 @@ export default defineTask({ .bind(now - DELIVERY_STATE_RETENTION_MS) .run(); removed += delivery.meta?.changes ?? 0; - const messages = await env.DB.prepare( - "DELETE FROM message_tracking WHERE updated_at < ?", - ) + const messages = await env.DB.prepare("DELETE FROM message_tracking WHERE updated_at < ?") .bind(now - MESSAGE_TRACKING_RETENTION_MS) .run(); removed += messages.meta?.changes ?? 0; @@ -37,4 +35,4 @@ export default defineTask({ return { result: "error" }; } }, -}); \ No newline at end of file +}); diff --git a/tests/config-store.test.ts b/tests/config-store.test.ts index 0ac8604..04bf7d5 100644 --- a/tests/config-store.test.ts +++ b/tests/config-store.test.ts @@ -171,4 +171,4 @@ describe("d1ConfigStore", () => { const second = await cfg.loadRoutes(); expect(second).toHaveLength(2); }); -}); \ No newline at end of file +}); diff --git a/tests/d1-stores.test.ts b/tests/d1-stores.test.ts index 0939de2..c771533 100644 --- a/tests/d1-stores.test.ts +++ b/tests/d1-stores.test.ts @@ -9,10 +9,7 @@ import { kvMessageTracker, messageTracker, } from "../server/lib/lib/message-tracker"; -import { - getDeliveryState, - setDeliveryState, -} from "../server/lib/queue/delivery"; +import { getDeliveryState, setDeliveryState } from "../server/lib/queue/delivery"; import { canUseD1 } from "../server/lib/storage/d1"; import type { Env } from "../server/lib/types"; @@ -45,10 +42,7 @@ function createMockD1(): { const messageRows = new Map(); const deliveryRows = new Map(); - const run = ( - sql: string, - args: unknown[], - ): { success: boolean; meta: { changes: number } } => { + const run = (sql: string, args: unknown[]): { success: boolean; meta: { changes: number } } => { if (sql.includes("INSERT INTO dedup_keys")) { const [key, claimedAt, expiresAt] = args as [string, number, number]; const existing = dedupKeys.get(key); @@ -63,12 +57,7 @@ function createMockD1(): { return { success: true, meta: { changes: 0 } }; } if (sql.includes("INSERT INTO message_tracking")) { - const [eventId, targetId, messageId, updatedAt] = args as [ - string, - string, - string, - number, - ]; + const [eventId, targetId, messageId, updatedAt] = args as [string, string, string, number]; messageRows.set(`${eventId}\u0000${targetId}`, { messageId, updatedAt }); return { success: true, meta: { changes: 1 } }; } @@ -106,14 +95,18 @@ function createMockD1(): { }; const db = { - prepare: (sql: string): { + prepare: ( + sql: string, + ): { bind: (...args: unknown[]) => { run: () => Promise<{ success: boolean; meta: { changes: number } }>; all: () => Promise<{ results: unknown[] }>; first: () => Promise; }; } => ({ - bind: (...args: unknown[]): { + bind: ( + ...args: unknown[] + ): { run: () => Promise<{ success: boolean; meta: { changes: number } }>; all: () => Promise<{ results: unknown[] }>; first: () => Promise; @@ -196,9 +189,7 @@ describe("delivery state backed by D1", () => { it("returns null when no state exists", async () => { const { db } = createMockD1(); const env = envWith(db, createMockKV()); - await expect( - getDeliveryState(env, "delivery-state:github:g9:nope"), - ).resolves.toBeNull(); + await expect(getDeliveryState(env, "delivery-state:github:g9:nope")).resolves.toBeNull(); }); }); @@ -211,8 +202,16 @@ describe("factory fallback decision", () => { it("idempotencyStore falls back to KV semantics without batch", async () => { const db = { - prepare: (): { bind: () => { run: () => Promise<{ success: boolean }>; all: () => Promise<{ results: unknown[] }> } } => ({ - bind: (): { run: () => Promise<{ success: boolean }>; all: () => Promise<{ results: unknown[] }> } => ({ + prepare: (): { + bind: () => { + run: () => Promise<{ success: boolean }>; + all: () => Promise<{ results: unknown[] }>; + }; + } => ({ + bind: (): { + run: () => Promise<{ success: boolean }>; + all: () => Promise<{ results: unknown[] }>; + } => ({ run: async () => ({ success: true }), all: async () => ({ results: [] }), }), @@ -259,4 +258,4 @@ describe("delivery state falls back to KV without batch", () => { await expect(getDeliveryState(env, key)).resolves.toBe("delivered"); await expect(getDeliveryState(env, "delivery-state:github:global:nope")).resolves.toBeNull(); }); -}); \ No newline at end of file +}); diff --git a/tests/payload.test.ts b/tests/payload.test.ts index a74341a..2651609 100644 --- a/tests/payload.test.ts +++ b/tests/payload.test.ts @@ -65,4 +65,4 @@ describe("r2PayloadStore", () => { await expect(store.get("x")).rejects.toThrow("R2 binding is not configured"); await expect(store.delete("x")).rejects.toThrow("R2 binding is not configured"); }); -}); \ No newline at end of file +}); diff --git a/tests/send-log-batch.test.ts b/tests/send-log-batch.test.ts index 8104e27..69e4ca2 100644 --- a/tests/send-log-batch.test.ts +++ b/tests/send-log-batch.test.ts @@ -86,9 +86,7 @@ describe("recordSendBatch", () => { it("serializes detail and encodes ok/error", async () => { const { db, rows } = createMockDB(); - await recordSendBatch(db, [ - record({ detail: { a: 1 }, ok: false, error: "x", status: 500 }), - ]); + await recordSendBatch(db, [record({ detail: { a: 1 }, ok: false, error: "x", status: 500 })]); expect(rows[0].detail).toBe('{"a":1}'); expect(rows[0].ok).toBe(0); expect(rows[0].status).toBe(500); @@ -105,8 +103,6 @@ describe("recordSendBatch", () => { throw new Error("batch failed"); }, } as unknown as D1Database; - await expect( - recordSendBatch(db, [record()]), - ).resolves.toBeUndefined(); + await expect(recordSendBatch(db, [record()])).resolves.toBeUndefined(); }); -}); \ No newline at end of file +});