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
+ }
+}