···1313 "margin.at/internal/db"
1414)
15151616+var (
1717+ Cache ProfileCache = NewInMemoryCache(5 * time.Minute)
1818+)
1919+1620type Author struct {
1721 DID string `json:"did"`
1822 Handle string `json:"handle"`
···148152149153 profiles := fetchProfilesForDIDs(collectDIDs(annotations, func(a db.Annotation) string { return a.AuthorDID }))
150154155155+ var likeCounts map[string]int
156156+ var replyCounts map[string]int
157157+ var viewerLikes map[string]bool
158158+159159+ if database != nil {
160160+ uris := make([]string, len(annotations))
161161+ for i, a := range annotations {
162162+ uris[i] = a.URI
163163+ }
164164+165165+ likeCounts, _ = database.GetLikeCounts(uris)
166166+ replyCounts, _ = database.GetReplyCounts(uris)
167167+ if viewerDID != "" {
168168+ viewerLikes, _ = database.GetViewerLikes(viewerDID, uris)
169169+ }
170170+ }
171171+151172 result := make([]APIAnnotation, len(annotations))
152173 for i, a := range annotations {
153174 var body *APIBody
···208229 }
209230210231 if database != nil {
211211- result[i].LikeCount, _ = database.GetLikeCount(a.URI)
212212- result[i].ReplyCount, _ = database.GetReplyCount(a.URI)
213213- if viewerDID != "" {
214214- if _, err := database.GetLikeByUserAndSubject(viewerDID, a.URI); err == nil {
215215- result[i].ViewerHasLiked = true
216216- }
232232+ result[i].LikeCount = likeCounts[a.URI]
233233+ result[i].ReplyCount = replyCounts[a.URI]
234234+ if viewerLikes != nil && viewerLikes[a.URI] {
235235+ result[i].ViewerHasLiked = true
217236 }
218237 }
219238 }
···228247229248 profiles := fetchProfilesForDIDs(collectDIDs(highlights, func(h db.Highlight) string { return h.AuthorDID }))
230249250250+ var likeCounts map[string]int
251251+ var replyCounts map[string]int
252252+ var viewerLikes map[string]bool
253253+254254+ if database != nil {
255255+ uris := make([]string, len(highlights))
256256+ for i, h := range highlights {
257257+ uris[i] = h.URI
258258+ }
259259+260260+ likeCounts, _ = database.GetLikeCounts(uris)
261261+ replyCounts, _ = database.GetReplyCounts(uris)
262262+ if viewerDID != "" {
263263+ viewerLikes, _ = database.GetViewerLikes(viewerDID, uris)
264264+ }
265265+ }
266266+231267 result := make([]APIHighlight, len(highlights))
232268 for i, h := range highlights {
233269 var selector *APISelector
···272308 }
273309274310 if database != nil {
275275- result[i].LikeCount, _ = database.GetLikeCount(h.URI)
276276- result[i].ReplyCount, _ = database.GetReplyCount(h.URI)
277277- if viewerDID != "" {
278278- if _, err := database.GetLikeByUserAndSubject(viewerDID, h.URI); err == nil {
279279- result[i].ViewerHasLiked = true
280280- }
311311+ result[i].LikeCount = likeCounts[h.URI]
312312+ result[i].ReplyCount = replyCounts[h.URI]
313313+ if viewerLikes != nil && viewerLikes[h.URI] {
314314+ result[i].ViewerHasLiked = true
281315 }
282316 }
283317 }
···292326293327 profiles := fetchProfilesForDIDs(collectDIDs(bookmarks, func(b db.Bookmark) string { return b.AuthorDID }))
294328329329+ var likeCounts map[string]int
330330+ var replyCounts map[string]int
331331+ var viewerLikes map[string]bool
332332+333333+ if database != nil {
334334+ uris := make([]string, len(bookmarks))
335335+ for i, b := range bookmarks {
336336+ uris[i] = b.URI
337337+ }
338338+339339+ likeCounts, _ = database.GetLikeCounts(uris)
340340+ replyCounts, _ = database.GetReplyCounts(uris)
341341+ if viewerDID != "" {
342342+ viewerLikes, _ = database.GetViewerLikes(viewerDID, uris)
343343+ }
344344+ }
345345+295346 result := make([]APIBookmark, len(bookmarks))
296347 for i, b := range bookmarks {
297348 var tags []string
···326377 CID: cid,
327378 }
328379 if database != nil {
329329- result[i].LikeCount, _ = database.GetLikeCount(b.URI)
330330- result[i].ReplyCount, _ = database.GetReplyCount(b.URI)
331331- if viewerDID != "" {
332332- if _, err := database.GetLikeByUserAndSubject(viewerDID, b.URI); err == nil {
333333- result[i].ViewerHasLiked = true
334334- }
380380+ result[i].LikeCount = likeCounts[b.URI]
381381+ result[i].ReplyCount = replyCounts[b.URI]
382382+ if viewerLikes != nil && viewerLikes[b.URI] {
383383+ result[i].ViewerHasLiked = true
335384 }
336385 }
337386 }
···388437389438func fetchProfilesForDIDs(dids []string) map[string]Author {
390439 profiles := make(map[string]Author)
440440+ missingDIDs := make([]string, 0)
391441392442 for _, did := range dids {
393393- profiles[did] = Author{
394394- DID: did,
395395- Handle: "unknown",
443443+ if author, ok := Cache.Get(did); ok {
444444+ profiles[did] = author
445445+ } else {
446446+ missingDIDs = append(missingDIDs, did)
396447 }
397448 }
398449399399- if len(dids) == 0 {
450450+ if len(missingDIDs) == 0 {
400451 return profiles
401452 }
402453···404455 var wg sync.WaitGroup
405456 var mu sync.Mutex
406457407407- for i := 0; i < len(dids); i += batchSize {
458458+ for i := 0; i < len(missingDIDs); i += batchSize {
408459 end := i + batchSize
409409- if end > len(dids) {
410410- end = len(dids)
460460+ if end > len(missingDIDs) {
461461+ end = len(missingDIDs)
411462 }
412412- batch := dids[i:end]
463463+ batch := missingDIDs[i:end]
413464414465 wg.Add(1)
415466 go func(actors []string) {
···417468 fetched, err := fetchProfiles(actors)
418469 if err == nil {
419470 mu.Lock()
471471+ defer mu.Unlock()
420472 for k, v := range fetched {
421473 profiles[k] = v
474474+ Cache.Set(k, v)
422475 }
423423- mu.Unlock()
424476 }
425477 }(batch)
426478 }
···484536485537 profiles := fetchProfilesForDIDs(collectDIDs(items, func(i db.CollectionItem) string { return i.AuthorDID }))
486538539539+ var collectionURIs []string
540540+ var annotationURIs []string
541541+ var highlightURIs []string
542542+ var bookmarkURIs []string
543543+544544+ for _, item := range items {
545545+ collectionURIs = append(collectionURIs, item.CollectionURI)
546546+ if strings.Contains(item.AnnotationURI, "at.margin.annotation") {
547547+ annotationURIs = append(annotationURIs, item.AnnotationURI)
548548+ } else if strings.Contains(item.AnnotationURI, "at.margin.highlight") {
549549+ highlightURIs = append(highlightURIs, item.AnnotationURI)
550550+ } else if strings.Contains(item.AnnotationURI, "at.margin.bookmark") {
551551+ bookmarkURIs = append(bookmarkURIs, item.AnnotationURI)
552552+ }
553553+ }
554554+555555+ collectionsMap := make(map[string]APICollection)
556556+ if len(collectionURIs) > 0 {
557557+ colls, err := database.GetCollectionsByURIs(collectionURIs)
558558+ if err == nil {
559559+ collProfiles := fetchProfilesForDIDs(collectDIDs(colls, func(c db.Collection) string { return c.AuthorDID }))
560560+ for _, coll := range colls {
561561+ icon := ""
562562+ if coll.Icon != nil {
563563+ icon = *coll.Icon
564564+ }
565565+ desc := ""
566566+ if coll.Description != nil {
567567+ desc = *coll.Description
568568+ }
569569+ collectionsMap[coll.URI] = APICollection{
570570+ URI: coll.URI,
571571+ Name: coll.Name,
572572+ Description: desc,
573573+ Icon: icon,
574574+ Creator: collProfiles[coll.AuthorDID],
575575+ CreatedAt: coll.CreatedAt,
576576+ IndexedAt: coll.IndexedAt,
577577+ }
578578+ }
579579+ }
580580+ }
581581+582582+ annotationsMap := make(map[string]APIAnnotation)
583583+ if len(annotationURIs) > 0 {
584584+ rawAnnos, err := database.GetAnnotationsByURIs(annotationURIs)
585585+ if err == nil {
586586+ hydrated, _ := hydrateAnnotations(database, rawAnnos, viewerDID)
587587+ for _, a := range hydrated {
588588+ annotationsMap[a.ID] = a
589589+ }
590590+ }
591591+ }
592592+593593+ highlightsMap := make(map[string]APIHighlight)
594594+ if len(highlightURIs) > 0 {
595595+ rawHighlights, err := database.GetHighlightsByURIs(highlightURIs)
596596+ if err == nil {
597597+ hydrated, _ := hydrateHighlights(database, rawHighlights, viewerDID)
598598+ for _, h := range hydrated {
599599+ highlightsMap[h.ID] = h
600600+ }
601601+ }
602602+ }
603603+604604+ bookmarksMap := make(map[string]APIBookmark)
605605+ if len(bookmarkURIs) > 0 {
606606+ rawBookmarks, err := database.GetBookmarksByURIs(bookmarkURIs)
607607+ if err == nil {
608608+ hydrated, _ := hydrateBookmarks(database, rawBookmarks, viewerDID)
609609+ for _, b := range hydrated {
610610+ bookmarksMap[b.ID] = b
611611+ }
612612+ }
613613+ }
614614+487615 result := make([]APICollectionItem, len(items))
488616 for i, item := range items {
489617 apiItem := APICollectionItem{
···495623 Position: item.Position,
496624 }
497625498498- if coll, err := database.GetCollectionByURI(item.CollectionURI); err == nil {
499499- icon := ""
500500- if coll.Icon != nil {
501501- icon = *coll.Icon
502502- }
503503- desc := ""
504504- if coll.Description != nil {
505505- desc = *coll.Description
506506- }
507507- apiItem.Collection = &APICollection{
508508- URI: coll.URI,
509509- Name: coll.Name,
510510- Description: desc,
511511- Icon: icon,
512512- Creator: profiles[coll.AuthorDID],
513513- CreatedAt: coll.CreatedAt,
514514- IndexedAt: coll.IndexedAt,
515515- }
626626+ if coll, ok := collectionsMap[item.CollectionURI]; ok {
627627+ apiItem.Collection = &coll
516628 }
517629518518- if strings.Contains(item.AnnotationURI, "at.margin.annotation") {
519519- if a, err := database.GetAnnotationByURI(item.AnnotationURI); err == nil {
520520- hydrated, _ := hydrateAnnotations(database, []db.Annotation{*a}, viewerDID)
521521- if len(hydrated) > 0 {
522522- apiItem.Annotation = &hydrated[0]
523523- }
524524- }
525525- } else if strings.Contains(item.AnnotationURI, "at.margin.highlight") {
526526- if h, err := database.GetHighlightByURI(item.AnnotationURI); err == nil {
527527- hydrated, _ := hydrateHighlights(database, []db.Highlight{*h}, viewerDID)
528528- if len(hydrated) > 0 {
529529- apiItem.Highlight = &hydrated[0]
530530- }
531531- }
532532- } else if strings.Contains(item.AnnotationURI, "at.margin.bookmark") {
533533- if b, err := database.GetBookmarkByURI(item.AnnotationURI); err == nil {
534534- hydrated, _ := hydrateBookmarks(database, []db.Bookmark{*b}, viewerDID)
535535- if len(hydrated) > 0 {
536536- apiItem.Bookmark = &hydrated[0]
537537- } else {
538538- log.Printf("Failed to hydrate bookmark %s: empty hydration result\n", item.AnnotationURI)
539539- }
540540- } else {
541541- }
542542- } else {
543543- log.Printf("Unknown item type for URI: %s\n", item.AnnotationURI)
630630+ if val, ok := annotationsMap[item.AnnotationURI]; ok {
631631+ apiItem.Annotation = &val
632632+ } else if val, ok := highlightsMap[item.AnnotationURI]; ok {
633633+ apiItem.Highlight = &val
634634+ } else if val, ok := bookmarksMap[item.AnnotationURI]; ok {
635635+ apiItem.Bookmark = &val
544636 }
545637546638 result[i] = apiItem
···577669578670 replyMap := make(map[string]APIReply)
579671 if len(replyURIs) > 0 {
580580- var replies []db.Reply
581581- for _, uri := range replyURIs {
582582- r, err := database.GetReplyByURI(uri)
583583- if err == nil {
584584- replies = append(replies, *r)
672672+ replies, err := database.GetRepliesByURIs(replyURIs)
673673+ if err == nil {
674674+ hydratedReplies, _ := hydrateReplies(replies)
675675+ for _, r := range hydratedReplies {
676676+ replyMap[r.ID] = r
585677 }
586586- }
587587-588588- hydratedReplies, _ := hydrateReplies(replies)
589589- for _, r := range hydratedReplies {
590590- replyMap[r.ID] = r
591678 }
592679 }
593680
+1
backend/internal/db/db.go
···241241 )`)
242242 db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_subject_uri ON likes(subject_uri)`)
243243 db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_author_did ON likes(author_did)`)
244244+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_author_subject ON likes(author_did, subject_uri)`)
244245245246 db.Exec(`CREATE TABLE IF NOT EXISTS collections (
246247 uri TEXT PRIMARY KEY,