WebHooker/server/lib/storage/payload.ts
RhenCloud 25ebae4ae5
feat(storage): migrate config, dedup and delivery state to D1
- Move oversized queue payloads from KV to R2 (PAYLOAD binding, webhooks/YYYY/MM/DD/*.json, KV queue:payload:* fallback)
- Persist routes/groups to D1 (d1_routes/d1_groups) with memory -> KV -> D1 three-tier cache, seeded from legacy KV config keys
- Move webhook dedup (dedup_keys), delivery state (delivery_state) and message tracking (message_tracking) to D1 via canUseD1 probe with automatic KV fallback
- Batch send_logs inserts (recordSendBatch) and add group_id/ts index
- Add storage-prune scheduled task for expired dedup/state/tracking rows
- Add TTL to invite:group:{id} index and audit all ephemeral KV keys
- Add D1 indexes for the new tables
- Sync AGENTS.md, README.md/zh and docs/ (en/zh) with the new storage layout
2026-08-17 15:01:20 +08:00

64 lines
No EOL
1.8 KiB
TypeScript

import type { Env } from "../types";
const PAYLOAD_PREFIX = "webhooks/";
const DEFAULT_TTL_SECONDS = 3600;
export interface PayloadStore {
put(payload: string, ttl?: number): Promise<string>;
get(key: string): Promise<string | null>;
delete(key: string): Promise<void>;
}
function generateKey(): string {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
const hex = Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
const now = new Date();
const y = now.getUTCFullYear();
const m = String(now.getUTCMonth() + 1).padStart(2, "0");
const d = String(now.getUTCDate()).padStart(2, "0");
return `${PAYLOAD_PREFIX}${y}/${m}/${d}/${hex}.json`;
}
export function r2PayloadStore(env: Env): PayloadStore {
const bucket = env.PAYLOAD;
if (!bucket) {
return {
async put(_payload: string): Promise<string> {
throw new Error("R2 binding is not configured");
},
async get(): Promise<null> {
throw new Error("R2 binding is not configured");
},
async delete(): Promise<void> {
throw new Error("R2 binding is not configured");
},
};
}
return {
async put(payload: string, ttl?: number): Promise<string> {
const key = generateKey();
const expires = new Date(
Date.now() + (ttl ?? DEFAULT_TTL_SECONDS) * 1000,
);
await bucket.put(key, payload, {
httpMetadata: { contentType: "application/json" },
customMetadata: { expires: expires.toISOString() },
});
return key;
},
async get(key: string): Promise<string | null> {
const obj = await bucket.get(key);
if (!obj) return null;
return obj.text();
},
async delete(key: string): Promise<void> {
await bucket.delete(key);
},
};
}