Skip to content

Commit be530ab

Browse files
authored
fix(feeds): enforce freshness window (#52)
1 parent 90fffb1 commit be530ab

2 files changed

Lines changed: 44 additions & 0 deletions

File tree

plugins/feed-discovery/src/discovery.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export async function discoverFeeds(query: FeedDiscoveryQuery, options: { provid
1515
q: query.q.trim(),
1616
type: query.type ?? "all",
1717
limit: clampLimit(query.limit),
18+
freshnessDays: normalizeFreshnessDays(query.freshnessDays),
1819
includeUnvalidated: query.includeUnvalidated ?? false,
1920
includeDeadFeeds: query.includeDeadFeeds ?? false
2021
};
@@ -54,6 +55,7 @@ export async function discoverFeeds(query: FeedDiscoveryQuery, options: { provid
5455
const validated = await validateCandidates(candidates.slice(0, Math.max((resolvedQuery.limit ?? 25) * 2, 25)), resolvedQuery, config);
5556
const scored = validated
5657
.filter((feed) => !resolvedQuery.type || resolvedQuery.type === "all" || feed.kind === resolvedQuery.type)
58+
.filter((feed) => isWithinFreshnessWindow(feed.lastPublishedAt, resolvedQuery.freshnessDays))
5759
.map((feed) => scoreFeed(feed, resolvedQuery));
5860
const results = dedupeFeeds(scored).slice(0, resolvedQuery.limit ?? 25);
5961

@@ -111,6 +113,23 @@ function clampLimit(limit: number | undefined) {
111113
return Math.min(Math.max(Math.trunc(limit), 1), 100);
112114
}
113115

116+
function normalizeFreshnessDays(freshnessDays: number | undefined) {
117+
return typeof freshnessDays === "number" && Number.isFinite(freshnessDays) && freshnessDays > 0
118+
? freshnessDays
119+
: undefined;
120+
}
121+
122+
function isWithinFreshnessWindow(lastPublishedAt: string | undefined, freshnessDays: number | undefined) {
123+
if (freshnessDays === undefined) {
124+
return true;
125+
}
126+
if (!lastPublishedAt) {
127+
return false;
128+
}
129+
const ageMs = Date.now() - Date.parse(lastPublishedAt);
130+
return Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= freshnessDays * 86_400_000;
131+
}
132+
114133
function safeCanonical(url: string) {
115134
try {
116135
return canonicalizeUrl(url);

plugins/feed-discovery/src/feed-discovery.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,29 @@ describe("discovery orchestration", () => {
100100
expect(response.providerErrors).toEqual([{ provider: "broken-provider", error: "provider unavailable" }]);
101101
expect(response.results).toHaveLength(1);
102102
});
103+
104+
it("enforces the requested freshness window", async () => {
105+
const provider: FeedDiscoveryProvider = {
106+
id: "freshness-provider",
107+
name: "Freshness",
108+
enabledByDefault: true,
109+
requiresApiKey: false,
110+
async search() {
111+
return [
112+
{ ...baseFeed, feedUrl: "https://example.com/fresh.xml", lastPublishedAt: new Date(Date.now() - 5 * 86_400_000).toISOString() },
113+
{ ...baseFeed, feedUrl: "https://example.com/stale.xml", lastPublishedAt: new Date(Date.now() - 60 * 86_400_000).toISOString() },
114+
{ ...baseFeed, feedUrl: "https://example.com/unknown.xml", lastPublishedAt: undefined },
115+
{ ...baseFeed, feedUrl: "https://example.com/invalid.xml", lastPublishedAt: "not-a-date" },
116+
{ ...baseFeed, feedUrl: "https://example.com/future.xml", lastPublishedAt: new Date(Date.now() + 86_400_000).toISOString() }
117+
];
118+
}
119+
};
120+
121+
const response = await discoverFeeds(
122+
{ q: "microsaas", freshnessDays: 30, includeUnvalidated: true },
123+
{ providers: [provider] }
124+
);
125+
126+
expect(response.results.map((feed) => feed.feedUrl)).toEqual(["https://example.com/fresh.xml"]);
127+
});
103128
});

0 commit comments

Comments
 (0)