From c5ea4def12be9b41b010825822c914469da170f4 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 11 Sep 2026 15:14:03 -0400 Subject: [PATCH] Mark viewer-independent RPC reads cacheable; drop duplicate get_hot_feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_profiles/get_standard_site_posts/get_standard_site_publications (300/3600), get_document_recommends/get_publication_recommendations/ get_document_interactions/search_publication_names (60/600) now serve CDN-cacheable GET. get_hot_feed duplicated GET /api/hot_feed, which already had these headers and no callers left to migrate — deleted. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MKNFQ8r67jjW8ByVYjtJoU --- .../[command]/get_document_interactions.ts | 11 ++----- .../rpc/[command]/get_document_recommends.ts | 1 + app/api/rpc/[command]/get_hot_feed.ts | 17 ----------- app/api/rpc/[command]/get_profiles.ts | 1 + .../get_publication_recommendations.ts | 1 + .../rpc/[command]/get_standard_site_posts.ts | 10 ++++--- .../get_standard_site_publications.ts | 1 + app/api/rpc/[command]/route.ts | 29 +++++++++++++++++-- .../rpc/[command]/search_publication_names.ts | 1 + 9 files changed, 40 insertions(+), 32 deletions(-) delete mode 100644 app/api/rpc/[command]/get_hot_feed.ts diff --git a/app/api/rpc/[command]/get_document_interactions.ts b/app/api/rpc/[command]/get_document_interactions.ts index 5b9d39cb..acc63134 100644 --- a/app/api/rpc/[command]/get_document_interactions.ts +++ b/app/api/rpc/[command]/get_document_interactions.ts @@ -17,10 +17,8 @@ export const get_document_interactions = makeRoute({ input: z.object({ document_uri: z.string(), }), - handler: async ( - { document_uri }, - { supabase }: Pick, - ) => { + cache: { sMaxAge: 60, staleWhileRevalidate: 600 }, + handler: async ({ document_uri }, { supabase }: Pick) => { let { data: document } = await supabase .from("documents") .select( @@ -46,10 +44,7 @@ export const get_document_interactions = makeRoute({ }; } - const normalizedData = normalizeDocumentRecord( - document.data, - document.uri, - ); + const normalizedData = normalizeDocumentRecord(document.data, document.uri); // Unauthenticated endpoint: discussion views only need the document's // metadata, so gated blocks never leave the server regardless of viewer. if (normalizedData) { diff --git a/app/api/rpc/[command]/get_document_recommends.ts b/app/api/rpc/[command]/get_document_recommends.ts index f3fedcdf..fd5b10d7 100644 --- a/app/api/rpc/[command]/get_document_recommends.ts +++ b/app/api/rpc/[command]/get_document_recommends.ts @@ -14,6 +14,7 @@ export const get_document_recommends = makeRoute({ input: z.object({ document: z.string(), }), + cache: { sMaxAge: 60, staleWhileRevalidate: 600 }, handler: async ({ document }, { supabase }: Pick) => { const { data } = await supabase .from("recommends_on_documents") diff --git a/app/api/rpc/[command]/get_hot_feed.ts b/app/api/rpc/[command]/get_hot_feed.ts deleted file mode 100644 index eed48b43..00000000 --- a/app/api/rpc/[command]/get_hot_feed.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from "zod"; -import { makeRoute } from "../lib"; -import type { Env } from "./route"; -import { getHotFeed } from "actions/reader/getHotFeed"; -import type { Post } from "actions/reader/getReaderFeed"; - -export type GetHotFeedReturnType = Awaited< - ReturnType<(typeof get_hot_feed)["handler"]> ->; - -export const get_hot_feed = makeRoute({ - route: "get_hot_feed", - input: z.object({}), - handler: async ({}, {}: Pick) => { - return await getHotFeed(); - }, -}); diff --git a/app/api/rpc/[command]/get_profiles.ts b/app/api/rpc/[command]/get_profiles.ts index fe4014da..d6ca55a4 100644 --- a/app/api/rpc/[command]/get_profiles.ts +++ b/app/api/rpc/[command]/get_profiles.ts @@ -15,6 +15,7 @@ export const get_profiles = makeRoute({ input: z.object({ dids: z.array(z.string()), }), + cache: { sMaxAge: 300, staleWhileRevalidate: 3600 }, handler: async ({ dids }) => { let profiles = await getProfiles(dids); return { diff --git a/app/api/rpc/[command]/get_publication_recommendations.ts b/app/api/rpc/[command]/get_publication_recommendations.ts index d2c0fec6..9ca86440 100644 --- a/app/api/rpc/[command]/get_publication_recommendations.ts +++ b/app/api/rpc/[command]/get_publication_recommendations.ts @@ -11,6 +11,7 @@ export const get_publication_recommendations = makeRoute({ input: z.object({ publication: z.string(), }), + cache: { sMaxAge: 60, staleWhileRevalidate: 600 }, handler: async ({ publication }, { supabase }: Pick) => { // One row per edge; the indexer already dedupes, drops // self-recommendations, and caps the list at 3. diff --git a/app/api/rpc/[command]/get_standard_site_posts.ts b/app/api/rpc/[command]/get_standard_site_posts.ts index 9f6c4e52..6d38f5df 100644 --- a/app/api/rpc/[command]/get_standard_site_posts.ts +++ b/app/api/rpc/[command]/get_standard_site_posts.ts @@ -51,6 +51,7 @@ export const get_standard_site_posts = makeRoute({ input: z.object({ uris: z.array(z.string()).max(100), }), + cache: { sMaxAge: 300, staleWhileRevalidate: 3600 }, handler: async ({ uris }, { supabase }: Pick) => { if (uris.length === 0) return { result: { posts: [] } }; @@ -106,7 +107,10 @@ export const get_standard_site_posts = makeRoute({ const pubRow = d.documents_in_publications?.[0]?.publications; const publication = pubRow - ? { uri: pubRow.uri, record: normalizePublicationRecord(pubRow.record) } + ? { + uri: pubRow.uri, + record: normalizePublicationRecord(pubRow.record), + } : null; let did: string | null; @@ -115,9 +119,7 @@ export const get_standard_site_posts = makeRoute({ } catch { did = null; } - const author = did - ? toBylineProfiles([did], profiles)[0] - : null; + const author = did ? toBylineProfiles([did], profiles)[0] : null; // Only expose contributors when the byline is more than just the // single author/owner; otherwise consumers fall back to `author` // (byte-for-byte identical to the previous single-author behavior). diff --git a/app/api/rpc/[command]/get_standard_site_publications.ts b/app/api/rpc/[command]/get_standard_site_publications.ts index f0d187e8..a29b8959 100644 --- a/app/api/rpc/[command]/get_standard_site_publications.ts +++ b/app/api/rpc/[command]/get_standard_site_publications.ts @@ -29,6 +29,7 @@ export const get_standard_site_publications = makeRoute({ input: z.object({ uris: z.array(z.string()).max(100), }), + cache: { sMaxAge: 300, staleWhileRevalidate: 3600 }, handler: async ({ uris }, { supabase }: Pick) => { if (uris.length === 0) return { result: { publications: [] } }; diff --git a/app/api/rpc/[command]/route.ts b/app/api/rpc/[command]/route.ts index 9d7d1ea9..151f3548 100644 --- a/app/api/rpc/[command]/route.ts +++ b/app/api/rpc/[command]/route.ts @@ -20,7 +20,6 @@ import { get_profile_data } from "./get_profile_data"; import { get_profiles } from "./get_profiles"; import { get_user_recommendations } from "./get_user_recommendations"; import { get_document_recommends } from "./get_document_recommends"; -import { get_hot_feed } from "./get_hot_feed"; import { get_document_interactions } from "./get_document_interactions"; import { get_publication_analytics } from "./get_publication_analytics"; import { get_publication_bsky_posts } from "./get_publication_bsky_posts"; @@ -28,6 +27,7 @@ import { get_publication_subscribers_timeseries } from "./get_publication_subscr import { get_user_mention_services } from "./get_user_mention_services"; import { proxy_mention_search } from "./proxy_mention_search"; import { get_active_user_stats } from "./get_active_user_stats"; +import { CACHEABLE_ROUTES } from "../cacheableRoutes"; let supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, @@ -57,7 +57,6 @@ let Routes = [ get_profiles, get_user_recommendations, get_document_recommends, - get_hot_feed, get_document_interactions, get_publication_analytics, get_publication_bsky_posts, @@ -66,11 +65,35 @@ let Routes = [ proxy_mention_search, get_active_user_stats, ]; + +// Every route that opts into GET caching (`cache` on its makeRoute) must be +// listed in cacheableRoutes.ts, since the browser client reads that set +// without importing these (server-only) route handlers. +{ + let declared = new Set( + Routes.filter((r) => r.cache).map((r) => r.route), + ); + let missing = [...declared].filter((r) => !CACHEABLE_ROUTES.has(r)); + let extra = [...CACHEABLE_ROUTES].filter((r) => !declared.has(r)); + if (missing.length || extra.length) { + throw new Error( + `cacheableRoutes.ts out of sync with route \`cache\` flags. Missing: [${missing}] Extra: [${extra}]`, + ); + } +} + +let router = makeRouter(Routes); +export async function GET( + req: Request, + { params }: { params: Promise<{ command: string }> }, +) { + let p = await params; + return router(p.command, req, Env); +} export async function POST( req: Request, { params }: { params: Promise<{ command: string }> }, ) { let p = await params; - let router = makeRouter(Routes); return router(p.command, req, Env); } diff --git a/app/api/rpc/[command]/search_publication_names.ts b/app/api/rpc/[command]/search_publication_names.ts index c94f1b8f..d92a5b24 100644 --- a/app/api/rpc/[command]/search_publication_names.ts +++ b/app/api/rpc/[command]/search_publication_names.ts @@ -14,6 +14,7 @@ export const search_publication_names = makeRoute({ query: z.string(), limit: z.number().optional().default(10), }), + cache: { sMaxAge: 60, staleWhileRevalidate: 600 }, handler: async ({ query, limit }, { supabase }: Pick) => { // Search publications by name in record (case-insensitive partial match) const { data: rawPublications, error } = await supabase -- knotmirror 0.0.1