mirror of
https://github.com/pezkuwichain/pezkuwi-api.git
synced 2026-04-30 06:08:04 +00:00
198 lines
4.6 KiB
TypeScript
198 lines
4.6 KiB
TypeScript
// Copyright 2017-2025 @pezkuwi/rpc-provider authors & contributors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Assuming all 1.5MB responses, we apply a default allowing for 192MB
|
|
// cache space (depending on the historic queries this would vary, metadata
|
|
// for Zagros/Pezkuwi/Bizinikiwi falls between 600-750K, 2x for estimate)
|
|
|
|
export const DEFAULT_CAPACITY = 1024;
|
|
export const DEFAULT_TTL = 30000; // 30 seconds
|
|
const MAX_TTL = 1800_000; // 30 minutes
|
|
|
|
// If the user decides to disable the TTL we set the value
|
|
// to a very high number (A year = 365 * 24 * 60 * 60 * 1000).
|
|
const DISABLED_TTL = 31_536_000_000;
|
|
|
|
class LRUNode {
|
|
readonly key: string;
|
|
#expires: number;
|
|
#ttl: number;
|
|
readonly createdAt: number;
|
|
|
|
public next: LRUNode;
|
|
public prev: LRUNode;
|
|
|
|
constructor (key: string, ttl: number) {
|
|
this.key = key;
|
|
this.#ttl = ttl;
|
|
this.#expires = Date.now() + ttl;
|
|
this.createdAt = Date.now();
|
|
this.next = this.prev = this;
|
|
}
|
|
|
|
public refresh (): void {
|
|
this.#expires = Date.now() + this.#ttl;
|
|
}
|
|
|
|
public get expiry (): number {
|
|
return this.#expires;
|
|
}
|
|
}
|
|
|
|
// https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU
|
|
export class LRUCache {
|
|
readonly capacity: number;
|
|
|
|
readonly #data = new Map<string, unknown>();
|
|
readonly #refs = new Map<string, LRUNode>();
|
|
|
|
#length = 0;
|
|
#head: LRUNode;
|
|
#tail: LRUNode;
|
|
|
|
readonly #ttl: number;
|
|
|
|
constructor (capacity = DEFAULT_CAPACITY, ttl: number | null = DEFAULT_TTL) {
|
|
// Validate capacity
|
|
if (!Number.isInteger(capacity) || capacity < 0) {
|
|
throw new Error(`LRUCache initialization error: 'capacity' must be a non-negative integer. Received: ${capacity}`);
|
|
}
|
|
|
|
// Validate ttl
|
|
if (ttl !== null && (!Number.isFinite(ttl) || ttl < 0 || ttl > MAX_TTL)) {
|
|
throw new Error(`LRUCache initialization error: 'ttl' must be between 0 and ${MAX_TTL} ms or null to disable. Received: ${ttl}`);
|
|
}
|
|
|
|
this.capacity = capacity;
|
|
ttl ? this.#ttl = ttl : this.#ttl = DISABLED_TTL;
|
|
this.#head = this.#tail = new LRUNode('<empty>', this.#ttl);
|
|
}
|
|
|
|
get ttl (): number | null {
|
|
return this.#ttl;
|
|
}
|
|
|
|
get length (): number {
|
|
return this.#length;
|
|
}
|
|
|
|
get lengthData (): number {
|
|
return this.#data.size;
|
|
}
|
|
|
|
get lengthRefs (): number {
|
|
return this.#refs.size;
|
|
}
|
|
|
|
entries (): [string, unknown][] {
|
|
const keys = this.keys();
|
|
const count = keys.length;
|
|
const entries = new Array<[string, unknown]>(count);
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const key = keys[i];
|
|
|
|
entries[i] = [key, this.#data.get(key)];
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
keys (): string[] {
|
|
const keys: string[] = [];
|
|
|
|
if (this.#length) {
|
|
let curr = this.#head;
|
|
|
|
while (curr !== this.#tail) {
|
|
keys.push(curr.key);
|
|
curr = curr.next;
|
|
}
|
|
|
|
keys.push(curr.key);
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
get <T> (key: string): T | null {
|
|
const data = this.#data.get(key);
|
|
|
|
if (data) {
|
|
this.#toHead(key);
|
|
|
|
// Evict TTL once data is refreshed
|
|
this.#evictTTL();
|
|
|
|
return data as T;
|
|
}
|
|
|
|
this.#evictTTL();
|
|
|
|
return null;
|
|
}
|
|
|
|
set <T> (key: string, value: T): void {
|
|
if (this.#data.has(key)) {
|
|
this.#toHead(key);
|
|
} else {
|
|
const node = new LRUNode(key, this.#ttl);
|
|
|
|
this.#refs.set(node.key, node);
|
|
|
|
if (this.length === 0) {
|
|
this.#head = this.#tail = node;
|
|
} else {
|
|
this.#head.prev = node;
|
|
node.next = this.#head;
|
|
this.#head = node;
|
|
}
|
|
|
|
if (this.#length === this.capacity) {
|
|
this.#data.delete(this.#tail.key);
|
|
this.#refs.delete(this.#tail.key);
|
|
|
|
this.#tail = this.#tail.prev;
|
|
this.#tail.next = this.#head;
|
|
} else {
|
|
this.#length += 1;
|
|
}
|
|
}
|
|
|
|
// Evict TTL once data is refreshed or added
|
|
this.#evictTTL();
|
|
|
|
this.#data.set(key, value);
|
|
}
|
|
|
|
#evictTTL () {
|
|
// Find last node to keep
|
|
// traverse map to find the expired nodes
|
|
while (this.#tail.expiry && this.#tail.expiry < Date.now() && this.#length > 0) {
|
|
this.#refs.delete(this.#tail.key);
|
|
this.#data.delete(this.#tail.key);
|
|
this.#length -= 1;
|
|
this.#tail = this.#tail.prev;
|
|
this.#tail.next = this.#head;
|
|
}
|
|
|
|
if (this.#length === 0) {
|
|
this.#head = this.#tail = new LRUNode('<empty>', this.#ttl);
|
|
}
|
|
}
|
|
|
|
#toHead (key: string): void {
|
|
const ref = this.#refs.get(key);
|
|
|
|
if (ref && ref !== this.#head) {
|
|
ref.refresh();
|
|
ref.prev.next = ref.next;
|
|
ref.next.prev = ref.prev;
|
|
ref.next = this.#head;
|
|
|
|
this.#head.prev = ref;
|
|
this.#head = ref;
|
|
}
|
|
}
|
|
}
|