diff --git a/src/core/cache/CHANGELOG.md b/src/core/cache/CHANGELOG.md index 1c114e8db..d1cade5bb 100644 --- a/src/core/cache/CHANGELOG.md +++ b/src/core/cache/CHANGELOG.md @@ -9,6 +9,21 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.0 (2022-0?-??) + +#### :boom: Breaking Change +* Changed typed parameters of `Cache` interface from `` to `` +* Change a default `Cache` interface key type + +#### :rocket: New Feature + +* Added new cache `DefaultCache` +* Added a new method `clone` + +#### :bug: Bug Fix + +* Fixed type inference in decorator + ## v3.50.0 (2021-06-07) #### :rocket: New Feature diff --git a/src/core/cache/README.md b/src/core/cache/README.md index 3cf3445fd..b7113e080 100644 --- a/src/core/cache/README.md +++ b/src/core/cache/README.md @@ -227,3 +227,29 @@ cache.clear(); console.log(cache.has('foo1')); // true ``` + +### clone + +Makes a new copy of the current cache and returns it. + +```js +import SimpleCache from 'core/cache/simple'; + +const + cache = new SimpleCache(), + obj = {foo: 'bar'}; + +cache.set('foo1', 'bar1'); +cache.set('foo2', obj); + +const + clonedCache = cache.clone(); + +console.log(cache !== clonedCache); // true + +console.log(clonedCache.has('foo1')); // true +console.log(clonedCache.has('foo2')); // true +console.log(clonedCache.has('foo5')); // false + +console.log(cache.get('foo2') === clonedCache.get('foo2')); // true +``` diff --git a/src/core/cache/decorators/helpers/add-emitter/CHANGELOG.md b/src/core/cache/decorators/helpers/add-emitter/CHANGELOG.md index 288ed22e4..49a6c9415 100644 --- a/src/core/cache/decorators/helpers/add-emitter/CHANGELOG.md +++ b/src/core/cache/decorators/helpers/add-emitter/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.? (2022-0?-??) + +#### :boom: Breaking Change + +* Change type parameters from `` to `` + +#### :rocket: New Feature + +* Added a new method `clone` processing + ## v3.59.1 (2021-09-21) #### :bug: Bug Fix diff --git a/src/core/cache/decorators/helpers/add-emitter/index.ts b/src/core/cache/decorators/helpers/add-emitter/index.ts index 6269653cf..bc30e0235 100644 --- a/src/core/cache/decorators/helpers/add-emitter/index.ts +++ b/src/core/cache/decorators/helpers/add-emitter/index.ts @@ -40,12 +40,12 @@ export const * * @param cache */ -const addEmitter: AddEmitter = , V = unknown, K extends string = string>(cache) => { +const addEmitter: AddEmitter = , K = unknown, V = unknown>(cache: T) => { const - expandedCache = , {[eventEmitter]?: EventEmitter}>>cache; + expandedCache = , {[eventEmitter]?: EventEmitter}>>cache; const - cacheWithEmitter = >expandedCache; + cacheWithEmitter = >expandedCache; let emitter; @@ -66,7 +66,10 @@ const addEmitter: AddEmitter = , V = unknown, K extends st originalRemove = cacheWithEmitter.remove, // eslint-disable-next-line @typescript-eslint/unbound-method - originalClear = cacheWithEmitter.clear; + originalClear = cacheWithEmitter.clear, + + // eslint-disable-next-line @typescript-eslint/unbound-method + originalClone = cacheWithEmitter.clone; if (originalSet[eventEmitter] == null) { cacheWithEmitter[$$.set] = originalSet; @@ -131,10 +134,29 @@ const addEmitter: AddEmitter = , V = unknown, K extends st originalClear = cacheWithEmitter[$$.clear] ?? originalClear; } + if (originalClone[eventEmitter] == null) { + cacheWithEmitter[$$.clone] = originalClone; + + cacheWithEmitter.clone = function clone(): Cache { + const + result = originalClone.call(this); + + emitter.emit('clone', cacheWithEmitter, {result}); + + return result; + }; + + cacheWithEmitter.clone[eventEmitter] = true; + + } else { + originalClone = cacheWithEmitter[$$.clone] ?? originalClone; + } + return >{ set: originalSet.bind(cacheWithEmitter), remove: originalRemove.bind(cacheWithEmitter), clear: originalClear.bind(cacheWithEmitter), + clone: originalClone.bind(cacheWithEmitter), subscribe: ((method, obj, cb): void => { emitter.on(method, handler); diff --git a/src/core/cache/decorators/helpers/add-emitter/interface.ts b/src/core/cache/decorators/helpers/add-emitter/interface.ts index 01f938e4e..9a862312f 100644 --- a/src/core/cache/decorators/helpers/add-emitter/interface.ts +++ b/src/core/cache/decorators/helpers/add-emitter/interface.ts @@ -14,7 +14,8 @@ import type Cache from 'core/cache/interface'; export type MethodsToWrap = 'set' | 'remove' | - 'clear'; + 'clear' | + 'clone'; export interface MutationEvent { args: Parameters; @@ -25,7 +26,11 @@ export interface MutationHandler { (e: MutationEvent): void; } -export interface CacheWithEmitter = Cache> extends Cache { +export interface CacheWithEmitter< + K = unknown, + V = unknown, + T extends Cache = Cache + > extends Cache { /** @override */ set(key: K, value: V, opts?: Parameters[2]): V; @@ -36,9 +41,9 @@ export interface CacheWithEmitter } export type AddEmitter = - , V = unknown, K extends string = string>(cache: T) => AddEmitterReturn; + , K = unknown, V = unknown>(cache: T) => AddEmitterReturn; -export interface AddEmitterReturn, V = unknown, K extends string = string> { +export interface AddEmitterReturn, K = unknown, V = unknown> { /** @see [[Cache.set]] */ set: T['set']; @@ -48,6 +53,9 @@ export interface AddEmitterReturn, V = unknown, K extends /** @see [[Cache.clear]] */ clear: T['clear']; + /** @see [[Cache.clone]] */ + clone: T['clone']; + /** * Subscribes for mutations of the specified cache object * diff --git a/src/core/cache/decorators/helpers/add-emitter/spec.js b/src/core/cache/decorators/helpers/add-emitter/spec.js index 01feff1f0..8f6f630bf 100644 --- a/src/core/cache/decorators/helpers/add-emitter/spec.js +++ b/src/core/cache/decorators/helpers/add-emitter/spec.js @@ -19,6 +19,7 @@ describe('core/cache/decorators/helpers/add-emitter', () => { this.remove = () => null; this.set = () => null; this.clear = () => null; + this.clone = () => null; } const @@ -147,4 +148,24 @@ describe('core/cache/decorators/helpers/add-emitter', () => { expect(memory[1]).toEqual(undefined); }); }); + + describe('clone', () => { + it('clones a cache', () => { + const + cache = new SimpleCache(), + {clone} = addEmitter(cache); + + const memory = []; + + cache[eventEmitter].on('clone', (...args) => { + memory.push(args); + }); + + cache.clone(); + expect(memory[0]).toEqual([cache, {result: new SimpleCache()}]); + + clone(); + expect(memory[1]).toEqual(undefined); + }); + }); }); diff --git a/src/core/cache/decorators/persistent/CHANGELOG.md b/src/core/cache/decorators/persistent/CHANGELOG.md index d9e2f441f..b3fae323d 100644 --- a/src/core/cache/decorators/persistent/CHANGELOG.md +++ b/src/core/cache/decorators/persistent/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.? (2022-0?-??) + +#### :boom: Breaking Change + +* Change type parameters from `` to `` + +#### :rocket: New Feature + +* Added a new method `clone` processing + ## v3.60.2 (2021-10-04) #### :bug: Bug Fix diff --git a/src/core/cache/decorators/persistent/engines/active.ts b/src/core/cache/decorators/persistent/engines/active.ts index 4d6b0e061..a3dc96ff3 100644 --- a/src/core/cache/decorators/persistent/engines/active.ts +++ b/src/core/cache/decorators/persistent/engines/active.ts @@ -13,12 +13,8 @@ import { INDEX_STORAGE_NAME } from 'core/cache/decorators/persistent/engines/con import { UncheckablePersistentEngine } from 'core/cache/decorators/persistent/engines/interface'; export default class ActivePersistentEngine extends UncheckablePersistentEngine { - /** - * Index with keys and TTL-s of stored values - */ - protected ttlIndex: Dictionary = Object.createDict(); - override async initCache(cache: Cache): Promise { + override async initCache(cache: Cache): Promise { if (await this.storage.has(INDEX_STORAGE_NAME)) { this.ttlIndex = (await this.storage.get>(INDEX_STORAGE_NAME))!; diff --git a/src/core/cache/decorators/persistent/engines/interface.ts b/src/core/cache/decorators/persistent/engines/interface.ts index f86097abd..a9cdf923b 100644 --- a/src/core/cache/decorators/persistent/engines/interface.ts +++ b/src/core/cache/decorators/persistent/engines/interface.ts @@ -16,10 +16,15 @@ export interface AbstractPersistentEngine { * Initializes a new cache instance from the past one * @param cache */ - initCache?(cache: Cache): CanPromise; + initCache?(cache: Cache): CanPromise; } export abstract class AbstractPersistentEngine { + /** + * Index with keys and TTL-s of stored values + */ + ttlIndex: Dictionary = Object.createDict(); + /** * API for async operations */ diff --git a/src/core/cache/decorators/persistent/engines/lazy.ts b/src/core/cache/decorators/persistent/engines/lazy.ts index 593a19368..9af03524b 100644 --- a/src/core/cache/decorators/persistent/engines/lazy.ts +++ b/src/core/cache/decorators/persistent/engines/lazy.ts @@ -23,8 +23,11 @@ export default class LazyPersistentEngine extends CheckablePersistentEngine( - cache: Cache, + cache: Cache, storage: SyncStorageNamespace | AsyncStorageNamespace, opts?: PersistentOptions -): Promise> => new PersistentWrapper, V>(cache, storage, opts).getInstance(); +): Promise> => + new PersistentWrapper, V>(cache, storage, opts).getInstance(); export default addPersistent; diff --git a/src/core/cache/decorators/persistent/interface.ts b/src/core/cache/decorators/persistent/interface.ts index 79f4bada6..85a1519e2 100644 --- a/src/core/cache/decorators/persistent/interface.ts +++ b/src/core/cache/decorators/persistent/interface.ts @@ -6,11 +6,12 @@ * https://github.com/V4Fire/Core/blob/master/LICENSE */ +import type { AsyncStorageNamespace, SyncStorageNamespace } from 'core/kv-storage'; import type { CacheWithEmitter } from 'core/cache/decorators/helpers/add-emitter/interface'; import type { eventEmitter } from 'core/cache/decorators/helpers/add-emitter'; -export type PersistentCache = CacheWithEmitter> = { - [key in Exclude<(keyof CacheWithEmitter), 'set' | 'size' | typeof eventEmitter>]: ReturnPromise[key]> +export type PersistentCache = CacheWithEmitter> = { + [key in Exclude<(keyof CacheWithEmitter), 'set' | 'size' | typeof eventEmitter>]: ReturnPromise[key]> } & { /** @see [[Cache.size]] */ size: [T['size']]; @@ -35,6 +36,8 @@ export type PersistentCache>; }; export interface PersistentTTLDecoratorOptions { diff --git a/src/core/cache/decorators/persistent/spec.js b/src/core/cache/decorators/persistent/spec.js index ae5dd1037..b51d42feb 100644 --- a/src/core/cache/decorators/persistent/spec.js +++ b/src/core/cache/decorators/persistent/spec.js @@ -7,7 +7,7 @@ */ import * as netModule from 'core/net'; -import { asyncLocal } from 'core/kv-storage'; +import { asyncLocal, asyncSession } from 'core/kv-storage'; import addPersistent from 'core/cache/decorators/persistent'; @@ -80,6 +80,32 @@ describe('core/cache/decorators/persistent', () => { expect(await asyncLocal.get(INDEX_STORAGE_NAME)).toEqual({bar: Number.MAX_SAFE_INTEGER}); }); + it('should clone the cache', async () => { + const opts = { + loadFromStorage: 'onInit' + }; + + const + persistentCache = await addPersistent(new SimpleCache(), asyncLocal, opts); + + await persistentCache.set('foo', 1, {persistentTTL: 100}); + await persistentCache.set('bar', 1, {persistentTTL: 10}); + + Date.now = () => 50; + + const + fakeClonedCache = persistentCache.clone(), + clonedCache = await persistentCache.cloneTo(asyncSession); + + expect(fakeClonedCache).toBe(undefined); + expect(await clonedCache.get('foo')).toBe(1); + expect(await clonedCache.get('bar')).toBe(undefined); + + expect(await asyncSession.get(INDEX_STORAGE_NAME)).toEqual({foo: 100}); + + Date.now = () => 0; + }); + it('`clear` caused by a side effect', async () => { const opts = { loadFromStorage: 'onInit' diff --git a/src/core/cache/decorators/persistent/wrapper.ts b/src/core/cache/decorators/persistent/wrapper.ts index d0a214c6e..ed6558870 100644 --- a/src/core/cache/decorators/persistent/wrapper.ts +++ b/src/core/cache/decorators/persistent/wrapper.ts @@ -7,6 +7,7 @@ */ import SyncPromise from 'core/promise/sync'; +import { unimplement } from 'core/functools'; import type { SyncStorageNamespace, AsyncStorageNamespace } from 'core/kv-storage'; import type Cache from 'core/cache/interface'; @@ -16,9 +17,15 @@ import engines from 'core/cache/decorators/persistent/engines'; import addEmitter from 'core/cache/decorators/helpers/add-emitter'; import type { PersistentEngine, CheckablePersistentEngine } from 'core/cache/decorators/persistent/engines/interface'; -import type { PersistentOptions, PersistentCache, PersistentTTLDecoratorOptions } from 'core/cache/decorators/persistent/interface'; +import type { -export default class PersistentWrapper, V = unknown> { + PersistentOptions, + PersistentCache, + PersistentTTLDecoratorOptions + +} from 'core/cache/decorators/persistent/interface'; + +export default class PersistentWrapper, V = unknown> { /** * Default TTL to store items */ @@ -32,7 +39,7 @@ export default class PersistentWrapper, V = unknown> /** * Wrapped cache object */ - protected readonly wrappedCache: PersistentCache; + protected readonly wrappedCache: PersistentCache; /** * Engine to save cache items within a storage @@ -44,6 +51,11 @@ export default class PersistentWrapper, V = unknown> */ protected readonly fetchedItems: Set = new Set(); + /** + * Object with incom + */ + protected readonly opts?: PersistentOptions; + /** * @param cache - cache object to wrap * @param storage - storage object to save cache items @@ -54,6 +66,7 @@ export default class PersistentWrapper, V = unknown> this.cache = cache; this.wrappedCache = Object.create(cache); + this.opts = opts; this.engine = new engines[opts?.loadFromStorage ?? 'onDemand'](storage); } @@ -61,7 +74,7 @@ export default class PersistentWrapper, V = unknown> /** * Returns an instance of the wrapped cache */ - async getInstance(): Promise> { + async getInstance(): Promise> { if (this.engine.initCache) { await this.engine.initCache(this.cache); } @@ -80,49 +93,121 @@ export default class PersistentWrapper, V = unknown> set: originalSet, clear: originalClear, subscribe - } = addEmitter(this.cache); - - this.wrappedCache.has = this.getDefaultImplementation('has'); - this.wrappedCache.get = this.getDefaultImplementation('get'); + } = addEmitter(this.cache); - this.wrappedCache.set = async (key: string, value: V, opts?: PersistentTTLDecoratorOptions & Parameters[2]) => { - const - ttl = opts?.persistentTTL ?? this.ttl; - - this.fetchedItems.add(key); - - const - res = originalSet(key, value, opts); + const descriptor = { + enumerable: false, + writable: true, + configurable: true + }; - if (this.cache.has(key)) { - await this.engine.set(key, value, ttl); - } + Object.defineProperties(this.wrappedCache, { + has: { + value: this.getDefaultImplementation('has'), + ...descriptor + }, - return res; - }; + get: { + value: this.getDefaultImplementation('get'), + ...descriptor + }, - this.wrappedCache.remove = async (key: string) => { - this.fetchedItems.add(key); - await this.engine.remove(key); - return originalRemove(key); - }; + set: { + value: async (key: string, value: V, opts?: PersistentTTLDecoratorOptions & Parameters[2]) => { + const + ttl = opts?.persistentTTL ?? this.ttl; - this.wrappedCache.keys = () => SyncPromise.resolve(this.cache.keys()); + this.fetchedItems.add(key); - this.wrappedCache.clear = async (filter?: ClearFilter) => { - const - removed = originalClear(filter), - removedKeys: string[] = []; + const + res = originalSet(key, value, opts); - removed.forEach((_, key) => { - removedKeys.push(key); - }); + if (this.cache.has(key)) { + await this.engine.set(key, value, ttl); + } - await Promise.allSettled(removedKeys.map((key) => this.engine.remove(key))); - return removed; - }; + return res; + }, + ...descriptor + }, - this.wrappedCache.removePersistentTTLFrom = (key) => this.engine.removeTTLFrom(key); + remove: { + value: async (key: string) => { + this.fetchedItems.add(key); + await this.engine.remove(key); + return originalRemove(key); + }, + ...descriptor + }, + + keys: { + value: () => SyncPromise.resolve(this.cache.keys()), + ...descriptor + }, + + clear: { + value: async (filter?: ClearFilter) => { + const + removed = originalClear(filter), + removedKeys: string[] = []; + + removed.forEach((_, key) => { + removedKeys.push(key); + }); + + await Promise.allSettled(removedKeys.map((key) => this.engine.remove(key))); + return removed; + }, + ...descriptor + }, + + clone: { + value: () => { + unimplement({ + type: 'function', + alternative: {name: 'cloneTo'} + }, this.wrappedCache.clone); + }, + ...descriptor + }, + + cloneTo: { + value: async ( + storage: SyncStorageNamespace | AsyncStorageNamespace + ): Promise> => { + const + cache = new PersistentWrapper, V>(this.cache.clone(), storage, {...this.opts}); + + Object.defineProperty(cache, 'fetchedItems', { + value: new Set(this.fetchedItems), + enumerable: true, + configurable: true, + writable: true + }); + + for (const [key, value] of this.cache.entries()) { + const + ttl = this.engine.ttlIndex[key] ?? 0, + time = Date.now(); + + if (ttl > time) { + await cache.engine.set(key, value, ttl - time); + + } else { + cache.cache.remove(key); + } + } + + return cache.getInstance(); + }, + ...descriptor + }, + + removePersistentTTLFrom: { + value: (key) => this.engine.removeTTLFrom(key), + ...descriptor + } + }); subscribe('remove', this.wrappedCache, ({args}) => this.engine.remove(args[0])); @@ -135,6 +220,9 @@ export default class PersistentWrapper, V = unknown> subscribe('clear', this.wrappedCache, ({result}) => { result.forEach((_, key) => this.engine.remove(key)); }); + + subscribe('clone', this.wrappedCache, () => + this.wrappedCache.clone()); } /** diff --git a/src/core/cache/decorators/ttl/CHANGELOG.md b/src/core/cache/decorators/ttl/CHANGELOG.md index 97a9316e2..7470ed9e8 100644 --- a/src/core/cache/decorators/ttl/CHANGELOG.md +++ b/src/core/cache/decorators/ttl/CHANGELOG.md @@ -9,6 +9,20 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.? (2022-0?-??) + +#### :bug: Bug Fix + +* Fixed type inference + +#### :boom: Breaking Change + +* Change type parameters from `` to `` + +#### :rocket: New Feature + +* Added a new method `clone` processing + ## v3.47.0 (2021-05-17) #### :bug: Bug Fix diff --git a/src/core/cache/decorators/ttl/index.ts b/src/core/cache/decorators/ttl/index.ts index 123d1c871..389d86ea9 100644 --- a/src/core/cache/decorators/ttl/index.ts +++ b/src/core/cache/decorators/ttl/index.ts @@ -22,8 +22,8 @@ export * from 'core/cache/decorators/ttl/interface'; /** * Wraps the specified cache object to add a feature of the cache expiring * - * @typeparam V - value type of the cache object * @typeparam K - key type of the cache object + * @typeparam V - value type of the cache object * * @param cache - cache object to wrap * @param ttl - default ttl value in milliseconds @@ -41,52 +41,98 @@ export * from 'core/cache/decorators/ttl/interface'; * ``` */ export default function addTTL< - T extends Cache, + T extends Cache, + K = unknown, V = unknown, - K extends string = string, ->(cache: T, ttl?: number): TTLCache> { +>(cache: Cache, ttl?: number): TTLCache> { // eslint-disable-next-line @typescript-eslint/unbound-method const { remove: originalRemove, set: originalSet, clear: originalClear, + clone: originalClone, subscribe - } = addEmitter, V, K>(>cache); + } = addEmitter, K, V>(>cache); const - cacheWithTTL: TTLCache = Object.create(cache), - ttlTimers = new Map(); - - cacheWithTTL.set = (key: K, value: V, opts?: TTLDecoratorOptions & Parameters[2]) => { - updateTTL(key, opts?.ttl); - return originalSet(key, value, opts); - }; + cacheWithTTL: TTLCache = Object.create(cache), + ttlTimers = new Map>]>(); - cacheWithTTL.remove = (key: K) => { - cacheWithTTL.removeTTLFrom(key); - return originalRemove(key); + const descriptor = { + enumerable: false, + writable: true, + configurable: true }; - cacheWithTTL.removeTTLFrom = (key: K) => { - if (ttlTimers.has(key)) { - clearTimeout(ttlTimers.get(key)); - ttlTimers.delete(key); - return true; + Object.defineProperties(cacheWithTTL, { + ttlTimers: { + get() { + return new Map(ttlTimers); + }, + enumerable: true + }, + + set: { + value: (key: K, value: V, opts?: TTLDecoratorOptions & Parameters[2]) => { + updateTTL(key, opts?.ttl); + return originalSet(key, value, opts); + }, + ...descriptor + }, + + remove: { + value: (key: K) => { + cacheWithTTL.removeTTLFrom(key); + return originalRemove(key); + }, + ...descriptor + }, + + removeTTLFrom: { + value: (key: K) => { + if (ttlTimers.has(key)) { + clearTimeout(ttlTimers.get(key)?.[0]); + ttlTimers.delete(key); + return true; + } + + return false; + }, + ...descriptor + }, + + clear: { + value: (filter?: ClearFilter) => { + const + removed = originalClear(filter); + + removed.forEach((_, key) => { + cacheWithTTL.removeTTLFrom(key); + }); + + return removed; + }, + ...descriptor + }, + + clone: { + value: () => { + const + cache = addTTL(originalClone(), ttl); + + for (const [key, [,promise]] of ttlTimers) { + void promise.then(() => { + if (!cache.ttlTimers.has(key)) { + cache.remove(key); + } + }); + } + + return cache; + }, + ...descriptor } - - return false; - }; - - cacheWithTTL.clear = (filter?: ClearFilter) => { - const - removed = originalClear(filter); - - removed.forEach((_, key) => { - cacheWithTTL.removeTTLFrom(key); - }); - - return removed; - }; + }); subscribe('remove', cacheWithTTL, ({args}) => cacheWithTTL.removeTTLFrom(args[0])); @@ -98,12 +144,22 @@ export default function addTTL< result.forEach((_, key) => cacheWithTTL.removeTTLFrom(key)); }); + subscribe('clone', cacheWithTTL, () => + cacheWithTTL.clone()); + return cacheWithTTL; function updateTTL(key: K, optionTTL?: number): void { if (optionTTL != null || ttl != null) { const time = optionTTL ?? ttl; - ttlTimers.set(key, setTimeout(() => cacheWithTTL.remove(key), time)); + + let timerId; + + const promise = new Promise>((resolve) => { + timerId = setTimeout(() => resolve(cacheWithTTL.remove(key)), time); + }); + + ttlTimers.set(key, [timerId, promise]); } else { cacheWithTTL.removeTTLFrom(key); diff --git a/src/core/cache/decorators/ttl/interface.ts b/src/core/cache/decorators/ttl/interface.ts index 0d7e4c95b..ddc7ffbbe 100644 --- a/src/core/cache/decorators/ttl/interface.ts +++ b/src/core/cache/decorators/ttl/interface.ts @@ -9,10 +9,15 @@ import type { CacheWithEmitter } from 'core/cache/decorators/helpers/add-emitter/interface'; export interface TTLCache< + K = unknown, V = unknown, - K = string, - T extends CacheWithEmitter = CacheWithEmitter -> extends CacheWithEmitter { + T extends CacheWithEmitter = CacheWithEmitter +> extends CacheWithEmitter { + /** + * Collection of cache's timers + */ + ttlTimers: Map>]>; + /** * Saves a value to the cache by the specified key * diff --git a/src/core/cache/decorators/ttl/spec.js b/src/core/cache/decorators/ttl/spec.js index bc5edc5e8..26948c5fb 100644 --- a/src/core/cache/decorators/ttl/spec.js +++ b/src/core/cache/decorators/ttl/spec.js @@ -12,7 +12,7 @@ import SimpleCache from 'core/cache/simple'; import RestrictedCache from 'core/cache/restricted'; describe('core/cache/decorators/ttl', () => { - it('should remove items after expiring', (done) => { + it.only('should remove items after expiring', (done) => { const cache = addTTL(new SimpleCache()); cache.set('foo', 1); @@ -118,6 +118,28 @@ describe('core/cache/decorators/ttl', () => { expect(memory).toEqual(['bar']); }); + it('should clone the cache with `ttl`', (done) => { + const + cache = addTTL(new SimpleCache()); + + cache.set('bar', 1, {ttl: 5}); + cache.set('baz', 1, {ttl: 10}); + + const + cloned = cache.clone(); + + expect(cloned.has('bar')).toBe(true); + + cloned.set('baz', 1, {ttl: 20}); + + setTimeout(() => { + expect(cloned.has('bar')).toBe(false); + expect(cloned.has('baz')).toBe(true); + + done(); + }, 15); + }); + it('`clear` caused by a side effect', () => { const originalCache = new SimpleCache(), diff --git a/src/core/cache/default/CHANGELOG.md b/src/core/cache/default/CHANGELOG.md new file mode 100644 index 000000000..26e302c16 --- /dev/null +++ b/src/core/cache/default/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.??.? (2022-0?-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/cache/default/README.md b/src/core/cache/default/README.md new file mode 100644 index 000000000..8a5805e90 --- /dev/null +++ b/src/core/cache/default/README.md @@ -0,0 +1,21 @@ +# core/cache/default + +This module provides a class for a [[Cache]] data structure with support for adding default value via the passed factory +when a non-existent key is accessed. + +```js +import DefaultCache from 'core/cache/default'; + +const + cache = new DefaultCache(Array); + +cache.get('key') + +console.log(cache.keys().length); // 1 +console.log(Object.isArray(cache.get('key'))); // true + +``` + +## API + +See [[Cache]]. diff --git a/src/core/cache/default/index.ts b/src/core/cache/default/index.ts new file mode 100644 index 000000000..ac6dea1c4 --- /dev/null +++ b/src/core/cache/default/index.ts @@ -0,0 +1,58 @@ +/*! + * V4Fire Core + * https://github.com/V4Fire/Core + * + * Released under the MIT license + * https://github.com/V4Fire/Core/blob/master/LICENSE + */ + +/** + * [[include:core/cache/default/README.md]] + * @packageDocumentation + */ + +import SimpleCache from 'core/cache/simple'; + +export * from 'core/cache/interface'; + +/** + * Implementation for a simple in-memory cache data structure + * + * @typeparam K - key type + * @typeparam V - value type + */ +export default class DefaultCache extends SimpleCache { + /** + * Function that returns a value which would be set as default value + */ + defaultFactory: () => V; + + /** + * @param [defaultFactory] - function that returns a value which would be set as default value + */ + constructor(defaultFactory: () => V) { + super(); + + this.defaultFactory = defaultFactory; + } + + /** @see [[Cache.get]] */ + override get(key: K): CanUndef { + if (!this.storage.has(key)) { + this.storage.set(key, this.defaultFactory()); + } + + return this.storage.get(key); + } + + /** @see [[Cache.clone]] */ + override clone(): DefaultCache { + const + newCache = new DefaultCache(this.defaultFactory), + mixin = {storage: new Map(this.storage)}; + + Object.assign(newCache, mixin); + + return newCache; + } +} diff --git a/src/core/cache/default/spec.js b/src/core/cache/default/spec.js new file mode 100644 index 000000000..5f532400c --- /dev/null +++ b/src/core/cache/default/spec.js @@ -0,0 +1,129 @@ +/*! + * V4Fire Core + * https://github.com/V4Fire/Core + * + * Released under the MIT license + * https://github.com/V4Fire/Core/blob/master/LICENSE + */ + +import DefaultCache from 'core/cache/default'; + +describe('core/cache/default', () => { + it('default value', () => { + const + cache = new DefaultCache(Array); + + expect(cache.has('foo')).toBe(false); + + expect(cache.get('foo')).toEqual([]); + expect(cache.size).toBe(1); + + cache.defaultFactory = () => 10; + + expect(cache.get('bla')).toEqual(10); + expect(cache.size).toBe(2); + + expect(cache.set('bar', 10)).toBe(10); + expect(cache.size).toBe(3); + }); + + it('crud', () => { + const + cache = new DefaultCache(); + + expect(cache.has('foo')).toBe(false); + expect(cache.set('foo', 1)).toBe(1); + expect(cache.get('foo')).toBe(1); + expect(cache.has('foo')).toBe(true); + expect(cache.size).toBe(1); + expect(cache.remove('foo')).toBe(1); + expect(cache.has('foo')).toBe(false); + }); + + it('default iterator', () => { + const + cache = new DefaultCache(); + + cache.set('1', 1); + cache.set('2', 2); + + expect(cache[Symbol.iterator]().next()).toEqual({value: '1', done: false}); + expect([...cache]).toEqual(['1', '2']); + }); + + it('`keys`', () => { + const + cache = new DefaultCache(); + + cache.set('1', 1); + cache.set('2', 2); + + expect([...cache.keys()]).toEqual(['1', '2']); + }); + + it('`values`', () => { + const + cache = new DefaultCache(); + + cache.set('1', 1); + cache.set('2', 2); + + expect([...cache.values()]).toEqual([1, 2]); + }); + + it('`entries`', () => { + const + cache = new DefaultCache(); + + cache.set('1', 1); + cache.set('2', 2); + + expect([...cache.entries()]).toEqual([['1', 1], ['2', 2]]); + }); + + it('`clear`', () => { + const + cache = new DefaultCache(); + + cache.set('foo', 1); + cache.set('bar', 2); + + expect(cache.has('foo')).toBe(true); + expect(cache.has('bar')).toBe(true); + + expect(cache.clear()).toEqual(new Map([['foo', 1], ['bar', 2]])); + }); + + it('`clear` with a filter', () => { + const + cache = new DefaultCache(); + + cache.set('foo', 1); + cache.set('bar', 2); + + expect(cache.has('foo')).toBe(true); + expect(cache.has('bar')).toBe(true); + + expect(cache.clear((el) => el > 1)).toEqual(new Map([['bar', 2]])); + }); + + it('`clones`', () => { + const + cache = new DefaultCache(() => 10), + obj = {a: 1}; + + cache.set('foo', 1); + cache.set('bar', obj); + + expect(cache.has('foo')).toBe(true); + expect(cache.has('bar')).toBe(true); + + const newCache = cache.clone(); + + expect(cache !== newCache).toBe(true); + expect(newCache.has('foo')).toBe(true); + expect(newCache.has('bar')).toBe(true); + expect(cache.storage !== newCache.storage).toBe(true); + expect(cache.get('bla') === newCache.get('bla')).toBe(true); + }); +}); diff --git a/src/core/cache/interface.ts b/src/core/cache/interface.ts index 9f3d27ba8..8762d6bbe 100644 --- a/src/core/cache/interface.ts +++ b/src/core/cache/interface.ts @@ -13,10 +13,10 @@ export interface ClearFilter { /** * Base interface for a cache data structure * + * @typeparam K - key type * @typeparam V - value type - * @typeparam K - key type (`string` by default) */ -export default interface Cache { +export default interface Cache { /** * Number of elements within the cache */ @@ -55,6 +55,11 @@ export default interface Cache { */ clear(filter?: ClearFilter): Map; + /** + * Clones the cache and returns a cloned one + */ + clone(): Cache; + /** * Returns an iterator by the cache keys */ diff --git a/src/core/cache/never/CHANGELOG.md b/src/core/cache/never/CHANGELOG.md index 5dde52bda..06dc24f45 100644 --- a/src/core/cache/never/CHANGELOG.md +++ b/src/core/cache/never/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.? (2022-0?-??) + +#### :boom: Breaking Change + +* Change type parameters from `` to `` + +#### :rocket: New Feature + +* Added a new `clone` method + ## v3.50.0 (2021-06-07) #### :rocket: New Feature diff --git a/src/core/cache/never/index.ts b/src/core/cache/never/index.ts index 148b9fe31..6beaccda6 100644 --- a/src/core/cache/never/index.ts +++ b/src/core/cache/never/index.ts @@ -21,7 +21,7 @@ export * from 'core/cache/interface'; /** * Loopback class for a cache data structure */ -export default class NeverCache implements Cache { +export default class NeverCache implements Cache { /** @see [[Cache.size]] */ get size(): number { return this.storage.size; @@ -75,4 +75,9 @@ export default class NeverCache implements Cache { clear(filter?: ClearFilter): Map { return new Map(); } + + /** @see [[Cache.clone]] */ + clone(): NeverCache { + return new NeverCache(); + } } diff --git a/src/core/cache/never/spec.js b/src/core/cache/never/spec.js index 78755078e..fb1d9b5aa 100644 --- a/src/core/cache/never/spec.js +++ b/src/core/cache/never/spec.js @@ -78,4 +78,19 @@ describe('core/cache/never', () => { expect(cache.clear((el) => el > 1)).toEqual(new Map([])); }); + + it('`clones`', () => { + const + cache = new NeverCache(); + + cache.set('foo', 1); + expect(cache.has('foo')).toBe(false); + + const + newCache = cache.clone(); + + expect(cache !== newCache).toBe(true); + expect(newCache.has('foo')).toBe(false); + expect(cache.storage !== newCache.storage).toBe(true); + }); }); diff --git a/src/core/cache/restricted/CHANGELOG.md b/src/core/cache/restricted/CHANGELOG.md index 095519926..e801887c0 100644 --- a/src/core/cache/restricted/CHANGELOG.md +++ b/src/core/cache/restricted/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.? (2022-0?-??) + +#### :boom: Breaking Change + +* Change type parameters from `` to `` + +#### :rocket: New Feature + +* Added a new `clone` method + ## v3.xx.x (2021-06-01) #### :rocket: New Feature diff --git a/src/core/cache/restricted/index.ts b/src/core/cache/restricted/index.ts index 96482391a..29d159d88 100644 --- a/src/core/cache/restricted/index.ts +++ b/src/core/cache/restricted/index.ts @@ -21,7 +21,7 @@ export * from 'core/cache/simple'; * @typeparam V - value type * @typeparam K - key type (`string` by default) */ -export default class RestrictedCache extends SimpleCache { +export default class RestrictedCache extends SimpleCache { /** * Queue object */ @@ -92,6 +92,16 @@ export default class RestrictedCache extends SimpleCach return removed; } + override clone(): RestrictedCache { + const + newCache = new RestrictedCache(this.capacity), + mixin = {queue: new Set(this.queue), storage: new Map(this.storage)}; + + Object.assign(newCache, mixin); + + return newCache; + } + /** * Sets a new capacity of the cache. * The method returns a map of truncated elements that the cache can't fit anymore. diff --git a/src/core/cache/restricted/spec.js b/src/core/cache/restricted/spec.js index c9f5e1c8c..afc52dcf3 100644 --- a/src/core/cache/restricted/spec.js +++ b/src/core/cache/restricted/spec.js @@ -124,4 +124,25 @@ describe('core/cache/restricted', () => { expect(cache.has('foo')).toBe(false); expect(cache.has('bar')).toBe(false); }); + + it('`clones`', () => { + const + cache = new RestrictedCache(), + obj = {a: 1}; + + cache.set('foo', 1); + cache.set('bar', obj); + + expect(cache.has('foo')).toBe(true); + expect(cache.has('bar')).toBe(true); + + const newCache = cache.clone(); + + expect(cache !== newCache).toBe(true); + expect(newCache.has('foo')).toBe(true); + expect(newCache.has('bar')).toBe(true); + expect(cache.storage !== newCache.storage).toBe(true); + expect(cache.queue !== newCache.queue).toBe(true); + expect(cache.get('bar') === newCache.get('bar')).toBe(true); + }); }); diff --git a/src/core/cache/simple/CHANGELOG.md b/src/core/cache/simple/CHANGELOG.md index ea3c6ac4f..e59fc3ef4 100644 --- a/src/core/cache/simple/CHANGELOG.md +++ b/src/core/cache/simple/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.? (2022-0?-??) + +#### :boom: Breaking Change + +* Change type parameters from `` to `` + +#### :rocket: New Feature + +* Added a new `clone` method + ## v3.20.0 (2020-07-05) #### :boom: Breaking Change diff --git a/src/core/cache/simple/index.ts b/src/core/cache/simple/index.ts index 775246c09..279689e52 100644 --- a/src/core/cache/simple/index.ts +++ b/src/core/cache/simple/index.ts @@ -19,10 +19,10 @@ export * from 'core/cache/interface'; /** * Implementation for a simple in-memory cache data structure * + * @typeparam K - key type * @typeparam V - value type - * @typeparam K - key type (`string` by default) */ -export default class SimpleCache implements Cache { +export default class SimpleCache implements Cache { /** @see [[Cache.size]] */ get size(): number { return this.storage.size; @@ -102,4 +102,15 @@ export default class SimpleCache implements Cache this.storage.clear(); return removed; } + + /** @see [[Cache.clone]] */ + clone(): SimpleCache { + const + newCache = new SimpleCache(), + mixin = {storage: new Map(this.storage)}; + + Object.assign(newCache, mixin); + + return newCache; + } } diff --git a/src/core/cache/simple/spec.js b/src/core/cache/simple/spec.js index b68232428..fa54b9d69 100644 --- a/src/core/cache/simple/spec.js +++ b/src/core/cache/simple/spec.js @@ -84,4 +84,24 @@ describe('core/cache/simple', () => { expect(cache.clear((el) => el > 1)).toEqual(new Map([['bar', 2]])); }); + + it('`clones`', () => { + const + cache = new SimpleCache(), + obj = {a: 1}; + + cache.set('foo', 1); + cache.set('bar', obj); + + expect(cache.has('foo')).toBe(true); + expect(cache.has('bar')).toBe(true); + + const newCache = cache.clone(); + + expect(cache !== newCache).toBe(true); + expect(newCache.has('foo')).toBe(true); + expect(newCache.has('bar')).toBe(true); + expect(cache.storage !== newCache.storage).toBe(true); + expect(cache.get('bar') === newCache.get('bar')).toBe(true); + }); }); diff --git a/src/core/request/modules/context/modules/params.ts b/src/core/request/modules/context/modules/params.ts index 13d0243ee..0b881d7d1 100644 --- a/src/core/request/modules/context/modules/params.ts +++ b/src/core/request/modules/context/modules/params.ts @@ -73,12 +73,12 @@ export default class RequestContext { /** * Storage to cache the resolved request */ - readonly cache!: AbstractCache>; + readonly cache!: AbstractCache>; /** * Storage to cache the request while it is pending a response */ - readonly pendingCache: AbstractCache< + readonly pendingCache: AbstractCache> | RequestResponse > = Object.cast(pendingCache);