@thunder04/supermap
Version:
Extended JS Map with Array-like methods
224 lines (222 loc) • 9.65 kB
JavaScript
"use strict";
const kDateCache = Symbol('supermap.date_cache');
class SuperMap extends Map {
#options;
[kDateCache] = null;
#interval = null;
constructor(options = {}){
if ('intervalTime' in options && (!Number.isSafeInteger(options.intervalTime) || options.intervalTime < 0)) throw new TypeError('options.intervalTime must be a safe positive integer.');
if ('expireAfter' in options && (!Number.isSafeInteger(options.expireAfter) || options.expireAfter < 0)) throw new TypeError('options.expireAfter must be a safe positive integer.');
if ('itemsLimit' in options && (!Number.isSafeInteger(options.itemsLimit) || options.itemsLimit < 0)) throw new TypeError('options.itemsLimit must be a safe positive integer.');
if ('onSweep' in options && typeof options.onSweep !== 'function') throw new TypeError('options.onSweep must be a function.');
options = {
expireAfter: 0,
itemsLimit: 0,
...options
};
super();
this.#options = options;
if ('intervalTime' in options) {
this[kDateCache] = new Map();
this.startInterval();
}
}
/** Converts the Map to an array of entries. */ toArray() {
return Array.from(this.entries());
}
delete(key) {
return this[kDateCache]?.delete(key), super.delete(key);
}
/**
* Identical to [Map.prototype.set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set) but with a third argument.
* @param ttl Time to live duration of this entry (in milliseconds).
* - It has no effect if `options.intervalTime` isn't provided.
* - `options.expireAfter` sums with `ttl`.
*/ set(key, value, ttl = 0) {
if (!Number.isSafeInteger(ttl)) throw new TypeError('ttl must be a safe integer');
const itemsLimit = this.#options.itemsLimit;
if (itemsLimit > 0 && this.size >= itemsLimit && !this.has(key)) {
this.delete(this.first(true));
}
this[kDateCache]?.set(key, Date.now() + ttl);
return super.set(key, value);
}
/** Update an entry without changing its TTL. If the entry doesn't exist, it returns `false`. */ update(key, value) {
if (!super.has(key)) {
return false;
}
const itemsLimit = this.#options.itemsLimit;
if (itemsLimit > 0 && this.size >= itemsLimit && !this.has(key)) {
this.delete(this.first(true));
}
super.set(key, value);
return true;
}
/**
* Identical to [Map.prototype.clear](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear) but with a second argument.
* @param stopInterval If set to `true`, the sweeping interval is also stopped.
*/ clear(stopInterval = false) {
if (stopInterval) this.stopInterval();
else this[kDateCache]?.clear();
return super.clear();
}
first(key) {
return key ? this.keys().next().value : this.values().next().value;
}
last(key = false) {
const entries = this.entries();
let lastEntry;
while(true){
const iter = entries.next();
if (iter.done) return lastEntry && lastEntry[key ? 0 : 1];
lastEntry = iter.value;
}
}
/** Identical to [Array.prototype.some](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) */ some(func) {
const entries = this.entries();
while(true){
const iter = entries.next();
if (iter.done) return false;
if (func(iter.value[1], iter.value[0], this)) return true;
}
}
/** Identical to [Array.prototype.every](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every) */ every(func) {
const entries = this.entries();
while(true){
const iter = entries.next();
if (iter.done) return true;
if (!func(iter.value[1], iter.value[0], this)) return false;
}
}
/** Deletes the entries that pass the `sweeper` callback and calls the `options.onSweep` callback (if provided). */ sweep(sweeper) {
if (this.size === 0) return -1;
const onSweep = this.#options.onSweep;
const prev = this.size;
super.forEach((v, k)=>{
if (sweeper(v, k, this)) {
onSweep?.(v, k);
this.delete(k);
}
});
return prev - this.size;
}
/** Identical to [Array.prototype.filter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) */ filter(func) {
const map = new SuperMap(this.#options);
const entries = this.entries();
while(true){
const iter = entries.next();
if (iter.done) return map;
const [k, v] = iter.value;
if (func(v, k, this)) map.set(k, v);
}
}
/**
* Identical to [Array.prototype.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
* but this method also accepts a filter callback to filter the entries before mapping them without iterating the whole map again.
* Prefer using this method instead of `<SuperMap>.filter(...).map(...)`.
* @param filterFn Optional filter callback to filter entries.
*/ map(mapFn, filterFn) {
return Array.from(this.#mapGenerator(mapFn, filterFn));
}
find(func, returnKey = false) {
const entries = this.entries();
while(true){
const iter = entries.next();
if (iter.done) return null;
const [k, v] = iter.value;
if (func(v, k, this)) return returnKey ? k : v;
}
}
/** Identical to [Array.prototype.reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) */ reduce(fn, initialValue) {
const entries = this.entries();
var accumulator = initialValue;
while(true){
const iter = entries.next();
if (iter.done) return accumulator;
accumulator = fn(accumulator, iter.value[1], iter.value[0], this);
}
}
/** Identical to [Array.prototype.concat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat) */ concat(...children) {
const results = new SuperMap(this.#options);
results[kDateCache] = this[kDateCache];
for (const child of children.concat(this)){
const entries = child.entries();
while(true){
const iter = entries.next();
if (iter.done) break;
results.set(...iter.value);
}
}
return results;
}
/**
* Identical to [Array.prototype.concat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat)
* but this method mutates this instance instead of creating a new one.
*/ concatMut(...children) {
for (const child of children){
const entries = child.entries();
while(true){
const iter = entries.next();
if (iter.done) break;
this.set(...iter.value);
}
}
return this;
}
/** Identical to [Array.prototype.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). */ sort(sortFn) {
this.toArray().sort(([kA, vA], [kB, vB])=>sortFn(vA, vB, kA, kB, this)).forEach((v)=>this.set(...v));
this.clear();
return this;
}
/** Starts or restarts the sweeping interval. It gets automatically called in the constructor if the `intervalTime` option has been provided. */ startInterval() {
if (this[kDateCache] === null || !('intervalTime' in this.#options)) {
return false;
}
this.stopInterval();
this.#interval = setInterval(()=>this.#onSweep(), this.#options.intervalTime).unref();
return true;
}
/** Stops the sweeping interval. */ stopInterval() {
if (this[kDateCache] === null) return false;
this[kDateCache].clear();
if (this.#interval !== null) {
clearInterval(this.#interval);
this.#interval = null;
}
return true;
}
#onSweep() {
const entries = this.entries(), dEntries = this[kDateCache].entries();
const time = Date.now() - this.#options.expireAfter;
const onSweep = this.#options.onSweep;
while(true){
const entry = entries.next();
if (entry.done) return;
if (time > (dEntries.next().value?.[1] || 0)) {
const k = entry.value[0];
onSweep?.(entry.value[1], k);
this.delete(k);
}
}
}
*#mapGenerator(mapFn, filterFn) {
const entries = this.entries();
// The code duplication is intentional to avoid unnecessary conditions.
if (filterFn) {
while(true){
const iter = entries.next();
if (iter.done) return;
const [k, v] = iter.value;
if (filterFn(v, k, this)) {
yield mapFn(v, k, this);
}
}
}
while(true){
const iter = entries.next();
if (iter.done) return;
yield mapFn(iter.value[1], iter.value[0], this);
}
}
}
module.exports = SuperMap;