WebHooker/server/lib/lib/message-tracker.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

59 lines
No EOL
2.2 KiB
TypeScript

import { canUseD1 } from "../storage/d1";
export interface MessageTracker {
get(eventId: string, targetId: string): Promise<string | null>;
set(eventId: string, targetId: string, messageId: string): Promise<void>;
delete(eventId: string, targetId: string): Promise<void>;
}
const MESSAGE_KEY_TTL_SECONDS = 86400;
export function kvMessageTracker(kv: KVNamespace): MessageTracker {
const key = (eventId: string, targetId: string): string => `msg:${eventId}:${targetId}`;
return {
async get(eventId: string, targetId: string): Promise<string | null> {
return kv.get(key(eventId, targetId));
},
async set(eventId: string, targetId: string, messageId: string): Promise<void> {
await kv.put(key(eventId, targetId), messageId, {
expirationTtl: MESSAGE_KEY_TTL_SECONDS,
});
},
async delete(eventId: string, targetId: string): Promise<void> {
await kv.delete(key(eventId, targetId));
},
};
}
export function d1MessageTracker(db: D1Database): MessageTracker {
return {
async get(eventId: string, targetId: string): Promise<string | null> {
const row = await db
.prepare("SELECT message_id FROM message_tracking WHERE event_id = ? AND target_id = ?")
.bind(eventId, targetId)
.first<{ message_id: string }>();
return row?.message_id ?? null;
},
async set(eventId: string, targetId: string, messageId: string): Promise<void> {
await db
.prepare(
`INSERT INTO message_tracking (event_id, target_id, message_id, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(event_id, target_id) DO UPDATE SET
message_id = excluded.message_id, updated_at = excluded.updated_at`,
)
.bind(eventId, targetId, messageId, Date.now())
.run();
},
async delete(eventId: string, targetId: string): Promise<void> {
await db
.prepare("DELETE FROM message_tracking WHERE event_id = ? AND target_id = ?")
.bind(eventId, targetId)
.run();
},
};
}
export function messageTracker(db: D1Database, kv: KVNamespace): MessageTracker {
return canUseD1(db) ? d1MessageTracker(db) : kvMessageTracker(kv);
}