From 40a4263084ea7c5c5939119e7a068d19df7e5a97 Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Fri, 5 Jul 2024 00:38:09 +0700 Subject: [PATCH] Add cache for profile --- src/pages/api/profiles.ts | 125 ++++++++++++++++++ src/pages/tg/index.tsx | 30 ++++- src/services/datahub/content-staking/query.ts | 11 +- src/services/datahub/identity/query.ts | 4 +- src/services/datahub/profiles/query.ts | 10 +- src/services/datahub/utils.ts | 9 -- src/utils/strings.ts | 9 ++ 7 files changed, 171 insertions(+), 27 deletions(-) create mode 100644 src/pages/api/profiles.ts diff --git a/src/pages/api/profiles.ts b/src/pages/api/profiles.ts new file mode 100644 index 000000000..df014f1c6 --- /dev/null +++ b/src/pages/api/profiles.ts @@ -0,0 +1,125 @@ +import { redisCallWrapper } from '@/server/cache' +import { ApiResponse, handlerWrapper } from '@/server/common' +import { + SubsocialProfile, + getProfiles, +} from '@/services/datahub/profiles/fetcher' +import { parseJSONData } from '@/utils/strings' +import { NextApiRequest, NextApiResponse } from 'next' +import { z } from 'zod' + +const querySchema = z.object({ + addresses: z.array(z.string()).or(z.string()), +}) +export type ApiProfilesParams = z.infer + +const bodySchema = z.object({ + address: z.string(), +}) +export type ApiProfilesInvalidationBody = z.infer + +type ResponseData = { + data?: SubsocialProfile[] +} +export type ApiProfilesResponse = ApiResponse +export type ApiProfilesInvalidationResponse = ApiResponse<{}> + +const INVALIDATED_MAX_AGE = 1 * 60 // 1 minute +const getInvalidatedProfileRedisKey = (id: string) => { + return `profiles-invalidated:${id}` +} + +const GET_handler = handlerWrapper({ + inputSchema: querySchema, + dataGetter: (req: NextApiRequest) => req.query, +})({ + errorLabel: 'profiles', + allowedMethods: ['GET'], + handler: async (data, _, res) => { + const addresses = Array.isArray(data.addresses) + ? data.addresses + : [data.addresses] + const profiles = await getProfilesServer(addresses) + return res + .status(200) + .send({ success: true, message: 'OK', data: profiles }) + }, +}) + +const POST_handler = handlerWrapper({ + inputSchema: bodySchema, + dataGetter: (req: NextApiRequest) => req.body, +})<{}>({ + errorLabel: 'posts', + allowedMethods: ['POST'], + handler: async (data, _, res) => { + redisCallWrapper(async (redis) => { + return redis?.set( + getInvalidatedProfileRedisKey(data.address), + data.address, + 'EX', + INVALIDATED_MAX_AGE + ) + }) + + return res.status(200).send({ success: true, message: 'OK' }) + }, +}) + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === 'GET') { + return GET_handler(req, res) + } else if (req.method === 'POST') { + return POST_handler(req, res) + } +} + +const PROFILE_MAX_AGE = 5 * 60 // 5 minutes +const getProfileRedisKey = (id: string) => { + return `profiles:${id}` +} +export async function getProfilesServer( + addresses: string[] +): Promise { + if (addresses.length === 0) return [] + + const profiles: SubsocialProfile[] = [] + const needToFetch: string[] = [] + const promises = addresses.map(async (address) => { + redisCallWrapper(async (redis) => { + const [profile, isInvalidated] = await Promise.all([ + redis?.get(getProfileRedisKey(address)), + redis?.get(getInvalidatedProfileRedisKey(address)), + ] as const) + const parsed = + profile && parseJSONData<{ data: SubsocialProfile | null }>(profile) + if (parsed && !isInvalidated) { + if (parsed.data) profiles.push(parsed.data) + // if null, we don't need to fetch it + } else { + needToFetch.push(address) + } + }) + }) + await Promise.allSettled(promises) + + const fetchedProfiles = await getProfiles(needToFetch) + const profilesMap = new Map() + fetchedProfiles.forEach(async (profile) => { + profilesMap.set(profile.address, profile) + }) + + needToFetch.map((address) => { + const profile = profilesMap.get(address) ?? null + redisCallWrapper(async (redis) => { + await redis?.set( + getProfileRedisKey(address), + JSON.stringify({ data: profile }), + 'EX', + PROFILE_MAX_AGE + ) + }) + }) + + return [...profiles, ...fetchedProfiles] +} diff --git a/src/pages/tg/index.tsx b/src/pages/tg/index.tsx index ef7750d4b..b183b8797 100644 --- a/src/pages/tg/index.tsx +++ b/src/pages/tg/index.tsx @@ -2,9 +2,30 @@ import { env } from '@/env.mjs' import MemesPage from '@/modules/telegram/MemesPage' import { AppCommonProps } from '@/pages/_app' import { prefetchBlockedEntities } from '@/server/moderation/prefetch' +import { getPostQuery } from '@/services/api/query' import { getPaginatedPostIdsByPostId } from '@/services/datahub/posts/query' +import { getProfileQuery } from '@/services/datahub/profiles/query' import { getCommonStaticProps } from '@/utils/page' import { QueryClient, dehydrate } from '@tanstack/react-query' +import { getProfilesServer } from '../api/profiles' + +async function prefetchChatData(client: QueryClient) { + const firstPageData = await getPaginatedPostIdsByPostId.fetchFirstPageQuery( + client, + env.NEXT_PUBLIC_MAIN_CHAT_ID, + 1 + ) + const ownerIds = firstPageData.data + .map((id) => { + const post = getPostQuery.getQueryData(client, id) + return post?.struct.ownerId + }) + .filter(Boolean) + const profiles = await getProfilesServer(ownerIds) + profiles.forEach((profile) => { + getProfileQuery.setQueryData(client, profile.address, profile) + }) +} export const getStaticProps = getCommonStaticProps( () => ({ @@ -17,17 +38,14 @@ export const getStaticProps = getCommonStaticProps( async () => { const client = new QueryClient() await Promise.all([ - getPaginatedPostIdsByPostId.fetchFirstPageQuery( - client, - env.NEXT_PUBLIC_MAIN_CHAT_ID, - 1 - ), + prefetchChatData(client), prefetchBlockedEntities( client, [env.NEXT_PUBLIC_MAIN_SPACE_ID].filter(Boolean), [env.NEXT_PUBLIC_MAIN_CHAT_ID].filter(Boolean) ), - ]) + ] as const) + getPaginatedPostIdsByPostId.invalidateFirstQuery( client, env.NEXT_PUBLIC_MAIN_CHAT_ID diff --git a/src/services/datahub/content-staking/query.ts b/src/services/datahub/content-staking/query.ts index fd72fa6b5..75b2bf9c6 100644 --- a/src/services/datahub/content-staking/query.ts +++ b/src/services/datahub/content-staking/query.ts @@ -4,6 +4,7 @@ import { apiInstance } from '@/services/api/utils' import { getSubIdRequest } from '@/services/external' import { createQuery, poolQuery } from '@/subsocial-query' import { LocalStorage } from '@/utils/storage' +import { parseJSONData } from '@/utils/strings' import { AxiosResponse } from 'axios' import dayjs from 'dayjs' import { gql } from 'graphql-request' @@ -25,11 +26,7 @@ import { GetUserYesterdayRewardQuery, GetUserYesterdayRewardQueryVariables, } from '../generated-query' -import { - datahubQueryRequest, - getDayAndWeekTimestamp, - parseCachedPlaceholderData, -} from '../utils' +import { datahubQueryRequest, getDayAndWeekTimestamp } from '../utils' const GET_SUPER_LIKE_COUNTS = gql` query GetSuperLikeCounts($postIds: [String!]!) { @@ -672,9 +669,7 @@ export const getTokenomicsMetadataQuery = createQuery({ const cache = getTokenomicsMetadataCache.get() return { placeholderData: - parseCachedPlaceholderData< - Awaited> - >(cache), + parseJSONData>>(cache), } }, }) diff --git a/src/services/datahub/identity/query.ts b/src/services/datahub/identity/query.ts index 21eca8ac0..f836546a8 100644 --- a/src/services/datahub/identity/query.ts +++ b/src/services/datahub/identity/query.ts @@ -2,7 +2,7 @@ import { apiInstance } from '@/services/api/utils' import { useMyAccount } from '@/stores/my-account' import { createQuery } from '@/subsocial-query' import { LocalStorage } from '@/utils/storage' -import { parseCachedPlaceholderData } from '../utils' +import { parseJSONData } from '@/utils/strings' import { getLinkedIdentity } from './fetcher' export const getMyLinkedIdentityCache = new LocalStorage( @@ -22,7 +22,7 @@ export const getLinkedIdentityQuery = createQuery({ undefined if (data === useMyAccount.getState().address) { const cacheData = getMyLinkedIdentityCache.get() - cache = parseCachedPlaceholderData(cacheData) + cache = parseJSONData(cacheData) } return { enabled: !!data, diff --git a/src/services/datahub/profiles/query.ts b/src/services/datahub/profiles/query.ts index 60358f711..e59cedf18 100644 --- a/src/services/datahub/profiles/query.ts +++ b/src/services/datahub/profiles/query.ts @@ -1,11 +1,17 @@ +import { ApiProfilesResponse } from '@/pages/api/profiles' +import { apiInstance } from '@/services/api/utils' import { createQuery, poolQuery } from '@/subsocial-query' -import { SubsocialProfile, getProfiles } from './fetcher' +import { SubsocialProfile } from './fetcher' const getProfile = poolQuery({ name: 'getProfile', multiCall: async (addresses) => { if (addresses.length === 0) return [] - return getProfiles(addresses) + const res = await apiInstance.get( + '/api/profiles?' + addresses.map((n) => `addresses=${n}`).join('&') + ) + const data = res.data as ApiProfilesResponse + return data.data ?? [] }, resultMapper: { paramToKey: (address) => address, diff --git a/src/services/datahub/utils.ts b/src/services/datahub/utils.ts index 21ba2254e..b70713c95 100644 --- a/src/services/datahub/utils.ts +++ b/src/services/datahub/utils.ts @@ -153,12 +153,3 @@ export async function augmentDatahubParams( timestamp, } } - -export function parseCachedPlaceholderData(data: string | null) { - if (!data) return undefined - try { - return JSON.parse(data) as T - } catch (err) { - return undefined - } -} diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 26c4e503d..bc78be97a 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -79,3 +79,12 @@ export function formatNumber( } return string } + +export function parseJSONData(data: string | null) { + if (!data) return undefined + try { + return JSON.parse(data) as T + } catch (err) { + return undefined + } +}