Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rssamplifier/web",
"version": "0.2.3",
"version": "0.2.4",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
27 changes: 22 additions & 5 deletions packages/db/src/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions packages/db/test/category-river.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading