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 <[email protected]>
This commit is contained in:
1 parent
a0eaebe749
commit
2b3f00f7e3
22 files changed
+616
-539
No files matched your search
+10
-20
@@ -1,11 +1,5 @@
|
||||
import { EmailParser } from "../utils/email-parser";
|
||||
import {
|
||||
AttachmentData,
|
||||
EmailMetadata,
|
||||
Env,
|
||||
FeedConfig,
|
||||
FeedMetadata,
|
||||
} from "../types";
|
||||
import { AttachmentData, EmailMetadata, Env, FeedConfig } from "../types";
|
||||
import { notifySubscribers } from "../utils/websub";
|
||||
import { bumpCounters } from "../utils/stats";
|
||||
import {
|
||||
@@ -14,6 +8,7 @@ import {
|
||||
} from "../utils/favicon-fetcher";
|
||||
import { parseOneClickUnsubscribe } from "../utils/unsubscribe";
|
||||
import { getAttachmentBucket } from "../utils/attachments";
|
||||
import { FeedRepository } from "../domain/feed-repository";
|
||||
import { logger } from "./logger";
|
||||
import { FEED_MAX_BYTES } from "../config/constants";
|
||||
|
||||
@@ -110,10 +105,7 @@ export async function validateEmail(
|
||||
};
|
||||
}
|
||||
|
||||
const feedConfig = (await env.EMAIL_STORAGE.get(
|
||||
`feed:${feedId}:config`,
|
||||
"json",
|
||||
)) as FeedConfig | null;
|
||||
const feedConfig = await FeedRepository.from(env).getConfig(feedId);
|
||||
if (!feedConfig) {
|
||||
logger.error("Feed not found", { feedId });
|
||||
return {
|
||||
@@ -188,12 +180,12 @@ export async function storeEmail(
|
||||
...(storedAttachments.length > 0 ? { attachments: storedAttachments } : {}),
|
||||
};
|
||||
|
||||
const emailKey = `feed:${feedId}:${Date.now()}`;
|
||||
const feedMetadataKey = `feed:${feedId}:metadata`;
|
||||
const repo = FeedRepository.from(env);
|
||||
const emailKey = repo.newEmailKey(feedId);
|
||||
|
||||
const [, rawMetadata] = await Promise.all([
|
||||
env.EMAIL_STORAGE.put(emailKey, JSON.stringify(emailData)),
|
||||
env.EMAIL_STORAGE.get(feedMetadataKey, "json"),
|
||||
repo.putEmail(emailKey, emailData),
|
||||
repo.getMetadata(feedId),
|
||||
]);
|
||||
|
||||
// Note: KV has no atomic compare-and-swap. Concurrent invocations for the
|
||||
@@ -202,9 +194,7 @@ export async function storeEmail(
|
||||
// KV's eventual-consistency model.
|
||||
// TODO: Migrate feed metadata writes to Cloudflare Durable Objects to serialise
|
||||
// concurrent writes and eliminate this race condition.
|
||||
const feedMetadata = ((rawMetadata as FeedMetadata | null) || {
|
||||
emails: [],
|
||||
}) as FeedMetadata;
|
||||
const feedMetadata = rawMetadata || { emails: [] };
|
||||
|
||||
const maxBytes =
|
||||
parseInt(env.FEED_MAX_SIZE_BYTES ?? "", 10) || FEED_MAX_BYTES;
|
||||
@@ -260,8 +250,8 @@ export async function storeEmail(
|
||||
: [];
|
||||
|
||||
await Promise.all([
|
||||
env.EMAIL_STORAGE.put(feedMetadataKey, JSON.stringify(feedMetadata)),
|
||||
...toDelete.map((e) => env.EMAIL_STORAGE.delete(e.key)),
|
||||
repo.putMetadata(feedId, feedMetadata),
|
||||
...toDelete.map((e) => repo.deleteEmail(e.key)),
|
||||
...r2Deletions,
|
||||
]);
|
||||
|
||||
|
||||
Reference in new issue
Block a user