-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): Initial DefaultCachePlugin implementation
Relates to #3043. This plugin implements a simple SQL cache strategy to store cache items in the main database. The implementation needs further testing and potential performance optimization.
- Loading branch information
1 parent
3603b11
commit 9c2433f
Showing
5 changed files
with
159 additions
and
0 deletions.
There are no files selected for viewing
21 changes: 21 additions & 0 deletions
21
packages/core/src/plugin/default-cache-plugin/cache-item.entity.ts
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,21 @@ | ||
import { DeepPartial } from '@vendure/common/lib/shared-types'; | ||
import { Column, Entity, Index } from 'typeorm'; | ||
|
||
import { VendureEntity } from '../../entity/base/base.entity'; | ||
|
||
@Entity() | ||
export class CacheItem extends VendureEntity { | ||
constructor(input: DeepPartial<CacheItem>) { | ||
super(input); | ||
} | ||
|
||
@Index('cache_item_key') | ||
@Column({ unique: true }) | ||
key: string; | ||
|
||
@Column('text') | ||
value: string; | ||
|
||
@Column({ nullable: true }) | ||
expiresAt?: Date; | ||
} |
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 @@ | ||
export const PLUGIN_INIT_OPTIONS = Symbol('PLUGIN_INIT_OPTIONS'); |
33 changes: 33 additions & 0 deletions
33
packages/core/src/plugin/default-cache-plugin/default-cache-plugin.ts
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,33 @@ | ||
import { PluginCommonModule } from '../plugin-common.module'; | ||
import { VendurePlugin } from '../vendure-plugin'; | ||
|
||
import { CacheItem } from './cache-item.entity'; | ||
import { PLUGIN_INIT_OPTIONS } from './constants'; | ||
import { SqlCacheStrategy } from './sql-cache-strategy'; | ||
|
||
export interface DefaultCachePluginInitOptions { | ||
cacheSize?: number; | ||
} | ||
|
||
@VendurePlugin({ | ||
imports: [PluginCommonModule], | ||
entities: [CacheItem], | ||
providers: [{ provide: PLUGIN_INIT_OPTIONS, useFactory: () => DefaultCachePlugin.options }], | ||
configuration: config => { | ||
config.systemOptions.cacheStrategy = new SqlCacheStrategy({ | ||
cacheSize: DefaultCachePlugin.options.cacheSize, | ||
}); | ||
return config; | ||
}, | ||
compatibility: '>0.0.0', | ||
}) | ||
export class DefaultCachePlugin { | ||
static options: DefaultCachePluginInitOptions = { | ||
cacheSize: 10_000, | ||
}; | ||
|
||
static init(options: DefaultCachePluginInitOptions) { | ||
this.options = options; | ||
return DefaultCachePlugin; | ||
} | ||
} |
102 changes: 102 additions & 0 deletions
102
packages/core/src/plugin/default-cache-plugin/sql-cache-strategy.ts
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,102 @@ | ||
import { JsonCompatible } from '@vendure/common/lib/shared-types'; | ||
|
||
import { Injector } from '../../common/index'; | ||
import { ConfigService, Logger } from '../../config/index'; | ||
import { CacheStrategy, SetCacheKeyOptions } from '../../config/system/cache-strategy'; | ||
import { TransactionalConnection } from '../../connection/index'; | ||
|
||
import { CacheItem } from './cache-item.entity'; | ||
|
||
/** | ||
* A {@link CacheStrategy} that stores the cache in memory using a simple | ||
* JavaScript Map. | ||
* | ||
* **Caution** do not use this in a multi-instance deployment because | ||
* cache invalidation will not propagate to other instances. | ||
* | ||
* @since 3.1.0 | ||
*/ | ||
export class SqlCacheStrategy implements CacheStrategy { | ||
protected cacheSize = 10_000; | ||
|
||
constructor(config?: { cacheSize?: number }) { | ||
if (config?.cacheSize) { | ||
this.cacheSize = config.cacheSize; | ||
} | ||
} | ||
|
||
protected connection: TransactionalConnection; | ||
protected configService: ConfigService; | ||
|
||
init(injector: Injector) { | ||
this.connection = injector.get(TransactionalConnection); | ||
this.configService = injector.get(ConfigService); | ||
} | ||
|
||
async get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined> { | ||
const hit = await this.connection.rawConnection.getRepository(CacheItem).findOne({ | ||
where: { | ||
key, | ||
}, | ||
}); | ||
|
||
if (hit) { | ||
const now = new Date().getTime(); | ||
if (!hit.expiresAt || (hit.expiresAt && now < hit.expiresAt.getTime())) { | ||
try { | ||
return JSON.parse(hit.value); | ||
} catch (e: any) { | ||
/* */ | ||
} | ||
} else { | ||
await this.connection.rawConnection.getRepository(CacheItem).delete({ | ||
key, | ||
}); | ||
} | ||
} | ||
} | ||
|
||
async set<T extends JsonCompatible<T>>(key: string, value: T, options?: SetCacheKeyOptions) { | ||
const cacheSize = await this.connection.rawConnection.getRepository(CacheItem).count(); | ||
if (cacheSize > this.cacheSize) { | ||
// evict oldest | ||
const subQuery1 = this.connection.rawConnection | ||
.getRepository(CacheItem) | ||
.createQueryBuilder('item') | ||
.select('item.id', 'item_id') | ||
.orderBy('item.updatedAt', 'DESC') | ||
.limit(1000) | ||
.offset(this.cacheSize); | ||
const subQuery2 = this.connection.rawConnection | ||
.createQueryBuilder() | ||
.select('t.item_id') | ||
.from(`(${subQuery1.getQuery()})`, 't'); | ||
const qb = this.connection.rawConnection | ||
.getRepository(CacheItem) | ||
.createQueryBuilder('cache_item') | ||
.delete() | ||
.from(CacheItem, 'cache_item') | ||
.where(`cache_item.id IN (${subQuery2.getQuery()})`); | ||
|
||
try { | ||
await qb.execute(); | ||
} catch (e: any) { | ||
Logger.error(`An error occured when attempting to prune the cache: ${e.message as string}`); | ||
} | ||
} | ||
await this.connection.rawConnection.getRepository(CacheItem).upsert( | ||
new CacheItem({ | ||
key, | ||
value: JSON.stringify(value), | ||
expiresAt: options?.ttl ? new Date(new Date().getTime() + options.ttl) : undefined, | ||
}), | ||
['key'], | ||
); | ||
} | ||
|
||
async delete(key: string) { | ||
await this.connection.rawConnection.getRepository(CacheItem).delete({ | ||
key, | ||
}); | ||
} | ||
} |
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