@joingo/sdk-cache
Version:
SDK for JavaScript: Caching Application Block!
550 lines (505 loc) • 18.3 kB
text/typescript
/* ************************************************************************************************************************ *\
* SDK for JavaScript! *
* *
* COPYRIGHT © 2021 BEIJING JOINGO INFORMATION TECHNOLOGY CO., LTD. ALL RIGHTS RESERVED. *
* LICENSED UNDER THE MIT LICENSE. SEE LICENSE FILE IN THE PROJECT ROOT FOR FULL LICENSE INFORMATION. *
* *
* AUTHOR : WANG YUCAI *
* E-MAIL ADDRESS: WANGYUCAI@JOINGO.VIP *
* DATE TIME : 2021-12-31 12:27 *
\* ************************************************************************************************************************ */
// CODEFILE: cache.ts
// FEATURE: 提供了缓存相关的方法。
// FILE-VERSION: v2021.12.31-build.1227
import { createException, Nullable, sealed, queryable, isNull } from "@joingo/sdk-core";
import { computeHash, AES } from "@joingo/sdk-security";
import { Dayjs } from "dayjs";
import Store2, { StoreBase } from "store2";
/**
* 缓存过期模式。
*
* @export
* @enum {number}
*/
export enum ExpirationMode {
/**
* 永不过期。
*/
neverExpire = 0,
/**
* 绝对时间过期。
*/
absoluteTimeExpire = 1,
/**
* 滑动过期。
*/
slidingTimeExpire = 2,
/**
* 基于会话的。
*/
session = 3,
}
/**
* 定义了过期策略的接口。
*
* @export
* @interface IExpirationPolicy
*/
export interface IExpirationPolicy {
/**
* 获取一个值,用于表示过期模式。
*
* @type {ExpirationMode}
* @memberof IExpirationPolicy
* @readonly
*/
readonly mode: ExpirationMode;
/**
* 是否已经失效。
*
* @returns {boolean}
* @memberof IExpirationPolicy
*/
invalid(): boolean;
}
/**
* 提供了缓存过期策略相关的基本方法。
*
* @export
* @abstract
* @class ExpirationPolicy
* @implements {IExpirationPolicy}
*/
export abstract class ExpirationPolicy implements IExpirationPolicy {
/**
* 用于初始化一个 ExpirationPolicy 类型的对象实例。
* @param {ExpirationMode} [mode=ExpirationMode.neverExpire] 过期模式。
* @memberof ExpirationPolicy
*/
constructor(mode: ExpirationMode = ExpirationMode.neverExpire) {
this.mode = mode;
}
mode: ExpirationMode;
abstract invalid(): boolean;
}
/**
* 提供了基于回话的超时策略相关的方法。
*
* @export
* @class SessionExpirationPolicy
* @extends {ExpirationPolicy}
* @implements {IExpirationPolicy}
*/
export class SessionExpirationPolicy extends ExpirationPolicy implements IExpirationPolicy {
/**
* 用于初始化一个 SessionExpirationPolicy 类型的对象实例。
*
* @memberof SessionExpirationPolicy
*/
constructor() {
super(ExpirationMode.session);
}
invalid(): boolean {
return true;
}
}
/**
* 提供了永不过期相关的方法。密闭的,不可以从此类型派生。
*
* @export
* @class NeverExpirationPolicy
* @extends {ExpirationPolicy}
* @implements {IExpirationPolicy}
*/
export class NeverExpirationPolicy extends ExpirationPolicy implements IExpirationPolicy {
/**
* 用于初始化一个 NeverExpirationPolicy 类型的对象实例。
* @memberof NeverExpirationPolicy
*/
constructor() {
super(ExpirationMode.neverExpire);
}
invalid(): boolean {
return false;
}
}
/**
* 提供了绝对时间过期策略相关的方法。密闭的,不可以从此类型派生。
*
* @export
* @class AbsoluteTimeExpirationPolicy
* @extends {ExpirationPolicy}
* @implements {IExpirationPolicy}
*/
export class AbsoluteTimeExpirationPolicy extends ExpirationPolicy implements IExpirationPolicy {
/**
* 获取一个值,用于表示绝对超时时间。
*
* @type {Date}
* @memberof AbsoluteTimeExpirationPolicy
* @readonly
*/
readonly absoluteExpire: Date;
private readonly _absoluteTime: Dayjs;
private static readonly _DEFAULT_EXPIRE_MILLISECONDS: number = 1000 * 60 * 30;
/**
* 用于初始化一个 AbsoluteTimeExpirationPolicy 类型的对象实例。
* @param {Date} absoluteTime
* @memberof AbsoluteTimeExpirationPolicy
*/
constructor(absoluteTime: Date) {
super(ExpirationMode.absoluteTimeExpire);
this.absoluteExpire = absoluteTime;
this._absoluteTime = new Dayjs(absoluteTime);
}
invalid(): boolean {
return this._absoluteTime.isBefore(new Date());
}
/**
* 创建绝对超时策略。
*
* @static
* @param {number} [milliSeconds=AbsoluteTimeExpirationPolicy._DEFAULT_EXPIRE_MILLISECONDS] 毫秒值。用于表示多少毫秒后失效。默认为 30 分钟。
* @returns {IExpirationPolicy}
* @memberof AbsoluteTimeExpirationPolicy
*/
static createAbsoluteExpirationPolicy(milliSeconds: number = AbsoluteTimeExpirationPolicy._DEFAULT_EXPIRE_MILLISECONDS): IExpirationPolicy {
return new AbsoluteTimeExpirationPolicy(new Dayjs().add(milliSeconds).toDate());
}
}
/**
* 提供了滑动时间超时策略相关的方法。密闭的,不可以从此类型派生。
*
* @export
* @class SlidingTimeExpirationPolicy
* @extends {ExpirationPolicy}
* @implements {IExpirationPolicy}
*/
export class SlidingTimeExpirationPolicy extends ExpirationPolicy implements IExpirationPolicy {
private static readonly _DEFAULT_SLIDING_MILLISECONDS: number = 5 * 1000 * 60;
private readonly _slidingMilliSeconds: number;
private _executeTime: Date;
invalid(): boolean {
return new Dayjs(this._executeTime).add(this._slidingMilliSeconds).isBefore(new Date());
}
/**
* 用于初始化一个 SlidingTimeExpirationPolicy 类型的对象实例。
*
* @param {number} [slidingMilliSeconds=SlidingTimeExpirationPolicy._DEFAULT_SLIDING_MILLISECONDS] 滑动时间毫秒值。默认 5 分钟。
* @memberof SlidingTimeExpirationPolicy
*/
constructor(slidingMilliSeconds: number = SlidingTimeExpirationPolicy._DEFAULT_SLIDING_MILLISECONDS) {
super(ExpirationMode.slidingTimeExpire);
this._slidingMilliSeconds = slidingMilliSeconds;
this._executeTime = new Date();
}
/**
* 刷新时间。
*
* @memberof SlidingTimeExpirationPolicy
*/
refresh(): void {
console.debug(`[DEBUG]: 滑动更新策略刷新。`);
this._executeTime = new Date();
}
}
/**
* 定义了缓存项目类型。
*/
export type CacheItemConstructor = {
/**
* 设置或获取一个字符串,用于表示缓存标识。
*
* @type {string}
*/
key: string;
/**
* 设置或获取 * 类型的对象实例或值,用于表示需要缓存的数据。
*
* @type {*}
*/
value?: any;
/**
* 设置或获取一个值,用于表示是否加密存储。
*
* @type {boolean}
*/
secure?: boolean;
/**
* 设置或获取 IExpirationPolicy 类型的对象实例,用于表示缓存时效策略。
*
* @type {IExpirationPolicy}
*/
expire?: IExpirationPolicy;
};
const DEFAULT_CACHEITEM: CacheItemConstructor = {
key: "",
value: undefined,
secure: true,
expire: new SessionExpirationPolicy(),
};
/**
* 定义了内部缓存项类型的接口。
*/
type InternalCacheItemConstructor = {
/**
* 设置或获取一个字符串,用于表示缓存标识。
*
* @type {string}
*/
key: string;
/**
* 设置或获取一个值,用于表示是否加密存储。
*
* @type {boolean}
*/
secure: boolean;
/**
* 设置或获取一个值,用于表示缓存超时策略。
*
* @type {ExpirationMode}
*/
expire: ExpirationMode;
};
const DEFAULT_INTERNAL_CACHEITEM_KEY: string = "vip.joingo.sdk.caching.internalTable";
let _DEFAULT_CACHE_SECUREKEY: string = "dIg40xfb6mnCEOpGh8c5YsQvNPAwBZJX";
/**
* 配置全局缓存加密安全密钥。
*
* @export
* @param {string} key 密钥。
*/
export function configureCacheSecureKey(key: string) {
console.debug(`[DEBUG]: 尝试更新全局缓存加密密钥。`);
_DEFAULT_CACHE_SECUREKEY = key;
}
/**
* (异步的方法) 创建内部缓存标识。
*
* @returns {Promise<string>}
* @async
*/
function createInternalCacheKeyAsync(): Promise<string> {
return new Promise<string>((resolve, reject) => {
try {
resolve(computeHash(DEFAULT_INTERNAL_CACHEITEM_KEY));
} catch (error) {
console.error(`[ERROR]: 创建内部缓存标识名称失败。详情参见:%o`, error);
reject(error);
}
});
}
/**
* (异步的方法) 获取内部缓存数据表。
*
* @returns {Promise<InternalCacheItemConstructor[]>}
* @async
*/
function getInternalCacheItemAsync(): Promise<InternalCacheItemConstructor[]> {
return new Promise<InternalCacheItemConstructor[]>((resolve) => {
createInternalCacheKeyAsync()
.then((key) => {
if (Store2.local.has(key)) {
const table: InternalCacheItemConstructor[] = (JSON.parse(AES.decrypt(Store2.local.get(key) as string, _DEFAULT_CACHE_SECUREKEY)) as InternalCacheItemConstructor[]) ?? [];
resolve(table);
} else resolve([]);
})
.catch((error) => {
resolve([]);
});
});
}
/**
* 定义了数据缓存的接口。
*
* @export
* @interface ICache
*/
export interface ICache {
/**
* (异步的方法) 用于校验指定标识名称的缓存是否存在。
*
* @param {string} key 缓存标识名称。
* @returns {Promise<boolean>}
* @memberof ICache
* @async
*/
existsAsync(key: string): Promise<boolean>;
/**
* (异步的方法) 获取指定标识名称的缓存数据。
*
* @template T
* @param {string} key 缓存标识名称。
* @returns {Promise<Nullable<T>>}
* @memberof ICache
* @async
*/
getAsync<T>(key: string): Promise<Nullable<T>>;
/**
* (异步的方法) 设置缓存数据。
*
* @param {CacheItemConstructor} item 需要缓存的数据。
* @returns {Promise<void>}
* @memberof ICache
* @async
*/
setAsync(item: CacheItemConstructor): Promise<void>;
/**
* (异步的方法) 删除指定标识名称的缓存数据。
*
* @param {string} key 缓存标识名称。
* @returns {Promise<void>}
* @memberof ICache
* @async
*/
removeAsync(key: string): Promise<void>;
/**
* (异步的方法) 清空缓存数据。
*
* @returns {Promise<void>}
* @memberof ICache
* @async
*/
clearAsync(): Promise<void>;
}
/**
* 提供了数据缓存相关的方法。密闭的,不可以从此类型派生。
*
* @export
* @class Cache
* @implements {ICache}
*/
export class Cache implements ICache {
/**
* (异步的方法) 用于校验指定标识名称的缓存是否存在。
*
* @private
* @param {string} key 缓存标识。
* @returns {(Promise<{ exists: boolean, internalItem: InternalCacheItemConstructor | undefined; }>)}
* @memberof Cache
*/
private internalExistsAsync(key: string): Promise<{ exists: boolean, internalItem: InternalCacheItemConstructor | undefined; }> {
return new Promise<{ exists: boolean, internalItem: InternalCacheItemConstructor | undefined; }>((resolve) => {
getInternalCacheItemAsync().then((table) => {
const internalItem = queryable<InternalCacheItemConstructor>(table).singleOrDefault((item) => item.key === key);
if (!internalItem) {
console.debug(`[DEBUG]: "${key}" 未收录在 SDK 内部缓存表中。`);
resolve({ exists: Store2.local.has(key) || Store2.session.has(key), internalItem: undefined });
} else {
if (internalItem.secure) key = computeHash(key);
const cacheProvider: StoreBase = internalItem.expire === ExpirationMode.session ? Store2.session : Store2.local;
resolve({ exists: cacheProvider.has(key), internalItem });
}
});
});
}
existsAsync(key: string): Promise<boolean> {
const context = this;
return new Promise<boolean>((resolve) => {
context.internalExistsAsync(key).then(value => {
resolve(value.exists);
});
});
}
getAsync<T>(key: string): Promise<Nullable<T>> {
const context = this;
return new Promise<Nullable<T>>((resolve) => {
context.internalExistsAsync(key).then(value => {
if (!value.exists) resolve(new Nullable<T>());
else if (!value.internalItem) {
resolve(new Nullable<T>((Store2.local.get(key) ?? Store2.session.get(key)) as T));
}
else {
let _key = key;
if (value.internalItem.secure) _key = computeHash(key);
const storeBase: StoreBase = value.internalItem.expire === ExpirationMode.session ? Store2.session : Store2.local;
let cachedStr: string = storeBase.get(_key);
if (value.internalItem.secure)
cachedStr = AES.decrypt(cachedStr, _DEFAULT_CACHE_SECUREKEY);
const cachedItem: CacheItemConstructor = JSON.parse(cachedStr) as CacheItemConstructor;
if (cachedItem.expire?.invalid) {
console.warn(`[WARN]: 缓存数据 "${key}" 已经失效,因此返回 undefined。`);
return new Nullable<T>();
}
else {
if (cachedItem.expire?.mode === ExpirationMode.slidingTimeExpire) {
(cachedItem.expire as SlidingTimeExpirationPolicy).refresh();
cachedStr = JSON.stringify(cachedItem);
if (value.internalItem.secure) cachedStr = AES.encrypt(cachedStr, _DEFAULT_CACHE_SECUREKEY);
storeBase.set(_key, cachedStr, true);
}
return new Nullable<T>(cachedItem.value);
}
}
});
});
}
setAsync(item: CacheItemConstructor): Promise<void> {
return new Promise((resolve) => {
if (!item.value) resolve();
else {
getInternalCacheItemAsync().then(table => {
if (!item.expire) item.expire = new SessionExpirationPolicy();
let key: string = item.key;
let jsonStr: string = JSON.stringify(item);
if (item.secure) {
key = computeHash(key);
jsonStr = AES.encrypt(jsonStr, _DEFAULT_CACHE_SECUREKEY);
}
if (queryable(table).any(i => i.key === item.key)) {
console.debug(`[DEBUG]: 尝试更新内部缓存表中标识名称为 ${item.key} 的缓存项信息。`);
table.forEach(i => {
if (i.key === item.key) {
i.secure = item.secure ?? false;
i.expire = item.expire?.mode ?? ExpirationMode.session;
return;
}
});
}
else {
table.push({ key: item.key, secure: item.secure ?? false, expire: item.expire?.mode ?? ExpirationMode.session });
}
((item.expire?.mode ?? ExpirationMode.session) === ExpirationMode.session ? Store2.session : Store2.local).set(key, jsonStr, true);
Store2.local.set(computeHash(DEFAULT_INTERNAL_CACHEITEM_KEY), AES.encrypt(JSON.stringify(table), _DEFAULT_CACHE_SECUREKEY), true);
resolve();
});
}
});
}
removeAsync(key: string): Promise<void> {
const context = this;
return new Promise(resolve => {
context.internalExistsAsync(key).then(value => {
if (value.exists) {
if (!value.internalItem) {
Store2.local.remove(key);
Store2.session.remove(key);
resolve();
}
else {
let _key: string = key;
if (value.internalItem.secure) _key = computeHash(key);
Store2.local.remove(_key);
Store2.session.remove(_key);
getInternalCacheItemAsync().then(table => {
const _table = queryable(table).where(item => item.key != key).toArray();
Store2.local.set(computeHash(DEFAULT_INTERNAL_CACHEITEM_KEY), AES.encrypt(JSON.stringify(_table), _DEFAULT_CACHE_SECUREKEY), true);
resolve();
});
}
}
});
});
}
clearAsync(): Promise<void> {
return new Promise((resolve) => {
Store2.local.clear();
Store2.session.clear();
resolve();
});
}
}