UNPKG

@creejs/commons-collection

Version:
710 lines (642 loc) 32.6 kB
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 }; export { CappedSet, Hour24TimeWheelCache, TimeWheelCache, index as default }; //# sourceMappingURL=index-dev.js.map