diff --git a/lib/utils/fc-cache.js b/lib/utils/fc-cache.js --- a/lib/utils/fc-cache.js +++ b/lib/utils/fc-cache.js @@ -2,6 +2,7 @@ import { NeynarClient, type FarcasterUser } from './neynar-client.js'; import sleep from './sleep.js'; +import type { NeynarChannel } from '../types/farcaster-types.js'; const cacheTimeout = 24 * 60 * 60 * 1000; // one day const failedQueryCacheTimeout = 5 * 60 * 1000; // five minutes @@ -18,6 +19,12 @@ +farcasterUser: ?FarcasterUser | Promise, }; +type FarcasterChannelQueryCacheEntry = { + +channelID: string, + +expirationTime: number, + +farcasterChannel: ?NeynarChannel | Promise, +}; + class FCCache { client: NeynarClient; @@ -25,6 +32,10 @@ farcasterUsernameQueryCache: Map = new Map(); + // Maps from Farcaster channel IDs to a cache entry for the channel's info + farcasterChannelQueryCache: Map = + new Map(); + constructor(client: NeynarClient) { this.client = client; } @@ -151,6 +162,83 @@ return farcasterUser; } + + getFarcasterChannelForChannelID(channelID: string): Promise { + const cachedChannelEntry = + this.getCachedFarcasterChannelEntryForChannelID(channelID); + + if (cachedChannelEntry) { + return Promise.resolve(cachedChannelEntry.farcasterChannel); + } + + const fetchFarcasterChannelPromise = (async () => { + let farcasterChannel; + try { + farcasterChannel = await Promise.race([ + this.client.fetchFarcasterChannelByID(channelID), + throwOnTimeout(`channel for ${channelID}`), + ]); + } catch (e) { + console.log(e); + return null; + } + + const timeout = + farcasterChannel === null ? failedQueryCacheTimeout : cacheTimeout; + + this.farcasterChannelQueryCache.set(channelID, { + channelID, + expirationTime: Date.now() + timeout, + farcasterChannel, + }); + + return farcasterChannel; + })(); + + this.farcasterChannelQueryCache.set(channelID, { + channelID, + expirationTime: Date.now() + queryTimeout * 2, + farcasterChannel: fetchFarcasterChannelPromise, + }); + + return fetchFarcasterChannelPromise; + } + + getCachedFarcasterChannelEntryForChannelID( + channelID: string, + ): ?FarcasterChannelQueryCacheEntry { + const cacheResult = this.farcasterChannelQueryCache.get(channelID); + if (!cacheResult) { + return undefined; + } + + const { expirationTime } = cacheResult; + if (expirationTime <= Date.now()) { + this.farcasterUsernameQueryCache.delete(channelID); + return undefined; + } + + return cacheResult; + } + + getCachedFarcasterChannelForChannelID(channelID: string): ?NeynarChannel { + const cacheResult = + this.getCachedFarcasterChannelEntryForChannelID(channelID); + if (!cacheResult) { + return undefined; + } + + const { farcasterChannel } = cacheResult; + if ( + typeof farcasterChannel !== 'object' || + farcasterChannel instanceof Promise || + !farcasterChannel + ) { + return undefined; + } + + return farcasterChannel; + } } export { FCCache };