Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions lib/AbstractFlatCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ export abstract class AbstractFlatCache<LoadedValue, LoadParams = string, LoadMa
}

public getInMemoryOnly(loadParams: LoadParams): LoadedValue | undefined | null {
const key = this.cacheKeyFromLoadParamsResolver(loadParams)
return this.getInMemoryOnlyResolved(this.cacheKeyFromLoadParamsResolver(loadParams), loadParams)
}

private getInMemoryOnlyResolved(key: string, loadParams: LoadParams): LoadedValue | undefined | null {
if (this.inMemoryCache.ttlLeftBeforeRefreshInMsecs && !this.runningLoads.has(key)) {
const expirationTime = this.inMemoryCache.getExpirationTime(key)
if (expirationTime && expirationTime - Date.now() < this.inMemoryCache.ttlLeftBeforeRefreshInMsecs) {
void this.getAsyncOnly(loadParams)
void this.getAsyncOnlyResolved(key, loadParams)
}
}

Expand All @@ -37,7 +40,10 @@ export abstract class AbstractFlatCache<LoadedValue, LoadParams = string, LoadMa
}

public getAsyncOnly(loadParams: LoadParams): Promise<LoadedValue | undefined | null> {
const key = this.cacheKeyFromLoadParamsResolver(loadParams)
return this.getAsyncOnlyResolved(this.cacheKeyFromLoadParamsResolver(loadParams), loadParams)
}

private getAsyncOnlyResolved(key: string, loadParams: LoadParams): Promise<LoadedValue | undefined | null> {
const existingLoad = this.runningLoads.get(key)
if (existingLoad) {
return existingLoad
Expand Down Expand Up @@ -86,12 +92,13 @@ export abstract class AbstractFlatCache<LoadedValue, LoadParams = string, LoadMa
}

public get(loadParams: LoadParams): Promise<LoadedValue | undefined | null> {
const inMemoryValue = this.getInMemoryOnly(loadParams)
const key = this.cacheKeyFromLoadParamsResolver(loadParams)
const inMemoryValue = this.getInMemoryOnlyResolved(key, loadParams)
if (inMemoryValue !== undefined) {
return Promise.resolve(inMemoryValue)
}

return this.getAsyncOnly(loadParams)
return this.getAsyncOnlyResolved(key, loadParams)
}

public getMany(keys: string[], loadParams?: LoadManyParams): Promise<LoadedValue[]> {
Expand All @@ -103,7 +110,12 @@ export abstract class AbstractFlatCache<LoadedValue, LoadParams = string, LoadMa
}

return this.getManyAsyncOnly(inMemoryValues.unresolvedKeys, loadParams).then((asyncRetrievedValues) => {
return [...inMemoryValues.resolvedValues, ...asyncRetrievedValues.resolvedValues]
// in-memory caches always return a fresh array, so it is safe to append to it in place
const mergedValues = inMemoryValues.resolvedValues
for (let i = 0; i < asyncRetrievedValues.resolvedValues.length; i++) {
mergedValues.push(asyncRetrievedValues.resolvedValues[i])
}
return mergedValues
})
}

Expand Down
31 changes: 25 additions & 6 deletions lib/AbstractGroupCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,19 @@ export abstract class AbstractGroupCache<LoadedValue, LoadParams = string, LoadM
}

public getInMemoryOnly(loadParams: LoadParams, group: string): LoadedValue | undefined | null {
const key = this.cacheKeyFromLoadParamsResolver(loadParams)
return this.getInMemoryOnlyResolved(this.cacheKeyFromLoadParamsResolver(loadParams), loadParams, group)
}

private getInMemoryOnlyResolved(
key: string,
loadParams: LoadParams,
group: string,
): LoadedValue | undefined | null {
if (this.inMemoryCache.ttlLeftBeforeRefreshInMsecs) {
if (!this.runningLoads.get(group)?.has(key)) {
const expirationTime = this.inMemoryCache.getExpirationTimeFromGroup(key, group)
if (expirationTime && expirationTime - Date.now() < this.inMemoryCache.ttlLeftBeforeRefreshInMsecs) {
void this.getAsyncOnly(loadParams, group)
void this.getAsyncOnlyResolved(key, loadParams, group)
}
}
}
Expand All @@ -61,7 +68,14 @@ export abstract class AbstractGroupCache<LoadedValue, LoadParams = string, LoadM
}

public getAsyncOnly(loadParams: LoadParams, group: string): Promise<LoadedValue | undefined | null> {
const key = this.cacheKeyFromLoadParamsResolver(loadParams)
return this.getAsyncOnlyResolved(this.cacheKeyFromLoadParamsResolver(loadParams), loadParams, group)
}

private getAsyncOnlyResolved(
key: string,
loadParams: LoadParams,
group: string,
): Promise<LoadedValue | undefined | null> {
const groupLoads = this.resolveGroupLoads(group)
const existingLoad = groupLoads.get(key)
if (existingLoad) {
Expand Down Expand Up @@ -111,12 +125,12 @@ export abstract class AbstractGroupCache<LoadedValue, LoadParams = string, LoadM

public get(loadParams: LoadParams, group: string): Promise<LoadedValue | undefined | null> {
const key = this.cacheKeyFromLoadParamsResolver(loadParams)
const inMemoryValue = this.getInMemoryOnly(loadParams, group)
const inMemoryValue = this.getInMemoryOnlyResolved(key, loadParams, group)
if (inMemoryValue !== undefined) {
return Promise.resolve(inMemoryValue)
}

return this.getAsyncOnly(loadParams, group)
return this.getAsyncOnlyResolved(key, loadParams, group)
}

public getMany(
Expand All @@ -133,7 +147,12 @@ export abstract class AbstractGroupCache<LoadedValue, LoadParams = string, LoadM

return this.getManyAsyncOnly(inMemoryValues.unresolvedKeys, group, loadParams).then(
(asyncRetrievedValues) => {
return [...inMemoryValues.resolvedValues, ...asyncRetrievedValues.resolvedValues]
// in-memory caches always return a fresh array, so it is safe to append to it in place
const mergedValues = inMemoryValues.resolvedValues
for (let i = 0; i < asyncRetrievedValues.resolvedValues.length; i++) {
mergedValues.push(asyncRetrievedValues.resolvedValues[i])
}
return mergedValues
},
)
}
Expand Down
26 changes: 15 additions & 11 deletions lib/GroupLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,13 @@ export class GroupLoader<LoadedValue, LoadParams = string, LoadManyParams = Load
const loadValues = await this.loadManyFromLoaders(cachedValues.unresolvedKeys, group, loadParams)

if (this.asyncCache) {
const cacheEntries: CacheEntry<LoadedValue>[] = loadValues.map((loadValue) => {
return {
key: this.cacheKeyFromValueResolver(loadValue),
value: loadValue,
}
})
const cacheEntries: CacheEntry<LoadedValue>[] = []
for (let i = 0; i < loadValues.length; i++) {
cacheEntries.push({
key: this.cacheKeyFromValueResolver(loadValues[i]),
value: loadValues[i],
})
}

await this.asyncCache.setManyForGroup(cacheEntries, group).catch((err) => {
this.cacheUpdateErrorHandler(
Expand All @@ -172,7 +173,8 @@ export class GroupLoader<LoadedValue, LoadParams = string, LoadManyParams = Load
}

return {
resolvedValues: [...cachedValues.resolvedValues, ...loadValues],
// concat instead of in-place push, as cachedValues may be owned by a user-implemented async cache
resolvedValues: cachedValues.resolvedValues.concat(loadValues),

// there actually may still be some unresolved keys, but we no longer know that
unresolvedKeys: [],
Expand All @@ -181,8 +183,9 @@ export class GroupLoader<LoadedValue, LoadParams = string, LoadManyParams = Load

private async loadFromLoaders(key: string, group: string, loadParams: LoadParams) {
for (let index = 0; index < this.dataSources.length; index++) {
const resolvedValue = await this.dataSources[index].getFromGroup(loadParams, group).catch((err) => {
this.loadErrorHandler(err, key, this.dataSources[index], this.logger)
const dataSource = this.dataSources[index]
const resolvedValue = await dataSource.getFromGroup(loadParams, group).catch((err) => {
this.loadErrorHandler(err, key, dataSource, this.logger)
if (this.throwIfLoadError) {
throw err
}
Expand All @@ -209,8 +212,9 @@ export class GroupLoader<LoadedValue, LoadParams = string, LoadManyParams = Load
private async loadManyFromLoaders(keys: string[], group: string, loadParams?: LoadManyParams) {
let lastResolvedValues
for (let index = 0; index < this.dataSources.length; index++) {
lastResolvedValues = await this.dataSources[index].getManyFromGroup(keys, group, loadParams).catch((err) => {
this.loadErrorHandler(err, keys.toString(), this.dataSources[index], this.logger)
const dataSource = this.dataSources[index]
lastResolvedValues = await dataSource.getManyFromGroup(keys, group, loadParams).catch((err) => {
this.loadErrorHandler(err, keys.toString(), dataSource, this.logger)
if (this.throwIfLoadError) {
throw err
}
Expand Down
26 changes: 15 additions & 11 deletions lib/Loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,9 @@ export class Loader<LoadedValue, LoadParams = string, LoadManyParams = LoadParam

private async loadFromLoaders(key: string, loadParams: LoadParams) {
for (let index = 0; index < this.dataSources.length; index++) {
const resolvedValue = await this.dataSources[index].get(loadParams).catch((err) => {
this.loadErrorHandler(err, key, this.dataSources[index], this.logger)
const dataSource = this.dataSources[index]
const resolvedValue = await dataSource.get(loadParams).catch((err) => {
this.loadErrorHandler(err, key, dataSource, this.logger)
if (this.throwIfLoadError) {
throw err
}
Expand Down Expand Up @@ -222,12 +223,13 @@ export class Loader<LoadedValue, LoadParams = string, LoadManyParams = LoadParam
const loadValues = await this.loadManyFromLoaders(cachedValues.unresolvedKeys, loadParams)

if (this.asyncCache) {
const cacheEntries: CacheEntry<LoadedValue>[] = loadValues.map((loadValue) => {
return {
key: this.cacheKeyFromValueResolver(loadValue),
value: loadValue,
}
})
const cacheEntries: CacheEntry<LoadedValue>[] = []
for (let i = 0; i < loadValues.length; i++) {
cacheEntries.push({
key: this.cacheKeyFromValueResolver(loadValues[i]),
value: loadValues[i],
})
}

await this.asyncCache.setMany(cacheEntries).catch((err) => {
this.cacheUpdateErrorHandler(
Expand All @@ -240,7 +242,8 @@ export class Loader<LoadedValue, LoadParams = string, LoadManyParams = LoadParam
}

return {
resolvedValues: [...cachedValues.resolvedValues, ...loadValues],
// concat instead of in-place push, as cachedValues may be owned by a user-implemented async cache
resolvedValues: cachedValues.resolvedValues.concat(loadValues),

// there actually may still be some unresolved keys, but we no longer know that
unresolvedKeys: [],
Expand All @@ -250,8 +253,9 @@ export class Loader<LoadedValue, LoadParams = string, LoadManyParams = LoadParam
private async loadManyFromLoaders(keys: string[], loadParams: LoadManyParams) {
let lastResolvedValues
for (let index = 0; index < this.dataSources.length; index++) {
lastResolvedValues = await this.dataSources[index].getMany(keys, loadParams).catch((err) => {
this.loadErrorHandler(err, keys.toString(), this.dataSources[index], this.logger)
const dataSource = this.dataSources[index]
lastResolvedValues = await dataSource.getMany(keys, loadParams).catch((err) => {
this.loadErrorHandler(err, keys.toString(), dataSource, this.logger)
if (this.throwIfLoadError) {
throw err
}
Expand Down
6 changes: 4 additions & 2 deletions lib/redis/AbstractRedisCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const DEFAULT_REDIS_CACHE_CONFIGURATION: RedisCacheConfiguration = {
export abstract class AbstractRedisCache<ConfigType extends RedisCacheConfiguration, LoadedValue> {
protected readonly redis: Redis
protected readonly config: ConfigType
protected readonly keyPrefix: string

constructor(redis: Redis, config: Partial<ConfigType>) {
this.redis = redis
Expand All @@ -26,6 +27,7 @@ export abstract class AbstractRedisCache<ConfigType extends RedisCacheConfigurat
...DEFAULT_REDIS_CACHE_CONFIGURATION,
...config,
}
this.keyPrefix = `${this.config.prefix}${this.config.separator}`
}

protected internalSet(resolvedKey: string, value: LoadedValue | null) {
Expand Down Expand Up @@ -64,10 +66,10 @@ export abstract class AbstractRedisCache<ConfigType extends RedisCacheConfigurat
}

resolveKey(key: string) {
return `${this.config.prefix}${this.config.separator}${key}`
return this.keyPrefix + key
}

resolveCachePattern() {
return `${this.config.prefix}${this.config.separator}*`
return `${this.keyPrefix}*`
}
}
19 changes: 14 additions & 5 deletions lib/redis/RedisGroupCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ export interface RedisGroupCacheConfiguration extends RedisCacheConfiguration, G
export class RedisGroupCache<T> extends AbstractRedisCache<RedisGroupCacheConfiguration, T> implements GroupCache<T> {
public readonly expirationTimeLoadingGroupedOperation: GroupLoader<number>
public ttlLeftBeforeRefreshInMsecs?: number
private readonly groupIndexPrefix: string
name = 'Redis group cache'

constructor(redis: Redis, config: Partial<RedisGroupCacheConfiguration> = {}) {
super(redis, config)

this.groupIndexPrefix = `${this.keyPrefix}${GROUP_INDEX_KEY}${this.config.separator}`

this.ttlLeftBeforeRefreshInMsecs = config.ttlLeftBeforeRefreshInMsecs
this.redis.defineCommand('getOrSetZeroWithTtl', {
lua: GET_OR_SET_ZERO_WITH_TTL,
Expand Down Expand Up @@ -88,7 +91,8 @@ export class RedisGroupCache<T> extends AbstractRedisCache<RedisGroupCacheConfig
}
}

const transformedKeys = keys.map((key) => this.resolveKeyWithGroup(key, groupId, currentGroupKey))
const entryPrefix = this.resolveGroupEntryPrefix(groupId, currentGroupKey)
const transformedKeys = keys.map((key) => entryPrefix + key)
const resolvedValues: T[] = []
const unresolvedKeys: string[] = []

Expand Down Expand Up @@ -182,13 +186,14 @@ export class RedisGroupCache<T> extends AbstractRedisCache<RedisGroupCacheConfig

const currentGroupKey = await getGroupKeyPromise

const entryPrefix = this.resolveGroupEntryPrefix(groupId, currentGroupKey)
if (this.config.ttlInMsecs) {
const setCommands = []
for (let i = 0; i < entries.length; i++) {
const entry = entries[i]
setCommands.push([
'set',
this.resolveKeyWithGroup(entry.key, groupId, currentGroupKey),
entryPrefix + entry.key,
entry.value && this.config.json ? JSON.stringify(entry.value) : entry.value,
'PX',
this.config.ttlInMsecs,
Expand All @@ -202,18 +207,22 @@ export class RedisGroupCache<T> extends AbstractRedisCache<RedisGroupCacheConfig
const commandParam = []
for (let i = 0; i < entries.length; i++) {
const entry = entries[i]
commandParam.push(this.resolveKeyWithGroup(entry.key, groupId, currentGroupKey))
commandParam.push(entryPrefix + entry.key)
commandParam.push(entry.value && this.config.json ? JSON.stringify(entry.value) : entry.value)
}
return this.redis.mset(commandParam)
}

resolveKeyWithGroup(key: string, groupId: string, groupIndexKey: string) {
return `${this.config.prefix}${this.config.separator}${groupId}${this.config.separator}${groupIndexKey}${this.config.separator}${key}`
return this.resolveGroupEntryPrefix(groupId, groupIndexKey) + key
}

private resolveGroupEntryPrefix(groupId: string, groupIndexKey: string) {
return `${this.keyPrefix}${groupId}${this.config.separator}${groupIndexKey}${this.config.separator}`
}

resolveGroupIndexPrefix(groupId: string) {
return `${this.config.prefix}${this.config.separator}${GROUP_INDEX_KEY}${this.config.separator}${groupId}`
return this.groupIndexPrefix + groupId
}

async close() {
Expand Down
10 changes: 10 additions & 0 deletions lib/util/unique.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ import { describe, expect, it } from 'vitest'
import { unique } from './unique'

describe('unique', () => {
it('returns the same array reference for an empty array', () => {
const input: string[] = []
expect(unique(input)).toBe(input)
})

it('returns the same array reference for a single-element array', () => {
const input = ['key']
expect(unique(input)).toBe(input)
})

it('returns the same array reference when there are no duplicates', () => {
const input = [1, 2, 3, 'a', 'b']
const result = unique(input)
Expand Down
3 changes: 3 additions & 0 deletions lib/util/unique.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const unique = <T>(arr: T[]): T[] => {
if (arr.length <= 1) {
return arr
}
const set = new Set(arr)
return set.size === arr.length ? arr : Array.from(set)
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"homepage": "https://github.com/kibertoad/layered-loader",
"dependencies": {
"ioredis": "^5.10.1",
"toad-cache": "^3.7.1"
"toad-cache": "^3.7.4"
},
"devDependencies": {
"@biomejs/biome": "^2.4.16",
Expand Down
Loading
Loading