From 87cb29aa36e1c81b20dd556ae1eba5f6c003935e Mon Sep 17 00:00:00 2001 From: Vladyslav Parashchenko Date: Wed, 24 Feb 2021 20:54:09 +0200 Subject: [PATCH 1/3] feat: implement sharable cache by redis as a transport --- .env.example | 3 +- .eslintrc.json | 3 + README.md | 40 ++++ adonis-typings/cache-events.ts | 7 + adonis-typings/container.ts | 9 + adonis-typings/index.ts | 2 + adonis-typings/sharable-cache.ts | 27 +++ docker-compose.yml | 8 +- japaFile.js | 20 +- npm-audit.html | 6 +- package.json | 38 ++-- providers/AdonisCacheProvider.ts | 65 +----- providers/AdonisSharableCacheProvider.ts | 33 +++ providers/BaseAdonisCacheProvider.ts | 77 +++++++ sharable-cache-provider.ts | 1 + src/CacheManager.ts | 18 +- src/CacheStorages/InMemoryStorage.ts | 1 - src/SharableCacheManager.ts | 126 +++++++++++ .../RedisCacheDistributorTransport.ts | 39 ++++ src/Utils/TimeConverter.ts | 62 ++++++ test-helpers/TestAdonisApp/index.ts | 12 +- .../testAdonisApp/.adonisrc.json | 2 +- test/fixtures/memcached-test-config.ts | 3 +- .../sharable-cache/sharable-cache-e2e.spec.ts | 207 ++++++++++++++++++ ...le-cache-manager-command-execution.spec.ts | 202 +++++++++++++++++ ...arable-cache-manager-command-queue.spec.ts | 193 ++++++++++++++++ tsconfig.eslint.json | 4 + tsconfig.json | 7 +- 28 files changed, 1089 insertions(+), 126 deletions(-) create mode 100644 adonis-typings/container.ts create mode 100644 adonis-typings/sharable-cache.ts create mode 100644 providers/AdonisSharableCacheProvider.ts create mode 100644 providers/BaseAdonisCacheProvider.ts create mode 100644 sharable-cache-provider.ts create mode 100644 src/SharableCacheManager.ts create mode 100644 src/SharableCacheTransports/RedisCacheDistributorTransport.ts create mode 100644 src/Utils/TimeConverter.ts create mode 100644 test/sharable-cache/sharable-cache-e2e.spec.ts create mode 100644 test/sharable-cache/sharable-cache-manager-command-execution.spec.ts create mode 100644 test/sharable-cache/sharable-cache-manager-command-queue.spec.ts create mode 100644 tsconfig.eslint.json diff --git a/.env.example b/.env.example index 50c940d..64f1045 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,2 @@ REDIS_PORT=6379 -MEMCHACHED_PORT=11211 -MEMCACHED_SERVER_URL="localhost:${MEMCACHED_PORT}" +MEMCACHED_SERVER_URL="localhost:11211" diff --git a/.eslintrc.json b/.eslintrc.json index 540d568..03b6bc1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,5 +11,8 @@ "prettier/prettier": [ "error" ] + }, + "parserOptions": { + "project": "./tsconfig.eslint.json" } } diff --git a/README.md b/README.md index 172998d..090d58d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Supported cache storages: - [Cache events](#cache-events) - [Cache tags](#cache-tags) - [Cache record TTL](#cache-record-ttl) +- [Sharable cache](#sharable-cache) @@ -334,6 +335,45 @@ Or you can set record ttl as function parameter: Your value will be transformed to milliseconds using time units which configured by **ttlUnits** parameter in your cache config. +# Sharable cache +When you need to share cache between several instances you can enable sharable mode. +For enabling you should add sharable cache provider: +```json +{ + "providers": [ + "./providers/AppProvider", + "adonis5-cache", + "adonis5-cache/sharable-cache-provider" + ] +} +``` +and setup your `config/cache.ts` for using shared cache +```js +{ +sharedCacheConfig: { + isSharingEnabled: true, + syncInterval: 2000 + } +} +``` +- isSharingEnabled - responds for enabling sharing mode for cache +- syncInterval - interval between running sync operation. Use milliseconds unit for this option + + +Cache synchronized via redis as transport layer. So you need to install [adonis-redis](https://www.npmjs.com/package/@adonisjs/redis/v/alpha) package for using sharable cache. +```bash +npm i @adonisjs/redis@alpha +``` + +For manual enabling and disabling synchronization you can call special methods on cache manager instance +```js +import CacheManager from "@ioc:Adonis/Addons/Adonis5-Cache"; + +CacheManager.stopSynchronization() + +CacheManager.runSynchronization() +``` + [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript [typescript-url]: "typescript" diff --git a/adonis-typings/cache-events.ts b/adonis-typings/cache-events.ts index ba1e4ff..ac4af56 100644 --- a/adonis-typings/cache-events.ts +++ b/adonis-typings/cache-events.ts @@ -63,6 +63,13 @@ declare module '@ioc:Adonis/Addons/Adonis5-Cache' { ttlUnits: TtlUnits enabledEvents: CacheEventsConfig + sharedCacheConfig?: SharedCacheConfig + } + + interface SharedCacheConfig { + isSharingEnabled: boolean + syncInterval: number + rootNode: boolean } } diff --git a/adonis-typings/container.ts b/adonis-typings/container.ts new file mode 100644 index 0000000..1ca658e --- /dev/null +++ b/adonis-typings/container.ts @@ -0,0 +1,9 @@ +declare module '@ioc:Adonis/Core/Application' { + import { CacheManagerContract } from '@ioc:Adonis/Addons/Adonis5-Cache' + import { SharableCacheManagerContract } from '@ioc:Adonis/Addons/Adonis5-SharableCache' + + export interface ContainerBindings { + 'Adonis/Addons/Adonis5-Cache': CacheManagerContract + 'Adonis/Addons/Adonis5-SharableCache': SharableCacheManagerContract + } +} diff --git a/adonis-typings/index.ts b/adonis-typings/index.ts index 47efa29..91839ea 100644 --- a/adonis-typings/index.ts +++ b/adonis-typings/index.ts @@ -1,2 +1,4 @@ /// /// +/// +/// diff --git a/adonis-typings/sharable-cache.ts b/adonis-typings/sharable-cache.ts new file mode 100644 index 0000000..7241f86 --- /dev/null +++ b/adonis-typings/sharable-cache.ts @@ -0,0 +1,27 @@ +declare module '@ioc:Adonis/Addons/Adonis5-SharableCache' { + import { CacheManagerContract } from '@ioc:Adonis/Addons/Adonis5-Cache' + + export type CacheCommand = { + method: string + args: unknown[] + isReturnThis: boolean + createdAt: string + } + + export interface SharableCacheManagerContract extends Omit { + transport: SharableCacheTransportContract + runSynchronization(): void + stopSynchronization(): void + addCommandToQueue(method: string, args: unknown[], isReturnThis: boolean): void + isSharingEnabled: boolean + } + + export interface SharableCacheTransportContract { + sync(cacheCommands: CacheCommand[]): void + subscribeForUpdates(handleCommands: (commands: CacheCommand[]) => Promise): void + } + + const SharableCacheManager: SharableCacheManagerContract + + export default SharableCacheManager +} diff --git a/docker-compose.yml b/docker-compose.yml index a679952..2dd65ba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,8 @@ services: image: 'redis:alpine' ports: - $REDIS_PORT:6379 - memcached: - image: 'memcached:alpine' - ports: - - $MEMCACHED_PORT:11211 + memcached: + image: 'memcached:alpine' + ports: + - $MEMCACHED_PORT:11211 diff --git a/japaFile.js b/japaFile.js index e87adea..582e740 100644 --- a/japaFile.js +++ b/japaFile.js @@ -1,24 +1,6 @@ const { configure } = require('japa') const { argv } = require('yargs') -const tsnode = require('ts-node') -const { iocTransformer } = require('@adonisjs/ioc-transformer') -const { files: typingsFiles } = require('./tsconfig.json') - -const testFrameworkConfiguration = { - aliases: { - App: 'app', - Contracts: 'contracts', - Config: 'config', - Database: 'database', - }, -} - -tsnode.register({ - transformers: { - after: [iocTransformer(require('typescript/lib/typescript'), testFrameworkConfiguration)], - }, - files: typingsFiles, -}) +require('@adonisjs/require-ts/build/register') const { files = ['test/**/*.spec.ts'], grep } = argv configure({ diff --git a/npm-audit.html b/npm-audit.html index ef76f19..58bc961 100644 --- a/npm-audit.html +++ b/npm-audit.html @@ -12,7 +12,7 @@ integrity="sha384-wXznGJNEXNG1NFsbm0ugrLFMQPWswR3lds2VeinahP8N0zJw9VWSopbjv2x7WCvX" crossorigin="anonymous"> + href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.3.1/build/styles/atom-one-dark.min.css"> NPM Audit Report @@ -47,7 +47,7 @@
- 3 + 4

Dependencies

@@ -55,7 +55,7 @@
- February 7th 2021, 12:16:25 am + February 24th 2021, 6:54:12 pm

Last updated

diff --git a/package.json b/package.json index 35eb664..6efd681 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,8 @@ "build/providers", "build/templates", "build/index.d.ts", - "build/index.js" + "build/index.js", + "build/sharable-cache-provider.js" ], "adonisjs": { "types": "adonis5-cache", @@ -96,7 +97,8 @@ "dependencies": { "dayjs": "^1.8.34", "ms": "^2.1.2", - "ramda": "^0.27.1" + "ramda": "^0.27.1", + "uuid": "^8.3.2" }, "peerDependencies": { "adonis5-memcached-client": "^1.0.5", @@ -104,15 +106,18 @@ "@adonisjs/events": "^4.0.1" }, "devDependencies": { - "@adonisjs/ace": "^6.9.3", - "@adonisjs/core": "^5.0.4-preview-rc-2.1", - "@adonisjs/fold": "^7.0.9", - "@adonisjs/ioc-transformer": "^1.0.2", + "@adonisjs/core": "^5.0.5-canary-rc-2", + "@adonisjs/events": "^6.0.0", "@adonisjs/logger": "^2.0.7", - "@adonisjs/mrm-preset": "^2.4.0", + "@adonisjs/mrm-preset": "^3.0.0", + "@adonisjs/redis": "^5.0.9", + "@adonisjs/require-ts": "^2.0.2", "@poppinss/dev-utils": "^1.0.11", - "@types/node": "^14.14.9", - "@types/ramda": "^0.27.19", + "@types/chai": "^4.2.15", + "@types/memcached": "^2.2.6", + "@types/node": "^14.14.31", + "@types/ramda": "^0.27.38", + "adonis5-memcached-client": "^1.0.5", "chai": "^4.2.0", "commitizen": "^4.1.2", "cpx": "^1.5.0", @@ -121,15 +126,15 @@ "del-cli": "^3.0.1", "doctoc": "^1.4.0", "dotenv": "^8.2.0", - "eslint": "^7.5.0", + "eslint": "^7.20.0", "eslint-config-prettier": "^6.11.0", - "eslint-plugin-adonis": "^1.0.14", + "eslint-plugin-adonis": "^1.2.1", "eslint-plugin-prettier": "^3.1.4", "get-port": "^5.1.1", "github-label-sync": "^2.0.0", "husky": "^4.2.5", "japa": "^3.1.1", - "mrm": "^2.3.3", + "mrm": "^2.5.19", "np": "^6.3.2", "npm-audit-html": "^1.4.1", "pino-pretty": "^4.1.0", @@ -138,12 +143,7 @@ "source-map-support": "^0.5.19", "supertest": "^4.0.2", "ts-mockito": "^2.6.1", - "ts-node": "^8.10.2", - "typescript": "^3.9.7", - "yargs": "^15.4.1", - "@types/memcached": "^2.2.6", - "adonis5-memcached-client": "^1.0.5", - "@adonisjs/redis": "^5.0.9", - "@adonisjs/events": "^4.0.1" + "typescript": "^4.1.5", + "yargs": "^15.4.1" } } diff --git a/providers/AdonisCacheProvider.ts b/providers/AdonisCacheProvider.ts index 692f0bc..b11ac60 100644 --- a/providers/AdonisCacheProvider.ts +++ b/providers/AdonisCacheProvider.ts @@ -1,64 +1,13 @@ -import { IocContract } from '@adonisjs/fold/build' import CacheManager from '../src/CacheManager' -import RedisStorage from '../src/CacheStorages/RedisStorage' -import InMemoryStorage from '../src/CacheStorages/InMemoryStorage' -import { EmitterContract } from '@ioc:Adonis/Core/Event' -import { ConfigContract } from '@ioc:Adonis/Core/Config' -import { CacheConfig, CacheManagerContract } from '@ioc:Adonis/Addons/Adonis5-Cache' -import { RedisManagerContract } from '@ioc:Adonis/Addons/Redis' -import { ContainerBindings } from '@ioc:Adonis/Core/Application' -import { AdonisMemcachedClientContract } from '@ioc:Adonis/Addons/Adonis5-MemcachedClient' -import MemcachedStorage from '../src/CacheStorages/MemcachedStorage' +import { ConstructorParams } from '@ioc:Adonis/Addons/Adonis5-Cache' +import BaseAdonisCacheProvider from './BaseAdonisCacheProvider' -export default class AdonisCacheProvider { - constructor(protected container: IocContract) {} - - public register(): void { - this.container.singleton('Adonis/Addons/Adonis5-Cache', () => { - const eventEmitter: EmitterContract = this.container.use('Adonis/Core/Event') - const config: ConfigContract = this.container.use('Adonis/Core/Config') - - return new CacheManager({ - eventEmitter, - config: config.get('cache'), - }) - }) - } - - public boot(): void { - const cache: CacheManagerContract = this.container.use('Adonis/Addons/Adonis5-Cache') - const cacheConfig: CacheConfig = this.container.use('Adonis/Core/Config').get('cache') - - if (cacheConfig.enabledCacheStorages.includes('redis')) { - this.registerRedisCacheStorage(cache) - } - - if (cacheConfig.enabledCacheStorages.includes('in-memory')) { - this.registerInMemoryCacheStorage(cache) - } - - if (cacheConfig.enabledCacheStorages.includes('in-memory')) { - this.registerInMemoryCacheStorage(cache) - } - - if (cacheConfig.enabledCacheStorages.includes('memcached')) { - this.registerMemcachedCacheStorage(cache) - } - } - - private registerRedisCacheStorage(cache: CacheManagerContract) { - const redis: RedisManagerContract = this.container.use('Adonis/Addons/Redis') - cache.registerStorage('redis', new RedisStorage(redis)) - } - - private registerInMemoryCacheStorage(cache: CacheManagerContract) { - cache.registerStorage('in-memory', new InMemoryStorage()) +export default class AdonisCacheProvider extends BaseAdonisCacheProvider { + public get providerAlias(): string { + return 'Adonis/Addons/Adonis5-Cache' } - private registerMemcachedCacheStorage(cache: CacheManagerContract) { - const memcachedClient: AdonisMemcachedClientContract = this.container.use( - 'Adonis/Addons/Adonis5-MemcachedClient' - ) - cache.registerStorage('memcached', new MemcachedStorage(memcachedClient)) + public get cacheManagerClass(): { new (args: ConstructorParams): CacheManager } { + return CacheManager } } diff --git a/providers/AdonisSharableCacheProvider.ts b/providers/AdonisSharableCacheProvider.ts new file mode 100644 index 0000000..a0ddc65 --- /dev/null +++ b/providers/AdonisSharableCacheProvider.ts @@ -0,0 +1,33 @@ +import CacheManager from '../src/CacheManager' +import { CacheConfig, ConstructorParams } from '@ioc:Adonis/Addons/Adonis5-Cache' +import BaseAdonisCacheProvider from './BaseAdonisCacheProvider' +import SharableCacheManager from '../src/SharableCacheManager' +import { RedisManagerContract } from '@ioc:Adonis/Addons/Redis' +import { RedisCacheDistributorTransport } from '../src/SharableCacheTransports/RedisCacheDistributorTransport' + +export default class AdonisSharableCacheProvider extends BaseAdonisCacheProvider { + public get providerAlias(): string { + return 'Adonis/Addons/Adonis5-SharableCache' + } + + public get cacheManagerClass(): { new (args: ConstructorParams): CacheManager } { + return SharableCacheManager + } + + public async boot() { + await super.boot() + + const cache: SharableCacheManager = this.container.resolveBinding(this.providerAlias) + + const cacheConfig: CacheConfig = this.container + .resolveBinding('Adonis/Core/Config') + .get('cache') + + const redis: RedisManagerContract = this.container.resolveBinding('Adonis/Addons/Redis') + cache.setTransport(new RedisCacheDistributorTransport(redis)) + + if (cacheConfig.sharedCacheConfig?.isSharingEnabled) { + cache.runSynchronization() + } + } +} diff --git a/providers/BaseAdonisCacheProvider.ts b/providers/BaseAdonisCacheProvider.ts new file mode 100644 index 0000000..56d3616 --- /dev/null +++ b/providers/BaseAdonisCacheProvider.ts @@ -0,0 +1,77 @@ +import { IocContract } from '@adonisjs/fold/build' +import CacheManager from '../src/CacheManager' +import RedisStorage from '../src/CacheStorages/RedisStorage' +import InMemoryStorage from '../src/CacheStorages/InMemoryStorage' +import { EmitterContract } from '@ioc:Adonis/Core/Event' +import { ConfigContract } from '@ioc:Adonis/Core/Config' +import { + CacheConfig, + CacheManagerContract, + ConstructorParams, +} from '@ioc:Adonis/Addons/Adonis5-Cache' +import { RedisManagerContract } from '@ioc:Adonis/Addons/Redis' +import { ApplicationContract, ContainerBindings } from '@ioc:Adonis/Core/Application' +import { AdonisMemcachedClientContract } from '@ioc:Adonis/Addons/Adonis5-MemcachedClient' +import MemcachedStorage from '../src/CacheStorages/MemcachedStorage' + +export default abstract class BaseAdonisCacheProvider { + public static needsApplication = true + private container: IocContract + + abstract get providerAlias(): string + + abstract get cacheManagerClass(): new (args: ConstructorParams) => CacheManager + + constructor(protected application: ApplicationContract) { + this.container = application.container + } + + public register(): void { + this.container.singleton(this.providerAlias, () => { + const eventEmitter: EmitterContract = this.container.resolveBinding('Adonis/Core/Event') + const config: ConfigContract = this.container.resolveBinding('Adonis/Core/Config') + + const CacheManagerClass = this.cacheManagerClass + + return new CacheManagerClass({ + eventEmitter, + config: config.get('cache'), + }) + }) + } + + public boot(): void { + const cache: CacheManagerContract = this.container.resolveBinding(this.providerAlias) + const cacheConfig: CacheConfig = this.container + .resolveBinding('Adonis/Core/Config') + .get('cache') + + if (cacheConfig.enabledCacheStorages.includes('redis')) { + this.registerRedisCacheStorage(cache) + } + + if (cacheConfig.enabledCacheStorages.includes('in-memory')) { + this.registerInMemoryCacheStorage(cache) + } + + if (cacheConfig.enabledCacheStorages.includes('memcached')) { + this.registerMemcachedCacheStorage(cache) + } + } + + private registerRedisCacheStorage(cache: CacheManagerContract) { + const redis: RedisManagerContract = this.container.resolveBinding('Adonis/Addons/Redis') + cache.registerStorage('redis', new RedisStorage(redis)) + } + + private registerInMemoryCacheStorage(cache: CacheManagerContract) { + cache.registerStorage('in-memory', new InMemoryStorage()) + } + + private registerMemcachedCacheStorage(cache: CacheManagerContract) { + const memcachedClient: AdonisMemcachedClientContract = this.container.resolveBinding( + 'Adonis/Addons/Adonis5-MemcachedClient' + ) + cache.registerStorage('memcached', new MemcachedStorage(memcachedClient)) + } +} diff --git a/sharable-cache-provider.ts b/sharable-cache-provider.ts new file mode 100644 index 0000000..685de64 --- /dev/null +++ b/sharable-cache-provider.ts @@ -0,0 +1 @@ +export { default } from './providers/AdonisSharableCacheProvider' diff --git a/src/CacheManager.ts b/src/CacheManager.ts index 9991b97..194cee3 100644 --- a/src/CacheManager.ts +++ b/src/CacheManager.ts @@ -12,13 +12,12 @@ import { zipObj, isNil } from 'ramda' import TaggableCacheManager from './TaggableCacheManager' import { isFunction } from './TypeGuards' import ms from 'ms' +import TimeConverter from './Utils/TimeConverter' -export type CacheStorageCollection = { [key: string]: CacheStorageContract } -export type CacheContextCollection = { [key: string]: CacheContextContract } +export type CacheStorageCollection = Record +export type CacheContextCollection = Record export default class CacheManager implements CacheManagerContract { - public static readonly DEFAULT_RECORD_TTL = 6000 - protected cacheStorages: CacheStorageCollection = {} protected cacheContexts: CacheContextCollection = {} protected cacheConfig: CacheConfig @@ -30,17 +29,18 @@ export default class CacheManager implements CacheManagerContract { protected tempStorageName: string | null = null protected eventEmitter: CacheEventEmitter protected cacheTags: string[] = [] + protected timeConverter: TimeConverter constructor({ config, eventEmitter }: ConstructorParams) { this.cacheConfig = config this.eventEmitter = new CacheEventEmitter(this.cacheConfig.enabledEvents, eventEmitter) this.currentCacheStorageName = this.cacheConfig.currentCacheStorage - - this.initCacheContexts() + this.timeConverter = new TimeConverter(config.recordTTL, this.cacheConfig.ttlUnits) + this.initializeManager() } public get recordTTL(): number { - return this.cacheConfig.recordTTL || CacheManager.DEFAULT_RECORD_TTL + return this.cacheConfig.recordTTL } public get recordKeyPrefix(): string { @@ -178,7 +178,7 @@ export default class CacheManager implements CacheManagerContract { this.cacheTags = [] } - private emitEventsOnReadOperations(cacheData: { [key: string]: T }) { + private emitEventsOnReadOperations(cacheData: Record) { const missedKeys: string[] = [] const storedData = {} for (const [key, value] of Object.entries(cacheData)) { @@ -193,7 +193,7 @@ export default class CacheManager implements CacheManagerContract { } } - private initCacheContexts() { + private initializeManager() { this.currentCacheContextName = 'DEFAULT' this.cacheContexts = { [this.currentCacheContextName]: DefaultCacheContext } } diff --git a/src/CacheStorages/InMemoryStorage.ts b/src/CacheStorages/InMemoryStorage.ts index 4c885b4..5b530fb 100644 --- a/src/CacheStorages/InMemoryStorage.ts +++ b/src/CacheStorages/InMemoryStorage.ts @@ -29,7 +29,6 @@ export default class InMemoryStorage implements CacheStorageContract, TaggableSt public async get(context: CacheContextContract, key: string): Promise { const { recordExpirationTime, recordValue = null } = this.cacheStorage[key] || {} - return recordValue !== null && dayjs().isBefore(dayjs(recordExpirationTime)) ? context.deserialize(recordValue) : null diff --git a/src/SharableCacheManager.ts b/src/SharableCacheManager.ts new file mode 100644 index 0000000..df484a6 --- /dev/null +++ b/src/SharableCacheManager.ts @@ -0,0 +1,126 @@ +import { CacheManagerContract, ConstructorParams } from '@ioc:Adonis/Addons/Adonis5-Cache' +import TaggableCacheManager from './TaggableCacheManager' +import CacheManager from './CacheManager' +import { + CacheCommand, + SharableCacheManagerContract, + SharableCacheTransportContract, +} from '@ioc:Adonis/Addons/Adonis5-SharableCache' + +export default class SharableCacheManager + extends CacheManager + implements SharableCacheManagerContract { + protected _transport: SharableCacheTransportContract + protected commandQueue: CacheCommand[] = [] + protected interval: NodeJS.Timeout | null = null + + constructor({ config, eventEmitter }: ConstructorParams) { + super({ config, eventEmitter }) + } + + public get sharedCacheConfig() { + return this.cacheConfig.sharedCacheConfig + } + + public get transport(): SharableCacheTransportContract { + return this._transport + } + + public set transport(value: SharableCacheTransportContract) { + this._transport = value + } + + public viaContext(contextName: string): CacheManagerContract { + this.addCommandToQueue('viaContext', [contextName], true) + return super.viaContext(contextName) + } + + public viaStorage(storageName: string): CacheManagerContract { + this.addCommandToQueue('viaStorage', [storageName], true) + return super.viaStorage(storageName) + } + + public tags(...tags: string[]): TaggableCacheManager { + return new TaggableCacheManager(this, tags) + } + + public enableStorage(storageName: string): CacheManagerContract { + this.addCommandToQueue('enableStorage', [storageName], true) + return super.enableStorage(storageName) + } + + public enableContext(contextName: string): CacheManagerContract { + this.addCommandToQueue('enableContext', [contextName], false) + return super.enableContext(contextName) + } + + public async put(key: string, value: T, ttl?: number) { + this.addCommandToQueue('put', [key, value, ttl], false) + return super.put(key, value, ttl) + } + + public async putMany(cacheDictionary: Record, ttl?: number) { + this.addCommandToQueue('putMany', [cacheDictionary, ttl], false) + return super.putMany(cacheDictionary, ttl) + } + + public async flush(): Promise { + this.addCommandToQueue('flush', [], false) + return super.flush() + } + + public async forget(key: string): Promise { + this.addCommandToQueue('forget', [key], false) + return super.forget(key) + } + + public setTransport(transport: SharableCacheTransportContract) { + this.transport = transport + this.transport.subscribeForUpdates(this.onCacheUpdate.bind(this)) + } + + public addCommandToQueue(method: string, args: unknown[], isReturnThis: boolean): void { + this.commandQueue.push({ method, args, isReturnThis, createdAt: new Date().toISOString() }) + } + + public runSynchronization() { + this.transport.subscribeForUpdates(this.onCacheUpdate.bind(this)) + this.interval = setInterval(async () => { + if (this.commandQueue.length !== 0) { + this.transport.sync(this.commandQueue) + this.commandQueue = [] + } + }, this.cacheConfig?.sharedCacheConfig?.syncInterval || 0) + } + + public stopSynchronization() { + if (this.interval) { + clearInterval(this.interval) + } + } + + public get isSharingEnabled(): boolean { + return this.cacheConfig?.sharedCacheConfig?.isSharingEnabled || false + } + + protected async onCacheUpdate(cacheCommands: CacheCommand[]): Promise { + let callResult: CacheManagerContract | null = null + for (let { method, args, isReturnThis, createdAt } of cacheCommands) { + if (method === 'put' || method === 'putMany') { + const ttl = args[args.length - 1] + const ttlInMs = this.timeConverter.toMS(parseFloat(ttl as string) || null) + const fixedTtl = ttlInMs - (new Date(createdAt).getTime() - new Date().getTime()) + args = [ + ...args.slice(0, args.length - 1), + this.timeConverter.fromMs(fixedTtl, this.cacheConfig.ttlUnits), + ] + } + + callResult = callResult ? callResult[method](...args) : super[method](...args) + + if (isReturnThis) { + callResult = null + } + } + } +} diff --git a/src/SharableCacheTransports/RedisCacheDistributorTransport.ts b/src/SharableCacheTransports/RedisCacheDistributorTransport.ts new file mode 100644 index 0000000..73e9259 --- /dev/null +++ b/src/SharableCacheTransports/RedisCacheDistributorTransport.ts @@ -0,0 +1,39 @@ +import { v4 as uuid } from 'uuid' +import { + CacheCommand, + SharableCacheTransportContract, +} from '@ioc:Adonis/Addons/Adonis5-SharableCache' +import { RedisManagerContract } from '@ioc:Adonis/Addons/Redis' + +type RedisCacheCommandPayload = { + sender: string + data: CacheCommand[] +} + +export class RedisCacheDistributorTransport implements SharableCacheTransportContract { + private readonly clientId: string + public static readonly SYNC_CHANNEL_NAME: string = 'REDIS_CACHE_SYNC_CHANNEL' + + constructor(protected readonly redisConnection: RedisManagerContract) { + this.clientId = uuid() + } + + public async sync(cacheCommands: CacheCommand[]): Promise { + await this.redisConnection.publish( + RedisCacheDistributorTransport.SYNC_CHANNEL_NAME, + JSON.stringify({ sender: this.clientId, data: cacheCommands }) + ) + } + + public subscribeForUpdates(handleCommands: (commands: CacheCommand[]) => Promise) { + this.redisConnection.subscribe( + RedisCacheDistributorTransport.SYNC_CHANNEL_NAME, + async (payload: string) => { + const { data, sender } = JSON.parse(payload) as RedisCacheCommandPayload + if (sender !== this.clientId) { + await handleCommands(data) + } + } + ) + } +} diff --git a/src/Utils/TimeConverter.ts b/src/Utils/TimeConverter.ts new file mode 100644 index 0000000..f200a96 --- /dev/null +++ b/src/Utils/TimeConverter.ts @@ -0,0 +1,62 @@ +import ms from 'ms' +import { TtlUnits } from '@ioc:Adonis/Addons/Adonis5-Cache' + +export default class TimeConverter { + protected defaultTime: number + + constructor(defaultTime: number, protected readonly defaultUnits: TtlUnits) { + this.defaultTime = this.toMS(defaultTime, this.defaultUnits) + } + + public toMS(time: number | undefined | null, units: TtlUnits = this.defaultUnits): number { + if (!time) { + return this.defaultTime + } + return ms(time + units) + } + + public fromMs(time: number, unit: TtlUnits) { + switch (unit) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return Math.round(time / (1000 * 60 * 60 * 24 * 365)) + case 'weeks': + case 'week': + case 'w': + return Math.round(time / (1000 * 60 * 60 * 24 * 7)) + case 'days': + case 'day': + case 'd': + return Math.round(time / (1000 * 60 * 60 * 24)) + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return Math.round(time / (1000 * 60 * 60)) + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return Math.round(time / (1000 * 60)) + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return Math.round(time / 1000) + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return time + default: + throw Error('Unregistered unit for ms') + } + } +} diff --git a/test-helpers/TestAdonisApp/index.ts b/test-helpers/TestAdonisApp/index.ts index ab37132..4471a5d 100644 --- a/test-helpers/TestAdonisApp/index.ts +++ b/test-helpers/TestAdonisApp/index.ts @@ -5,7 +5,7 @@ import { ConfigContract } from '@ioc:Adonis/Core/Config' import { join } from 'path' import { Application } from '@adonisjs/application' import { ApplicationContract, ContainerBindings } from '@ioc:Adonis/Core/Application' -import { IocContract } from '@adonisjs/fold' +import { IocContract } from '@adonisjs/fold/build' export interface AdonisProvider { register(): void @@ -66,17 +66,15 @@ export class AdonisApplication { private async initCustomProviders() { this.customerProviderInstances = this.customProviders.map((Provider) => { - if (['AdonisMemcachedClientProvider', 'AdonisCacheProvider'].includes(Provider.name)) { - return new Provider(this.iocContainer) - } else { - return new Provider(this._application) - } + return new Provider( + Provider?.needsApplication ? this.application : this.application.container + ) }) } private async registerProviders() { await this.application.setup() - this.application.registerProviders() + await this.application.registerProviders() this.customerProviderInstances.map((provider) => provider.register()) } diff --git a/test-helpers/TestAdonisApp/testAdonisApp/.adonisrc.json b/test-helpers/TestAdonisApp/testAdonisApp/.adonisrc.json index aaf2537..154892b 100644 --- a/test-helpers/TestAdonisApp/testAdonisApp/.adonisrc.json +++ b/test-helpers/TestAdonisApp/testAdonisApp/.adonisrc.json @@ -3,5 +3,5 @@ "App": "./app" }, "exceptionHandlerNamespace": "App/Exceptions/Handler", - "providers": ["@adonisjs/core"] + "providers": ["@adonisjs/core", "@adonisjs/events"] } diff --git a/test/fixtures/memcached-test-config.ts b/test/fixtures/memcached-test-config.ts index ad6335d..51675b5 100644 --- a/test/fixtures/memcached-test-config.ts +++ b/test/fixtures/memcached-test-config.ts @@ -8,7 +8,6 @@ if (!process.env.MEMCACHED_SERVER_URL) { } const config: AdonisMemcachedClientConfig = { - server: process.env.MEMCACHED_SERVER_URL || process.env.MEMCACHED_SERVER_URL, + server: process.env.MEMCACHED_SERVER_URL, } - export default config diff --git a/test/sharable-cache/sharable-cache-e2e.spec.ts b/test/sharable-cache/sharable-cache-e2e.spec.ts new file mode 100644 index 0000000..f5ffb1e --- /dev/null +++ b/test/sharable-cache/sharable-cache-e2e.spec.ts @@ -0,0 +1,207 @@ +import test from 'japa' +import { AdonisApplication } from '../../test-helpers/TestAdonisApp' +import { CacheManagerContract, CacheConfig } from '@ioc:Adonis/Addons/Adonis5-Cache' +import { expect } from 'chai' +import RedisProvider from '@adonisjs/redis/build/providers/RedisProvider' +import redisConfig from '../fixtures/redis-test-config' +import { RedisManagerContract } from '@ioc:Adonis/Addons/Redis' +import { RedisCacheDistributorTransport } from '../../src/SharableCacheTransports/RedisCacheDistributorTransport' +import sleep from '../../test-helpers/utils/sleep' +import AdonisSharableCacheProvider from '../../providers/AdonisSharableCacheProvider' + +const cacheConfig: CacheConfig = { + recordTTL: 1000, + currentCacheStorage: 'in-memory', + enabledCacheStorages: ['in-memory'], + cacheKeyPrefix: '', + ttlUnits: 'seconds', + enabledEvents: { + 'cache-record:read': false, + 'cache-record:written': false, + 'cache-record:missed': false, + 'cache-record:forgotten': false, + }, + sharedCacheConfig: { + isSharingEnabled: true, + syncInterval: 10, + }, +} + +test.group('Adonis shared cache - e2e testing', (group) => { + let adonisApp: AdonisApplication + let cacheManager: CacheManagerContract + let redis: RedisManagerContract + let testTransport: RedisCacheDistributorTransport + + group.before(async () => { + adonisApp = new AdonisApplication() + await adonisApp + .registerProvider(RedisProvider) + .registerProvider(AdonisSharableCacheProvider) + .registerAppConfig({ configName: 'redis', appConfig: redisConfig }) + .registerAppConfig({ configName: 'cache', appConfig: cacheConfig }) + .loadApp() + + cacheManager = adonisApp.iocContainer.use('Adonis/Addons/Adonis5-SharableCache') + redis = adonisApp.iocContainer.use('Adonis/Addons/Redis') + testTransport = new RedisCacheDistributorTransport(redis) + await sleep(1000) + }) + + test('should call put operation via sharable cache', async () => { + const testKey = 'testKey' + const testValue = 'testValue' + + await testTransport.sync([ + { + method: 'put', + isReturnThis: false, + args: [testKey, testValue, 10000], + createdAt: new Date().toISOString(), + }, + ]) + + await sleep(1000) + + const synchronizedValue = await cacheManager.get(testKey) + + expect(synchronizedValue).equal(testValue) + }).timeout(0) + + test('should call putMany operation via sharable cache', async () => { + const testMap = { a: 1, b: 2 } + + await testTransport.sync([ + { + method: 'putMany', + isReturnThis: false, + args: [testMap, 10000], + createdAt: new Date().toISOString(), + }, + ]) + + await sleep(1000) + + for (const [testKey, testValue] of Object.entries(testMap)) { + const synchronizedValue = await cacheManager.get(testKey) + expect(synchronizedValue).equal(testValue) + } + }).timeout(0) + + test('should call forget operation via sharable cache', async () => { + const testKey = 'testKey' + + await cacheManager.put(testKey, 'value', 100000) + + await testTransport.sync([ + { + method: 'forget', + isReturnThis: false, + args: [testKey], + createdAt: new Date().toISOString(), + }, + ]) + + await sleep(1000) + + const forgottenValue = await cacheManager.get(testKey) + + expect(forgottenValue).to.be.null + }).timeout(0) + + test('should call flush operation via sharable cache', async () => { + const testMap = { a: 'a', b: 'b' } + + await cacheManager.putMany(testMap, 100) + + await testTransport.sync([ + { method: 'flush', isReturnThis: false, args: [], createdAt: new Date().toISOString() }, + ]) + + await sleep(1000) + + for (const forgottenKey of Object.keys(testMap)) { + const forgottenValue = await cacheManager.get(forgottenKey) + expect(forgottenValue).to.be.null + } + }).timeout(0) + + test('should toggle tempStorageName via sharable cache', async () => { + const testStorage = 'test-storage' + const testStorageName = 'test-storage-name' + cacheManager.registerStorage(testStorageName, testStorage as any) + + await testTransport.sync([ + { + method: 'viaStorage', + isReturnThis: true, + args: [testStorageName], + createdAt: new Date().toISOString(), + }, + ]) + + await sleep(1000) + + // @ts-ignore + expect(cacheManager.tempStorageName).to.equal(testStorageName) + }).timeout(0) + + test('should toggle tempContextName via sharable cache', async () => { + const testContext = 'test-context' + const testContextName = 'test-context-name' + cacheManager.registerContext(testContextName, testContext as any) + + await testTransport.sync([ + { + method: 'viaContext', + isReturnThis: true, + args: [testContextName], + createdAt: new Date().toISOString(), + }, + ]) + + await sleep(1000) + + // @ts-ignore + expect(cacheManager.tempContextName).to.equal(testContextName) + }).timeout(0) + + test('should toggle storage via sharable cache', async () => { + const testStorage = 'test-storage' + const testStorageName = 'test-storage-name' + cacheManager.registerStorage(testStorageName, testStorage as any) + + await testTransport.sync([ + { + method: 'enableStorage', + isReturnThis: false, + args: [testStorageName], + createdAt: new Date().toISOString(), + }, + ]) + + await sleep(1000) + + expect(cacheManager.storage).to.equal(testStorage) + }).timeout(0) + + test('should toggle context via sharable cache', async () => { + const testContext = 'test-context' + const testContextName = 'test-context-name' + cacheManager.registerContext(testContextName, testContext as any) + + await testTransport.sync([ + { + method: 'enableContext', + isReturnThis: false, + args: [testContextName], + createdAt: new Date().toISOString(), + }, + ]) + + await sleep(1000) + + // @ts-ignore + expect(cacheManager.currentCacheContextName).to.equal(testContextName) + }).timeout(0) +}) diff --git a/test/sharable-cache/sharable-cache-manager-command-execution.spec.ts b/test/sharable-cache/sharable-cache-manager-command-execution.spec.ts new file mode 100644 index 0000000..692e7d7 --- /dev/null +++ b/test/sharable-cache/sharable-cache-manager-command-execution.spec.ts @@ -0,0 +1,202 @@ +import test from 'japa' +import { + CacheCommand, + SharableCacheTransportContract, +} from '@ioc:Adonis/Addons/Adonis5-SharableCache' +import { CacheConfig } from '@ioc:Adonis/Addons/Adonis5-Cache' +import InMemoryStorage from '../../src/CacheStorages/InMemoryStorage' +import { anyNumber, anything, instance, mock, verify, when } from 'ts-mockito' +import { expect } from 'chai' +import SharableCacheManager from '../../src/SharableCacheManager' +const cacheConfig: CacheConfig = { + recordTTL: 1000, + currentCacheStorage: 'in-memory', + enabledCacheStorages: [], + cacheKeyPrefix: '', + ttlUnits: 'ms', + enabledEvents: { + 'cache-record:read': false, + 'cache-record:written': false, + 'cache-record:missed': false, + 'cache-record:forgotten': false, + }, + sharedCacheConfig: { + isSharingEnabled: true, + syncInterval: 200, + }, +} + +class TestTransport implements SharableCacheTransportContract { + private handler: (commands: CacheCommand[]) => Promise + + public subscribeForUpdates(handler: (commands: CacheCommand[]) => Promise) { + this.handler = handler + } + + public sync(_cacheCommands: CacheCommand[]): void {} + + public async pushCommand(...commands: CacheCommand[]) { + await this.handler(JSON.parse(JSON.stringify(commands)) as CacheCommand[]) + } +} + +test.group('Adonis sharable cache provider - command-execution', () => { + function initSharableCacheManager(config: CacheConfig) { + const cacheManager = new SharableCacheManager({ + config, + eventEmitter: {} as any, + }) + + const transport = new TestTransport() + cacheManager.setTransport(transport) + + return { cacheManager, transport } + } + + test('should call PUT operation on storage after receiving', async () => { + const testKey = 'testKey' + const testValue = 'testValue' + + const { cacheManager, transport } = initSharableCacheManager(cacheConfig) + const mockedStorage = mock(InMemoryStorage) + when(mockedStorage.resolveTtl(anyNumber())).thenReturn(cacheConfig.recordTTL) + cacheManager.registerStorage('test-transport', instance(mockedStorage)) + cacheManager.enableStorage('test-transport') + + await transport.pushCommand({ + method: 'put', + args: [testKey, testValue, undefined], + isReturnThis: false, + createdAt: new Date().toISOString(), + }) + + verify(mockedStorage.put(anything(), testKey, testValue, cacheConfig.recordTTL)).once() + }).timeout(0) + + test('should call PUT operation with custom ttl params on storage after receiving', async () => { + const testKey = 'testKey' + const testValue = 'testValue' + const testTtl = 2500 + + const { transport, cacheManager } = initSharableCacheManager(cacheConfig) + const mockedStorage = mock(InMemoryStorage) + when(mockedStorage.resolveTtl(anyNumber())).thenReturn(testTtl) + cacheManager.registerStorage('test-storage', instance(mockedStorage)) + cacheManager.enableStorage('test-storage') + + await transport.pushCommand({ + method: 'put', + args: [testKey, testValue, testTtl], + isReturnThis: false, + createdAt: new Date().toISOString(), + }) + + verify(mockedStorage.put(anything(), testKey, testValue, testTtl)).once() + }).timeout(0) + + test('should call PUT MANY operation with custom ttl params on storage after receiving', async () => { + const testMap = { a: 'value-a', b: 'value-b' } + const testTtl = 2500 + + const { transport, cacheManager } = initSharableCacheManager(cacheConfig) + const mockedStorage = mock(InMemoryStorage) + when(mockedStorage.resolveTtl(anyNumber())).thenReturn(testTtl) + cacheManager.registerStorage('test-transport', instance(mockedStorage)) + cacheManager.enableStorage('test-transport') + + await transport.pushCommand({ + method: 'putMany', + args: [testMap, testTtl], + isReturnThis: false, + createdAt: new Date().toISOString(), + }) + + const transformedTestMap = Object.entries(testMap).reduce((acc, [key, value]) => { + return { ...acc, [cacheConfig.cacheKeyPrefix + key]: value } + }, {}) + + verify(mockedStorage.putMany(anything(), transformedTestMap, testTtl)) + }).timeout(0) + + test('should call FLUSH operation on storage after receiving', async () => { + const { transport, cacheManager } = initSharableCacheManager(cacheConfig) + const mockedStorage = mock(InMemoryStorage) + cacheManager.registerStorage('test-transport', instance(mockedStorage)) + cacheManager.enableStorage('test-transport') + + await transport.pushCommand({ + method: 'flush', + args: [], + isReturnThis: false, + createdAt: new Date().toISOString(), + }) + + verify(mockedStorage.flush()).once() + }).timeout(0) + + test('should change manager context after receiving command', async () => { + const { transport, cacheManager } = initSharableCacheManager(cacheConfig) + const testContextName = 'test-context' + + cacheManager.registerContext(testContextName, testContextName as any) + cacheManager.enableContext(testContextName) + + await transport.pushCommand({ + method: 'viaContext', + args: [testContextName], + isReturnThis: true, + createdAt: new Date().toISOString(), + }) + + expect(cacheManager.context).to.equal(testContextName) + }).timeout(0) + + test('should change manager storage after receiving command', async () => { + const { transport, cacheManager } = initSharableCacheManager(cacheConfig) + const testStorageName = 'test-storage' + + cacheManager.registerStorage(testStorageName, testStorageName as any) + cacheManager.enableStorage(testStorageName) + + await transport.pushCommand({ + method: 'viaStorage', + args: [testStorageName], + isReturnThis: true, + createdAt: new Date().toISOString(), + }) + + expect(cacheManager.storage).to.equal(testStorageName) + }).timeout(0) + + test('should change manager storage and put new value to new storage after receiving commands', async () => { + const testKey = 'testKey' + const testValue = 'testValue' + const { transport, cacheManager } = initSharableCacheManager(cacheConfig) + const testStorageName = 'test-storage' + const testTtl = 2500 + + cacheManager.registerStorage('initialStorage', new InMemoryStorage()) + cacheManager.enableStorage('initialStorage') + + const mockedStorage = mock(InMemoryStorage) + when(mockedStorage.resolveTtl(testTtl)).thenReturn(testTtl) + cacheManager.registerStorage(testStorageName, instance(mockedStorage)) + + await transport.pushCommand( + { + method: 'viaStorage', + args: [testStorageName], + isReturnThis: true, + createdAt: new Date().toISOString(), + }, + { + method: 'put', + args: [testKey, testValue, testTtl], + isReturnThis: false, + createdAt: new Date().toISOString(), + } + ) + + verify(mockedStorage.put(anything(), testKey, testValue, testTtl)).once() + }).timeout(0) +}) diff --git a/test/sharable-cache/sharable-cache-manager-command-queue.spec.ts b/test/sharable-cache/sharable-cache-manager-command-queue.spec.ts new file mode 100644 index 0000000..c62e429 --- /dev/null +++ b/test/sharable-cache/sharable-cache-manager-command-queue.spec.ts @@ -0,0 +1,193 @@ +import test from 'japa' +import { CacheConfig } from '@ioc:Adonis/Addons/Adonis5-Cache' +import { CacheCommand } from '@ioc:Adonis/Addons/Adonis5-SharableCache' +import { expect } from 'chai' +import InMemoryStorage from '../../src/CacheStorages/InMemoryStorage' +import { anyString, instance, mock } from 'ts-mockito' + +import SharableCacheManager from '../../src/SharableCacheManager' +import { Emitter } from '@adonisjs/events/build/src/Emitter' +import { RedisCacheDistributorTransport } from '../../src/SharableCacheTransports/RedisCacheDistributorTransport' +import DefaultCacheContext from '../../src/CacheContexts/DefaultCacheContext' + +const cacheConfig: CacheConfig = { + recordTTL: 1000, + currentCacheStorage: 'in-memory', + enabledCacheStorages: [], + cacheKeyPrefix: '', + ttlUnits: 'ms', + enabledEvents: { + 'cache-record:read': false, + 'cache-record:written': false, + 'cache-record:missed': false, + 'cache-record:forgotten': false, + }, + sharedCacheConfig: { + isSharingEnabled: true, + syncInterval: 2000, + }, +} + +function extractQueue(cacheManager: SharableCacheManager): CacheCommand[] { + // @ts-ignore + return cacheManager.commandQueue +} + +test.group('Adonis sharable cache provider - test queue', () => { + function initSharableCacheManager(config: CacheConfig) { + const transportMock = mock(RedisCacheDistributorTransport) + const mockedStorage: InMemoryStorage = mock(InMemoryStorage) + + const cacheManager = new SharableCacheManager({ + config, + eventEmitter: instance(mock(Emitter)), + }) + + cacheManager.registerStorage(config.currentCacheStorage, instance(mockedStorage)) + cacheManager.enableStorage(config.currentCacheStorage) + + // @ts-ignore + cacheManager.commandQueue = [] + + return { transportMock, cacheManager, mockedStorage } + } + + test('should add command to command queue on put operation', async () => { + const testKey = 'testKey' + const testValue = 'testValue' + + const { cacheManager } = initSharableCacheManager(cacheConfig) + + await cacheManager.put(testKey, testValue) + + expect(extractQueue(cacheManager)).to.deep.eq([ + { + method: 'put', + args: [testKey, testValue, undefined], + isReturnThis: false, + createdAt: new Date().toISOString(), + }, + ]) + }).timeout(0) + + test('should add command to command queue on put operation with ttl in args', async () => { + const testKey = 'testKey' + const testValue = 'testValue' + const testTtl = 200 + + const { cacheManager } = initSharableCacheManager(cacheConfig) + + await cacheManager.put(testKey, testValue, testTtl) + + const queue = extractQueue(cacheManager) + expect(queue.length).to.equal(1) + + const [queueElement] = queue + expect(queueElement).to.deep.include({ + method: 'put', + args: [testKey, testValue, testTtl], + isReturnThis: false, + }) + }).timeout(0) + + test('should add command to command queue on putMany operation', async () => { + const testCacheMap = { a: 2, b: 3 } + + const { cacheManager } = initSharableCacheManager(cacheConfig) + + await cacheManager.putMany(testCacheMap) + + expect(extractQueue(cacheManager)).to.deep.eq([ + { + method: 'putMany', + args: [testCacheMap, undefined], + isReturnThis: false, + createdAt: new Date().toISOString(), + }, + ]) + }).timeout(0) + + test('should add command to command queue on putMany operation with specified ttl', async () => { + const testCacheMap = { a: 2, b: 3 } + const testTtl = 200 + + const { cacheManager } = initSharableCacheManager(cacheConfig) + + await cacheManager.putMany(testCacheMap, testTtl) + + expect(extractQueue(cacheManager)).to.deep.eq([ + { + method: 'putMany', + args: [testCacheMap, testTtl], + isReturnThis: false, + createdAt: new Date().toISOString(), + }, + ]) + }).timeout(0) + + test('should add command to command queue on forget operation', async () => { + const testKey = 'key' + + const { cacheManager } = initSharableCacheManager(cacheConfig) + + await cacheManager.forget(testKey) + + expect(extractQueue(cacheManager)).to.deep.eq([ + { + method: 'forget', + args: [testKey], + isReturnThis: false, + createdAt: new Date().toISOString(), + }, + ]) + }).timeout(0) + + test('should add command to command queue on flush operation', async () => { + const { cacheManager } = initSharableCacheManager(cacheConfig) + + await cacheManager.flush() + + expect(extractQueue(cacheManager)).to.deep.eq([ + { + method: 'flush', + args: [], + isReturnThis: false, + createdAt: new Date().toISOString(), + }, + ]) + }).timeout(0) + + test('should add command to command queue on viaContext operation', async () => { + const { cacheManager } = initSharableCacheManager(cacheConfig) + + const context = 'test-context' + cacheManager.registerContext(context, DefaultCacheContext) + + cacheManager.viaContext(context) + + expect(extractQueue(cacheManager)).to.deep.eq([ + { + method: 'viaContext', + args: [context], + isReturnThis: true, + createdAt: new Date().toISOString(), + }, + ]) + }).timeout(0) + + test('should add command to command queue on viaStorage operation', async () => { + const { cacheManager } = initSharableCacheManager(cacheConfig) + + const storage = 'in-memory' + await cacheManager.viaStorage(storage) + + expect(extractQueue(cacheManager)).to.deep.eq([ + { + method: 'viaStorage', + args: [storage], + isReturnThis: true, + createdAt: new Date().toISOString(), + }, + ]) + }).timeout(0) +}) diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..cb6b990 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": [] +} diff --git a/tsconfig.json b/tsconfig.json index 53a0b1b..04ca4fc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,9 @@ { "compileOnSave": true, "compilerOptions": { + "target": "es2019", + "module": "commonjs", + "lib": ["es2019"], "skipLibCheck": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, @@ -13,5 +16,7 @@ "./node_modules/@adonisjs/events/build/adonis-typings/events.d.ts", "./node_modules/adonis5-memcached-client/build/adonis-typings/index.d.ts", "./adonis-typings/index.ts" - ] + ], + "include": ["**/*"], + "exclude": ["test", "test-helpers"] } From 1ecc90e9c85b29216649310b33434b8b42f40cce Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 25 Feb 2021 08:46:00 +0300 Subject: [PATCH 2/3] fix: readme config and provider --- README.md | 2 +- npm-audit.html | 4 ++-- providers/BaseAdonisCacheProvider.ts | 2 +- templates/config.txt | 6 +++++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 090d58d..fb4fc99 100644 --- a/README.md +++ b/README.md @@ -350,7 +350,7 @@ For enabling you should add sharable cache provider: and setup your `config/cache.ts` for using shared cache ```js { -sharedCacheConfig: { + sharedCacheConfig: { isSharingEnabled: true, syncInterval: 2000 } diff --git a/npm-audit.html b/npm-audit.html index 58bc961..f7f3712 100644 --- a/npm-audit.html +++ b/npm-audit.html @@ -12,7 +12,7 @@ integrity="sha384-wXznGJNEXNG1NFsbm0ugrLFMQPWswR3lds2VeinahP8N0zJw9VWSopbjv2x7WCvX" crossorigin="anonymous"> + href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.16.2/build/styles/atom-one-dark.min.css"> NPM Audit Report @@ -55,7 +55,7 @@
- February 24th 2021, 6:54:12 pm + February 25th 2021, 5:45:59 am

Last updated

diff --git a/providers/BaseAdonisCacheProvider.ts b/providers/BaseAdonisCacheProvider.ts index 56d3616..c8110a7 100644 --- a/providers/BaseAdonisCacheProvider.ts +++ b/providers/BaseAdonisCacheProvider.ts @@ -16,7 +16,7 @@ import MemcachedStorage from '../src/CacheStorages/MemcachedStorage' export default abstract class BaseAdonisCacheProvider { public static needsApplication = true - private container: IocContract + public container: IocContract abstract get providerAlias(): string diff --git a/templates/config.txt b/templates/config.txt index 4bcba6f..ad177a8 100644 --- a/templates/config.txt +++ b/templates/config.txt @@ -7,7 +7,11 @@ export default { currentCacheStorage: 'redis', // storages which used as default cache storage - enabledCacheStorages: ['in-memory', 'redis', 'memcached'], // storages which will be loaded + enabledCacheStorages: [ + 'in-memory', + // 'redis', + // 'memcached' + ], // storages which will be loaded cacheKeyPrefix: 'cache_record_', // prefix for keys, which will be stored in cache storage From 05961de5174e7d5869862d16aa434a08983a4cd2 Mon Sep 17 00:00:00 2001 From: Vladyslav Parashchenko Date: Thu, 25 Feb 2021 22:27:44 +0200 Subject: [PATCH 3/3] fix: provider access modifiers and config template --- .env.example | 1 + npm-audit.html | 4 ++-- providers/BaseAdonisCacheProvider.ts | 2 +- templates/config.txt | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 64f1045..7f9c87c 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ REDIS_PORT=6379 MEMCACHED_SERVER_URL="localhost:11211" +MEMCACHED_PORT=11211 diff --git a/npm-audit.html b/npm-audit.html index f7f3712..2d937db 100644 --- a/npm-audit.html +++ b/npm-audit.html @@ -12,7 +12,7 @@ integrity="sha384-wXznGJNEXNG1NFsbm0ugrLFMQPWswR3lds2VeinahP8N0zJw9VWSopbjv2x7WCvX" crossorigin="anonymous"> + href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.3.1/build/styles/atom-one-dark.min.css"> NPM Audit Report @@ -55,7 +55,7 @@
- February 25th 2021, 5:45:59 am + February 25th 2021, 8:28:35 pm

Last updated

diff --git a/providers/BaseAdonisCacheProvider.ts b/providers/BaseAdonisCacheProvider.ts index c8110a7..b1ee00b 100644 --- a/providers/BaseAdonisCacheProvider.ts +++ b/providers/BaseAdonisCacheProvider.ts @@ -16,7 +16,7 @@ import MemcachedStorage from '../src/CacheStorages/MemcachedStorage' export default abstract class BaseAdonisCacheProvider { public static needsApplication = true - public container: IocContract + protected container: IocContract abstract get providerAlias(): string diff --git a/templates/config.txt b/templates/config.txt index ad177a8..95fb5a5 100644 --- a/templates/config.txt +++ b/templates/config.txt @@ -5,7 +5,7 @@ export default { ttlUnits: 'ms', // time units for ttl record - currentCacheStorage: 'redis', // storages which used as default cache storage + currentCacheStorage: 'in-memory', // storages which used as default cache storage enabledCacheStorages: [ 'in-memory',