diff --git a/lib/hooks/fc-cache.js b/lib/hooks/fc-cache.js index 168855921..dfc9eccd4 100644 --- a/lib/hooks/fc-cache.js +++ b/lib/hooks/fc-cache.js @@ -1,144 +1,144 @@ // @flow import * as React from 'react'; import { NeynarClientContext } from '../components/neynar-client-provider.react.js'; import { getFCNames } from '../utils/farcaster-helpers.js'; type BaseFCInfo = { +fid?: ?string, +farcasterUsername?: ?string, ... }; export type UseFCNamesOptions = { +allAtOnce?: ?boolean, }; function useFCNames( users: $ReadOnlyArray, options?: ?UseFCNamesOptions, ): T[] { const neynarClientContext = React.useContext(NeynarClientContext); const fcCache = neynarClientContext?.fcCache; const allAtOnce = options?.allAtOnce ?? false; const cachedInfo = React.useMemo( () => users.map(user => { if (!user) { return user; } const { fid, farcasterUsername } = user; let cachedResult = null; if (farcasterUsername) { cachedResult = farcasterUsername; } else if (fid && fcCache) { - cachedResult = fcCache.getCachedFarcasterUsernameForFID(fid); + cachedResult = fcCache.getCachedFarcasterUserForFID(fid); } return { input: user, fid, cachedResult, }; }), [users, fcCache], ); const [fetchedFIDs, setFetchedFIDs] = React.useState<$ReadOnlySet>( new Set(), ); const [farcasterUsernames, setFarcasterUsernames] = React.useState< $ReadOnlyMap, >(new Map()); React.useEffect(() => { if (!fcCache) { return; } const needFetchUsers: $ReadOnlyArray<{ +fid: string, +farcasterUsername?: ?string, }> = cachedInfo .map(user => { if (!user) { return null; } const { fid, cachedResult } = user; if (cachedResult || !fid || fetchedFIDs.has(fid)) { return null; } return { fid }; }) .filter(Boolean); if (needFetchUsers.length === 0) { return; } const needFetchFIDs = needFetchUsers.map(({ fid }) => fid); setFetchedFIDs(oldFetchedFIDs => { const newFetchedFIDs = new Set(oldFetchedFIDs); for (const fid of needFetchFIDs) { newFetchedFIDs.add(fid); } return newFetchedFIDs; }); if (allAtOnce) { void (async () => { const withFarcasterUsernames = await getFCNames( fcCache, needFetchUsers, ); setFarcasterUsernames(oldFarcasterUsernames => { const newFarcasterUsernames = new Map(oldFarcasterUsernames); for (let i = 0; i < withFarcasterUsernames.length; i++) { const fid = needFetchFIDs[i]; const result = withFarcasterUsernames[i].farcasterUsername; if (result) { newFarcasterUsernames.set(fid, result); } } return newFarcasterUsernames; }); })(); return; } for (const fid of needFetchFIDs) { void (async () => { - const [result] = await fcCache.getFarcasterUsernamesForFIDs([fid]); + const [result] = await fcCache.getFarcasterUsersForFIDs([fid]); if (!result) { return; } setFarcasterUsernames(oldFarcasterUsernames => { const newFarcasterUsernames = new Map(oldFarcasterUsernames); - newFarcasterUsernames.set(fid, result); + newFarcasterUsernames.set(fid, result.username); return newFarcasterUsernames; }); })(); } }, [cachedInfo, fetchedFIDs, fcCache, allAtOnce]); return React.useMemo( () => cachedInfo.map(user => { if (!user) { return user; } const { input, fid, cachedResult } = user; if (cachedResult) { return { ...input, farcasterUsername: cachedResult }; } else if (!fid) { return input; } const farcasterUsername = farcasterUsernames.get(fid); if (farcasterUsername) { return { ...input, farcasterUsername }; } return input; }), [cachedInfo, farcasterUsernames], ); } export { useFCNames }; diff --git a/lib/utils/farcaster-helpers.js b/lib/utils/farcaster-helpers.js index 00d3aa599..579f88efa 100644 --- a/lib/utils/farcaster-helpers.js +++ b/lib/utils/farcaster-helpers.js @@ -1,79 +1,79 @@ // @flow import { FCCache } from './fc-cache.js'; export type BaseFCInfo = { +fid?: ?string, +farcasterUsername?: ?string, ... }; export type GetFCNames = ( users: $ReadOnlyArray, ) => Promise; async function getFCNames( fcCache: FCCache, users: $ReadOnlyArray, ): Promise { const info = users.map(user => { if (!user) { return user; } const { fid, farcasterUsername } = user; let cachedResult = null; if (farcasterUsername) { cachedResult = farcasterUsername; } else if (fid) { - cachedResult = fcCache.getCachedFarcasterUsernameForFID(fid); + cachedResult = fcCache.getCachedFarcasterUserForFID(fid)?.username; } return { input: user, fid, cachedResult, }; }); const needFetch = info .map(user => { if (!user) { return null; } const { fid, cachedResult } = user; if (cachedResult || !fid) { return null; } return fid; }) .filter(Boolean); const farcasterUsernames = new Map(); if (needFetch.length > 0) { - const results = await fcCache.getFarcasterUsernamesForFIDs(needFetch); + const results = await fcCache.getFarcasterUsersForFIDs(needFetch); for (let i = 0; i < needFetch.length; i++) { const fid = needFetch[i]; const result = results[i]; if (result) { - farcasterUsernames.set(fid, result); + farcasterUsernames.set(fid, result.username); } } } return info.map(user => { if (!user) { return user; } const { input, fid, cachedResult } = user; if (cachedResult) { return { ...input, farcasterUsername: cachedResult }; } else if (!fid) { return input; } const farcasterUsername = farcasterUsernames.get(fid); if (farcasterUsername) { return { ...input, farcasterUsername }; } return input; }); } export { getFCNames }; diff --git a/lib/utils/fc-cache.js b/lib/utils/fc-cache.js index 491b3e3fd..74ecc19ac 100644 --- a/lib/utils/fc-cache.js +++ b/lib/utils/fc-cache.js @@ -1,155 +1,152 @@ // @flow -import { NeynarClient } from './neynar-client.js'; +import { NeynarClient, type FarcasterUser } from './neynar-client.js'; import sleep from './sleep.js'; const cacheTimeout = 24 * 60 * 60 * 1000; // one day const failedQueryCacheTimeout = 5 * 60 * 1000; // five minutes const queryTimeout = 10 * 1000; // ten seconds async function throwOnTimeout(identifier: string) { await sleep(queryTimeout); throw new Error(`Farcaster fetch for ${identifier} timed out`); } type FarcasterUsernameQueryCacheEntry = { +fid: string, +expirationTime: number, - +farcasterUsername: ?string | Promise, + +farcasterUser: ?FarcasterUser | Promise, }; class FCCache { client: NeynarClient; - // Maps from FIDs to a cache entry for its Farcaster username + // Maps from FIDs to a cache entry for its Farcaster user farcasterUsernameQueryCache: Map = new Map(); constructor(client: NeynarClient) { this.client = client; } - getFarcasterUsernamesForFIDs( + getFarcasterUsersForFIDs( fids: $ReadOnlyArray, - ): Promise> { + ): Promise> { const cacheMatches = fids.map(fid => - this.getCachedFarcasterUsernameEntryForFID(fid), + this.getCachedFarcasterUserEntryForFID(fid), ); const cacheResultsPromise = Promise.all( cacheMatches.map(match => - Promise.resolve(match ? match.farcasterUsername : match), + Promise.resolve(match ? match.farcasterUser : match), ), ); if (cacheMatches.every(Boolean)) { return cacheResultsPromise; } const needFetch = []; for (let i = 0; i < fids.length; i++) { const fid = fids[i]; const cacheMatch = cacheMatches[i]; if (!cacheMatch) { needFetch.push(fid); } } - const fetchFarcasterUsernamesPromise = (async () => { - let farcasterUsernames: $ReadOnlyArray; + const fetchFarcasterUsersPromise = (async () => { + let farcasterUsers: $ReadOnlyArray; try { - const farcasterUsers = await Promise.race([ + farcasterUsers = await Promise.race([ this.client.getFarcasterUsers(needFetch), - throwOnTimeout(`usernames for ${JSON.stringify(needFetch)}`), + throwOnTimeout(`users for ${JSON.stringify(needFetch)}`), ]); - farcasterUsernames = farcasterUsers.map( - farcasterUser => farcasterUser?.username, - ); } catch (e) { console.log(e); - farcasterUsernames = new Array(needFetch.length).fill(null); + farcasterUsers = new Array(needFetch.length).fill(null); } - const resultMap = new Map(); + const resultMap = new Map(); for (let i = 0; i < needFetch.length; i++) { const fid = needFetch[i]; - const farcasterUsername = farcasterUsernames[i]; - resultMap.set(fid, farcasterUsername); + const farcasterUser = farcasterUsers[i]; + resultMap.set(fid, farcasterUser); } return resultMap; })(); for (let i = 0; i < needFetch.length; i++) { const fid = needFetch[i]; - const fetchFarcasterUsernamePromise = (async () => { - const resultMap = await fetchFarcasterUsernamesPromise; + const fetchFarcasterUserPromise = (async () => { + const resultMap = await fetchFarcasterUsersPromise; return resultMap.get(fid) ?? null; })(); this.farcasterUsernameQueryCache.set(fid, { fid, expirationTime: Date.now() + queryTimeout * 2, - farcasterUsername: fetchFarcasterUsernamePromise, + farcasterUser: fetchFarcasterUserPromise, }); } return (async () => { const [resultMap, cacheResults] = await Promise.all([ - fetchFarcasterUsernamesPromise, + fetchFarcasterUsersPromise, cacheResultsPromise, ]); for (let i = 0; i < needFetch.length; i++) { const fid = needFetch[i]; - const farcasterUsername = resultMap.get(fid); + const farcasterUser = resultMap.get(fid); const timeout = - farcasterUsername === null ? failedQueryCacheTimeout : cacheTimeout; + farcasterUser === null ? failedQueryCacheTimeout : cacheTimeout; this.farcasterUsernameQueryCache.set(fid, { fid, expirationTime: Date.now() + timeout, - farcasterUsername, + farcasterUser, }); } const results = []; for (let i = 0; i < fids.length; i++) { const cachedResult = cacheResults[i]; if (cachedResult) { results.push(cachedResult); } else { results.push(resultMap.get(fids[i])); } } return results; })(); } - getCachedFarcasterUsernameEntryForFID( + getCachedFarcasterUserEntryForFID( fid: string, ): ?FarcasterUsernameQueryCacheEntry { const cacheResult = this.farcasterUsernameQueryCache.get(fid); if (!cacheResult) { return undefined; } const { expirationTime } = cacheResult; if (expirationTime <= Date.now()) { this.farcasterUsernameQueryCache.delete(fid); return undefined; } return cacheResult; } - getCachedFarcasterUsernameForFID(fid: string): ?string { - const cacheResult = this.getCachedFarcasterUsernameEntryForFID(fid); + getCachedFarcasterUserForFID(fid: string): ?FarcasterUser { + const cacheResult = this.getCachedFarcasterUserEntryForFID(fid); if (!cacheResult) { return undefined; } - const { farcasterUsername } = cacheResult; - if (typeof farcasterUsername !== 'string') { + const { farcasterUser } = cacheResult; + if (typeof farcasterUser !== 'object' || farcasterUser instanceof Promise) { return undefined; } - return farcasterUsername; + return farcasterUser; } } export { FCCache };