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
17 changes: 13 additions & 4 deletions backend/src/routes/reading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@ interface ItemLabelRow {
// 1. Slim payload — only item_guid/read_at/rkey are returned (no url/title).
// 2. Delta sync — `?since=<updated_at>` returns only rows changed since the
// client's cursor; the common refresh is then tiny.
// 3. Windowing — a full sync (no `since`) is bounded to recent reads, since
// older read state can't suppress articles no longer fetched.
// 3. Windowing — a full sync (no `since`) is bounded to recent reads.
// The window must be wide enough to cover every article the feed can surface on
// a cold start (empty cache). The cold-start fetch pulls the most-recent N items
// PER FEED regardless of age (see COLD_START_LIMIT in feedFetcher.ts), so a
// low-frequency feed's backlog can be well over a year old. If a read falls
// outside this window, that already-read article reappears as unread on a fresh
// login — there's no local read state to fall back on after logout clears the
// cache. Hence a generous window; the delta sync keeps steady-state cheap anyway.
// NOTE: the window must stay in sync with READ_POSITIONS_WINDOW_MS in the
// frontend's itemLabels store, which scopes its reconcile deletions to the same
// window so it never drops older local read state.
const READ_POSITIONS_WINDOW_SECONDS = 90 * 24 * 60 * 60; // 90 days
const READ_POSITIONS_MAX_ROWS = 5000; // hard backstop on a single response
const READ_POSITIONS_WINDOW_SECONDS = 2 * 365 * 24 * 60 * 60; // 2 years
// Backstop on a single response. Must stay above the number of reads a heavy
// user can accumulate within the window, or ORDER BY updated_at DESC would drop
// the OLDEST in-window reads first — exactly the old articles we need to suppress.
const READ_POSITIONS_MAX_ROWS = 100000;

// GET /api/reading/positions - List read positions for the current user.
// Pass `?since=<unix_seconds>` for an incremental (delta) fetch; omit it for a
Expand Down
2 changes: 1 addition & 1 deletion backend/test/reading-positions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
const TEST_DID = 'did:plc:readingpositions123';
const TEST_SESSION_ID = 'test-session-reading-positions';
const DAY_SECONDS = 24 * 60 * 60;
const WINDOW_SECONDS = 90 * DAY_SECONDS; // must match READ_POSITIONS_WINDOW_SECONDS
const WINDOW_SECONDS = 2 * 365 * DAY_SECONDS; // must match READ_POSITIONS_WINDOW_SECONDS

async function setupTestUser() {
await env.DB.prepare(
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/stores/itemLabels.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const BULK_BATCH_SIZE = 500;
// READ_POSITIONS_WINDOW_SECONDS in the backend's reading route: a full sync only
// reconciles (deletes) read labels within this window, since the server only
// returns reads this recent. Older local read state is preserved untouched.
const READ_POSITIONS_WINDOW_MS = 90 * 24 * 60 * 60 * 1000; // 90 days
const READ_POSITIONS_WINDOW_MS = 2 * 365 * 24 * 60 * 60 * 1000; // 2 years

// Re-export for consumers that used this type from reading store
export interface SavedArticle {
Expand Down
Loading