@pezkuwi/rpc-provider 16.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +68 -0
  2. package/build/bundle.d.ts +5 -0
  3. package/build/coder/error.d.ts +29 -0
  4. package/build/coder/index.d.ts +8 -0
  5. package/build/defaults.d.ts +5 -0
  6. package/build/http/index.d.ts +81 -0
  7. package/build/http/types.d.ts +7 -0
  8. package/build/index.d.ts +2 -0
  9. package/build/lru.d.ts +15 -0
  10. package/build/mock/index.d.ts +35 -0
  11. package/build/mock/mockHttp.d.ts +9 -0
  12. package/build/mock/mockWs.d.ts +26 -0
  13. package/build/mock/types.d.ts +23 -0
  14. package/build/packageDetect.d.ts +1 -0
  15. package/build/packageInfo.d.ts +6 -0
  16. package/build/substrate-connect/Health.d.ts +7 -0
  17. package/build/substrate-connect/index.d.ts +22 -0
  18. package/build/substrate-connect/types.d.ts +12 -0
  19. package/build/types.d.ts +85 -0
  20. package/build/ws/errors.d.ts +1 -0
  21. package/build/ws/index.d.ts +121 -0
  22. package/package.json +43 -0
  23. package/src/bundle.ts +8 -0
  24. package/src/coder/decodeResponse.spec.ts +70 -0
  25. package/src/coder/encodeJson.spec.ts +20 -0
  26. package/src/coder/encodeObject.spec.ts +25 -0
  27. package/src/coder/error.spec.ts +111 -0
  28. package/src/coder/error.ts +66 -0
  29. package/src/coder/index.ts +88 -0
  30. package/src/defaults.ts +10 -0
  31. package/src/http/index.spec.ts +72 -0
  32. package/src/http/index.ts +238 -0
  33. package/src/http/send.spec.ts +61 -0
  34. package/src/http/types.ts +11 -0
  35. package/src/index.ts +6 -0
  36. package/src/lru.spec.ts +74 -0
  37. package/src/lru.ts +197 -0
  38. package/src/mock/index.ts +259 -0
  39. package/src/mock/mockHttp.ts +35 -0
  40. package/src/mock/mockWs.ts +92 -0
  41. package/src/mock/on.spec.ts +43 -0
  42. package/src/mock/send.spec.ts +38 -0
  43. package/src/mock/subscribe.spec.ts +81 -0
  44. package/src/mock/types.ts +36 -0
  45. package/src/mock/unsubscribe.spec.ts +57 -0
  46. package/src/mod.ts +4 -0
  47. package/src/packageDetect.ts +12 -0
  48. package/src/packageInfo.ts +6 -0
  49. package/src/substrate-connect/Health.ts +325 -0
  50. package/src/substrate-connect/index.spec.ts +638 -0
  51. package/src/substrate-connect/index.ts +415 -0
  52. package/src/substrate-connect/types.ts +16 -0
  53. package/src/types.ts +101 -0
  54. package/src/ws/connect.spec.ts +167 -0
  55. package/src/ws/errors.ts +41 -0
  56. package/src/ws/index.spec.ts +97 -0
  57. package/src/ws/index.ts +652 -0
  58. package/src/ws/send.spec.ts +126 -0
  59. package/src/ws/state.spec.ts +20 -0
  60. package/src/ws/subscribe.spec.ts +68 -0
  61. package/src/ws/unsubscribe.spec.ts +100 -0
  62. package/tsconfig.build.json +17 -0
  63. package/tsconfig.build.tsbuildinfo +1 -0
  64. package/tsconfig.spec.json +18 -0
@@ -0,0 +1,238 @@
1
+ // Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type RpcError from '../coder/error.js';
5
+ import type { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback, ProviderInterfaceEmitCb, ProviderInterfaceEmitted, ProviderStats } from '../types.js';
6
+
7
+ import { logger, noop, stringify } from '@pezkuwi/util';
8
+ import { fetch } from '@pezkuwi/x-fetch';
9
+
10
+ import { RpcCoder } from '../coder/index.js';
11
+ import defaults from '../defaults.js';
12
+ import { DEFAULT_CAPACITY, DEFAULT_TTL, LRUCache } from '../lru.js';
13
+
14
+ const ERROR_SUBSCRIBE = 'HTTP Provider does not have subscriptions, use WebSockets instead';
15
+
16
+ const l = logger('api-http');
17
+
18
+ /**
19
+ * # @polkadot/rpc-provider
20
+ *
21
+ * @name HttpProvider
22
+ *
23
+ * @description The HTTP Provider allows sending requests using HTTP to a HTTP RPC server TCP port. It does not support subscriptions so you won't be able to listen to events such as new blocks or balance changes. It is usually preferable using the [[WsProvider]].
24
+ *
25
+ * @example
26
+ * <BR>
27
+ *
28
+ * ```javascript
29
+ * import Api from '@pezkuwi/api/promise';
30
+ * import { HttpProvider } from '@pezkuwi/rpc-provider';
31
+ *
32
+ * const provider = new HttpProvider('http://127.0.0.1:9933');
33
+ * const api = new Api(provider);
34
+ * ```
35
+ *
36
+ * @see [[WsProvider]]
37
+ */
38
+ export class HttpProvider implements ProviderInterface {
39
+ readonly #callCache: LRUCache;
40
+ readonly #cacheCapacity: number;
41
+ readonly #coder: RpcCoder;
42
+ readonly #endpoint: string;
43
+ readonly #headers: Record<string, string>;
44
+ readonly #stats: ProviderStats;
45
+ readonly #ttl: number | null | undefined;
46
+
47
+ /**
48
+ * @param {string} endpoint The endpoint url starting with http://
49
+ * @param {Record<string, string>} headers The headers provided to the underlying Http Endpoint
50
+ * @param {number} [cacheCapacity] Custom size of the HttpProvider LRUCache. Defaults to `DEFAULT_CAPACITY` (1024)
51
+ * @param {number} [cacheTtl] Custom TTL of the HttpProvider LRUCache. Determines how long an object can live in the cache. Defaults to `DEFAULT_TTL` (30000)
52
+ */
53
+ constructor (endpoint: string = defaults.HTTP_URL, headers: Record<string, string> = {}, cacheCapacity?: number, cacheTtl?: number | null) {
54
+ if (!/^(https|http):\/\//.test(endpoint)) {
55
+ throw new Error(`Endpoint should start with 'http://' or 'https://', received '${endpoint}'`);
56
+ }
57
+
58
+ this.#coder = new RpcCoder();
59
+ this.#endpoint = endpoint;
60
+ this.#headers = headers;
61
+ this.#cacheCapacity = cacheCapacity === 0 ? 0 : cacheCapacity || DEFAULT_CAPACITY;
62
+
63
+ const ttl = cacheTtl === undefined ? DEFAULT_TTL : cacheTtl;
64
+
65
+ this.#callCache = new LRUCache(cacheCapacity === 0 ? 0 : cacheCapacity || DEFAULT_CAPACITY, ttl);
66
+ this.#ttl = cacheTtl;
67
+
68
+ this.#stats = {
69
+ active: { requests: 0, subscriptions: 0 },
70
+ total: { bytesRecv: 0, bytesSent: 0, cached: 0, errors: 0, requests: 0, subscriptions: 0, timeout: 0 }
71
+ };
72
+ }
73
+
74
+ /**
75
+ * @summary `true` when this provider supports subscriptions
76
+ */
77
+ public get hasSubscriptions (): boolean {
78
+ return !!false;
79
+ }
80
+
81
+ /**
82
+ * @description Returns a clone of the object
83
+ */
84
+ public clone (): HttpProvider {
85
+ return new HttpProvider(this.#endpoint, this.#headers);
86
+ }
87
+
88
+ /**
89
+ * @description Manually connect from the connection
90
+ */
91
+ public async connect (): Promise<void> {
92
+ // noop
93
+ }
94
+
95
+ /**
96
+ * @description Manually disconnect from the connection
97
+ */
98
+ public async disconnect (): Promise<void> {
99
+ // noop
100
+ }
101
+
102
+ /**
103
+ * @description Returns the connection stats
104
+ */
105
+ public get stats (): ProviderStats {
106
+ return this.#stats;
107
+ }
108
+
109
+ /**
110
+ * @description Returns the connection stats
111
+ */
112
+ public get ttl (): number | null | undefined {
113
+ return this.#ttl;
114
+ }
115
+
116
+ /**
117
+ * @summary `true` when this provider supports clone()
118
+ */
119
+ public get isClonable (): boolean {
120
+ return !!true;
121
+ }
122
+
123
+ /**
124
+ * @summary Whether the node is connected or not.
125
+ * @return {boolean} true if connected
126
+ */
127
+ public get isConnected (): boolean {
128
+ return !!true;
129
+ }
130
+
131
+ /**
132
+ * @summary Events are not supported with the HttpProvider, see [[WsProvider]].
133
+ * @description HTTP Provider does not have 'on' emitters. WebSockets should be used instead.
134
+ */
135
+ public on (_type: ProviderInterfaceEmitted, _sub: ProviderInterfaceEmitCb): () => void {
136
+ l.error('HTTP Provider does not have \'on\' emitters, use WebSockets instead');
137
+
138
+ return noop;
139
+ }
140
+
141
+ /**
142
+ * @summary Send HTTP POST Request with Body to configured HTTP Endpoint.
143
+ */
144
+ public async send <T> (method: string, params: unknown[], isCacheable?: boolean): Promise<T> {
145
+ this.#stats.total.requests++;
146
+
147
+ const [, body] = this.#coder.encodeJson(method, params);
148
+
149
+ if (this.#cacheCapacity === 0) {
150
+ return this.#send(body);
151
+ }
152
+
153
+ const cacheKey = isCacheable ? `${method}::${stringify(params)}` : '';
154
+ let resultPromise: Promise<T> | null = isCacheable
155
+ ? this.#callCache.get(cacheKey)
156
+ : null;
157
+
158
+ if (!resultPromise) {
159
+ resultPromise = this.#send(body);
160
+
161
+ if (isCacheable) {
162
+ this.#callCache.set(cacheKey, resultPromise);
163
+ }
164
+ } else {
165
+ this.#stats.total.cached++;
166
+ }
167
+
168
+ return resultPromise;
169
+ }
170
+
171
+ async #send <T> (body: string): Promise<T> {
172
+ this.#stats.active.requests++;
173
+ this.#stats.total.bytesSent += body.length;
174
+
175
+ try {
176
+ const response = await fetch(this.#endpoint, {
177
+ body,
178
+ headers: {
179
+ Accept: 'application/json',
180
+ 'Content-Length': `${body.length}`,
181
+ 'Content-Type': 'application/json',
182
+ ...this.#headers
183
+ },
184
+ method: 'POST'
185
+ });
186
+
187
+ if (!response.ok) {
188
+ throw new Error(`[${response.status}]: ${response.statusText}`);
189
+ }
190
+
191
+ const result = await response.text();
192
+
193
+ this.#stats.total.bytesRecv += result.length;
194
+
195
+ const decoded = this.#coder.decodeResponse(JSON.parse(result) as JsonRpcResponse<T>);
196
+
197
+ this.#stats.active.requests--;
198
+
199
+ return decoded;
200
+ } catch (e) {
201
+ this.#stats.active.requests--;
202
+ this.#stats.total.errors++;
203
+
204
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
205
+ const { method, params } = JSON.parse(body);
206
+
207
+ const rpcError: RpcError = e as RpcError;
208
+
209
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
210
+ const failedRequest = `\nFailed HTTP Request: ${JSON.stringify({ method, params })}`;
211
+
212
+ // Provide HTTP Request alongside the error
213
+ rpcError.message = `${rpcError.message}${failedRequest}`;
214
+
215
+ throw rpcError;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * @summary Subscriptions are not supported with the HttpProvider, see [[WsProvider]].
221
+ */
222
+ // eslint-disable-next-line @typescript-eslint/require-await
223
+ public async subscribe (_types: string, _method: string, _params: unknown[], _cb: ProviderInterfaceCallback): Promise<number> {
224
+ l.error(ERROR_SUBSCRIBE);
225
+
226
+ throw new Error(ERROR_SUBSCRIBE);
227
+ }
228
+
229
+ /**
230
+ * @summary Subscriptions are not supported with the HttpProvider, see [[WsProvider]].
231
+ */
232
+ // eslint-disable-next-line @typescript-eslint/require-await
233
+ public async unsubscribe (_type: string, _method: string, _id: number): Promise<boolean> {
234
+ l.error(ERROR_SUBSCRIBE);
235
+
236
+ throw new Error(ERROR_SUBSCRIBE);
237
+ }
238
+ }
@@ -0,0 +1,61 @@
1
+ // Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /// <reference types="@pezkuwi/dev-test/globals.d.ts" />
5
+
6
+ import type { Mock } from '../mock/types.js';
7
+
8
+ import { mockHttp, TEST_HTTP_URL } from '../mock/mockHttp.js';
9
+ import { HttpProvider } from './index.js';
10
+
11
+ // Does not work with Node 18 (native fetch)
12
+ // See https://github.com/nock/nock/issues/2397
13
+ // eslint-disable-next-line jest/no-disabled-tests
14
+ describe.skip('send', (): void => {
15
+ let http: HttpProvider;
16
+ let mock: Mock;
17
+
18
+ beforeEach((): void => {
19
+ http = new HttpProvider(TEST_HTTP_URL);
20
+ });
21
+
22
+ afterEach(async () => {
23
+ if (mock) {
24
+ await mock.done();
25
+ }
26
+ });
27
+
28
+ it('passes the body through correctly', (): Promise<void> => {
29
+ mock = mockHttp([{
30
+ method: 'test_body',
31
+ reply: {
32
+ result: 'ok'
33
+ }
34
+ }]);
35
+
36
+ return http
37
+ .send('test_body', ['param'])
38
+ .then((): void => {
39
+ expect(mock.body['test_body']).toEqual({
40
+ id: 1,
41
+ jsonrpc: '2.0',
42
+ method: 'test_body',
43
+ params: ['param']
44
+ });
45
+ });
46
+ });
47
+
48
+ it('throws error when !response.ok', async (): Promise<any> => {
49
+ mock = mockHttp([{
50
+ code: 500,
51
+ method: 'test_error'
52
+ }]);
53
+
54
+ return http
55
+ .send('test_error', [])
56
+ .catch((error): void => {
57
+ // eslint-disable-next-line jest/no-conditional-expect
58
+ expect((error as Error).message).toMatch(/\[500\]/);
59
+ });
60
+ });
61
+ });
@@ -0,0 +1,11 @@
1
+ // Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { Logger } from '@pezkuwi/util/types';
5
+ import type { RpcCoder } from '../coder/index.js';
6
+
7
+ export interface HttpState {
8
+ coder: RpcCoder;
9
+ endpoint: string;
10
+ l: Logger;
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ // Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import './packageDetect.js';
5
+
6
+ export * from './bundle.js';
@@ -0,0 +1,74 @@
1
+ // Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /// <reference types="@pezkuwi/dev-test/globals.d.ts" />
5
+
6
+ import { LRUCache } from './lru.js';
7
+
8
+ describe('LRUCache', (): void => {
9
+ let lru: LRUCache | undefined;
10
+
11
+ beforeEach((): void => {
12
+ lru = new LRUCache(4, 500);
13
+ });
14
+
15
+ it('allows getting of items below capacity', (): void => {
16
+ const keys = ['1', '2', '3', '4'];
17
+
18
+ keys.forEach((k) => lru?.set(k, `${k}${k}${k}`));
19
+ const lruKeys = lru?.keys();
20
+
21
+ expect(lruKeys?.join(', ')).toBe(keys.reverse().join(', '));
22
+ expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
23
+
24
+ keys.forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`));
25
+ });
26
+
27
+ it('drops items when at capacity', (): void => {
28
+ const keys = ['1', '2', '3', '4', '5', '6'];
29
+
30
+ keys.forEach((k) => lru?.set(k, `${k}${k}${k}`));
31
+
32
+ expect(lru?.keys().join(', ')).toEqual(keys.slice(2).reverse().join(', '));
33
+ expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
34
+
35
+ keys.slice(2).forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`));
36
+ });
37
+
38
+ it('adjusts the order as they are used', (): void => {
39
+ const keys = ['1', '2', '3', '4', '5'];
40
+
41
+ keys.forEach((k) => lru?.set(k, `${k}${k}${k}`));
42
+
43
+ expect(lru?.entries()).toEqual([['5', '555'], ['4', '444'], ['3', '333'], ['2', '222']]);
44
+ expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
45
+
46
+ lru?.get('3');
47
+
48
+ expect(lru?.entries()).toEqual([['3', '333'], ['5', '555'], ['4', '444'], ['2', '222']]);
49
+ expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
50
+
51
+ lru?.set('4', '4433');
52
+
53
+ expect(lru?.entries()).toEqual([['4', '4433'], ['3', '333'], ['5', '555'], ['2', '222']]);
54
+ expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
55
+
56
+ lru?.set('6', '666');
57
+
58
+ expect(lru?.entries()).toEqual([['6', '666'], ['4', '4433'], ['3', '333'], ['5', '555']]);
59
+ expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
60
+ });
61
+
62
+ it('evicts items with TTL', (): void => {
63
+ const keys = ['1', '2', '3', '4', '5'];
64
+
65
+ keys.forEach((k) => lru?.set(k, `${k}${k}${k}`));
66
+
67
+ expect(lru?.entries()).toEqual([['5', '555'], ['4', '444'], ['3', '333'], ['2', '222']]);
68
+
69
+ setTimeout((): void => {
70
+ lru?.get('3');
71
+ expect(lru?.entries()).toEqual([['3', '333']]);
72
+ }, 800);
73
+ });
74
+ });
package/src/lru.ts ADDED
@@ -0,0 +1,197 @@
1
+ // Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // Assuming all 1.5MB responses, we apply a default allowing for 192MB
5
+ // cache space (depending on the historic queries this would vary, metadata
6
+ // for Kusama/Polkadot/Substrate falls between 600-750K, 2x for estimate)
7
+
8
+ export const DEFAULT_CAPACITY = 1024;
9
+ export const DEFAULT_TTL = 30000; // 30 seconds
10
+ const MAX_TTL = 1800_000; // 30 minutes
11
+
12
+ // If the user decides to disable the TTL we set the value
13
+ // to a very high number (A year = 365 * 24 * 60 * 60 * 1000).
14
+ const DISABLED_TTL = 31_536_000_000;
15
+
16
+ class LRUNode {
17
+ readonly key: string;
18
+ #expires: number;
19
+ #ttl: number;
20
+ readonly createdAt: number;
21
+
22
+ public next: LRUNode;
23
+ public prev: LRUNode;
24
+
25
+ constructor (key: string, ttl: number) {
26
+ this.key = key;
27
+ this.#ttl = ttl;
28
+ this.#expires = Date.now() + ttl;
29
+ this.createdAt = Date.now();
30
+ this.next = this.prev = this;
31
+ }
32
+
33
+ public refresh (): void {
34
+ this.#expires = Date.now() + this.#ttl;
35
+ }
36
+
37
+ public get expiry (): number {
38
+ return this.#expires;
39
+ }
40
+ }
41
+
42
+ // https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU
43
+ export class LRUCache {
44
+ readonly capacity: number;
45
+
46
+ readonly #data = new Map<string, unknown>();
47
+ readonly #refs = new Map<string, LRUNode>();
48
+
49
+ #length = 0;
50
+ #head: LRUNode;
51
+ #tail: LRUNode;
52
+
53
+ readonly #ttl: number;
54
+
55
+ constructor (capacity = DEFAULT_CAPACITY, ttl: number | null = DEFAULT_TTL) {
56
+ // Validate capacity
57
+ if (!Number.isInteger(capacity) || capacity < 0) {
58
+ throw new Error(`LRUCache initialization error: 'capacity' must be a non-negative integer. Received: ${capacity}`);
59
+ }
60
+
61
+ // Validate ttl
62
+ if (ttl !== null && (!Number.isFinite(ttl) || ttl < 0 || ttl > MAX_TTL)) {
63
+ throw new Error(`LRUCache initialization error: 'ttl' must be between 0 and ${MAX_TTL} ms or null to disable. Received: ${ttl}`);
64
+ }
65
+
66
+ this.capacity = capacity;
67
+ ttl ? this.#ttl = ttl : this.#ttl = DISABLED_TTL;
68
+ this.#head = this.#tail = new LRUNode('<empty>', this.#ttl);
69
+ }
70
+
71
+ get ttl (): number | null {
72
+ return this.#ttl;
73
+ }
74
+
75
+ get length (): number {
76
+ return this.#length;
77
+ }
78
+
79
+ get lengthData (): number {
80
+ return this.#data.size;
81
+ }
82
+
83
+ get lengthRefs (): number {
84
+ return this.#refs.size;
85
+ }
86
+
87
+ entries (): [string, unknown][] {
88
+ const keys = this.keys();
89
+ const count = keys.length;
90
+ const entries = new Array<[string, unknown]>(count);
91
+
92
+ for (let i = 0; i < count; i++) {
93
+ const key = keys[i];
94
+
95
+ entries[i] = [key, this.#data.get(key)];
96
+ }
97
+
98
+ return entries;
99
+ }
100
+
101
+ keys (): string[] {
102
+ const keys: string[] = [];
103
+
104
+ if (this.#length) {
105
+ let curr = this.#head;
106
+
107
+ while (curr !== this.#tail) {
108
+ keys.push(curr.key);
109
+ curr = curr.next;
110
+ }
111
+
112
+ keys.push(curr.key);
113
+ }
114
+
115
+ return keys;
116
+ }
117
+
118
+ get <T> (key: string): T | null {
119
+ const data = this.#data.get(key);
120
+
121
+ if (data) {
122
+ this.#toHead(key);
123
+
124
+ // Evict TTL once data is refreshed
125
+ this.#evictTTL();
126
+
127
+ return data as T;
128
+ }
129
+
130
+ this.#evictTTL();
131
+
132
+ return null;
133
+ }
134
+
135
+ set <T> (key: string, value: T): void {
136
+ if (this.#data.has(key)) {
137
+ this.#toHead(key);
138
+ } else {
139
+ const node = new LRUNode(key, this.#ttl);
140
+
141
+ this.#refs.set(node.key, node);
142
+
143
+ if (this.length === 0) {
144
+ this.#head = this.#tail = node;
145
+ } else {
146
+ this.#head.prev = node;
147
+ node.next = this.#head;
148
+ this.#head = node;
149
+ }
150
+
151
+ if (this.#length === this.capacity) {
152
+ this.#data.delete(this.#tail.key);
153
+ this.#refs.delete(this.#tail.key);
154
+
155
+ this.#tail = this.#tail.prev;
156
+ this.#tail.next = this.#head;
157
+ } else {
158
+ this.#length += 1;
159
+ }
160
+ }
161
+
162
+ // Evict TTL once data is refreshed or added
163
+ this.#evictTTL();
164
+
165
+ this.#data.set(key, value);
166
+ }
167
+
168
+ #evictTTL () {
169
+ // Find last node to keep
170
+ // traverse map to find the expired nodes
171
+ while (this.#tail.expiry && this.#tail.expiry < Date.now() && this.#length > 0) {
172
+ this.#refs.delete(this.#tail.key);
173
+ this.#data.delete(this.#tail.key);
174
+ this.#length -= 1;
175
+ this.#tail = this.#tail.prev;
176
+ this.#tail.next = this.#head;
177
+ }
178
+
179
+ if (this.#length === 0) {
180
+ this.#head = this.#tail = new LRUNode('<empty>', this.#ttl);
181
+ }
182
+ }
183
+
184
+ #toHead (key: string): void {
185
+ const ref = this.#refs.get(key);
186
+
187
+ if (ref && ref !== this.#head) {
188
+ ref.refresh();
189
+ ref.prev.next = ref.next;
190
+ ref.next.prev = ref.prev;
191
+ ref.next = this.#head;
192
+
193
+ this.#head.prev = ref;
194
+ this.#head = ref;
195
+ }
196
+ }
197
+ }