mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-23 00:21:28 +00:00
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
This commit is contained in:
parent
2e1b0f022e
commit
25ebae4ae5
46 changed files with 1450 additions and 117 deletions
68
tests/payload.test.ts
Normal file
68
tests/payload.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { describe, it, expect } from "bun:test";
|
||||
import { r2PayloadStore, type PayloadStore } from "../server/lib/storage/payload";
|
||||
import type { Env } from "../server/lib/types";
|
||||
|
||||
interface FakeBucket {
|
||||
put: (key: string, value: string) => Promise<void>;
|
||||
get: (key: string) => Promise<{ text: () => Promise<string> } | null>;
|
||||
delete: (key: string) => Promise<void>;
|
||||
objects: Map<string, string>;
|
||||
}
|
||||
|
||||
function createBucket(): FakeBucket {
|
||||
const objects = new Map<string, string>();
|
||||
const bucket: FakeBucket = {
|
||||
objects,
|
||||
put: async (key, value) => {
|
||||
objects.set(key, value);
|
||||
},
|
||||
get: async (key) => {
|
||||
const v = objects.get(key);
|
||||
return v ? { text: async () => v } : null;
|
||||
},
|
||||
delete: async (key) => {
|
||||
objects.delete(key);
|
||||
},
|
||||
};
|
||||
return bucket;
|
||||
}
|
||||
|
||||
function envWith(bucket?: FakeBucket): Env {
|
||||
return {
|
||||
GITHUB_WEBHOOK_SECRET: "secret",
|
||||
KV: {} as KVNamespace,
|
||||
DB: {} as D1Database,
|
||||
PAYLOAD: bucket as unknown as R2Bucket,
|
||||
};
|
||||
}
|
||||
|
||||
describe("r2PayloadStore", () => {
|
||||
it("round-trips a payload through put/get/delete", async () => {
|
||||
const bucket = createBucket();
|
||||
const store: PayloadStore = r2PayloadStore(envWith(bucket));
|
||||
const key = await store.put('{"hello":"world"}');
|
||||
expect(key.startsWith("webhooks/")).toBe(true);
|
||||
expect(await store.get(key)).toBe('{"hello":"world"}');
|
||||
await store.delete(key);
|
||||
expect(await store.get(key)).toBeNull();
|
||||
});
|
||||
|
||||
it("generates unique keys per put", async () => {
|
||||
const store = r2PayloadStore(envWith(createBucket()));
|
||||
const a = await store.put("a");
|
||||
const b = await store.put("b");
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
it("returns null for a missing key", async () => {
|
||||
const store = r2PayloadStore(envWith(createBucket()));
|
||||
expect(await store.get("webhooks/nope.json")).toBeNull();
|
||||
});
|
||||
|
||||
it("throws when the R2 binding is not configured", async () => {
|
||||
const store = r2PayloadStore(envWith(undefined));
|
||||
await expect(store.put("x")).rejects.toThrow("R2 binding is not configured");
|
||||
await expect(store.get("x")).rejects.toThrow("R2 binding is not configured");
|
||||
await expect(store.delete("x")).rejects.toThrow("R2 binding is not configured");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue