diff --git a/keyserver/src/updaters/thread-updaters.js b/keyserver/src/updaters/thread-updaters.js --- a/keyserver/src/updaters/thread-updaters.js +++ b/keyserver/src/updaters/thread-updaters.js @@ -67,7 +67,7 @@ verifyUserOrCookieIDs, } from '../fetchers/user-fetchers.js'; import type { Viewer } from '../session/viewer.js'; -import { neynarClient, fcCache } from '../utils/fc-cache.js'; +import { fcCache } from '../utils/fc-cache.js'; import { findUserIdentities } from '../utils/identity-utils.js'; import { redisCache } from '../utils/redis-cache.js'; import RelationshipChangeset from '../utils/relationship-changeset.js'; @@ -990,7 +990,7 @@ ignorePromiseRejections( (async () => { const followedChannels = - await neynarClient?.fetchFollowedFarcasterChannels(farcasterID); + await fcCache?.getFollowedFarcasterChannelsForFID(farcasterID); if (followedChannels) { await Promise.allSettled( followedChannels.map(followedChannel => diff --git a/lib/components/base-auto-join-community-handler.react.js b/lib/components/base-auto-join-community-handler.react.js --- a/lib/components/base-auto-join-community-handler.react.js +++ b/lib/components/base-auto-join-community-handler.react.js @@ -63,7 +63,7 @@ const fid = useCurrentUserFID(); - const neynarClient = React.useContext(NeynarClientContext)?.client; + const fcCache = React.useContext(NeynarClientContext)?.fcCache; const identityClientContext = React.useContext(IdentityClientContext); invariant(identityClientContext, 'IdentityClientContext should be set'); @@ -87,7 +87,7 @@ } prevCanQueryRef.current = canQuery; - if (!canQuery || !isActive || !fid || !neynarClient) { + if (!canQuery || !isActive || !fid || !fcCache) { return; } @@ -100,13 +100,17 @@ })(); const followedFarcasterChannelsPromise = - neynarClient.fetchFollowedFarcasterChannels(fid); + fcCache.getFollowedFarcasterChannelsForFID(fid); const [authMetadata, followedFarcasterChannels] = await Promise.all([ authMetadataPromise, followedFarcasterChannelsPromise, ]); + if (!followedFarcasterChannels) { + return; + } + const headers = authMetadata ? createDefaultHTTPRequestHeaders(authMetadata) : {}; @@ -178,7 +182,7 @@ threadInfos, fid, isActive, - neynarClient, + fcCache, getAuthMetadata, keyserverInfos, canQuery, 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 @@ -25,6 +25,14 @@ +farcasterChannel: ?NeynarChannel | Promise, }; +type FollowedFarcasterChannelsQueryCacheEntry = { + +fid: string, + +expirationTime: number, + +followedFarcasterChannels: + | ?$ReadOnlyArray + | Promise>, +}; + class FCCache { client: NeynarClient; @@ -36,6 +44,12 @@ farcasterChannelQueryCache: Map = new Map(); + // Maps from FIDs to a cache entry for the Farcaster user's followed channels + followedFarcasterChannelsQueryCache: Map< + string, + FollowedFarcasterChannelsQueryCacheEntry, + > = new Map(); + constructor(client: NeynarClient) { this.client = client; } @@ -236,6 +250,83 @@ return farcasterChannel; } + + getFollowedFarcasterChannelsForFID( + fid: string, + ): Promise> { + const cachedChannelEntry = + this.getCachedFollowedFarcasterChannelsEntryForFID(fid); + + if (cachedChannelEntry) { + return Promise.resolve(cachedChannelEntry.followedFarcasterChannels); + } + + const fetchFollowedFarcasterChannelsPromise = (async () => { + let followedFarcasterChannels; + try { + followedFarcasterChannels = await Promise.race([ + this.client.fetchFollowedFarcasterChannels(fid), + throwOnTimeout(`followed channels for ${fid}`), + ]); + } catch (e) { + console.log(e); + return null; + } + + this.followedFarcasterChannelsQueryCache.set(fid, { + fid, + expirationTime: Date.now() + cacheTimeout, + followedFarcasterChannels, + }); + + return followedFarcasterChannels; + })(); + + this.followedFarcasterChannelsQueryCache.set(fid, { + fid, + expirationTime: Date.now() + queryTimeout * 2, + followedFarcasterChannels: fetchFollowedFarcasterChannelsPromise, + }); + + return fetchFollowedFarcasterChannelsPromise; + } + + getCachedFollowedFarcasterChannelsEntryForFID( + fid: string, + ): ?FollowedFarcasterChannelsQueryCacheEntry { + const cacheResult = this.followedFarcasterChannelsQueryCache.get(fid); + if (!cacheResult) { + return undefined; + } + + const { expirationTime } = cacheResult; + if (expirationTime <= Date.now()) { + this.followedFarcasterChannelsQueryCache.delete(fid); + return undefined; + } + + return cacheResult; + } + + getCachedFollowedFarcasterChannelsForFID( + fid: string, + ): ?$ReadOnlyArray { + const cacheResult = this.getCachedFollowedFarcasterChannelsEntryForFID(fid); + if (!cacheResult) { + return undefined; + } + + const { followedFarcasterChannels } = cacheResult; + if ( + typeof followedFarcasterChannels !== 'object' || + followedFarcasterChannels instanceof Promise || + !followedFarcasterChannels + ) { + return undefined; + } + + return followedFarcasterChannels; + } } export { FCCache }; diff --git a/native/community-settings/tag-farcaster-channel/tag-channel-button.react.js b/native/community-settings/tag-farcaster-channel/tag-channel-button.react.js --- a/native/community-settings/tag-farcaster-channel/tag-channel-button.react.js +++ b/native/community-settings/tag-farcaster-channel/tag-channel-button.react.js @@ -41,17 +41,22 @@ const neynarClientContext = React.useContext(NeynarClientContext); invariant(neynarClientContext, 'NeynarClientContext is missing'); - const { client } = neynarClientContext; + const { fcCache } = neynarClientContext; React.useEffect(() => { void (async () => { - const channels = await client.fetchFollowedFarcasterChannels(fid); + const channels = await fcCache.getFollowedFarcasterChannelsForFID(fid); + if (!channels) { + return; + } - const sortedChannels = channels.sort((a, b) => a.id.localeCompare(b.id)); + const sortedChannels = [...channels].sort((a, b) => + a.id.localeCompare(b.id), + ); setChannelOptions(sortedChannels); })(); - }, [client, fid]); + }, [fcCache, fid]); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); diff --git a/web/tag-farcaster-channel/create-farcaster-channel-tag-modal.react.js b/web/tag-farcaster-channel/create-farcaster-channel-tag-modal.react.js --- a/web/tag-farcaster-channel/create-farcaster-channel-tag-modal.react.js +++ b/web/tag-farcaster-channel/create-farcaster-channel-tag-modal.react.js @@ -33,7 +33,7 @@ const neynarClientContext = React.useContext(NeynarClientContext); invariant(neynarClientContext, 'NeynarClientContext is missing'); - const { client, fcCache } = neynarClientContext; + const { fcCache } = neynarClientContext; const [channelOptions, setChannelOptions] = React.useState< $ReadOnlyArray, @@ -44,9 +44,12 @@ React.useEffect(() => { void (async () => { - const channels = await client.fetchFollowedFarcasterChannels(fid); + const channels = await fcCache.getFollowedFarcasterChannelsForFID(fid); + if (!channels) { + return; + } - const sortedChannels = channels + const sortedChannels = [...channels] .sort((a, b) => a.id.localeCompare(b.id)) .map(channel => ({ id: channel.id, @@ -57,7 +60,7 @@ setChannelOptions(options); })(); - }, [client, fid]); + }, [fcCache, fid]); const onChangeSelectedOption = React.useCallback((option: string) => { setError(null);