From d9fca44540e440a66ad144762a956ff0d8cceb45 Mon Sep 17 00:00:00 2001 From: Tom Scanlan Date: Thu, 2 Jul 2026 15:57:24 -0400 Subject: [PATCH] fix(search): tag Meili offset cursors and strip tag on parse (om-7dbs) runEventSearchPage/runNearMePage now emit a meili:-tagged offset, and parseOffsetCursor accepts both the tagged cursor and a bare legacy offset. A d1-tagged keyset reaching here resolves to offset 0 (clean restart) instead of Number(base64url)->NaN. First-page search results therefore emit a tagged cursor for free. --- apps/web/src/lib/search/server/query.test.ts | 27 +++++++++++++++++--- apps/web/src/lib/search/server/query.ts | 14 ++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/web/src/lib/search/server/query.test.ts b/apps/web/src/lib/search/server/query.test.ts index 67ea9ee..ad7a801 100644 --- a/apps/web/src/lib/search/server/query.test.ts +++ b/apps/web/src/lib/search/server/query.test.ts @@ -55,7 +55,11 @@ describe('searchBackendFromEnv', () => { it('carries SEARCH_INDEX through so the read path can match the sink index', () => { expect( - searchBackendFromEnv({ SEARCH_URL: 'https://s', SEARCH_API_KEY: 'k', SEARCH_INDEX: 'events-test' }) + searchBackendFromEnv({ + SEARCH_URL: 'https://s', + SEARCH_API_KEY: 'k', + SEARCH_INDEX: 'events-test' + }) ).toMatchObject({ url: 'https://s', apiKey: 'k', indexUid: 'events-test' }); }); }); @@ -75,7 +79,12 @@ describe('runEventSearchPage', () => { [{ did: 'did:plc:a', handle: 'alice.test' }] ); - const page = await runEventSearchPage(backend(fetchFn), client, { q: 'fest', cursor: '10' }); + // A tagged `meili:10` cursor (from a load-more round-trip) parses back to + // offset 10 — the tag is stripped before it reaches Meilisearch. + const page = await runEventSearchPage(backend(fetchFn), client, { + q: 'fest', + cursor: 'meili:10' + }); expect(bodies[0].offset).toBe(10); expect(bodies[0].q).toBe('fest'); @@ -97,9 +106,19 @@ describe('runEventSearchPage', () => { const page = await runEventSearchPage(backend(fetchFn), client, { q: 'fest', cursor: null }); - // Page fills at SEARCH_PAGE_SIZE hits; the next offset is that count. + // Page fills at SEARCH_PAGE_SIZE hits; the next offset is that count, tagged + // with the Meili backend so load-more can't misroute it to D1 (om-7dbs). expect(page.events).toHaveLength(SEARCH_PAGE_SIZE); - expect(page.cursor).toBe(String(SEARCH_PAGE_SIZE)); + expect(page.cursor).toBe(`meili:${SEARCH_PAGE_SIZE}`); + }); + + it('accepts a bare legacy offset cursor (untagged, pre-deploy in-flight)', async () => { + const { fetchFn, bodies } = meiliFetch([{ uri: 'at://did:plc:a/c/1' }], 5); + const client = fakeClient([record('at://did:plc:a/c/1', 'alpha')]); + + await runEventSearchPage(backend(fetchFn), client, { q: 'fest', cursor: '10' }); + + expect(bodies[0].offset).toBe(10); }); it('throws when D1 hydration fails so the caller can fall back instead of skipping hits', async () => { diff --git a/apps/web/src/lib/search/server/query.ts b/apps/web/src/lib/search/server/query.ts index 14b0c09..795ea9b 100644 --- a/apps/web/src/lib/search/server/query.ts +++ b/apps/web/src/lib/search/server/query.ts @@ -16,6 +16,7 @@ import { listDiscoverableEventsByUrisFromContrail, type FlatEventRecord } from '$lib/contrail'; +import { parseCursor, tagCursor } from '$lib/contrail/cursor'; export interface SearchPageResult { events: FlatEventRecord[]; @@ -38,7 +39,14 @@ export function searchBackendFromEnv(env?: { } function parseOffsetCursor(cursor: string | null | undefined): number { - const n = Number(cursor); + // Accept both a tagged `meili:` cursor (a load-more round-trip) and a bare + // `` (first page, or a legacy pre-deploy cursor). A D1-tagged keyset should + // never reach here now that load-more routes by tag — but if one does, treat + // it as offset 0 (a clean restart) rather than Number(base64url) -> NaN, which + // this guard already collapses to 0 anyway. + const { backend, raw } = parseCursor(cursor); + if (backend === 'd1') return 0; + const n = Number(raw); return Number.isInteger(n) && n > 0 ? n : 0; } @@ -82,7 +90,9 @@ async function hydrateToPage( return { events: flattenEventRecords(items.map((i) => i.record)), handles, - cursor: consumed > 0 && moreToScan ? String(next) : null, + // Tag the offset with the Meili backend so load-more continues on Meili and + // never feeds this number to D1 listRecords (om-7dbs). + cursor: consumed > 0 && moreToScan ? tagCursor('meili', String(next)) : null, distances }; } -- 2.51.2