strong-tie/inbound-calls
0
1/**2 * toad-cache3 *4 * @copyright 2024 Igor Savin <kibertoad@gmail.com>5 * @license MIT6 * @version 3.7.07 */8'use strict';9 10class FifoMap {11 constructor(max = 1000, ttlInMsecs = 0) {12 if (isNaN(max) || max < 0) {13 throw new Error('Invalid max value')14 }15 16 if (isNaN(ttlInMsecs) || ttlInMsecs < 0) {17 throw new Error('Invalid ttl value')18 }19 20 this.first = null;21 this.items = new Map();22 this.last = null;23 this.max = max;24 this.ttl = ttlInMsecs;25 }26 27 get size() {28 return this.items.size29 }30 31 clear() {32 this.items = new Map();33 this.first = null;34 this.last = null;35 }36 37 delete(key) {38 if (this.items.has(key)) {39 const deletedItem = this.items.get(key);40 41 this.items.delete(key);42 43 if (deletedItem.prev !== null) {44 deletedItem.prev.next = deletedItem.next;45 }46 47 if (deletedItem.next !== null) {48 deletedItem.next.prev = deletedItem.prev;49 }50 51 if (this.first === deletedItem) {52 this.first = deletedItem.next;53 }54 55 if (this.last === deletedItem) {56 this.last = deletedItem.prev;57 }58 }59 }60 61 deleteMany(keys) {62 for (var i = 0; i < keys.length; i++) {63 this.delete(keys[i]);64 }65 }66 67 evict() {68 if (this.size > 0) {69 const item = this.first;70 71 this.items.delete(item.key);72 73 if (this.size === 0) {74 this.first = null;75 this.last = null;76 } else {77 this.first = item.next;78 this.first.prev = null;79 }80 }81 }82 83 expiresAt(key) {84 if (this.items.has(key)) {85 return this.items.get(key).expiry86 }87 }88 89 get(key) {90 if (this.items.has(key)) {91 const item = this.items.get(key);92 93 if (this.ttl > 0 && item.expiry <= Date.now()) {94 this.delete(key);95 return96 }97 98 return item.value99 }100 }101 102 getMany(keys) {103 const result = [];104 105 for (var i = 0; i < keys.length; i++) {106 result.push(this.get(keys[i]));107 }108 109 return result110 }111 112 keys() {113 return this.items.keys()114 }115 116 set(key, value) {117 // Replace existing item118 if (this.items.has(key)) {119 const item = this.items.get(key);120 item.value = value;121 122 item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;123 124 return125 }126 127 // Add new item128 if (this.max > 0 && this.size === this.max) {129 this.evict();130 }131 132 const item = {133 expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,134 key: key,135 prev: this.last,136 next: null,137 value,138 };139 this.items.set(key, item);140 141 if (this.size === 1) {142 this.first = item;143 } else {144 this.last.next = item;145 }146 147 this.last = item;148 }149}150 151class LruMap {152 constructor(max = 1000, ttlInMsecs = 0) {153 if (isNaN(max) || max < 0) {154 throw new Error('Invalid max value')155 }156 157 if (isNaN(ttlInMsecs) || ttlInMsecs < 0) {158 throw new Error('Invalid ttl value')159 }160 161 this.first = null;162 this.items = new Map();163 this.last = null;164 this.max = max;165 this.ttl = ttlInMsecs;166 }167 168 get size() {169 return this.items.size170 }171 172 bumpLru(item) {173 if (this.last === item) {174 return // Item is already the last one, no need to bump175 }176 177 const last = this.last;178 const next = item.next;179 const prev = item.prev;180 181 if (this.first === item) {182 this.first = next;183 }184 185 item.next = null;186 item.prev = last;187 last.next = item;188 189 if (prev !== null) {190 prev.next = next;191 }192 193 if (next !== null) {194 next.prev = prev;195 }196 197 this.last = item;198 }199 200 clear() {201 this.items = new Map();202 this.first = null;203 this.last = null;204 }205 206 delete(key) {207 if (this.items.has(key)) {208 const item = this.items.get(key);209 210 this.items.delete(key);211 212 if (item.prev !== null) {213 item.prev.next = item.next;214 }215 216 if (item.next !== null) {217 item.next.prev = item.prev;218 }219 220 if (this.first === item) {221 this.first = item.next;222 }223 224 if (this.last === item) {225 this.last = item.prev;226 }227 }228 }229 230 deleteMany(keys) {231 for (var i = 0; i < keys.length; i++) {232 this.delete(keys[i]);233 }234 }235 236 evict() {237 if (this.size > 0) {238 const item = this.first;239 240 this.items.delete(item.key);241 242 if (this.size === 0) {243 this.first = null;244 this.last = null;245 } else {246 this.first = item.next;247 this.first.prev = null;248 }249 }250 }251 252 expiresAt(key) {253 if (this.items.has(key)) {254 return this.items.get(key).expiry255 }256 }257 258 get(key) {259 if (this.items.has(key)) {260 const item = this.items.get(key);261 262 // Item has already expired263 if (this.ttl > 0 && item.expiry <= Date.now()) {264 this.delete(key);265 return266 }267 268 // Item is still fresh269 this.bumpLru(item);270 return item.value271 }272 }273 274 getMany(keys) {275 const result = [];276 277 for (var i = 0; i < keys.length; i++) {278 result.push(this.get(keys[i]));279 }280 281 return result282 }283 284 keys() {285 return this.items.keys()286 }287 288 set(key, value) {289 // Replace existing item290 if (this.items.has(key)) {291 const item = this.items.get(key);292 item.value = value;293 294 item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;295 296 if (this.last !== item) {297 this.bumpLru(item);298 }299 300 return301 }302 303 // Add new item304 if (this.max > 0 && this.size === this.max) {305 this.evict();306 }307 308 const item = {309 expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,310 key: key,311 prev: this.last,312 next: null,313 value,314 };315 this.items.set(key, item);316 317 if (this.size === 1) {318 this.first = item;319 } else {320 this.last.next = item;321 }322 323 this.last = item;324 }325}326 327class LruObject {328 constructor(max = 1000, ttlInMsecs = 0) {329 if (isNaN(max) || max < 0) {330 throw new Error('Invalid max value')331 }332 333 if (isNaN(ttlInMsecs) || ttlInMsecs < 0) {334 throw new Error('Invalid ttl value')335 }336 337 this.first = null;338 this.items = Object.create(null);339 this.last = null;340 this.size = 0;341 this.max = max;342 this.ttl = ttlInMsecs;343 }344 345 bumpLru(item) {346 if (this.last === item) {347 return // Item is already the last one, no need to bump348 }349 350 const last = this.last;351 const next = item.next;352 const prev = item.prev;353 354 if (this.first === item) {355 this.first = next;356 }357 358 item.next = null;359 item.prev = last;360 last.next = item;361 362 if (prev !== null) {363 prev.next = next;364 }365 366 if (next !== null) {367 next.prev = prev;368 }369 370 this.last = item;371 }372 373 clear() {374 this.items = Object.create(null);375 this.first = null;376 this.last = null;377 this.size = 0;378 }379 380 delete(key) {381 if (Object.prototype.hasOwnProperty.call(this.items, key)) {382 const item = this.items[key];383 384 delete this.items[key];385 this.size--;386 387 if (item.prev !== null) {388 item.prev.next = item.next;389 }390 391 if (item.next !== null) {392 item.next.prev = item.prev;393 }394 395 if (this.first === item) {396 this.first = item.next;397 }398 399 if (this.last === item) {400 this.last = item.prev;401 }402 }403 }404 405 deleteMany(keys) {406 for (var i = 0; i < keys.length; i++) {407 this.delete(keys[i]);408 }409 }410 411 evict() {412 if (this.size > 0) {413 const item = this.first;414 415 delete this.items[item.key];416 417 if (--this.size === 0) {418 this.first = null;419 this.last = null;420 } else {421 this.first = item.next;422 this.first.prev = null;423 }424 }425 }426 427 expiresAt(key) {428 if (Object.prototype.hasOwnProperty.call(this.items, key)) {429 return this.items[key].expiry430 }431 }432 433 get(key) {434 if (Object.prototype.hasOwnProperty.call(this.items, key)) {435 const item = this.items[key];436 437 // Item has already expired438 if (this.ttl > 0 && item.expiry <= Date.now()) {439 this.delete(key);440 return441 }442 443 // Item is still fresh444 this.bumpLru(item);445 return item.value446 }447 }448 449 getMany(keys) {450 const result = [];451 452 for (var i = 0; i < keys.length; i++) {453 result.push(this.get(keys[i]));454 }455 456 return result457 }458 459 keys() {460 return Object.keys(this.items)461 }462 463 set(key, value) {464 // Replace existing item465 if (Object.prototype.hasOwnProperty.call(this.items, key)) {466 const item = this.items[key];467 item.value = value;468 469 item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;470 471 if (this.last !== item) {472 this.bumpLru(item);473 }474 475 return476 }477 478 // Add new item479 if (this.max > 0 && this.size === this.max) {480 this.evict();481 }482 483 const item = {484 expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,485 key: key,486 prev: this.last,487 next: null,488 value,489 };490 this.items[key] = item;491 492 if (++this.size === 1) {493 this.first = item;494 } else {495 this.last.next = item;496 }497 498 this.last = item;499 }500}501 502class HitStatisticsRecord {503 constructor() {504 this.records = {};505 }506 507 initForCache(cacheId, currentTimeStamp) {508 this.records[cacheId] = {509 [currentTimeStamp]: {510 cacheSize: 0,511 hits: 0,512 falsyHits: 0,513 emptyHits: 0,514 misses: 0,515 expirations: 0,516 evictions: 0,517 invalidateOne: 0,518 invalidateAll: 0,519 sets: 0,520 },521 };522 }523 524 resetForCache(cacheId) {525 for (let key of Object.keys(this.records[cacheId])) {526 this.records[cacheId][key] = {527 cacheSize: 0,528 hits: 0,529 falsyHits: 0,530 emptyHits: 0,531 misses: 0,532 expirations: 0,533 evictions: 0,534 invalidateOne: 0,535 invalidateAll: 0,536 sets: 0,537 };538 }539 }540 541 getStatistics() {542 return this.records543 }544}545 546/**547 *548 * @param {Date} date549 * @returns {string}550 */551function getTimestamp(date) {552 return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date553 .getDate()554 .toString()555 .padStart(2, '0')}`556}557 558class HitStatistics {559 constructor(cacheId, statisticTtlInHours, globalStatisticsRecord) {560 this.cacheId = cacheId;561 this.statisticTtlInHours = statisticTtlInHours;562 563 this.collectionStart = new Date();564 this.currentTimeStamp = getTimestamp(this.collectionStart);565 566 this.records = globalStatisticsRecord || new HitStatisticsRecord();567 this.records.initForCache(this.cacheId, this.currentTimeStamp);568 }569 570 get currentRecord() {571 // safety net572 /* c8 ignore next 14 */573 if (!this.records.records[this.cacheId][this.currentTimeStamp]) {574 this.records.records[this.cacheId][this.currentTimeStamp] = {575 cacheSize: 0,576 hits: 0,577 falsyHits: 0,578 emptyHits: 0,579 misses: 0,580 expirations: 0,581 evictions: 0,582 sets: 0,583 invalidateOne: 0,584 invalidateAll: 0,585 };586 }587 588 return this.records.records[this.cacheId][this.currentTimeStamp]589 }590 591 hoursPassed() {592 return (Date.now() - this.collectionStart) / 1000 / 60 / 60593 }594 595 addHit() {596 this.archiveIfNeeded();597 this.currentRecord.hits++;598 }599 addFalsyHit() {600 this.archiveIfNeeded();601 this.currentRecord.falsyHits++;602 }603 604 addEmptyHit() {605 this.archiveIfNeeded();606 this.currentRecord.emptyHits++;607 }608 609 addMiss() {610 this.archiveIfNeeded();611 this.currentRecord.misses++;612 }613 614 addEviction() {615 this.archiveIfNeeded();616 this.currentRecord.evictions++;617 }618 619 setCacheSize(currentSize) {620 this.archiveIfNeeded();621 this.currentRecord.cacheSize = currentSize;622 }623 624 addExpiration() {625 this.archiveIfNeeded();626 this.currentRecord.expirations++;627 }628 629 addSet() {630 this.archiveIfNeeded();631 this.currentRecord.sets++;632 }633 634 addInvalidateOne() {635 this.archiveIfNeeded();636 this.currentRecord.invalidateOne++;637 }638 639 addInvalidateAll() {640 this.archiveIfNeeded();641 this.currentRecord.invalidateAll++;642 }643 644 getStatistics() {645 return this.records.getStatistics()646 }647 648 archiveIfNeeded() {649 if (this.hoursPassed() >= this.statisticTtlInHours) {650 this.collectionStart = new Date();651 this.currentTimeStamp = getTimestamp(this.collectionStart);652 this.records.initForCache(this.cacheId, this.currentTimeStamp);653 }654 }655}656 657class LruObjectHitStatistics extends LruObject {658 constructor(max, ttlInMsecs, cacheId, globalStatisticsRecord, statisticTtlInHours) {659 super(max || 1000, ttlInMsecs || 0);660 661 if (!cacheId) {662 throw new Error('Cache id is mandatory')663 }664 665 this.hitStatistics = new HitStatistics(666 cacheId,667 statisticTtlInHours !== undefined ? statisticTtlInHours : 24,668 globalStatisticsRecord,669 );670 }671 672 getStatistics() {673 return this.hitStatistics.getStatistics()674 }675 676 set(key, value) {677 super.set(key, value);678 this.hitStatistics.addSet();679 this.hitStatistics.setCacheSize(this.size);680 }681 682 evict() {683 super.evict();684 this.hitStatistics.addEviction();685 this.hitStatistics.setCacheSize(this.size);686 }687 688 delete(key, isExpiration = false) {689 super.delete(key);690 691 if (!isExpiration) {692 this.hitStatistics.addInvalidateOne();693 }694 this.hitStatistics.setCacheSize(this.size);695 }696 697 clear() {698 super.clear();699 700 this.hitStatistics.addInvalidateAll();701 this.hitStatistics.setCacheSize(this.size);702 }703 704 get(key) {705 if (Object.prototype.hasOwnProperty.call(this.items, key)) {706 const item = this.items[key];707 708 // Item has already expired709 if (this.ttl > 0 && item.expiry <= Date.now()) {710 this.delete(key, true);711 this.hitStatistics.addExpiration();712 return713 }714 715 // Item is still fresh716 this.bumpLru(item);717 if (!item.value) {718 this.hitStatistics.addFalsyHit();719 }720 if (item.value === undefined || item.value === null || item.value === '') {721 this.hitStatistics.addEmptyHit();722 }723 this.hitStatistics.addHit();724 return item.value725 }726 this.hitStatistics.addMiss();727 }728}729 730class FifoObject {731 constructor(max = 1000, ttlInMsecs = 0) {732 if (isNaN(max) || max < 0) {733 throw new Error('Invalid max value')734 }735 736 if (isNaN(ttlInMsecs) || ttlInMsecs < 0) {737 throw new Error('Invalid ttl value')738 }739 740 this.first = null;741 this.items = Object.create(null);742 this.last = null;743 this.size = 0;744 this.max = max;745 this.ttl = ttlInMsecs;746 }747 748 clear() {749 this.items = Object.create(null);750 this.first = null;751 this.last = null;752 this.size = 0;753 }754 755 delete(key) {756 if (Object.prototype.hasOwnProperty.call(this.items, key)) {757 const deletedItem = this.items[key];758 759 delete this.items[key];760 this.size--;761 762 if (deletedItem.prev !== null) {763 deletedItem.prev.next = deletedItem.next;764 }765 766 if (deletedItem.next !== null) {767 deletedItem.next.prev = deletedItem.prev;768 }769 770 if (this.first === deletedItem) {771 this.first = deletedItem.next;772 }773 774 if (this.last === deletedItem) {775 this.last = deletedItem.prev;776 }777 }778 }779 780 deleteMany(keys) {781 for (var i = 0; i < keys.length; i++) {782 this.delete(keys[i]);783 }784 }785 786 evict() {787 if (this.size > 0) {788 const item = this.first;789 790 delete this.items[item.key];791 792 if (--this.size === 0) {793 this.first = null;794 this.last = null;795 } else {796 this.first = item.next;797 this.first.prev = null;798 }799 }800 }801 802 expiresAt(key) {803 if (Object.prototype.hasOwnProperty.call(this.items, key)) {804 return this.items[key].expiry805 }806 }807 808 get(key) {809 if (Object.prototype.hasOwnProperty.call(this.items, key)) {810 const item = this.items[key];811 812 if (this.ttl > 0 && item.expiry <= Date.now()) {813 this.delete(key);814 return815 }816 817 return item.value818 }819 }820 821 getMany(keys) {822 const result = [];823 824 for (var i = 0; i < keys.length; i++) {825 result.push(this.get(keys[i]));826 }827 828 return result829 }830 831 keys() {832 return Object.keys(this.items)833 }834 835 set(key, value) {836 // Replace existing item837 if (Object.prototype.hasOwnProperty.call(this.items, key)) {838 const item = this.items[key];839 item.value = value;840 841 item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;842 843 return844 }845 846 // Add new item847 if (this.max > 0 && this.size === this.max) {848 this.evict();849 }850 851 const item = {852 expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,853 key: key,854 prev: this.last,855 next: null,856 value,857 };858 this.items[key] = item;859 860 if (++this.size === 1) {861 this.first = item;862 } else {863 this.last.next = item;864 }865 866 this.last = item;867 }868}869 870exports.Fifo = FifoObject;871exports.FifoMap = FifoMap;872exports.FifoObject = FifoObject;873exports.HitStatisticsRecord = HitStatisticsRecord;874exports.Lru = LruObject;875exports.LruHitStatistics = LruObjectHitStatistics;876exports.LruMap = LruMap;877exports.LruObject = LruObject;878exports.LruObjectHitStatistics = LruObjectHitStatistics;879 