mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
cache features for 600 seconds
This commit is contained in:
parent
bcdff19a3d
commit
e05ff942b1
6 changed files with 122 additions and 22 deletions
|
|
@ -6,8 +6,10 @@ import {
|
|||
} from '../../generated/graphql'
|
||||
import {
|
||||
getFeatureName,
|
||||
getFeaturesCache,
|
||||
isOptInFeatureErrorCode,
|
||||
optInFeature,
|
||||
setFeaturesCache,
|
||||
signFeatureToken,
|
||||
} from '../../services/features'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
|
|
@ -34,7 +36,8 @@ export const optInFeatureResolver = authorized<
|
|||
}
|
||||
}
|
||||
|
||||
const optedInFeature = await optInFeature(featureName, claims.uid)
|
||||
const userId = claims.uid
|
||||
const optedInFeature = await optInFeature(featureName, userId)
|
||||
if (isOptInFeatureErrorCode(optedInFeature)) {
|
||||
return {
|
||||
errorCodes: [optedInFeature],
|
||||
|
|
@ -42,7 +45,11 @@ export const optInFeatureResolver = authorized<
|
|||
}
|
||||
log.info('Opted in to a feature', optedInFeature)
|
||||
|
||||
const token = signFeatureToken(optedInFeature, claims.uid)
|
||||
const cachedFeatures = (await getFeaturesCache(userId)) || []
|
||||
const updatedFeatures = [...cachedFeatures, optedInFeature]
|
||||
await setFeaturesCache(userId, updatedFeatures)
|
||||
|
||||
const token = signFeatureToken(optedInFeature, userId)
|
||||
|
||||
return {
|
||||
feature: {
|
||||
|
|
|
|||
|
|
@ -28,17 +28,19 @@ import {
|
|||
PageType,
|
||||
User,
|
||||
} from '../generated/graphql'
|
||||
import { redisDataSource } from '../redis_data_source'
|
||||
import { getAISummary } from '../services/ai-summaries'
|
||||
import { findUserFeatures } from '../services/features'
|
||||
import { countLibraryItems } from '../services/library_item'
|
||||
import { Merge } from '../util'
|
||||
import {
|
||||
isBase64Image,
|
||||
stringToHash,
|
||||
validatedDate,
|
||||
wordsCount,
|
||||
} from '../utils/helpers'
|
||||
findUserFeatures,
|
||||
getFeaturesCache,
|
||||
setFeaturesCache,
|
||||
} from '../services/features'
|
||||
import {
|
||||
countLibraryItems,
|
||||
getCachedTotalCount,
|
||||
setCachedTotalCount,
|
||||
} from '../services/library_item'
|
||||
import { Merge } from '../util'
|
||||
import { isBase64Image, validatedDate, wordsCount } from '../utils/helpers'
|
||||
import { createImageProxyUrl } from '../utils/imageproxy'
|
||||
import { contentConverter } from '../utils/parser'
|
||||
import {
|
||||
|
|
@ -406,7 +408,16 @@ export const functionResolvers = {
|
|||
return undefined
|
||||
}
|
||||
|
||||
return findUserFeatures(ctx.claims.uid)
|
||||
const userId = ctx.claims.uid
|
||||
const cachedFeatures = await getFeaturesCache(userId)
|
||||
if (cachedFeatures) {
|
||||
return cachedFeatures
|
||||
}
|
||||
|
||||
const features = await findUserFeatures(userId)
|
||||
await setFeaturesCache(userId, features)
|
||||
|
||||
return features
|
||||
},
|
||||
picture: (user: UserEntity) => user.profile.pictureUrl,
|
||||
// not implemented yet
|
||||
|
|
@ -607,17 +618,12 @@ export const functionResolvers = {
|
|||
if (pageInfo.searchLibraryItemArgs && ctx.claims) {
|
||||
const args = pageInfo.searchLibraryItemArgs
|
||||
const userId = ctx.claims.uid
|
||||
// hash the arguments to create a unique cache key
|
||||
const argsHash = stringToHash(JSON.stringify(args))
|
||||
const cacheKey = `countLibraryItems:${userId}:${argsHash}`
|
||||
const cachedCount = await redisDataSource.redisClient?.get(cacheKey)
|
||||
if (cachedCount) {
|
||||
return parseInt(cachedCount, 10)
|
||||
}
|
||||
const cachedCount = await getCachedTotalCount(userId, args)
|
||||
if (cachedCount) return cachedCount
|
||||
|
||||
const count = await countLibraryItems(args, userId)
|
||||
await setCachedTotalCount(userId, args, count)
|
||||
|
||||
await redisDataSource.redisClient?.set(cacheKey, count, 'EX', 600)
|
||||
return count
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ import {
|
|||
import { userRepository } from '../../repository/user'
|
||||
import { createUser } from '../../services/create_user'
|
||||
import { sendAccountChangeEmail } from '../../services/send_emails'
|
||||
import { softDeleteUser } from '../../services/user'
|
||||
import { cacheUser, getCachedUser, softDeleteUser } from '../../services/user'
|
||||
import { Merge } from '../../util'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
import { validateUsername } from '../../utils/usernamePolicy'
|
||||
|
|
@ -254,11 +254,19 @@ export const getMeUserResolver: ResolverFn<
|
|||
return undefined
|
||||
}
|
||||
|
||||
const userId = claims.uid
|
||||
const cachedUser = await getCachedUser(userId)
|
||||
if (cachedUser) {
|
||||
return cachedUser
|
||||
}
|
||||
|
||||
const user = await userRepository.findById(claims.uid)
|
||||
if (!user) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
await cacheUser(user)
|
||||
|
||||
return user
|
||||
} catch (error) {
|
||||
return undefined
|
||||
|
|
@ -355,6 +363,11 @@ export const updateEmailResolver = authorized<
|
|||
})
|
||||
)
|
||||
|
||||
await cacheUser({
|
||||
...user,
|
||||
email,
|
||||
})
|
||||
|
||||
return { email }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { LibraryItem } from '../entity/library_item'
|
|||
import { Subscription, SubscriptionStatus } from '../entity/subscription'
|
||||
import { env } from '../env'
|
||||
import { OptInFeatureErrorCode } from '../generated/graphql'
|
||||
import { redisDataSource } from '../redis_data_source'
|
||||
import { authTrx, getRepository } from '../repository'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
|
|
@ -201,3 +202,26 @@ export const userDigestEligible = async (uid: string): Promise<boolean> => {
|
|||
|
||||
return subscriptionsCount >= 2 && libraryItemsCount >= 10
|
||||
}
|
||||
|
||||
const featuresCacheKey = (userId: string) => `features:${userId}`
|
||||
|
||||
export const getFeaturesCache = async (userId: string) => {
|
||||
const cachedFeatures = await redisDataSource.redisClient?.get(
|
||||
featuresCacheKey(userId)
|
||||
)
|
||||
if (!cachedFeatures) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return JSON.parse(cachedFeatures) as Feature[]
|
||||
}
|
||||
|
||||
export const setFeaturesCache = async (userId: string, features: Feature[]) => {
|
||||
const value = JSON.stringify(features)
|
||||
return redisDataSource.redisClient?.set(
|
||||
featuresCacheKey(userId),
|
||||
value,
|
||||
'EX',
|
||||
600
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,11 @@ import {
|
|||
} from '../repository'
|
||||
import { libraryItemRepository } from '../repository/library_item'
|
||||
import { Merge, PickTuple } from '../util'
|
||||
import { deepDelete, setRecentlySavedItemInRedis } from '../utils/helpers'
|
||||
import {
|
||||
deepDelete,
|
||||
setRecentlySavedItemInRedis,
|
||||
stringToHash,
|
||||
} from '../utils/helpers'
|
||||
import { logger } from '../utils/logger'
|
||||
import { parseSearchQuery } from '../utils/search'
|
||||
import { HighlightEvent } from './highlights'
|
||||
|
|
@ -1711,3 +1715,28 @@ export const filterItemEvents = (
|
|||
|
||||
throw new Error('Unexpected state.')
|
||||
}
|
||||
|
||||
const totalCountCacheKey = (userId: string, args: SearchArgs) => {
|
||||
return `cache:library_items_count:${userId}:${stringToHash(
|
||||
JSON.stringify(args)
|
||||
)}`
|
||||
}
|
||||
|
||||
export const getCachedTotalCount = async (userId: string, args: SearchArgs) => {
|
||||
const cacheKey = totalCountCacheKey(userId, args)
|
||||
const cachedCount = await redisDataSource.redisClient?.get(cacheKey)
|
||||
if (!cachedCount) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return parseInt(cachedCount, 10)
|
||||
}
|
||||
|
||||
export const setCachedTotalCount = async (
|
||||
userId: string,
|
||||
args: SearchArgs,
|
||||
count: number
|
||||
) => {
|
||||
const cacheKey = totalCountCacheKey(userId, args)
|
||||
await redisDataSource.redisClient?.set(cacheKey, count, 'EX', 600)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Notification } from 'firebase-admin/messaging'
|
|||
import { DeepPartial, FindOptionsWhere, In } from 'typeorm'
|
||||
import { Profile } from '../entity/profile'
|
||||
import { StatusType, User } from '../entity/user'
|
||||
import { redisDataSource } from '../redis_data_source'
|
||||
import { authTrx, getRepository, queryBuilderToRawSql } from '../repository'
|
||||
import { userRepository } from '../repository/user'
|
||||
import { SetClaimsRole } from '../utils/dictionary'
|
||||
|
|
@ -156,3 +157,23 @@ export const findUserAndPersonalization = async (id: string) => {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
const userCacheKey = (id: string) => `cache:user:${id}`
|
||||
|
||||
export const getCachedUser = async (id: string) => {
|
||||
const user = await redisDataSource.redisClient?.get(userCacheKey(id))
|
||||
if (!user) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return JSON.parse(user) as User
|
||||
}
|
||||
|
||||
export const cacheUser = async (user: User) => {
|
||||
await redisDataSource.redisClient?.set(
|
||||
userCacheKey(user.id),
|
||||
JSON.stringify(user),
|
||||
'EX',
|
||||
600
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue