diff --git a/src/modules/points/PointsWidget.tsx b/src/modules/points/PointsWidget.tsx index 6df0c85be..fa7beb5b8 100644 --- a/src/modules/points/PointsWidget.tsx +++ b/src/modules/points/PointsWidget.tsx @@ -221,7 +221,7 @@ const UserStatsSection = ({
-
+
See the Leaderboard 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/balances/subscription.tsx b/src/services/datahub/balances/subscription.tsx index 9fa4dd4e8..9d79f9a40 100644 --- a/src/services/datahub/balances/subscription.tsx +++ b/src/services/datahub/balances/subscription.tsx @@ -4,7 +4,10 @@ import { gql } from 'graphql-request' import { useRouter } from 'next/router' import { useEffect, useRef } from 'react' import { SubscribeBalancesSubscription } from '../generated-query' -import { getBalanceQuery } from '../leaderboard/points-balance/query' +import { + getBalanceQuery, + getMyBalanceCache, +} from '../leaderboard/points-balance/query' import { datahubSubscription } from '../utils' export function useDatahubBalancesSubscriber() { @@ -95,4 +98,5 @@ async function processSubscriptionEvent( mainAddress, Number(eventData.entity.activeStakingPoints) ) + getMyBalanceCache.set(eventData.entity.activeStakingPoints) } diff --git a/src/services/datahub/content-staking/query.ts b/src/services/datahub/content-staking/query.ts index 894f290eb..75b2bf9c6 100644 --- a/src/services/datahub/content-staking/query.ts +++ b/src/services/datahub/content-staking/query.ts @@ -3,6 +3,8 @@ import { ApiDatahubSuperLikeGetResponse } from '@/pages/api/datahub/super-like' 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' @@ -645,6 +647,9 @@ export const GET_TOKENOMICS_METADATA = gql` } } ` +const getTokenomicsMetadataCache = new LocalStorage( + () => 'tokenomics-metadata-cache' +) export async function getTokenomicsMetadata() { const res = await datahubQueryRequest< GetTokenomicsMetadataQuery, @@ -652,12 +657,19 @@ export async function getTokenomicsMetadata() { >({ document: GET_TOKENOMICS_METADATA, }) + getTokenomicsMetadataCache.set( + JSON.stringify(res.activeStakingTokenomicMetadata) + ) return res.activeStakingTokenomicMetadata } export const getTokenomicsMetadataQuery = createQuery({ key: 'getTokenomicsMetadata', fetcher: getTokenomicsMetadata, - defaultConfigGenerator: () => ({ - enabled: true, - }), + defaultConfigGenerator: () => { + const cache = getTokenomicsMetadataCache.get() + return { + placeholderData: + parseJSONData>>(cache), + } + }, }) diff --git a/src/services/datahub/identity/query.ts b/src/services/datahub/identity/query.ts index eb1dd28b5..f836546a8 100644 --- a/src/services/datahub/identity/query.ts +++ b/src/services/datahub/identity/query.ts @@ -1,13 +1,34 @@ import { apiInstance } from '@/services/api/utils' +import { useMyAccount } from '@/stores/my-account' import { createQuery } from '@/subsocial-query' +import { LocalStorage } from '@/utils/storage' +import { parseJSONData } from '@/utils/strings' import { getLinkedIdentity } from './fetcher' +export const getMyLinkedIdentityCache = new LocalStorage( + () => 'my-linked-identity-cache' +) export const getLinkedIdentityQuery = createQuery({ key: 'getLinkedIdentity', - fetcher: (address: string) => getLinkedIdentity({ sessionAddress: address }), - defaultConfigGenerator: (data) => ({ - enabled: !!data, - }), + fetcher: async (address: string) => { + const res = await getLinkedIdentity({ sessionAddress: address }) + if (address === useMyAccount.getState().address) { + getMyLinkedIdentityCache.set(JSON.stringify(res)) + } + return res + }, + defaultConfigGenerator: (data) => { + let cache: Awaited> | undefined = + undefined + if (data === useMyAccount.getState().address) { + const cacheData = getMyLinkedIdentityCache.get() + cache = parseJSONData(cacheData) + } + return { + enabled: !!data, + placeholderData: cache, + } + }, }) export const getLinkedIdentityFromMainAddressQuery = createQuery({ diff --git a/src/services/datahub/identity/subscription.ts b/src/services/datahub/identity/subscription.ts index 0d3282f4c..3d7dbf617 100644 --- a/src/services/datahub/identity/subscription.ts +++ b/src/services/datahub/identity/subscription.ts @@ -8,7 +8,8 @@ import { SubscribeIdentitySubscription, } from '../generated-query' import { datahubSubscription, isDatahubAvailable } from '../utils' -import { getLinkedIdentityQuery } from './query' +import { Identity } from './fetcher' +import { getLinkedIdentityQuery, getMyLinkedIdentityCache } from './query' export function useDatahubIdentitySubscriber() { const queryClient = useQueryClient() @@ -134,7 +135,7 @@ async function processSessionCreated( session: SubscribeIdentitySubscription['linkedIdentitySubscription']['entity']['session'] ) { if (!session?.id) return - getLinkedIdentityQuery.setQueryData(queryClient, session.id, { + const newIdentity: Identity = { mainAddress: session.linkedIdentity.id, externalProviders: session.linkedIdentity.externalProviders?.map((p) => ({ @@ -142,7 +143,10 @@ async function processSessionCreated( provider: p.provider, username: p.username, })) ?? [], - }) + } + if (session.id === useMyAccount.getState().address) + getMyLinkedIdentityCache.set(JSON.stringify(newIdentity)) + getLinkedIdentityQuery.setQueryData(queryClient, session.id, newIdentity) } async function processExternalProviderUpdate( diff --git a/src/services/datahub/leaderboard/points-balance/optimistic.ts b/src/services/datahub/leaderboard/points-balance/optimistic.ts index 976a1b4b4..2a4242c3c 100644 --- a/src/services/datahub/leaderboard/points-balance/optimistic.ts +++ b/src/services/datahub/leaderboard/points-balance/optimistic.ts @@ -6,6 +6,7 @@ import { FULL_ENERGY_VALUE, getBalanceQuery, getEnergyStateQuery, + getMyBalanceCache, } from './query' dayjs.extend(utc) @@ -24,6 +25,7 @@ export const increasePointsBalance = ({ getBalanceQuery.setQueryData(client, address, (oldData) => { if (!oldData) return oldData + getMyBalanceCache.set((oldData + pointsByClick).toString()) return oldData + pointsByClick }) } diff --git a/src/services/datahub/leaderboard/points-balance/query.ts b/src/services/datahub/leaderboard/points-balance/query.ts index ca3b2c099..36d12fc90 100644 --- a/src/services/datahub/leaderboard/points-balance/query.ts +++ b/src/services/datahub/leaderboard/points-balance/query.ts @@ -1,4 +1,6 @@ +import { getMyMainAddress } from '@/stores/my-account' import { createQuery } from '@/subsocial-query' +import { LocalStorage } from '@/utils/storage' import { gql } from 'graphql-request' import { GetBalanceQuery, @@ -15,6 +17,7 @@ const GET_BALANCE = gql` } } ` +export const getMyBalanceCache = new LocalStorage(() => 'my-balance-cache') async function getBalance(address: string): Promise { const res = await datahubQueryRequest< GetBalanceQuery, @@ -24,15 +27,26 @@ async function getBalance(address: string): Promise { variables: { address }, }) - return Number(res.socialProfileBalances?.activeStakingPoints ?? 0) || 0 + const balance = + Number(res.socialProfileBalances?.activeStakingPoints ?? 0) || 0 + if (address === getMyMainAddress()) { + getMyBalanceCache.set(balance + '') + } + return balance } - export const getBalanceQuery = createQuery({ key: 'getBalance', fetcher: getBalance, - defaultConfigGenerator: (address) => ({ - enabled: !!address, - }), + defaultConfigGenerator: (address) => { + const cache = + getMyMainAddress() === address + ? Number(getMyBalanceCache.get()) + : undefined + return { + enabled: !!address, + placeholderData: cache || undefined, + } + }, }) const GET_ENERGY_STATE = gql` 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/stores/my-account.tsx b/src/stores/my-account.tsx index 1036c73cb..9877a0f58 100644 --- a/src/stores/my-account.tsx +++ b/src/stores/my-account.tsx @@ -1,7 +1,6 @@ import Toast from '@/components/Toast' import { getReferralIdInUrl } from '@/components/referral/ReferralUrlChanger' import { sendEventWithRef } from '@/components/referral/analytics' -import { IdentityProvider } from '@/services/datahub/generated-query' import { getLinkedIdentityQuery } from '@/services/datahub/identity/query' import { getReferrerIdQuery } from '@/services/datahub/referral/query' import { getDayAndWeekTimestamp } from '@/services/datahub/utils' @@ -105,24 +104,24 @@ const sendLaunchEvent = async ( } else { const [ // linkedTgAccData, - linkedIdentity, + // linkedIdentity, referrerId, ] = await Promise.allSettled([ // getLinkedTelegramAccountsQuery.fetchQuery(queryClient, { // address, // }), - getLinkedIdentityQuery.fetchQuery(queryClient, address), + // getLinkedIdentityQuery.fetchQuery(queryClient, address), getReferrerIdQuery.fetchQuery(queryClient, address), ] as const) // if (linkedTgAccData.status === 'fulfilled') // userProperties.tgNotifsConnected = // (linkedTgAccData.value?.length || 0) > 0 - if (linkedIdentity.status === 'fulfilled') - userProperties.twitterLinked = - !!linkedIdentity.value?.externalProviders.find( - (el) => el.provider === IdentityProvider.Twitter - ) + // if (linkedIdentity.status === 'fulfilled') + // userProperties.twitterLinked = + // !!linkedIdentity.value?.externalProviders.find( + // (el) => el.provider === IdentityProvider.Twitter + // ) if (referrerId.status === 'fulfilled') userProperties.ref = referrerId.value || getReferralIdInUrl() 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 + } +}