193 lines
5.0 KiB
TypeScript
193 lines
5.0 KiB
TypeScript
import { redisDataSource } from '../redis_data_source'
|
|
import { logger } from '../utils/logger'
|
|
|
|
export const CACHED_READING_POSITION_PREFIX = `omnivore:reading-progress`
|
|
|
|
export type ReadingProgressCacheItem = {
|
|
uid: string
|
|
libraryItemID: string
|
|
readingProgressPercent: number
|
|
readingProgressTopPercent: number | undefined
|
|
readingProgressAnchorIndex: number | undefined
|
|
updatedAt: string | undefined
|
|
}
|
|
|
|
export const isReadingProgressCacheItem = (
|
|
item: any
|
|
): item is ReadingProgressCacheItem => {
|
|
return (
|
|
'uid' in item && 'libraryItemID' in item && 'readingProgressPercent' in item
|
|
)
|
|
}
|
|
|
|
export const parseReadingProgressCacheItem = (
|
|
item: any
|
|
): ReadingProgressCacheItem | undefined => {
|
|
const result = JSON.parse(item) as unknown
|
|
if (isReadingProgressCacheItem(result)) {
|
|
return result
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export const keyForCachedReadingPosition = (
|
|
uid: string,
|
|
libraryItemID: string
|
|
): string => {
|
|
return `${CACHED_READING_POSITION_PREFIX}:${uid}:${libraryItemID}`
|
|
}
|
|
|
|
export const componentsForCachedReadingPositionKey = (
|
|
cacheKey: string
|
|
): { uid: string; libraryItemID: string } | undefined => {
|
|
try {
|
|
const [_owner, _prefix, uid, libraryItemID] = cacheKey.split(':')
|
|
return {
|
|
uid,
|
|
libraryItemID,
|
|
}
|
|
} catch (error) {
|
|
logger.log('exception getting cache key components', { cacheKey, error })
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
// 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<boolean> => {
|
|
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<boolean> => {
|
|
const cacheKey = keyForCachedReadingPosition(uid, libraryItemID)
|
|
try {
|
|
// Its critical that the date is set so the entry will be a unique
|
|
// set value.
|
|
position.updatedAt = new Date().toISOString()
|
|
const result = await redisDataSource.redisClient?.sadd(
|
|
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<ReadingProgressCacheItem | undefined> => {
|
|
try {
|
|
const items = await fetchCachedReadingPositionsAndMembers(
|
|
uid,
|
|
libraryItemID
|
|
)
|
|
if (!items) {
|
|
return undefined
|
|
}
|
|
return reduceCachedReadingPositionMembers(
|
|
uid,
|
|
libraryItemID,
|
|
items.positionItems
|
|
)
|
|
} catch (error) {
|
|
logger.error('exception looking up cached reading position', {
|
|
uid,
|
|
libraryItemID,
|
|
error,
|
|
})
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export const reduceCachedReadingPositionMembers = (
|
|
uid: string,
|
|
libraryItemID: string,
|
|
items: ReadingProgressCacheItem[]
|
|
): ReadingProgressCacheItem | undefined => {
|
|
try {
|
|
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 : 0
|
|
)
|
|
)
|
|
const anchor = Math.max(
|
|
...items.map((o) =>
|
|
'readingProgressAnchorIndex' in o
|
|
? o.readingProgressAnchorIndex ?? 0
|
|
: 0
|
|
)
|
|
)
|
|
return {
|
|
uid,
|
|
libraryItemID,
|
|
readingProgressPercent: percent,
|
|
readingProgressTopPercent: top,
|
|
readingProgressAnchorIndex: anchor,
|
|
updatedAt: undefined,
|
|
}
|
|
} catch (error) {
|
|
logger.error('exception reducing cached reading items', {
|
|
uid,
|
|
libraryItemID,
|
|
error,
|
|
})
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export const fetchCachedReadingPositionsAndMembers = async (
|
|
uid: string,
|
|
libraryItemID: string
|
|
): Promise<
|
|
{ positionItems: ReadingProgressCacheItem[]; members: string[] } | undefined
|
|
> => {
|
|
const cacheKey = keyForCachedReadingPosition(uid, libraryItemID)
|
|
try {
|
|
const members = await redisDataSource.redisClient?.smembers(cacheKey)
|
|
if (!members) {
|
|
return undefined
|
|
}
|
|
const positionItems = members
|
|
?.map((item) => parseReadingProgressCacheItem(item))
|
|
.filter(isReadingProgressCacheItem)
|
|
return { members, positionItems }
|
|
} catch (error) {
|
|
logger.error('exception looking up cached reading position', {
|
|
cacheKey,
|
|
error,
|
|
})
|
|
}
|
|
return undefined
|
|
}
|