From a40f5bed55786b0c3dda3a7afe14372091ee54f9 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 31 Jan 2024 10:17:51 +0800 Subject: [PATCH] Create a service for interacting with cached read positions --- .../reading_progress_data_source.ts | 38 ++----- .../src/services/cached_reading_position.ts | 106 ++++++++++++++++++ 2 files changed, 118 insertions(+), 26 deletions(-) create mode 100644 packages/api/src/services/cached_reading_position.ts diff --git a/packages/api/src/datasources/reading_progress_data_source.ts b/packages/api/src/datasources/reading_progress_data_source.ts index 5f9e7358b..3c860556f 100644 --- a/packages/api/src/datasources/reading_progress_data_source.ts +++ b/packages/api/src/datasources/reading_progress_data_source.ts @@ -1,17 +1,14 @@ import { redisDataSource } from '../redis_data_source' - -type ReadingProgressCacheItem = { - readingProgressPercent: number - readingProgressTopPercent: number | undefined - readingProgressAnchorIndex: number | undefined - updatedAt: string -} +import { + ReadingProgressCacheItem, + fetchCachedReadingPosition, + keyForCachedReadingPosition, + pushCachedReadingPosition, +} from '../services/cached_reading_position' export class ReadingProgressDataSource { private cacheItems: { [id: string]: ReadingProgressCacheItem } = {} - constructor() {} - async getReadingProgress( uid: string, libraryItemID: string @@ -21,7 +18,7 @@ export class ReadingProgressDataSource { if (cached) { return cached } - return this.valueFromRedis(cacheKey) + return fetchCachedReadingPosition(uid, libraryItemID) } async updateReadingProgress( @@ -33,11 +30,14 @@ export class ReadingProgressDataSource { readingProgressAnchorIndex: number | undefined } ): Promise { - const cacheKey = `omnivore:reading-progress:${uid}:${libraryItemID}` const cacheItem: ReadingProgressCacheItem = { - ...progress, + uid, + libraryItemID, updatedAt: new Date().toISOString(), + ...progress, } + const cacheKey = keyForCachedReadingPosition(uid, libraryItemID) + pushCachedReadingPosition(uid, libraryItemID, cacheItem) this.cacheItems[cacheKey] = cacheItem if ( @@ -51,18 +51,4 @@ export class ReadingProgressDataSource { console.log('failed to cache reading progress') } } - - async valueFromRedis( - cacheKey: string - ): Promise { - const redisCached = await redisDataSource.redisClient?.lrange( - cacheKey, - 0, - 0 - ) - if (redisCached && redisCached.length > 0) { - return JSON.parse(redisCached[0]) - } - return undefined - } } diff --git a/packages/api/src/services/cached_reading_position.ts b/packages/api/src/services/cached_reading_position.ts new file mode 100644 index 000000000..3ae1965ac --- /dev/null +++ b/packages/api/src/services/cached_reading_position.ts @@ -0,0 +1,106 @@ +import { redisDataSource } from '../redis_data_source' +import { logger } from '../utils/logger' + +export type ReadingProgressCacheItem = { + uid: string + libraryItemID: string + readingProgressPercent: number + readingProgressTopPercent: number | undefined + readingProgressAnchorIndex: number | undefined + updatedAt: string | undefined +} + +export const keyForCachedReadingPosition = ( + uid: string, + libraryItemID: string +): string => { + return `omnivore:reading-progress:${uid}:${libraryItemID}` +} + +// Reading positions are cached as an array of positions, when +// we fetch them from the cache we find the maximum values +export const clearCachedReadingPosition = async ( + uid: string, + libraryItemID: string +): Promise => { + const cacheKey = keyForCachedReadingPosition(uid, libraryItemID) + try { + const res = await redisDataSource.redisClient?.del(cacheKey) + return res ? res > 0 : false + } catch (error) { + logger.error('exception clearing cached reading position', { + cacheKey, + error, + }) + } + return false +} + +export const pushCachedReadingPosition = async ( + uid: string, + libraryItemID: string, + position: ReadingProgressCacheItem +): Promise => { + const cacheKey = keyForCachedReadingPosition(uid, libraryItemID) + try { + const result = await redisDataSource.redisClient?.lpush( + cacheKey, + JSON.stringify(position) + ) + return result ? result > 0 : false + } catch (error) { + logger.error('error writing cached reading position', { cacheKey, error }) + } + return false +} + +// Reading positions are cached as an array of positions, when +// we fetch them from the cache we find the maximum values +export const fetchCachedReadingPosition = async ( + uid: string, + libraryItemID: string +): Promise => { + const cacheKey = keyForCachedReadingPosition(uid, libraryItemID) + try { + const cacheItemList = await redisDataSource.redisClient?.lrange( + cacheKey, + 0, + -1 + ) + const items = cacheItemList?.map((item) => JSON.parse(item)) + if (!items || items.length < 1) { + return undefined + } + + const percent = Math.max( + ...items.map((o) => + 'readingProgressPercent' in o ? o.readingProgressPercent : 0 + ) + ) + const top = Math.max( + ...items.map((o) => + 'readingProgressTopPercent' in o ? o.readingProgressTopPercent : 0 + ) + ) + const anchor = Math.max( + ...items.map((o) => + 'readingProgressAnchorIndex' in o ? o.readingProgressAnchorIndex : 0 + ) + ) + + return { + uid, + libraryItemID, + readingProgressPercent: percent, + readingProgressTopPercent: top, + readingProgressAnchorIndex: anchor, + updatedAt: undefined, + } + } catch (error) { + logger.error('exception looking up cached reading position', { + cacheKey, + error, + }) + } + return undefined +}