diff --git a/README.md b/README.md index 2a37ea8..0ac9bc9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ --- -Common TypeScript code I used in multiple app. +Simply and generic TypeScript utils. ![Check](https://github.com/jokester/ts-commonutil/workflows/Check/badge.svg) [![codecov](https://codecov.io/gh/jokester/ts-commonutil/graph/badge.svg?token=95f53H027x)](https://codecov.io/gh/jokester/ts-commonutil) @@ -10,10 +10,14 @@ Common TypeScript code I used in multiple app. ## How to Use -``` -yarn add @jokester/ts-commonutil +install `@jokester/ts-commonutil` from NPM. -``` +## Not included but my go-to libraries + +- LRU: [lru-cache](https://www.npmjs.com/package/lru-cache) +- fp: [fp-ts](https://www.npmjs.com/package/fp-ts) +- React hooks + - [foxact](https://foxact.skk.moe/) ## Content diff --git a/src/algebra/monoid.spec.ts b/obsolete/algebra/monoid.spec.ts similarity index 100% rename from src/algebra/monoid.spec.ts rename to obsolete/algebra/monoid.spec.ts diff --git a/src/algebra/monoid.ts b/obsolete/algebra/monoid.ts similarity index 100% rename from src/algebra/monoid.ts rename to obsolete/algebra/monoid.ts diff --git a/src/algebra/total-ordered.spec.ts b/obsolete/algebra/total-ordered.spec.ts similarity index 100% rename from src/algebra/total-ordered.spec.ts rename to obsolete/algebra/total-ordered.spec.ts diff --git a/src/algebra/total-ordered.ts b/obsolete/algebra/total-ordered.ts similarity index 100% rename from src/algebra/total-ordered.ts rename to obsolete/algebra/total-ordered.ts diff --git a/src/event-emitter/typed-event-emitter.ts b/obsolete/event-emitter/typed-event-emitter.ts similarity index 100% rename from src/event-emitter/typed-event-emitter.ts rename to obsolete/event-emitter/typed-event-emitter.ts diff --git a/src/collection/default-map.ts b/src/collection/default-map.ts index 58f62bd..fcc3dfa 100644 --- a/src/collection/default-map.ts +++ b/src/collection/default-map.ts @@ -13,3 +13,19 @@ export class DefaultMap extends Map { return this.get(k)!; } } + +export class WeakDefaultMap extends WeakMap { + constructor( + private readonly createDefault: (k: K) => V, + entries?: readonly [K, V][], + ) { + super(entries); + } + + getOrCreate(k: K): V { + if (!this.has(k)) { + this.set(k, this.createDefault(k)); + } + return this.get(k)!; + } +} diff --git a/src/collection/iterables.ts b/src/collection/iterables.ts index b0a44bf..ec224d9 100644 --- a/src/collection/iterables.ts +++ b/src/collection/iterables.ts @@ -89,3 +89,11 @@ export const Iterables = { type GeneralIterable = Iterable | AsyncIterable; type MaybePromise = T | PromiseLike; + +export function toMap(items: Iterable, keyer: (t: T) => K): Map { + const ret = new Map(); + for (const i of items) { + ret.set(keyer(i), i); + } + return ret; +} diff --git a/src/collection/maps.ts b/src/collection/maps.ts deleted file mode 100644 index a66d37c..0000000 --- a/src/collection/maps.ts +++ /dev/null @@ -1,11 +0,0 @@ -function fromIterable(items: Iterable, hash: (t: T) => K): Map { - const ret = new Map(); - for (const i of items) { - ret.set(hash(i), i); - } - return ret; -} - -export const Maps = { - buildMap: fromIterable, -} as const; diff --git a/src/collection/min-heap.spec.ts b/src/collection/min-heap.spec.ts index 8122810..93ce929 100644 --- a/src/collection/min-heap.spec.ts +++ b/src/collection/min-heap.spec.ts @@ -1,9 +1,9 @@ import { MinHeap } from './min-heap'; -import { NumericOrder } from '../algebra/total-ordered'; +import * as fpts from 'fp-ts'; describe('MinHeap', () => { it('insert elements', () => { - const testee = new MinHeap(NumericOrder); + const testee = new MinHeap(fpts.number.Ord); expect(testee.slice()).toEqual([]); expect(testee.insert(5).slice()).toEqual([5]); @@ -16,7 +16,7 @@ describe('MinHeap', () => { }); it('removes element', () => { - const testee = new MinHeap(NumericOrder).insertMany(5, 2, 1, 6, 0); + const testee = new MinHeap(fpts.number.Ord).insertMany(5, 2, 1, 6, 0); expect(testee.remove()).toEqual(0); expect(testee.slice()).toEqual([1, 5, 2, 6]); @@ -30,7 +30,7 @@ describe('MinHeap', () => { }); it('removes element - 2', () => { - const testee = new MinHeap(NumericOrder).insert(0).insert(2).insert(3).insert(100).insert(200).insert(4); + const testee = new MinHeap(fpts.number.Ord).insert(0).insert(2).insert(3).insert(100).insert(200).insert(4); expect(testee.slice()).toEqual([0, 2, 3, 100, 200, 4]); expect(testee.remove()).toEqual(0); @@ -38,7 +38,7 @@ describe('MinHeap', () => { }); it('throws when remove from or peek an empty && strict heap', () => { - const testee = new MinHeap(NumericOrder, true); + const testee = new MinHeap(fpts.number.Ord, true); expect(testee.slice()).toEqual([]); expect(() => testee.remove()).toThrow(/nothing to remove/); @@ -47,25 +47,25 @@ describe('MinHeap', () => { }); it('can be initialized with initialTree', () => { - const testee = new MinHeap(NumericOrder, false, /* illegal tree */ [1, 0]); + const testee = new MinHeap(fpts.number.Ord, false, /* illegal tree */ [1, 0]); - expect(() => new MinHeap(NumericOrder, true, [1, 0])).toThrow(/assertInvariants/); + expect(() => new MinHeap(fpts.number.Ord, true, [1, 0])).toThrow(/assertInvariants/); }); it('can be cloned', () => { - const testee = new MinHeap(NumericOrder, false, /* illegal tree */ [1, 0]); + const testee = new MinHeap(fpts.number.Ord, false, /* illegal tree */ [1, 0]); expect(testee.clone().slice()).toEqual([1, 0]); }); it('can be shrinked', () => { - const testee = new MinHeap(NumericOrder).insertMany(5, 4, 3, 2, 1, -1); + const testee = new MinHeap(fpts.number.Ord).insertMany(5, 4, 3, 2, 1, -1); expect(testee.shrink(3).removeMany(3)).toEqual([-1, 1, 2]); }); it('can shrink to a given upperlimit', () => { - const testee = new MinHeap(NumericOrder).insertMany(5, 4, 3, 2, 1, -1); + const testee = new MinHeap(fpts.number.Ord).insertMany(5, 4, 3, 2, 1, -1); expect(testee.clone().shrinkUntil(4).removeMany(100)).toEqual([-1, 1, 2, 3]); diff --git a/src/collection/min-heap.ts b/src/collection/min-heap.ts index 943ec87..90ecb33 100644 --- a/src/collection/min-heap.ts +++ b/src/collection/min-heap.ts @@ -1,12 +1,16 @@ -import { TotalOrdered } from '../algebra/total-ordered'; import { positions } from './btree'; +import { Ord } from 'fp-ts/Ord'; + +function isBefore(ord: Ord, a: T, b: T): boolean { + return ord.compare(a, b) < 0; +} export class MinHeap { private readonly tree: T[] = []; slice = this.tree.slice.bind(this.tree); constructor( - private readonly order: TotalOrdered, + private readonly order: Ord, private readonly strict = false, initialTree?: T[], ) { @@ -27,7 +31,8 @@ export class MinHeap { j = positions.parent(i); this.tree[i] = value; - while (i && this.order.before(value /* i.e. this.tree[i] */, this.tree[j])) { + while (i && isBefore(this.order, value /* i.e. this.tree[i] */, this.tree[j])) { + // pop up new value this.tree[i] = this.tree[j]; this.tree[j] = value; @@ -61,12 +66,12 @@ export class MinHeap { */ if (r < this.tree.length) { // when it has 2 children - if (this.order.before(this.tree[l], this.tree[r]) && this.order.before(this.tree[l], v)) { + if (isBefore(this.order, this.tree[l], this.tree[r]) && isBefore(this.order, this.tree[l], v)) { // swap [i] and [l] and continue this.tree[i] = this.tree[l]; this.tree[l] = v; i = l; - } else if (this.order.before(this.tree[r], this.tree[l]) && this.order.before(this.tree[r], v)) { + } else if (isBefore(this.order, this.tree[r], this.tree[l]) && isBefore(this.order, this.tree[r], v)) { // swap [i] and [l] and continue this.tree[i] = this.tree[r]; this.tree[r] = v; @@ -74,7 +79,7 @@ export class MinHeap { } else { break; // v is already before all children } - } else if (l < this.tree.length && this.order.before(this.tree[l], v)) { + } else if (l < this.tree.length && isBefore(this.order, this.tree[l], v)) { this.tree[i] = this.tree[l]; this.tree[l] = v; break; // there cannot be next level @@ -119,7 +124,7 @@ export class MinHeap { const afterShrink: T[] = []; while ( this.tree.length && - (this.order.before(this.tree[0], v) || (inclusive && this.order.equal(this.tree[0], v))) + (isBefore(this.order, this.tree[0], v) || (inclusive && !this.order.compare(this.tree[0], v))) ) { afterShrink.push(this.remove()!); } @@ -130,7 +135,7 @@ export class MinHeap { private assertInvariants() { for (let i = 1; i < this.tree.length; i++) { const p = positions.parent(i); - if (!this.order.before(this.tree[p], this.tree[i])) { + if (!isBefore(this.order, this.tree[p], this.tree[i])) { throw new Error(`MinHeap#assertInvariants(): expected this.tree[${p} to be ordered before this.tree[${i}]`); } } diff --git a/src/collection/multiset.spec.ts b/src/collection/multiset.spec.ts index ec53e77..fd4ef2f 100644 --- a/src/collection/multiset.spec.ts +++ b/src/collection/multiset.spec.ts @@ -16,8 +16,8 @@ describe(Multiset, () => { testee.setCount('abc', 0); testee.setCount('abc', 1); testee.setCount('abc', 1); - testee.setCount('abc', 0, false); - expect(testee.maxCount()).toEqual(1); + testee.setCount('abc', 0); + expect(testee.maxCount()).toBe(1); expect(testee.getCount('abc')).toEqual(0); expect(testee.getCount('abd')).toEqual(1); expect(testee.findByCount(0)).toEqual(['abc']); diff --git a/src/collection/multiset.ts b/src/collection/multiset.ts index 68f9dda..9926687 100644 --- a/src/collection/multiset.ts +++ b/src/collection/multiset.ts @@ -4,7 +4,7 @@ export class Multiset { private map = new DefaultMap>((k) => new Set()); private countMap = new Map(); - setCount(obj: T, count: number, removeOnZeroFreq = true): void { + setCount(obj: T, count: number): void { const existedCount = this.countMap.get(obj); if (existedCount === count) { @@ -16,7 +16,7 @@ export class Multiset { const existedSet = this.map.get(existedCount)!; existedSet.delete(obj); - if (!existedSet.size && removeOnZeroFreq) { + if (!existedSet.size) { this.map.delete(existedCount); } } else { diff --git a/src/collection/segment-tree.ts b/src/collection/segment-tree.ts index 700357d..e452fd0 100644 --- a/src/collection/segment-tree.ts +++ b/src/collection/segment-tree.ts @@ -1,4 +1,4 @@ -import { Monoid } from '../algebra/monoid'; +import { Monoid } from 'fp-ts/lib/Monoid'; import { positions, powOf2 } from './btree'; /** @@ -35,7 +35,7 @@ export class SegmentTree { const sums: T[] = (this.sums = []); for (let i = 0; i < lenSums; i++) { - sums[i] = this.monoid.id; + sums[i] = this.monoid.empty; } // FIXME: set diff --git a/src/concurrency/resource-pool.spec.ts b/src/concurrency/resource-pool.spec.ts index e78f0f8..f9f3a6e 100644 --- a/src/concurrency/resource-pool.spec.ts +++ b/src/concurrency/resource-pool.spec.ts @@ -37,7 +37,7 @@ describe(ResourcePool.name, () => { testee.use(() => wait(0.5e3)); const becameEmpty = await testee.wait({ freeCount: 2 }); expect(becameEmpty).toBeTruthy(); - expect(Date.now() - start).toBeGreaterThan(0.5e3); + expect(Date.now() - start).toBeGreaterThanOrEqual(0.5e3); }); it('returns false when other tasks running', async () => { diff --git a/src/stress/chunk.ts b/src/stress/chunk.ts new file mode 100644 index 0000000..5957652 --- /dev/null +++ b/src/stress/chunk.ts @@ -0,0 +1,23 @@ +export function* chunk(elements: Iterable, chunkSize: number): Iterable { + let currentChunk: T[] = []; + for (const e of elements) { + currentChunk.push(e); + if (currentChunk.length >= chunkSize) { + yield currentChunk; + currentChunk = []; + } + } + if (currentChunk.length) { + yield currentChunk; + } +} + +export function* chunkArray(elements: readonly T[], chunkSize: number): Iterable { + for (let s = 0; ; s += chunkSize) { + const chunk = elements.slice(s, s + chunkSize); + if (!chunk.length) { + break; + } + yield chunk; + } +} diff --git a/src/stress/groupBy.spec.ts b/src/stress/groupBy.spec.ts new file mode 100644 index 0000000..9648922 --- /dev/null +++ b/src/stress/groupBy.spec.ts @@ -0,0 +1,11 @@ +import { groupBy } from './groupBy'; + +describe(groupBy, () => { + it('groups value with provided keyer()', () => { + expect(groupBy([], () => 0)).toEqual({}); + expect(groupBy(new Set([1, 2, 4]), (v) => v % 2)).toEqual({ + 1: [1], + 0: [2, 4], + }); + }); +}); diff --git a/src/stress/groupBy.ts b/src/stress/groupBy.ts new file mode 100644 index 0000000..bed1f0d --- /dev/null +++ b/src/stress/groupBy.ts @@ -0,0 +1,16 @@ +import { DefaultMap } from '../collection/default-map'; + +export function groupBy( + values: Iterable, + keyer: (value: T) => K, +): Record { + return Object.fromEntries(groupByAsMap(values, keyer)) as Record; +} + +export function groupByAsMap(values: Iterable, keyer: (value: T) => K): ReadonlyMap { + const map = new DefaultMap(() => []); + for (const v of values) { + map.getOrCreate(keyer(v)).push(v); + } + return map; +} diff --git a/src/stress/sortBy.spec.ts b/src/stress/sortBy.spec.ts new file mode 100644 index 0000000..bd34033 --- /dev/null +++ b/src/stress/sortBy.spec.ts @@ -0,0 +1,12 @@ +import { sortBy } from './sortBy'; + +describe('orderBy', () => { + it('sorts value DESC by "natural" JS ordering', () => { + expect(sortBy([2, 3, 1], (v) => v, false)).toEqual([3, 2, 1]); + expect(sortBy([1, 1, 2], (v) => v, false)).toEqual([2, 1, 1]); + }); + it('sorts value ASC by "natural" JS ordering', () => { + expect(sortBy([2, 3, 1], (v) => v)).toEqual([1, 2, 3]); + expect(sortBy([1, 1, 2], (v) => v)).toEqual([1, 1, 2]); + }); +}); diff --git a/src/stress/sortBy.ts b/src/stress/sortBy.ts new file mode 100644 index 0000000..9ff9a12 --- /dev/null +++ b/src/stress/sortBy.ts @@ -0,0 +1,18 @@ +/** + * (for more complicated case, consider fp-ts Ord typeclass) + * @param values + * @param key (if it should be cached, caller should use a cached impl) + * @param asc + */ +export function sortBy(values: T[], key: (v: T) => O, asc = true): T[] { + const indexes = values.map((v, i) => ({ v, i })); + return indexes.sort((a, b) => (asc ? compare(a.v, b.v) : -compare(a.v, b.v))).map((_) => _.v); +} + +function compare(a: any, b: any): number { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } else return 0; +} diff --git a/src/util/lru.spec.ts b/src/util/lru.spec.ts deleted file mode 100644 index 08f21fc..0000000 --- a/src/util/lru.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { SingleThreadedLRU } from './lru'; - -const k1 = 'k1'; -const k2 = 'k2'; -const k3 = 'k3'; - -describe('SingleThreadedLRU', () => { - function createLRU(capacity: number) { - return new SingleThreadedLRU(capacity); - } - - function checkInvariant(lru: SingleThreadedLRU) { - const lru$ = toInspectable(lru); - } - - function toInspectable(s: SingleThreadedLRU) { - return s as any as { - values: Map; - recentKeys: string[]; - recentKeyCount: Map; - }; - } - - function countKeys(keys: string[]) { - const v = new Map(); - for (const k of keys) { - v.set(k, 1 + (v.get(k) || 0)); - } - return v; - } - - it('creates', () => { - const lru = createLRU(10333); - expect(lru.capacity).toBe(10333); - - expect(createLRU(1048576)).toBeInstanceOf(SingleThreadedLRU); - expect(() => createLRU(1048577)).toThrow(); - expect(() => createLRU(0)).toThrow(); - expect(() => createLRU(3.5)).toThrow(); - }); - - it('updates 1', () => { - const lru = createLRU(1); - - // lru with exposed private properties - const lru$ = toInspectable(lru); - expect(lru$.recentKeys).toEqual([]); - expect(lru$.recentKeyCount).toEqual(new Map()); - - // initial status - expect(lru$.recentKeys).toEqual([]); - expect(lru$.recentKeyCount).toEqual(new Map()); - - // #1: get() or contain() on a non-existent key: should not cause squeeze - expect(lru.contain('k1')).toEqual(false); - expect(lru$.recentKeys).toEqual([]); - expect(lru$.recentKeyCount).toEqual(new Map()); - expect(lru.get(k2)).toEqual(null); - expect(lru$.recentKeys).toEqual([]); - expect(lru$.recentKeyCount).toEqual(new Map()); - - // #2: put new key - lru.put(k1, 'put#2'); - expect(lru$.values).toEqual(new Map([[k1, 'put#2']])); - expect(lru$.recentKeys).toEqual([k1]); - expect(lru$.recentKeyCount).toEqual(new Map([[k1, 1]])); - - // #3: put existing key, not causing squeeze - lru.put(k1, 'put#3'); - expect(lru$.values).toEqual(new Map([[k1, 'put#3']])); - expect(lru$.recentKeys).toEqual([k1, k1]); - expect(lru$.recentKeyCount).toEqual(countKeys(lru$.recentKeys)); - - // #4: put new key & swap out least recent key - lru.put(k2, 'put#4'); - expect(lru$.values).toEqual(new Map([[k2, 'put#4']])); - expect(lru$.recentKeys).toEqual([k2]); - expect(lru$.recentKeyCount).toEqual(countKeys(lru$.recentKeys)); - - // #5: put existing key, not causing swap out - lru.put(k2, 'put#5'); - expect(lru$.values).toEqual(new Map([[k2, 'put#5']])); - expect(lru$.recentKeys).toEqual([k2, k2]); - expect(lru$.recentKeyCount).toEqual(countKeys(lru$.recentKeys)); - - // #6: put existing key & remove necessary key - lru.put(k2, 'put#6'); - expect(lru$.values).toEqual(new Map([[k2, 'put#6']])); - expect(lru$.recentKeys).toEqual([k2]); - expect(lru$.recentKeyCount).toEqual(countKeys(lru$.recentKeys)); - }); - - it('updates 2', () => { - const lru = createLRU(2); - - // lru with non-private properties - const lru$ = toInspectable(lru); - expect(lru$.recentKeys).toEqual([]); - expect(lru$.recentKeyCount).toEqual(new Map()); - - lru.put(k1, k1); - for (let v = 0; v < 5; v++) { - lru.put(k2, k2); - } - expect(lru$.recentKeys).toEqual([k1, k2, k2, k2, k2, k2]); - expect(lru$.recentKeyCount).toEqual(countKeys(lru$.recentKeys)); - }); - - it('swap 1', () => { - const lru = createLRU(2); - for (const k of [k1, k2, k3, k3, k3, k2, k2, k3, k3, k1]) { - lru.put(k, k); - } - expect(lru.currentSize()).toEqual(2); - expect(lru.contain(k2)).toEqual(false); - expect(lru.contain(k1)).toEqual(true); - expect(lru.contain(k3)).toEqual(true); - - // swapout until last (k1) - lru.squeezeValues(1); - expect(lru.currentSize()).toEqual(1); - expect(lru.contain(k1)).toEqual(true); - expect(lru.contain(k3)).toEqual(false); - }); - - it('swap 2', () => { - const lru = createLRU(2); - for (const k of [k1, k2, k3, k3, k3, k2, k2, k3, k3, k1]) { - lru.put(k, k); - } - expect(lru.currentSize()).toEqual(2); - expect(lru.contain(k2)).toEqual(false); - expect(lru.contain(k1)).toEqual(true); - expect(lru.contain(k3)).toEqual(true); - expect(lru.get(k3)).toEqual(k3); - expect(lru.currentSize()).toEqual(2); - - // swapout until last (k3) - lru.squeezeValues(1); - expect(lru.currentSize()).toEqual(1); - expect(lru.contain(k1)).toEqual(false); - expect(lru.contain(k3)).toEqual(true); - }); -}); diff --git a/src/util/lru.ts b/src/util/lru.ts deleted file mode 100644 index b4432e2..0000000 --- a/src/util/lru.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * LRU cache for string-indexed, non-falsy values - * - * NOTE all methods are synchronized, - * i.e. they do no use timeout/promise/async,await, - * and will not run before/after function calls. - * - * @class SingleThreadedLRU - * @template T {type} type of cached values, must be non-falsy - * @deprecated prefer npm/flru instead, as fox said - */ -export class SingleThreadedLRU { - private readonly values = new Map(); - - /** - * Keys recently used in get() and put() - * - * Essentially the last elements in a unlimited ordered list of used keys. - */ - private readonly recentKeys: string[] = []; - /** - * #occurance of key in recentKeys - */ - private readonly recentKeyCount = new Map(); - - /** - * @param capacity max size of this.values - */ - constructor(readonly capacity: number) { - if (capacity !== capacity >>> 0 || capacity < 1) { - throw new Error('capacity must be a positive integer'); - } else if (capacity > 1 << 20) { - throw new Error(`capacity too large: ${capacity}`); - } - } - - /** - * Query if key exists in cache - * (not mutating state in any way) - * - * @param {string} key - * @returns {boolean} it exists - */ - contain(key: string): boolean { - return this.values.has(key); - } - - /** - * - * @param {string} key - * - * @param value - * @memberOf SingleThreadedLRU - */ - put(key: string, value: T) { - this.values.set(key, value); - if (!this.refreshKey(key) && this.currentSize() > this.capacity) { - this.removeLeastUsedValue(); - } - if (this.recentKeys.length > this.capacity * 2) { - this.squeezeRecentKeys(); - } - } - - /** - * - * @param {string} key - * @param refreshKey - * @returns {T} value if it exists in cache, null otherwise - * - * @memberOf SingleThreadedLRU - */ - get(key: string, refreshKey = true): T | null { - if (this.values.has(key)) { - const value = this.values.get(key)!; - if (refreshKey) { - this.refreshKey(key); - } - return value; - } - return null; - } - - /** - * Swap out least recent values - * - * @param {number} targetSize loop until falls under targetSize - * - * @memberOf SingleThreadedLRU - */ - squeezeValues(targetSize: number) { - const initialSize = this.currentSize(); - while (this.recentKeys.length && this.values.size > targetSize) { - this.removeLeastUsedValue(); - } - - return this.currentSize() - initialSize; - } - - /** - * Current size of values - * - * @returns {number} recently used - * - * @memberOf SingleThreadedLRU - */ - currentSize(): number { - return this.values.size; - } - - /** - * Refresh a key when it get used - * @return whether the k existed before refresh - */ - private refreshKey(k: string): boolean { - this.recentKeys.push(k); - return incNum(this.recentKeyCount, k, 1) > 1; - } - - private removeLeastUsedValue() { - while (this.recentKeys.length) { - const k = this.recentKeys.shift()!; - if (getNum(this.recentKeyCount, k) === 1) { - this.recentKeyCount.delete(k); - this.values.delete(k); - break; - } else { - incNum(this.recentKeyCount, k, -1); - } - } - } - - /** - * Remove first ones from recent keys if they have other occurrences - */ - private squeezeRecentKeys() { - while (this.recentKeys.length > this.capacity) { - const k = this.recentKeys[0]; - const restOccurrence = getNum(this.recentKeyCount, k); - if (restOccurrence > 1) { - this.recentKeys.shift(); - incNum(this.recentKeyCount, k, -1); - } else { - break; // while - } - } - } -} - -function getNum(map: Map, k: string) { - return map.get(k) || 0; -} - -function incNum(map: Map, k: string, delta: number): number { - const newValue = getNum(map, k) + delta; - if (newValue) { - map.set(k, newValue); - } else { - map.delete(k); - } - return newValue; -} - -function assertInvariant(values: Map, recentKeyCount: Map, recentKeys: string[]) { - for (const k of values.keys()) { - if (!recentKeyCount.has(k)) { - throw new Error('assertion error'); - } - } - - if (Array.from(recentKeyCount.keys()).length !== Array.from(values.keys()).length) { - throw new Error('assertion error'); - } - - const actualCount = recentKeys.reduce((count, k) => { - incNum(count, k, 1); - return count; - }, new Map()); - - if (Array.from(actualCount.keys()).length !== Array.from(actualCount.keys()).length) { - throw new Error('assertion error'); - } - - for (const k of actualCount.keys()) { - if (actualCount.get(k) !== recentKeyCount.get(k)) { - throw new Error('assertion error'); - } - } -}