diff --git a/packages/rpc-core/src/bundle.ts b/packages/rpc-core/src/bundle.ts index 49b879029f2..7a99908b054 100644 --- a/packages/rpc-core/src/bundle.ts +++ b/packages/rpc-core/src/bundle.ts @@ -11,11 +11,11 @@ import type { RpcCoreStats, RpcInterfaceMethod } from './types/index.js'; import { Observable, publishReplay, refCount } from 'rxjs'; +import { DEFAULT_CAPACITY, LRUCache } from '@polkadot/rpc-provider'; import { rpcDefinitions } from '@polkadot/types'; import { hexToU8a, isFunction, isNull, isUndefined, lazyMethod, logger, memoize, objectSpread, u8aConcat, u8aToU8a } from '@polkadot/util'; import { drr, refCountDelay } from './util/index.js'; -import { DEFAULT_CAPACITY, LRUCache } from './lru.js'; export { packageInfo } from './packageInfo.js'; export * from './util/index.js'; diff --git a/packages/rpc-core/src/lru.spec.ts b/packages/rpc-core/src/lru.spec.ts deleted file mode 100644 index d36e05b677c..00000000000 --- a/packages/rpc-core/src/lru.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -/// - -import { LRUCache } from './lru.js'; - -describe('LRUCache', (): void => { - let lru: LRUCache | undefined; - - beforeEach((): void => { - lru = new LRUCache(4); - }); - afterEach(async () => { - await lru?.clearInterval(); - lru = undefined; - }); - - it('allows getting of items below capacity', (): void => { - const keys = ['1', '2', '3', '4']; - - keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); - const lruKeys = lru?.keys(); - - expect(lruKeys?.join(', ')).toBe(keys.reverse().join(', ')); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - - keys.forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`)); - }); - - it('drops items when at capacity', (): void => { - const keys = ['1', '2', '3', '4', '5', '6']; - - keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); - - expect(lru?.keys().join(', ')).toEqual(keys.slice(2).reverse().join(', ')); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - - keys.slice(2).forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`)); - }); - - it('adjusts the order as they are used', (): void => { - const keys = ['1', '2', '3', '4', '5']; - - keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); - - expect(lru?.entries()).toEqual([['5', '555'], ['4', '444'], ['3', '333'], ['2', '222']]); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - - lru?.get('3'); - - expect(lru?.entries()).toEqual([['3', '333'], ['5', '555'], ['4', '444'], ['2', '222']]); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - - lru?.set('4', '4433'); - - expect(lru?.entries()).toEqual([['4', '4433'], ['3', '333'], ['5', '555'], ['2', '222']]); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - - lru?.set('6', '666'); - - expect(lru?.entries()).toEqual([['6', '666'], ['4', '4433'], ['3', '333'], ['5', '555']]); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - }); -}); diff --git a/packages/rpc-core/src/lru.ts b/packages/rpc-core/src/lru.ts deleted file mode 100644 index 9483b718b4f..00000000000 --- a/packages/rpc-core/src/lru.ts +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2017-2024 @polkadot/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 Kusama/Polkadot/Substrate falls between 600-750K, 2x for estimate) - -export const DEFAULT_CAPACITY = 64; - -class LRUNode { - readonly key: string; - #lastAccess: number; - readonly createdAt: number; - - public next: LRUNode; - public prev: LRUNode; - - constructor (key: string) { - this.key = key; - this.#lastAccess = Date.now(); - this.createdAt = this.#lastAccess; - this.next = this.prev = this; - } - - public refresh (): void { - this.#lastAccess = Date.now(); - } - - public get lastAccess (): number { - return this.#lastAccess; - } -} - -// https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU -export class LRUCache { - readonly capacity: number; - - readonly #data = new Map(); - readonly #refs = new Map(); - - #length = 0; - #head: LRUNode; - #tail: LRUNode; - // TTL - readonly #ttl: number; - readonly #ttlInterval: number; - #ttlTimerId: ReturnType | null = null; - - constructor (capacity = DEFAULT_CAPACITY, ttl = 30000, ttlInterval = 15000) { - this.capacity = capacity; - this.#ttl = ttl; - this.#ttlInterval = ttlInterval; - this.#head = this.#tail = new LRUNode(''); - - // make sure the interval is not longer than the ttl - if (this.#ttlInterval > this.#ttl) { - this.#ttlInterval = this.#ttl; - } - } - - get ttl (): number { - return this.#ttl; - } - - get ttlInterval (): number { - return this.#ttlInterval; - } - - 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 (key: string): T | null { - const data = this.#data.get(key); - - if (data) { - this.#toHead(key); - - return data as T; - } - - return null; - } - - set (key: string, value: T): void { - if (this.#data.has(key)) { - this.#toHead(key); - } else { - const node = new LRUNode(key); - - 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; - } - } - - if (this.#ttl > 0 && !this.#ttlTimerId) { - this.#ttlTimerId = setInterval(() => { - this.#ttlClean(); - }, this.#ttlInterval); - } - - this.#data.set(key, value); - } - - #ttlClean () { - // Find last node to keep - const expires = Date.now() - this.#ttl; - - // traverse map to find the lastAccessed - while (this.#tail.lastAccess && this.#tail.lastAccess < expires && this.#length > 0) { - if (this.#ttlTimerId && this.#length === 0) { - clearInterval(this.#ttlTimerId); - this.#ttlTimerId = null; - this.#head = this.#tail = new LRUNode(''); - } else { - 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; - } - } - } - - #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; - } - } - - // eslint-disable-next-line @typescript-eslint/require-await - public async clearInterval (): Promise { - if (this.#ttlTimerId) { - clearInterval(this.#ttlTimerId); - this.#ttlTimerId = null; - } - } -} diff --git a/packages/rpc-provider/src/bundle.ts b/packages/rpc-provider/src/bundle.ts index 06b0acaf1aa..8a41c307b73 100644 --- a/packages/rpc-provider/src/bundle.ts +++ b/packages/rpc-provider/src/bundle.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 export { HttpProvider } from './http/index.js'; +export { DEFAULT_CAPACITY, LRUCache } from './lru.js'; export { packageInfo } from './packageInfo.js'; export { ScProvider } from './substrate-connect/index.js'; export { WsProvider } from './ws/index.js';