diff --git a/lib/CacheMapper.ts b/lib/CacheMapper.ts new file mode 100644 index 00000000..c9cbc1dc --- /dev/null +++ b/lib/CacheMapper.ts @@ -0,0 +1,161 @@ +/** @internal */ +export class CacheMapper implements Map { + private readonly _innerMap: Map; + readonly size: number; + + constructor() { + this._innerMap = new Map(); + } + clear(): void { + this._innerMap.clear(); + } + delete(key: K): boolean { + throw this._innerMap.delete(key); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: any): void { + return this._innerMap.forEach(callbackfn, thisArg); + } + entries(): IterableIterator<[K, V]> { + return this._innerMap.entries(); + } + keys(): IterableIterator { + return this._innerMap.keys(); + } + values(): IterableIterator { + return this._innerMap.values(); + } + [Symbol.iterator](): IterableIterator<[K, V]> { + return this._innerMap.entries(); + } + [Symbol.toStringTag]: string; + + typeof() { + return typeof this._innerMap; + } + + set(id: K, value: V) { + this._innerMap.set(id, value); + // set cache size while set new value + // this.size = this._innerMap.size; + return this; + } + + has(id: K) { + return this._innerMap.has(id); + } + + get(id: K) { + return this._innerMap.get(id); + } + + del(id: K) { + this._innerMap.delete(id); + // set cache size while delete value + // this.size = this._innerMap.size; + } +} + +/** + * Generic Mutable Cache with `Map` + * * A Map holds key-value pairs where the keys can be any datatype (Generic) + * @example + * import { Cache } from 'hexo-util'; + * const c = new Cache(); + * // error + * c.set('key', 'xxxx'); // cache value must be instance of number + * // pass + * c.set('key', 1); + */ +export class Cache { + private cache: CacheMapper; + constructor() { + this.cache = new CacheMapper(); + } + + /** + * check cache is exist with given key + * @param key cache key string + * @returns + */ + has(key: string) { + return this.cache.has(key); + } + + /** + * get cache + * @param key + * @returns + */ + get(key: string) { + return this.cache.get(key); + } + + /** + * set cache + * @param key + * @param value cache value must same as constructor generic type + * @returns + */ + set(key: string, value: V) { + return this.cache.set(key, value); + } + + /** + * dump cache + * @returns + */ + dump() { + return Object.fromEntries(this.cache); + } + + /** + * get cache total + * @returns + */ + size() { + return Array.from(this.cache.keys()).length; + } + + /** + * cacheable setter non-function + * * new value will never updated when previous key already exist + * @param key cache key string + * @param value cache value must same as constructor generic type + */ + apply(key: string, value: V): V; + + /** + * cacheable setter with function + * * new value will never updated when previous key already exist + * @param key cache key string + * @param value cache value must same as constructor generic type + */ + apply(key: string, value: () => V): V; + + /** + * cacheable setter + * * new value will never updated when previous key already exist + * @param key cache key string + * @param value cache value must same as constructor generic type + */ + apply(id: string, value: V & (() => V)) { + if (this.has(id)) return this.get(id); + let newValue: V; + if (typeof value === 'function') { + newValue = value(); + } else { + newValue = value; + } + + this.cache.set(id, newValue); + return newValue; + } + + del(key: string) { + return this.cache.del(key); + } + flush() { + this.cache.clear(); + } +} diff --git a/lib/cache.ts b/lib/cache.ts index 4b0de14a..01164dff 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -1,44 +1,3 @@ -export = class Cache { - cache: Map; +import { Cache } from './CacheMapper'; - constructor() { - this.cache = new Map(); - } - - set(id: string, value: T) { - this.cache.set(id, value); - } - - has(id: string) { - return this.cache.has(id); - } - - get(id: string) { - return this.cache.get(id); - } - - del(id: string) { - this.cache.delete(id); - } - - apply(id: string, value): T { - if (this.has(id)) return this.get(id); - - if (typeof value === 'function') value = value(); - - this.set(id, value); - return value; - } - - flush() { - this.cache.clear(); - } - - size() { - return this.cache.size; - } - - dump() { - return Object.fromEntries(this.cache); - } -}; +export = Cache; diff --git a/package.json b/package.json index 88c3b8ba..1ceceea9 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "pretest": "npm run clean && npm run build", "test": "mocha --require ts-node/register", "test-cov": "nyc --reporter=lcovonly npm run test", + "test-single": "npm run pretest && mocha --require ts-node/register --exit --grep", "build:highlight": "node scripts/build_highlight_alias.js", "postinstall": "npm run build:highlight" }, @@ -34,7 +35,9 @@ ], "license": "MIT", "devDependencies": { + "@types/chai": "^4.3.5", "@types/cross-spawn": "^6.0.2", + "@types/mocha": "^10.0.1", "@types/node": "^18.11.8", "@types/prismjs": "^1.26.0", "@typescript-eslint/eslint-plugin": "^5.41.0", diff --git a/test/.eslintrc.json b/test/.eslintrc.json index 26f258b3..db9548e4 100644 --- a/test/.eslintrc.json +++ b/test/.eslintrc.json @@ -1,7 +1,16 @@ { "extends": "hexo/test", + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "node/no-unsupported-features/es-syntax": 0, + "@typescript-eslint/no-non-null-assertion": 0 + } + } + ], "rules": { "@typescript-eslint/no-var-requires": 0, "@typescript-eslint/no-empty-function": 0 } -} \ No newline at end of file +} diff --git a/test/cache-number.spec.ts b/test/cache-number.spec.ts new file mode 100644 index 00000000..eb991319 --- /dev/null +++ b/test/cache-number.spec.ts @@ -0,0 +1,49 @@ +'use strict'; + +import { describe, it } from 'mocha'; +import * as Hutil from '../lib'; +import { expect } from 'chai'; + +// to run single test +// npm run test-single -- "Cache - number" + +describe('Cache - number', () => { + const cache = new Hutil.Cache(); + const dumpExpect = { foo: 1, bar: 2 }; + + it('should be number', () => { + // apply non-function + expect(cache.apply('foo', 1)).to.be.an('number'); + // apply with function + expect(cache.apply('bar', () => 2)).to.be.an('number'); + // validate cache exist + expect(cache.has('foo') && cache.has('bar')).to.be.true; + }); + + it('add another and delete it', () => { + // add `another` + expect(cache.apply('another', 3)).to.equal(3); + // size should be 3 + expect(cache.size()).to.equal(3); + // add with function + expect(cache.apply('another', () => 3)).to.equal(3); + // size should be still 3 + expect(cache.size()).to.equal(3); + // delete `another` + cache.del('another'); + }); + + it('final size should be 2', () => { + // final size should be 2 + expect(cache.size()).to.equal(2); + }); + + it('should dump matches', () => { + expect(cache.dump()).deep.equal(dumpExpect); + }); + + it('should be empty after flush', () => { + cache.flush(); + expect(cache.size()).to.be.equal(0); + }); +}); diff --git a/test/cache-object.spec.ts b/test/cache-object.spec.ts new file mode 100644 index 00000000..674f6d10 --- /dev/null +++ b/test/cache-object.spec.ts @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use strict'; + +import { Cache } from '../lib'; +import { describe, it } from 'mocha'; +import { expect, should } from 'chai'; + +// to run single test +// npm run test-single -- "Cache - object" + +interface HexoLocals { + page: { + path: string; + }; + path: string; + url: string; + config: Record & { + relative_link: boolean; + }; + theme: Record; + layout: string; + env: any; + view_dir: string; + site: Record; + cache?: boolean; +} + +describe('Cache - object', () => { + const cache = new Cache(); + const value: HexoLocals = { + page: { + path: 'dummy/path/post.md' + }, + path: 'dummy/path/post.md', + url: 'http://example.com/', + config: { + relative_link: true + }, + theme: {}, + layout: '', + env: {}, + view_dir: '', + site: {} + }; + const valueMap = new Map(Object.entries(value)); + const cacheMap = new Cache(); + // built-in Set same as Array + const valueSet = new Set(Object.keys(value)); + const cacheSet = new Cache(); + + describe('plain object', () => { + it('set', () => { + cache.set('foo', value); + should().equal(cache.size(), 1); + }); + + it('apply', () => { + expect(cache.has('bar')).to.be.false; + // should applied and return the same value + should().equal(cache.apply('bar', value), value); + // should not apply new value + should().equal(cache.apply('bar', {} as typeof value), value); + should().equal(cache.size(), 2); + }); + }); + + describe('object Map', () => { + it('set', () => { + cacheMap.set('foo', valueMap); + should().equal(cacheMap.size(), 1); + }); + it('apply', () => { + // should applied and return the same value + should().equal(cacheMap.apply('bar', valueMap), valueMap); + }); + it('is valid Map', () => { + const targetValue = cacheMap.get('bar'); + // built-in Map validate + expect(targetValue! instanceof Map).to.be.true; + expect('has' in targetValue!).to.be.true; + expect(targetValue?.has('page')).to.be.true; + // targetValue.page should same as value.page + should().equal(targetValue?.get('page'), value.page); + }); + }); + + describe('object Set', () => { + it('set', () => { + cacheSet.set('foo', valueSet); + should().equal(cacheSet.size(), 1); + }); + it('apply', () => { + // should applied and return the same value + should().equal(cacheSet.apply('bar', valueSet), valueSet); + }); + it('is valid Set', () => { + const targetValue = cacheSet.get('bar'); + // built-in Map validate + expect(targetValue! instanceof Set).to.be.true; + expect(targetValue?.has('page')).to.be.true; + }); + }); + + it('size 0 after flush', () => { + cache.flush(); + expect(cache.size()).to.equal(0); + cacheMap.flush(); + expect(cacheMap.size()).to.equal(0); + }); +}); diff --git a/test/cache-typescript.spec.ts b/test/cache-typescript.spec.ts new file mode 100644 index 00000000..ea97d58a --- /dev/null +++ b/test/cache-typescript.spec.ts @@ -0,0 +1,11 @@ +'use strict'; + +import { describe } from 'mocha'; + +// to run single test +// npm run test-single -- "Cache - Typescript" + +describe('Cache - Typescript', async () => { + await import('./cache-number.spec'); + await import('./cache-object.spec'); +}); diff --git a/test/cache.spec.js b/test/cache.spec.js index 2864825a..29219e53 100644 --- a/test/cache.spec.js +++ b/test/cache.spec.js @@ -1,63 +1,74 @@ 'use strict'; -require('chai').should(); +const chai = require('chai'); +const should = chai.should(); +const expect = chai.expect; describe('Cache', () => { + // const Cache = require('../dist').Cache; // <-- this also works const Cache = require('../dist/cache'); const cache = new Cache(); it('get & set', () => { cache.set('foo', 123); - cache.get('foo').should.eql(123); + should.equal(cache.get('foo'), 123); }); it('size', () => { cache.set('foobar', 456); - cache.size().should.eql(2); + should.equal(cache.size(), 2); }); it('has', () => { - cache.has('foo').should.eql(true); - cache.has('bar').should.eql(false); + should.equal(cache.has('foo'), true); + should.equal(cache.has('bar'), false); }); it('apply - non function', () => { - cache.apply('bar', 123).should.eql(123); - cache.apply('bar', 456).should.eql(123); - - cache.apply('foo', 456).should.eql(123); + should.equal(cache.apply('bar', 123), 123); + should.equal(cache.apply('bar', 456), 123); + should.equal(cache.apply('foo', 456), 123); }); it('apply - function', () => { - cache.apply('baz', () => 123).should.eql(123); - cache.apply('baz', () => 456).should.eql(123); + should.equal( + cache.apply('baz', () => 123), + 123 + ); + should.equal( + cache.apply('baz', () => 456), + 123 + ); }); it('dump', () => { - cache.dump().should.eql({ - 'bar': 123, - 'baz': 123, - 'foo': 123, - 'foobar': 456 + expect(cache.dump()).to.include({ + bar: 123, + baz: 123, + foo: 123, + foobar: 456 }); }); it('del', () => { cache.del('baz'); - cache.has('foo').should.eql(true); - cache.has('baz').should.eql(false); + should.equal(cache.has('foo'), true); + should.equal(cache.has('baz'), false); }); it('flush', () => { cache.flush(); - cache.has('foo').should.eql(false); - cache.has('bar').should.eql(false); - cache.has('baz').should.eql(false); - cache.size().should.eql(0); + should.equal(cache.has('foo'), false); + should.equal(cache.has('bar'), false); + should.equal(cache.has('baz'), false); + should.equal(cache.size(), 0); }); it('cache null', () => { cache.apply('foo', null); - (cache.apply('foo', 123) === null).should.eql(true); + should.equal(cache.apply('foo', 123) === null, true); }); + + // include typescript test + require('./cache-typescript.spec.ts'); });