mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
chore: auto-fix lint & formatting [skip ci]
This commit is contained in:
parent
9eeafb8c15
commit
7c37f5d8b6
19 changed files with 101 additions and 116 deletions
|
|
@ -18,11 +18,11 @@ GitHub / Gitea Webhook → Cloudflare Worker (Nuxt 4 / Nitro)
|
||||||
|
|
||||||
### Components
|
### Components
|
||||||
|
|
||||||
| Component | Role |
|
| Component | Role |
|
||||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Cloudflare Worker** | HTTP ingress, signature verification, delivery dedup, event parsing, route matching, platform dispatch |
|
| **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) |
|
| **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:*`) |
|
| **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 |
|
| **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
|
### Data Flow
|
||||||
|
|
|
||||||
|
|
@ -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)).
|
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 |
|
| Key Pattern | Value | TTL |
|
||||||
| ------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------ |
|
| ------------------------------------------ | ----------------------------------------------------------------------------- | ------------------ |
|
||||||
| `config:routes` | Route config cache (D1 `d1_routes` is authoritative) | 1 hour |
|
| `config:routes` | Route config cache (D1 `d1_routes` is authoritative) | 1 hour |
|
||||||
| `config:groups` | Group config cache (D1 `d1_groups` is authoritative) | 1 hour |
|
| `config:groups` | Group config cache (D1 `d1_groups` is authoritative) | 1 hour |
|
||||||
| `session:{id}` | Admin session `{ userId, login }` | 7 days |
|
| `session:{id}` | Admin session `{ userId, login }` | 7 days |
|
||||||
| `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × token expiry |
|
| `token:{userId}` | `{ userId, accessToken, expiresAt, refreshToken? }` | 0.9 × token expiry |
|
||||||
| `token-reverse:{sha256}` | User id for reverse lookup by token | 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 |
|
| `state:{hex}` | `{ redirectTo, expiresAt, discordUserId?, telegramUserId?, telegramChatId? }` | 600 seconds |
|
||||||
| `invite:{token}` | `{ groupId, role, expiresAt, createdBy, note? }` | 7 days |
|
| `invite:{token}` | `{ groupId, role, expiresAt, createdBy, note? }` | 7 days |
|
||||||
| `invite:group:{id}` | Token index per group (keeps invite listing consistent) | 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:{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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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:guild:{id}` | Guild id whose commands were registered (dedup) | Permanent |
|
||||||
| `cmd:registered:global` | Global command registration marker (dedup) | 1 day |
|
| `cmd:registered:global` | Global command registration marker (dedup) | 1 day |
|
||||||
| `config:discord-app-id` | Cached Discord application id | Permanent |
|
| `config:discord-app-id` | Cached Discord application id | Permanent |
|
||||||
| `i18n:{lang}` | Translation overrides merged on top of English | Permanent |
|
| `i18n:{lang}` | Translation overrides merged on top of English | Permanent |
|
||||||
|
|
||||||
## D1 Storage Layout
|
## D1 Storage Layout
|
||||||
|
|
||||||
The D1 database (`DB` binding, database `webhooker`) holds the source of truth for configuration, delivery logs and high-frequency ephemeral state:
|
The D1 database (`DB` binding, database `webhooker`) holds the source of truth for configuration, delivery logs and high-frequency ephemeral state:
|
||||||
|
|
||||||
| Table | Purpose |
|
| Table | Purpose |
|
||||||
| ------------------ | ---------------------------------------------------------------------------------------------------- |
|
| ------------------ | ---------------------------------------------------------------------------------------------- |
|
||||||
| `d1_groups` | Groups (authoritative config, seeded from legacy KV `config:groups`) |
|
| `d1_groups` | Groups (authoritative config, seeded from legacy KV `config:groups`) |
|
||||||
| `d1_routes` | Routes per group (authoritative config, seeded from legacy KV `config:routes`) |
|
| `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) |
|
| `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) |
|
| `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) |
|
| `dedup_keys` | Webhook delivery dedup (atomic `INSERT ... ON CONFLICT` UPSERT, key + expiry) |
|
||||||
| `delivery_state` | Queue delivery state (`pending`/`processing`/`delivered`/`retrying`/`failed`/`dead`) |
|
| `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`) |
|
| `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 |
|
| `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 |
|
| `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.
|
`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:
|
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/<uuid>.json` | Oversized payload parked for the queue consumer | deleted after dispatch |
|
| `webhooks/YYYY/MM/DD/<uuid>.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).
|
When the `PAYLOAD` binding is absent, oversized payloads fall back to the KV key `queue:payload:{provider}:{groupId}:{id}` (1 hour TTL).
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
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 |
|
| Task | Purpose |
|
||||||
| --------------- | ---------------------------------------------------------------------------------------------------------------- |
|
| --------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `discord-sync` | Registers the Discord slash/context-menu commands: per-guild (instant) and globally (24h dedup, ~1h propagation) |
|
| `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) |
|
| `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) |
|
| `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 |
|
| `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`).
|
There is nothing to configure beyond the secrets the tasks use (`DISCORD_TOKEN`, `DISCORD_APPLICATION_ID`, `TELEGRAM_TOKEN`, `BASE_URL`, `AUDIT_RETENTION_DAYS`).
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ GitHub / Gitea Webhook → Cloudflare Worker (Nuxt 4 / Nitro)
|
||||||
|
|
||||||
### 组件
|
### 组件
|
||||||
|
|
||||||
| 组件 | 职责 |
|
| 组件 | 职责 |
|
||||||
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Cloudflare Worker** | HTTP 入口、签名验证、投递去重、事件解析、路由匹配、平台分发 |
|
| **Cloudflare Worker** | HTTP 入口、签名验证、投递去重、事件解析、路由匹配、平台分发 |
|
||||||
| **Interactions Endpoint** | 验证 Ed25519 签名并处理 `/gh` 交互(斜杠命令、右键菜单、按钮、modal) |
|
| **Interactions Endpoint** | 验证 Ed25519 签名并处理 `/gh` 交互(斜杠命令、右键菜单、按钮、modal) |
|
||||||
| **KV** | Token 存储 (`token:{userId}`)、OAuth 状态 (`state:{hex}`)、管理员会话、分组级 secret、配置缓存、投递去重/投递状态/消息更新追踪的回退(`delivery:*`、`delivery-state:*`、`msg:*`,仅在 D1 不可用时使用)、消息更新锁 (`msg:lock:*`) |
|
| **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 存储 |
|
| **D1** | 路由/分组 (`d1_routes`/`d1_groups`)、发送日志 (`send_logs`)、审计日志 (`audit_logs`)、去重 (`dedup_keys`)、投递状态 (`delivery_state`)、消息追踪 (`message_tracking`)、Discord↔GitHub 绑定 (`discord_links`)、Telegram↔GitHub 绑定 (`telegram_links`),超大负载可选的 R2 存储 |
|
||||||
|
|
||||||
### 数据流
|
### 数据流
|
||||||
|
|
|
||||||
|
|
@ -29,17 +29,17 @@ KV 只保留缓存数据和短期/临时状态。高频写入(webhook 去重
|
||||||
|
|
||||||
D1 数据库(`DB` 绑定,数据库 `webhooker`)保存配置、投递日志和高频临时状态的权威数据源:
|
D1 数据库(`DB` 绑定,数据库 `webhooker`)保存配置、投递日志和高频临时状态的权威数据源:
|
||||||
|
|
||||||
| 表 | 用途 |
|
| 表 | 用途 |
|
||||||
| ------------------ | ---------------------------------------------------------------------- |
|
| ------------------ | ----------------------------------------------------------------------------- |
|
||||||
| `d1_groups` | 分组(权威配置,从旧版 KV `config:groups` 播种) |
|
| `d1_groups` | 分组(权威配置,从旧版 KV `config:groups` 播种) |
|
||||||
| `d1_routes` | 每组分组的路由(权威配置,从旧版 KV `config:routes` 播种) |
|
| `d1_routes` | 每组分组的路由(权威配置,从旧版 KV `config:routes` 播种) |
|
||||||
| `send_logs` | 每次分发尝试一行(路由 id、事件、目标、ok/error、耗时、错误码、详情) |
|
| `send_logs` | 每次分发尝试一行(路由 id、事件、目标、ok/error、耗时、错误码、详情) |
|
||||||
| `audit_logs` | 每次管理员操作一行(登录/登出、分组/路由/成员/邀请变更) |
|
| `audit_logs` | 每次管理员操作一行(登录/登出、分组/路由/成员/邀请变更) |
|
||||||
| `dedup_keys` | Webhook 投递去重(原子 `INSERT ... ON CONFLICT` UPSERT,键 + 过期时间) |
|
| `dedup_keys` | Webhook 投递去重(原子 `INSERT ... ON CONFLICT` UPSERT,键 + 过期时间) |
|
||||||
| `delivery_state` | 队列投递状态(`pending`/`processing`/`delivered`/`retrying`/`failed`/`dead`) |
|
| `delivery_state` | 队列投递状态(`pending`/`processing`/`delivered`/`retrying`/`failed`/`dead`) |
|
||||||
| `message_tracking` | 原地更新用消息 id 追踪(`event_id` + `target_id` → `message_id`) |
|
| `message_tracking` | 原地更新用消息 id 追踪(`event_id` + `target_id` → `message_id`) |
|
||||||
| `discord_links` | 映射 `discord_user_id` → `github_user_id`,供 `/gh` Discord 命令使用 |
|
| `discord_links` | 映射 `discord_user_id` → `github_user_id`,供 `/gh` Discord 命令使用 |
|
||||||
| `telegram_links` | 映射 `telegram_user_id` → `github_user_id`,供 `/gh` Telegram 命令使用 |
|
| `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)。
|
`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 负载:
|
R2(`PAYLOAD` 绑定,bucket `webhooker-payloads`)存储对队列消息或 KV 来说过大的 webhook 负载:
|
||||||
|
|
||||||
| 对象模式 | 用途 | 保留期 |
|
| 对象模式 | 用途 | 保留期 |
|
||||||
| ------------------------------- | ----------------------------------------------- | ------------ |
|
| --------------------------------- | ------------------------------ | ---------- |
|
||||||
| `webhooks/YYYY/MM/DD/<uuid>.json` | 暂存供队列消费者读取的超大负载 | 分发后删除 |
|
| `webhooks/YYYY/MM/DD/<uuid>.json` | 暂存供队列消费者读取的超大负载 | 分发后删除 |
|
||||||
|
|
||||||
当缺少 `PAYLOAD` 绑定时,超大负载回退到 KV 键 `queue:payload:{provider}:{groupId}:{id}`(1 小时 TTL)。
|
当缺少 `PAYLOAD` 绑定时,超大负载回退到 KV 键 `queue:payload:{provider}:{groupId}:{id}`(1 小时 TTL)。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,6 @@ WebHooker 通过定时触发器(`*/5 * * * *`,每 5 分钟)运行四个维
|
||||||
| `discord-sync` | 注册 Discord 斜杠/右键菜单命令:按服务器即时注册,并全局注册(24h 去重,约 1 小时传播) |
|
| `discord-sync` | 注册 Discord 斜杠/右键菜单命令:按服务器即时注册,并全局注册(24h 去重,约 1 小时传播) |
|
||||||
| `telegram-sync` | 调用 `setWebhook` 指向 `{BASE_URL}/telegram/webhook`(设置了 `TELEGRAM_WEBHOOK_SECRET` 时作为 `secret_token` 传入) |
|
| `telegram-sync` | 调用 `setWebhook` 指向 `{BASE_URL}/telegram/webhook`(设置了 `TELEGRAM_WEBHOOK_SECRET` 时作为 `secret_token` 传入) |
|
||||||
| `audit-prune` | 删除早于 `AUDIT_RETENTION_DAYS`(默认 90)天的 `audit_logs` 记录 |
|
| `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`)外无需其他配置。
|
除任务用到的密钥(`DISCORD_TOKEN`、`DISCORD_APPLICATION_ID`、`TELEGRAM_TOKEN`、`BASE_URL`、`AUDIT_RETENTION_DAYS`)外无需其他配置。
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
import type { SendRecord } from "./send-log";
|
import type { SendRecord } from "./send-log";
|
||||||
import { log } from "./log";
|
import { log } from "./log";
|
||||||
|
|
||||||
export async function recordSendBatch(
|
export async function recordSendBatch(db: D1Database, records: SendRecord[]): Promise<void> {
|
||||||
db: D1Database,
|
|
||||||
records: SendRecord[],
|
|
||||||
): Promise<void> {
|
|
||||||
if (records.length === 0) return;
|
if (records.length === 0) return;
|
||||||
try {
|
try {
|
||||||
const stmts = records.map((r) =>
|
const stmts = records.map((r) =>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { r2PayloadStore } from "../storage/payload";
|
||||||
import { canUseD1 } from "../storage/d1";
|
import { canUseD1 } from "../storage/d1";
|
||||||
|
|
||||||
export type DeliveryStatus =
|
export type DeliveryStatus =
|
||||||
| "pending" | "processing" | "delivered" | "retrying" | "failed" | "dead";
|
"pending" | "processing" | "delivered" | "retrying" | "failed" | "dead";
|
||||||
|
|
||||||
export interface DeliveryMessage {
|
export interface DeliveryMessage {
|
||||||
deliveryId: string;
|
deliveryId: string;
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,5 @@
|
||||||
* while production eagerly routes through D1.
|
* while production eagerly routes through D1.
|
||||||
*/
|
*/
|
||||||
export function canUseD1(db: D1Database | undefined | null): boolean {
|
export function canUseD1(db: D1Database | undefined | null): boolean {
|
||||||
return (
|
return typeof db?.prepare === "function" && typeof (db as D1Database).batch === "function";
|
||||||
typeof db?.prepare === "function" &&
|
|
||||||
typeof (db as D1Database).batch === "function"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
@ -41,9 +41,7 @@ export function r2PayloadStore(env: Env): PayloadStore {
|
||||||
return {
|
return {
|
||||||
async put(payload: string, ttl?: number): Promise<string> {
|
async put(payload: string, ttl?: number): Promise<string> {
|
||||||
const key = generateKey();
|
const key = generateKey();
|
||||||
const expires = new Date(
|
const expires = new Date(Date.now() + (ttl ?? DEFAULT_TTL_SECONDS) * 1000);
|
||||||
Date.now() + (ttl ?? DEFAULT_TTL_SECONDS) * 1000,
|
|
||||||
);
|
|
||||||
await bucket.put(key, payload, {
|
await bucket.put(key, payload, {
|
||||||
httpMetadata: { contentType: "application/json" },
|
httpMetadata: { contentType: "application/json" },
|
||||||
customMetadata: { expires: expires.toISOString() },
|
customMetadata: { expires: expires.toISOString() },
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,7 @@ export default defineTask({
|
||||||
.bind(now - DELIVERY_STATE_RETENTION_MS)
|
.bind(now - DELIVERY_STATE_RETENTION_MS)
|
||||||
.run();
|
.run();
|
||||||
removed += delivery.meta?.changes ?? 0;
|
removed += delivery.meta?.changes ?? 0;
|
||||||
const messages = await env.DB.prepare(
|
const messages = await env.DB.prepare("DELETE FROM message_tracking WHERE updated_at < ?")
|
||||||
"DELETE FROM message_tracking WHERE updated_at < ?",
|
|
||||||
)
|
|
||||||
.bind(now - MESSAGE_TRACKING_RETENTION_MS)
|
.bind(now - MESSAGE_TRACKING_RETENTION_MS)
|
||||||
.run();
|
.run();
|
||||||
removed += messages.meta?.changes ?? 0;
|
removed += messages.meta?.changes ?? 0;
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,7 @@ import {
|
||||||
kvMessageTracker,
|
kvMessageTracker,
|
||||||
messageTracker,
|
messageTracker,
|
||||||
} from "../server/lib/lib/message-tracker";
|
} from "../server/lib/lib/message-tracker";
|
||||||
import {
|
import { getDeliveryState, setDeliveryState } from "../server/lib/queue/delivery";
|
||||||
getDeliveryState,
|
|
||||||
setDeliveryState,
|
|
||||||
} from "../server/lib/queue/delivery";
|
|
||||||
import { canUseD1 } from "../server/lib/storage/d1";
|
import { canUseD1 } from "../server/lib/storage/d1";
|
||||||
import type { Env } from "../server/lib/types";
|
import type { Env } from "../server/lib/types";
|
||||||
|
|
||||||
|
|
@ -45,10 +42,7 @@ function createMockD1(): {
|
||||||
const messageRows = new Map<string, { messageId: string; updatedAt: number }>();
|
const messageRows = new Map<string, { messageId: string; updatedAt: number }>();
|
||||||
const deliveryRows = new Map<string, { status: string; updatedAt: number }>();
|
const deliveryRows = new Map<string, { status: string; updatedAt: number }>();
|
||||||
|
|
||||||
const run = (
|
const run = (sql: string, args: unknown[]): { success: boolean; meta: { changes: number } } => {
|
||||||
sql: string,
|
|
||||||
args: unknown[],
|
|
||||||
): { success: boolean; meta: { changes: number } } => {
|
|
||||||
if (sql.includes("INSERT INTO dedup_keys")) {
|
if (sql.includes("INSERT INTO dedup_keys")) {
|
||||||
const [key, claimedAt, expiresAt] = args as [string, number, number];
|
const [key, claimedAt, expiresAt] = args as [string, number, number];
|
||||||
const existing = dedupKeys.get(key);
|
const existing = dedupKeys.get(key);
|
||||||
|
|
@ -63,12 +57,7 @@ function createMockD1(): {
|
||||||
return { success: true, meta: { changes: 0 } };
|
return { success: true, meta: { changes: 0 } };
|
||||||
}
|
}
|
||||||
if (sql.includes("INSERT INTO message_tracking")) {
|
if (sql.includes("INSERT INTO message_tracking")) {
|
||||||
const [eventId, targetId, messageId, updatedAt] = args as [
|
const [eventId, targetId, messageId, updatedAt] = args as [string, string, string, number];
|
||||||
string,
|
|
||||||
string,
|
|
||||||
string,
|
|
||||||
number,
|
|
||||||
];
|
|
||||||
messageRows.set(`${eventId}\u0000${targetId}`, { messageId, updatedAt });
|
messageRows.set(`${eventId}\u0000${targetId}`, { messageId, updatedAt });
|
||||||
return { success: true, meta: { changes: 1 } };
|
return { success: true, meta: { changes: 1 } };
|
||||||
}
|
}
|
||||||
|
|
@ -106,14 +95,18 @@ function createMockD1(): {
|
||||||
};
|
};
|
||||||
|
|
||||||
const db = {
|
const db = {
|
||||||
prepare: (sql: string): {
|
prepare: (
|
||||||
|
sql: string,
|
||||||
|
): {
|
||||||
bind: (...args: unknown[]) => {
|
bind: (...args: unknown[]) => {
|
||||||
run: () => Promise<{ success: boolean; meta: { changes: number } }>;
|
run: () => Promise<{ success: boolean; meta: { changes: number } }>;
|
||||||
all: () => Promise<{ results: unknown[] }>;
|
all: () => Promise<{ results: unknown[] }>;
|
||||||
first: () => Promise<Row | null>;
|
first: () => Promise<Row | null>;
|
||||||
};
|
};
|
||||||
} => ({
|
} => ({
|
||||||
bind: (...args: unknown[]): {
|
bind: (
|
||||||
|
...args: unknown[]
|
||||||
|
): {
|
||||||
run: () => Promise<{ success: boolean; meta: { changes: number } }>;
|
run: () => Promise<{ success: boolean; meta: { changes: number } }>;
|
||||||
all: () => Promise<{ results: unknown[] }>;
|
all: () => Promise<{ results: unknown[] }>;
|
||||||
first: () => Promise<Row | null>;
|
first: () => Promise<Row | null>;
|
||||||
|
|
@ -196,9 +189,7 @@ describe("delivery state backed by D1", () => {
|
||||||
it("returns null when no state exists", async () => {
|
it("returns null when no state exists", async () => {
|
||||||
const { db } = createMockD1();
|
const { db } = createMockD1();
|
||||||
const env = envWith(db, createMockKV());
|
const env = envWith(db, createMockKV());
|
||||||
await expect(
|
await expect(getDeliveryState(env, "delivery-state:github:g9:nope")).resolves.toBeNull();
|
||||||
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 () => {
|
it("idempotencyStore falls back to KV semantics without batch", async () => {
|
||||||
const db = {
|
const db = {
|
||||||
prepare: (): { 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[] }>;
|
||||||
|
};
|
||||||
|
} => ({
|
||||||
|
bind: (): {
|
||||||
|
run: () => Promise<{ success: boolean }>;
|
||||||
|
all: () => Promise<{ results: unknown[] }>;
|
||||||
|
} => ({
|
||||||
run: async () => ({ success: true }),
|
run: async () => ({ success: true }),
|
||||||
all: async () => ({ results: [] }),
|
all: async () => ({ results: [] }),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,7 @@ describe("recordSendBatch", () => {
|
||||||
|
|
||||||
it("serializes detail and encodes ok/error", async () => {
|
it("serializes detail and encodes ok/error", async () => {
|
||||||
const { db, rows } = createMockDB();
|
const { db, rows } = createMockDB();
|
||||||
await recordSendBatch(db, [
|
await recordSendBatch(db, [record({ detail: { a: 1 }, ok: false, error: "x", status: 500 })]);
|
||||||
record({ detail: { a: 1 }, ok: false, error: "x", status: 500 }),
|
|
||||||
]);
|
|
||||||
expect(rows[0].detail).toBe('{"a":1}');
|
expect(rows[0].detail).toBe('{"a":1}');
|
||||||
expect(rows[0].ok).toBe(0);
|
expect(rows[0].ok).toBe(0);
|
||||||
expect(rows[0].status).toBe(500);
|
expect(rows[0].status).toBe(500);
|
||||||
|
|
@ -105,8 +103,6 @@ describe("recordSendBatch", () => {
|
||||||
throw new Error("batch failed");
|
throw new Error("batch failed");
|
||||||
},
|
},
|
||||||
} as unknown as D1Database;
|
} as unknown as D1Database;
|
||||||
await expect(
|
await expect(recordSendBatch(db, [record()])).resolves.toBeUndefined();
|
||||||
recordSendBatch(db, [record()]),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue