forked from nocobase/nocobase
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(cache): improve cache (nocobase#3004)
* feat: improve cache * fix: bug * fix: test * fix: test * fix: test * chore: add cache test * feat: add wrapWithCondition * fix: test * refactor: improve api * fix: test * fix: test * fix: test * fix: improve code * fix: test * feat: register redis store * fix: tst * fix: test * fix: bug * chore: update * fix: ttl unit * chore: cachemanager constructor * chore: remove code * feat: support close connection * chore: add close options for redis store
- Loading branch information
Showing
21 changed files
with
540 additions
and
247 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,18 @@ | ||
import { createDefaultCacheConfig } from '@nocobase/cache'; | ||
import { CacheManagerOptions } from '@nocobase/cache'; | ||
|
||
const cacheConfig = process.env.CACHE_CONFIG ? JSON.parse(process.env.CACHE_CONFIG) : createDefaultCacheConfig(); | ||
|
||
export default cacheConfig; | ||
export const cacheManager = { | ||
defaultStore: process.env.CACHE_DEFAULT_STORE || 'memory', | ||
stores: { | ||
memory: { | ||
store: 'memory', | ||
max: parseInt(process.env.CACHE_MEMORY_MAX) || 2000, | ||
}, | ||
...(process.env.CACHE_REDIS_URL | ||
? { | ||
redis: { | ||
url: process.env.CACHE_REDIS_URL, | ||
}, | ||
} | ||
: {}), | ||
}, | ||
} as CacheManagerOptions; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { Cache } from '../cache'; | ||
import { CacheManager } from '../cache-manager'; | ||
|
||
describe('cache-manager', () => { | ||
let cacheManager: CacheManager; | ||
|
||
beforeEach(() => { | ||
cacheManager = new CacheManager(); | ||
}); | ||
|
||
afterEach(() => { | ||
cacheManager = null; | ||
}); | ||
|
||
it('create with default config', async () => { | ||
cacheManager.registerStore({ name: 'memory', store: 'memory' }); | ||
const cache = await cacheManager.createCache({ name: 'test', store: 'memory' }); | ||
expect(cache).toBeDefined(); | ||
expect(cache.name).toBe('test'); | ||
expect(cacheManager.caches.has('test')).toBeTruthy(); | ||
}); | ||
|
||
it('create with custom config', async () => { | ||
cacheManager.registerStore({ name: 'memory', store: 'memory' }); | ||
const cache = (await cacheManager.createCache({ name: 'test', store: 'memory', ttl: 100 })) as Cache; | ||
expect(cache).toBeDefined(); | ||
expect(cache.name).toBe('test'); | ||
expect(cacheManager.caches.has('test')).toBeTruthy(); | ||
}); | ||
|
||
it('should close store', async () => { | ||
const close = jest.fn(); | ||
cacheManager.registerStore({ | ||
name: 'memory', | ||
store: 'memory', | ||
close, | ||
}); | ||
await cacheManager.createCache({ name: 'test', store: 'memory' }); | ||
await cacheManager.close(); | ||
expect(close).toBeCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { Cache } from '../cache'; | ||
import { CacheManager } from '../cache-manager'; | ||
import lodash from 'lodash'; | ||
|
||
describe('cache', () => { | ||
let cache: Cache; | ||
|
||
beforeEach(async () => { | ||
const cacheManager = new CacheManager(); | ||
cacheManager.registerStore({ name: 'memory', store: 'memory' }); | ||
cache = await cacheManager.createCache({ name: 'test', store: 'memory' }); | ||
}); | ||
|
||
afterEach(async () => { | ||
await cache.reset(); | ||
}); | ||
|
||
it('should set and get value', async () => { | ||
await cache.set('key', 'value'); | ||
const value = await cache.get('key'); | ||
expect(value).toBe('value'); | ||
}); | ||
|
||
it('set and get value in object', async () => { | ||
const value = { a: 1 }; | ||
await cache.set('key', value); | ||
const cacheA = await cache.getValueInObject('key', 'a'); | ||
expect(cacheA).toEqual(1); | ||
|
||
await cache.setValueInObject('key', 'a', 2); | ||
const cacheVal2 = await cache.getValueInObject('key', 'a'); | ||
expect(cacheVal2).toEqual(2); | ||
}); | ||
|
||
it('wrap with condition, useCache', async () => { | ||
const obj = {}; | ||
const get = () => obj; | ||
const val = await cache.wrapWithCondition('key', get, { | ||
useCache: false, | ||
}); | ||
expect(val).toBe(obj); | ||
expect(await cache.get('key')).toBeUndefined(); | ||
const val2 = await cache.wrapWithCondition('key', get); | ||
expect(val2).toBe(obj); | ||
expect(await cache.get('key')).toMatchObject(obj); | ||
}); | ||
|
||
it('wrap with condition, isCacheable', async () => { | ||
let obj = {}; | ||
const get = () => obj; | ||
const isCacheable = (val: any) => !lodash.isEmpty(val); | ||
const val = await cache.wrapWithCondition('key', get, { | ||
isCacheable, | ||
}); | ||
expect(val).toBe(obj); | ||
expect(await cache.get('key')).toBeUndefined(); | ||
obj = { a: 1 }; | ||
const val2 = await cache.wrapWithCondition('key', get, { | ||
isCacheable, | ||
}); | ||
expect(val2).toBe(obj); | ||
expect(await cache.get('key')).toMatchObject(obj); | ||
}); | ||
}); |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { FactoryStore, Store, caching, Cache as BasicCache } from 'cache-manager'; | ||
import { Cache } from './cache'; | ||
import lodash from 'lodash'; | ||
import { RedisStore, redisStore } from 'cache-manager-redis-yet'; | ||
import deepmerge from 'deepmerge'; | ||
|
||
type StoreOptions = { | ||
store?: 'memory' | FactoryStore<Store, any>; | ||
close?: (store: Store) => Promise<void>; | ||
// global config | ||
[key: string]: any; | ||
}; | ||
|
||
export type CacheManagerOptions = Partial<{ | ||
defaultStore: string; | ||
stores: { | ||
[storeType: string]: StoreOptions; | ||
}; | ||
}>; | ||
|
||
export class CacheManager { | ||
defaultStore: string; | ||
private stores = new Map< | ||
string, | ||
{ | ||
store: BasicCache; | ||
close?: (store: Store) => Promise<void>; | ||
} | ||
>(); | ||
storeTypes = new Map<string, StoreOptions>(); | ||
caches = new Map<string, Cache>(); | ||
|
||
constructor(options?: CacheManagerOptions) { | ||
const defaultOptions: CacheManagerOptions = { | ||
defaultStore: 'memory', | ||
stores: { | ||
memory: { | ||
store: 'memory', | ||
// global config | ||
max: 2000, | ||
}, | ||
redis: { | ||
store: redisStore, | ||
close: async (redis: RedisStore) => { | ||
await redis.client.quit(); | ||
}, | ||
}, | ||
}, | ||
}; | ||
const cacheOptions = deepmerge(defaultOptions, options || {}); | ||
const { defaultStore = 'memory', stores } = cacheOptions; | ||
this.defaultStore = defaultStore; | ||
for (const [name, store] of Object.entries(stores)) { | ||
const { store: s, ...globalConfig } = store; | ||
this.registerStore({ name, store: s, ...globalConfig }); | ||
} | ||
} | ||
|
||
private async createStore(options: { name: string; storeType: string; [key: string]: any }) { | ||
const { name, storeType: type, ...config } = options; | ||
const storeType = this.storeTypes.get(type) as any; | ||
if (!storeType) { | ||
throw new Error(`Create cache failed, store type [${type}] is unavailable or not registered`); | ||
} | ||
const { store: s, close, ...globalConfig } = storeType; | ||
const store = await caching(s, { ...globalConfig, ...config }); | ||
this.stores.set(name, { close, store }); | ||
return store; | ||
} | ||
|
||
registerStore(options: { name: string } & StoreOptions) { | ||
const { name, ...rest } = options; | ||
this.storeTypes.set(name, rest); | ||
} | ||
|
||
private newCache(options: { name: string; prefix?: string; store: BasicCache }) { | ||
const { name, prefix, store } = options; | ||
const cache = new Cache({ name, prefix, store }); | ||
this.caches.set(name, cache); | ||
return cache; | ||
} | ||
|
||
async createCache(options: { name: string; prefix?: string; store?: string; [key: string]: any }) { | ||
const { name, prefix, store = this.defaultStore, ...config } = options; | ||
if (!lodash.isEmpty(config)) { | ||
const newStore = await this.createStore({ name, storeType: store, ...config }); | ||
return this.newCache({ name, prefix, store: newStore }); | ||
} | ||
const s = this.stores.get(store); | ||
if (!s) { | ||
const defaultStore = await this.createStore({ name: store, storeType: store }); | ||
return this.newCache({ name, prefix, store: defaultStore }); | ||
} | ||
return this.newCache({ name, prefix, store: s.store }); | ||
} | ||
|
||
getCache(name: string): Cache { | ||
const cache = this.caches.get(name); | ||
if (!cache) { | ||
throw new Error(`Get cache failed, ${name} is not found`); | ||
} | ||
return cache; | ||
} | ||
|
||
async flushAll() { | ||
const promises = []; | ||
for (const cache of this.caches.values()) { | ||
promises.push(cache.reset()); | ||
} | ||
await Promise.all(promises); | ||
} | ||
|
||
async close() { | ||
const promises = []; | ||
for (const s of this.stores.values()) { | ||
const { close, store } = s; | ||
close && promises.push(close(store.store)); | ||
} | ||
await Promise.all(promises); | ||
} | ||
} |
Oops, something went wrong.