A simple Bluesky bot to make sense of the noise, with responses powered by Gemini, similar to Grok.

feat: cache post URIs & prevent recursion loop

Changed files
+73 -10
src
+42
src/utils/cache.ts
··· 1 + interface CacheEntry<T> { 2 + value: T; 3 + expiry: number; 4 + } 5 + 6 + class TimedCache<T> { 7 + private cache = new Map<string, CacheEntry<T>>(); 8 + private ttl: number; // Time to live in milliseconds 9 + 10 + constructor(ttl: number) { 11 + this.ttl = ttl; 12 + } 13 + 14 + get(key: string): T | undefined { 15 + const entry = this.cache.get(key); 16 + if (!entry) { 17 + return undefined; 18 + } 19 + 20 + if (Date.now() > entry.expiry) { 21 + this.cache.delete(key); // Entry expired 22 + return undefined; 23 + } 24 + 25 + return entry.value; 26 + } 27 + 28 + set(key: string, value: T): void { 29 + const expiry = Date.now() + this.ttl; 30 + this.cache.set(key, { value, expiry }); 31 + } 32 + 33 + delete(key: string): void { 34 + this.cache.delete(key); 35 + } 36 + 37 + clear(): void { 38 + this.cache.clear(); 39 + } 40 + } 41 + 42 + export const postCache = new TimedCache<any>(2 * 60 * 1000); // 2 minutes cache
+8 -2
src/utils/conversation.ts
··· 11 11 import { env } from "../env"; 12 12 import { bot, ERROR_MESSAGE, MAX_GRAPHEMES } from "../core"; 13 13 import { parsePost, parsePostImages, traverseThread } from "./post"; 14 + import { postCache } from "../utils/cache"; 14 15 15 16 /* 16 17 Utilities ··· 123 124 }); 124 125 } 125 126 126 - const post = await bot.getPost(row.postUri); 127 + let post = postCache.get(row.postUri); 128 + if (!post) { 129 + post = await bot.getPost(row.postUri); 130 + postCache.set(row.postUri, post); 131 + } 127 132 const convoMessages = await getRelevantMessages(row!); 128 133 129 134 let parseResult = null; 130 135 try { 136 + const parsedPost = await parsePost(post, true, new Set()); 131 137 parseResult = { 132 138 context: yaml.dump({ 133 - post: await parsePost(post, true), 139 + post: parsedPost || null, 134 140 }), 135 141 messages: convoMessages.map((message) => { 136 142 const role = message.did == env.DID ? "model" : "user";
+23 -8
src/utils/post.ts
··· 8 8 import * as c from "../core"; 9 9 import * as yaml from "js-yaml"; 10 10 import type { ParsedPost } from "../types"; 11 + import { postCache } from "../utils/cache"; 11 12 12 13 export async function parsePost( 13 14 post: Post, 14 15 includeThread: boolean, 15 - ): Promise<ParsedPost> { 16 + seenUris: Set<string> = new Set(), 17 + ): Promise<ParsedPost | undefined> { 18 + if (seenUris.has(post.uri)) { 19 + return undefined; 20 + } 21 + seenUris.add(post.uri); 22 + 16 23 const [images, quotePost, ancestorPosts] = await Promise.all([ 17 24 parsePostImages(post), 18 - parseQuote(post), 25 + parseQuote(post, seenUris), 19 26 includeThread ? traverseThread(post) : Promise.resolve(null), 20 27 ]); 21 28 ··· 28 35 ...(quotePost && { quotePost }), 29 36 ...(ancestorPosts && { 30 37 thread: { 31 - ancestors: await Promise.all( 32 - ancestorPosts.map((ancestor) => parsePost(ancestor, false)), 33 - ), 38 + ancestors: (await Promise.all( 39 + ancestorPosts.map((ancestor) => parsePost(ancestor, false, seenUris)), 40 + )).filter((post): post is ParsedPost => post !== undefined), 34 41 }, 35 42 }), 36 43 }; 37 44 } 38 45 39 - async function parseQuote(post: Post) { 46 + async function parseQuote(post: Post, seenUris: Set<string>) { 40 47 if ( 41 48 !post.embed || (!post.embed.isRecord() && !post.embed.isRecordWithMedia()) 42 49 ) return undefined; 43 50 44 51 const record = (post.embed as RecordEmbed || RecordWithMediaEmbed).record; 45 - const embedPost = await c.bot.getPost(record.uri); 52 + if (seenUris.has(record.uri)) { 53 + return undefined; 54 + } 55 + 56 + let embedPost = postCache.get(record.uri); 57 + if (!embedPost) { 58 + embedPost = await c.bot.getPost(record.uri); 59 + postCache.set(record.uri, embedPost); 60 + } 46 61 47 - return await parsePost(embedPost, false); 62 + return await parsePost(embedPost, false, seenUris); 48 63 } 49 64 50 65 export function parsePostImages(post: Post) {