From e0e6aa0e250026fab064b11ebf7c76a5196a4012 Mon Sep 17 00:00:00 2001 From: Tom Scanlan Date: Thu, 2 Jul 2026 15:57:43 -0400 Subject: [PATCH] fix(search): tag D1 first-page cursors and refresh stale routing comments (om-7dbs) Home events, profile hosting/past-events first pages now emit d1:-tagged cursors and untag any inbound (deep-link) cursor before the D1 read, so first-load and load-more can't diverge. Search D1 fallback and topics pages keep their null cursor (no pipeline in fetchParams to resume through); their comments are updated to describe the new tag-based routing and defer re-enabling pagination to om-47ak. --- apps/web/src/routes/(app)/events/+page.server.ts | 9 +++++++-- .../(app)/p/[actor]/hosting/+page.server.ts | 7 +++++-- .../(app)/p/[actor]/past-events/+page.server.ts | 7 +++++-- apps/web/src/routes/(app)/search/+page.server.ts | 14 +++++++------- .../routes/(app)/topics/[slug]/+page.server.ts | 15 ++++++++------- 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/apps/web/src/routes/(app)/events/+page.server.ts b/apps/web/src/routes/(app)/events/+page.server.ts index cb8d7c6..787b2bb 100644 --- a/apps/web/src/routes/(app)/events/+page.server.ts +++ b/apps/web/src/routes/(app)/events/+page.server.ts @@ -3,6 +3,7 @@ import { getServerClient, listDiscoverableEventsFromContrail } from '$lib/contrail'; +import { parseCursor, tagCursor } from '$lib/contrail/cursor'; import type { PageServerLoad } from './$types'; const PAGE_SIZE = 20; @@ -10,7 +11,9 @@ const PAGE_SIZE = 20; export const load: PageServerLoad = async ({ url, platform }) => { const client = getServerClient(platform!.env.DB); const now = new Date().toISOString(); - const cursor = url.searchParams.get('cursor') ?? undefined; + // Untag any inbound cursor (deep link) before handing the opaque keyset to D1; + // legacy untagged cursors pass through unchanged (om-7dbs). + const cursor = parseCursor(url.searchParams.get('cursor')).raw ?? undefined; const isPopular = url.searchParams.get('filter') !== 'all'; const response = await listDiscoverableEventsFromContrail(client, { @@ -33,6 +36,8 @@ export const load: PageServerLoad = async ({ url, platform }) => { return { events: flattenEventRecords(response.records), handles, - cursor: response.cursor ?? null + // Tag the first-page cursor so load-more routes back to this same D1 + // discoverable pipeline instead of re-inferring a backend (om-7dbs). + cursor: tagCursor('d1', response.cursor ?? null) }; }; diff --git a/apps/web/src/routes/(app)/p/[actor]/hosting/+page.server.ts b/apps/web/src/routes/(app)/p/[actor]/hosting/+page.server.ts index 22c63ed..6486d82 100644 --- a/apps/web/src/routes/(app)/p/[actor]/hosting/+page.server.ts +++ b/apps/web/src/routes/(app)/p/[actor]/hosting/+page.server.ts @@ -5,6 +5,7 @@ import { getServerClient, listAuthoredEventsFromContrail } from '$lib/contrail'; +import { parseCursor, tagCursor } from '$lib/contrail/cursor'; import { isActorIdentifier } from '@atcute/lexicons/syntax'; import { error } from '@sveltejs/kit'; @@ -19,7 +20,8 @@ export async function load({ params, url, platform }) { if (!did) throw error(404, 'Actor not found'); - const cursor = url.searchParams.get('cursor') ?? undefined; + // Untag any inbound cursor before the D1 read; legacy untagged passes through. + const cursor = parseCursor(url.searchParams.get('cursor')).raw ?? undefined; const now = new Date().toISOString(); const [profile, response] = await Promise.all([ @@ -37,7 +39,8 @@ export async function load({ params, url, platform }) { return { events: response ? flattenEventRecords(response.records) : [], - cursor: response?.cursor ?? null, + // Tag so load-more stays on this D1 authored pipeline (om-7dbs). + cursor: tagCursor('d1', response?.cursor ?? null), actorProfile: profile, actor, actorDid: did diff --git a/apps/web/src/routes/(app)/p/[actor]/past-events/+page.server.ts b/apps/web/src/routes/(app)/p/[actor]/past-events/+page.server.ts index 9e08c64..56549b0 100644 --- a/apps/web/src/routes/(app)/p/[actor]/past-events/+page.server.ts +++ b/apps/web/src/routes/(app)/p/[actor]/past-events/+page.server.ts @@ -5,6 +5,7 @@ import { getServerClient, listAuthoredEventsFromContrail } from '$lib/contrail'; +import { parseCursor, tagCursor } from '$lib/contrail/cursor'; import { isActorIdentifier } from '@atcute/lexicons/syntax'; import { error } from '@sveltejs/kit'; @@ -19,7 +20,8 @@ export async function load({ params, url, platform }) { if (!did) throw error(404, 'Actor not found'); - const cursor = url.searchParams.get('cursor') ?? undefined; + // Untag any inbound cursor before the D1 read; legacy untagged passes through. + const cursor = parseCursor(url.searchParams.get('cursor')).raw ?? undefined; const now = new Date().toISOString(); const [profile, response] = await Promise.all([ @@ -42,7 +44,8 @@ export async function load({ params, url, platform }) { return { events, - cursor: response?.cursor ?? null, + // Tag so load-more stays on this D1 authored pipeline (om-7dbs). + cursor: tagCursor('d1', response?.cursor ?? null), actorProfile: profile, actor, actorDid: did diff --git a/apps/web/src/routes/(app)/search/+page.server.ts b/apps/web/src/routes/(app)/search/+page.server.ts index a93e668..95eeb5e 100644 --- a/apps/web/src/routes/(app)/search/+page.server.ts +++ b/apps/web/src/routes/(app)/search/+page.server.ts @@ -50,13 +50,13 @@ export const load: PageServerLoad = async ({ url, platform }) => { return { events: flattenEventRecords(response.records), handles, - // The D1 fallback is first-batch-only; don't hand its cursor back. - // loadMoreEvents has no search backend here, so it paginates via - // listRecords, which applies neither the discoverable filter nor the - // startsAtMin this page uses — later pages would drift into past and - // non-discoverable events. (When a configured backend errored the cursor - // is also unusable: it's a D1 cursor but loadMoreEvents re-routes to Meili - // whenever a backend is configured.) Drop it in both cases. + // The D1 fallback is first-batch-only; don't hand its cursor back. Even + // with self-describing cursors (om-7dbs), the search fetchParams carry no + // `pipeline`, so a d1-tagged cursor would route load-more through plain + // listRecords — dropping the discoverable filter and startsAtMin this page + // applies — and later pages would drift into past and non-discoverable + // events. Re-enabling consistent D1 pagination here would mean threading the + // discoverable pipeline + filters through; deferred. Drop the cursor. cursor: null, query: q }; diff --git a/apps/web/src/routes/(app)/topics/[slug]/+page.server.ts b/apps/web/src/routes/(app)/topics/[slug]/+page.server.ts index 2b4ae58..e7f9628 100644 --- a/apps/web/src/routes/(app)/topics/[slug]/+page.server.ts +++ b/apps/web/src/routes/(app)/topics/[slug]/+page.server.ts @@ -41,13 +41,14 @@ export const load: PageServerLoad = async ({ params, platform }) => { topic, events: flattenEventRecords(response?.records ?? []), handles, - // First-batch-only, like the search page's D1 fallback. This D1 FTS read has - // no cursor loadMoreEvents can resume: with a search backend configured it - // re-routes to Meili (whose offset cursor is incompatible with this D1 - // cursor), and without one it paginates via listRecords (dropping the - // discoverable + startsAtMin filters this page relies on). Either way later - // pages would drift, so don't hand back a cursor. Deep topic pagination is - // tracked separately (om-47ak). + // First-batch-only, like the search page's D1 fallback. Self-describing + // cursors (om-7dbs) now stop this page's D1 cursor from being mis-consumed + // as a Meili offset, but they don't make it resumable here: this fetchParams + // contract carries no `pipeline`, so a d1-tagged cursor would still route + // load-more through plain listRecords, dropping the discoverable + + // startsAtMin filters this page relies on. So don't hand back a cursor. Deep + // topic pagination (threading the discoverable pipeline through) is tracked + // separately (om-47ak). cursor: null, query }; -- 2.51.2