From b347f2f625ded58372fbd1035ea37f2cf6be79ab Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Sun, 24 May 2026 00:24:43 +0200 Subject: [PATCH] refactor(domain): extract the KV key schema into feed-keys.ts Move the feed:/icon:/websub: key builders out of FeedRepository into a pure feed-keys module so the wire format lives in one place, shared by the repositories to come. Strings are byte-identical; behaviour unchanged. Co-Authored-By: Claude Opus 4.7 --- src/domain/feed-keys.ts | 40 +++++++++++++++++++++++++++++++++++ src/domain/feed-repository.ts | 29 ++++++++++++------------- 2 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 src/domain/feed-keys.ts diff --git a/src/domain/feed-keys.ts b/src/domain/feed-keys.ts new file mode 100644 index 0000000..77dff4d --- /dev/null +++ b/src/domain/feed-keys.ts @@ -0,0 +1,40 @@ +import { FEEDS_LIST_KEY, STATS_KEY } from "../config/constants"; + +/** + * The KV key schema, in one pure place. Every repository builds its keys here so + * the wire format lives in a single module — never inline a `feed:`/`icon:`/ + * `websub:` string elsewhere. Strings are byte-identical to the original schema; + * changing them would require migrating live KV data. + */ + +const WEBSUB_PREFIX = "websub:subs:"; + +export const feedKeys = { + config: (feedId: string): string => `feed:${feedId}:config`, + metadata: (feedId: string): string => `feed:${feedId}:metadata`, + + /** Prefix covering every key owned by a feed (config, metadata, emails). */ + feedPrefix: (feedId: string): string => `feed:${feedId}:`, + + /** Mint a fresh, time-ordered email key. Call once and reuse the result. */ + newEmail: (feedId: string): string => `feed:${feedId}:${Date.now()}`, + + /** KV key for a domain's cached favicon (shared across feeds). */ + icon: (domain: string): string => `icon:${domain}`, + + websub: (feedId: string): string => `${WEBSUB_PREFIX}${feedId}`, + + /** Prefix matching every per-feed WebSub subscription key. */ + websubPrefix: (): string => WEBSUB_PREFIX, + + /** True when `key` is an email entry (not the feed's config/metadata key). */ + isEmail: (feedId: string, key: string): boolean => { + const suffix = key.slice(feedKeys.feedPrefix(feedId).length); + return suffix !== "config" && suffix !== "metadata"; + }, + + /** Recover the feed id embedded in an email key (`feed::`). */ + feedIdFromEmail: (key: string): string => key.split(":")[1], +} as const; + +export { FEEDS_LIST_KEY, STATS_KEY }; diff --git a/src/domain/feed-repository.ts b/src/domain/feed-repository.ts index a80a8cc..7c74baa 100644 --- a/src/domain/feed-repository.ts +++ b/src/domain/feed-repository.ts @@ -9,14 +9,14 @@ import { WebSubSubscription, } from "../types"; import { FEEDS_LIST_KEY, STATS_KEY } from "../config/constants"; +import { feedKeys } from "./feed-keys"; import { logger } from "../lib/logger"; -const WEBSUB_PREFIX = "websub:subs:"; - /** - * Single source of truth for the KV key schema and all KV access. No other - * module should build a `feed:`/`feeds:list`/`websub:`/`icon:`/`stats:counters` - * key string — go through a repository method instead. + * Single source of truth for KV access to the Feed aggregate. The key schema + * itself lives in `feed-keys.ts`; this repository owns the get/put operations. + * No other module should build a `feed:`/`feeds:list`/`websub:`/`icon:`/ + * `stats:counters` key string — go through `feed-keys` or a repository method. * * Wraps one `KVNamespace`; construct per request via `FeedRepository.from(env)`. */ @@ -27,44 +27,43 @@ export class FeedRepository { return new FeedRepository(env.EMAIL_STORAGE); } - // ── Key schema ──────────────────────────────────────────────────────────── + // ── Key schema (delegates to feed-keys) ─────────────────────────────────── private configKey(feedId: string): string { - return `feed:${feedId}:config`; + return feedKeys.config(feedId); } private metadataKey(feedId: string): string { - return `feed:${feedId}:metadata`; + return feedKeys.metadata(feedId); } /** KV key for a domain's cached favicon (shared across feeds). */ iconKey(domain: string): string { - return `icon:${domain}`; + return feedKeys.icon(domain); } private websubKey(feedId: string): string { - return `${WEBSUB_PREFIX}${feedId}`; + return feedKeys.websub(feedId); } /** Prefix covering every key owned by a feed (config, metadata, emails). */ feedKeyPrefix(feedId: string): string { - return `feed:${feedId}:`; + return feedKeys.feedPrefix(feedId); } /** Mint a fresh, time-ordered email key. Call once and reuse the result. */ newEmailKey(feedId: string): string { - return `feed:${feedId}:${Date.now()}`; + return feedKeys.newEmail(feedId); } /** True when `key` is an email entry (not the feed's config/metadata key). */ isEmailKey(feedId: string, key: string): boolean { - const suffix = key.slice(this.feedKeyPrefix(feedId).length); - return suffix !== "config" && suffix !== "metadata"; + return feedKeys.isEmail(feedId, key); } /** Recover the feed id embedded in an email key (`feed::`). */ feedIdFromEmailKey(key: string): string { - return key.split(":")[1]; + return feedKeys.feedIdFromEmail(key); } // ── Feed config ───────────────────────────────────────────────────────────