seyfert
Version:
The most advanced framework for discord bots
474 lines (473 loc) • 17.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LimitedCollection = exports.Collection = void 0;
const common_1 = require("./common");
/**
* Represents a collection that extends the built-in Map class.
* @template K The type of the keys in the collection.
* @template V The type of the values in the collection.
*/
class Collection extends Map {
/**
* Removes elements from the collection based on a filter function.
* @param fn The filter function that determines which elements to remove.
* @param thisArg The value to use as `this` when executing the filter function.
* @returns The number of elements removed from the collection.
* @example
* const collection = new Collection<number, string>();
* collection.set(1, 'one');
* collection.set(2, 'two');
* collection.set(3, 'three');
* const removedCount = collection.sweep((value, key) => key % 2 === 0);
* console.log(removedCount); // Output: 1
* console.log(collection.size); // Output: 2
*/
sweep(fn) {
const previous = this.size;
for (const [key, val] of this) {
if (fn(val, key, this))
this.delete(key);
}
return previous - this.size;
}
/**
* Creates a new array with the results of calling a provided function on every element in the collection.
* @param fn The function that produces an element of the new array.
* @param thisArg The value to use as `this` when executing the map function.
* @returns A new array with the results of calling the provided function on every element in the collection.
* @example
* const collection = new Collection<number, string>();
* collection.set(1, 'one');
* collection.set(2, 'two');
* collection.set(3, 'three');
* const mappedArray = collection.map((value, key) => `${key}: ${value}`);
* console.log(mappedArray); // Output: ['1: one', '2: two', '3: three']
*/
map(fn) {
const result = [];
for (const [key, value] of this.entries()) {
result.push(fn(value, key, this));
}
return result;
}
/**
* Creates a new array with all elements that pass the test implemented by the provided function.
* @param fn The function to test each element of the collection.
* @param thisArg The value to use as `this` when executing the filter function.
* @returns A new array with the elements that pass the test.
* @example
* const collection = new Collection<number, string>();
* collection.set(1, 'one');
* collection.set(2, 'two');
* collection.set(3, 'three');
* const filteredArray = collection.filter((value, key) => key % 2 === 0);
* console.log(filteredArray); // Output: ['two']
*/
filter(fn) {
const result = [];
for (const [key, value] of this.entries()) {
if (fn(value, key, this))
result.push(value);
}
return result;
}
/**
* Apply a function against an accumulator and each element in the collection (from left to right) to reduce it to a single value.
* @param fn The function to execute on each element in the collection.
* @param initialValue The initial value of the accumulator.
* @returns The value that results from the reduction.
* @example
* const collection = new Collection<number, number>();
* collection.set(1, 1);
* collection.set(2, 2);
* collection.set(3, 3);
* const sum = collection.reduce((acc, value) => acc + value, 0);
* console.log(sum); // Output: 6
*/
reduce(fn, initialValue) {
const entries = this.entries();
let result;
if (initialValue !== undefined) {
result = initialValue;
}
else {
const first = entries.next();
if (first.done)
throw new TypeError('Reduce of empty collection with no initial value');
result = first.value[1];
}
for (const [key, value] of entries) {
result = fn(result, value, key, this);
}
return result;
}
/**
* Checks if all elements in the collection pass a test implemented by the provided function.
* @param fn The function to test each element of the collection.
* @returns `true` if all elements pass the test, otherwise `false`.
* @example
* const collection = new Collection<number, number>();
* collection.set(1, 1);
* collection.set(2, 2);
* collection.set(3, 3);
* const allGreaterThanZero = collection.every(value => value > 0);
* console.log(allGreaterThanZero); // Output: true
*/
every(fn) {
for (const [key, value] of this.entries()) {
if (!fn(value, key, this)) {
return false;
}
}
return true;
}
/**
* Checks if at least one element in the collection passes a test implemented by the provided function.
* @param fn The function to test each element of the collection.
* @returns `true` if at least one element passes the test, otherwise `false`.
* @example
* const collection = new Collection<number, number>();
* collection.set(1, 1);
* collection.set(2, 2);
* collection.set(3, 3);
* const hasEvenValue = collection.some(value => value % 2 === 0);
* console.log(hasEvenValue); // Output: true
*/
some(fn) {
for (const [key, value] of this.entries()) {
if (fn(value, key, this)) {
return true;
}
}
return false;
}
/**
* Returns the value of the first element in the collection that satisfies the provided testing function.
* @param fn The function to test each element of the collection.
* @returns The value of the first element that passes the test. `undefined` if no element passes the test.
* @example
* const collection = new Collection<number, number>();
* collection.set(1, 1);
* collection.set(2, 2);
* collection.set(3, 3);
* const firstEvenValue = collection.find(value => value % 2 === 0);
* console.log(firstEvenValue); // Output: 2
*/
find(fn) {
for (const [key, value] of this.entries()) {
if (fn(value, key, this)) {
return value;
}
}
return undefined;
}
/**
* Returns the first key in the collection that satisfies the provided testing function.
* @param fn The function to test each element of the collection.
* @returns The first key that passes the test. `undefined` if no element passes the test.
* @example
* const collection = new Collection<number, number>();
* collection.set(1, 1);
* collection.set(2, 2);
* collection.set(3, 3);
* const firstEvenKey = collection.findKey(value => value % 2 === 0);
* console.log(firstEvenKey); // Output: 2
*/
findKey(fn) {
for (const [key, value] of this.entries()) {
if (fn(value, key, this)) {
return key;
}
}
return undefined;
}
}
exports.Collection = Collection;
/**
* Creates a new array with the results of calling a provided function on every element in the collection.
* @param fn The function that produces an element of the new array.
* @param thisArg The value to use as `this` when executing the map function.
* @returns A new array with the results of calling the provided function on every element in the collection.
* @example
* const collection = new Collection<number, string>();
* collection.set(1, 'one');
* collection.set(2, 'two');
* collection.set(3, 'three');
* const mappedArray = collection.map((value, key) => `${key}: ${value}`);
* console.log(mappedArray); // Output: ['1: one', '2: two', '3: three']
*/
class LimitedCollection {
static default = {
resetOnDemand: false,
limit: Number.POSITIVE_INFINITY,
expire: 0,
};
data = new Map();
expirations = new Map();
options;
timeout = undefined;
_closer = undefined;
_closerDirty = true;
constructor(options = {}) {
this.options = (0, common_1.MergeOptions)(LimitedCollection.default, options);
}
/**
* Adds an element to the limited collection.
* @param key The key of the element.
* @param value The value of the element.
* @param customExpire The custom expiration time for the element.
* @example
* const collection = new LimitedCollection<number, string>({ limit: 3 });
* collection.set(1, 'one');
* collection.set(2, 'two');
* collection.set(3, 'three');
* console.log(collection.size); // Output: 3
* collection.set(4, 'four');
* console.log(collection.size); // Output: 3
* console.log(collection.get(1)); // Output: undefined
*/
set(key, value, customExpire = this.options.expire) {
if (this.options.limit <= 0) {
return;
}
const previousExpiration = this.expirations.get(key);
const replacedCloser = previousExpiration !== undefined && this._closer?.key === key;
if (replacedCloser) {
this._closerDirty = true;
}
const expireOn = customExpire > 0 ? Date.now() + customExpire : -1;
this.data.set(key, value);
if (customExpire > 0) {
this.expirations.set(key, { expire: customExpire, expireOn });
}
else {
this.expirations.delete(key);
}
if (customExpire > 0 &&
!replacedCloser &&
(!this._closer || this._closerDirty || expireOn <= this._closer.expireOn)) {
this._closer = { key, expire: customExpire, expireOn };
this._closerDirty = false;
}
if (this.size > this.options.limit) {
const iter = this.data.keys();
while (this.size > this.options.limit) {
const keyValue = iter.next().value;
this.delete(keyValue);
}
}
if (this.closer?.expireOn === expireOn) {
this.resetTimeout();
}
}
/**
* Returns the raw data of an element in the limited collection.
* @param key The key of the element.
* @returns The raw data of the element, or `undefined` if the element does not exist.
* @example
* const collection = new LimitedCollection<number, string>();
* collection.set(1, 'one');
* const rawData = collection.raw(1);
* console.log(rawData); // Output: { value: 'one', expire: -1, expireOn: -1 }
*/
raw(key) {
const value = this.data.get(key);
if (value === undefined && !this.data.has(key)) {
return;
}
const resolvedValue = value;
if (this.expirations.size === 0) {
return {
value: resolvedValue,
expire: -1,
expireOn: -1,
};
}
const expiration = this.expirations.get(key);
return {
value: resolvedValue,
expire: expiration?.expire ?? -1,
expireOn: expiration?.expireOn ?? -1,
};
}
/**
* Returns the value of an element in the limited collection.
* @param key The key of the element.
* @returns The value of the element, or `undefined` if the element does not exist.
* @example
* const collection = new LimitedCollection<number, string>();
* collection.set(1, 'one');
* const value = collection.get(1);
* console.log(value); // Output: 'one'
*/
get(key) {
const value = this.data.get(key);
if (value === undefined && !this.data.has(key)) {
return;
}
if (!this.options.resetOnDemand || this.expirations.size === 0) {
return value;
}
const expiration = this.expirations.get(key);
if (this.options.resetOnDemand && expiration) {
const wasCloser = this._closer?.key === key;
expiration.expireOn = Date.now() + expiration.expire;
if (wasCloser) {
this._closerDirty = true;
this.resetTimeout();
}
}
return value;
}
/**
* Checks if an element exists in the limited collection.
* @param key The key of the element.
* @returns `true` if the element exists, `false` otherwise.
* @example
* const collection = new LimitedCollection<number, string>();
* collection.set(1, 'one');
* console.log(collection.has(1)); // Output: true
* console.log(collection.has(2)); // Output: false
*/
has(key) {
return this.data.has(key);
}
_pendingReset = false;
/**
* Removes an element from the limited collection.
* @param key The key of the element to remove.
* @returns `true` if the element was removed, `false` otherwise.
* @example
* const collection = new LimitedCollection<number, string>();
* collection.set(1, 'one');
* console.log(collection.delete(1)); // Output: true
* console.log(collection.delete(2)); // Output: false
*/
delete(key) {
const value = this.data.get(key);
if (value === undefined && !this.data.has(key)) {
return false;
}
const resolvedValue = value;
const wasCloser = this._closer?.key === key;
if (wasCloser)
this._closerDirty = true;
this.options.onDelete?.(key, resolvedValue);
const result = this.data.delete(key);
this.expirations.delete(key);
if (wasCloser && !this._pendingReset) {
this._pendingReset = true;
setImmediate(() => {
this._pendingReset = false;
this.resetTimeout();
});
}
return result;
}
/**
* Returns the element in the limited collection that is closest to expiration.
* @returns The element that is closest to expiration, or `undefined` if the collection is empty.
* @example
* const collection = new LimitedCollection<number, string>();
* collection.set(1, 'one', 1000);
* collection.set(2, 'two', 2000);
* collection.set(3, 'three', 500);
* const closestElement = collection.closer;
* console.log(closestElement); // Output: { value: 'three', expire: 500, expireOn: [current timestamp + 500] }
*/
get closer() {
if (this._closerDirty) {
this._closer = undefined;
for (const [key, expiration] of this.expirations.entries()) {
if (!this._closer || expiration.expireOn < this._closer.expireOn) {
this._closer = { key, ...expiration };
}
}
this._closerDirty = false;
}
if (!this._closer) {
return;
}
const value = this.data.get(this._closer.key);
if (value === undefined && !this.data.has(this._closer.key)) {
return;
}
return {
value: value,
expire: this._closer.expire,
expireOn: this._closer.expireOn,
};
}
/**
* Returns the number of elements in the limited collection.
* @returns The number of elements in the collection.
* @example
* const collection = new LimitedCollection<number, string>();
* collection.set(1, 'one');
* collection.set(2, 'two');
* console.log(collection.size); // Output: 2
*/
get size() {
return this.data.size;
}
resetTimeout() {
this.stopTimeout();
this.startTimeout();
}
stopTimeout() {
clearTimeout(this.timeout);
this.timeout = undefined;
}
startTimeout() {
const { expireOn, expire } = this.closer || { expire: -1, expireOn: -1 };
if (expire === -1) {
return;
}
if (this.timeout) {
this.stopTimeout();
}
this.timeout = setTimeout(() => {
this.clearExpired();
this.resetTimeout();
}, expireOn - Date.now());
}
keys() {
return this.data.keys();
}
values() {
return (function* (self) {
for (const key of self.data.keys()) {
yield self.raw(key);
}
})(this);
}
entries() {
return (function* (self) {
for (const key of self.data.keys()) {
yield [key, self.raw(key)];
}
})(this);
}
clear() {
this.data.clear();
this.expirations.clear();
this._closer = undefined;
this._closerDirty = false;
this.resetTimeout();
}
clearExpired() {
for (const [key, expiration] of this.expirations) {
if (Date.now() >= expiration.expireOn) {
if (this._closer?.key === key) {
this._closerDirty = true;
}
if (this.data.has(key)) {
this.options.onDelete?.(key, this.data.get(key));
}
this.data.delete(key);
this.expirations.delete(key);
}
}
}
}
exports.LimitedCollection = LimitedCollection;