Write behing cache for reading progress

This commit is contained in:
Jackson Harper 2024-01-30 13:02:32 +08:00
parent 7289480ca5
commit 86c80d991f
5 changed files with 172 additions and 15 deletions

View file

@ -25,6 +25,7 @@ import { tracer } from './tracing'
import { getClaimsByToken, setAuthInCookie } from './utils/auth'
import { SetClaimsRole } from './utils/dictionary'
import { logger } from './utils/logger'
import { ReadingProgressDataSource } from './datasources/reading_progress_data_source'
const signToken = promisify(jwt.sign)
const pubsub = createPubSubClient()
@ -84,6 +85,9 @@ const contextFunc: ContextFunction<ExpressContext, ResolverContext> = async ({
return cb(tx)
}),
tracingSpan: tracer.startSpan('apollo.request'),
dataSources: {
readingProgress: new ReadingProgressDataSource(),
},
}
return ctx

View file

@ -0,0 +1,86 @@
import { redisDataSource } from '../redis_data_source'
type ReadingProgressCacheItem = {
readingProgressPercent: number
readingProgressTopPercent: number | undefined
readingProgressAnchorIndex: number | undefined
updatedAt: Date
}
export class ReadingProgressDataSource {
private cacheItems: { [id: string]: ReadingProgressCacheItem } = {}
constructor() {}
async getReadingProgress(
libraryItemID: string
): Promise<ReadingProgressCacheItem | undefined> {
const cacheKey = `omnivore:reading-progress:${libraryItemID}`
const cached = this.cacheItems[cacheKey]
if (cached) {
return cached
}
return this.valueFromRedis(libraryItemID)
}
async updateReadingProgress(
libraryItemID: string,
progress: {
readingProgressPercent: number
readingProgressTopPercent: number | undefined | null
readingProgressAnchorIndex: number | undefined | null
}
): Promise<void> {
const cacheKey = `omnivore:reading-progress:${libraryItemID}`
const existingItem = await this.valueFromRedis(cacheKey)
const cacheItem = {
readingProgressPercent: Math.max(
progress.readingProgressPercent,
existingItem?.readingProgressPercent ?? 0
),
readingProgressTopPercent: Math.max(
progress.readingProgressTopPercent ?? 0,
existingItem?.readingProgressTopPercent ?? 0
),
readingProgressAnchorIndex: Math.max(
progress.readingProgressAnchorIndex ?? 0,
existingItem?.readingProgressAnchorIndex ?? 0
),
updatedAt: new Date(),
}
this.cacheItems[cacheKey] = cacheItem
if (await redisDataSource.redisClient?.hmset(cacheKey, cacheItem)) {
console.log('cached reading progress')
} else {
console.log('failed to cache reading progress')
}
}
async valueFromRedis(
libraryItemID: string
): Promise<ReadingProgressCacheItem | undefined> {
const cacheKey = `omnivore:reading-progress:${libraryItemID}`
const redisCached = await redisDataSource.redisClient?.hgetall(cacheKey)
if (redisCached) {
const readingProgressPercent = parseInt(
redisCached.readingProgressPercent,
10
)
const updatedAt = new Date(parseInt(redisCached.updatedAt, 10))
if (!Number.isNaN(readingProgressPercent) && updatedAt) {
return {
readingProgressPercent,
readingProgressTopPercent: redisCached.readingProgressTopPercent
? parseInt(redisCached.readingProgressTopPercent, 10)
: undefined,
readingProgressAnchorIndex: redisCached.readingProgressAnchorIndex
? parseInt(redisCached.readingProgressAnchorIndex, 10)
: undefined,
updatedAt,
}
}
}
return undefined
}
}

View file

@ -607,7 +607,7 @@ export const saveArticleReadingProgressResolver = authorized<
force,
},
},
{ log, pubsub, uid }
{ log, pubsub, uid, dataSources }
) => {
if (
readingProgressPercent < 0 ||
@ -640,6 +640,11 @@ export const saveArticleReadingProgressResolver = authorized<
}
}
dataSources.readingProgress.updateReadingProgress(id, {
readingProgressPercent,
readingProgressTopPercent,
readingProgressAnchorIndex,
})
// update reading progress only if the current value is lower
const updatedItem = await updateLibraryItemReadingProgress(
id,

View file

@ -312,20 +312,6 @@ export const functionResolvers = {
publishedAt(article: { publishedAt: Date }) {
return validatedDate(article.publishedAt)
},
// async shareInfo(
// article: { id: string; sharedBy?: User; shareInfo?: LinkShareInfo },
// __: unknown,
// ctx: WithDataSourcesContext
// ): Promise<LinkShareInfo | undefined> {
// if (article.shareInfo) return article.shareInfo
// if (!ctx.claims?.uid) return undefined
// return getShareInfoForArticle(
// ctx.kx,
// ctx.claims?.uid,
// article.id,
// ctx.models
// )
// },
image(article: { image?: string }): string | undefined {
return article.image && createImageProxyUrl(article.image, 320, 320)
},
@ -342,6 +328,42 @@ export const functionResolvers = {
return findLabelsByLibraryItemId(article.id, ctx.uid)
},
async readingProgressPercent(
article: { id: string; readingProgressPercent?: number },
_: unknown,
ctx: WithDataSourcesContext
) {
const readingProgress =
await ctx.dataSources.readingProgress.getReadingProgress(article.id)
if (readingProgress) {
return readingProgress.readingProgressPercent
}
return article.readingProgressPercent
},
async readingProgressAnchorIndex(
article: { id: string; readingProgressAnchorIndex?: number },
_: unknown,
ctx: WithDataSourcesContext
) {
const readingProgress =
await ctx.dataSources.readingProgress.getReadingProgress(article.id)
if (readingProgress) {
return readingProgress.readingProgressAnchorIndex
}
return article.readingProgressAnchorIndex
},
async readingProgressTopPercent(
article: { id: string; readingProgressTopPercent?: number },
_: unknown,
ctx: WithDataSourcesContext
) {
const readingProgress =
await ctx.dataSources.readingProgress.getReadingProgress(article.id)
if (readingProgress) {
return readingProgress.readingProgressTopPercent
}
return article.readingProgressTopPercent
},
},
Highlight: {
// async reactions(
@ -447,6 +469,42 @@ export const functionResolvers = {
const highlights = await findHighlightsByLibraryItemId(item.id, ctx.uid)
return highlights.map(highlightDataToHighlight)
},
async readingProgressPercent(
article: { id: string; readingProgressPercent?: number },
_: unknown,
ctx: WithDataSourcesContext
) {
const readingProgress =
await ctx.dataSources.readingProgress.getReadingProgress(article.id)
if (readingProgress) {
return readingProgress.readingProgressPercent
}
return article.readingProgressPercent
},
async readingProgressAnchorIndex(
article: { id: string; readingProgressAnchorIndex?: number },
_: unknown,
ctx: WithDataSourcesContext
) {
const readingProgress =
await ctx.dataSources.readingProgress.getReadingProgress(article.id)
if (readingProgress) {
return readingProgress.readingProgressAnchorIndex
}
return article.readingProgressAnchorIndex
},
async readingProgressTopPercent(
article: { id: string; readingProgressTopPercent?: number },
_: unknown,
ctx: WithDataSourcesContext
) {
const readingProgress =
await ctx.dataSources.readingProgress.getReadingProgress(article.id)
if (readingProgress) {
return readingProgress.readingProgressTopPercent
}
return article.readingProgressTopPercent
},
},
Subscription: {
newsletterEmail(subscription: Subscription) {

View file

@ -5,6 +5,7 @@ import * as jwt from 'jsonwebtoken'
import { EntityManager } from 'typeorm'
import winston from 'winston'
import { PubsubClient } from '../pubsub'
import { ReadingProgressDataSource } from '../datasources/reading_progress_data_source'
export interface Claims {
uid: string
@ -37,6 +38,9 @@ export interface RequestContext {
userRole?: string
) => Promise<TResult>
tracingSpan: Span
dataSources: {
readingProgress: ReadingProgressDataSource
}
}
export type ResolverContext = ApolloContext<RequestContext>