UNPKG

@joingo/sdk-cache

Version:

SDK for JavaScript: Caching Application Block!

550 lines (505 loc) 18.3 kB
/* ************************************************************************************************************************ *\ * 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} */ @sealed 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} */ @sealed 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} */ @sealed 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} */ @sealed 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(); }); } }