From 1ec40d8e8fc27dedf0e47a059e9e8a382c644908 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 29 Oct 2026 05:48:22 +0000 Subject: [PATCH] Rivers hide future-dated entries until their date (0.2.4) /podcasts opened on episodes dated 2026-12-02..12-23: shows that schedule episodes ahead sat above everything actually out. latestItems and itemsForTopic now stop at now + one day, the same slack publishedTimes gives a feed's own dates. The entries stay stored and on their feed page. Co-Authored-By: Claude Opus 5.5 --- apps/web/package.json | 2 +- package.json | 2 +- packages/db/src/queries.js | 27 ++++++++++++++++++++----- packages/db/test/category-river.test.js | 11 ++++++++++ 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 7d0e6fb..98d1c7f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@rssamplifier/web", - "version": "0.2.3", + "version": "0.2.4", "private": true, "type": "module", "scripts": { diff --git a/package.json b/package.json index 5f41b1f..8f58b90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rssamplifier.com", - "version": "0.2.3", + "version": "0.2.4", "private": true, "type": "module", "description": "Agent-first public RSS directory. Submit a URL, a list or an OPML file; every blog gets its own page.", diff --git a/packages/db/src/queries.js b/packages/db/src/queries.js index f88c63a..2d6093c 100644 --- a/packages/db/src/queries.js +++ b/packages/db/src/queries.js @@ -1363,6 +1363,22 @@ const CATEGORY_RIVER_FEEDS = 400; */ const CATEGORY_RIVER_DAYS = 365; +/** + * How far ahead of now a river will show an entry: a day, the same slack + * `publishedTimes` gives a feed's own dates for clock skew. + * + * Without it a show that schedules its next eight episodes, or a feed that + * stamps every entry with next year, sits on top of "newest" until the date + * arrives, pushing down everything that has actually come out. The entry is + * still stored and still on its feed's own page; it joins the river on its date. + */ +const RIVER_FUTURE_SLACK_MS = 86_400_000; + +/** The newest `published_at` a river shows, as the ISO string the column holds. */ +function riverCeiling() { + return new Date(Date.now() + RIVER_FUTURE_SLACK_MS).toISOString(); +} + /** * The newest entries published across a whole category, newest first. * @@ -1423,9 +1439,10 @@ export async function latestItems(db, opts = {}) { where i.feed_id in (select id from picked) and i.published_at is not null and i.published_at >= ? + and i.published_at <= ? order by i.published_at desc, i.id desc limit ? offset ?`, - args: [...filter.args, feedCap, since, limit, offset], + args: [...filter.args, feedCap, since, riverCeiling(), limit, offset], }); return rows; @@ -1506,16 +1523,16 @@ export async function itemsForTopic(db, slug, opts = {}) { -- same page. join lateral ( select * from feed_items x - where x.feed_id = p.feed_id and x.published_at >= ? + where x.feed_id = p.feed_id and x.published_at >= ? and x.published_at <= ? order by x.published_at desc limit ? ) i on true join feeds f on f.id = i.feed_id order by i.published_at desc limit ?`, - // `filter.args` belongs to the `picked` subquery; `since` and the first - // `want` to the per-feed range; the last `want` to the page. - args: [slug, ...filter.args, feedCap, since, want, want], + // `filter.args` belongs to the `picked` subquery; `since`, the ceiling and the + // first `want` to the per-feed range; the last `want` to the page. + args: [slug, ...filter.args, feedCap, since, riverCeiling(), want, want], }); if (!group) return rows; diff --git a/packages/db/test/category-river.test.js b/packages/db/test/category-river.test.js index 358f145..1ee366e 100644 --- a/packages/db/test/category-river.test.js +++ b/packages/db/test/category-river.test.js @@ -184,6 +184,17 @@ test('the window is what makes the query affordable, and it excludes old items', assert.ok(wide.some((row) => String(row.feed_slug) === 'ancient-show')); }); +test('a scheduled episode waits for its date instead of topping the river', async () => { + // Seen live on /podcasts 2026-10-29: a show dated its next eight episodes into + // December and they sat above everything that had actually come out. + await publisher({ slug: 'scheduled-show', kind: 'podcast', dates: [iso(-3), iso(30), iso(60)] }); + + const rows = await q.latestItems(db, { kinds: ['podcast'], limit: 50 }); + const mine = rows.filter((row) => String(row.feed_slug) === 'scheduled-show'); + assert.equal(mine.length, 1, 'only the episode already out is listed'); + assert.ok(rows.every((row) => Date.parse(String(row.published_at)) <= Date.now() + 86_400_000)); +}); + test('an unknown kind is the whole directory rather than an empty page', async () => { // normalizeKinds drops what it does not recognise and null means every kind, // so a caller who guessed a category name gets the directory. Worth pinning: