diff --git a/packages/multicache/package.json b/packages/multicache/package.json new file mode 100644 index 00000000..173b90ca --- /dev/null +++ b/packages/multicache/package.json @@ -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": "hello@microlink.io", + "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" + } +} diff --git a/packages/multicache/src/index.js b/packages/multicache/src/index.js new file mode 100644 index 00000000..9888c1b2 --- /dev/null +++ b/packages/multicache/src/index.js @@ -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 diff --git a/packages/multicache/test/index.js b/packages/multicache/test/index.js new file mode 100644 index 00000000..4433ac36 --- /dev/null +++ b/packages/multicache/test/index.js @@ -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 }) +})