mirror of
https://github.com/koel/koel
synced 2024-12-01 00:09:17 +00:00
feat(test): add cache tests
This commit is contained in:
parent
9aa50d1963
commit
2fae65bb91
8 changed files with 110 additions and 49 deletions
57
resources/assets/js/services/cache.spec.ts
Normal file
57
resources/assets/js/services/cache.spec.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { expect, it, vi } from 'vitest'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { Cache } from '@/services/cache'
|
||||
|
||||
let cache: Cache
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected beforeEach () {
|
||||
super.beforeEach(() => cache = new Cache())
|
||||
}
|
||||
|
||||
protected afterEach () {
|
||||
super.afterEach(() => vi.useRealTimers())
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('sets and gets a value', () => {
|
||||
cache.set('foo', 'bar')
|
||||
expect(cache.get('foo')).toBe('bar')
|
||||
})
|
||||
|
||||
it('invalidates an entry after set time', () => {
|
||||
vi.useFakeTimers()
|
||||
cache.set('foo', 'bar', 999)
|
||||
expect(cache.has('foo')).toBe(true)
|
||||
|
||||
vi.advanceTimersByTime(1000 * 1000)
|
||||
expect(cache.has('foo')).toBe(false)
|
||||
})
|
||||
|
||||
it('removes an entry', () => {
|
||||
cache.set('foo', 'bar')
|
||||
cache.remove('foo')
|
||||
expect(cache.get('foo')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('checks an entry\'s presence', () => {
|
||||
cache.set('foo', 'bar')
|
||||
expect(cache.hit('foo')).toBe(true)
|
||||
expect(cache.has('foo')).toBe(true)
|
||||
expect(cache.miss('foo')).toBe(false)
|
||||
|
||||
cache.remove('foo')
|
||||
expect(cache.hit('foo')).toBe(false)
|
||||
expect(cache.has('foo')).toBe(false)
|
||||
expect(cache.miss('foo')).toBe(true)
|
||||
})
|
||||
|
||||
it('remembers a value', async () => {
|
||||
const resolver = vi.fn().mockResolvedValue('bar')
|
||||
expect(cache.has('foo')).toBe(false)
|
||||
|
||||
expect(await cache.remember('foo', resolver)).toBe('bar')
|
||||
expect(cache.get('foo')).toBe('bar')
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,54 +1,58 @@
|
|||
const CACHE_EXPIRATION_TIME = 1000 * 60 * 60 * 24 // 1 day
|
||||
const DEFAULT_EXPIRATION_TIME = 1000 * 60 * 60 * 24 // 1 day
|
||||
|
||||
export const Cache = {
|
||||
storage: new Map<any, {
|
||||
time: number,
|
||||
export class Cache {
|
||||
private storage = new Map<any, {
|
||||
expires: number,
|
||||
value: any
|
||||
}>(),
|
||||
}>()
|
||||
|
||||
normalizeKey: (key: any) => typeof key === 'object' ? JSON.stringify(key) : key,
|
||||
private static normalizeKey (key: any) {
|
||||
return typeof key === 'object' ? JSON.stringify(key) : key
|
||||
}
|
||||
|
||||
has (key: any) {
|
||||
return this.hit(this.normalizeKey(key))
|
||||
},
|
||||
public has (key: any) {
|
||||
return this.hit(Cache.normalizeKey(key))
|
||||
}
|
||||
|
||||
get<T> (key: any) {
|
||||
return this.storage.get(this.normalizeKey(key))!.value as T
|
||||
},
|
||||
public get<T> (key: any) {
|
||||
return this.storage.get(Cache.normalizeKey(key))?.value as T
|
||||
}
|
||||
|
||||
set (key: any, value: any) {
|
||||
this.storage.set(this.normalizeKey(key), {
|
||||
public set (key: any, value: any, seconds: number = DEFAULT_EXPIRATION_TIME) {
|
||||
this.storage.set(Cache.normalizeKey(key), {
|
||||
value,
|
||||
time: Date.now()
|
||||
expires: Date.now() + seconds * 1000
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
hit (key: any) {
|
||||
return !this.miss(this.normalizeKey(key))
|
||||
},
|
||||
public hit (key: any) {
|
||||
return !this.miss(Cache.normalizeKey(key))
|
||||
}
|
||||
|
||||
miss (key: any) {
|
||||
key = this.normalizeKey(key)
|
||||
public miss (key: any) {
|
||||
key = Cache.normalizeKey(key)
|
||||
|
||||
if (!this.storage.has(key)) return true
|
||||
const { time } = this.storage.get(key)!
|
||||
const { expires } = this.storage.get(key)!
|
||||
|
||||
if (time < Date.now() - CACHE_EXPIRATION_TIME) {
|
||||
if (expires < Date.now()) {
|
||||
this.storage.delete(key)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
invalidate (key: any) {
|
||||
this.storage.delete(this.normalizeKey(key))
|
||||
},
|
||||
public remove (key: any) {
|
||||
this.storage.delete(Cache.normalizeKey(key))
|
||||
}
|
||||
|
||||
async resolve<T> (key: any, fetcher: Closure) {
|
||||
key = this.normalizeKey(key)
|
||||
async remember<T> (key: any, resolver: Closure, seconds: number = DEFAULT_EXPIRATION_TIME) {
|
||||
key = Cache.normalizeKey(key)
|
||||
|
||||
this.hit(key) || this.set(key, await fetcher())
|
||||
this.hit(key) || this.set(key, await resolver(), seconds)
|
||||
return this.get<T>(key)
|
||||
}
|
||||
}
|
||||
|
||||
export const cache = new Cache()
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Cache, httpService } from '@/services'
|
||||
import { cache, httpService } from '@/services'
|
||||
import { albumStore, artistStore, songStore } from '@/stores'
|
||||
|
||||
export const mediaInfoService = {
|
||||
async fetchForArtist (artist: Artist) {
|
||||
const cacheKey = ['artist.info', artist.id]
|
||||
if (Cache.has(cacheKey)) return Cache.get<ArtistInfo>(cacheKey)
|
||||
if (cache.has(cacheKey)) return cache.get<ArtistInfo>(cacheKey)
|
||||
|
||||
const info = await httpService.get<ArtistInfo | null>(`artists/${artist.id}/information`)
|
||||
info && Cache.set(cacheKey, info)
|
||||
info && cache.set(cacheKey, info)
|
||||
|
||||
if (info?.image) {
|
||||
artistStore.byId(artist.id)!.image = info.image
|
||||
|
@ -18,10 +18,10 @@ export const mediaInfoService = {
|
|||
|
||||
async fetchForAlbum (album: Album) {
|
||||
const cacheKey = ['album.info', album.id]
|
||||
if (Cache.has(cacheKey)) return Cache.get<AlbumInfo>(cacheKey)
|
||||
if (cache.has(cacheKey)) return cache.get<AlbumInfo>(cacheKey)
|
||||
|
||||
const info = await httpService.get<AlbumInfo | null>(`albums/${album.id}/information`)
|
||||
info && Cache.set(cacheKey, info)
|
||||
info && cache.set(cacheKey, info)
|
||||
|
||||
if (info?.cover) {
|
||||
albumStore.byId(album.id)!.cover = info.cover
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Cache, httpService } from '@/services'
|
||||
import { cache, httpService } from '@/services'
|
||||
import { eventBus } from '@/utils'
|
||||
import router from '@/router'
|
||||
|
||||
|
@ -9,7 +9,7 @@ interface YouTubeSearchResult {
|
|||
|
||||
export const youTubeService = {
|
||||
searchVideosBySong: async (song: Song, nextPageToken: string) => {
|
||||
return await Cache.resolve<YouTubeSearchResult>(
|
||||
return await cache.remember<YouTubeSearchResult>(
|
||||
['youtube.search', song.id, nextPageToken],
|
||||
async () => await httpService.get<YouTubeSearchResult>(
|
||||
`youtube/search/song/${song.id}?pageToken=${nextPageToken}`
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { reactive, UnwrapNestedRefs } from 'vue'
|
||||
import { differenceBy, merge, orderBy, take, unionBy } from 'lodash'
|
||||
import { Cache, httpService } from '@/services'
|
||||
import { cache, httpService } from '@/services'
|
||||
import { arrayify, logger } from '@/utils'
|
||||
import { songStore } from '@/stores'
|
||||
|
||||
|
@ -66,7 +66,7 @@ export const albumStore = {
|
|||
if (!album) {
|
||||
try {
|
||||
album = this.syncWithVault(
|
||||
await Cache.resolve<Album>(['album', id], async () => await httpService.get<Album>(`albums/${id}`))
|
||||
await cache.remember<Album>(['album', id], async () => await httpService.get<Album>(`albums/${id}`))
|
||||
)[0]
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { reactive, UnwrapNestedRefs } from 'vue'
|
||||
import { differenceBy, orderBy, take, unionBy } from 'lodash'
|
||||
import { Cache, httpService } from '@/services'
|
||||
import { cache, httpService } from '@/services'
|
||||
import { arrayify, logger } from '@/utils'
|
||||
|
||||
const UNKNOWN_ARTIST_ID = 1
|
||||
|
@ -59,7 +59,7 @@ export const artistStore = {
|
|||
if (!artist) {
|
||||
try {
|
||||
artist = this.syncWithVault(
|
||||
await Cache.resolve<Artist>(['artist', id], async () => await httpService.get<Artist>(`artists/${id}`))
|
||||
await cache.remember<Artist>(['artist', id], async () => await httpService.get<Artist>(`artists/${id}`))
|
||||
)[0]
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { differenceBy, orderBy } from 'lodash'
|
||||
import { reactive } from 'vue'
|
||||
import { logger } from '@/utils'
|
||||
import { Cache, httpService } from '@/services'
|
||||
import { cache, httpService } from '@/services'
|
||||
import models from '@/config/smart-playlist/models'
|
||||
import operators from '@/config/smart-playlist/operators'
|
||||
|
||||
|
@ -61,7 +61,7 @@ export const playlistStore = {
|
|||
}
|
||||
|
||||
await httpService.post(`playlists/${playlist.id}/songs`, { songs: songs.map(song => song.id) })
|
||||
Cache.invalidate(['playlist.songs', playlist.id])
|
||||
cache.remove(['playlist.songs', playlist.id])
|
||||
|
||||
return playlist
|
||||
},
|
||||
|
@ -72,7 +72,7 @@ export const playlistStore = {
|
|||
}
|
||||
|
||||
await httpService.delete(`playlists/${playlist.id}/songs`, { songs: songs.map(song => song.id) })
|
||||
Cache.invalidate(['playlist.songs', playlist.id])
|
||||
cache.remove(['playlist.songs', playlist.id])
|
||||
|
||||
return playlist
|
||||
},
|
||||
|
@ -83,7 +83,7 @@ export const playlistStore = {
|
|||
rules: this.serializeSmartPlaylistRulesForStorage(data.rules || [])
|
||||
})
|
||||
|
||||
playlist.is_smart && Cache.invalidate(['playlist.songs', playlist.id])
|
||||
playlist.is_smart && cache.remove(['playlist.songs', playlist.id])
|
||||
Object.assign(playlist, data)
|
||||
|
||||
return playlist
|
||||
|
|
|
@ -3,7 +3,7 @@ import slugify from 'slugify'
|
|||
import { merge, orderBy, sumBy, take, unionBy } from 'lodash'
|
||||
import { reactive, UnwrapNestedRefs, watch } from 'vue'
|
||||
import { arrayify, eventBus, logger, secondsToHis, use } from '@/utils'
|
||||
import { authService, Cache, httpService } from '@/services'
|
||||
import { authService, cache, httpService } from '@/services'
|
||||
import { albumStore, artistStore, commonStore, overviewStore, preferenceStore } from '@/stores'
|
||||
|
||||
export type SongUpdateData = {
|
||||
|
@ -155,21 +155,21 @@ export const songStore = {
|
|||
},
|
||||
|
||||
async fetchForAlbum (album: Album) {
|
||||
return await Cache.resolve<Song[]>(
|
||||
return await cache.remember<Song[]>(
|
||||
[`album.songs`, album.id],
|
||||
async () => this.syncWithVault(await httpService.get<Song[]>(`albums/${album.id}/songs`))
|
||||
)
|
||||
},
|
||||
|
||||
async fetchForArtist (artist: Artist) {
|
||||
return await Cache.resolve<Song[]>(
|
||||
return await cache.remember<Song[]>(
|
||||
['artist.songs', artist.id],
|
||||
async () => this.syncWithVault(await httpService.get<Song[]>(`artists/${artist.id}/songs`))
|
||||
)
|
||||
},
|
||||
|
||||
async fetchForPlaylist (playlist: Playlist) {
|
||||
return await Cache.resolve<Song[]>(
|
||||
return await cache.remember<Song[]>(
|
||||
[`playlist.songs`, playlist.id],
|
||||
async () => this.syncWithVault(await httpService.get<Song[]>(`playlists/${playlist.id}/songs`))
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue