├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── commands ├── ConfigGenerator.js ├── TableGenerator.js └── templates │ ├── config.mustache │ └── table.mustache ├── credits.md ├── package-lock.json ├── package.json ├── providers ├── CacheProvider.js └── CommandsProvider.js ├── src ├── Events │ ├── CacheHit.js │ ├── CacheMissed.js │ ├── KeyForgotten.js │ └── KeyWritten.js ├── Stores │ ├── CacheManager.js │ ├── DatabaseStore.js │ ├── NullStore.js │ ├── ObjectStore.js │ ├── RedisStore.js │ ├── RedisTaggedCache.js │ ├── Repository.js │ ├── TagSet.js │ ├── TaggableStore.js │ └── TaggedCache.js └── Util │ └── index.js └── test ├── DatabaseStore.spec.js ├── NullStore.spec.js ├── ObjectStore.spec.js ├── RedisStore.spec.js ├── RedisTaggedCache.spec.js ├── Repository.spec.js └── TestHelpers.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | test/mydb.sqlite -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Hany El Nokaly < hany.elnokaly@gmail.com > 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AdonisCache 2 | 3 | This is a cache service provider for AdonisJS framework 4 | 5 | - [Installation](#installation) 6 | - [Configuration](#configuration) 7 | - [Driver Prerequisites](#driver-prerequisites) 8 | - [Cache Usage](#cache-usage) 9 | - [Obtaining A Cache Instance](#obtaining-a-cache-instance) 10 | - [Retrieving Items From The Cache](#retrieving-items-from-the-cache) 11 | - [Storing Items In The Cache](#storing-items-in-the-cache) 12 | - [Removing Items From The Cache](#removing-items-from-the-cache) 13 | - [Cache Tags](#cache-tags) 14 | - [Storing Tagged Cache Items](#storing-tagged-cache-items) 15 | - [Accessing Tagged Cache Items](#accessing-tagged-cache-items) 16 | - [Removing Tagged Cache Items](#removing-tagged-cache-items) 17 | - [Events](#events) 18 | 19 | 20 | ## Installation 21 | 22 | ```js 23 | npm i adonis-cache --save 24 | ``` 25 | 26 | After installation, you need to register the providers inside `start/app.js` file. 27 | 28 | ##### start/app.js 29 | ```javascript 30 | const providers = [ 31 | ..., 32 | 'adonis-cache/providers/CacheProvider' 33 | ] 34 | ``` 35 | 36 | Also, for registering commands. 37 | 38 | ##### start/app.js 39 | ```javascript 40 | const aceProviders = [ 41 | ..., 42 | 'adonis-cache/providers/CommandsProvider' 43 | ] 44 | ``` 45 | 46 | Also, it is a good practice to setup an alias to avoid typing the complete namespace. 47 | 48 | ##### start/app.js 49 | ```javascript 50 | const aliases = { 51 | ..., 52 | Cache: 'Adonis/Addons/Cache' 53 | } 54 | ``` 55 | 56 | Then, for generating a config file. 57 | ```bash 58 | adonis cache:config 59 | ``` 60 | 61 | 62 | ## Configuration 63 | 64 | AdonisCache provides an expressive, unified API for various caching backends. The cache configuration is located at `config/cache.js`. In this file you may specify which cache driver you would like used by default throughout your application. AdonisCache supports popular caching backends like [Redis](http://redis.io) out of the box. 65 | 66 | The cache configuration file also contains various other options, which are documented within the file, so make sure to read over these options. By default, AdonisCache is configured to use the `object` cache driver, which stores cached objects in plain JavaScript object (use only for development). For larger applications, it is recommended that you use a more robust driver such as Redis. You may even configure multiple cache configurations for the same driver. 67 | 68 | 69 | ### Driver Prerequisites 70 | 71 | #### Database 72 | 73 | When using the `database` cache driver, you will need to setup a table to contain the cache items. You'll find an example `Schema` declaration for the table below: 74 | ```javascript 75 | this.create('cache', (table) => { 76 | table.string('key').unique() 77 | table.text('value') 78 | table.integer('expiration') 79 | }) 80 | ``` 81 | 82 | > {tip} You may also use the `adonis cache:table` Ace command to generate a migration with the proper schema. 83 | 84 | #### Redis 85 | 86 | Before using a Redis cache, you will need to have the Redis provider installed. 87 | 88 | For more information on configuring Redis, consult its [AdonisJs documentation page](http://adonisjs.com/docs/redis). 89 | 90 | 91 | ## Cache Usage 92 | 93 | 94 | ### Obtaining A Cache Instance 95 | 96 | ```javascript 97 | 'use strict' 98 | 99 | const Cache = use('Cache') 100 | 101 | class UserController { 102 | 103 | async index(request, response) { 104 | const value = await Cache.get('key') 105 | 106 | // 107 | } 108 | } 109 | ``` 110 | 111 | #### Accessing Multiple Cache Stores 112 | 113 | You may access various cache stores via the `store` method. The key passed to the `store` method should correspond to one of the stores listed in the `stores` configuration object in your `cache` configuration file: 114 | 115 | ```javascript 116 | value = await Cache.store('database').get('foo') 117 | 118 | await Cache.store('redis').put('bar', 'baz', 10) 119 | ``` 120 | 121 | ### Retrieving Items From The Cache 122 | 123 | The `get` method is used to retrieve items from the cache. If the item does not exist in the cache, `null` will be returned. If you wish, you may pass a second argument to the `get` method specifying the default value you wish to be returned if the item doesn't exist: 124 | 125 | ```javascript 126 | value = await Cache.get('key') 127 | 128 | value = await Cache.get('key', 'default') 129 | ``` 130 | 131 | You may even pass a `Closure` as the default value. The result of the `Closure` will be returned if the specified item does not exist in the cache. Passing a Closure allows you to defer the retrieval of default values from a database or other external service: 132 | 133 | ```javascript 134 | value = await Cache.get('key', async () => { 135 | return await Database.table(...).where(...).first() 136 | }) 137 | ``` 138 | 139 | Retrieving multiple items: 140 | 141 | ```javascript 142 | values = await Cache.many(['key1', 'key2', 'key3']) 143 | // values = { 144 | // key1: value, 145 | // key2: value, 146 | // key3: value 147 | // } 148 | ``` 149 | 150 | #### Checking For Item Existence 151 | 152 | The `has` method may be used to determine if an item exists in the cache: 153 | 154 | ```javascript 155 | if (await Cache.has('key')) { 156 | // 157 | } 158 | ``` 159 | 160 | #### Incrementing / Decrementing Values 161 | 162 | The `increment` and `decrement` methods may be used to adjust the value of integer items in the cache. Both of these methods accept an optional second argument indicating the amount by which to increment or decrement the item's value: 163 | 164 | ```javascript 165 | await Cache.increment('key') 166 | await Cache.increment('key', amount) 167 | await Cache.decrement('key') 168 | await Cache.decrement('key', amount) 169 | ``` 170 | 171 | #### Retrieve & Store 172 | 173 | Sometimes you may wish to retrieve an item from the cache, but also store a default value if the requested item doesn't exist. For example, you may wish to retrieve all users from the cache or, if they don't exist, retrieve them from the database and add them to the cache. You may do this using the `Cache.remember` method: 174 | 175 | ```javascript 176 | value = await Cache.remember('key', minutes, async () => { 177 | return await Database.table(...).where(...).first() 178 | }) 179 | ``` 180 | 181 | If the item does not exist in the cache, the `Closure` passed to the `remember` method will be executed and its result will be placed in the cache. 182 | 183 | #### Retrieve & Delete 184 | 185 | If you need to retrieve an item from the cache and then delete the item, you may use the `pull` method. Like the `get` method, `null` will be returned if the item does not exist in the cache: 186 | 187 | ```javascript 188 | value = await Cache.pull('key') 189 | ``` 190 | 191 | 192 | ### Storing Items In The Cache 193 | 194 | You may use the `put` method on the `Cache` to store items in the cache. When you place an item in the cache, you need to specify the number of minutes for which the value should be cached: 195 | 196 | ```javascript 197 | await Cache.put('key', 'value', minutes) 198 | ``` 199 | 200 | Instead of passing the number of minutes as an integer, you may also pass a `Date` instance representing the expiration time of the cached item: 201 | 202 | ```javascript 203 | const expiresAt = new Date(2016, 11, 1, 12, 0) 204 | 205 | await Cache.put('key', 'value', expiresAt) 206 | ``` 207 | 208 | Storing multiple items: 209 | 210 | ```javascript 211 | const items = { 212 | key1: 'value1', 213 | key2: 'value2', 214 | key3: 'value3' 215 | } 216 | 217 | await Cache.putMany(items, minutes) 218 | ``` 219 | 220 | #### Store If Not Present 221 | 222 | The `add` method will only add the item to the cache if it does not already exist in the cache store. The method will return `true` if the item is actually added to the cache. Otherwise, the method will return `false`: 223 | 224 | ```javascript 225 | await Cache.add('key', 'value', minutes) 226 | ``` 227 | 228 | #### Storing Items Forever 229 | 230 | The `forever` method may be used to store an item in the cache permanently. Since these items will not expire, they must be manually removed from the cache using the `forget` method: 231 | 232 | ```javascript 233 | await Cache.forever('key', 'value') 234 | ``` 235 | 236 | 237 | ### Removing Items From The Cache 238 | 239 | You may remove items from the cache using the `forget` method: 240 | 241 | ```javascript 242 | await Cache.forget('key') 243 | ``` 244 | 245 | You may clear the entire cache using the `flush` method: 246 | 247 | ```javascript 248 | await Cache.flush() 249 | ``` 250 | 251 | > {note} Flushing the cache does not respect the cache prefix and will remove all entries from the cache. Consider this carefully when clearing a cache which is shared by other applications. 252 | 253 | 254 | ## Cache Tags 255 | 256 | > {note} Cache tags are not supported when using the `database` cache driver. 257 | 258 | 259 | ### Storing Tagged Cache Items 260 | 261 | Cache tags allow you to tag related items in the cache and then flush all cached values that have been assigned a given tag. You may access a tagged cache by passing in an ordered array of tag names. For example, let's access a tagged cache and `put` value in the cache: 262 | 263 | ```javascript 264 | await Cache.tags(['people', 'artists']).put('John', john, minutes) 265 | 266 | await Cache.tags(['people', 'authors']).put('Anne', anne, minutes) 267 | ``` 268 | 269 | 270 | ### Accessing Tagged Cache Items 271 | 272 | To retrieve a tagged cache item, pass the same ordered list of tags to the `tags` method and then call the `get` method with the key you wish to retrieve: 273 | 274 | ```javascript 275 | const john = await Cache.tags(['people', 'artists']).get('John') 276 | 277 | const anne = await Cache.tags(['people', 'authors']).get('Anne') 278 | ``` 279 | 280 | 281 | ### Removing Tagged Cache Items 282 | 283 | You may flush all items that are assigned a tag or list of tags. For example, this statement would remove all caches tagged with either `people`, `authors`, or both. So, both `Anne` and `John` would be removed from the cache: 284 | 285 | ```javascript 286 | await Cache.tags(['people', 'authors']).flush() 287 | ``` 288 | 289 | In contrast, this statement would remove only caches tagged with `authors`, so `Anne` would be removed, but not `John`: 290 | 291 | ```javascript 292 | await Cache.tags('authors').flush() 293 | ``` 294 | 295 | 296 | ## Events 297 | 298 | To execute code on every cache operation, you may listen for the [events](http://adonisjs.com/docs/events) fired by the cache. Typically, you should place these event listeners within your `start/events.js`: 299 | 300 | ``` 301 | Cache.hit 302 | Cache.missed 303 | Cache.keyForgotten 304 | Cache.keyWritten 305 | ``` 306 | -------------------------------------------------------------------------------- /commands/ConfigGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const path = require('path') 13 | const { Command } = require('@adonisjs/ace') 14 | 15 | /** 16 | * Command to generate a cache config file 17 | * 18 | * @class ConfigGenerator 19 | * @constructor 20 | */ 21 | class ConfigGenerator extends Command { 22 | constructor (Helpers) { 23 | super() 24 | this.Helpers = Helpers 25 | } 26 | 27 | /** 28 | * IoC container injections 29 | * 30 | * @method inject 31 | * 32 | * @return {Array} 33 | */ 34 | static get inject () { 35 | return ['Adonis/Src/Helpers'] 36 | } 37 | 38 | /** 39 | * The command signature 40 | * 41 | * @method signature 42 | * 43 | * @return {String} 44 | */ 45 | static get signature () { 46 | return 'cache:config' 47 | } 48 | 49 | /** 50 | * The command description 51 | * 52 | * @method description 53 | * 54 | * @return {String} 55 | */ 56 | static get description () { 57 | return 'Generate cache config file' 58 | } 59 | 60 | /** 61 | * Method called when command is executed 62 | * 63 | * @method handle 64 | * 65 | * @param {Object} options 66 | * @return {void} 67 | */ 68 | async handle (options) { 69 | /** 70 | * Reading template as a string form the mustache file 71 | */ 72 | const template = await this.readFile(path.join(__dirname, './templates/config.mustache'), 'utf8') 73 | 74 | /** 75 | * Directory paths 76 | */ 77 | const relativePath = path.join('config', 'cache.js') 78 | const configPath = path.join(this.Helpers.appRoot(), relativePath) 79 | 80 | /** 81 | * If command is not executed via command line, then return 82 | * the response 83 | */ 84 | if (!this.viaAce) { 85 | return this.generateFile(configPath, template, {}) 86 | } 87 | 88 | /** 89 | * Otherwise wrap in try/catch and show appropriate messages 90 | * to the end user. 91 | */ 92 | try { 93 | await this.generateFile(configPath, template, {}) 94 | this.completed('create', relativePath) 95 | } catch (error) { 96 | this.error(`${relativePath} cache config file already exists`) 97 | } 98 | } 99 | } 100 | 101 | module.exports = ConfigGenerator 102 | -------------------------------------------------------------------------------- /commands/TableGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const path = require('path') 13 | const { Command } = require('@adonisjs/ace') 14 | 15 | /** 16 | * Command to generate a migration for the cache database table 17 | * 18 | * @class TableGenerator 19 | * @constructor 20 | */ 21 | class TableGenerator extends Command { 22 | constructor(Helpers) { 23 | super() 24 | this.Helpers = Helpers 25 | } 26 | 27 | /** 28 | * IoC container injections 29 | * 30 | * @method inject 31 | * 32 | * @return {Array} 33 | */ 34 | static get inject() { 35 | return ['Adonis/Src/Helpers'] 36 | } 37 | 38 | /** 39 | * The command signature 40 | * 41 | * @method signature 42 | * 43 | * @return {String} 44 | */ 45 | static get signature() { 46 | return 'cache:table' 47 | } 48 | 49 | /** 50 | * The command description 51 | * 52 | * @method description 53 | * 54 | * @return {String} 55 | */ 56 | static get description() { 57 | return 'Generate a migration for the cache database table' 58 | } 59 | 60 | /** 61 | * Method called when command is executed 62 | * 63 | * @method handle 64 | * 65 | * @param {Object} options 66 | * @return {void} 67 | */ 68 | async handle(options) { 69 | /** 70 | * Reading template as a string form the mustache file 71 | */ 72 | const template = await this.readFile(path.join(__dirname, './templates/table.mustache'), 'utf8') 73 | 74 | /** 75 | * Directory paths 76 | */ 77 | const relativePath = path.join('database/migrations', `${new Date().getTime()}_create_cache_table.js`) 78 | const fullPath = path.join(this.Helpers.appRoot(), relativePath) 79 | 80 | /** 81 | * If command is not executed via command line, then return 82 | * the response 83 | */ 84 | if (!this.viaAce) { 85 | return this.generateFile(fullPath, template, {}) 86 | } 87 | 88 | /** 89 | * Otherwise wrap in try/catch and show appropriate messages 90 | * to the end user. 91 | */ 92 | try { 93 | await this.generateFile(fullPath, template, {}) 94 | this.completed('create', relativePath) 95 | } catch (error) { 96 | this.error(`${relativePath} file already exists`) 97 | } 98 | } 99 | } 100 | 101 | module.exports = TableGenerator 102 | -------------------------------------------------------------------------------- /commands/templates/config.mustache: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Env = use('Env') 4 | const Helpers = use('Helpers') 5 | 6 | module.exports = { 7 | 8 | /* 9 | |-------------------------------------------------------------------------- 10 | | Default Cache Store 11 | |-------------------------------------------------------------------------- 12 | | 13 | | This option controls the default cache store that gets used while 14 | | using this caching library. This store is used when another is 15 | | not explicitly specified when executing a given caching function. 16 | | 17 | */ 18 | 19 | default: Env.get('CACHE_STORE', 'object'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Cache Stores 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Here you may define all of the cache "stores" for your application as 27 | | well as their drivers. You may even define multiple stores for the 28 | | same cache driver to group types of items stored in your caches. 29 | | 30 | | Supported drivers: "object", "database", "redis" 31 | | 32 | | Hint: Use "null" driver for disabling caching 33 | | Warning: Use the "object" driver only for development, it does not have a garbage collector. 34 | | 35 | */ 36 | 37 | stores: { 38 | 39 | object: { 40 | driver: 'object' 41 | }, 42 | 43 | database: { 44 | driver: 'database', 45 | table: 'cache', 46 | connection: 'sqlite' 47 | }, 48 | 49 | redis: { 50 | driver: 'redis', 51 | connection: 'local' 52 | }, 53 | 54 | null: { 55 | driver: 'null' 56 | } 57 | 58 | }, 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Cache Key Prefix 63 | |-------------------------------------------------------------------------- 64 | | 65 | | When utilizing a RAM based store, there might be other applications 66 | | utilizing the same cache. So, we'll specify a value to get prefixed 67 | | to all our keys so we can avoid collisions. 68 | | 69 | */ 70 | 71 | prefix: 'adonis' 72 | 73 | } 74 | -------------------------------------------------------------------------------- /commands/templates/table.mustache: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Schema = use('Schema') 4 | 5 | class CreateCacheTable extends Schema { 6 | 7 | /** 8 | * Run the migrations. 9 | * 10 | * @return {void} 11 | */ 12 | up () { 13 | this.create('cache', (table) => { 14 | table.string('key').unique() 15 | table.text('value') 16 | table.integer('expiration') 17 | }) 18 | } 19 | 20 | /** 21 | * Reverse the migrations/ 22 | * 23 | * @return {void} 24 | */ 25 | down () { 26 | this.drop('cache') 27 | } 28 | 29 | } 30 | 31 | module.exports = CreateCacheTable 32 | -------------------------------------------------------------------------------- /credits.md: -------------------------------------------------------------------------------- 1 | ### Laravel Framework 2 | ### This project is inspired and substantial portions of it are ported from the Laravel Framework 3 | --- 4 | The MIT License (MIT) 5 | 6 | Copyright (c) Taylor Otwell 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adonis-cache", 3 | "version": "0.3.4", 4 | "description": "Cache provider for AdonisJs framework", 5 | "scripts": { 6 | "test": "node node_modules/.bin/mocha", 7 | "lint": "standard src/**/*.js test/*.js providers/*.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/helnokaly/adonis-cache.git" 12 | }, 13 | "keywords": [ 14 | "adonisjs", 15 | "adonis", 16 | "cache", 17 | "caching" 18 | ], 19 | "author": "Hany El Nokaly", 20 | "license": "MIT", 21 | "homepage": "https://github.com/helnokaly/adonis-cache", 22 | "devDependencies": { 23 | "@adonisjs/ace": "^4.0.5", 24 | "@adonisjs/fold": "^4.0.2", 25 | "chai": "^4.1.2", 26 | "ioredis": "^3.2.2", 27 | "knex": "^0.16.3", 28 | "mocha": "^4.0.1", 29 | "sqlite3": "^4.0.6", 30 | "standard": "^10.0.3" 31 | }, 32 | "dependencies": { 33 | "lodash": "^4.17.11" 34 | }, 35 | "standard": { 36 | "globals": [ 37 | "describe", 38 | "it" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /providers/CacheProvider.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const { ServiceProvider } = require('@adonisjs/fold') 13 | const CacheManager = require('../src/Stores/CacheManager') 14 | 15 | class CacheProvider extends ServiceProvider { 16 | /** 17 | * Register all the required providers 18 | * 19 | * @method register 20 | * 21 | * @return {void} 22 | */ 23 | register () { 24 | this.app.singleton('Adonis/Addons/Cache', (app) => { 25 | return new CacheManager(app) 26 | }) 27 | } 28 | } 29 | 30 | module.exports = CacheProvider 31 | -------------------------------------------------------------------------------- /providers/CommandsProvider.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const { ServiceProvider } = require('@adonisjs/fold') 13 | 14 | class CommandsProvider extends ServiceProvider { 15 | /** 16 | * Register all the required providers 17 | * 18 | * @method register 19 | * 20 | * @return {void} 21 | */ 22 | register () { 23 | this.app.bind('Adonis/Commands/Cache:Config', () => require('../commands/ConfigGenerator')) 24 | this.app.bind('Adonis/Commands/Cache:Table', () => require('../commands/TableGenerator')) 25 | } 26 | 27 | /** 28 | * On boot 29 | * 30 | * @method boot 31 | * 32 | * @return {void} 33 | */ 34 | boot () { 35 | /** 36 | * Register command with ace. 37 | */ 38 | const ace = require('@adonisjs/ace') 39 | ace.addCommand('Adonis/Commands/Cache:Config') 40 | ace.addCommand('Adonis/Commands/Cache:Table') 41 | } 42 | } 43 | 44 | module.exports = CommandsProvider 45 | -------------------------------------------------------------------------------- /src/Events/CacheHit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | class CacheHit { 13 | /** 14 | * Create a new event instance. 15 | * 16 | * @param {string} key The key that was hit 17 | * @param {mixed} value The value that was retrieved 18 | * @param {array} tags The tags that were assigned to the key 19 | * @returns {void} 20 | */ 21 | constructor (key, value, tags = []) { 22 | this.key = key 23 | this.tags = tags 24 | this.value = value 25 | } 26 | } 27 | 28 | CacheHit.EVENT = 'Cache.hit' 29 | 30 | module.exports = CacheHit 31 | -------------------------------------------------------------------------------- /src/Events/CacheMissed.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | class CacheMissed { 13 | /** 14 | * Create a new event instance. 15 | * 16 | * @param {string} key The key that was missed 17 | * @param {array} tags The tags that were assigned to the key 18 | * @returns {void} 19 | */ 20 | constructor (key, tags = []) { 21 | this.key = key 22 | this.tags = tags 23 | } 24 | } 25 | 26 | CacheMissed.EVENT = 'Cache.missed' 27 | 28 | module.exports = CacheMissed 29 | -------------------------------------------------------------------------------- /src/Events/KeyForgotten.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | class KeyForgotten { 13 | /** 14 | * Create a new event instance. 15 | * 16 | * @param {string} key The key that was forgotten 17 | * @param {array} tags The tags that were assigned to the key 18 | * @returns {void} 19 | */ 20 | constructor (key, tags = []) { 21 | this.key = key 22 | this.tags = tags 23 | } 24 | } 25 | 26 | KeyForgotten.EVENT = 'Cache.keyForgotten' 27 | 28 | module.exports = KeyForgotten 29 | -------------------------------------------------------------------------------- /src/Events/KeyWritten.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | class KeyWritten { 13 | /** 14 | * Create a new event instance. 15 | * 16 | * @param {string} key The key that was written 17 | * @param {mixed} value The value that was written 18 | * @param {int} minutes The number of minutes the key should be valid 19 | * @param {array} tags The tags that were assigned to the key 20 | * @returns {void} 21 | */ 22 | constructor (key, value, minutes, tags = []) { 23 | this.key = key 24 | this.tags = tags 25 | this.value = value 26 | this.minutes = minutes 27 | } 28 | } 29 | 30 | KeyWritten.EVENT = 'Cache.keyWritten' 31 | 32 | module.exports = KeyWritten 33 | -------------------------------------------------------------------------------- /src/Stores/CacheManager.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const ObjectStore = require('./ObjectStore') 13 | const RedisStore = require('./RedisStore') 14 | const NullStore = require('./NullStore') 15 | const DatabaseStore = require('./DatabaseStore') 16 | const Repository = require('./Repository') 17 | 18 | class CacheManager { 19 | constructor (app) { 20 | this._app = app // The application instance 21 | this._stores = [] // The array of resolved cache stores 22 | this._customCreators = [] // The registered custom driver creators 23 | 24 | return new Proxy(this, { 25 | get: function (target, name) { 26 | if (target[name] !== undefined) { 27 | return target[name] 28 | } 29 | // Dynamically call the default driver instance 30 | const store = target.store() 31 | if (typeof store[name] === 'function') { 32 | return store[name].bind(store) 33 | } 34 | } 35 | }) 36 | } 37 | 38 | /** 39 | * Get a cache store instance by name. 40 | * 41 | * @param {string|null} name 42 | * @return {mixed} 43 | */ 44 | store (name = null) { 45 | name = name || this.getDefaultDriver() 46 | this._stores[name] = this._get(name) 47 | return this._stores[name] 48 | } 49 | 50 | /** 51 | * Get a cache driver instance. 52 | * 53 | * @param {string} driver 54 | * @return {mixed} 55 | */ 56 | driver (driver = null) { 57 | return this._store(driver) 58 | } 59 | 60 | /** 61 | * Attempt to get the store from the local cache. 62 | * 63 | * @param {string} name 64 | * @return {Repository} 65 | * @private 66 | */ 67 | _get (name) { 68 | return this._stores[name] != null ? this._stores[name] : this._resolve(name) 69 | } 70 | 71 | /** 72 | * Resolve the given store. 73 | * 74 | * @param {string} name 75 | * @return {Repository} 76 | * 77 | * @throws {InvalidArgumentException} 78 | * @private 79 | */ 80 | _resolve (name) { 81 | const config = this._getConfig(name) 82 | 83 | if (config == null) { 84 | throw new Error(`InvalidArgumentException: Cache store [${name}] is not defined.`) 85 | } 86 | 87 | if (this._customCreators[config['driver']] != null) { 88 | return this._callCustomCreator(config) 89 | } else { 90 | const driveName = config['driver'].charAt(0).toUpperCase() + config['driver'].substr(1).toLowerCase() 91 | const driverMethod = '_create' + driveName + 'Driver' 92 | 93 | if (typeof this[driverMethod] === 'function') { 94 | return this[driverMethod](config) 95 | } else { 96 | throw new Error(`InvalidArgumentException: Driver [${config['driver']}] is not supported.`) 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * Call a custom driver creator. 103 | * 104 | * @param {object} config 105 | * @return {mixed} 106 | * @private 107 | */ 108 | _callCustomCreator (config) { 109 | return this._customCreators[config['driver']](this._app, config) 110 | } 111 | 112 | /** 113 | * Create an instance of the Null cache driver. 114 | * 115 | * @return {Repository} 116 | * @private 117 | */ 118 | _createNullDriver () { 119 | return this.repository(new NullStore()) 120 | } 121 | 122 | /** 123 | * Create an instance of the object cache driver. 124 | * 125 | * @return {Repository} 126 | * @private 127 | */ 128 | _createObjectDriver () { 129 | return this.repository(new ObjectStore()) 130 | } 131 | 132 | /** 133 | * Create an instance of the Redis cache driver. 134 | * 135 | * @param {object} config 136 | * @return {Repository} 137 | * @private 138 | */ 139 | _createRedisDriver (config) { 140 | const redis = this._app.use('Adonis/Addons/Redis') 141 | const connection = config['connection'] ? config['connection'] : 'local' 142 | return this.repository(new RedisStore(redis, this._getPrefix(config), connection)) 143 | } 144 | 145 | /** 146 | * Create an instance of the database cache driver. 147 | * 148 | * @param {object} config 149 | * @return {Repository} 150 | * @private 151 | */ 152 | _createDatabaseDriver (config) { 153 | const connection = this._app.use('Adonis/Src/Database').connection(config['connection']) 154 | 155 | return this.repository(new DatabaseStore(connection, config['table'], this._getPrefix(config))) 156 | } 157 | 158 | /** 159 | * Create a new cache repository with the given implementation. 160 | * 161 | * @param {Store} store 162 | * @return {Repository} 163 | */ 164 | repository (store) { 165 | const repository = new Repository(store) 166 | 167 | const Event = this._app.use('Adonis/Src/Event') 168 | if (Event != null) { 169 | repository.setEventDispatcher(Event) 170 | } 171 | 172 | return repository 173 | } 174 | 175 | /** 176 | * Get the cache prefix. 177 | * 178 | * @param {object} config 179 | * @return {string} 180 | * @private 181 | */ 182 | _getPrefix (config) { 183 | return config['prefix'] ? config['prefix'] : this._app.use('Adonis/Src/Config').get('cache.prefix') 184 | } 185 | 186 | /** 187 | * Get the cache connection configuration. 188 | * 189 | * @param {string} name 190 | * @return {object} 191 | * @private 192 | */ 193 | _getConfig (name) { 194 | return this._app.use('Adonis/Src/Config').get(`cache.stores.${name}`) 195 | } 196 | 197 | /** 198 | * Get the default cache driver name. 199 | * 200 | * @return {string} 201 | */ 202 | getDefaultDriver () { 203 | return this._app.use('Adonis/Src/Config').get('cache.default') 204 | } 205 | 206 | /** 207 | * Set the default cache driver name. 208 | * 209 | * @param {string} name 210 | * @return {void} 211 | */ 212 | setDefaultDriver (name) { 213 | // this._app.use('Adonis/Src/Config').get('cache.default') = name 214 | } 215 | 216 | /** 217 | * Register a custom driver creator Closure. 218 | * 219 | * @param {string} driver 220 | * @param {function} closure 221 | * @return {this} 222 | */ 223 | extend (driver, closure) { 224 | // this._customCreators[driver] = closure.bindTo(this, this) 225 | this._customCreators[driver] = closure.bind(this) 226 | return this 227 | } 228 | } 229 | 230 | module.exports = CacheManager 231 | -------------------------------------------------------------------------------- /src/Stores/DatabaseStore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const Util = require('../Util') 13 | 14 | class DatabaseStore { 15 | constructor (connection, tableName, prefix = '') { 16 | this._connection = connection 17 | this._tableName = tableName 18 | this._prefix = prefix 19 | 20 | /** 21 | * Probability (parts per million) that garbage collection (GC) should be performed 22 | * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance. 23 | * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all. 24 | */ 25 | this._gcProbability = 100; 26 | } 27 | 28 | /** 29 | * Return a new query builder instance with cache's table set 30 | * 31 | * @returns {mixed} 32 | * @private 33 | */ 34 | _table () { 35 | return this._connection.table(this._tableName) 36 | } 37 | 38 | /** 39 | * Retrieve an item from the cache by key. 40 | * 41 | * @param {string} key 42 | * @return {Promise} 43 | */ 44 | async get (key) { 45 | const cache = await this._table().where('key', this._prefix + key).first() 46 | 47 | if (cache === undefined) { 48 | return null 49 | } 50 | 51 | if (Date.now() / 1000 >= cache.expiration) { 52 | await this.forget(key) 53 | return null 54 | } 55 | 56 | return Util.deserialize(cache.value) 57 | } 58 | 59 | /** 60 | * Retrieve multiple items from the cache by key. 61 | * 62 | * Items not found in the cache will have a null value. 63 | * 64 | * @param {Array} keys 65 | * @return {Promise} 66 | */ 67 | async many (keys) { 68 | let values = await Promise.all(keys.map(key => this.get(key))) 69 | let mappedValues = {} 70 | for (let i = 0; i < keys.length; i++) { 71 | mappedValues[keys[i]] = values[i] 72 | } 73 | return mappedValues 74 | } 75 | 76 | /** 77 | * Store an item in the cache for a given number of minutes. 78 | * 79 | * @param {string} key 80 | * @param {mixed} value 81 | * @param {int|float} minutes 82 | * @return {Promise} 83 | */ 84 | async put (key, value, minutes = 0) { 85 | const prefixedKey = this._prefix + key 86 | const serializedValue = Util.serialize(value) 87 | const expiration = Math.floor((Date.now() / 1000) + minutes * 60) 88 | 89 | try { 90 | await this._table().insert({key: prefixedKey, value: serializedValue, expiration: expiration}) 91 | } catch (e) { 92 | await this._table().where('key', prefixedKey).update({value: serializedValue, expiration: expiration}) 93 | } 94 | 95 | // Call garbage collection function 96 | await this._gc() 97 | } 98 | 99 | /** 100 | * Store multiple items in the cache for a given number of minutes. 101 | * 102 | * @param {object} values 103 | * @param {int} minutes 104 | * @return {Promise} 105 | */ 106 | async putMany (object, minutes) { 107 | let operations = [] 108 | for (let prop in object) { 109 | operations.push(this.put(prop, object[prop], minutes)) 110 | } 111 | await Promise.all(operations) 112 | } 113 | 114 | /** 115 | * Increment the value of an item in the cache. 116 | * 117 | * @param {string} key 118 | * @param {mixed} value 119 | * @return {Promise} 120 | */ 121 | increment (key, value = 1) { 122 | return this._incrementOrDecrement(key, value, (currentValue) => { 123 | return currentValue + value 124 | }) 125 | } 126 | 127 | /** 128 | * Decrement the value of an item in the cache. 129 | * 130 | * @param {string} key 131 | * @param {mixed} value 132 | * @return {Promise} 133 | */ 134 | decrement (key, value = 1) { 135 | return this._incrementOrDecrement(key, value, (currentValue) => { 136 | return currentValue - value 137 | }) 138 | } 139 | 140 | /** 141 | * Increment or decrement the value of an item in the cache. 142 | * 143 | * @param {string} key 144 | * @param {mixed} value 145 | * @param {function} callback 146 | * @return {Promise} 147 | * 148 | * @private 149 | */ 150 | _incrementOrDecrement (key, value, callback) { 151 | return new Promise((resolve, reject) => { 152 | this._connection.transaction(trx => { 153 | const prefixedKey = this._prefix + key 154 | return trx.table(this._tableName).where('key', prefixedKey).forUpdate().first() 155 | .then(r => { 156 | if (r === undefined) { 157 | resolve(false) 158 | return 159 | } 160 | const currentValue = parseInt(r.value) 161 | if (isNaN(currentValue)) { 162 | resolve(false) 163 | return 164 | } 165 | const newValue = callback(currentValue) 166 | return trx.table(this._tableName).where('key', prefixedKey).update('value', newValue) 167 | .then(r => resolve(newValue)) 168 | }) 169 | .catch(error => reject(error)) 170 | }) 171 | }) 172 | } 173 | 174 | /** 175 | * Store an item in the cache indefinitely. 176 | * 177 | * @param {string} key 178 | * @param {mixed} value 179 | * @return {Promise} 180 | */ 181 | forever (key, value) { 182 | return this.put(key, value, 5256000) 183 | } 184 | 185 | /** 186 | * Remove an item from the cache. 187 | * 188 | * @param {string} key 189 | * @return {Promise} 190 | */ 191 | async forget (key) { 192 | await this._table().where('key', this._prefix + key).delete() 193 | return true 194 | } 195 | 196 | /** 197 | * Remove all items from the cache. 198 | * 199 | * @return {Promise} 200 | */ 201 | async flush () { 202 | await this._table().delete() 203 | } 204 | 205 | /** 206 | * Get the underlying database connection. 207 | * 208 | * @return {Object} database connection 209 | */ 210 | getConnection () { 211 | return this._connection 212 | } 213 | 214 | /** 215 | * Get the cache key prefix. 216 | * 217 | * @return {string} 218 | */ 219 | getPrefix () { 220 | return this._prefix 221 | } 222 | 223 | /** 224 | * Removes the expired data values. 225 | * @param {bool} force whether to enforce the garbage collection regardless of [[gcProbability]]. 226 | * Defaults to false, meaning the actual deletion happens with the probability as specified by [[gcProbability]]. 227 | */ 228 | async _gc (force = false) { 229 | if (force || Util.randomIntBetween(0, 1000000) < this._gcProbability) { 230 | await this._table().where('expiration', '<=', Math.floor(Date.now() / 1000)).delete() 231 | } 232 | } 233 | } 234 | 235 | module.exports = DatabaseStore 236 | -------------------------------------------------------------------------------- /src/Stores/NullStore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const TaggableStore = require('./TaggableStore') 13 | 14 | class NullStore extends TaggableStore { 15 | /** 16 | * Retrieve an item from the cache by key. 17 | * 18 | * @param {string} key 19 | * @return {Promise} 20 | */ 21 | async get (key) { 22 | return null 23 | } 24 | 25 | /** 26 | * Retrieve multiple items from the cache by key. 27 | * 28 | * Items not found in the cache will have a null value. 29 | * 30 | * @param {Array} keys 31 | * @return {Promise} 32 | */ 33 | async many (keys) { 34 | let mappedValues = {} 35 | for (let key of keys) { 36 | mappedValues[key] = null 37 | } 38 | return mappedValues 39 | } 40 | 41 | /** 42 | * Store an item in the cache for a given number of minutes. 43 | * 44 | * @param {string} key 45 | * @param {mixed} value 46 | * @param {float|int} minutes 47 | * @return {Promise} 48 | */ 49 | async put (key, value, minutes) { 50 | return undefined 51 | } 52 | 53 | /** 54 | * Store multiple items in the cache for a given number of minutes. 55 | * 56 | * @param {object} object 57 | * @param {int} minutes 58 | * @return {Promise} 59 | */ 60 | async putMany (object, minutes) { 61 | return undefined 62 | } 63 | 64 | /** 65 | * Increment the value of an item in the cache. 66 | * 67 | * @param {string} key 68 | * @param {mixed} value 69 | * @return {Promise} 70 | */ 71 | async increment (key, value = 1) { 72 | return false 73 | } 74 | 75 | /** 76 | * Decrement the value of an item in the cache. 77 | * 78 | * @param {string} key 79 | * @param {mixed} value 80 | * @return {Promise} 81 | */ 82 | async decrement (key, value = 1) { 83 | return false 84 | } 85 | 86 | /** 87 | * Store an item in the cache indefinitely. 88 | * 89 | * @param {string} key 90 | * @param {mixed} value 91 | * @return {Promise} 92 | */ 93 | async forever (key, value) { 94 | return undefined 95 | } 96 | 97 | /** 98 | * Remove an item from the cache. 99 | * 100 | * @param {string} key 101 | * @return {Promise} 102 | */ 103 | async forget (key) { 104 | return true 105 | } 106 | 107 | /** 108 | * Remove all items from the cache. 109 | * 110 | * @return {Promise} 111 | */ 112 | async flush () { 113 | return undefined 114 | } 115 | 116 | /** 117 | * Get the cache key prefix. 118 | * 119 | * @return {string} 120 | */ 121 | getPrefix () { 122 | return '' 123 | } 124 | } 125 | 126 | module.exports = NullStore 127 | -------------------------------------------------------------------------------- /src/Stores/ObjectStore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const TaggableStore = require('./TaggableStore') 13 | const Util = require('../Util') 14 | 15 | class ObjectStore extends TaggableStore { 16 | constructor () { 17 | super() 18 | this._storage = {} 19 | } 20 | 21 | /** 22 | * Retrieve an item from the cache by key. 23 | * 24 | * @param {string} key 25 | * @return {Promise} 26 | */ 27 | async get (key) { 28 | const cache = this._storage[key] 29 | if (cache === undefined) { 30 | return null 31 | } 32 | if (Date.now() / 1000 >= cache.expiration) { 33 | this.forget(key) 34 | return null 35 | } 36 | return Util.deserialize(cache.value) 37 | } 38 | 39 | /** 40 | * Retrieve multiple items from the cache by key. 41 | * 42 | * Items not found in the cache will have a null value. 43 | * 44 | * @param {Array} keys 45 | * @return {Promise} 46 | */ 47 | many (keys) { 48 | return Promise.all(keys.map(key => this.get(key))) 49 | .then(values => { 50 | let mappedValues = {} 51 | for (let i = 0; i < keys.length; i++) { 52 | mappedValues[keys[i]] = values[i] 53 | } 54 | return mappedValues 55 | }) 56 | } 57 | 58 | /** 59 | * Store an item in the cache for a given number of minutes. 60 | * 61 | * @param {string} key 62 | * @param {mixed} value 63 | * @param {int|float} minutes 64 | * @return {Promise} 65 | */ 66 | put (key, value, minutes = 0) { 67 | return new Promise((resolve, reject) => { 68 | const expiration = Math.floor((Date.now() / 1000) + minutes * 60) 69 | this._storage[key] = { 70 | value: Util.serialize(value), 71 | expiration: expiration 72 | } 73 | resolve() 74 | }) 75 | } 76 | 77 | /** 78 | * Store multiple items in the cache for a given number of minutes. 79 | * 80 | * @param {object} object 81 | * @param {int} minutes 82 | * @return {Promise} 83 | */ 84 | putMany (object, minutes) { 85 | let promiseArray = [] 86 | for (let prop in object) { 87 | promiseArray.push(this.put(prop, object[prop], minutes)) 88 | } 89 | return Promise.all(promiseArray) 90 | .then(r => {}) 91 | } 92 | 93 | /** 94 | * Increment the value of an item in the cache. 95 | * 96 | * @param {string} key 97 | * @param {mixed} value 98 | * @return {Promise} 99 | */ 100 | increment (key, value = 1) { 101 | return this._incrementOrDecrement(key, value, (currentValue) => { 102 | return currentValue + value 103 | }) 104 | } 105 | 106 | /** 107 | * Decrement the value of an item in the cache. 108 | * 109 | * @param {string} key 110 | * @param {mixed} value 111 | * @return {Promise} 112 | */ 113 | decrement (key, value = 1) { 114 | return this._incrementOrDecrement(key, value, (currentValue) => { 115 | return currentValue - value 116 | }) 117 | } 118 | 119 | /** 120 | * Increment or decrement the value of an item in the cache. 121 | * 122 | * @param {string} key 123 | * @param {mixed} value 124 | * @param {function} callback 125 | * @return {Promise} 126 | * 127 | * @private 128 | */ 129 | _incrementOrDecrement (key, value, callback) { 130 | return new Promise((resolve, reject) => { 131 | const cache = this._storage[key] 132 | if (cache === undefined) { 133 | resolve(false) 134 | return 135 | } 136 | const currentValue = parseInt(cache.value) 137 | if (isNaN(currentValue)) { 138 | resolve(false) 139 | return 140 | } 141 | const newValue = callback(currentValue) 142 | this._storage[key].value = newValue 143 | resolve(newValue) 144 | }) 145 | } 146 | 147 | /** 148 | * Store an item in the cache indefinitely. 149 | * 150 | * @param {string} key 151 | * @param {mixed} value 152 | * @return {Promise} 153 | */ 154 | forever (key, value) { 155 | return this.put(key, value, 5256000) 156 | } 157 | 158 | /** 159 | * Remove an item from the cache. 160 | * 161 | * @param {string} key 162 | * @return {Promise} 163 | */ 164 | async forget (key) { 165 | delete this._storage[key] 166 | return true 167 | } 168 | 169 | /** 170 | * Remove all items from the cache. 171 | * 172 | * @return {Promise} 173 | */ 174 | flush () { 175 | return new Promise((resolve, reject) => { 176 | this._storage = {} 177 | resolve() 178 | }) 179 | } 180 | 181 | /** 182 | * Get the cache key prefix. 183 | * 184 | * @return string 185 | */ 186 | getPrefix () { 187 | return '' 188 | } 189 | } 190 | 191 | module.exports = ObjectStore 192 | -------------------------------------------------------------------------------- /src/Stores/RedisStore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const _ = require('lodash') 13 | const TaggableStore = require('./TaggableStore') 14 | const Util = require('../Util') 15 | const RedisTaggedCache = require('./RedisTaggedCache') 16 | const TagSet = require('./TagSet') 17 | 18 | class RedisStore extends TaggableStore { 19 | constructor (Redis, prefix = '', connection) { 20 | super() 21 | this._redis = Redis 22 | this._prefix = prefix 23 | this.setPrefix(prefix) 24 | this.setConnection(connection) 25 | } 26 | 27 | /** 28 | * Retrieve an item from the cache by key. 29 | * 30 | * @param {string} key 31 | * @return {Promise} 32 | */ 33 | async get (key) { 34 | return Util.deserialize(await this.connection().get(this._prefix + key)) 35 | } 36 | 37 | /** 38 | * Retrieve multiple items from the cache by key. 39 | * 40 | * Items not found in the cache will have a null value. 41 | * 42 | * @param {Array} keys 43 | * @return {Promise} 44 | */ 45 | async many (keys) { 46 | let values = await Promise.all(keys.map(key => this.get(key))) 47 | let mappedValues = {} 48 | for (let i = 0; i < keys.length; i++) { 49 | mappedValues[keys[i]] = values[i] 50 | } 51 | return mappedValues 52 | } 53 | 54 | /** 55 | * Store an item in the cache for a given number of minutes. 56 | * 57 | * @param {string} key 58 | * @param {mixed} value 59 | * @param {int|float} minutes 60 | * @return {Promise} 61 | */ 62 | async put (key, value, minutes = 0) { 63 | const prefixedKey = this._prefix + key 64 | let expiration = Math.floor(minutes * 60) 65 | const serializedValue = Util.serialize(value) 66 | 67 | if (isNaN(expiration) || expiration < 1) { 68 | expiration = 1 69 | } 70 | 71 | await this.connection().setex(prefixedKey, expiration, serializedValue) 72 | } 73 | 74 | /** 75 | * Store multiple items in the cache for a given number of minutes. 76 | * 77 | * @param {object} object 78 | * @param {int} minutes 79 | * @return {Promise} 80 | */ 81 | async putMany (object, minutes) { 82 | for (let prop in object) { 83 | await this.put(prop, object[prop], minutes) 84 | } 85 | } 86 | 87 | /** 88 | * Increment the value of an item in the cache. 89 | * 90 | * @param {string} key 91 | * @param {mixed} value 92 | * @return {Promise} 93 | */ 94 | async increment (key, value = 1) { 95 | try { 96 | return await this.connection().incrby(this._prefix + key, value) 97 | } catch (error) { 98 | if (error.name === 'ReplyError') { 99 | return false 100 | } else { 101 | throw error 102 | } 103 | } 104 | } 105 | 106 | /** 107 | * Decrement the value of an item in the cache. 108 | * 109 | * @param {string} key 110 | * @param {mixed} value 111 | * @return {Promise} 112 | */ 113 | async decrement (key, value = 1) { 114 | try { 115 | return await this.connection().decrby(this._prefix + key, value) 116 | } catch (error) { 117 | if (error.name === 'ReplyError') { 118 | return false 119 | } else { 120 | throw error 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Store an item in the cache indefinitely. 127 | * 128 | * @param {string} key 129 | * @param {mixed} value 130 | * @return {Promise} 131 | */ 132 | async forever (key, value) { 133 | await this.connection().set(this._prefix + key, Util.serialize(value)) 134 | } 135 | 136 | /** 137 | * Remove an item from the cache. 138 | * 139 | * @param {string} key 140 | * @return {Promise} 141 | */ 142 | async forget (key) { 143 | await this.connection().del(this._prefix + key) 144 | return true 145 | } 146 | 147 | /** 148 | * Remove all items from the cache. 149 | * 150 | * @return {Promise} 151 | */ 152 | async flush () { 153 | await this.connection().flushdb() 154 | } 155 | 156 | /** 157 | * Begin executing a new tags operation. 158 | * 159 | * @param array|mixed $names 160 | * @return {RedisTaggedCache} 161 | */ 162 | tags (names) { 163 | names = Array.isArray(names) ? names : Array.from(arguments) 164 | return new RedisTaggedCache(this, new TagSet(this, names)) 165 | } 166 | 167 | /** 168 | * Get the Redis connection instance 169 | * 170 | * @return {Object} 171 | * 172 | */ 173 | connection () { 174 | return this._redis.connection(this._connection) 175 | } 176 | 177 | /** 178 | * Set the connection name to be used 179 | * 180 | * @param {string} connection 181 | * @return {void} 182 | */ 183 | setConnection (connection) { 184 | this._connection = connection 185 | } 186 | 187 | /** 188 | * Get the Redis database instance 189 | * 190 | * @return {object} 191 | */ 192 | getRedis () { 193 | return this._redis 194 | } 195 | 196 | /** 197 | * Get the cache key prefix 198 | * 199 | * @return {string} 200 | */ 201 | getPrefix () { 202 | return this._prefix 203 | } 204 | 205 | /** 206 | * Set the cache key prefix 207 | * 208 | * @param {string} prefix 209 | * @return {void} 210 | */ 211 | setPrefix (prefix) { 212 | this.prefix = !_.isEmpty(prefix) ? prefix + ':' : '' 213 | } 214 | } 215 | 216 | module.exports = RedisStore 217 | -------------------------------------------------------------------------------- /src/Stores/RedisTaggedCache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const TaggedCache = require('./TaggedCache') 13 | const crypto = require('crypto') 14 | const _ = require('lodash') 15 | 16 | class RedisTaggedCache extends TaggedCache { 17 | /** 18 | * Store an item in the cache. 19 | * 20 | * @param {string} key 21 | * @param {mixed} value 22 | * @param {Date|float|int} minutes 23 | * @return {Promise} 24 | */ 25 | async put (key, value, minutes = null) { 26 | await this._pushStandardKeys(await this._tags.getNamespace(), key) 27 | await super.put(key, value, minutes) 28 | } 29 | 30 | /** 31 | * Store an item in the cache indefinitely. 32 | * 33 | * @param {string} key 34 | * @param {mixed} value 35 | * @return {Promise} 36 | */ 37 | async forever (key, value) { 38 | await this._pushForeverKeys(await this._tags.getNamespace(), key) 39 | await super.forever(key, value) 40 | } 41 | 42 | /** 43 | * Remove all items from the cache. 44 | * 45 | * @return {Promise} 46 | */ 47 | async flush () { 48 | await this._deleteForeverKeys() 49 | await this._deleteStandardKeys() 50 | await super.flush() 51 | } 52 | 53 | /** 54 | * Store standard key references into store. 55 | * 56 | * @param {string} namespace 57 | * @param {string} key 58 | * @return {Promise} 59 | * 60 | * @private 61 | */ 62 | _pushStandardKeys (namespace, key) { 63 | return this._pushKeys(namespace, key, RedisTaggedCache.REFERENCE_KEY_STANDARD) 64 | } 65 | 66 | /** 67 | * Store forever key references into store. 68 | * 69 | * @param {string} namespace 70 | * @param {string} key 71 | * @return {Promise} 72 | * 73 | * @private 74 | */ 75 | _pushForeverKeys (namespace, key) { 76 | return this._pushKeys(namespace, key, RedisTaggedCache.REFERENCE_KEY_FOREVER) 77 | } 78 | 79 | /** 80 | * Store a reference to the cache key against the reference key. 81 | * 82 | * @param {string} namespace 83 | * @param {string} key 84 | * @param {string} reference 85 | * @return {Promise} 86 | * 87 | * @private 88 | */ 89 | async _pushKeys (namespace, key, reference) { 90 | const fullKey = this._store.getPrefix() + crypto.createHash('sha1').update(namespace).digest('hex') + ':' + key 91 | for (let segment of namespace.split('|')) { 92 | await this._store.connection().sadd(this._referenceKey(segment, reference), fullKey) 93 | } 94 | } 95 | 96 | /** 97 | * Delete all of the items that were stored forever. 98 | * 99 | * @return {Promise} 100 | * 101 | * @private 102 | */ 103 | _deleteForeverKeys () { 104 | return this._deleteKeysByReference(RedisTaggedCache.REFERENCE_KEY_FOREVER) 105 | } 106 | 107 | /** 108 | * Delete all standard items. 109 | * 110 | * @return {Promise} 111 | * 112 | * @private 113 | */ 114 | _deleteStandardKeys () { 115 | return this._deleteKeysByReference(RedisTaggedCache.REFERENCE_KEY_STANDARD) 116 | } 117 | 118 | /** 119 | * Find and delete all of the items that were stored against a reference. 120 | * 121 | * @param {string} reference 122 | * @return {Promise} 123 | * 124 | * @private 125 | */ 126 | async _deleteKeysByReference (reference) { 127 | for (let segment of await this._tags.getNamespace()) { 128 | await this._deleteValues(segment = this._referenceKey(segment, reference)) 129 | await this._store.connection().del(segment) 130 | } 131 | } 132 | 133 | /** 134 | * Delete item keys that have been stored against a reference. 135 | * 136 | * @param {string} referenceKey 137 | * @return {Promise} 138 | * 139 | * @private 140 | */ 141 | async _deleteValues (referenceKey) { 142 | const values = _.uniq(await this._store.connection().smembers(referenceKey)) 143 | for (let i = 0; i < values.length; i++) { 144 | await this._store.connection().del(values[i]) 145 | } 146 | } 147 | 148 | /** 149 | * Get the reference key for the segment. 150 | * 151 | * @param {string} segment 152 | * @param {string} suffix 153 | * @return {string} 154 | * 155 | * @private 156 | */ 157 | _referenceKey (segment, suffix) { 158 | return this._store.getPrefix() + segment + ':' + suffix 159 | } 160 | } 161 | 162 | /** 163 | * Forever reference key. 164 | * 165 | * @var string 166 | */ 167 | RedisTaggedCache.REFERENCE_KEY_FOREVER = 'forever_ref' 168 | 169 | /** 170 | * Standard reference key. 171 | * 172 | * @var string 173 | */ 174 | RedisTaggedCache.REFERENCE_KEY_STANDARD = 'standard_ref' 175 | 176 | module.exports = RedisTaggedCache 177 | -------------------------------------------------------------------------------- /src/Stores/Repository.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const Util = require('../Util') 13 | 14 | // Events 15 | const CacheHit = require('../Events/CacheHit') 16 | const CacheMissed = require('../Events/CacheMissed') 17 | const KeyForgotten = require('../Events/KeyForgotten') 18 | const KeyWritten = require('../Events/KeyWritten') 19 | 20 | class Repository { 21 | /** 22 | * Create a new cache repository instance. 23 | * 24 | * @param {Object} store The cache store implementation 25 | * @return {void} 26 | */ 27 | constructor (store) { 28 | this._store = store // The cache store implementation 29 | this._events = null // The event dispatcher implementation 30 | 31 | return new Proxy(this, { 32 | get: function (target, name) { 33 | if (target[name] !== undefined) { 34 | return target[name] 35 | } 36 | // Pass missing functions to the store. 37 | if (typeof target._store[name] === 'function') { 38 | return target._store[name].bind(target._store) 39 | } 40 | } 41 | }) 42 | } 43 | 44 | /** 45 | * Set the event dispatcher instance. 46 | * 47 | * @param {Adonis/Src/Event} events 48 | * @return {void} 49 | */ 50 | setEventDispatcher (events) { 51 | this._events = events 52 | } 53 | 54 | /** 55 | * Fire an event for this cache instance. 56 | * 57 | * @param {string} event 58 | * @param {array} payload 59 | * @return {void} 60 | */ 61 | _fireCacheEvent (event, payload) { 62 | if (this._events == null) { 63 | return 64 | } 65 | 66 | switch (event) { 67 | case 'hit': 68 | if (payload.length === 2) { 69 | payload.push([]) 70 | } 71 | return this._events.fire(CacheHit.EVENT, new CacheHit(payload[0], payload[1], payload[2])) 72 | case 'missed': 73 | if (payload.length === 1) { 74 | payload.push([]) 75 | } 76 | return this._events.fire(CacheMissed.EVENT, new CacheMissed(payload[0], payload[1])) 77 | case 'delete': 78 | if (payload.length === 1) { 79 | payload.push([]) 80 | } 81 | return this._events.fire(KeyForgotten.EVENT, new KeyForgotten(payload[0], payload[1])) 82 | case 'write': 83 | if (payload.length === 3) { 84 | payload.push([]) 85 | } 86 | return this._events.fire(KeyWritten.EVENT, new KeyWritten(payload[0], payload[1], payload[2], payload[3])) 87 | } 88 | } 89 | 90 | /** 91 | * Determine if an item exists in the cache. 92 | * 93 | * @param {string} key 94 | * @return {Promise} 95 | */ 96 | async has (key) { 97 | return (await this.get(key)) != null 98 | } 99 | 100 | /** 101 | * Retrieve an item from the cache by key. 102 | * 103 | * @param {string} key 104 | * @param {mixed} defaultValue 105 | * @return {Promise} 106 | */ 107 | async get (key, defaultValue = null) { 108 | let value = await this._store.get(await this._itemKey(key)) 109 | 110 | if (value == null) { 111 | this._fireCacheEvent('missed', [key]) 112 | 113 | return Util.valueOf(defaultValue) 114 | } else { 115 | this._fireCacheEvent('hit', [key, value]) 116 | } 117 | 118 | return value 119 | } 120 | 121 | /** 122 | * Retrieve multiple items from the cache by key. 123 | * 124 | * Items not found in the cache will have a null value. 125 | * 126 | * @param {Array} keys 127 | * @return {Promise} 128 | */ 129 | async many (keys) { 130 | const values = await this._store.many(keys) 131 | for (let key in values) { 132 | if (values[key] == null) { 133 | this._fireCacheEvent('missed', [key]) 134 | } else { 135 | this._fireCacheEvent('hit', [key, values[key]]) 136 | } 137 | } 138 | return values 139 | } 140 | 141 | /** 142 | * Retrieve an item from the cache and delete it. 143 | * 144 | * @param {string} key 145 | * @param {mixed} default 146 | * @return {Promise} 147 | */ 148 | async pull (key, defaultValue = null) { 149 | const value = await this.get(key, defaultValue) 150 | await this.forget(key) 151 | return value 152 | } 153 | 154 | /** 155 | * Store an item in the cache. 156 | * 157 | * @param {string} key 158 | * @param {mixed} value 159 | * @param {Date|float|int} minutes 160 | * @return {Promise} 161 | */ 162 | async put (key, value, minutes = null) { 163 | if (value == null) { 164 | return 165 | } 166 | 167 | minutes = this._getMinutes(minutes) 168 | 169 | if (minutes != null) { 170 | await this._store.put(await this._itemKey(key), value, minutes) 171 | this._fireCacheEvent('write', [key, value, minutes]) 172 | } 173 | } 174 | 175 | /** 176 | * Store multiple items in the cache for a given number of minutes. 177 | * 178 | * @param {object} values 179 | * @param {Date|float|int} minutes 180 | * @return {Promise} 181 | */ 182 | async putMany (values, minutes) { 183 | minutes = this._getMinutes(minutes) 184 | 185 | if (minutes != null) { 186 | await this._store.putMany(values, minutes) 187 | 188 | for (let key in values) { 189 | this._fireCacheEvent('write', [key, values[key], minutes]) 190 | } 191 | } 192 | } 193 | 194 | /** 195 | * Store an item in the cache if the key does not exist. 196 | * 197 | * @param {string} key 198 | * @param {mixed} value 199 | * @param {DateTime|float|int} minutes 200 | * @return {Promise} 201 | */ 202 | async add (key, value, minutes) { 203 | minutes = this._getMinutes(minutes) 204 | 205 | if (minutes == null) { 206 | return false 207 | } 208 | 209 | if (typeof this._store['add'] === 'function') { 210 | return this._store.add(await this._itemKey(key), value, minutes) 211 | } 212 | 213 | if ((await this.get(key)) == null) { 214 | await this.put(key, value, minutes) 215 | return true 216 | } 217 | 218 | return false 219 | } 220 | 221 | /** 222 | * Increment the value of an item in the cache. 223 | * 224 | * @param {string} key 225 | * @param {int} value 226 | * @return {Promise} 227 | */ 228 | increment (key, value = 1) { 229 | return this._store.increment(key, value) 230 | } 231 | 232 | /** 233 | * Decrement the value of an item in the cache. 234 | * 235 | * @param {string} key 236 | * @param {mixed} value 237 | * @return {Promise} 238 | */ 239 | decrement (key, value = 1) { 240 | return this._store.decrement(key, value) 241 | } 242 | 243 | /** 244 | * Store an item in the cache indefinitely. 245 | * 246 | * @param {string} key 247 | * @param {mixed} value 248 | * @return {void} 249 | */ 250 | async forever (key, value) { 251 | this._store.forever(await this._itemKey(key), value) 252 | this._fireCacheEvent('write', [key, value, 0]) 253 | } 254 | 255 | /** 256 | * Get an item from the cache, or store the default value. 257 | * 258 | * @param {string} key 259 | * @param {Date|float|int} minutes 260 | * @param {function} closure 261 | * @return {Promise} 262 | */ 263 | async remember (key, minutes, closure) { 264 | // If the item exists in the cache we will just return this immediately 265 | // otherwise we will execute the given Closure and cache the result 266 | // of that execution for the given number of minutes in storage. 267 | let value = await this.get(key) 268 | if (value != null) { 269 | return value 270 | } 271 | 272 | value = await Util.valueOf(closure) 273 | await this.put(key, value, minutes) 274 | return Util.deserialize(Util.serialize(value)) 275 | } 276 | 277 | /** 278 | * Get an item from the cache, or store the default value forever. 279 | * 280 | * @param {string} key 281 | * @param {function} closure 282 | * @return {Promise} 283 | */ 284 | sear (key, closure) { 285 | return this.rememberForever(key, closure) 286 | } 287 | 288 | /** 289 | * Get an item from the cache, or store the default value forever. 290 | * 291 | * @param {string} key 292 | * @param {function} closure 293 | * @return {Promise} 294 | */ 295 | async rememberForever (key, closure) { 296 | // If the item exists in the cache we will just return this immediately 297 | // otherwise we will execute the given Closure and cache the result 298 | // of that execution for the given number of minutes. It's easy. 299 | let value = await this.get(key) 300 | if (value != null) { 301 | return value 302 | } 303 | 304 | value = await Util.valueOf(closure) 305 | await this.forever(key, value) 306 | return Util.deserialize(Util.serialize(value)) 307 | } 308 | 309 | /** 310 | * Remove an item from the cache. 311 | * 312 | * @param {string} key 313 | * @return {Promise} 314 | */ 315 | async forget (key) { 316 | const success = await this._store.forget(await this._itemKey(key)) 317 | this._fireCacheEvent('delete', [key]) 318 | return success 319 | } 320 | 321 | /** 322 | * Begin executing a new tags operation if the store supports it. 323 | * 324 | * @param {Array} names 325 | * @return {TaggedCache} 326 | * 327 | * @throws {BadMethodCallException} 328 | */ 329 | tags (names) { 330 | names = Array.isArray(names) ? names : Array.from(arguments) 331 | if (typeof this._store['tags'] === 'function') { 332 | const taggedCache = this._store.tags(names) 333 | 334 | if (this._events != null) { 335 | taggedCache.setEventDispatcher(this._events) 336 | } 337 | 338 | return taggedCache 339 | } 340 | throw new Error('BadMethodCallException: This cache store does not support tagging.') 341 | } 342 | 343 | /** 344 | * Format the key for a cache item. 345 | * 346 | * @param {string} key 347 | * @return {Promise} 348 | */ 349 | async _itemKey (key) { 350 | return key 351 | } 352 | 353 | /** 354 | * Get the cache store implementation. 355 | * 356 | * @return {Store} 357 | */ 358 | getStore () { 359 | return this._store 360 | } 361 | 362 | /** 363 | * Calculate the number of minutes with the given duration. 364 | * 365 | * @param {Date|float|int} duration 366 | * @return {float|int|null} 367 | */ 368 | _getMinutes (duration) { 369 | if (duration instanceof Date) { 370 | duration = ((duration.getTime() - Date.now()) / 1000) / 60 371 | } 372 | 373 | return (duration * 60) > 0 ? duration : null 374 | } 375 | } 376 | 377 | module.exports = Repository 378 | -------------------------------------------------------------------------------- /src/Stores/TagSet.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const crypto = require('crypto') 13 | 14 | class TagSet { 15 | /** 16 | * Create a new TagSet instance. 17 | * 18 | * @param {Store} store The cache store implementation 19 | * @param {array} names The tag names 20 | * @return {TagSet} 21 | */ 22 | constructor (store, names = []) { 23 | this._store = store 24 | this._names = names 25 | } 26 | 27 | /** 28 | * Reset all tags in the set. 29 | * 30 | * @return {Promise} 31 | */ 32 | async reset () { 33 | for (let name of this._names) { 34 | await this.resetTag(name) 35 | } 36 | } 37 | 38 | /** 39 | * Get the unique tag identifier for a given tag. 40 | * 41 | * @param {string} name 42 | * @return {Promise} 43 | */ 44 | async tagId (name) { 45 | const id = await this._store.get(this.tagKey(name)) 46 | return id || this.resetTag(name) 47 | } 48 | 49 | /** 50 | * Get an array of tag identifiers for all of the tags in the set. 51 | * 52 | * @return {Promise} 53 | */ 54 | _tagIds () { 55 | return Promise.all(this._names.map(name => this.tagId(name))) 56 | } 57 | 58 | /** 59 | * Get a unique namespace that changes when any of the tags are flushed. 60 | * 61 | * @return {Promise} 62 | */ 63 | async getNamespace () { 64 | return (await this._tagIds()).join('|') 65 | } 66 | 67 | /** 68 | * Reset the tag and return the new tag identifier. 69 | * 70 | * @param {string} name 71 | * @return {Promise} 72 | */ 73 | async resetTag (name) { 74 | const id = crypto.randomBytes(8).toString('hex') 75 | await this._store.forever(this.tagKey(name), id) 76 | return id 77 | } 78 | 79 | /** 80 | * Get the tag identifier key for a given tag. 81 | * 82 | * @param {string} name 83 | * @return {string} 84 | */ 85 | tagKey (name) { 86 | return 'tag:' + name + ':key' 87 | } 88 | 89 | /** 90 | * Get all of the tag names in the set. 91 | * 92 | * @return {array} 93 | */ 94 | getNames () { 95 | return this._names 96 | } 97 | } 98 | 99 | module.exports = TagSet 100 | -------------------------------------------------------------------------------- /src/Stores/TaggableStore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const TaggedCache = require('./TaggedCache') 13 | const TagSet = require('./TagSet') 14 | 15 | // Abstract 16 | class TaggableStore { 17 | /** 18 | * Begin executing a new tags operation. 19 | * 20 | * @param {array|mixed} names 21 | * @return {TaggedCache} 22 | */ 23 | tags (names) { 24 | names = Array.isArray(names) ? names : Array.from(arguments) 25 | return new TaggedCache(this, new TagSet(this, names)) 26 | } 27 | } 28 | 29 | module.exports = TaggableStore 30 | -------------------------------------------------------------------------------- /src/Stores/TaggedCache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const crypto = require('crypto') 13 | const Repository = require('./Repository') 14 | 15 | class TaggedCache extends Repository { 16 | /** 17 | * Create a new tagged cache instance. 18 | * 19 | * @param {Store} store 20 | * @param {TagSet} tags The tag set instance 21 | * @return {void} 22 | */ 23 | constructor (store, tags) { 24 | super(store) 25 | this._tags = tags 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | _fireCacheEvent (event, payload) { 32 | payload.push(this._tags.getNames()) 33 | super._fireCacheEvent(event, payload) 34 | } 35 | 36 | /** 37 | * Increment the value of an item in the cache. 38 | * 39 | * @param {string} key 40 | * @param {mixed} value 41 | * @return {Promsie} 42 | */ 43 | async increment (key, value = 1) { 44 | return this._store.increment(await this._itemKey(key), value) 45 | } 46 | 47 | /** 48 | * Increment the value of an item in the cache. 49 | * 50 | * @param {string} key 51 | * @param {mixed} value 52 | * @return {Promise} 53 | */ 54 | async decrement (key, value = 1) { 55 | return this._store.decrement(await this._itemKey(key), value) 56 | } 57 | 58 | /** 59 | * Remove all items from the cache. 60 | * 61 | * @return {Promise} 62 | */ 63 | flush () { 64 | return this._tags.reset() 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | _itemKey (key) { 71 | return this.taggedItemKey(key) 72 | } 73 | 74 | /** 75 | * Get a fully qualified key for a tagged item. 76 | * 77 | * @param {string} key 78 | * @return {Promise} 79 | */ 80 | async taggedItemKey (key) { 81 | return crypto.createHash('sha1').update(await this._tags.getNamespace()).digest('hex') + ':' + key 82 | } 83 | } 84 | 85 | module.exports = TaggedCache 86 | -------------------------------------------------------------------------------- /src/Util/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | function serialize (data) { 13 | return JSON.stringify(data) 14 | } 15 | 16 | function deserialize (data) { 17 | return JSON.parse(data) 18 | } 19 | 20 | async function valueOf (value) { 21 | if (typeof value === 'function') { 22 | value = value() 23 | } 24 | 25 | return value 26 | } 27 | 28 | /** 29 | * Returns integer number between two numbers (inclusive) 30 | * 31 | * @param {int} min 32 | * @param {int} max 33 | * @return int 34 | */ 35 | function randomIntBetween(min, max) { 36 | return Math.floor(Math.random() * (max - min + 1)) + min 37 | } 38 | 39 | module.exports = {serialize, deserialize, valueOf, randomIntBetween} 40 | -------------------------------------------------------------------------------- /test/DatabaseStore.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const TestHelpers = require('./TestHelpers') 4 | const chai = require('chai') 5 | const expect = chai.expect 6 | 7 | const knex = require('knex')({ 8 | client: 'sqlite3', 9 | connection: { 10 | filename: './test/mydb.sqlite' 11 | }, 12 | useNullAsDefault: true 13 | }) 14 | 15 | const Store = new (require('../src/Stores/DatabaseStore'))(knex, 'cache', 'adonis') 16 | 17 | describe('Database Store', function () { 18 | describe('Initializing', function () { 19 | it('Should create a database table for caching if it does not exist or empty existing one', async () => { 20 | if (await knex.schema.hasTable('cache')) { 21 | await knex.table('cache').del() 22 | } else { 23 | await knex.schema.createTable('cache', function (table) { 24 | table.string('key').unique() 25 | table.text('value') 26 | table.integer('expiration') 27 | }) 28 | } 29 | }) 30 | }) 31 | 32 | const name = 'David' 33 | const age = 26 34 | const height = 170.5 35 | const person = { 36 | name: 'David', 37 | age: 26, 38 | height: 170.5, 39 | tags: ['smart', 'ambitious'] 40 | } 41 | const array = ['string', 1, 2.0, {}] 42 | 43 | describe('put', function () { 44 | it('Should put key in cache and return void (value is string)', async () => { 45 | expect(await Store.put('name', name, 1)).to.equal(undefined) 46 | }) 47 | it('Should put key in cache and return void (value is integer)', async () => { 48 | expect(await Store.put('age', age, 1)).to.equal(undefined) 49 | }) 50 | it('Should put key in cache and return void (value is float)', async () => { 51 | expect(await Store.put('height', height, 1)).to.equal(undefined) 52 | }) 53 | it('Should put key in cache and return void (value is plain object)', async () => { 54 | expect(await Store.put('person', person, 1)).to.equal(undefined) 55 | }) 56 | it('Should put key in cache and return void (value is array)', async () => { 57 | expect(await Store.put('array', array, 1)).to.equal(undefined) 58 | }) 59 | }) 60 | 61 | describe('get', function () { 62 | it('Should get cached value (value is string)', async () => { 63 | expect(await Store.get('name')).to.equal(name) 64 | }) 65 | it('Should get cached value (value is integer)', async () => { 66 | expect(await Store.get('age')).to.equal(age) 67 | }) 68 | it('Should get cached value (value is float)', async () => { 69 | expect(await Store.get('height')).to.equal(height) 70 | }) 71 | it('Should get cached value (value is plain object)', async () => { 72 | expect(await Store.get('person')).to.deep.equal(person) 73 | }) 74 | it('Should get cached value (value is array)', async () => { 75 | expect(await Store.get('array')).to.deep.equal(array) 76 | }) 77 | it('Should return null for a key that is not cached', async () => { 78 | expect(await Store.get('unknown')).to.equal(null) 79 | }) 80 | }) 81 | 82 | describe('many', function () { 83 | it('Should get many cached value at once', async () => { 84 | expect(await Store.many(['name', 'age', 'height'])).to.deep.equal({name, age, height}) 85 | }) 86 | }) 87 | 88 | describe('flush', function () { 89 | it('Should flush cached data and return void', async () => { 90 | expect(await Store.flush()).to.equal(undefined) 91 | }) 92 | it('Should get null for cached data after flushing', async () => { 93 | expect(await Store.get('name')).to.equal(null) 94 | }) 95 | it('Should get null for cached data after flushing', async () => { 96 | expect(await Store.get('age')).to.equal(null) 97 | }) 98 | it('Should get null for cached data after flushing', async () => { 99 | expect(await Store.get('height')).to.equal(null) 100 | }) 101 | }) 102 | 103 | describe('putMany', function () { 104 | it('Should put many key:value pairs in cache and return void', async () => { 105 | expect(await Store.putMany(person, 1)).to.equal(undefined) 106 | }) 107 | it('Should get cached value added through putMany', async () => { 108 | expect(await Store.get('name')).to.equal(name) 109 | }) 110 | it('Should get cached value added through putMany', async () => { 111 | expect(await Store.get('age')).to.equal(age) 112 | }) 113 | it('Should get cached value added through putMany', async () => { 114 | expect(await Store.get('height')).to.equal(height) 115 | }) 116 | }) 117 | 118 | describe('forget', function () { 119 | it('Should forget a key and return true', async () => { 120 | expect(await Store.forget('name')).to.equal(true) 121 | }) 122 | it('Should get get null for forgotten key', async () => { 123 | expect(await Store.get('name')).to.equal(null) 124 | }) 125 | }) 126 | 127 | describe('increment', function () { 128 | it('Should increment age and return incremented value', async () => { 129 | expect((await Store.increment('age')) === 27).to.equal(true) 130 | }) 131 | it('Should return false for unincrementable value', async () => { 132 | expect(await Store.increment('tags')).to.equal(false) 133 | }) 134 | }) 135 | 136 | describe('decrement', function () { 137 | it('Should decrement age and return incremented value', async () => { 138 | expect((await Store.decrement('age', 7)) === 20).to.equal(true) 139 | }) 140 | it('Should return false for undecrementable value', async () => { 141 | expect(await Store.increment('tags')).to.equal(false) 142 | }) 143 | }) 144 | 145 | describe('expiration', function () { 146 | it('Should put a new key to test expiration', async () => { 147 | await Store.put('framework', 'adonis', 1) 148 | expect(await Store.get('framework')).to.equal('adonis') 149 | }) 150 | 151 | it('Should not be able to get key value after 1 minute', async function () { 152 | this.timeout(1 * 60 * 1000 + 5000) 153 | await TestHelpers.sleep(1 * 60 * 1000) 154 | expect(await Store.get('framework')).to.equal(null) 155 | }) 156 | 157 | it('Should not be able to get an expired key (0 minutes)', async function () { 158 | this.timeout(5000) 159 | await Store.put('year', 2016) 160 | await TestHelpers.sleep(1000) 161 | expect(await Store.get('year')).to.equal(null) 162 | }) 163 | 164 | it('Should allow expiration in sub minute (seconds)', async function () { 165 | this.timeout(10000) 166 | let key = 'submin-key-1' 167 | let value = 'submin-value-1' 168 | await Store.put(key, value, 5 / 60) 169 | await TestHelpers.sleep(3000) 170 | expect(await Store.get(key)).to.equal(value) 171 | }) 172 | 173 | it('Should not be able to get an expired key (seconds)', async function () { 174 | this.timeout(10000) 175 | let key = 'submin-key-2' 176 | let value = 'submin-value-2' 177 | await Store.put(key, value, 5 / 60) 178 | await TestHelpers.sleep(6000) 179 | expect(await Store.get(key)).to.equal(null) 180 | }) 181 | }) 182 | }) 183 | -------------------------------------------------------------------------------- /test/NullStore.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chai = require('chai') 4 | const expect = chai.expect 5 | 6 | const Store = new (require('../src/Stores/NullStore'))() 7 | 8 | describe('Null Store', function () { 9 | describe('put', function () { 10 | it('Should return undefined', async () => { 11 | expect(await Store.put('key', 'value', 1)).to.equal(undefined) 12 | }) 13 | }) 14 | 15 | describe('get', function () { 16 | it('Should return null', async () => { 17 | expect(await Store.get('key')).to.equal(null) 18 | }) 19 | }) 20 | 21 | describe('flush', function () { 22 | it('Should return undefined', async () => { 23 | expect(await Store.flush()).to.equal(undefined) 24 | }) 25 | }) 26 | 27 | describe('putMany', function () { 28 | it('Should return undefined', async () => { 29 | expect(await Store.putMany({key1: 'value1', key2: 'value2'}, 1)).to.equal(undefined) 30 | }) 31 | }) 32 | 33 | describe('many', function () { 34 | it('Should return cached values as null', async () => { 35 | expect(await Store.many(['key1', 'key2'])).to.deep.equal({key1: null, key2: null}) 36 | }) 37 | }) 38 | 39 | describe('forget', function () { 40 | it('Should return true', async () => { 41 | expect(await Store.forget('name')).to.equal(true) 42 | }) 43 | }) 44 | 45 | describe('increment', function () { 46 | it('Should return false', async () => { 47 | expect(await Store.increment('key')).to.equal(false) 48 | }) 49 | }) 50 | 51 | describe('decrement', function () { 52 | it('Should return false', async () => { 53 | expect(await Store.decrement('age')).to.equal(false) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/ObjectStore.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const TestHelpers = require('./TestHelpers') 4 | const chai = require('chai') 5 | const expect = chai.expect 6 | 7 | const Store = new (require('../src/Stores/ObjectStore'))() 8 | 9 | describe('Object Store', function () { 10 | const name = 'David' 11 | const age = 26 12 | const height = 170.5 13 | const person = { 14 | name: 'David', 15 | age: 26, 16 | height: 170.5, 17 | tags: ['smart', 'ambitious'] 18 | } 19 | const array = ['string', 1, 2.0, {}] 20 | 21 | describe('put', function () { 22 | it('Should put key in cache and return void (value is string)', async () => { 23 | expect(await Store.put('name', name, 1)).to.equal(undefined) 24 | }) 25 | it('Should put key in cache and return void (value is integer)', async () => { 26 | expect(await Store.put('age', age, 1)).to.equal(undefined) 27 | }) 28 | it('Should put key in cache and return void (value is float)', async () => { 29 | expect(await Store.put('height', height, 1)).to.equal(undefined) 30 | }) 31 | it('Should put key in cache and return void (value is plain object)', async () => { 32 | expect(await Store.put('person', person, 1)).to.equal(undefined) 33 | }) 34 | it('Should put key in cache and return void (value is array)', async () => { 35 | expect(await Store.put('array', array, 1)).to.equal(undefined) 36 | }) 37 | }) 38 | 39 | describe('get', function () { 40 | it('Should get cached value (value is string)', async () => { 41 | expect(await Store.get('name')).to.equal(name) 42 | }) 43 | it('Should get cached value (value is integer)', async () => { 44 | expect(await Store.get('age')).to.equal(age) 45 | }) 46 | it('Should get cached value (value is float)', async () => { 47 | expect(await Store.get('height')).to.equal(height) 48 | }) 49 | it('Should get cached value (value is plain object)', async () => { 50 | expect(await Store.get('person')).to.deep.equal(person) 51 | }) 52 | it('Should get cached value (value is array)', async () => { 53 | expect(await Store.get('array')).to.deep.equal(array) 54 | }) 55 | it('Should return null for a key that is not cached', async () => { 56 | expect(await Store.get('unknown')).to.equal(null) 57 | }) 58 | }) 59 | 60 | describe('many', function () { 61 | it('Should get many cached value at once', async () => { 62 | expect(await Store.many(['name', 'age', 'height'])).to.deep.equal({name, age, height}) 63 | }) 64 | }) 65 | 66 | describe('flush', function () { 67 | it('Should flush cached data and return void', async () => { 68 | expect(await Store.flush()).to.equal(undefined) 69 | }) 70 | it('Should get null for cached data after flushing', async () => { 71 | expect(await Store.get('name')).to.equal(null) 72 | }) 73 | it('Should get null for cached data after flushing', async () => { 74 | expect(await Store.get('age')).to.equal(null) 75 | }) 76 | it('Should get null for cached data after flushing', async () => { 77 | expect(await Store.get('height')).to.equal(null) 78 | }) 79 | }) 80 | 81 | describe('putMany', function () { 82 | it('Should put many key:value pairs in cache and return void', async () => { 83 | expect(await Store.putMany(person, 1)).to.equal(undefined) 84 | }) 85 | it('Should get cached value added through putMany', async () => { 86 | expect(await Store.get('name')).to.equal(name) 87 | }) 88 | it('Should get cached value added through putMany', async () => { 89 | expect(await Store.get('age')).to.equal(age) 90 | }) 91 | it('Should get cached value added through putMany', async () => { 92 | expect(await Store.get('height')).to.equal(height) 93 | }) 94 | }) 95 | 96 | describe('forget', function () { 97 | it('Should forget a key and return true', async () => { 98 | expect(await Store.forget('name')).to.equal(true) 99 | }) 100 | it('Should get get null for forgotten key', async () => { 101 | expect(await Store.get('name')).to.equal(null) 102 | }) 103 | }) 104 | 105 | describe('increment', function () { 106 | it('Should increment age and return incremented value', async () => { 107 | expect((await Store.increment('age'))).to.equal(27) 108 | }) 109 | it('Should return false for unincrementable value', async () => { 110 | expect(await Store.increment('tags')).to.equal(false) 111 | }) 112 | }) 113 | 114 | describe('decrement', function () { 115 | it('Should decrement age and return incremented value', async () => { 116 | expect((await Store.decrement('age', 7))).to.equal(20) 117 | }) 118 | it('Should return false for undecrementable value', async () => { 119 | expect(await Store.increment('tags')).to.equal(false) 120 | }) 121 | }) 122 | 123 | describe('expiration', function () { 124 | it('Should put a new key to test expiration', async () => { 125 | await Store.put('framework', 'adonis', 1) 126 | expect(await Store.get('framework')).to.equal('adonis') 127 | }) 128 | 129 | it('Should not be able to get key value after 1 minute', async function () { 130 | this.timeout(1 * 60 * 1000 + 5000) 131 | await TestHelpers.sleep(1 * 60 * 1000) 132 | expect(await Store.get('framework')).to.equal(null) 133 | }) 134 | 135 | it('Should not be able to get an expired key (0 minutes)', async function () { 136 | this.timeout(5000) 137 | await Store.put('year', 2016) 138 | await TestHelpers.sleep(1000) 139 | expect(await Store.get('year')).to.equal(null) 140 | }) 141 | 142 | it('Should allow expiration in sub minute (seconds)', async function () { 143 | this.timeout(10000) 144 | let key = 'submin-key-1' 145 | let value = 'submin-value-1' 146 | await Store.put(key, value, 5 / 60) 147 | await TestHelpers.sleep(3000) 148 | expect(await Store.get(key)).to.equal(value) 149 | }) 150 | 151 | it('Should not be able to get an expired key (seconds)', async function () { 152 | this.timeout(10000) 153 | let key = 'submin-key-2' 154 | let value = 'submin-value-2' 155 | await Store.put(key, value, 5 / 60) 156 | await TestHelpers.sleep(6000) 157 | expect(await Store.get(key)).to.equal(null) 158 | }) 159 | }) 160 | }) 161 | -------------------------------------------------------------------------------- /test/RedisStore.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const TestHelpers = require('./TestHelpers') 4 | const chai = require('chai') 5 | const expect = chai.expect 6 | 7 | var Redis = require('ioredis') 8 | var redis = new Redis() 9 | 10 | var redisStub = { 11 | connection: () => redis 12 | } 13 | 14 | const Store = new (require('../src/Stores/RedisStore'))(redisStub) 15 | 16 | describe('Redis Store', function () { 17 | const name = 'David' 18 | const age = 26 19 | const height = 170.5 20 | const person = { 21 | name: 'David', 22 | age: 26, 23 | height: 170.5, 24 | tags: ['smart', 'ambitious'] 25 | } 26 | const array = ['string', 1, 2.0, {}] 27 | 28 | describe('put', function () { 29 | it('Should put key in cache and return void (value is string)', async () => { 30 | expect(await Store.put('name', name, 1)).to.equal(undefined) 31 | }) 32 | it('Should put key in cache and return void (value is integer)', async () => { 33 | expect(await Store.put('age', age, 1)).to.equal(undefined) 34 | }) 35 | it('Should put key in cache and return void (value is float)', async () => { 36 | expect(await Store.put('height', height, 1)).to.equal(undefined) 37 | }) 38 | it('Should put key in cache and return void (value is plain object)', async () => { 39 | expect(await Store.put('person', person, 1)).to.equal(undefined) 40 | }) 41 | it('Should put key in cache and return void (value is array)', async () => { 42 | expect(await Store.put('array', array, 1)).to.equal(undefined) 43 | }) 44 | }) 45 | 46 | describe('get', function () { 47 | it('Should get cached value (value is string)', async () => { 48 | expect(await Store.get('name')).to.equal(name) 49 | }) 50 | it('Should get cached value (value is integer)', async () => { 51 | expect(await Store.get('age')).to.equal(age) 52 | }) 53 | it('Should get cached value (value is float)', async () => { 54 | expect(await Store.get('height')).to.equal(height) 55 | }) 56 | it('Should get cached value (value is plain object)', async () => { 57 | expect(await Store.get('person')).to.deep.equal(person) 58 | }) 59 | it('Should get cached value (value is array)', async () => { 60 | expect(await Store.get('array')).to.deep.equal(array) 61 | }) 62 | it('Should return null for a key that is not cached', async () => { 63 | expect(await Store.get('unknown')).to.equal(null) 64 | }) 65 | }) 66 | 67 | describe('many', function () { 68 | it('Should get many cached value at once', async () => { 69 | expect(await Store.many(['name', 'age', 'height'])).to.deep.equal({name, age, height}) 70 | }) 71 | }) 72 | 73 | describe('flush', function () { 74 | it('Should flush cached data and return void', async () => { 75 | expect(await Store.flush()).to.equal(undefined) 76 | }) 77 | it('Should get null for cached data after flushing', async () => { 78 | expect(await Store.get('name')).to.equal(null) 79 | }) 80 | it('Should get null for cached data after flushing', async () => { 81 | expect(await Store.get('age')).to.equal(null) 82 | }) 83 | it('Should get null for cached data after flushing', async () => { 84 | expect(await Store.get('height')).to.equal(null) 85 | }) 86 | }) 87 | 88 | describe('putMany', function () { 89 | it('Should put many key:value pairs in cache and return void', async () => { 90 | expect(await Store.putMany(person, 1)).to.equal(undefined) 91 | }) 92 | it('Should get cached value added through putMany', async () => { 93 | expect(await Store.get('name')).to.equal(name) 94 | }) 95 | it('Should get cached value added through putMany', async () => { 96 | expect(await Store.get('age')).to.equal(age) 97 | }) 98 | it('Should get cached value added through putMany', async () => { 99 | expect(await Store.get('height')).to.equal(height) 100 | }) 101 | }) 102 | 103 | describe('forget', function () { 104 | it('Should forget a key and return true', async () => { 105 | expect(await Store.forget('name')).to.equal(true) 106 | }) 107 | it('Should get get null for forgotten key', async () => { 108 | expect(await Store.get('name')).to.equal(null) 109 | }) 110 | }) 111 | 112 | describe('increment', function () { 113 | it('Should increment age and return incremented value', async () => { 114 | expect((await Store.increment('age'))).to.equal(27) 115 | }) 116 | it('Should return false for unincrementable value', async () => { 117 | expect(await Store.increment('tags')).to.equal(false) 118 | }) 119 | }) 120 | 121 | describe('decrement', function () { 122 | it('Should decrement age and return incremented value', async () => { 123 | expect((await Store.decrement('age', 7))).to.equal(20) 124 | }) 125 | it('Should return false for undecrementable value', async () => { 126 | expect(await Store.increment('tags')).to.equal(false) 127 | }) 128 | }) 129 | 130 | describe('expiration', function () { 131 | it('Should put a new key to test expiration', async () => { 132 | await Store.put('framework', 'adonis', 1) 133 | expect(await Store.get('framework')).to.equal('adonis') 134 | }) 135 | 136 | it('Should not be able to get key value after 1 minute', async function () { 137 | this.timeout(1 * 60 * 1000 + 5000) 138 | await TestHelpers.sleep(1 * 60 * 1000) 139 | expect(await Store.get('framework')).to.equal(null) 140 | }) 141 | 142 | it('Should not be able to get an expired key (0 minutes)', async function () { 143 | this.timeout(5000) 144 | await Store.put('year', 2016) 145 | await TestHelpers.sleep(1000) 146 | expect(await Store.get('year')).to.equal(null) 147 | }) 148 | 149 | it('Should not be able to get an expired key (0 minutes)', async function () { 150 | this.timeout(5000) 151 | await Store.put('year', 2016) 152 | await TestHelpers.sleep(1000) 153 | expect(await Store.get('year')).to.equal(null) 154 | }) 155 | 156 | it('Should allow expiration in sub minute (seconds)', async function () { 157 | this.timeout(10000) 158 | let key = 'submin-key-1' 159 | let value = 'submin-value-1' 160 | await Store.put(key, value, 5 / 60) 161 | await TestHelpers.sleep(3000) 162 | expect(await Store.get(key)).to.equal(value) 163 | }) 164 | 165 | it('Should not be able to get an expired key (seconds)', async function () { 166 | this.timeout(10000) 167 | let key = 'submin-key-2' 168 | let value = 'submin-value-2' 169 | await Store.put(key, value, 5 / 60) 170 | await TestHelpers.sleep(6000) 171 | expect(await Store.get(key)).to.equal(null) 172 | }) 173 | }) 174 | }) 175 | -------------------------------------------------------------------------------- /test/RedisTaggedCache.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chai = require('chai') 4 | const expect = chai.expect 5 | 6 | var Redis = require('ioredis') 7 | var redis = new Redis() 8 | 9 | var redisStub = { 10 | connection: () => redis 11 | } 12 | // const Store = new (require('../src/Stores/DatabaseStore'))(knex, encryptionStub, 'cache') 13 | 14 | const Store = new (require('../src/Stores/RedisStore'))(redisStub) 15 | const Repository = new (require('../src/Stores/Repository'))(Store) 16 | // const TagSet = new(require('../src/Stores/TagSet'))(Store, ['post', 'bo', 'offer']) 17 | // const RedisTaggedCache = new(require('../src/Stores/RedisTaggedCache'))(Store, TagSet) 18 | 19 | Repository.setEventDispatcher({ 20 | fire: function (event, model) { 21 | // console.log(event, '\n', model) 22 | } 23 | }) 24 | 25 | describe('RedisTaggedCache', function () { 26 | it('should ...', function () { 27 | return Repository.tags(['people', 'programmer']).put('Hany', 'Hany', 1) 28 | .then(r => Repository.tags(['people', 'artist']).put('Hamza', 'Hamza', 1)) 29 | .then(r => Repository.tags(['artist']).flush()) 30 | .then(r => Repository.tags(['people', 'artist']).get('Hamza')) 31 | .then(r => expect(r).to.equal(null)) 32 | .then(r => Repository.tags(['people', 'programmer']).get('Hany')) 33 | .then(r => expect(r).to.equal('Hany')) 34 | }) 35 | it('should ...', function () { 36 | return Repository.tags(['people', 'programmer']).put('Hany', 'Hany', 1) 37 | .then(r => Repository.tags(['people', 'artist']).put('Hamza', 'Hamza', 1)) 38 | .then(r => Repository.tags(['people']).flush()) 39 | .then(r => Repository.tags(['people', 'artist']).get('Hamza')) 40 | .then(r => expect(r).to.equal(null)) 41 | .then(r => Repository.tags(['people', 'programmer']).get('Hany')) 42 | .then(r => expect(r).to.equal(null)) 43 | }) 44 | it('should ...', function () { 45 | return Repository.tags(['people', 'programmer']).put('Hany', 'Hany', 1) 46 | .then(r => Repository.tags(['people', 'artist']).put('Hamza', 'Hamza', 1)) 47 | .then(r => Repository.flush()) 48 | .then(r => Repository.tags(['people', 'artist']).get('Hamza')) 49 | .then(r => expect(r).to.equal(null)) 50 | .then(r => Repository.tags(['people', 'programmer']).get('Hany')) 51 | .then(r => expect(r).to.equal(null)) 52 | }) 53 | }) 54 | 55 | // describe('Repository', function() { 56 | 57 | // describe('flush', function() { 58 | // it('should flush store and return undefined', function() { 59 | // return RedisTaggedCache.flush() 60 | // .then(r => expect(r).to.equal(undefined)) 61 | // // .then(r => { RedisTaggedCache.tags('hany'); }) 62 | // }) 63 | // }) 64 | 65 | // describe('has', function() { 66 | // it('should return false for a key that is not in the cache', function() { 67 | // return RedisTaggedCache.has('ruby') 68 | // .then(r => expect(r).to.equal(false)) 69 | // }) 70 | 71 | // it('should return true for a key that is in the cache', function() { 72 | // return RedisTaggedCache.put('ruby', 'rails', 1) 73 | // .then(r => expect(r).to.equal(undefined)) 74 | // .then(r => RedisTaggedCache.has('ruby')) 75 | // .then(r => expect(r).to.equal(true)) 76 | // }) 77 | // }) 78 | 79 | // describe('pull', function() { 80 | // it('should pull a value of existing key in the cache', function() { 81 | // return RedisTaggedCache.pull('ruby') 82 | // .then(r => expect(r).to.equal('rails')) 83 | // }) 84 | 85 | // it('should return a null for a key that was pulled', function() { 86 | // return RedisTaggedCache.pull('ruby') 87 | // .then(r => expect(r).to.equal(null)) 88 | // }) 89 | 90 | // it('should return default value for a key that does not exist', function() { 91 | // return RedisTaggedCache.pull('ruby', 'gems') 92 | // .then(r => expect(r).to.equal('gems')) 93 | // }) 94 | // }) 95 | 96 | // describe('get', function() { 97 | // it('should get a value of existing key in the cache', function() { 98 | // return RedisTaggedCache.put('name', 'david', 1) 99 | // .then(r => expect(r).to.equal(undefined)) 100 | // .then(r => RedisTaggedCache.get('name')) 101 | // .then(r => expect(r).to.equal('david')) 102 | // }) 103 | 104 | // it('should return a null for a key that was pulled', function() { 105 | // return RedisTaggedCache.get('age') 106 | // .then(r => expect(r).to.equal(null)) 107 | // }) 108 | 109 | // it('should return default value for a key that does not exist', function() { 110 | // return RedisTaggedCache.get('country', 'usa') 111 | // .then(r => expect(r).to.equal('usa')) 112 | // }) 113 | // }) 114 | 115 | // describe('add', function() { 116 | // it('should return false when adding key that is in the cache and should not change value of existing one', function() { 117 | // return RedisTaggedCache.add('name', 'john', 1) 118 | // .then(r => expect(r).to.equal(false)) 119 | // .then(r => RedisTaggedCache.get('name')) 120 | // .then(r => expect(r).to.equal('david')) 121 | // }) 122 | 123 | // it('should return a false when adding a key with 0 minutes expiration and should not change value of existing one', function() { 124 | // return RedisTaggedCache.add('name', 'john', 0) 125 | // .then(r => expect(r).to.equal(false)) 126 | // .then(r => RedisTaggedCache.get('name')) 127 | // .then(r => expect(r).to.equal('david')) 128 | // }) 129 | 130 | // it('should return true when adding key that is not in the cache and should add it', function() { 131 | // return RedisTaggedCache.add('state', 'california', 1) 132 | // .then(r => expect(r).to.equal(true)) 133 | // .then(r => RedisTaggedCache.get('state')) 134 | // .then(r => expect(r).to.equal('california')) 135 | // }) 136 | // }) 137 | 138 | // describe('remember', function() { 139 | // it('should return generator function value and cache its value for a key that is not in the cache', function() { 140 | // return RedisTaggedCache.put('firstname', 'david', 1) 141 | // .then(r => RedisTaggedCache.put('lastname', 'king')) 142 | // .then(r => RedisTaggedCache.remember('test', 1, function * () { 143 | // const temp = yield RedisTaggedCache.get('firstname') 144 | // const temp2 = yield RedisTaggedCache.get('lastname') 145 | // return temp + ' ' + temp2 146 | // })) 147 | // .then(r => expect(r).to.equal('david king')) 148 | // .then(r => RedisTaggedCache.get('test')) 149 | // .then(r => expect(r).to.equal('david king')) 150 | // }) 151 | 152 | // it('should return promise value and cache its value for a key that is not in the cache', function() { 153 | // return RedisTaggedCache.put('firstname2', 'david', 1) 154 | // .then(r => RedisTaggedCache.put('lastname2', 'king')) 155 | // .then(r => RedisTaggedCache.remember('test2', 1, function () { 156 | // return RedisTaggedCache.get('firstname2') 157 | // .then(r2 => RedisTaggedCache.get('lastname2') 158 | // .then(r3 => r2 + ' ' + r3)) 159 | // })) 160 | // .then(r => expect(r).to.equal('david king')) 161 | // .then(r => RedisTaggedCache.get('test2')) 162 | // .then(r => expect(r).to.equal('david king')) 163 | // }) 164 | // }) 165 | 166 | // }) 167 | -------------------------------------------------------------------------------- /test/Repository.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chai = require('chai') 4 | const expect = chai.expect 5 | 6 | var Redis = require('ioredis') 7 | var redis = new Redis() 8 | 9 | var redisStub = { 10 | connection: () => redis 11 | } 12 | 13 | const Store = new (require('../src/Stores/RedisStore'))(redisStub) 14 | const Repository = new (require('../src/Stores/Repository'))(Store) 15 | Repository.setEventDispatcher({ 16 | fire: function (event, model) { 17 | // console.log(event, '\n', model) 18 | } 19 | }) 20 | 21 | describe('Repository', function () { 22 | describe('flush', function () { 23 | it('should flush store and return undefined', function () { 24 | return Repository.flush() 25 | .then(r => expect(r).to.equal(undefined)) 26 | }) 27 | }) 28 | 29 | describe('has', function () { 30 | it('should return false for a key that is not in the cache', function () { 31 | return Repository.has('ruby') 32 | .then(r => expect(r).to.equal(false)) 33 | }) 34 | 35 | it('should return true for a key that is in the cache', function () { 36 | return Repository.put('ruby', 'rails', 1) 37 | .then(r => expect(r).to.equal(undefined)) 38 | .then(r => Repository.has('ruby')) 39 | .then(r => expect(r).to.equal(true)) 40 | }) 41 | }) 42 | 43 | describe('pull', function () { 44 | it('should pull a value of existing key in the cache', function () { 45 | return Repository.pull('ruby') 46 | .then(r => expect(r).to.equal('rails')) 47 | }) 48 | 49 | it('should return a null for a key that was pulled', function () { 50 | return Repository.pull('ruby') 51 | .then(r => expect(r).to.equal(null)) 52 | }) 53 | 54 | it('should return default value for a key that does not exist', function () { 55 | return Repository.pull('ruby', 'gems') 56 | .then(r => expect(r).to.equal('gems')) 57 | }) 58 | }) 59 | 60 | describe('get', function () { 61 | it('should get a value of existing key in the cache', function () { 62 | return Repository.put('name', 'david', 1) 63 | .then(r => expect(r).to.equal(undefined)) 64 | .then(r => Repository.get('name')) 65 | .then(r => expect(r).to.equal('david')) 66 | }) 67 | 68 | it('should return a null for a key that was pulled', function () { 69 | return Repository.get('age') 70 | .then(r => expect(r).to.equal(null)) 71 | }) 72 | 73 | it('should return default value for a key that does not exist', function () { 74 | return Repository.get('country', 'usa') 75 | .then(r => expect(r).to.equal('usa')) 76 | }) 77 | }) 78 | 79 | describe('add', function () { 80 | it('should return false when adding key that is in the cache and should not change value of existing one', function () { 81 | return Repository.add('name', 'john', 1) 82 | .then(r => expect(r).to.equal(false)) 83 | .then(r => Repository.get('name')) 84 | .then(r => expect(r).to.equal('david')) 85 | }) 86 | 87 | it('should return a false when adding a key with 0 minutes expiration and should not change value of existing one', function () { 88 | return Repository.add('name', 'john', 0) 89 | .then(r => expect(r).to.equal(false)) 90 | .then(r => Repository.get('name')) 91 | .then(r => expect(r).to.equal('david')) 92 | }) 93 | 94 | it('should return true when adding key that is not in the cache and should add it', function () { 95 | return Repository.add('state', 'california', 1) 96 | .then(r => expect(r).to.equal(true)) 97 | .then(r => Repository.get('state')) 98 | .then(r => expect(r).to.equal('california')) 99 | }) 100 | }) 101 | 102 | describe('remember', function () { 103 | it('should return async function value and cache its value for a key that is not in the cache', function () { 104 | return Repository.put('firstname', 'david', 1) 105 | .then(r => Repository.put('lastname', 'king', 1)) 106 | .then(r => Repository.remember('test', 1, async () => { 107 | const temp = await Repository.get('firstname') 108 | const temp2 = await Repository.get('lastname') 109 | return temp + ' ' + temp2 110 | })) 111 | .then(r => expect(r).to.equal('david king')) 112 | .then(r => Repository.get('test')) 113 | .then(r => expect(r).to.equal('david king')) 114 | }) 115 | 116 | it('should return promise value and cache its value for a key that is not in the cache', function () { 117 | return Repository.put('firstname2', 'david', 1) 118 | .then(r => Repository.put('lastname2', 'king', 1)) 119 | .then(r => Repository.remember('test2', 1, function () { 120 | return Repository.get('firstname2') 121 | .then(r2 => Repository.get('lastname2') 122 | .then(r3 => r2 + ' ' + r3)) 123 | })) 124 | .then(r => expect(r).to.equal('david king')) 125 | .then(r => Repository.get('test2')) 126 | .then(r => expect(r).to.equal('david king')) 127 | }) 128 | }) 129 | }) 130 | -------------------------------------------------------------------------------- /test/TestHelpers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * adonis-cache 5 | * 6 | * (c) Hany El Nokaly 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | /** 13 | * Returns a promise that resolves after given milliseconds are passed 14 | * 15 | * @param {int} ms 16 | * @return {Promise} 17 | */ 18 | function sleep(ms) { 19 | return new Promise(resolve => setTimeout(resolve, ms)); 20 | } 21 | 22 | module.exports = {sleep} 23 | --------------------------------------------------------------------------------