diff --git a/app/(home-pages)/reader/enrichPost.ts b/app/(home-pages)/reader/enrichPost.ts index de71f468..0fccc8c0 100644 --- a/app/(home-pages)/reader/enrichPost.ts +++ b/app/(home-pages)/reader/enrichPost.ts @@ -34,7 +34,10 @@ export async function enrichDocumentToPost( const handle = await idResolver.did.resolve(uri.host); const normalizedData = normalizeDocumentRecord(doc.data, doc.uri); - if (!normalizedData) return null; + if (!normalizedData) { + console.log("[enrichPost] normalizeDocumentRecord returned null for:", doc.uri); + return null; + } const normalizedPubRecord = pub ? normalizePublicationRecord(pub.record) diff --git a/app/(home-pages)/reader/getReaderFeed.ts b/app/(home-pages)/reader/getReaderFeed.ts index cf2703b3..78c4fc00 100644 --- a/app/(home-pages)/reader/getReaderFeed.ts +++ b/app/(home-pages)/reader/getReaderFeed.ts @@ -6,7 +6,6 @@ import type { NormalizedDocument, NormalizedPublication, } from "src/utils/normalizeRecords"; -import { deduplicateByUriOrdered } from "src/utils/deduplicateRecords"; import { enrichDocumentToPost } from "./enrichPost"; export type Cursor = { @@ -19,35 +18,50 @@ export async function getReaderFeed( ): Promise<{ posts: Post[]; nextCursor: Cursor | null }> { let auth_res = await getIdentityData(); if (!auth_res?.atp_did) return { posts: [], nextCursor: null }; - let query = supabaseServerClient - .from("documents") - .select( - `*, - comments_on_documents(count), - document_mentions_in_bsky(count), - recommends_on_documents(count), - documents_in_publications!inner(publications!inner(*, publication_subscriptions!inner(*)))`, - ) - .eq( - "documents_in_publications.publications.publication_subscriptions.identity", - auth_res.atp_did, - ) - .order("sort_date", { ascending: false }) - .order("uri", { ascending: false }) - .limit(25); - if (cursor) { - query = query.or( - `sort_date.lt.${cursor.timestamp},and(sort_date.eq.${cursor.timestamp},uri.lt.${cursor.uri})`, - ); + + const { data: rawFeed, error } = await supabaseServerClient.rpc( + "get_reader_feed", + { + p_identity: auth_res.atp_did, + p_cursor_timestamp: cursor?.timestamp ?? null, + p_cursor_uri: cursor?.uri ?? null, + p_limit: 25, + }, + ); + if (error) { + console.error("[getReaderFeed] rpc error:", error); + return { posts: [], nextCursor: null }; } - let { data: rawFeed, error } = await query; - // Deduplicate records that may exist under both pub.leaflet and site.standard namespaces - const feed = deduplicateByUriOrdered(rawFeed || []); + if (rawFeed.length === 0) return { posts: [], nextCursor: null }; + + // Reshape rows to match the structure enrichDocumentToPost expects + const feed = rawFeed.map((row: any) => ({ + uri: row.uri, + data: row.data, + sort_date: row.sort_date, + comments_on_documents: [{ count: Number(row.comments_count) }], + document_mentions_in_bsky: [{ count: Number(row.mentions_count) }], + recommends_on_documents: [{ count: Number(row.recommends_count) }], + documents_in_publications: row.publication_uri + ? [ + { + publications: { + uri: row.publication_uri, + record: row.publication_record, + name: row.publication_name, + }, + }, + ] + : [], + })); let posts = ( await Promise.all(feed.map((post) => enrichDocumentToPost(post as any))) ).filter((post): post is Post => post !== null); + if (feed.length > 0 && posts.length !== feed.length) { + console.log(`[getReaderFeed] ${feed.length - posts.length}/${feed.length} posts dropped during enrichment`); + } const nextCursor = posts.length > 0 diff --git a/supabase/database.types.ts b/supabase/database.types.ts index c4f53bde..dfd01d43 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -1360,6 +1360,25 @@ export type Database = { like: unknown }[] } + get_reader_feed: { + Args: { + p_identity: string + p_cursor_timestamp?: string + p_cursor_uri?: string + p_limit?: number + } + Returns: { + uri: string + data: Json + sort_date: string + comments_count: number + mentions_count: number + recommends_count: number + publication_uri: string + publication_record: Json + publication_name: string + }[] + } parse_iso_timestamp: { Args: { "": string diff --git a/supabase/migrations/20260303000000_add_get_reader_feed_function.sql b/supabase/migrations/20260303000000_add_get_reader_feed_function.sql new file mode 100644 index 00000000..faae0e5b --- /dev/null +++ b/supabase/migrations/20260303000000_add_get_reader_feed_function.sql @@ -0,0 +1,49 @@ +CREATE OR REPLACE FUNCTION get_reader_feed( + p_identity text, + p_cursor_timestamp timestamptz DEFAULT NULL, + p_cursor_uri text DEFAULT NULL, + p_limit int DEFAULT 25 +) +RETURNS TABLE ( + uri text, + data jsonb, + sort_date timestamptz, + comments_count bigint, + mentions_count bigint, + recommends_count bigint, + publication_uri text, + publication_record jsonb, + publication_name text +) +LANGUAGE sql STABLE +AS $$ + SELECT + d.uri, + d.data, + d.sort_date, + (SELECT count(*) FROM comments_on_documents c WHERE c.document = d.uri), + (SELECT count(*) FROM document_mentions_in_bsky m WHERE m.document = d.uri), + (SELECT count(*) FROM recommends_on_documents r WHERE r.document = d.uri), + pub.uri, + pub.record, + pub.name + FROM documents d + JOIN documents_in_publications dip ON dip.document = d.uri + JOIN publication_subscriptions ps ON ps.publication = dip.publication + LEFT JOIN LATERAL ( + SELECT p.uri, p.record, p.name + FROM documents_in_publications dip2 + JOIN publications p ON p.uri = dip2.publication + WHERE dip2.document = d.uri + LIMIT 1 + ) pub ON true + WHERE ps.identity = p_identity + AND ( + p_cursor_timestamp IS NULL + OR d.sort_date < p_cursor_timestamp + OR (d.sort_date = p_cursor_timestamp AND d.uri < p_cursor_uri) + ) + GROUP BY d.uri, d.data, d.sort_date, pub.uri, pub.record, pub.name + ORDER BY d.sort_date DESC, d.uri DESC + LIMIT p_limit; +$$;