Skip to content

Commit

Permalink
Add cache for profile
Browse files Browse the repository at this point in the history
  • Loading branch information
teodorus-nathaniel committed Jul 4, 2024
1 parent ed7499c commit 40a4263
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 27 deletions.
125 changes: 125 additions & 0 deletions src/pages/api/profiles.ts
Original file line number Diff line number Diff line change
@@ -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<typeof querySchema>

const bodySchema = z.object({
address: z.string(),
})
export type ApiProfilesInvalidationBody = z.infer<typeof bodySchema>

type ResponseData = {
data?: SubsocialProfile[]
}
export type ApiProfilesResponse = ApiResponse<ResponseData>
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,
})<ResponseData>({
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<SubsocialProfile[]> {
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<string, SubsocialProfile>()
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]
}
30 changes: 24 additions & 6 deletions src/pages/tg/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppCommonProps>(
() => ({
Expand All @@ -17,17 +38,14 @@ export const getStaticProps = getCommonStaticProps<AppCommonProps>(
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
Expand Down
11 changes: 3 additions & 8 deletions src/services/datahub/content-staking/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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!]!) {
Expand Down Expand Up @@ -672,9 +669,7 @@ export const getTokenomicsMetadataQuery = createQuery({
const cache = getTokenomicsMetadataCache.get()
return {
placeholderData:
parseCachedPlaceholderData<
Awaited<ReturnType<typeof getTokenomicsMetadata>>
>(cache),
parseJSONData<Awaited<ReturnType<typeof getTokenomicsMetadata>>>(cache),
}
},
})
4 changes: 2 additions & 2 deletions src/services/datahub/identity/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down
10 changes: 8 additions & 2 deletions src/services/datahub/profiles/query.ts
Original file line number Diff line number Diff line change
@@ -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<string, SubsocialProfile>({
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,
Expand Down
9 changes: 0 additions & 9 deletions src/services/datahub/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,3 @@ export async function augmentDatahubParams<T>(
timestamp,
}
}

export function parseCachedPlaceholderData<T>(data: string | null) {
if (!data) return undefined
try {
return JSON.parse(data) as T
} catch (err) {
return undefined
}
}
9 changes: 9 additions & 0 deletions src/utils/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,12 @@ export function formatNumber(
}
return string
}

export function parseJSONData<T>(data: string | null) {
if (!data) return undefined
try {
return JSON.parse(data) as T
} catch (err) {
return undefined
}
}

0 comments on commit 40a4263

Please sign in to comment.