extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.

Add game field to reactions for efficient Constellation queries

- Add required "game" field to reaction lexicon
- Update ReactionRecord type with game field
- Refactor fetchGameReactions to query by game URI instead of
individual move URIs (single Constellation query vs N queries)
- Group reactions by move URI client-side
- Update reaction API endpoint to include game field
- Update game page to use new reaction fetching approach

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

+43 -50
+5 -1
lexicons/boo.sky.go.reaction.json
··· 8 8 "key": "tid", 9 9 "record": { 10 10 "type": "object", 11 - "required": ["move", "text", "createdAt"], 11 + "required": ["game", "move", "text", "createdAt"], 12 12 "properties": { 13 + "game": { 14 + "type": "string", 15 + "description": "AT URI reference to the game record" 16 + }, 13 17 "move": { 14 18 "type": "string", 15 19 "description": "AT URI reference to the move record being reacted to"
+22 -33
src/lib/atproto-client.ts
··· 302 302 authorHandle?: string; 303 303 } 304 304 305 - /** Fetch all reactions for a specific move from Constellation backlinks. */ 306 - export async function fetchMoveReactions( 307 - moveAtUri: string 308 - ): Promise<ReactionWithAuthor[]> { 309 - const reactions: ReactionWithAuthor[] = []; 305 + /** Fetch all reactions for a game from Constellation backlinks. Returns a map of moveUri -> reactions. */ 306 + export async function fetchGameReactions( 307 + gameAtUri: string 308 + ): Promise<Map<string, ReactionWithAuthor[]>> { 309 + const reactionsByMove = new Map<string, ReactionWithAuthor[]>(); 310 310 let cursor: string | undefined; 311 311 312 312 try { 313 313 do { 314 314 const params = new URLSearchParams({ 315 - subject: moveAtUri, 316 - source: 'boo.sky.go.reaction:move', 315 + subject: gameAtUri, 316 + source: 'boo.sky.go.reaction:game', 317 317 limit: '100', 318 318 }); 319 319 if (cursor) params.set('cursor', cursor); ··· 330 330 // Skip records without URI or value 331 331 if (!rec.uri || !rec.value) continue; 332 332 333 + const reaction = rec.value as ReactionRecord; 334 + const moveUri = reaction.move; 335 + 333 336 // Parse author DID from the URI (at://did/collection/rkey) 334 337 const uriParts = rec.uri.split('/'); 335 338 const author = uriParts[2] || ''; 336 - reactions.push({ 337 - ...(rec.value as ReactionRecord), 339 + 340 + const reactionWithAuthor: ReactionWithAuthor = { 341 + ...reaction, 338 342 uri: rec.uri, 339 343 author, 340 - }); 344 + }; 345 + 346 + // Group by move URI 347 + const existing = reactionsByMove.get(moveUri) || []; 348 + existing.push(reactionWithAuthor); 349 + reactionsByMove.set(moveUri, existing); 341 350 } 342 351 cursor = body.cursor ?? undefined; 343 352 } while (cursor); ··· 345 354 console.error('Failed to fetch reactions from Constellation:', err); 346 355 } 347 356 348 - // Sort by creation time, newest first 349 - reactions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); 350 - return reactions; 351 - } 352 - 353 - /** Fetch all reactions for all moves in a game. Returns a map of moveUri -> reactions. */ 354 - export async function fetchGameReactions( 355 - moveUris: string[] 356 - ): Promise<Map<string, ReactionWithAuthor[]>> { 357 - const reactionsByMove = new Map<string, ReactionWithAuthor[]>(); 358 - 359 - // Fetch reactions for all moves in parallel 360 - const results = await Promise.all( 361 - moveUris.map(async (uri) => { 362 - const reactions = await fetchMoveReactions(uri); 363 - return { uri, reactions }; 364 - }) 365 - ); 366 - 367 - for (const { uri, reactions } of results) { 368 - if (reactions.length > 0) { 369 - reactionsByMove.set(uri, reactions); 370 - } 357 + // Sort reactions within each move by creation time, newest first 358 + for (const [moveUri, reactions] of reactionsByMove) { 359 + reactions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); 371 360 } 372 361 373 362 return reactionsByMove;
+1
src/lib/types.ts
··· 45 45 46 46 export interface ReactionRecord { 47 47 $type: 'boo.sky.go.reaction'; 48 + game: string; // AT URI of the game 48 49 move: string; // AT URI of the move being reacted to 49 50 emoji?: string; 50 51 text: string;
+1
src/routes/api/games/[id]/reaction/+server.ts
··· 61 61 62 62 const reactionRecord: Record<string, unknown> = { 63 63 $type: 'boo.sky.go.reaction', 64 + game: game.id, // AT URI of the game 64 65 move: moveUri, 65 66 text: text.trim(), 66 67 createdAt: now,
+14 -16
src/routes/game/[id]/+page.svelte
··· 177 177 passes = result.passes; 178 178 resigns = result.resigns; 179 179 180 - // Fetch reactions for all moves 181 - if (result.moves.length > 0) { 182 - const moveUris = result.moves.map((m) => getMoveUri(m)); 183 - const reactionsMap = await fetchGameReactions(moveUris); 184 - reactions = reactionsMap; 180 + // Fetch reactions for the game (single query via game backlink) 181 + const reactionsMap = await fetchGameReactions(data.gameAtUri); 182 + reactions = reactionsMap; 185 183 186 - // Resolve handles for reaction authors 187 - const authorDids = new Set<string>(); 188 - for (const reacts of reactionsMap.values()) { 189 - for (const r of reacts) { 190 - authorDids.add(r.author); 191 - } 184 + // Resolve handles for reaction authors 185 + const authorDids = new Set<string>(); 186 + for (const reacts of reactionsMap.values()) { 187 + for (const r of reacts) { 188 + authorDids.add(r.author); 192 189 } 193 - for (const did of authorDids) { 194 - resolveDidToHandle(did).then((h) => { 195 - reactionHandles = { ...reactionHandles, [did]: h }; 196 - }); 197 - } 190 + } 191 + for (const did of authorDids) { 192 + resolveDidToHandle(did).then((h) => { 193 + reactionHandles = { ...reactionHandles, [did]: h }; 194 + }); 198 195 } 199 196 200 197 loading = false; ··· 329 326 // Add the new reaction to local state 330 327 const newReaction: ReactionWithAuthor = { 331 328 $type: 'boo.sky.go.reaction', 329 + game: data.gameAtUri, 332 330 move: moveUri, 333 331 text: reactionText.trim(), 334 332 emoji: reactionEmoji || undefined,