Skip to content

Commit

Permalink
feat: multicache
Browse files Browse the repository at this point in the history
Signed-off-by: Jytesh <[email protected]>
  • Loading branch information
Jytesh committed Sep 7, 2021
1 parent 2d6debd commit 2defb17
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 0 deletions.
51 changes: 51 additions & 0 deletions packages/multicache/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@keyvhq/multicache",
"description": "Layered cache with any backend",
"homepage": "https://keyv.js.org",
"version": "1.2.7",
"main": "src/index.js",
"author": {
"email": "[email protected]",
"name": "microlink.io",
"url": "https://microlink.io"
},
"repository": {
"directory": "packages/memo",
"type": "git",
"url": "git+https://github.com/microlinkhq/keyv.git"
},
"bugs": {
"url": "https://github.com/microlinkhq/keyv/issues"
},
"keywords": [
"cache",
"key",
"multicache",
"multilevel",
"store",
"ttl",
"value"
],
"dependencies": {
"@keyvhq/core": "~1.2.6",
"@keyvhq/keyv-sqlite": "^1.0.0"
},
"devDependencies": {
"ava": "latest",
"delay": "latest",
"nyc": "latest"
},
"engines": {
"node": ">= 12"
},
"files": [
"src"
],
"scripts": {
"test": "nyc ava"
},
"license": "MIT",
"publishConfig": {
"access": "public"
}
}
57 changes: 57 additions & 0 deletions packages/multicache/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const Keyv = require('@keyvhq/core')

class MultiCache {
constructor (remote = new Keyv(), local = new Keyv(), options) {
const normalizedOptions = Object.assign(
{
validator: () => true
},
options
)
this.remote = remote
this.local = local

Object.keys(normalizedOptions).forEach(
key => (this[key] = normalizedOptions[key])
)
}

async get (...args) {
let res = await this.local.get(...args)
if (!res || !this.validator(res, ...args)) {
res = await this.remote.get(...args)
}
return res
}

async has (...args) {
let res = await this.local.has(...args)
if (!res || !this.validator(res, ...args)) {
res = await this.remote.has(...args)
}
return res
}

async set (key, value, ttl) {
const localRes = this.local.set(key, value, ttl)
const remoteRes = this.remote.set(key, value, ttl)
await Promise.all([localRes, remoteRes])
return true
}

async delete (key, options = { localOnly: false }) {
const localRes = this.local.delete(key)
const remoteRes = !options.localOnly && this.remote.delete(key)
await Promise.all([localRes, remoteRes])
return true
}

async clear (options = { localOnly: false }) {
const localRes = this.local.clear()
const remoteRes = !options.localOnly && this.remote.clear()
await Promise.all([localRes, remoteRes])
return true
}
}

module.exports = MultiCache
147 changes: 147 additions & 0 deletions packages/multicache/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
'use strict'

const test = require('ava')
const delay = require('delay')

const MultiCache = require('..')
const Keyv = require('@keyvhq/core')
const KeyvSqlite = require('@keyvhq/keyv-sqlite')

const remoteStore = () =>
new Keyv({
store: new KeyvSqlite({
uri: 'sqlite://test/testdb.sqlite',
busyTimeout: 30000
})
})
const localStore = () => new Keyv()

test.beforeEach(async () => {
const remote = remoteStore()
const local = localStore()
const store = new MultiCache(remote, local)
return store.clear()
})

test.serial('.set() sets to both stores', async t => {
const remote = remoteStore()
const local = localStore()
const store = new MultiCache(remote, local)

await store.set('foo', 'bar')

const [remoteRes, localRes, storeRes] = await Promise.all([
remote.get('foo'),
store.get('foo'),
local.get('foo')
])
const result = remoteRes === localRes && storeRes === localRes // Check equality as 'bar' is just a string
t.is(result, true)
})

test.serial('.has() returns boolean', async t => {
const remote = remoteStore()
const local = localStore()
const store = new MultiCache(remote, local)

await store.set('foo', 'bar')

t.is(await store.has('foo'), true)
})

test.serial('.has() checks both stores', async t => {
const remote = remoteStore()
const store = new MultiCache(remote)

await remote.set('fizz', 'buzz')

t.is(await store.has('fizz'), true)
})

test.serial('.delete() deletes both stores', async t => {
const remote = remoteStore()
const local = localStore()
const store = new MultiCache(remote, local)

await store.set('fizz', 'buzz')
await store.delete('fizz')

t.is(await store.get('fizz'), undefined)
t.is(await local.get('fizz'), undefined)
t.is(await remote.get('fizz'), undefined)
})

test.serial(
'.delete({ localOnly: true }) deletes only local store',
async t => {
const remote = remoteStore()
const local = localStore()
const store = new MultiCache(remote, local)

await store.set('fizz', 'buzz')
await store.delete('fizz', { localOnly: true })

t.is(await store.get('fizz'), 'buzz')
t.is(await local.get('fizz'), undefined)
t.is(await remote.get('fizz'), 'buzz')
}
)

test.serial('.clear() clears both stores', async t => {
const remote = remoteStore()
const local = localStore()
const store = new MultiCache(remote, local)

await store.set('fizz', 'buzz')
await store.clear()

t.is(await store.get('fizz'), undefined)
})

test.serial('.clear({ localOnly: true }) clears local store alone', async t => {
const remote = remoteStore()
const local = localStore()
const store = new MultiCache(remote, local)

await store.set('fizz', 'buzz')
await store.clear({ localOnly: true })

t.is(await store.get('fizz'), 'buzz')
t.is(await local.get('fizz'), undefined)
t.is(await remote.get('fizz'), 'buzz')
})

test.serial('ttl is valid', async t => {
const remote = remoteStore()
const local = new Keyv({ ttl: 100 }) // set local ttl
const store = new MultiCache(remote, local)

await store.set('foo', 'bar')
await remote.set('foo', 'notbar')

await delay(100)
t.is(await store.get('foo'), 'notbar')
})

test.serial('custom validator', async t => {
const remote = remoteStore()
const local = new Keyv()
const store = new MultiCache(remote, local, {
validator: val => {
if (val.timeSensitiveData) return false // fetch from remote store only
return true
}
})

await store.set('1', { timeSensitiveData: 'bar' })
await store.set('2', { timeSensitiveData: false })

t.deepEqual(await store.get('1'), { timeSensitiveData: 'bar' }) // fetched from remote
t.deepEqual(await store.get('2'), { timeSensitiveData: false })

await remote.set('1', { timeSensitiveData: 'foo1' })
await remote.set('2', { timeSensitiveData: 'foo2' }) // set to remote so local has not been updated

t.deepEqual(await store.get('1'), { timeSensitiveData: 'foo1' })
t.deepEqual(await store.get('2'), { timeSensitiveData: false })
})

0 comments on commit 2defb17

Please sign in to comment.