@creejs/commons-collection
Version:
Commons Collection
717 lines (647 loc) • 32.7 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function a$1(t){return "boolean"==typeof t}function c$1(t){return "function"==typeof t}function l$1(t){return null!=t&&"object"==typeof t&&!A(t)}function y(t){return null==t}function p$1(t){return !!m$1(t)&&t>0}function h$1(t){return !!m$1(t)&&t>=0}function g$1(t){return !!m$1(t)&&t<0}function w$1(t){return null===t}function d$1(t){return void 0===t}function m$1(t){return null!=t&&"number"==typeof t}function b$1(t){return null!=t&&"object"==typeof t}function A(t){return null!==t&&"object"==typeof t&&(t.constructor===Object||void 0===t.constructor)}function E(t){return null!=t&&"function"==typeof t.then}function N(t){return null!=t&&"string"==typeof t}function $(t){return null!=t&&"symbol"==typeof t}function O$1(t){return ArrayBuffer.isView(t)&&t.constructor!==DataView}function v$1(t){return t instanceof Int8Array}function j(t){return t instanceof Uint8Array}function P(t){return t instanceof Uint8ClampedArray}function S(t){return t instanceof Int16Array}function x(t){return t instanceof Uint16Array}function U(t){return t instanceof Int32Array}function T(t){return t instanceof Uint32Array}function I(t){return t instanceof Float32Array}function B(t){return t instanceof Float64Array}function k$1(t){return t instanceof BigInt64Array}function L$1(t){return t instanceof BigUint64Array}function F(t){return t instanceof ArrayBuffer}var C={assertNumber:q,assertPositive:_$1,assertNegative:function(t,r){if(!g$1(t))throw new Error(`${r?'"'+r+'" ':""}Not Negative: ${t}`)},assertNotNegative:J,assertBoolean:function(t,r){if(!a$1(t))throw new Error(`${r?'"'+r+'" ':""}Not Boolean: type=${typeof t} value=${Z(t)}`)},assertObject:R,assertPlainObject:function(t,r){if(!A(t))throw new Error(`${r?'"'+r+'" ':""}Not PlainObject: type=${typeof t} value=${Z(t)}`)},assertSymbol:function(t,r){if(!$(t))throw new Error(`${r?'"'+r+'" ':""}Not Symbol: type=${typeof t} value=${Z(t)}`)},assertFunction:W,assertInstance:function(t,r){if(!l$1(t))throw new Error(`${r?'"'+r+'" ':""}Not Class Instance: type=${typeof t} value=${Z(t)}`)},assertPromise:H,assertNil:function(t,r){if(!y(t))throw new Error(`${r?'"'+r+'" ':""}Neither Null nor Undefined: type=${typeof t} value=${Z(t)}`)},assertNotNil:V,assertNull:function(t,r){if(!w$1(t))throw new Error(`${r?'"'+r+'" ':""}Not Null: type=${typeof t} value=${Z(t)}`)},assertNotNull:function(t,r){if(w$1(t))throw new Error((r?'"'+r+'" ':"")+"Should Not Null")},assertUndefined:function(t,r){if(!d$1(t))throw new Error(`${r?'"'+r+'" ':""}Not Undefined: type=${typeof t} value=${Z(t)}`)},assertString:M,assertArray:D,assertStringOrSymbol:function(t,r){if(!N(t)&&!$(t))throw new Error(`${r?'"'+r+'" ':""}Not String or Symbol: type=${typeof t} value=${Z(t)}`)},assertInt8Array:function(t,r){if(v$1(t))throw new Error((r?'"'+r+'" ':"")+"Not Int8Array")},assertUint8Array:function(t,r){if(j(t))throw new Error((r?'"'+r+'" ':"")+"Not Uint8Array")},assertUint8ClampedArray:function(t,r){if(P(t))throw new Error((r?'"'+r+'" ':"")+"Not Uint8ClampedArray")},assertInt16Array:function(t,r){if(S(t))throw new Error((r?'"'+r+'" ':"")+"Not Int16Array")},assertUint16Array:function(t,r){if(x(t))throw new Error((r?'"'+r+'" ':"")+"Not Uint16Array")},assertInt32Array:function(t,r){if(U(t))throw new Error((r?'"'+r+'" ':"")+"Not Int32Array")},assertUint32Array:function(t,r){if(T(t))throw new Error((r?'"'+r+'" ':"")+"Not Uint32Array")},assertFloat32Array:function(t,r){if(I(t))throw new Error((r?'"'+r+'" ':"")+"Not Float32Array")},assertFloat64Array:function(t,r){if(B(t))throw new Error((r?'"'+r+'" ':"")+"Not Float64Array")},assertBigInt64Array:function(t,r){if(k$1(t))throw new Error((r?'"'+r+'" ':"")+"Not BigInt64Array")},assertBigUint64Array:function(t,r){if(L$1(t))throw new Error((r?'"'+r+'" ':"")+"Not BigUint64Array")},assertTypedArray:function(t,r){if(O$1(t))throw new Error((r?'"'+r+'" ':"")+"Not TypedArray")},assertArrayBuffer:z};function D(t,r){if(!Array.isArray(t))throw new Error(`${r?'"'+r+'" ':""}Not Array: type=${typeof t} value=${Z(t)}`)}function M(t,r){if(!N(t))throw new Error(`${r?'"'+r+'" ':""}Not String: type=${typeof t} value=${Z(t)}`)}function q(t,r){if(!m$1(t))throw new Error(`${r?'"'+r+'" ':""}Not Number: type=${typeof t} value=${Z(t)}`)}function _$1(t,r){if(!p$1(t))throw new Error(`${r?'"'+r+'" ':""}Not Positive: ${t}`)}function J(t,r){if(!h$1(t))throw new Error(`${r?'"'+r+'" ':""}Not "0 or Positive": ${t}`)}function R(t,r){if(!b$1(t))throw new Error(`${r?'"'+r+'" ':""}Not Object: type=${typeof t} value=${Z(t)}`)}function W(t,r){if(!c$1(t))throw new Error(`${r?'"'+r+'" ':""}Not Function: type=${typeof t} value=${Z(t)}`)}function H(t,r){if(!E(t))throw new Error(`${r?'"'+r+'" ':""}Not Promise: type=${typeof t} value=${Z(t)}`)}function V(t,r){if(y(t))throw new Error((r?'"'+r+'" ':"")+"Should Not Nil")}function z(t,r){if(!F(t))throw new Error((r?'"'+r+'" ':"")+"Not ArrayBuffer")}function Z(t){if(null===t)return "null";if(void 0===t)return "undefined";let r;try{r=JSON.stringify(t);}catch(e){r=t.toString();}return r}new TextDecoder;new TextEncoder;const dt=1e6;var mt={s2ns:1e9,ms2ns:dt,timestamp:function(){if("undefined"!=typeof performance&&"number"==typeof performance.timeOrigin){const t=performance.timeOrigin,r=performance.now();return Math.ceil((t+r)/dt)}return Date.now()},timestamp64:bt,lapseNano:At,lapseMillis:Et,timeoutNano:function(t,r){return At(t)>r},timeoutMillis:function(t,r){return Et(t)>r}};function bt(){if("undefined"!=typeof performance&&"number"==typeof performance.timeOrigin){const t=performance.timeOrigin,r=performance.now();return BigInt((t+r)*dt)}return BigInt(Date.now()*dt)}function At(t,r){return (r??bt())-t}function Et(t,r){r=r??bt();return BigInt(r-t)/BigInt(dt)}
// internal
// owned
/**
* @typedef {{
* value: any,
* prev: Node|undefined,
* next: Node|undefined
* }} Node
*/
// module vars
const { assertPositive } = C;
/**
* A set that has a fixed capacity and automatically removes the oldest element when the capacity is reached.
*
* @class CappedSet
*/
class CappedSet {
/**
* Creates a new CappedSet instance with a fixed capacity.
* @constructor
* @param {number} capacity - The maximum number of elements the set can hold (must be > 0)
* @throws {Error} If capacity is less than or equal to 0
*/
constructor (capacity) {
assertPositive(capacity, 'capacity');
this.capacity = capacity;
/**
* 1. key is the Value stored in CappedSet
* 2. value is a Node
* 3. JS Map preserve the insertion order
* @type {Map<any, Node>}
*/
this._map = new Map();
/**
* @type {Node|undefined}
*/
this._head = undefined;
/**
* @type {Node|undefined}
*/
this._tail = undefined;
}
get size () {
return this._map.size
}
/**
* get the oldest element
*/
get oldest () {
return this._head?.value
}
/**
* get the newest element
*/
get newest () {
return this._tail?.value
}
[Symbol.iterator] () {
return this._map.keys()
}
/**
* Adds a value to the capped set. If the value already exists, it will be moved to the most recent position.
* If the set is at capacity, the oldest element will be removed before adding the new value.
* @param {*} value - The value to add to the set
*/
add (value) {
if (this._map.has(value)) {
const node = this._map.get(value);
node && this._removeNode(node);
} else if (this.size >= this.capacity) {
this._removeOldest();
}
this._addNew(value);
}
/**
* Checks if the value exists in the set.
* @param {*} value - The value to check for existence.
* @returns {boolean} True if the value exists, false otherwise.
*/
has (value) {
return this._map.has(value)
}
/**
* Deletes a value from the capped set.
* @param {*} value - The value to remove from the set.
* @returns {boolean} True if the value was found and removed, false otherwise.
*/
delete (value) {
if (this._map.has(value)) {
const node = this._map.get(value);
node && this._removeNode(node);
return true
}
return false
}
clear () {
this._map.clear();
this._head = undefined;
this._tail = undefined;
}
/**
* Returns an iterator of the values in the set.
* @returns {Iterator<any>} An iterator object that yields the values of the set.
*/
values () {
return this._map.keys()
}
/**
* Adds a new value to the set by creating a node and appending it to the tail.
* Updates the linked list structure and maintains the set's size.
* @private
* @param {*} value - The value to be added to the set.
*/
_addNew (value) {
/**
* @type {Node}
*/
const node = { value, prev: this._tail, next: undefined };
if (this._tail) {
this._tail.next = node;
} else {
this._head = node;
}
this._tail = node;
this._map.set(value, node);
}
/**
* Removes a node from the linked list and the internal Set.
* Updates head/tail pointers and maintains list consistency.
* @param {Node} node - The node to be removed
* @private
*/
_removeNode (node) {
if (node.prev) {
node.prev.next = node.next;
} else {
this._head = node.next;
}
if (node.next) {
node.next.prev = node.prev;
} else {
this._tail = node.prev;
}
this._map.delete(node.value);
}
_removeOldest () {
if (this._head) {
this._removeNode(this._head);
}
}
}
var e={isFunction:t,isNil:s};function t(e){return "function"==typeof e}function s(e){return null==e}function n(e){return null!=e&&"string"==typeof e}var r={assertNumber:function(e,t){if(!function(e){return null!=e&&"number"==typeof e}(e))throw new Error(`${t?'"'+t+'" ':""}Not Number: type=${typeof e} value=${i(e)}`)},assertFunction:function(e,s){if(!t(e))throw new Error(`${s?'"'+s+'" ':""}Not Function: type=${typeof e} value=${i(e)}`)},assertNotNil:function(e,t){if(s(e))throw new Error((t?'"'+t+'" ':"")+"Should Not Nil")},assertString:function(e,t){if(!n(e))throw new Error(`${t?'"'+t+'" ':""}Not String: type=${typeof e} value=${i(e)}`)},assertStringOrSymbol:function(e,t){if(!n(e)&&!function(e){return null!=e&&"symbol"==typeof e}(e))throw new Error(`${t?'"'+t+'" ':""}Not String or Symbol: type=${typeof e} value=${i(e)}`)}};function i(e){if(null===e)return "null";if(void 0===e)return "undefined";let t;try{t=JSON.stringify(e);}catch(s){t=e.toString();}return t}new TextDecoder,new TextEncoder;const l="DOwner$#$",{assertFunction:a,assertNotNil:o}=r;class c{constructor(e,t,s=false){o(e,"event"),a(t,"callback"),this._event=e,this._callback=t,this._isOnce=!!s,this._owner=void 0;}set owner(e){this._owner=e;}get owner(){return this._owner===l?void 0:this._owner}get event(){return this._event}get isOnce(){return this._isOnce}isSameCallback(e){return this._callback===e}get callback(){return this._callback}invoke(...e){try{return this._callback(...e)}finally{if(this._isOnce)try{this._event._remove(this);}catch(e){console.warn(e);}}}listener(...e){return this.invoke(...e)}}const{isFunction:h,isNil:u}=e,{assertStringOrSymbol:_,assertFunction:f}=r;class m{static get DefaultOwner(){return l}constructor(e){_(e,"eventName"),this._name=e,this._callbacks=new Set,this._listeners=[],this._callback2Listeners=new Map,this._listener2Owner=new Map,this._owner2Listeners=new Map;}get name(){return this._name}isEmpty(){return 0===this._callbacks.size}rawListeners(){return [...this._listeners]}listenerCount(e){return null==e?this._listeners.length:this._callback2Listeners.get(e)?.size??0}callbacks(){return [...this.rawListeners().map(e=>e.callback)]}emit(...e){if(0===this._listeners.length)return false;for(const t of [...this._listeners])t.invoke(...e);return true}emitOwner(e,...t){if(0===this._listeners.length)return false;const s=this._owner2Listeners.get(e);if(null==s)return false;for(const e of [...s])e.invoke(...t);return true}hasListener(e){return !!h(e)&&this._callbacks.has(e)}hasOwner(e){return !u(e)&&this._owner2Listeners.has(e)}addListener(e,t){return this._addListener(e,t,false,false)}prependListener(e,t){return this._addListener(e,t,false,true)}addOnceListener(e,t){return this._addListener(e,t,true,false)}prependOnceListener(e,t){return this._addListener(e,t,true,true)}_addListener(e,t,s,n){if(u(e))return false;f(e),this._callbacks.has(e)||this._callbacks.add(e),t=t??l;const r=new c(this,e,s);r.owner=t,n?this._listeners.unshift(r):this._listeners.push(r),this._listener2Owner.set(r,t);let i=this._callback2Listeners.get(e);null==i&&(i=new Set,this._callback2Listeners.set(e,i)),i.add(r);let a=this._owner2Listeners.get(t);return null==a&&(a=new Set,this._owner2Listeners.set(t,a)),a.add(r),true}removeListener(e){if(u(e))return false;if(!this._callbacks.has(e))return false;this._callbacks.delete(e);const t=this._callback2Listeners.get(e);if(null==t)return false;this._callback2Listeners.delete(e);for(const e of t){ -1!==this._listeners.indexOf(e)&&this._listeners.splice(this._listeners.indexOf(e),1);const t=this._listener2Owner.get(e);if(null==t)continue;this._listener2Owner.delete(e);const s=this._owner2Listeners.get(t);null!=s&&(s.delete(e),0===s.size&&this._owner2Listeners.delete(t));}return true}_remove(e){const t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1);const{callback:s}=e,n=this._callback2Listeners.get(s);null!=n&&(n.delete(e),0===n.size&&(this._callback2Listeners.delete(s),this._callbacks.delete(s)));const r=this._listener2Owner.get(e);if(null==r)return;this._listener2Owner.delete(e);const i=this._owner2Listeners.get(r);null!=i&&(i.delete(e),0===i.size&&this._owner2Listeners.delete(r));}removeAllListeners(e){if(u(e))return this._callbacks.clear(),this._listeners.length=0,this._callback2Listeners.clear(),this._listener2Owner.clear(),this._owner2Listeners.clear(),this;const t=this._owner2Listeners.get(e);if(null==t)return this;this._owner2Listeners.delete(e);for(const e of t){ -1!==this._listeners.indexOf(e)&&this._listeners.splice(this._listeners.indexOf(e),1),this._listener2Owner.delete(e);const{callback:t}=e,s=this._callback2Listeners.get(t);null!=s&&(s.delete(e),0===s.size&&(this._callback2Listeners.delete(t),this._callbacks.delete(t)));}return this}}const{isNil:d}=e,{assertString:w,assertFunction:L,assertNumber:v,assertStringOrSymbol:g,assertNotNil:p}=r,b=["on","once","addListener","prependListener","prependOnceListener","off","offAll","offOwner","removeAllListeners","removeListener","emit","emitOwner","setMaxListeners","getMaxListeners","hasOwner","listeners","listenerCount","eventNames","rawListeners"];let O=10;class k{static mixin(e){const t=new k;e.__emitter=t;for(const s of b){const n=t[s];e[s]=n.bind(t);}return e}static get defaultMaxListeners(){return O}static set defaultMaxListeners(e){v(e),O=e??10;}constructor(){this._name2Event=new Map,this._maxListeners=O;}addListener(e,t,s){return this.on(e,t,s)}prependListener(e,t,s){w(e),L(t),this._checkMaxListeners(e);return this._getOrCreateEvent(e).prependListener(t,s),this}prependOnceListener(e,t,s){w(e),L(t),this._checkMaxListeners(e);return this._getOrCreateEvent(e).prependOnceListener(t,s),this}emit(e,...t){const s=this._name2Event.get(e);return null!=s&&!s.isEmpty()&&(s.emit(...t),true)}emitOwner(e,t,...s){if(null==t)throw new Error('Missing "owner"');const n=this._name2Event.get(e);return null!=n&&!n.isEmpty()&&(n.emitOwner(t,...s),true)}eventNames(){return [...this._name2Event.keys()]}getMaxListeners(){return this._maxListeners}listenerCount(e,t){g(e,"eventName");const s=this._name2Event.get(e);return null==s||s.isEmpty()?0:s.listenerCount(t)}listeners(e){g(e,"eventName");const t=this._name2Event.get(e);return null==t||t.isEmpty()?[]:t.callbacks()}off(e,t){const s=this._name2Event.get(e);return null==s?this:(s.removeListener(t),s.isEmpty()?(this._name2Event.delete(e),this):this)}offAll(e,t){g(e,"eventName");const s=this._name2Event.get(e);return null==s?this:(s.removeAllListeners(t),s.isEmpty()?(this._name2Event.delete(e),this):this)}offOwner(e){p(e,"owner");const t=[...this._name2Event.values()];for(const s of t)s.removeAllListeners(e),s.isEmpty()&&this._name2Event.delete(s.name);return this}on(e,t,s){w(e),L(t),this._checkMaxListeners(e);return this._getOrCreateEvent(e).addListener(t,s),this}_checkMaxListeners(e){let t=0;0!==this._maxListeners&&this._maxListeners!==1/0&&(t=this.listenerCount(e))>=this._maxListeners&&console.warn(`maxlistenersexceededwarning: Possible EventEmitter memory leak detected. ${t} ${e} listeners added to [${this}]. Use emitter.setMaxListeners() to increase limit`);}once(e,t,s){w(e),L(t);return this._getOrCreateEvent(e).addOnceListener(t,s),this}rawListeners(e){return this._name2Event.get(e)?.rawListeners()||[]}removeAllListeners(e,t){return this.offAll(e,t)}removeListener(e,t){return this.off(e,t)}setMaxListeners(e){if(v(e),e<0)throw new RangeError("maxListeners must >=0");return this._maxListeners=e,this}_getOrCreateEvent(e){if(this._name2Event.has(e))return this._name2Event.get(e);const t=new m(e);return this._name2Event.set(e,t),t}hasOwner(e){if(d(e))return false;for(const t of this._name2Event.values())if(t.hasOwner(e))return true;return false}}
// 3rd
// internal
// owned
/**
* @typedef {number} Timestamp
* @typedef {{
* tickInterval: number, // ms
* tickCount: number // ms
* }} TimeWheelCacheOptions
*/
// module vars
const Event$1 = {
Advance: 'advance',
Expired: 'expired'
};
/**
* Not Found Better Implementation in npmjs.com, so do it by ourselves.
*
* Key Points:
* 1. Basic Atom Unit is "tick", 1 tick = 1 tickInterval
* 3. How many milliseconds does 1 Tick represent is defined by "tickInterval"
* 4. How many Ticks does the Wheel own is defined by "tickCount"
*/
class TimeWheelCache extends k {
get Event () {
return Event$1
}
/**
* @param {TimeWheelCacheOptions} [options]
*/
constructor (options) {
super();
this.options = options ?? {};
/**
* How many milliseconds does one Tick represent
*/
this._tickInterval = this.options.tickInterval ?? 1000;
/**
* How many Ticks does the Wheel have
*/
this._tickCount = this.options.tickCount ?? 60;
/**
* Slots, one Tick owns one Slot to store key and expire timestamp
* @type {Map<any, {value: any, slotIndex: number, expireTimestamp: number}>[]}
*/
this._slots = Array(this._tickCount).fill(undefined).map(() => new Map());
/**
* Data Cache
* @type {Map<any, {value: any, slotIndex: number, expireTimestamp: number}>}
*/
this._cache = new Map();
this._currentSlotIndex = 0;
/** @type {NodeJS.Timeout|undefined} */
this._timer = undefined;
// must start it immediately
this._startAutoEvict();
}
get tickInterval () {
return this._tickInterval
}
get tickCount () {
return this._tickCount
}
/**
* Max Time to Live, atom unit "millisecond"
* @returns {number}
*/
get maxTtl () {
if (this._maxTtl == null) {
this._maxTtl = this._tickCount * this._tickInterval; // ms
}
return this._maxTtl
}
assertStarted () {
if (!this.autoEvictRunning) {
throw new Error(`${this.constructor.name} is not started`)
}
}
get autoEvictRunning () {
return this._timer != null
}
_startAutoEvict () {
this._timer = setInterval(() => this._advance(), this._tickInterval);
}
_stopAutoEvict () {
this._timer && clearInterval(this._timer);
this._timer = undefined;
}
_advance () {
const nextSlotIndex = (this._currentSlotIndex + 1) % this._tickCount;
const nextSlot = this._slots[nextSlotIndex];
const currentSlot = this._slots[this._currentSlotIndex];
this.emit(Event$1.Advance, this._currentSlotIndex, currentSlot, nextSlot);
if (currentSlot.size > 0) {
// clear current slot, and expired data
for (const key of currentSlot.keys()) {
const wrapped = this._cache.get(key);
this._cache.delete(key);
// @ts-ignore
this.emit(Event$1.Expired, key, wrapped.value, wrapped.expireTimestamp);
}
currentSlot.clear();
}
this._currentSlotIndex = nextSlotIndex;
}
start () {
if (this.autoEvictRunning) {
return
}
this._startAutoEvict();
}
stop () {
if (!this.autoEvictRunning) {
return
}
this._stopAutoEvict();
}
destroy () {
this._stopAutoEvict();
this.clear();
}
/**
* 1. "key" is not Nil
* 2. "value" is not Nil
* 3. "ttl" must > 0 && <= maxTtl, maxTtl = tickCount * tickInterval
* * > 0: CAN NOT go to past
* * < maxTtl: Wheel is Round, eg.
* * "Hour Wheel" has 24 Hour-Tick-Mark
* * CAN NOT distinguish 24 hours and 48 hours
* 4. Understand the difference between "ttl" and "timestamp"
* * TTL is Indexed from 1
* * eg. ttl = 60, should be stored in Slot[59]
* * Timestamp is Indexed from 0
* @param {any} key
* @param {any} value
* @param {number} ttl Time to Live, atom unit "millisecond"
* @returns {boolean}
*/
set (key, value, ttl) {
this.assertStarted();
C.assertNotNil(key, 'key');
C.assertNotNil(value, 'value');
if (ttl <= 0) {
throw new Error(`Bad ttl "${ttl}", must > 0`)
}
if (ttl > this.maxTtl) {
throw new Error(`Bad ttl "${ttl}", must <= maxTtl(${this.maxTtl})`)
}
// delete existed firstly, Slot may be changed
if (this._cache.has(key)) {
this.delete(key);
}
// TimeUtils.timestamp() is built on Uptime, not affected by system time change
const expireTimestamp = mt.timestamp() + ttl;
// 1 tick at least
// ttl -1, eg. 1~60s should be stored in Slot[0]
const ticks = Math.floor((ttl - 1) / this._tickInterval);
const targetSlotIndex = (this._currentSlotIndex + ticks) % this._tickCount;
const storedItem = { value, slotIndex: targetSlotIndex, expireTimestamp };
this._slots[targetSlotIndex].set(key, storedItem);
this._cache.set(key, storedItem);
return true
}
/**
* @param {any} key
* @returns {boolean}
*/
delete (key) {
const wrapped = this._cache.get(key);
if (!wrapped) {
return false
}
this._cache.delete(key);
// delete from target slot
// Time-Wheel may be Minute-Hour-Based, it's too long to leave it in slot
const { slotIndex } = wrapped;
const targetSlot = this._slots[slotIndex];
targetSlot.delete(key);
return true
}
/**
* Checks if the cache contains the specified key.
* @param {any} key - The key to check for existence in the cache.
* @returns {boolean} - True if the key exists in the cache, false otherwise.
*/
has (key) {
const wrapped = this._cache.get(key);
if (!wrapped) {
return false
}
if (wrapped.expireTimestamp <= mt.timestamp()) {
this.delete(key);
this.emit('expired', key, wrapped.value, wrapped.expireTimestamp);
return false
}
return true
}
clear () {
this._cache.clear();
this._slots.forEach(slot => slot.clear());
this._currentSlotIndex = 0;
}
/**
*
* @param {any} key
* @returns {any}
*/
get (key) {
const wrapped = this._cache.get(key);
if (!wrapped) {
return
}
if (wrapped.expireTimestamp <= mt.timestamp()) {
this.delete(key);
this.emit('expired', key, wrapped.value, wrapped.expireTimestamp);
return
}
return wrapped.value
}
size () {
// current slot may contain expired data
const slot = this._slots[this._currentSlotIndex];
if (slot.size > 0) {
const now = mt.timestamp();
for (const [key, { expireTimestamp }] of slot.entries()) {
if (expireTimestamp <= now) {
const wrapped = this._cache.get(key);
// @ts-ignore
this.emit('expired', key, wrapped.value, wrapped.expireTimestamp);
slot.delete(key);
this._cache.delete(key);
}
}
}
return this._cache.size
}
}
// 3rd
// internal
/**
* @typedef {number} Timestamp
*/
// module vars
const SecondInMillisecond = 1_000; // 1s
const MinuteInMillisecond = 60_000; // 1m, 60s
const HourInMillisecond = 3_600_000; // 1h, 3600s
const Hour24InMillisecond = 86_400_000; // 24 hours, 86400s
const DowngradType = {
HourToSecond: 'hour->second',
HourToMinute: 'hour->minute',
MinuteToSecond: 'minute->second'
};
const Event = {
Downgrade: 'downgrade',
Expired: 'expired'
};
const MaxTtl = Hour24InMillisecond - 1;
/**
* Hour-Minute-Second-Time-Wheel Cache
* 1. TTL must be less than 24 Hours
*/
class Hour24TimeWheelCache extends k {
static get DowngradType () {
return DowngradType
}
static get Event () {
return Event
}
/**
*
*/
constructor () {
super();
/**
* Second Wheel:
* 1. 1 Tick Mark is 1 Second, is 1000 Milliseconds
* 2. 60 Slots, maximumly, SencondWheel can contain 60 Seconds
* 3. 60 Seconds should be stored in MinuteWheel
* * 01 00:00:00.XXX -> 00:00:59.XXX in Slot01 Index00
* * 02 00:00:01.XXX -> 00:00:01.XXX in Slot02 Index01
* * 60 00:00:59.XXX -> 00:00:59.XXX in Slot60 Index59
* @type {TimeWheelCache}
*/
this._secondWheel = new TimeWheelCache({ tickInterval: SecondInMillisecond, tickCount: 60 });
/**
* Minute Wheel:
* 1. 1 Tick Mark is 1 Minute, is 60 * 1000 Milliseconds
* 2. 60 Slots, maximumly, MinuteWheel can contain 60 Minutes
* * 01 00:00:00 -> 00:00:59 in Slot01 Index00
* * 02 00:01:00 -> 00:01:59 in Slot02 Index01
* * 60 00:59:00 -> 00:59:59 in Slot60 Index59
* @type {TimeWheelCache}
*/
this._minuteWheel = new TimeWheelCache({ tickInterval: MinuteInMillisecond, tickCount: 60 });
/**
* Hour Wheel:
* 1. 1 Tick Mark is 1 Hour, is 60 * 60 * 1000 Milliseconds
* 2. 24 Slots, maximumly, HourWheel can contain 23:59:59
* * 01 00:00:00 -> 00:59:59 in Slot01 Index00
* * 02 01:00:00 -> 01:59:59 in Slot02 Index01
* * 24 23:00:00 -> 23:59:59 in Slot23 Index23
* @type {TimeWheelCache}
*/
this._hourWheel = new TimeWheelCache({ tickInterval: HourInMillisecond, tickCount: 24 });
/**
* @type {Map<any, TimeWheelCache>}
*/
this._cache = new Map();
this._init();
}
/**
* Max Time to Live, atom unit "millisecond"
* @returns {number}
*/
get maxTtl () {
return MaxTtl
}
get autoEvictRunning () {
return this._secondWheel.autoEvictRunning || this._minuteWheel.autoEvictRunning || this._hourWheel.autoEvictRunning
}
_init () {
/**
* 1. We don't store "Timetick: < 00:00:01 -> 00:59:59" into HourWheel, instead, we store them into MinuteWheel
* 2. When HourWeel advances, take Elements of next Slot, and downgrade them to MinuteWheel
*/
this._hourWheel.on('advance', (/** @type {number} */currentSlotIndex,
/** @type {Map<any, {value:any, expireTimestamp: Timestamp}>} */currentSlot,
/** @type {Map<any, {value:any, expireTimestamp: Timestamp}>} */nextSlot) => {
for (const [key, { value, expireTimestamp }] of nextSlot) {
let leftTtl = expireTimestamp - mt.timestamp();
let downgradeType;
if (leftTtl <= 0) {
leftTtl = 1000;
downgradeType = DowngradType.HourToSecond;
this._secondWheel.set(key, value, leftTtl);
this.emit(Event.Downgrade, key, value, downgradeType, leftTtl);
} else if (leftTtl < MinuteInMillisecond) {
downgradeType = DowngradType.HourToSecond;
this._secondWheel.set(key, value, leftTtl);
this.emit(Event.Downgrade, key, value, downgradeType, leftTtl);
} else {
downgradeType = DowngradType.HourToMinute;
this._minuteWheel.set(key, value, leftTtl);
}
this._hourWheel.delete(key);
this.emit(Event.Downgrade, key, value, downgradeType, leftTtl);
}
});
/**
* 1. We don't store "Timetick: < 00:01:00 -> 00:01:59" into MinuteWheel, instead, we store them into SecondWheel
* 2. When MinuteWheel advances, take Elements of next Slot, and downgrade them to SecondWheel
*/
this._minuteWheel.on('advance', (/** @type {number} */currentSlotIndex,
/** @type {Map<any, {value:any, expireTimestamp: Timestamp}>} */currentSlot,
/** @type {Map<any, {value:any, expireTimestamp: Timestamp}>} */nextSlot) => {
// console.log('advance', currentSlotIndex)
for (const [key, { value, expireTimestamp }] of nextSlot) {
let leftTtl = expireTimestamp - mt.timestamp();
if (leftTtl <= 0) {
leftTtl = 999; // ms
}
// console.log('downgrad', key, value, DowngradType.MinuteToSecond, leftTtl)
this._secondWheel.set(key, value, leftTtl);
this.emit(Event.Downgrade, key, value, DowngradType.MinuteToSecond, leftTtl);
this._minuteWheel.delete(key);
}
});
this._secondWheel.on('expired', (/** @type {any} */key, /** @type {any} */value, /** @type {Timestamp} */expireTimestamp) => {
this.emit(Event.Expired, key, value, expireTimestamp);
});
}
start () {
this._secondWheel.start();
this._minuteWheel.start();
this._hourWheel.start();
}
stop () {
this._secondWheel.stop();
this._minuteWheel.stop();
this._hourWheel.stop();
}
destroy () {
this._secondWheel.destroy();
this._minuteWheel.destroy();
this._hourWheel.destroy();
}
/**
* @param {any} key
* @param {any} value
* @param {number} ttl Time To Live, unit "millisencond", ttl should < 24 Hours
* @returns {boolean}
*/
set (key, value, ttl) {
if (ttl <= 0) {
throw new Error(`Bad ttl "${ttl}", must > 0`)
}
/**
*/
let wheel;
if (ttl < MinuteInMillisecond) { // Timetick: 00:00:01 -> 00:00:59
wheel = this._secondWheel;
} else if (ttl < HourInMillisecond) { // Timetick: < 00:00:01 -> 00:59:59
wheel = this._minuteWheel;
} else if (ttl < Hour24InMillisecond) { // Timetick: 01:00:00 -> 23:59:59
wheel = this._hourWheel;
} else {
throw new Error('"ttl" Should <= Millisencod Of 24 Hours')
}
wheel.set(key, value, ttl);
this._cache.set(key, wheel);
return true
}
/**
* @param {any} key
* @returns {boolean}
*/
delete (key) {
const wheel = this._cache.get(key);
if (!wheel) {
return false
}
return wheel.delete(key)
}
/**
* Checks if the cache contains the specified key.
* @param {any} key - The key to check for existence in the cache.
* @returns {boolean} - True if the key exists in the cache, false otherwise.
*/
has (key) {
const wheel = this._cache.get(key);
if (!wheel) { // No wheel, No Data
return false
}
if (wheel.has(key)) { // In Wheel, Has Data
return true
}
// current wheel is second-Wheel, Not existed or expired
if (wheel === this._secondWheel) {
return false
}
// Not In Wheel, It may be down-graded to lower wheel
if (wheel === this._hourWheel) { // current wheel is hour, check minute wheel
return this._minuteWheel.has(key)
} else if (wheel === this._minuteWheel) { // current wheel is minute, check second wheel
return this._secondWheel.has(key)
}
// It doesn't exist actually
return false
}
clear () {
this._secondWheel.clear();
this._minuteWheel.clear();
this._hourWheel.clear();
this._cache.clear();
}
/**
*
* @param {any} key
* @returns {any}
*/
get (key) {
const wheel = this._cache.get(key);
if (!wheel) { // No wheel, No Data
return
}
const value = wheel.get(key);
if (value != null) { // In Wheel, Has Data
return value
}
// current wheel is second-Wheel, Not existed or expired
if (wheel === this._secondWheel) {
return
}
// Not In Wheel, It may be down-graded to lower wheel
if (wheel === this._hourWheel) { // current wheel is hour, check minute wheel
return this._minuteWheel.get(key)
} else if (wheel === this._minuteWheel) { // current wheel is minute, check second wheel
return this._secondWheel.get(key)
}
// It doesn't exist actually
return undefined
}
size () {
return this._hourWheel.size() + this._minuteWheel.size() + this._secondWheel.size()
}
}
var index = { CappedSet, TimeWheelCache, Hour24TimeWheelCache };
exports.CappedSet = CappedSet;
exports.Hour24TimeWheelCache = Hour24TimeWheelCache;
exports.TimeWheelCache = TimeWheelCache;
exports.default = index;
//# sourceMappingURL=index-dev.cjs.map