├── .editorconfig ├── .gitignore ├── .travis.yml ├── Readme.md ├── index.js ├── lib ├── memclient.js ├── wrapAwait.js └── wrapCB.js ├── package.json └── test ├── abstract-cache.test.js ├── memclient.test.js ├── mocks.js ├── wrapAwait.test.js └── wrapCB.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | 12 | # [*.md] 13 | # trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # 0x 36 | .__browserify_string_empty.js 37 | profile-* 38 | 39 | # tap --cov 40 | .nyc_output/ 41 | 42 | # JetBrains IntelliJ IDEA 43 | .idea/ 44 | *.iml 45 | 46 | # VS Code 47 | .vscode/ 48 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "9" 5 | - "8" 6 | 7 | script: 8 | - npm run lint-ci 9 | - npm run test-ci 10 | 11 | notifications: 12 | email: 13 | on_success: never 14 | on_failure: always 15 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # abstract-cache 2 | 3 | *abstract-cache* is a module that provides a common interface to multiple 4 | caching strategies. It allows for requiring *abstract-cache* everywhere while 5 | defining the strategy via a simple configuration. 6 | 7 | *abstract-cache* is heavily inspired by the excellent [Catbox][catbox]. The 8 | decision to create *abstract-cache* was predicated on a desire to require 9 | implementing clients accept previously established connections in addtion to 10 | accepting configuration to create their own connections. It also seeks to 11 | allow using either the callback or async/await style of asynchronous 12 | programming. 13 | 14 | [catbox]: https://npm.im/catbox 15 | 16 | ### Available Clients 17 | 18 | + [abstract-cache-mongo](https://github.com/jsumners/abstract-cache-mongo) 19 | + [abstract-cache-redis](https://github.com/jsumners/abstract-cache-redis) 20 | 21 | ## Example 22 | 23 | The following example uses *abstract-cache*'s included in-memory cache. Of note, 24 | is that the included in-memory cache is actually callback based. 25 | 26 | ```js 27 | const cache = require('abstract-cache')({useAwait: true}) 28 | 29 | async function doSomething () { 30 | const val = await cache.get('foo') 31 | console.log(val) 32 | } 33 | 34 | cache.set('foo', 'foo', 100) 35 | .then(doSomething) 36 | .catch(console.error) 37 | ``` 38 | 39 | This example shows instantiating *abstract-cache* with a specific store that 40 | relies upon a connection to a remote system. In this example we are supplying 41 | an already existing connection to the remote system; all *abstract-cache* 42 | compliant clients **must** support this. 43 | 44 | ```js 45 | const cache = require('abstract-cache')({ 46 | useAwait: true, 47 | driver: { 48 | name: 'abstract-cache-redis', 49 | options: { 50 | client: require('redis')({url: 'some.redis.url'}) 51 | } 52 | } 53 | }) 54 | 55 | async function doSomething () { 56 | const val = await cache.get('foo') 57 | console.log(val) 58 | } 59 | 60 | cache.set('foo', 'foo', 100) 61 | .then(doSomething) 62 | .catch(console.error) 63 | ``` 64 | 65 | ## Options 66 | 67 | *abstract-client* accepts an options object with the following properties: 68 | 69 | + `useAwait` (Default: `false`): designate that the resulting cache client 70 | should use `async/await` functions. When `false`, every method accepts a 71 | standard `callback(err, result)`. 72 | + `client` (Default: `undefined`): an already instantiated strategy client. 73 | In combination with `useAwait` the client can be wrapped accordingly. Specifying 74 | a `client` superceeds the `driver` configuration. 75 | + `driver`: 76 | * `name` (Default: `undefined`): specifies the implementing strategy to 77 | load. The default value results in the buil-in in-memory strategy being 78 | loaded -- this is **not recommended** for production environments. 79 | * `options` (Default: `{}`): an options object to pass to the strategy 80 | while loading. The strategy should describe this object. 81 | 82 | ### memclient 83 | 84 | The included in-memory client is available as: 85 | 86 | ```js 87 | const memclientFactory = require('abstract-cache').memclient 88 | ``` 89 | 90 | It accepts an options object: 91 | 92 | + `segment` (Default: `abstractMemcache`): the default segment in which to store 93 | items. 94 | + `maxItems` (Default: `100000`): the maximum number of items to keep in the 95 | cache. The backing is an LRU cache with an upper bound. 96 | 97 | ## Protocol 98 | 99 | All implementing strategies **must** implement the protocol described in this 100 | section. 101 | 102 | 1. The module should export a factory `function (optionsObject) {}`. 103 | 1. Accept an existing connection to data stores via the `optionsObject`. 104 | 1. Manage connections created by itself. 105 | 1. In all cases where a `key` is required, the `key` may be a simple string, 106 | or it may be an object of the format `{id: 'name', segment: 'name'}`. It is 107 | up to the implementing strategy to decide how to handle these keys. 108 | 1. The factory function should return an object (client) that has the following 109 | methods and properties: 110 | * `await` (boolean property): `true` indicates that the strategy's methods 111 | are *all* `async` functions. If `false`, all methods **must** have a 112 | `callback(err, result)` as the last parameter. 113 | * `delete(key[, callback])`: removes the specified item from the cache. 114 | * `get(key[, callback])`: retrieves the desired item from the cache. The 115 | returned item should be a deep copy of the stored value to prevent alterations 116 | from affecting the cache. The result should be an object with the properties: 117 | + `item`: the item the user cached. 118 | + `stored`: a `Date`, in Epoch milliseconds, indicating when the item 119 | was stored. 120 | + `ttl`: the *remaining* lifetime of the item in the cache (milliseconds). 121 | * `has(key[, callback])`: returns a boolean result indicating if the cache 122 | contains the desired `key`. 123 | * `set(key, value, ttl[, callback])`: stores the specified `value` in the 124 | cache under the specified `key` for the time `ttl` in milliseconds. 125 | * `start([callback])` (optional): clients that require extra initialization, 126 | e.g. to start a database connection, may export this method. When present, 127 | this method **must** be invoked by the user before any other method. This 128 | method may be an `async` function at the discretion of the implementor. 129 | * `stop([callback])` (optional): required when `start()` is present. This 130 | should shutdown any connections/processes started via `start()`. It is 131 | left to the user to invoke this method in their shutdown procedure. This 132 | method may be an `async` function at the discretion of the implementor. 133 | 134 | ## License 135 | 136 | [MIT License](http://jsumners.mit-license.org/) 137 | 138 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const merge = require('merge-options') 4 | const defaultOptions = { 5 | useAwait: false, 6 | client: undefined, 7 | driver: { 8 | name: undefined, 9 | options: {} 10 | } 11 | } 12 | 13 | module.exports = function abstractCache (options) { 14 | const opts = merge({}, defaultOptions, options) 15 | 16 | let client 17 | if (opts.client) { 18 | client = opts.client 19 | } else if (!opts.driver.name) { 20 | client = require('./lib/memclient')(opts.driver.options) 21 | } else { 22 | client = require(opts.driver.name)(opts.driver.options) 23 | } 24 | 25 | if (opts.useAwait === true && client.await === true) { 26 | return client 27 | } else if (opts.useAwait === true && !client.await) { 28 | return require('./lib/wrapCB')(client) 29 | } else if (opts.useAwait === false && client.await === true) { 30 | return require('./lib/wrapAwait')(client) 31 | } 32 | 33 | // User wants callback style and client is callback style. 34 | return client 35 | } 36 | 37 | module.exports.memclient = require('./lib/memclient') 38 | -------------------------------------------------------------------------------- /lib/memclient.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const LMap = require('lru_map').LRUMap 4 | const clone = require('clone') 5 | 6 | function mapKey (key, segment) { 7 | if (typeof key === 'string') return `${segment}:${key}` 8 | return `${key.segment || segment}:${key.id}` 9 | } 10 | 11 | const cacheProto = { 12 | delete: function (key, callback) { 13 | this._cache.delete(mapKey(key, this._segment)) 14 | callback(null) 15 | }, 16 | 17 | get: function (key, callback) { 18 | const _key = mapKey(key, this._segment) 19 | const obj = this._cache.get(_key) 20 | if (!obj) return callback(null, null) 21 | const now = Date.now() 22 | const expires = obj.ttl + obj.stored 23 | const ttl = expires - now 24 | if (ttl < 0) { 25 | this._cache.delete(_key) 26 | return callback(null, null) 27 | } 28 | callback(null, { 29 | item: clone(obj.item), 30 | stored: obj.stored, 31 | ttl 32 | }) 33 | }, 34 | 35 | has: function (key, callback) { 36 | callback(null, this._cache.has(mapKey(key, this._segment))) 37 | }, 38 | 39 | set: function (key, value, ttl, callback) { 40 | this._cache.set(mapKey(key, this._segment), { 41 | ttl: ttl, 42 | item: value, 43 | stored: Date.now() 44 | }) 45 | callback(null) 46 | } 47 | } 48 | 49 | module.exports = function (config) { 50 | const _config = config || {} 51 | const _segment = _config.segment || 'abstractMemcache' 52 | const _maxItems = (_config.maxItems && Number.isInteger(_config.maxItems)) 53 | ? _config.maxItems 54 | : 100000 55 | const map = new LMap(_maxItems) 56 | const cache = Object.create(cacheProto) 57 | 58 | Object.defineProperties(cache, { 59 | await: { 60 | value: false 61 | }, 62 | _cache: { 63 | enumerable: false, 64 | value: map 65 | }, 66 | _segment: { 67 | enumerable: false, 68 | value: _segment 69 | } 70 | }) 71 | return cache 72 | } 73 | -------------------------------------------------------------------------------- /lib/wrapAwait.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const proto = { 4 | delete: function (key, callback) { 5 | this.client.delete(key) 6 | .then(() => callback(null)) 7 | .catch(callback) 8 | }, 9 | 10 | get: function (key, callback) { 11 | this.client.get(key) 12 | .then((result) => callback(null, result)) 13 | .catch(callback) 14 | }, 15 | 16 | has: function (key, callback) { 17 | this.client.has(key) 18 | .then((result) => callback(null, result)) 19 | .catch(callback) 20 | }, 21 | 22 | set: function (key, value, ttl, callback) { 23 | this.client.set(key, value, ttl) 24 | .then((result) => callback(null, result)) 25 | .catch(callback) 26 | } 27 | } 28 | 29 | function start (callback) { 30 | this.client.start() 31 | .then(() => callback(null)) 32 | .catch((err) => callback(err)) 33 | } 34 | 35 | function stop (callback) { 36 | this.client.stop() 37 | .then(() => callback(null)) 38 | .catch((err) => callback(err)) 39 | } 40 | 41 | module.exports = function (client) { 42 | const instance = Object.create(proto) 43 | Object.defineProperty(instance, 'client', { 44 | enumerable: false, 45 | value: client 46 | }) 47 | if (client.start) { 48 | instance.start = start.bind(instance) 49 | instance.stop = stop.bind(instance) 50 | } 51 | return instance 52 | } 53 | -------------------------------------------------------------------------------- /lib/wrapCB.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const proto = { 4 | delete: function (key) { 5 | return new Promise((resolve, reject) => { 6 | this.client.delete(key, (err) => { 7 | if (err) return reject(err) 8 | resolve() 9 | }) 10 | }) 11 | }, 12 | 13 | get: function (key) { 14 | return new Promise((resolve, reject) => { 15 | this.client.get(key, (err, result) => { 16 | if (err) return reject(err) 17 | resolve(result) 18 | }) 19 | }) 20 | }, 21 | 22 | has: function (key) { 23 | return new Promise((resolve, reject) => { 24 | this.client.has(key, (err, result) => { 25 | if (err) return reject(err) 26 | resolve(result) 27 | }) 28 | }) 29 | }, 30 | 31 | set: function (key, value, ttl) { 32 | return new Promise((resolve, reject) => { 33 | this.client.set(key, value, ttl, (err) => { 34 | if (err) return reject(err) 35 | resolve() 36 | }) 37 | }) 38 | } 39 | } 40 | 41 | function start () { 42 | return new Promise((resolve, reject) => { 43 | this.client.start((err) => { 44 | if (err) return reject(err) 45 | resolve() 46 | }) 47 | }) 48 | } 49 | 50 | function stop () { 51 | return new Promise((resolve, reject) => { 52 | this.client.stop((err) => { 53 | if (err) return reject(err) 54 | resolve() 55 | }) 56 | }) 57 | } 58 | 59 | module.exports = function abstractCacheWrapCB (client) { 60 | const instance = Object.create(proto) 61 | Object.defineProperty(instance, 'client', { 62 | enumerable: false, 63 | value: client 64 | }) 65 | if (client.start) { 66 | instance.start = start.bind(instance) 67 | instance.stop = stop.bind(instance) 68 | } 69 | return instance 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abstract-cache", 3 | "version": "1.0.1", 4 | "description": "An abstract object cache interface", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tap 'test/**/*.test.js'", 8 | "test-ci": "tap --cov 'test/**/*.test.js'", 9 | "lint": "standard | snazzy", 10 | "lint-ci": "standard" 11 | }, 12 | "precommit": [ 13 | "lint", 14 | "test" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+ssh://git@github.com/jsumners/abstract-cache.git" 19 | }, 20 | "keywords": [ 21 | "cache", 22 | "generic", 23 | "adapter" 24 | ], 25 | "author": "James Sumners ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/jsumners/abstract-cache/issues" 29 | }, 30 | "homepage": "https://github.com/jsumners/abstract-cache#readme", 31 | "devDependencies": { 32 | "mock-require": "^3.0.1", 33 | "pre-commit": "^1.2.2", 34 | "snazzy": "^7.0.0", 35 | "standard": "^11.0.0", 36 | "tap": "^11.1.1" 37 | }, 38 | "dependencies": { 39 | "clone": "^2.1.1", 40 | "lru_map": "^0.3.3", 41 | "merge-options": "^1.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/abstract-cache.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const mockquire = require('mock-require') 5 | const mocks = require('./mocks') 6 | const factory = require('../') 7 | 8 | test('uses a previously established client', (t) => { 9 | t.plan(1) 10 | const client1 = factory.memclient() 11 | const client2 = factory({client: client1}) 12 | t.is(client2, client1) 13 | }) 14 | 15 | test('existing client overrides driver config', (t) => { 16 | t.plan(1) 17 | const client1 = factory.memclient() 18 | const client2 = factory({ 19 | client: client1, 20 | driver: { 21 | name: 'foo' 22 | } 23 | }) 24 | t.is(client2, client1) 25 | }) 26 | 27 | test('creates an in-memory client if no configuration provided', (t) => { 28 | t.plan(2) 29 | const client = factory() 30 | t.is(client.await, false) 31 | client.set('foo', 'foo', 1000, (err) => { 32 | if (err) t.threw(err) 33 | client.has('foo', (err, result) => { 34 | if (err) t.threw(err) 35 | t.is(result, true) 36 | }) 37 | }) 38 | }) 39 | 40 | test('creates a client by specifying a driver', (t) => { 41 | t.plan(1) 42 | mockquire('abstract-cache-foo', factory.memclient) 43 | t.tearDown(() => mockquire.stopAll()) 44 | const client = factory({driver: {name: 'abstract-cache-foo'}}) 45 | t.is(client.await, false) 46 | }) 47 | 48 | test('returns an await client when await style is desired', (t) => { 49 | t.plan(2) 50 | const client = factory({ 51 | useAwait: true, 52 | client: mocks.awaitClient() 53 | }) 54 | const future = client.set('foo', 'foo', 1000) 55 | t.ok(future.then) 56 | t.type(future.then, Function) 57 | }) 58 | 59 | test('wraps a callback client for await style', (t) => { 60 | t.plan(2) 61 | const client = factory({ 62 | useAwait: true, 63 | client: mocks.cbClient() 64 | }) 65 | const future = client.set('foo', 'foo', 1000) 66 | t.ok(future.then) 67 | t.type(future.then, Function) 68 | }) 69 | 70 | test('wraps an await client for callback style', (t) => { 71 | t.plan(2) 72 | const client = factory({ 73 | client: mocks.awaitClient() 74 | }) 75 | const future = client.set('foo', 'foo', 1000, (err) => { 76 | t.is(err, null) 77 | }) 78 | t.is(future, undefined) 79 | }) 80 | 81 | test('returns a callback client when callback style is desired', (t) => { 82 | t.plan(2) 83 | const client = factory({ 84 | useAwait: false, 85 | client: mocks.cbClient() 86 | }) 87 | const future = client.set('foo', 'foo', 1000, (err) => { 88 | t.is(err, null) 89 | }) 90 | t.is(future, undefined) 91 | }) 92 | 93 | test('supports clients with start/stop', (t) => { 94 | t.plan(2) 95 | const client = factory({ 96 | useAwait: true, 97 | client: mocks.startAsyncClient() 98 | }) 99 | client.start() 100 | .then((started) => t.is(started, true)) 101 | .then(() => client.stop()) 102 | .then((stopped) => t.is(stopped, true)) 103 | .catch(t.threw) 104 | }) 105 | 106 | test('exposes start and stop via driver', (t) => { 107 | t.plan(6) 108 | mockquire('abstract-cache-foo', () => { 109 | return { 110 | await: true, 111 | start () { 112 | t.pass() 113 | return Promise.resolve() 114 | }, 115 | stop () { 116 | t.pass() 117 | return Promise.resolve() 118 | } 119 | } 120 | }) 121 | t.tearDown(() => mockquire.stopAll()) 122 | const client = factory({useAwait: true, driver: {name: 'abstract-cache-foo'}}) 123 | t.type(client.start, Function) 124 | t.type(client.stop, Function) 125 | client.start() 126 | .then(() => t.pass()) 127 | .then(() => client.stop()) 128 | .then(() => t.pass()) 129 | .catch(t.threw) 130 | }) 131 | -------------------------------------------------------------------------------- /test/memclient.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const memclientFactory = require('../').memclient 5 | 6 | test('instances indicate asynchronous nature', (t) => { 7 | t.plan(1) 8 | const cache = memclientFactory() 9 | t.is(cache.await, false) 10 | }) 11 | 12 | test('cache stores items', (t) => { 13 | t.plan(6) 14 | const cache = memclientFactory() 15 | cache.set('foo', 'foo', 100, (err) => { 16 | t.error(err) 17 | 18 | setTimeout(() => { 19 | cache.get('foo', (err, cached) => { 20 | t.error(err) 21 | t.ok(cached.item) 22 | t.ok(cached.ttl) 23 | t.ok(cached.stored) 24 | t.ok(cached.ttl < 100) 25 | }) 26 | }, 10) 27 | }) 28 | }) 29 | 30 | test('cache returns deeply cloned items', (t) => { 31 | t.plan(4) 32 | const cache = memclientFactory() 33 | const original = {foo: 'foo'} 34 | cache.set('foo', original, 1000, (err) => { 35 | t.error(err) 36 | 37 | cache.get('foo', (err, cached) => { 38 | t.error(err) 39 | t.strictDeepEqual(cached.item, original) 40 | cached.item.bar = 'bar' 41 | t.strictDeepInequal(cached.item, original) 42 | }) 43 | }) 44 | }) 45 | 46 | test('cache deletes items', (t) => { 47 | t.plan(4) 48 | const cache = memclientFactory() 49 | cache.set('foo', 'foo', 100, (err) => { 50 | t.error(err) 51 | cache.delete('foo', (err) => { 52 | t.error(err) 53 | cache.get('foo', (err, cached) => { 54 | t.error(err) 55 | t.is(cached, null) 56 | }) 57 | }) 58 | }) 59 | }) 60 | 61 | test('keys can be objects', (t) => { 62 | t.plan(6) 63 | const cache = memclientFactory() 64 | cache.set({id: 'foo', segment: 'bar'}, 'foobar', 200, (err) => { 65 | if (err) t.threw(err) 66 | cache.get({id: 'foo', segment: 'bar'}, (err, cached) => { 67 | if (err) t.threw(err) 68 | t.type(cached, Object) 69 | t.ok(cached.item) 70 | t.ok(cached.ttl) 71 | t.ok(cached.stored) 72 | t.ok(cached.ttl) 73 | t.is(cached.item, 'foobar') 74 | }) 75 | }) 76 | }) 77 | 78 | test('supports configuring default segment', (t) => { 79 | t.plan(6) 80 | const cache = memclientFactory({segment: 'fooseg'}) 81 | cache.set('foo', 'foobar', 200, (err) => { 82 | if (err) t.threw(err) 83 | cache.get({id: 'foo', segment: 'fooseg'}, (err, cached) => { 84 | if (err) t.threw(err) 85 | t.type(cached, Object) 86 | t.ok(cached.item) 87 | t.ok(cached.ttl) 88 | t.ok(cached.stored) 89 | t.ok(cached.ttl) 90 | t.is(cached.item, 'foobar') 91 | }) 92 | }) 93 | }) 94 | 95 | test('supports configuring maximum items', (t) => { 96 | t.plan(5) 97 | const cache = memclientFactory({maxItems: 1}) 98 | const errHandler = (err) => { 99 | if (err) t.threw(err) 100 | } 101 | Promise 102 | .all([ 103 | cache.set('foo', 'foo', 1000, errHandler), 104 | cache.set('bar', 'bar', 1000, errHandler) 105 | ]) 106 | .then(() => { 107 | cache.get('foo', (err, cached) => { 108 | if (err) t.threw(err) 109 | t.is(cached, null) 110 | }) 111 | cache.get('bar', (err, cached) => { 112 | if (err) t.threw(err) 113 | t.ok(cached) 114 | t.type(cached, Object) 115 | t.ok(cached.item) 116 | t.is(cached.item, 'bar') 117 | }) 118 | }) 119 | .catch(t.threw) 120 | }) 121 | 122 | test('supports configuring maximum items and default segment', (t) => { 123 | t.plan(5) 124 | const cache = memclientFactory({maxItems: 1, segment: 'fooseg'}) 125 | const errHandler = (err) => { 126 | if (err) t.threw(err) 127 | } 128 | Promise 129 | .all([ 130 | cache.set('foo', 'foo', 1000, errHandler), 131 | cache.set('bar', 'bar', 1000, errHandler) 132 | ]) 133 | .then(() => { 134 | cache.get('foo', (err, cached) => { 135 | if (err) t.threw(err) 136 | t.is(cached, null) 137 | }) 138 | cache.get({id: 'bar', segment: 'fooseg'}, (err, cached) => { 139 | if (err) t.threw(err) 140 | t.ok(cached) 141 | t.type(cached, Object) 142 | t.ok(cached.item) 143 | t.is(cached.item, 'bar') 144 | }) 145 | }) 146 | .catch(t.threw) 147 | }) 148 | -------------------------------------------------------------------------------- /test/mocks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports.awaitClient = function () { 4 | return { 5 | await: true, 6 | delete: function () { 7 | return Promise.resolve() 8 | }, 9 | get: function () { 10 | return Promise.resolve('foo') 11 | }, 12 | has: function () { 13 | return Promise.resolve(true) 14 | }, 15 | set: function () { 16 | return Promise.resolve() 17 | } 18 | } 19 | } 20 | 21 | module.exports.cbClient = function () { 22 | return { 23 | await: false, 24 | delete: function (key, cb) { 25 | cb(null) 26 | }, 27 | get: function (key, cb) { 28 | cb(null, 'foo') 29 | }, 30 | has: function (key, cb) { 31 | cb(null, true) 32 | }, 33 | set: function (key, value, ttl, cb) { 34 | cb(null) 35 | } 36 | } 37 | } 38 | 39 | module.exports.startAsyncClient = function () { 40 | const client = module.exports.awaitClient() 41 | client.start = function () { 42 | return Promise.resolve(true) 43 | } 44 | client.stop = async function () { 45 | return Promise.resolve(true) 46 | } 47 | return client 48 | } 49 | -------------------------------------------------------------------------------- /test/wrapAwait.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const memclientFactory = require('../lib/memclient') 5 | const wrapCBFactory = require('../lib/wrapCB') 6 | const wrapAwaitFactory = require('../lib/wrapAwait') 7 | 8 | function inceptify () { 9 | return wrapAwaitFactory( 10 | wrapCBFactory(memclientFactory()) 11 | ) 12 | } 13 | 14 | test('wraps set', (t) => { 15 | t.plan(1) 16 | const client = inceptify() 17 | client.set('foo', 'foo', 1000, (err) => { 18 | if (err) t.threw(err) 19 | t.pass() 20 | }) 21 | }) 22 | 23 | test('wraps has', (t) => { 24 | t.plan(1) 25 | const client = inceptify() 26 | client.set('foo', 'foo', 1000, (err) => { 27 | if (err) t.threw(err) 28 | client.has('foo', (err, result) => { 29 | if (err) t.threw(err) 30 | t.is(result, true) 31 | }) 32 | }) 33 | }) 34 | 35 | test('wraps get', (t) => { 36 | t.plan(3) 37 | const client = inceptify() 38 | client.set('foo', 'foo', 1000, (err) => { 39 | if (err) t.threw(err) 40 | client.get('foo', (err, cached) => { 41 | if (err) t.threw(err) 42 | t.type(cached, Object) 43 | t.ok(cached.item) 44 | t.is(cached.item, 'foo') 45 | }) 46 | }) 47 | }) 48 | 49 | test('wraps delete', (t) => { 50 | t.plan(1) 51 | const client = inceptify() 52 | client.set('foo', 'foo', 1000, (err) => { 53 | if (err) t.threw(err) 54 | client.delete('foo', (err) => { 55 | if (err) t.threw(err) 56 | client.has('foo', (err, result) => { 57 | if (err) t.threw(err) 58 | t.is(result, false) 59 | }) 60 | }) 61 | }) 62 | }) 63 | 64 | test('has start and stop', (t) => { 65 | t.plan(4) 66 | const client = { 67 | start () { 68 | t.pass() 69 | return Promise.resolve() 70 | }, 71 | stop () { 72 | t.pass() 73 | return Promise.resolve() 74 | } 75 | } 76 | 77 | const wrapped = wrapAwaitFactory(client) 78 | wrapped.start((err) => t.is(err, null)) 79 | wrapped.stop((err) => t.is(err, null)) 80 | }) 81 | -------------------------------------------------------------------------------- /test/wrapCB.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const memclientFactory = require('../lib/memclient') 5 | const wrapCBFactory = require('../lib/wrapCB') 6 | 7 | test('wraps set', (t) => { 8 | t.plan(1) 9 | const client = wrapCBFactory(memclientFactory()) 10 | client.set('foo', 'foo', 1000) 11 | .then(() => { 12 | t.pass() 13 | }) 14 | .catch(t.threw) 15 | }) 16 | 17 | test('wraps has', (t) => { 18 | t.plan(1) 19 | const client = wrapCBFactory(memclientFactory()) 20 | client.set('foo', 'foo', 1000) 21 | .then(() => client.has('foo')) 22 | .then((result) => t.is(result, true)) 23 | .catch(t.threw) 24 | }) 25 | 26 | test('wraps get', (t) => { 27 | t.plan(3) 28 | const client = wrapCBFactory(memclientFactory()) 29 | client.set('foo', 'foo', 1000) 30 | .then(() => client.get('foo')) 31 | .then((cached) => { 32 | t.type(cached, Object) 33 | t.ok(cached.item) 34 | t.is(cached.item, 'foo') 35 | }) 36 | .catch(t.threw) 37 | }) 38 | 39 | test('wraps delete', (t) => { 40 | t.plan(1) 41 | const client = wrapCBFactory(memclientFactory()) 42 | client.set('foo', 'foo', 1000) 43 | .then(() => client.delete('foo')) 44 | .then(() => client.has('foo')) 45 | .then((result) => t.is(result, false)) 46 | .catch(t.threw) 47 | }) 48 | 49 | test('has start and stop', (t) => { 50 | t.plan(4) 51 | const client = { 52 | start (cb) { 53 | t.type(cb, 'function') 54 | cb(null) 55 | }, 56 | stop (cb) { 57 | t.type(cb, 'function') 58 | cb(null) 59 | } 60 | } 61 | const wrapped = wrapCBFactory(client) 62 | wrapped.start().then(t.pass).catch(t.threw) 63 | wrapped.stop().then(t.pass).catch(t.threw) 64 | }) 65 | --------------------------------------------------------------------------------