@athenna/cache
Advanced tools
Sorry, the diff of this file is not supported yet
| version: '3' | ||
| services: | ||
| redis: | ||
| container_name: athenna_redis | ||
| image: redis | ||
| ports: | ||
| - '6379:6379' |
| /** | ||
| * @athenna/cache | ||
| * | ||
| * (c) João Lenon <lenon@athenna.io> | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
| declare const _default: { | ||
| container_name: string; | ||
| image: string; | ||
| ports: string[]; | ||
| }; | ||
| export default _default; |
| /** | ||
| * @athenna/cache | ||
| * | ||
| * (c) João Lenon <lenon@athenna.io> | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
| export default { | ||
| container_name: 'athenna_redis', | ||
| image: 'redis', | ||
| ports: ['6379:6379'] | ||
| }; |
| /** | ||
| * @athenna/cache | ||
| * | ||
| * (c) João Lenon <lenon@athenna.io> | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
| export default { | ||
| container_name: 'athenna_redis', | ||
| image: 'redis', | ||
| ports: ['6379:6379'] | ||
| } |
| /** | ||
| * @athenna/cache | ||
| * | ||
| * (c) João Lenon <lenon@athenna.io> | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
| import type { RedisClientType } from 'redis'; | ||
| import type { StoreOptions } from '#src/types'; | ||
| import { Driver } from '#src/cache/drivers/Driver'; | ||
| export declare class RedisDriver extends Driver<RedisClientType> { | ||
| /** | ||
| * Redis database URL. | ||
| */ | ||
| url: string; | ||
| /** | ||
| * Redis database socket. | ||
| */ | ||
| socket: any; | ||
| /** | ||
| * Redis database host. | ||
| */ | ||
| host: string; | ||
| /** | ||
| * Redis database port. | ||
| */ | ||
| port: number; | ||
| /** | ||
| * Redis connection protocol. | ||
| */ | ||
| protocol: string; | ||
| /** | ||
| * Redis database username. | ||
| */ | ||
| username: string; | ||
| /** | ||
| * Redis database password. | ||
| */ | ||
| password: string; | ||
| /** | ||
| * Redis database number. | ||
| */ | ||
| database: number; | ||
| constructor(store: string, client?: any, options?: StoreOptions['options']); | ||
| /** | ||
| * Connect to client. | ||
| */ | ||
| connect(options: StoreOptions): void; | ||
| /** | ||
| * Close the connection with the client in this instance. | ||
| */ | ||
| close(): Promise<void>; | ||
| /** | ||
| * Reset all data defined inside cache. | ||
| */ | ||
| truncate(): Promise<void>; | ||
| /** | ||
| * Get a value from the cache. | ||
| */ | ||
| get<T = any>(key: string, defaultValue?: T): Promise<T>; | ||
| /** | ||
| * Validate if a value exists in the cache. | ||
| */ | ||
| has(key: string): Promise<boolean>; | ||
| /** | ||
| * Set a value in the cache. | ||
| */ | ||
| set(key: string, value: any, options?: { | ||
| ttl?: number; | ||
| }): Promise<void>; | ||
| /** | ||
| * Get a value from the cache and delete it at | ||
| * the same time. | ||
| */ | ||
| pull<T = any>(key: string): Promise<T>; | ||
| /** | ||
| * Delete a value from the cache. | ||
| */ | ||
| delete(key: string): Promise<void>; | ||
| } |
| /** | ||
| * @athenna/cache | ||
| * | ||
| * (c) João Lenon <lenon@athenna.io> | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
| import { Log } from '@athenna/logger'; | ||
| import { Config } from '@athenna/config'; | ||
| import { Driver } from '#src/cache/drivers/Driver'; | ||
| import { Is, Parser, Options } from '@athenna/common'; | ||
| import { StoreFactory } from '#src/factories/StoreFactory'; | ||
| export class RedisDriver extends Driver { | ||
| constructor(store, client = null, options) { | ||
| super(store, client, options); | ||
| const config = Config.get(`cache.stores.${store}`); | ||
| this.url = options?.url || config?.url; | ||
| this.host = options?.host || config?.host; | ||
| this.port = options?.port || config?.port || 6379; | ||
| this.socket = options?.socket || config?.socket; | ||
| this.username = options?.username || config?.username; | ||
| this.password = options?.password || config?.password; | ||
| this.database = options?.database || config?.database || 0; | ||
| this.protocol = options?.protocol || config?.protocol || 'redis'; | ||
| if (!this.url) { | ||
| this.url = Parser.connectionObjToDbUrl({ | ||
| host: this.host, | ||
| port: this.port, | ||
| user: this.username, | ||
| password: this.password, | ||
| protocol: this.protocol, | ||
| database: '' | ||
| }); | ||
| this.url = `${this.url.slice(0, -1)}?database=${this.database}`; | ||
| } | ||
| } | ||
| /** | ||
| * Connect to client. | ||
| */ | ||
| connect(options) { | ||
| options = Options.create(options, { | ||
| force: false, | ||
| connect: true, | ||
| saveOnFactory: true | ||
| }); | ||
| if (!options.connect) { | ||
| return; | ||
| } | ||
| if (this.isConnected && !options.force) { | ||
| return; | ||
| } | ||
| const { createClient } = this.getRedis(); | ||
| this.client = createClient({ url: this.url, socket: this.socket }); | ||
| this.client | ||
| .connect() | ||
| .then(() => { | ||
| if (Config.is('rc.bootLogs', true)) { | ||
| Log.channelOrVanilla('application').success(`Successfully connected to ({yellow} ${this.store}) cache store`); | ||
| } | ||
| }) | ||
| .catch(err => { | ||
| console.error(err); | ||
| }); | ||
| this.isConnected = true; | ||
| this.isSavedOnFactory = options.saveOnFactory; | ||
| if (options.saveOnFactory) { | ||
| StoreFactory.setClient(this.store, this.client); | ||
| } | ||
| } | ||
| /** | ||
| * Close the connection with the client in this instance. | ||
| */ | ||
| async close() { | ||
| if (!this.client || !this.isConnected) { | ||
| return; | ||
| } | ||
| try { | ||
| await this.client.quit(); | ||
| } | ||
| finally { | ||
| this.client = null; | ||
| this.isConnected = false; | ||
| StoreFactory.setClient(this.store, null); | ||
| } | ||
| } | ||
| /** | ||
| * Reset all data defined inside cache. | ||
| */ | ||
| async truncate() { | ||
| let cursor = '0'; | ||
| do { | ||
| const response = await this.client.scan(cursor, { | ||
| COUNT: 500, | ||
| MATCH: `${this.prefix}*` | ||
| }); | ||
| cursor = response.cursor; | ||
| if (response.keys.length) { | ||
| await this.client.del(response.keys); | ||
| } | ||
| } while (cursor !== '0'); | ||
| } | ||
| /** | ||
| * Get a value from the cache. | ||
| */ | ||
| async get(key, defaultValue) { | ||
| const value = await this.client.get(this.getCacheKey(key)); | ||
| if (Is.Null(value) || Is.Undefined(value)) { | ||
| return defaultValue; | ||
| } | ||
| return value; | ||
| } | ||
| /** | ||
| * Validate if a value exists in the cache. | ||
| */ | ||
| async has(key) { | ||
| const value = await this.get(key); | ||
| return !!value; | ||
| } | ||
| /** | ||
| * Set a value in the cache. | ||
| */ | ||
| async set(key, value, options) { | ||
| await this.client.set(this.getCacheKey(key), value, { | ||
| expiration: { | ||
| type: 'EX', | ||
| value: Math.ceil((options?.ttl || this.ttl) / 1000) | ||
| } | ||
| }); | ||
| } | ||
| /** | ||
| * Get a value from the cache and delete it at | ||
| * the same time. | ||
| */ | ||
| async pull(key) { | ||
| const value = await this.get(key); | ||
| await this.delete(key); | ||
| return value; | ||
| } | ||
| /** | ||
| * Delete a value from the cache. | ||
| */ | ||
| async delete(key) { | ||
| await this.client.del(this.getCacheKey(key)); | ||
| } | ||
| } |
+33
-1
@@ -9,7 +9,9 @@ /** | ||
| */ | ||
| import { File, Path } from '@athenna/common'; | ||
| import { BaseConfigurer } from '@athenna/artisan'; | ||
| import { File, Path, Parser, Module } from '@athenna/common'; | ||
| export default class CacheConfigurer extends BaseConfigurer { | ||
| async configure() { | ||
| const stores = await this.prompt.checkbox('Which cache stores do you plan to use?', ['redis', 'memory']); | ||
| const task = this.logger.task(); | ||
| const hasSelectedRedis = stores.find(store => store === 'redis'); | ||
| task.addPromise(`Create cache.${Path.ext()} config file`, () => { | ||
@@ -23,2 +25,32 @@ return new File('./cache').copy(Path.config(`cache.${Path.ext()}`)); | ||
| }); | ||
| task.addPromise('Update .env, .env.test and .env.example', () => { | ||
| let envs = '\nCACHE_STORE=memory\n'; | ||
| if (hasSelectedRedis) { | ||
| envs += 'REDIS_URL=redis://localhost:6379?database=0\n'; | ||
| } | ||
| return new File(Path.pwd('.env'), '') | ||
| .append(envs) | ||
| .then(() => new File(Path.pwd('.env.test'), '').append(envs)) | ||
| .then(() => new File(Path.pwd('.env.example'), '').append(envs)); | ||
| }); | ||
| if (hasSelectedRedis) { | ||
| task.addPromise('Add service to docker-compose.yml file', async () => { | ||
| const hasDockerCompose = await File.exists(Path.pwd('docker-compose.yml')); | ||
| if (hasDockerCompose) { | ||
| const docker = await new File(Path.pwd('docker-compose.yml')).getContentAsYaml(); | ||
| docker.services.redis = await Module.get(import('./docker/redis/service.js')); | ||
| return new File(Path.pwd('docker-compose.yml')).setContent(Parser.objectToYamlString(docker)); | ||
| } | ||
| return new File(`./docker/redis/file.yml`).copy(Path.pwd('docker-compose.yml')); | ||
| }); | ||
| } | ||
| const libraries = { | ||
| redis: ['redis'], | ||
| memory: ['lru-cache'] | ||
| }; | ||
| task.addPromise(`Install ${stores.join(', ')} libraries`, () => { | ||
| return stores.athenna.concurrently(store => { | ||
| return this.npm.install(libraries[store]); | ||
| }); | ||
| }); | ||
| await task.run(); | ||
@@ -25,0 +57,0 @@ console.log(); |
+57
-1
@@ -10,8 +10,14 @@ /** | ||
| import { File, Path } from '@athenna/common' | ||
| import { BaseConfigurer } from '@athenna/artisan' | ||
| import { File, Path, Parser, Module } from '@athenna/common' | ||
| export default class CacheConfigurer extends BaseConfigurer { | ||
| public async configure() { | ||
| const stores = await this.prompt.checkbox( | ||
| 'Which cache stores do you plan to use?', | ||
| ['redis', 'memory'] | ||
| ) | ||
| const task = this.logger.task() | ||
| const hasSelectedRedis = stores.find(store => store === 'redis') | ||
@@ -28,2 +34,52 @@ task.addPromise(`Create cache.${Path.ext()} config file`, () => { | ||
| task.addPromise('Update .env, .env.test and .env.example', () => { | ||
| let envs = '\nCACHE_STORE=memory\n' | ||
| if (hasSelectedRedis) { | ||
| envs += 'REDIS_URL=redis://localhost:6379?database=0\n' | ||
| } | ||
| return new File(Path.pwd('.env'), '') | ||
| .append(envs) | ||
| .then(() => new File(Path.pwd('.env.test'), '').append(envs)) | ||
| .then(() => new File(Path.pwd('.env.example'), '').append(envs)) | ||
| }) | ||
| if (hasSelectedRedis) { | ||
| task.addPromise('Add service to docker-compose.yml file', async () => { | ||
| const hasDockerCompose = await File.exists( | ||
| Path.pwd('docker-compose.yml') | ||
| ) | ||
| if (hasDockerCompose) { | ||
| const docker = await new File( | ||
| Path.pwd('docker-compose.yml') | ||
| ).getContentAsYaml() | ||
| docker.services.redis = await Module.get( | ||
| import('./docker/redis/service.js') | ||
| ) | ||
| return new File(Path.pwd('docker-compose.yml')).setContent( | ||
| Parser.objectToYamlString(docker) | ||
| ) | ||
| } | ||
| return new File(`./docker/redis/file.yml`).copy( | ||
| Path.pwd('docker-compose.yml') | ||
| ) | ||
| }) | ||
| } | ||
| const libraries = { | ||
| redis: ['redis'], | ||
| memory: ['lru-cache'] | ||
| } | ||
| task.addPromise(`Install ${stores.join(', ')} libraries`, () => { | ||
| return stores.athenna.concurrently(store => { | ||
| return this.npm.install(libraries[store]) | ||
| }) | ||
| }) | ||
| await task.run() | ||
@@ -30,0 +86,0 @@ |
+4
-5
| { | ||
| "name": "@athenna/cache", | ||
| "version": "5.1.0", | ||
| "version": "5.2.0", | ||
| "description": "The cache handler for Athenna Framework.", | ||
@@ -73,3 +73,5 @@ "license": "MIT", | ||
| "lint-staged": "^12.5.0", | ||
| "prettier": "^2.8.8" | ||
| "lru-cache": "^11.1.0", | ||
| "prettier": "^2.8.8", | ||
| "redis": "^5.8.1" | ||
| }, | ||
@@ -168,6 +170,3 @@ "c8": { | ||
| } | ||
| }, | ||
| "dependencies": { | ||
| "lru-cache": "^11.1.0" | ||
| } | ||
| } |
@@ -11,3 +11,4 @@ /** | ||
| import type { StoreOptions } from '#src/types/StoreOptions'; | ||
| import { MemoryDriver } from '#src/cache/drivers/MemoryDriver'; | ||
| import type { RedisDriver } from '#src/cache/drivers/RedisDriver'; | ||
| import type { MemoryDriver } from '#src/cache/drivers/MemoryDriver'; | ||
| import type { Driver as DriverImpl } from '#src/cache/drivers/Driver'; | ||
@@ -22,3 +23,3 @@ export declare class CacheImpl<Driver extends DriverImpl = any> extends Macroable { | ||
| */ | ||
| driver: Driver; | ||
| driver: RedisDriver | MemoryDriver; | ||
| /** | ||
@@ -28,4 +29,5 @@ * Creates a new instance of CacheImpl. | ||
| constructor(athennaCacheOpts?: StoreOptions); | ||
| store(store: 'redis', options?: StoreOptions): CacheImpl<RedisDriver>; | ||
| store(store: 'memory', options?: StoreOptions): CacheImpl<MemoryDriver>; | ||
| store(store: 'memory' | string): CacheImpl<MemoryDriver>; | ||
| store(store: 'redis' | 'memory' | string, options?: StoreOptions): CacheImpl<RedisDriver> | CacheImpl<MemoryDriver>; | ||
| /** | ||
@@ -104,3 +106,3 @@ * Verify if client is already connected. | ||
| */ | ||
| pull(key: string): Promise<string>; | ||
| pull(key: string): Promise<any>; | ||
| /** | ||
@@ -107,0 +109,0 @@ * Delete a value from the cache my its key. |
@@ -11,3 +11,2 @@ /** | ||
| import { StoreFactory } from '#src/factories/StoreFactory'; | ||
| import { MemoryDriver } from '#src/cache/drivers/MemoryDriver'; | ||
| export class CacheImpl extends Macroable { | ||
@@ -14,0 +13,0 @@ /** |
@@ -32,2 +32,6 @@ /** | ||
| /** | ||
| * Set the cache prefix of the driver. | ||
| */ | ||
| prefix: string; | ||
| /** | ||
| * Define the max number of items that could be inserted in the cache. | ||
@@ -45,2 +49,10 @@ */ | ||
| /** | ||
| * Import the redis module if it exists. | ||
| */ | ||
| getRedis(): any; | ||
| /** | ||
| * Import the lru-cache module if it exists. | ||
| */ | ||
| getLruCache(): any; | ||
| /** | ||
| * Clone the driver instance. | ||
@@ -58,2 +70,11 @@ */ | ||
| /** | ||
| * Sanitize the cache prefix by removing any trailing colons. | ||
| */ | ||
| sanitizePrefix(prefix: string): string; | ||
| /** | ||
| * Automatically set the cache prefix if it exists. | ||
| * Otherwise just return the key defined. | ||
| */ | ||
| getCacheKey(key: string): string; | ||
| /** | ||
| * Connect to client. | ||
@@ -60,0 +81,0 @@ */ |
@@ -10,2 +10,3 @@ /** | ||
| import { Config } from '@athenna/config'; | ||
| import { Module } from '@athenna/common'; | ||
| export class Driver { | ||
@@ -32,2 +33,3 @@ /** | ||
| this.maxEntrySize = options?.maxEntrySize || config.maxEntrySize; | ||
| this.prefix = this.sanitizePrefix(options?.prefix || config?.prefix); | ||
| this.store = store; | ||
@@ -41,2 +43,16 @@ if (client) { | ||
| /** | ||
| * Import the redis module if it exists. | ||
| */ | ||
| getRedis() { | ||
| const require = Module.createRequire(import.meta.url); | ||
| return require('redis'); | ||
| } | ||
| /** | ||
| * Import the lru-cache module if it exists. | ||
| */ | ||
| getLruCache() { | ||
| const require = Module.createRequire(import.meta.url); | ||
| return require('lru-cache'); | ||
| } | ||
| /** | ||
| * Clone the driver instance. | ||
@@ -62,2 +78,21 @@ */ | ||
| } | ||
| /** | ||
| * Sanitize the cache prefix by removing any trailing colons. | ||
| */ | ||
| sanitizePrefix(prefix) { | ||
| if (!prefix) { | ||
| return ''; | ||
| } | ||
| return prefix.replace(/:+$/, ''); | ||
| } | ||
| /** | ||
| * Automatically set the cache prefix if it exists. | ||
| * Otherwise just return the key defined. | ||
| */ | ||
| getCacheKey(key) { | ||
| if (this.prefix) { | ||
| return `${this.prefix}:${key}`; | ||
| } | ||
| return key; | ||
| } | ||
| } |
@@ -9,3 +9,3 @@ /** | ||
| */ | ||
| import { LRUCache } from 'lru-cache'; | ||
| import type { LRUCache } from 'lru-cache'; | ||
| import type { StoreOptions } from '#src/types'; | ||
@@ -12,0 +12,0 @@ import { Driver } from '#src/cache/drivers/Driver'; |
@@ -9,3 +9,2 @@ /** | ||
| */ | ||
| import { LRUCache } from 'lru-cache'; | ||
| import { Driver } from '#src/cache/drivers/Driver'; | ||
@@ -30,2 +29,3 @@ import { Parser, Options, Is } from '@athenna/common'; | ||
| } | ||
| const { LRUCache } = this.getLruCache(); | ||
| this.client = new LRUCache({ | ||
@@ -32,0 +32,0 @@ max: this.maxItems, |
@@ -11,2 +11,3 @@ /** | ||
| import type { Driver } from '#src/cache/drivers/Driver'; | ||
| import { RedisDriver } from '#src/cache/drivers/RedisDriver'; | ||
| import { MemoryDriver } from '#src/cache/drivers/MemoryDriver'; | ||
@@ -22,4 +23,7 @@ export declare class StoreFactory { | ||
| static drivers: Map<string, any>; | ||
| static fabricate(store: 'redis', options?: StoreOptions['options']): RedisDriver; | ||
| static fabricate(store: 'redis' | string, options?: StoreOptions['options']): RedisDriver; | ||
| static fabricate(con: 'memory', options?: StoreOptions['options']): MemoryDriver; | ||
| static fabricate(con: 'memory' | string, options?: StoreOptions['options']): MemoryDriver; | ||
| static fabricate(con: 'redis' | 'memory' | string, options?: StoreOptions['options']): RedisDriver | MemoryDriver; | ||
| /** | ||
@@ -26,0 +30,0 @@ * Verify if client is present on a driver connection. |
@@ -10,2 +10,3 @@ /** | ||
| import { debug } from '#src/debug'; | ||
| import { RedisDriver } from '#src/cache/drivers/RedisDriver'; | ||
| import { MemoryDriver } from '#src/cache/drivers/MemoryDriver'; | ||
@@ -22,3 +23,5 @@ import { NotFoundDriverException } from '#src/exceptions/NotFoundDriverException'; | ||
| */ | ||
| static { this.drivers = new Map().set('memory', MemoryDriver); } | ||
| static { this.drivers = new Map() | ||
| .set('redis', RedisDriver) | ||
| .set('memory', MemoryDriver); } | ||
| /** | ||
@@ -25,0 +28,0 @@ * Fabricate a new connection for a specific driver. |
@@ -51,2 +51,9 @@ /** | ||
| /** | ||
| * Define a prefix for the store. By default, prefix | ||
| * will always be used in front of your keys if it exists. | ||
| * | ||
| * @default Config.get(`cache.stores.${store}.prefix`) | ||
| */ | ||
| prefix?: string; | ||
| /** | ||
| * Define the max number of items that could be inserted in the cache. | ||
@@ -53,0 +60,0 @@ * |
52668
37.25%0
-100%37
23.33%1510
37.9%23
9.52%- Removed
- Removed