Page MenuHomePhabricator

D11876.id39815.diff
No OneTemporary

D11876.id39815.diff

diff --git a/lib/components/neynar-client-provider.react.js b/lib/components/neynar-client-provider.react.js
--- a/lib/components/neynar-client-provider.react.js
+++ b/lib/components/neynar-client-provider.react.js
@@ -2,10 +2,12 @@
import * as React from 'react';
+import { FCCache } from '../utils/fc-cache.js';
import { NeynarClient } from '../utils/neynar-client.js';
type NeynarClientContextType = {
+client: NeynarClient,
+ +fcCache: FCCache,
};
const NeynarClientContext: React.Context<?NeynarClientContextType> =
@@ -31,6 +33,7 @@
}
return {
client: neynarClient,
+ fcCache: new FCCache(neynarClient),
};
}, [neynarClient]);
diff --git a/lib/types/farcaster-types.js b/lib/types/farcaster-types.js
--- a/lib/types/farcaster-types.js
+++ b/lib/types/farcaster-types.js
@@ -14,13 +14,19 @@
+fid: string,
};
-export type NeynarUserWithViewerContext = {
+export type NeynarUser = {
+fid: number,
+ +username: string,
+ ...
+};
+
+export type NeynarUserWithViewerContext = $ReadOnly<{
+ ...NeynarUser,
+viewerContext: {
+following: boolean,
},
...
-};
+}>;
export type NeynarChannel = {
+id: string,
diff --git a/lib/utils/fc-cache.js b/lib/utils/fc-cache.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/fc-cache.js
@@ -0,0 +1,152 @@
+// @flow
+
+import { NeynarClient } 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<?string>,
+};
+
+class FCCache {
+ client: NeynarClient;
+
+ // Maps from FIDs to a cache entry for its Farcaster username
+ farcasterUsernameQueryCache: Map<string, FarcasterUsernameQueryCacheEntry> =
+ new Map();
+
+ constructor(client: NeynarClient) {
+ this.client = client;
+ }
+
+ getFarcasterUsernamesForFIDs(
+ fids: $ReadOnlyArray<string>,
+ ): Promise<Array<?string>> {
+ const cacheMatches = fids.map(fid =>
+ this.getCachedFarcasterUsernameEntryForFID(fid),
+ );
+ const cacheResultsPromise = Promise.all(
+ cacheMatches.map(match =>
+ Promise.resolve(match ? match.farcasterUsername : 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<?string>;
+ try {
+ farcasterUsernames = await Promise.race([
+ this.client.getFarcasterUsernames(needFetch),
+ throwOnTimeout(`usernames for ${JSON.stringify(needFetch)}`),
+ ]);
+ } catch (e) {
+ console.log(e);
+ farcasterUsernames = new Array<?string>(needFetch.length).fill(null);
+ }
+
+ const resultMap = new Map<string, ?string>();
+ for (let i = 0; i < needFetch.length; i++) {
+ const fid = needFetch[i];
+ const farcasterUsername = farcasterUsernames[i];
+ resultMap.set(fid, farcasterUsername);
+ }
+ return resultMap;
+ })();
+
+ for (let i = 0; i < needFetch.length; i++) {
+ const fid = needFetch[i];
+ const fetchFarcasterUsernamePromise = (async () => {
+ const resultMap = await fetchFarcasterUsernamesPromise;
+ return resultMap.get(fid) ?? null;
+ })();
+ this.farcasterUsernameQueryCache.set(fid, {
+ fid,
+ expirationTime: Date.now() + queryTimeout * 2,
+ farcasterUsername: fetchFarcasterUsernamePromise,
+ });
+ }
+
+ return (async () => {
+ const [resultMap, cacheResults] = await Promise.all([
+ fetchFarcasterUsernamesPromise,
+ cacheResultsPromise,
+ ]);
+ for (let i = 0; i < needFetch.length; i++) {
+ const fid = needFetch[i];
+ const farcasterUsername = resultMap.get(fid);
+ const timeout =
+ farcasterUsername === null ? failedQueryCacheTimeout : cacheTimeout;
+ this.farcasterUsernameQueryCache.set(fid, {
+ fid,
+ expirationTime: Date.now() + timeout,
+ farcasterUsername,
+ });
+ }
+
+ 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(
+ 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);
+ if (!cacheResult) {
+ return undefined;
+ }
+
+ const { farcasterUsername } = cacheResult;
+ if (typeof farcasterUsername !== 'string') {
+ return undefined;
+ }
+
+ return farcasterUsername;
+ }
+}
+
+export { FCCache };
diff --git a/lib/utils/neynar-client.js b/lib/utils/neynar-client.js
--- a/lib/utils/neynar-client.js
+++ b/lib/utils/neynar-client.js
@@ -5,6 +5,7 @@
import { getMessageForException } from './errors.js';
import type {
NeynarChannel,
+ NeynarUser,
NeynarUserWithViewerContext,
} from '../types/farcaster-types.js';
@@ -28,6 +29,10 @@
+channels: $ReadOnlyArray<NeynarChannel>,
};
+type FetchUsersResponse = {
+ +users: $ReadOnlyArray<NeynarUser>,
+};
+
const neynarBaseURL = 'https://api.neynar.com/';
const neynarURLs = {
'1': `${neynarBaseURL}v1/farcaster/`,
@@ -183,6 +188,47 @@
throw error;
}
}
+
+ async getFarcasterUsernames(
+ fids: $ReadOnlyArray<string>,
+ ): Promise<Array<?string>> {
+ const fidsLeft = [...fids];
+ const results: Array<?string> = [];
+ do {
+ // Neynar API allows querying 100 at a time
+ const batch = fidsLeft.splice(0, 100);
+ const url = getNeynarURL('2', 'user/bulk', { fids: batch.join(',') });
+ try {
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ api_key: this.apiKey,
+ },
+ });
+
+ const json: FetchUsersResponse = await response.json();
+ const { users } = json;
+
+ const neynarUserMap = new Map<number, NeynarUser>();
+ for (const neynarUser of users) {
+ neynarUserMap.set(neynarUser.fid, neynarUser);
+ }
+
+ for (const fid of batch) {
+ const neynarUser = neynarUserMap.get(parseInt(fid));
+ results.push(neynarUser ? neynarUser.username : null);
+ }
+ } catch (error) {
+ console.log(
+ 'Failed to fetch Farcaster usernames:',
+ getMessageForException(error) ?? 'unknown',
+ );
+ throw error;
+ }
+ } while (fidsLeft.length > 0);
+ return results;
+ }
}
export { NeynarClient };

File Metadata

Mime Type
text/plain
Expires
Mon, Dec 23, 9:53 PM (16 h, 46 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2696247
Default Alt Text
D11876.id39815.diff (7 KB)

Event Timeline