diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9517af95..a31369f9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,6 +7,7 @@ export * from './utils/crypto' export * from './utils/peer-utils' export * from './utils/tl-json' export * from './utils/async-lock' +export * from './utils/lru-map' export { BinaryReader } from './utils/binary/binary-reader' export { BinaryWriter } from './utils/binary/binary-writer' diff --git a/packages/core/src/utils/lru-map.ts b/packages/core/src/utils/lru-map.ts new file mode 100644 index 00000000..3e7d7ce4 --- /dev/null +++ b/packages/core/src/utils/lru-map.ts @@ -0,0 +1,184 @@ +interface TwoWayLinkedList { + // k = key + k: K + // v = value + v: T + // p = previous + p?: TwoWayLinkedList + // n = next + n?: TwoWayLinkedList +} + +/** + * Simple class implementing LRU-like behaviour for a map, + * falling back to objects when `Map` is not available. + * + * Can be used to handle local cache of *something* + * + * Uses two-way linked list internally to keep track of insertion/access order + */ +export class LruMap { + private _capacity: number + private _first?: TwoWayLinkedList + private _last?: TwoWayLinkedList + + private _size = 0 + + constructor(capacity: number, useObject = false) { + this._capacity = capacity + + if (typeof Map === 'undefined' || useObject) { + const obj = {} as any + this._set = (k, v) => obj[k] = v + this._has = (k) => k in obj + this._get = (k) => obj[k] + this._del = (k) => delete obj[k] + } else { + const map = new Map() + this._set = map.set.bind(map) + this._has = map.has.bind(map) + this._get = map.get.bind(map) + this._del = map.delete.bind(map) + } + } + + private readonly _set: (key: K, value: V) => void + private readonly _has: (key: K) => boolean + private readonly _get: (key: K) => TwoWayLinkedList | undefined + private readonly _del: (key: K) => void + + private _markUsed(item: TwoWayLinkedList): void { + if (item === this._first) { + return // already the most recently used + } + + if (item.p) { + if (item === this._last) { + this._last = item.p + } + item.p.n = item.n + } + + if (item.n) { + item.n.p = item.p + } + + item.p = undefined + item.n = this._first + if (this._first) { + this._first.p = item + } + this._first = item + } + + get(key: K): V | undefined { + const item = this._get(key) + if (!item) return undefined + + this._markUsed(item) + return item.v + } + + has(key: K): boolean { + return this._has(key) + } + + set(key: K, value: V): void { + let item = this._get(key) + + if (item) { + // already in cache, update + item.v = value + this._markUsed(item) + return + } + + item = { + k: key, + v: value + } + this._set(key, item as any) + + if (this._first) { + this._first.p = item + item.n = this._first + } else { + // first item ever + this._last = item + } + + this._first = item + this._size += 1 + if (this._size > this._capacity) { + // remove the last item + const oldest = this._last + if (oldest) { + if (oldest.p) { + this._last = oldest.p + this._last!.n = undefined + } else { + // exhausted + this._last = undefined + this._first = undefined + } + + // remove strong refs to and from the item + oldest.p = oldest.n = undefined + this._del(oldest.k) + this._size -= 1 + } + } + } + + // private _setForMap(key: K, value: V) { + // const old = this._items[key] + // + // if (old) { + // bring old one to the beginning + // + // } + // if (!this._first) this._first = { v: str } + // if (!this._last) this._last = this._first + // else { + // this._last.n = { v: str } + // this._last = this._last.n + // } + // + // this._set!.add(str) + // + // if (this._set!.size > this._capacity && this._first) { + // // remove least recently used + // this._set!.delete(this._first.v) + // this._first = this._first.n + // } + // } + + // private _hasForMap(str: string) { + // return this._set!.has(str) + // } + + // private _setForObj(str: string) { + // if (str in this._obj!) return + // + // if (!this._first) this._first = { v: str } + // if (!this._last) this._last = this._first + // else { + // this._last.n = { v: str } + // this._last = this._last.n + // } + // + // this._obj![str] = true + // + // if (this._objSize === this._capacity) { + // // remove least recently used + // delete this._obj![this._first.v] + // this._first = this._first.n + // } else { + // this._objSize! += 1 + // } + // } + // + // private _hasForObj(str: string) { + // return str in this._obj! + // } +} diff --git a/packages/core/tests/lru-map.spec.ts b/packages/core/tests/lru-map.spec.ts new file mode 100644 index 00000000..88022c93 --- /dev/null +++ b/packages/core/tests/lru-map.spec.ts @@ -0,0 +1,83 @@ +import { describe, it } from 'mocha' +import { expect } from 'chai' +import { LruMap } from '../src' + +describe('LruMap', () => { + it('Map backend', () => { + const lru = new LruMap(2) + + lru.set('first', 1) + expect(lru.has('first')).true + expect(lru.has('second')).false + expect(lru.get('first')).eq(1) + + lru.set('first', 42) + expect(lru.has('first')).true + expect(lru.has('second')).false + expect(lru.get('first')).eq(42) + + lru.set('second', 2) + expect(lru.has('first')).true + expect(lru.has('second')).true + expect(lru.get('first')).eq(42) + expect(lru.get('second')).eq(2) + + lru.set('third', 3) + expect(lru.has('first')).false + expect(lru.has('second')).true + expect(lru.has('third')).true + expect(lru.get('first')).eq(undefined) + expect(lru.get('second')).eq(2) + expect(lru.get('third')).eq(3) + + lru.get('second') // update lru so that last = third + lru.set('fourth', 4) + expect(lru.has('first')).false + expect(lru.has('second')).true + expect(lru.has('third')).false + expect(lru.has('fourth')).true + expect(lru.get('first')).eq(undefined) + expect(lru.get('second')).eq(2) + expect(lru.get('third')).eq(undefined) + expect(lru.get('fourth')).eq(4) + }) + + it('Object backend', () => { + const lru = new LruMap(2, true) + + lru.set('first', 1) + expect(lru.has('first')).true + expect(lru.has('second')).false + expect(lru.get('first')).eq(1) + + lru.set('first', 42) + expect(lru.has('first')).true + expect(lru.has('second')).false + expect(lru.get('first')).eq(42) + + lru.set('second', 2) + expect(lru.has('first')).true + expect(lru.has('second')).true + expect(lru.get('first')).eq(42) + expect(lru.get('second')).eq(2) + + lru.set('third', 3) + expect(lru.has('first')).false + expect(lru.has('second')).true + expect(lru.has('third')).true + expect(lru.get('first')).eq(undefined) + expect(lru.get('second')).eq(2) + expect(lru.get('third')).eq(3) + + lru.get('second') // update lru so that last = third + lru.set('fourth', 4) + expect(lru.has('first')).false + expect(lru.has('second')).true + expect(lru.has('third')).false + expect(lru.has('fourth')).true + expect(lru.get('first')).eq(undefined) + expect(lru.get('second')).eq(2) + expect(lru.get('third')).eq(undefined) + expect(lru.get('fourth')).eq(4) + }) +})