···13 "margin.at/internal/db"
14)
15000016type Author struct {
17 DID string `json:"did"`
18 Handle string `json:"handle"`
···148149 profiles := fetchProfilesForDIDs(collectDIDs(annotations, func(a db.Annotation) string { return a.AuthorDID }))
15000000000000000000151 result := make([]APIAnnotation, len(annotations))
152 for i, a := range annotations {
153 var body *APIBody
···208 }
209210 if database != nil {
211- result[i].LikeCount, _ = database.GetLikeCount(a.URI)
212- result[i].ReplyCount, _ = database.GetReplyCount(a.URI)
213- if viewerDID != "" {
214- if _, err := database.GetLikeByUserAndSubject(viewerDID, a.URI); err == nil {
215- result[i].ViewerHasLiked = true
216- }
217 }
218 }
219 }
···228229 profiles := fetchProfilesForDIDs(collectDIDs(highlights, func(h db.Highlight) string { return h.AuthorDID }))
23000000000000000000231 result := make([]APIHighlight, len(highlights))
232 for i, h := range highlights {
233 var selector *APISelector
···272 }
273274 if database != nil {
275- result[i].LikeCount, _ = database.GetLikeCount(h.URI)
276- result[i].ReplyCount, _ = database.GetReplyCount(h.URI)
277- if viewerDID != "" {
278- if _, err := database.GetLikeByUserAndSubject(viewerDID, h.URI); err == nil {
279- result[i].ViewerHasLiked = true
280- }
281 }
282 }
283 }
···292293 profiles := fetchProfilesForDIDs(collectDIDs(bookmarks, func(b db.Bookmark) string { return b.AuthorDID }))
29400000000000000000295 result := make([]APIBookmark, len(bookmarks))
296 for i, b := range bookmarks {
297 var tags []string
···326 CID: cid,
327 }
328 if database != nil {
329- result[i].LikeCount, _ = database.GetLikeCount(b.URI)
330- result[i].ReplyCount, _ = database.GetReplyCount(b.URI)
331- if viewerDID != "" {
332- if _, err := database.GetLikeByUserAndSubject(viewerDID, b.URI); err == nil {
333- result[i].ViewerHasLiked = true
334- }
335 }
336 }
337 }
···388389func fetchProfilesForDIDs(dids []string) map[string]Author {
390 profiles := make(map[string]Author)
0391392 for _, did := range dids {
393- profiles[did] = Author{
394- DID: did,
395- Handle: "unknown",
0396 }
397 }
398399- if len(dids) == 0 {
400 return profiles
401 }
402···404 var wg sync.WaitGroup
405 var mu sync.Mutex
406407- for i := 0; i < len(dids); i += batchSize {
408 end := i + batchSize
409- if end > len(dids) {
410- end = len(dids)
411 }
412- batch := dids[i:end]
413414 wg.Add(1)
415 go func(actors []string) {
···417 fetched, err := fetchProfiles(actors)
418 if err == nil {
419 mu.Lock()
0420 for k, v := range fetched {
421 profiles[k] = v
0422 }
423- mu.Unlock()
424 }
425 }(batch)
426 }
···484485 profiles := fetchProfilesForDIDs(collectDIDs(items, func(i db.CollectionItem) string { return i.AuthorDID }))
4860000000000000000000000000000000000000000000000000000000000000000000000000000487 result := make([]APICollectionItem, len(items))
488 for i, item := range items {
489 apiItem := APICollectionItem{
···495 Position: item.Position,
496 }
497498- if coll, err := database.GetCollectionByURI(item.CollectionURI); err == nil {
499- icon := ""
500- if coll.Icon != nil {
501- icon = *coll.Icon
502- }
503- desc := ""
504- if coll.Description != nil {
505- desc = *coll.Description
506- }
507- apiItem.Collection = &APICollection{
508- URI: coll.URI,
509- Name: coll.Name,
510- Description: desc,
511- Icon: icon,
512- Creator: profiles[coll.AuthorDID],
513- CreatedAt: coll.CreatedAt,
514- IndexedAt: coll.IndexedAt,
515- }
516 }
517518- if strings.Contains(item.AnnotationURI, "at.margin.annotation") {
519- if a, err := database.GetAnnotationByURI(item.AnnotationURI); err == nil {
520- hydrated, _ := hydrateAnnotations(database, []db.Annotation{*a}, viewerDID)
521- if len(hydrated) > 0 {
522- apiItem.Annotation = &hydrated[0]
523- }
524- }
525- } else if strings.Contains(item.AnnotationURI, "at.margin.highlight") {
526- if h, err := database.GetHighlightByURI(item.AnnotationURI); err == nil {
527- hydrated, _ := hydrateHighlights(database, []db.Highlight{*h}, viewerDID)
528- if len(hydrated) > 0 {
529- apiItem.Highlight = &hydrated[0]
530- }
531- }
532- } else if strings.Contains(item.AnnotationURI, "at.margin.bookmark") {
533- if b, err := database.GetBookmarkByURI(item.AnnotationURI); err == nil {
534- hydrated, _ := hydrateBookmarks(database, []db.Bookmark{*b}, viewerDID)
535- if len(hydrated) > 0 {
536- apiItem.Bookmark = &hydrated[0]
537- } else {
538- log.Printf("Failed to hydrate bookmark %s: empty hydration result\n", item.AnnotationURI)
539- }
540- } else {
541- }
542- } else {
543- log.Printf("Unknown item type for URI: %s\n", item.AnnotationURI)
544 }
545546 result[i] = apiItem
···577578 replyMap := make(map[string]APIReply)
579 if len(replyURIs) > 0 {
580- var replies []db.Reply
581- for _, uri := range replyURIs {
582- r, err := database.GetReplyByURI(uri)
583- if err == nil {
584- replies = append(replies, *r)
585 }
586- }
587-588- hydratedReplies, _ := hydrateReplies(replies)
589- for _, r := range hydratedReplies {
590- replyMap[r.ID] = r
591 }
592 }
593
···13 "margin.at/internal/db"
14)
1516+var (
17+ Cache ProfileCache = NewInMemoryCache(5 * time.Minute)
18+)
19+20type Author struct {
21 DID string `json:"did"`
22 Handle string `json:"handle"`
···152153 profiles := fetchProfilesForDIDs(collectDIDs(annotations, func(a db.Annotation) string { return a.AuthorDID }))
154155+ var likeCounts map[string]int
156+ var replyCounts map[string]int
157+ var viewerLikes map[string]bool
158+159+ if database != nil {
160+ uris := make([]string, len(annotations))
161+ for i, a := range annotations {
162+ uris[i] = a.URI
163+ }
164+165+ likeCounts, _ = database.GetLikeCounts(uris)
166+ replyCounts, _ = database.GetReplyCounts(uris)
167+ if viewerDID != "" {
168+ viewerLikes, _ = database.GetViewerLikes(viewerDID, uris)
169+ }
170+ }
171+172 result := make([]APIAnnotation, len(annotations))
173 for i, a := range annotations {
174 var body *APIBody
···229 }
230231 if database != nil {
232+ result[i].LikeCount = likeCounts[a.URI]
233+ result[i].ReplyCount = replyCounts[a.URI]
234+ if viewerLikes != nil && viewerLikes[a.URI] {
235+ result[i].ViewerHasLiked = true
00236 }
237 }
238 }
···247248 profiles := fetchProfilesForDIDs(collectDIDs(highlights, func(h db.Highlight) string { return h.AuthorDID }))
249250+ var likeCounts map[string]int
251+ var replyCounts map[string]int
252+ var viewerLikes map[string]bool
253+254+ if database != nil {
255+ uris := make([]string, len(highlights))
256+ for i, h := range highlights {
257+ uris[i] = h.URI
258+ }
259+260+ likeCounts, _ = database.GetLikeCounts(uris)
261+ replyCounts, _ = database.GetReplyCounts(uris)
262+ if viewerDID != "" {
263+ viewerLikes, _ = database.GetViewerLikes(viewerDID, uris)
264+ }
265+ }
266+267 result := make([]APIHighlight, len(highlights))
268 for i, h := range highlights {
269 var selector *APISelector
···308 }
309310 if database != nil {
311+ result[i].LikeCount = likeCounts[h.URI]
312+ result[i].ReplyCount = replyCounts[h.URI]
313+ if viewerLikes != nil && viewerLikes[h.URI] {
314+ result[i].ViewerHasLiked = true
00315 }
316 }
317 }
···326327 profiles := fetchProfilesForDIDs(collectDIDs(bookmarks, func(b db.Bookmark) string { return b.AuthorDID }))
328329+ var likeCounts map[string]int
330+ var replyCounts map[string]int
331+ var viewerLikes map[string]bool
332+333+ if database != nil {
334+ uris := make([]string, len(bookmarks))
335+ for i, b := range bookmarks {
336+ uris[i] = b.URI
337+ }
338+339+ likeCounts, _ = database.GetLikeCounts(uris)
340+ replyCounts, _ = database.GetReplyCounts(uris)
341+ if viewerDID != "" {
342+ viewerLikes, _ = database.GetViewerLikes(viewerDID, uris)
343+ }
344+ }
345+346 result := make([]APIBookmark, len(bookmarks))
347 for i, b := range bookmarks {
348 var tags []string
···377 CID: cid,
378 }
379 if database != nil {
380+ result[i].LikeCount = likeCounts[b.URI]
381+ result[i].ReplyCount = replyCounts[b.URI]
382+ if viewerLikes != nil && viewerLikes[b.URI] {
383+ result[i].ViewerHasLiked = true
00384 }
385 }
386 }
···437438func fetchProfilesForDIDs(dids []string) map[string]Author {
439 profiles := make(map[string]Author)
440+ missingDIDs := make([]string, 0)
441442 for _, did := range dids {
443+ if author, ok := Cache.Get(did); ok {
444+ profiles[did] = author
445+ } else {
446+ missingDIDs = append(missingDIDs, did)
447 }
448 }
449450+ if len(missingDIDs) == 0 {
451 return profiles
452 }
453···455 var wg sync.WaitGroup
456 var mu sync.Mutex
457458+ for i := 0; i < len(missingDIDs); i += batchSize {
459 end := i + batchSize
460+ if end > len(missingDIDs) {
461+ end = len(missingDIDs)
462 }
463+ batch := missingDIDs[i:end]
464465 wg.Add(1)
466 go func(actors []string) {
···468 fetched, err := fetchProfiles(actors)
469 if err == nil {
470 mu.Lock()
471+ defer mu.Unlock()
472 for k, v := range fetched {
473 profiles[k] = v
474+ Cache.Set(k, v)
475 }
0476 }
477 }(batch)
478 }
···536537 profiles := fetchProfilesForDIDs(collectDIDs(items, func(i db.CollectionItem) string { return i.AuthorDID }))
538539+ var collectionURIs []string
540+ var annotationURIs []string
541+ var highlightURIs []string
542+ var bookmarkURIs []string
543+544+ for _, item := range items {
545+ collectionURIs = append(collectionURIs, item.CollectionURI)
546+ if strings.Contains(item.AnnotationURI, "at.margin.annotation") {
547+ annotationURIs = append(annotationURIs, item.AnnotationURI)
548+ } else if strings.Contains(item.AnnotationURI, "at.margin.highlight") {
549+ highlightURIs = append(highlightURIs, item.AnnotationURI)
550+ } else if strings.Contains(item.AnnotationURI, "at.margin.bookmark") {
551+ bookmarkURIs = append(bookmarkURIs, item.AnnotationURI)
552+ }
553+ }
554+555+ collectionsMap := make(map[string]APICollection)
556+ if len(collectionURIs) > 0 {
557+ colls, err := database.GetCollectionsByURIs(collectionURIs)
558+ if err == nil {
559+ collProfiles := fetchProfilesForDIDs(collectDIDs(colls, func(c db.Collection) string { return c.AuthorDID }))
560+ for _, coll := range colls {
561+ icon := ""
562+ if coll.Icon != nil {
563+ icon = *coll.Icon
564+ }
565+ desc := ""
566+ if coll.Description != nil {
567+ desc = *coll.Description
568+ }
569+ collectionsMap[coll.URI] = APICollection{
570+ URI: coll.URI,
571+ Name: coll.Name,
572+ Description: desc,
573+ Icon: icon,
574+ Creator: collProfiles[coll.AuthorDID],
575+ CreatedAt: coll.CreatedAt,
576+ IndexedAt: coll.IndexedAt,
577+ }
578+ }
579+ }
580+ }
581+582+ annotationsMap := make(map[string]APIAnnotation)
583+ if len(annotationURIs) > 0 {
584+ rawAnnos, err := database.GetAnnotationsByURIs(annotationURIs)
585+ if err == nil {
586+ hydrated, _ := hydrateAnnotations(database, rawAnnos, viewerDID)
587+ for _, a := range hydrated {
588+ annotationsMap[a.ID] = a
589+ }
590+ }
591+ }
592+593+ highlightsMap := make(map[string]APIHighlight)
594+ if len(highlightURIs) > 0 {
595+ rawHighlights, err := database.GetHighlightsByURIs(highlightURIs)
596+ if err == nil {
597+ hydrated, _ := hydrateHighlights(database, rawHighlights, viewerDID)
598+ for _, h := range hydrated {
599+ highlightsMap[h.ID] = h
600+ }
601+ }
602+ }
603+604+ bookmarksMap := make(map[string]APIBookmark)
605+ if len(bookmarkURIs) > 0 {
606+ rawBookmarks, err := database.GetBookmarksByURIs(bookmarkURIs)
607+ if err == nil {
608+ hydrated, _ := hydrateBookmarks(database, rawBookmarks, viewerDID)
609+ for _, b := range hydrated {
610+ bookmarksMap[b.ID] = b
611+ }
612+ }
613+ }
614+615 result := make([]APICollectionItem, len(items))
616 for i, item := range items {
617 apiItem := APICollectionItem{
···623 Position: item.Position,
624 }
625626+ if coll, ok := collectionsMap[item.CollectionURI]; ok {
627+ apiItem.Collection = &coll
0000000000000000628 }
629630+ if val, ok := annotationsMap[item.AnnotationURI]; ok {
631+ apiItem.Annotation = &val
632+ } else if val, ok := highlightsMap[item.AnnotationURI]; ok {
633+ apiItem.Highlight = &val
634+ } else if val, ok := bookmarksMap[item.AnnotationURI]; ok {
635+ apiItem.Bookmark = &val
00000000000000000000636 }
637638 result[i] = apiItem
···669670 replyMap := make(map[string]APIReply)
671 if len(replyURIs) > 0 {
672+ replies, err := database.GetRepliesByURIs(replyURIs)
673+ if err == nil {
674+ hydratedReplies, _ := hydrateReplies(replies)
675+ for _, r := range hydratedReplies {
676+ replyMap[r.ID] = r
677 }
00000678 }
679 }
680
+1
backend/internal/db/db.go
···241 )`)
242 db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_subject_uri ON likes(subject_uri)`)
243 db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_author_did ON likes(author_did)`)
0244245 db.Exec(`CREATE TABLE IF NOT EXISTS collections (
246 uri TEXT PRIMARY KEY,
···241 )`)
242 db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_subject_uri ON likes(subject_uri)`)
243 db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_author_did ON likes(author_did)`)
244+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_author_subject ON likes(author_did, subject_uri)`)
245246 db.Exec(`CREATE TABLE IF NOT EXISTS collections (
247 uri TEXT PRIMARY KEY,