A community based topic aggregation platform built on atproto

docs(beads): add PDS ListRecords performance bottleneck issue

Coves-fqg: Documents the root cause of slow vote creation (~2-3s)
and initial feed loads (~800ms) due to repeated listRecords calls
to user's PDS for vote existence checks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+2
+2
.beads/issues.jsonl
··· 1 + {"id":"Coves-327","content_hash":"4f3413d1657a67ed1f71c0a5f06a9934b9e397a539de14e744c8ef622f0e7f73","title":"Bluesky embed Phase 3: Extract images and videos from embedded posts","description":"Currently Bluesky post embeds only indicate media presence (hasMedia, mediaCount). Phase 3 should extract actual media data for display.\n\n## Images (app.bsky.embed.images#view)\nExtract from images array:\n- thumb: thumbnail URL (CDN)\n- fullsize: full-size URL (CDN)\n- alt: accessibility text\n- aspectRatio: { width, height }\n\n## Videos (app.bsky.embed.video#view)\nExtract video data:\n- thumbnail: video thumbnail URL\n- playlist: HLS playlist URL (.m3u8)\n- aspectRatio: { width, height }\n\n## Scope\nApply to all Bluesky post contexts:\n1. Regular embedded Bluesky posts\n2. Quoted posts within embeds\n3. recordWithMedia embeds (quote + media)\n\n## Types to update\n- BlueskyPostResult: add Images []ImageEmbed and Video *VideoEmbed fields\n- Add ImageEmbed struct: Thumb, Fullsize, Alt, AspectRatio\n- Add VideoEmbed struct: Thumbnail, Playlist, AspectRatio\n\n## Dependencies\n- Should be implemented after moderation features are complete\n- Images/videos from Bluesky may contain content requiring moderation labels\n\n## Notes\n- All URLs are CDN URLs from cdn.bsky.app (already resolved by Bluesky API)\n- No blob fetching required - just pass through the URLs","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-26T15:53:51.873611055-08:00","updated_at":"2025-12-26T15:53:51.873611055-08:00","source_repo":"."} 1 2 {"id":"Coves-8b1","content_hash":"a949ba526ad819badab625c0d5fdbc6a7994d22f059f4a4f7e68635750bd5ea3","title":"Apply functional options pattern to NewGetDiscoverHandler","description":"Location: internal/api/handlers/discover/get.go\n\nApply functional options pattern for optional dependencies (votes, bluesky).\n\nDepends on: Coves-jdf (NewPostService refactor should be done first to establish pattern)\nParent: Coves-8k1","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-22T21:35:27.315877238-08:00","updated_at":"2025-12-22T21:35:58.061823373-08:00","source_repo":"."} 2 3 {"id":"Coves-8k1","content_hash":"a10053af68636b722a86aa75dd483ece4509d0de4884230beb52453585895589","title":"Refactor service constructors to use functional options pattern","description":"Multiple service constructors have grown to accept many optional dependencies, leading to hard-to-read nil chains:\n```go\nposts.NewPostService(repo, communityService, nil, nil, nil, nil, \"http://localhost:3001\")\n```\n\nApply the functional options pattern to all affected constructors:\n- NewPostService (7 params, 4 optional)\n- NewGetDiscoverHandler (3 params, 2 optional)\n- NewGetCommunityHandler (3 params, 2 optional)\n- NewGetTimelineHandler (3 params, 2 optional)\n- RegisterTimelineRoutes (5 params, 2 optional)\n\nThis will improve readability, make tests self-documenting, and prevent breakage when adding new optional params.\n\nScope: ~20 files, ~50 call sites\nRisk: Low (purely mechanical, no logic changes)","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-22T21:35:19.91257167-08:00","updated_at":"2025-12-22T21:35:39.69736147-08:00","source_repo":"."} 3 4 {"id":"Coves-95q","content_hash":"8ec99d598f067780436b985f9ad57f0fa19632026981038df4f65f192186620b","title":"Add comprehensive API documentation","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-17T20:30:34.835721854-08:00","updated_at":"2025-11-17T20:30:34.835721854-08:00","source_repo":".","dependencies":[{"issue_id":"Coves-95q","depends_on_id":"Coves-e16","type":"blocks","created_at":"2025-11-17T20:30:46.273899399-08:00","created_by":"daemon"}]} 4 5 {"id":"Coves-e16","content_hash":"7c5d0fc8f0e7f626be3dad62af0e8412467330bad01a244e5a7e52ac5afff1c1","title":"Complete post creation and moderation features","description":"","status":"open","priority":1,"issue_type":"feature","created_at":"2025-11-17T20:30:12.885991306-08:00","updated_at":"2025-11-17T20:30:12.885991306-08:00","source_repo":"."} 5 6 {"id":"Coves-f9q","content_hash":"a1a38759edc37d11227d5992cdbed1b8cf27e09496165e45c542b208f58d34ce","title":"Apply functional options pattern to NewGetTimelineHandler and RegisterTimelineRoutes","description":"Locations:\n- internal/api/handlers/timeline/get.go (NewGetTimelineHandler)\n- internal/api/routes/timeline.go (RegisterTimelineRoutes)\n\nApply functional options pattern for optional dependencies (votes, bluesky).\n\nUpdate RegisterTimelineRoutes last after handlers are refactored.\n\nDepends on: Coves-jdf, Coves-8b1, Coves-iw5\nParent: Coves-8k1","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-22T21:35:27.420117481-08:00","updated_at":"2025-12-22T21:35:58.166765845-08:00","source_repo":"."} 6 7 {"id":"Coves-fce","content_hash":"26b3e16b99f827316ee0d741cc959464bd0c813446c95aef8105c7fd1e6b09ff","title":"Implement aggregator feed federation","description":"","status":"open","priority":1,"issue_type":"feature","created_at":"2025-11-17T20:30:21.453326012-08:00","updated_at":"2025-11-17T20:30:21.453326012-08:00","source_repo":"."} 8 + {"id":"Coves-fqg","content_hash":"715c3a860b5000385f2787f89ea199d940dc06ef6b50902dd5fcdea71c96edbb","title":"Performance: PDS ListRecords bottleneck causing slow votes and feed loads","description":"## Problem Summary\n\nMultiple user-facing operations are slow due to repeated `listRecords` calls to the user's PDS (Personal Data Server). This affects both vote creation (~2-3s) and initial feed loads (~800ms).\n\n## Root Cause\n\nThe AppView queries the user's PDS to list ALL vote records whenever it needs to:\n1. Check if a vote already exists (for toggle logic)\n2. Populate the vote cache for viewer state\n\n**The problematic code path** (`service_impl.go:317-374`):\n\n```go\nfunc (s *voteService) findExistingVote(ctx context.Context, pdsClient pds.Client, subjectURI string) (*existingVote, error) {\n cursor := \"\"\n for {\n // Fetches 100 records per page from user's PDS\n result, err := pdsClient.ListRecords(ctx, voteCollection, pageSize, cursor)\n // Iterates through ALL records looking for matching subject URI\n for _, rec := range result.Records {\n // Linear search through every vote...\n }\n }\n}\n```\n\n## Affected Operations\n\n| Operation | Latency | Cause |\n|-----------|---------|-------|\n| Vote create/toggle | 2-3s | `getPDSClient` → token refresh w/ DPoP retry → `listRecords` (find existing) → `createRecord` |\n| First feed load | ~800ms | `listRecords` to populate vote cache |\n| Subsequent feeds | ~100ms | Cache hit (no PDS call) |\n\n## Why Token Refresh Adds Latency\n\nLogs show DPoP nonce mismatches requiring retries:\n```\n22:13:09 [AUTH_SUCCESS]\n22:13:11 WARN auth server request failed request=token-refresh statusCode=400 body=\"use_dpop_nonce\"\n22:13:12 INFO vote created\n```\n\nThe OAuth DPoP flow requires a server-provided nonce. On first request, the server rejects with the nonce, client retries with it. This adds ~1s per token refresh.\n\n## Scaling Concern\n\nThe `findExistingVote` function paginates through ALL votes (100 per page):\n\n| User's Vote Count | PDS Calls | Estimated Latency |\n|-------------------|-----------|-------------------|\n| 36 (current) | 1 | ~1s |\n| 200 | 2 | ~2s |\n| 500 | 5 | ~5s |\n| 1000 | 10 | ~10s |\n| 2000+ | 20+ | Timeout risk (30s limit) |\n\n**Active user projection**: 20 votes/day × 30 days = 600 votes/month → UX degradation within weeks of active use.\n\n## Current Architecture Flow\n\n```\n┌─────────────┐ ┌─────────────┐ ┌─────────────┐\n│ Mobile App │────▶│ AppView │────▶│ User's PDS │\n└─────────────┘ └─────────────┘ └─────────────┘\n │\n ▼\n ┌─────────────┐\n │ Vote Cache │ (in-memory, per-user)\n └─────────────┘\n```\n\n**Vote Creation Flow:**\n1. Mobile sends vote request to AppView\n2. AppView refreshes OAuth token (DPoP nonce retry) → **+1s**\n3. AppView calls `listRecords` on PDS to find existing vote → **+1s per 100 votes**\n4. AppView creates/deletes record on PDS → **+0.5s**\n5. Total: **2-3s+ for 36 votes**\n\n**First Feed Load Flow:**\n1. Mobile requests feed\n2. AppView checks if vote cache populated\n3. If not, calls `listRecords` to fetch ALL votes → **+800ms**\n4. Returns feed with viewer vote state\n\n## The Irony\n\nA vote cache already exists and is properly maintained:\n- Updated on every vote create/delete (`service_impl.go:157-159`, `service_impl.go:209-215`)\n- Indexed by subject URI for O(1) lookup\n- Cleared on sign-out\n\nBut it's **bypassed for vote existence checks** because the code treats PDS as \"source of truth\" to avoid eventual consistency issues.","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-13T14:29:00.821389478-08:00","updated_at":"2026-01-13T14:29:13.666892869-08:00","source_repo":"."} 7 9 {"id":"Coves-iw5","content_hash":"d3379c617b7583f6b88a0523b3cdd1e4415176877ab00b48710819f2484c4856","title":"Apply functional options pattern to NewGetCommunityHandler","description":"Location: internal/api/handlers/communityFeed/get.go\n\nApply functional options pattern for optional dependencies (votes, bluesky).\n\nDepends on: Coves-jdf (NewPostService refactor should be done first to establish pattern)\nParent: Coves-8k1","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-22T21:35:27.369297201-08:00","updated_at":"2025-12-22T21:35:58.115771178-08:00","source_repo":"."} 8 10 {"id":"Coves-jdf","content_hash":"cb27689d71f44fd555e29d2988f2ad053efb6c565cd4f803ff68eaade59c7546","title":"Apply functional options pattern to NewPostService","description":"Location: internal/core/posts/service.go\n\nCurrent constructor (7 params, 4 optional):\n```go\nfunc NewPostService(repo Repository, communityService communities.Service, aggregatorService aggregators.Service, blobService blobs.Service, unfurlService unfurl.Service, blueskyService blueskypost.Service, pdsURL string) Service\n```\n\nRefactor to:\n```go\ntype Option func(*postService)\n\nfunc WithAggregatorService(svc aggregators.Service) Option\nfunc WithBlobService(svc blobs.Service) Option\nfunc WithUnfurlService(svc unfurl.Service) Option\nfunc WithBlueskyService(svc blueskypost.Service) Option\n\nfunc NewPostService(repo Repository, communityService communities.Service, pdsURL string, opts ...Option) Service\n```\n\nFiles to update:\n- internal/core/posts/service.go (define Option type and With* functions)\n- cmd/server/main.go (production caller)\n- ~15 test files with call sites\n\nStart with this one as it has the most params and is most impacted.\nParent: Coves-8k1","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-22T21:35:27.264325344-08:00","updated_at":"2025-12-22T21:35:58.003863381-08:00","source_repo":"."} 9 11 {"id":"Coves-p44","content_hash":"6f12091f6e5f1ad9812f8da4ecd720e0f9df1afd1fdb593b3e52c32be0193d94","title":"Bluesky embed conversion Phase 2: resolve post and populate CID","description":"When converting a Bluesky URL to a social.coves.embed.post, we need to:\n\n1. Call blueskyService.ResolvePost() to get the full post data including CID\n2. Populate both URI and CID in the strongRef\n3. Consider caching/re-using resolved post data for rendering\n\nCurrently disabled in Phase 1 (text-only) because:\n- social.coves.embed.post requires a valid CID in com.atproto.repo.strongRef\n- Empty CID causes PDS to reject the record creation\n\nRelated files:\n- internal/core/posts/service.go:tryConvertBlueskyURLToPostEmbed()\n- internal/atproto/lexicon/social/coves/embed/post.json\n\nThis is part of the Bluesky post cross-posting feature (images/embeds phase).","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-22T21:25:23.540135876-08:00","updated_at":"2025-12-23T14:41:49.014541876-08:00","closed_at":"2025-12-23T14:41:49.014541876-08:00","source_repo":"."}