refactor(domain): introduce FeedRepository as the single KV access layer

Centralise the KV key schema and all get/put access behind a FeedRepository
class under src/domain/. Every feed/email/list/icon/websub/counter key was
previously inlined across ~12 modules with two divergent storeEmail and
addFeedToList implementations; the dead src/utils/storage.ts write path is
removed and the email key convention unified on feed:<id>:<ts>.

Behaviour-preserving: existing tests pass unchanged in logic, plus a new
feed-repository.test.ts covering CRUD, key builders, list ops and counters.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-23 23:56:44 +02:00
parent a0eaebe749
commit 2b3f00f7e3
22 changed files with 616 additions and 539 deletions
+5 -7
View File
@@ -13,11 +13,8 @@ import { apiApp } from "./routes/api";
import { handleCloudflareEmail } from "./lib/cloudflare-email";
import { Env } from "./types";
import { logger } from "./lib/logger";
import {
listAllFeeds,
purgeExpiredFeeds,
removeFeedsFromListBulk,
} from "./routes/admin/helpers";
import { FeedRepository } from "./domain/feed-repository";
import { purgeExpiredFeeds } from "./routes/admin/helpers";
import {
bumpCounters,
scanR2Usage,
@@ -201,7 +198,8 @@ export default {
},
async scheduled(_event: ScheduledEvent, env: Env, _ctx: ExecutionContext) {
const attachmentBucket = getAttachmentBucket(env);
const feeds = await listAllFeeds(env.EMAIL_STORAGE);
const repo = FeedRepository.from(env);
const feeds = await repo.listFeeds();
const now = Date.now();
const expiredIds = feeds
.filter((f) => f.expires_at !== undefined && f.expires_at <= now)
@@ -211,7 +209,7 @@ export default {
await purgeExpiredFeeds(env.EMAIL_STORAGE, feedId, attachmentBucket);
}
if (expiredIds.length > 0) {
await removeFeedsFromListBulk(env.EMAIL_STORAGE, expiredIds);
await repo.removeFromListBulk(expiredIds);
await bumpCounters(env.EMAIL_STORAGE, {
feeds_deleted: expiredIds.length,
});