- February 7th 2021, 12:16:25 am
+ February 25th 2021, 8:28:35 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..b1ee00b
--- /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
+ protected 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/templates/config.txt b/templates/config.txt
index 4bcba6f..95fb5a5 100644
--- a/templates/config.txt
+++ b/templates/config.txt
@@ -5,9 +5,13 @@ 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', '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
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"]
}