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 @@ -33,6 +33,11 @@ | Promise>, }; +type QueuedChannelQuery = { + +channelID: string, + +resolve: (?NeynarChannel) => void, +}; + class FCCache { client: NeynarClient; @@ -50,6 +55,8 @@ FollowedFarcasterChannelsQueryCacheEntry, > = new Map(); + queuedChannelQueries: Array = []; + constructor(client: NeynarClient) { this.client = client; } @@ -186,6 +193,15 @@ } const fetchFarcasterChannelPromise = (async () => { + let channelQuery; + const promise = new Promise(resolve => { + channelQuery = { + channelID, + resolve, + }; + this.queuedChannelQueries.push(channelQuery); + }); + // First, we finish any ongoing fetches of followed channels, // since our channel might be one of them const fidsInFollowedChannelQueryCache = [ @@ -216,34 +232,106 @@ if (channel.id !== channelID) { continue; } + this.queuedChannelQueries = this.queuedChannelQueries.filter( + possibleChannelQuery => possibleChannelQuery !== channelQuery, + ); return channel; } } } - let farcasterChannel; - try { - farcasterChannel = await Promise.race([ - this.client.fetchFarcasterChannelByID(channelID), - throwOnTimeout(`channel for ${channelID}`), - ]); - } catch (e) { - console.log(e); - return null; + if (this.queuedChannelQueries.length === 0) { + return promise; } - this.farcasterChannelQueryCache.set(channelID, { - channelID, - expirationTime: Date.now() + cacheTimeout, - farcasterChannel, - }); + const channelQueries = this.queuedChannelQueries; + this.queuedChannelQueries = []; + + const channelIDs = channelQueries.map(query => query.channelID); + const bulkQueryPromise = (async () => { + // If we only need to query for one, don't bother with the bulk API + if (channelIDs.length === 1) { + const [chanID] = channelIDs; + + let farcasterChannel; + try { + farcasterChannel = await Promise.race([ + this.client.fetchFarcasterChannelByID(chanID), + throwOnTimeout(`channel for ${chanID}`), + ]); + } catch (e) { + console.log(e); + return null; + } + + this.farcasterChannelQueryCache.set(chanID, { + channelID: chanID, + expirationTime: Date.now() + cacheTimeout, + farcasterChannel, + }); + + return farcasterChannel ? [farcasterChannel] : []; + } + + let farcasterChannels; + try { + farcasterChannels = await Promise.race([ + this.client.fetchFarcasterChannelsByIDs(channelIDs), + throwOnTimeout(`channels for ${JSON.stringify(channelIDs)}`), + ]); + } catch (e) { + console.log(e); + return null; + } + + const channelIDsInResultSet = new Set(); + for (const farcasterChannel of farcasterChannels) { + const chanID = farcasterChannel.id; + channelIDsInResultSet.add(chanID); + this.farcasterChannelQueryCache.set(chanID, { + channelID: chanID, + expirationTime: Date.now() + cacheTimeout, + farcasterChannel, + }); + } - return farcasterChannel; + for (const chanID of channelIDs) { + if (!channelIDsInResultSet.has(chanID)) { + this.farcasterChannelQueryCache.set(chanID, { + channelID: chanID, + expirationTime: Date.now() + cacheTimeout, + farcasterChannel: undefined, + }); + } + } + + return farcasterChannels; + })(); + + for (const query of channelQueries) { + const { channelID: chanID, resolve } = query; + void (async () => { + const channels = await bulkQueryPromise; + if (!channels) { + resolve(null); + return; + } + for (const channel of channels) { + if (channel.id === chanID) { + resolve(channel); + return; + } + } + resolve(undefined); + })(); + } + + return promise; })(); this.farcasterChannelQueryCache.set(channelID, { channelID, - expirationTime: Date.now() + queryTimeout * 2, + expirationTime: Date.now() + queryTimeout * 4, farcasterChannel: fetchFarcasterChannelPromise, });