[discovery][lists] semi working lists and discovery

This commit is contained in:
Devin Zuczek
2026-08-20 13:35:18 -04:00
parent f678f87d00
commit 5db02db172
10 changed files with 685 additions and 289 deletions
+65 -5
View File
@@ -4,17 +4,23 @@ import { useWorkersLogger } from 'workers-tagged-logger'
import { withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
import { DiscoverySections, json, PAGE_SOURCE_PARAM, ServiceStatus } from './openapi'
import { fetchPageSource } from './page-sources'
import {
DiscoverySections,
json,
PAGE_SOURCE_PARAM,
SECTION_IDS_PARAM,
ServiceStatus,
} from './openapi'
import { fetchPageSource, readSections, SECTIONS_CATALOGUE } from './page-sources'
import type { App } from './context'
/**
* Discovery Worker. Serves the layout of the client's discovery pages — which carousels a
* page shows and in what order — out of `static/`, one file per page source, through the
* ASSETS binding (see `page-sources.ts`). It does not serve the carousels' CONTENTS: each
* section names a client-side feed the client resolves against the `rooms`/`api` workers
* itself.
* ASSETS binding (see `page-sources.ts`), plus `sections.json`, the id-keyed catalogue the
* bulk lookup filters. It does not serve the carousels' CONTENTS: each section names a
* client-side feed the client resolves against the `rooms`/`api` workers itself.
*
* Unauthenticated: every client gets the same layout, and the client fetches this before
* anything player-specific.
@@ -45,6 +51,57 @@ const app = new Hono<App>()
(c) => c.json({ service: 'discovery', status: 'ok' })
)
// A set of sections looked up by id, out of the catalogue in `static/sections.json`.
//
// This is the id-keyed counterpart to the page-source route: a page source hands back a
// whole page's rows in draw order, while this hands back exactly the rows asked for,
// which is how the client refreshes sections it already knows the ids of without
// re-fetching every page they came from.
//
// The reference reads its catalogue file and filters it, so the failure modes are the
// file's, not the query's: an id matching nothing is simply absent from the answer
// rather than an error, and a query naming NO ids answers `[]` rather than the whole
// catalogue — the client asks for nothing when it wants nothing. Only a missing
// catalogue file is a 404.
.get(
'/sections/bulk',
describeRoute({
tags: ['Discovery'],
summary: 'Look up sections by id',
description: [
'The sections named by the repeated `?id=` query, drawn from the catalogue in',
'`static/sections.json` — the union of the rows the page sources are built from.',
'',
'The answer is that file FILTERED, which fixes the edges: rows come back in the',
'catalogues order rather than the querys, an id that matches nothing is left out',
'instead of erroring, and repeating an id still yields it once. A query with no `id`',
'at all answers `[]`. Rows are served exactly as stored, so a field this service',
'doesnt model survives the round trip.',
'',
'Same section shape as `/sections/pagesource/{type}`: a section NAMES a feed',
'(`source`/`sourceMetadata`) that the client resolves itself. Nothing here is',
'player-specific, so there is no auth.',
].join('\n'),
parameters: [SECTION_IDS_PARAM],
responses: {
200: json(DiscoverySections, 'The requested sections, in catalogue order'),
404: { description: 'The catalogue file is not published' },
},
}),
async (c) => {
// `queries` and not `query`: the ids arrive as a repeated parameter, and `query`
// would collapse them to the first one and silently drop the rest of the page.
const ids = c.req.queries('id')
if (ids === undefined || ids.length === 0) return c.json([])
const sections = await readSections(c, SECTIONS_CATALOGUE)
if (sections === null) return c.notFound()
const wanted = new Set(ids)
return c.json(sections.filter((s) => typeof s.id === 'string' && wanted.has(s.id)))
}
)
// One discovery page's section layout, served verbatim from `static/<type>.json`.
.get(
'/sections/pagesource/:type',
@@ -104,6 +161,9 @@ app.get(
'than anything the code enumerates. Nothing is editable at runtime and every client',
'gets the same answer, so the routes are unauthenticated.',
'',
'Sections can also be fetched by id rather than by page: `/sections/bulk` filters',
'`static/sections.json`, the catalogue those layouts draw their rows from.',
'',
'A section names a feed rather than carrying its contents: the client resolves the',
'rooms, items and accounts behind each carousel against the `rooms` and `api` workers',
'itself.',