Skip to content

Commit

Permalink
feat: make cache more customizable
Browse files Browse the repository at this point in the history
  • Loading branch information
César Alberca committed Dec 15, 2021
1 parent 980a4a1 commit 7942e57
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 47 deletions.
79 changes: 69 additions & 10 deletions packages/arch/src/cache/cache-manager.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { CacheManager } from './cache-manager'
import { CacheOptions } from './cache-options'
import { anyString, capture, instance, mock, verify, when } from 'ts-mockito'
import { Cache, CacheResult } from './cache'
import { Datetime, MockDatetime } from '@archimedes/utils'

describe('CacheManager', () => {
it('should return the result', () => {
const { cacheManager } = setup()
let count = 0

const actual = cacheManager.cache('foo', () => {
const actual = cacheManager.set('foo', () => {
count++
return count
})
Expand All @@ -21,9 +25,9 @@ describe('CacheManager', () => {
count++
return count
}
cacheManager.cache('foo', fn)
cacheManager.cache('foo', fn)
const actual = cacheManager.cache('foo', fn)
cacheManager.set('foo', fn)
cacheManager.set('foo', fn)
const actual = cacheManager.set('foo', fn)

expect(actual).toBe(1)
})
Expand All @@ -36,17 +40,72 @@ describe('CacheManager', () => {
count++
return count + (otherValue ?? 0)
}
cacheManager.cache('foo', () => fn())
cacheManager.cache('foo', () => fn())
cacheManager.cache('foo', () => fn(1))
const actual = cacheManager.cache('foo', () => fn(1))
cacheManager.set('foo', () => fn())
cacheManager.set('foo', () => fn())
cacheManager.set('foo', () => fn(1))
const actual = cacheManager.set('foo', () => fn(1))

expect(actual).toBe(1)
})

it('should use custom cache', () => {
MockDatetime.mock(Datetime.fromIsoString('2020-01-01T00:00:00Z'))
const cache = mock<Cache>()
when(cache.get(anyString())).thenReturn({ createdAt: 1, returns: 1 })
const { cacheManager } = setup({ cache: instance(cache) })
let count = 0

const fn = () => {
count++
return count
}

cacheManager.set('foo', fn)

const [first, second] = capture(cache.set).last()
expect(first).toBe('d751713988987e9331980363e24189ce')
expect(second).toEqual<CacheResult>({ createdAt: 1577836800000, returns: 1 })
})

it('should delete cache if ttl has passed', () => {
MockDatetime.mock(Datetime.fromIsoString('2020-01-01T00:00:00Z'))
const mockedDatetime = Datetime.now().toMillis() - 1
const cache = mock<Cache>()
when(cache.get(anyString())).thenReturn({ createdAt: mockedDatetime, returns: 1 })
const { cacheManager } = setup({ cache: instance(cache), ttl: 0 })
let count = 0

const fn = () => {
count++
return count
}

cacheManager.set('foo', fn)

verify(cache.delete(anyString())).once()
})

it('should not delete cache if the ttl has not passed', () => {
MockDatetime.mock(Datetime.fromIsoString('2020-01-01T00:00:00Z'))
const mockedDatetime = Datetime.now().toMillis() - 1
const cache = mock<Cache>()
when(cache.get(anyString())).thenReturn({ createdAt: mockedDatetime, returns: 1 })
const { cacheManager } = setup({ cache: instance(cache), ttl: 1 })
let count = 0

const fn = () => {
count++
return count
}

cacheManager.set('foo', fn)

verify(cache.delete(anyString())).never()
})
})

function setup() {
function setup(cacheOptions?: Partial<CacheOptions>) {
return {
cacheManager: new CacheManager()
cacheManager: new CacheManager(cacheOptions)
}
}
30 changes: 19 additions & 11 deletions packages/arch/src/cache/cache-manager.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
import { LruCache } from './lru-cache'
import { Cache } from './cache'
import { CacheKey } from './cache-key'
import { Md5 } from 'ts-md5'
import { Datetime, isPromise } from '@archimedes/utils'
import { CacheOptions } from './cache-options'
import { LruCache } from './lru-cache'

export class CacheManager {
private caches: Map<CacheKey, Cache<any>> = new Map()
private caches: Map<CacheKey, Cache> = new Map()

private readonly cacheOptions: CacheOptions

private readonly ttl = 500_000
constructor(cacheOptions?: Partial<CacheOptions>) {
this.cacheOptions = {
ttl: cacheOptions?.ttl ?? 500_000,
cache: cacheOptions?.cache ?? new LruCache()
}
}

isCached(cacheKey: CacheKey, args: unknown[]): boolean {
has(cacheKey: CacheKey, args: unknown[]): boolean {
const existingCache = this.caches.get(cacheKey)
const hash = this.getHash(args)
return existingCache?.has(hash) ?? false
}

cache(cacheKey: CacheKey, fn: (...fnArgs: unknown[]) => unknown, ...args: any[]) {
set(cacheKey: CacheKey, fn: (...fnArgs: unknown[]) => unknown, ...args: any[]): unknown {
if (!this.caches.has(cacheKey)) {
this.caches.set(cacheKey, new LruCache())
this.caches.set(cacheKey, this.cacheOptions.cache)
}

const existingCache = this.caches.get(cacheKey)!
const hash = this.getHash(args)
const now = Datetime.now()

if (!this.isCached(cacheKey, args)) {
if (!this.has(cacheKey, args)) {
existingCache.set(hash, { createdAt: now.toMillis(), returns: fn.apply(this, args) })
}

Expand All @@ -36,22 +44,22 @@ export class CacheManager {
})
}

if (now.toMillis() - existingResult.createdAt > this.ttl) {
if (now.toMillis() - existingResult.createdAt > this.cacheOptions.ttl) {
existingCache.delete(hash)
}

return existingResult.returns
}

invalidateCache(cacheKey: CacheKey) {
invalidate(cacheKey: CacheKey): void {
this.caches.delete(cacheKey)
}

invalidateCaches() {
invalidateAll(): void {
this.caches.clear()
}

private getHash(args: unknown[]) {
private getHash(args: unknown[]): string {
return new Md5().appendStr(JSON.stringify(args)).end() as string
}
}
6 changes: 6 additions & 0 deletions packages/arch/src/cache/cache-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Cache } from './cache'

export interface CacheOptions {
ttl: number
cache: Cache
}
13 changes: 10 additions & 3 deletions packages/arch/src/cache/cache.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { CacheKey } from './cache-key'

export interface Cache<T> {
get(key: CacheKey): T | undefined
set(key: CacheKey, value: T): void
type Milliseconds = number

export interface CacheResult<T = unknown> {
createdAt: Milliseconds
returns: T
}

export interface Cache<T = unknown> {
get(key: CacheKey): CacheResult<T> | undefined
set(key: CacheKey, value: CacheResult<T>): void
has(key: CacheKey): boolean
delete(key: CacheKey): void
clear(): void
Expand Down
8 changes: 4 additions & 4 deletions packages/arch/src/cache/lru-cache.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Cache } from './cache'
import { Cache, CacheResult } from './cache'
import lru, { Lru } from 'tiny-lru'
import { CacheKey } from './cache-key'

export class LruCache<T> implements Cache<T> {
private _lru: Lru<T> = lru(100)
private _lru: Lru<CacheResult<T>> = lru(100)

get(key: CacheKey): T | undefined {
get(key: CacheKey): CacheResult<T> | undefined {
return this._lru.get(key)
}

set(key: CacheKey, value: T) {
set(key: CacheKey, value: CacheResult<T>) {
this._lru.set(key, value)
}

Expand Down
6 changes: 3 additions & 3 deletions packages/arch/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export type { Cache } from './cache/cache'
export type { Cache, CacheResult } from './cache/cache'
export { InvalidationPolicy } from './cache/invalidation-policy'
export { LruCache } from './cache/lru-cache'
export { CacheManager } from './cache/cache-manager'
export { EvictCache } from './cache/evict-cache'
export { NotificationCenter } from './notifications/notification-center'
export { Notification } from './notifications/notification'
export { Runner } from './runner/runner'
Expand All @@ -14,10 +16,8 @@ export { ExecutorLink } from './runner/links/executor-link'
export { EmptyLink } from './runner/links/empty-link'
export { NullLink } from './runner/links/null-link'
export { ChainError } from './runner/chain-error'
export { EvictCache } from './cache/evict-cache'
export { CacheInvalidations } from './runner/cache-invalidations'
export type { Logger } from './runner/logger'
export { InvalidationPolicy } from './cache/invalidation-policy'
export { Command } from './use-case/command'
export { Query } from './use-case/query'
export type { ExecutionOptions } from './use-case/execution-options'
Expand Down
22 changes: 11 additions & 11 deletions packages/arch/src/runner/links/cache-link.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Command } from '../../use-case/command'
describe('CacheLink', () => {
it('should use the cache', async () => {
const { link, cacheManager, cacheLink } = setup()
when(cacheManager.isCached(anything(), anything())).thenReturn(false)
when(cacheManager.has(anything(), anything())).thenReturn(false)

class MockUseCase extends UseCase<unknown, unknown> {
readonly = true
Expand All @@ -33,7 +33,7 @@ describe('CacheLink', () => {

it('should break the link if it is cached', async () => {
const { link, cacheManager, cacheLink } = setup()
when(cacheManager.isCached(anything(), anything())).thenReturn(true)
when(cacheManager.has(anything(), anything())).thenReturn(true)

class MockUseCase extends UseCase<unknown, unknown> {
readonly = true
Expand All @@ -55,7 +55,7 @@ describe('CacheLink', () => {

it('should not cache commands', async () => {
const { link, cacheManager, cacheLink } = setup()
when(cacheManager.isCached(anything(), anything())).thenReturn(false)
when(cacheManager.has(anything(), anything())).thenReturn(false)

class MockUseCase extends Command<unknown, unknown> {
async internalExecute(): Promise<void> {}
Expand All @@ -70,12 +70,12 @@ describe('CacheLink', () => {

await cacheLink.next(context)

verify(cacheManager.cache(anything(), anything())).never()
verify(cacheManager.set(anything(), anything())).never()
})

it('should invalidate using no cache policy', async () => {
const { link, cacheManager, cacheLink } = setup()
when(cacheManager.isCached(anything(), anything())).thenReturn(true)
when(cacheManager.has(anything(), anything())).thenReturn(true)

class MockUseCase extends UseCase<unknown, unknown> {
readonly = true
Expand All @@ -93,13 +93,13 @@ describe('CacheLink', () => {

await cacheLink.next(context)

verify(cacheManager.invalidateCache(MockUseCase.name)).once()
verify(cacheManager.invalidate(MockUseCase.name)).once()
CacheInvalidations.clear()
})

it('should invalidate using all cache policy', async () => {
const { link, cacheManager, cacheLink } = setup()
when(cacheManager.isCached(anything(), anything())).thenReturn(true)
when(cacheManager.has(anything(), anything())).thenReturn(true)

class MockUseCase extends UseCase<unknown, unknown> {
readonly = true
Expand All @@ -117,13 +117,13 @@ describe('CacheLink', () => {

await cacheLink.next(context)

verify(cacheManager.invalidateCaches()).once()
verify(cacheManager.invalidateAll()).once()
CacheInvalidations.clear()
})

it('should invalidate using all cache policy', async () => {
const { link, cacheManager, cacheLink } = setup()
when(cacheManager.isCached(anything(), anything())).thenReturn(true)
when(cacheManager.has(anything(), anything())).thenReturn(true)

class MockUseCase extends UseCase<unknown, unknown> {
readonly = true
Expand All @@ -142,8 +142,8 @@ describe('CacheLink', () => {

await cacheLink.next(context)

verify(cacheManager.invalidateCache('Foo')).once()
verify(cacheManager.invalidateCache('Bar')).once()
verify(cacheManager.invalidate('Foo')).once()
verify(cacheManager.invalidate('Bar')).once()
CacheInvalidations.clear()
})
})
Expand Down
10 changes: 5 additions & 5 deletions packages/arch/src/runner/links/cache-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ export class CacheLink extends BaseLink {
async next(context: Context): Promise<void> {
const name = context.useCase.constructor.name

if (!this.cacheManager.isCached(name, [context.param])) {
if (!this.cacheManager.has(name, [context.param])) {
this.nextLink.next(context)
}

context.result = context.useCase.readonly
? this.cacheManager.cache(name, () => context.result, context.param)
? (this.cacheManager.set(name, () => context.result, context.param) as Promise<unknown>)
: context.result

this.invalidateCache(name)
Expand All @@ -28,13 +28,13 @@ export class CacheLink extends BaseLink {
CacheInvalidations.invalidations.get(cacheKey)?.forEach(invalidation => {
switch (invalidation) {
case InvalidationPolicy.NO_CACHE:
this.cacheManager.invalidateCache(cacheKey)
this.cacheManager.invalidate(cacheKey)
break
case InvalidationPolicy.ALL:
this.cacheManager.invalidateCaches()
this.cacheManager.invalidateAll()
break
default:
this.cacheManager.invalidateCache(invalidation)
this.cacheManager.invalidate(invalidation)
if (CacheInvalidations.invalidations.has(invalidation)) {
this.invalidateCache(invalidation)
}
Expand Down

0 comments on commit 7942e57

Please sign in to comment.