diff --git a/packages/api/src/apollo.ts b/packages/api/src/apollo.ts index 7d4c8ea02..ef8ea5cb5 100644 --- a/packages/api/src/apollo.ts +++ b/packages/api/src/apollo.ts @@ -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 = async ({ return cb(tx) }), tracingSpan: tracer.startSpan('apollo.request'), + dataSources: { + readingProgress: new ReadingProgressDataSource(), + }, } return ctx diff --git a/packages/api/src/datasources/reading_progress_data_source.ts b/packages/api/src/datasources/reading_progress_data_source.ts new file mode 100644 index 000000000..1155aef1a --- /dev/null +++ b/packages/api/src/datasources/reading_progress_data_source.ts @@ -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 { + 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 { + 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 { + 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 + } +} diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 921a01cf8..5a5aa38f9 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -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, diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 871782068..bcc17360b 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -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 { - // 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) { diff --git a/packages/api/src/resolvers/types.ts b/packages/api/src/resolvers/types.ts index 6c842d650..6fc24405f 100644 --- a/packages/api/src/resolvers/types.ts +++ b/packages/api/src/resolvers/types.ts @@ -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 tracingSpan: Span + dataSources: { + readingProgress: ReadingProgressDataSource + } } export type ResolverContext = ApolloContext