Barazo AppView backend barazo.forum
at main 122 lines 4.1 kB view raw
1import { sql } from 'drizzle-orm' 2import type { Database } from '../db/index.js' 3import type { Logger } from '../lib/logger.js' 4import { interactionGraph } from '../db/schema/interaction-graph.js' 5import { replies } from '../db/schema/replies.js' 6 7// --------------------------------------------------------------------------- 8// Types 9// --------------------------------------------------------------------------- 10 11export interface InteractionGraphService { 12 recordReply(replierDid: string, topicAuthorDid: string, communityId: string): Promise<void> 13 recordReaction(reactorDid: string, contentAuthorDid: string, communityId: string): Promise<void> 14 recordCoParticipation(topicUri: string, communityId: string): Promise<void> 15} 16 17// --------------------------------------------------------------------------- 18// Constants 19// --------------------------------------------------------------------------- 20 21const MAX_COPARTICIPATION_AUTHORS = 50 22 23// --------------------------------------------------------------------------- 24// Factory 25// --------------------------------------------------------------------------- 26 27export function createInteractionGraphService( 28 db: Database, 29 logger: Logger 30): InteractionGraphService { 31 async function upsertInteraction( 32 sourceDid: string, 33 targetDid: string, 34 communityId: string, 35 interactionType: 'reply' | 'reaction' | 'topic_coparticipation' 36 ): Promise<void> { 37 // Skip self-interaction 38 if (sourceDid === targetDid) return 39 40 await db 41 .insert(interactionGraph) 42 .values({ 43 sourceDid, 44 targetDid, 45 communityId, 46 interactionType, 47 weight: 1, 48 firstInteractionAt: new Date(), 49 lastInteractionAt: new Date(), 50 }) 51 .onConflictDoUpdate({ 52 target: [ 53 interactionGraph.sourceDid, 54 interactionGraph.targetDid, 55 interactionGraph.communityId, 56 interactionGraph.interactionType, 57 ], 58 set: { 59 weight: sql`${interactionGraph.weight} + 1`, 60 lastInteractionAt: new Date(), 61 }, 62 }) 63 } 64 65 async function recordReply( 66 replierDid: string, 67 topicAuthorDid: string, 68 communityId: string 69 ): Promise<void> { 70 await upsertInteraction(replierDid, topicAuthorDid, communityId, 'reply') 71 logger.debug({ replierDid, topicAuthorDid, communityId }, 'Recorded reply interaction') 72 } 73 74 async function recordReaction( 75 reactorDid: string, 76 contentAuthorDid: string, 77 communityId: string 78 ): Promise<void> { 79 await upsertInteraction(reactorDid, contentAuthorDid, communityId, 'reaction') 80 logger.debug({ reactorDid, contentAuthorDid, communityId }, 'Recorded reaction interaction') 81 } 82 83 async function recordCoParticipation(topicUri: string, communityId: string): Promise<void> { 84 // Get unique reply authors for the topic 85 const authorRows = await db 86 .select({ authorDid: replies.authorDid }) 87 .from(replies) 88 .where(sql`${replies.rootUri} = ${topicUri}`) 89 90 // Deduplicate 91 const uniqueAuthors = [...new Set(authorRows.map((r) => r.authorDid))] 92 93 // Skip if too many authors or not enough for pairs 94 if (uniqueAuthors.length > MAX_COPARTICIPATION_AUTHORS || uniqueAuthors.length < 2) { 95 if (uniqueAuthors.length > MAX_COPARTICIPATION_AUTHORS) { 96 logger.debug( 97 { topicUri, authorCount: uniqueAuthors.length }, 98 'Skipping co-participation: too many authors' 99 ) 100 } 101 return 102 } 103 104 // Create pairwise interactions 105 for (let i = 0; i < uniqueAuthors.length; i++) { 106 const authorA = uniqueAuthors[i] 107 if (!authorA) continue 108 for (let j = i + 1; j < uniqueAuthors.length; j++) { 109 const authorB = uniqueAuthors[j] 110 if (!authorB) continue 111 await upsertInteraction(authorA, authorB, communityId, 'topic_coparticipation') 112 } 113 } 114 115 logger.debug( 116 { topicUri, authorCount: uniqueAuthors.length, communityId }, 117 'Recorded co-participation interactions' 118 ) 119 } 120 121 return { recordReply, recordReaction, recordCoParticipation } 122}