Light "collect + dispatch" variant: the Feed aggregate records FeedEvents (FeedCreated, EmailIngested) on the mutations that have consequences, exposed via pullEvents(). A new application dispatcher (feed-events.applyFeedEvents) maps those events to their side effects — counters (awaited) plus WebSub pings and favicon fetches handed to a BackgroundScheduler. This removes the inline, scattered side effects from the ingest hot path (email-processor) and from createFeedRecord; the aggregate is now the source of truth for "what happened". Side effects with no aggregate mutation (rejected email, feed deletion bypassing the aggregate, bulk admin ops, the cron, unsubscribes-sent) stay imperative by design — there is no aggregate event for them to ride on. BackgroundScheduler type moved to infrastructure/worker.ts (shared). CLAUDE.md updated. 355 tests pass (+4 event tests); tsc --noEmit clean. Co-Authored-By: Claude Opus 4.7 <[email protected]>
43 lines
1.5 KiB
TypeScript
43 lines
1.5 KiB
TypeScript
import { Env } from "../types";
|
|
import { FeedEvent } from "../domain/events";
|
|
import { FeedId } from "../domain/value-objects/feed-id";
|
|
import { BackgroundScheduler } from "../infrastructure/worker";
|
|
import { bumpCounters } from "./stats";
|
|
import { notifySubscribers } from "../infrastructure/websub";
|
|
import { cacheFaviconForDomain } from "../infrastructure/favicon-fetcher";
|
|
|
|
/**
|
|
* Apply the side effects of a feed's domain events — the single place that maps
|
|
* "what happened" (FeedCreated, EmailIngested) to its consequences. Counter
|
|
* writes are awaited (they must land); WebSub pings and favicon fetches are
|
|
* handed to the caller's background scheduler (`ctx.waitUntil` at the edge, a
|
|
* no-op when none is available).
|
|
*/
|
|
export async function applyFeedEvents(
|
|
feedId: FeedId,
|
|
events: FeedEvent[],
|
|
env: Env,
|
|
schedule: BackgroundScheduler,
|
|
): Promise<void> {
|
|
for (const event of events) {
|
|
switch (event.type) {
|
|
case "FeedCreated":
|
|
await bumpCounters(env.EMAIL_STORAGE, {
|
|
feeds_created: 1,
|
|
last_feed_created_at: new Date().toISOString(),
|
|
});
|
|
break;
|
|
case "EmailIngested":
|
|
await bumpCounters(env.EMAIL_STORAGE, {
|
|
emails_received: 1,
|
|
last_email_at: new Date().toISOString(),
|
|
});
|
|
schedule(notifySubscribers(feedId.value, env));
|
|
if (event.iconDomain) {
|
|
schedule(cacheFaviconForDomain(event.iconDomain, env));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|