@dedot/api 0.15.3-next.65898ecf.10 → 0.16.0

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.
@@ -1 +1,2 @@
1
+ // Generated by dedot cli
1
2
  export * from './types.js';
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ // Generated by dedot cli
2
3
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
4
  if (k2 === undefined) k2 = k;
4
5
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -10,6 +10,8 @@ const proxychain_js_1 = require("../proxychain.js");
10
10
  const index_js_3 = require("../storage/index.js");
11
11
  const SUPPORTED_METADATA_VERSIONS = [16, 15, 14];
12
12
  const MetadataApiHash = (0, utils_1.calcRuntimeApiHash)('Metadata'); // 0x37e397fc7c91f5e4
13
+ const API_AT_CACHE_CAPACITY = 64;
14
+ const API_AT_CACHE_TTL = 300_000; // 5 minutes
13
15
  const MESSAGE = 'Make sure to call `.connect()` method first before using the API interfaces.';
14
16
  function ensurePresence(value) {
15
17
  return (0, utils_1.ensurePresence)(value, MESSAGE);
@@ -28,10 +30,12 @@ class BaseSubstrateClient extends index_js_2.JsonRpcClient {
28
30
  _runtimeVersion;
29
31
  _localCache;
30
32
  _runtimeUpgrading;
33
+ _apiAtCache;
31
34
  constructor(rpcVersion, options) {
32
35
  super(options);
33
36
  this.rpcVersion = rpcVersion;
34
37
  this._options = this.normalizeOptions(options);
38
+ this._apiAtCache = new utils_1.LRUCache(API_AT_CACHE_CAPACITY, API_AT_CACHE_TTL);
35
39
  }
36
40
  /// --- Internal logics
37
41
  normalizeOptions(options) {
@@ -189,12 +193,17 @@ class BaseSubstrateClient extends index_js_2.JsonRpcClient {
189
193
  this._genesisHash = undefined;
190
194
  this._runtimeVersion = undefined;
191
195
  this._localCache = undefined;
196
+ this._apiAtCache.clear();
192
197
  }
193
198
  /**
194
- * @description Clear local cache
199
+ * @description Clear local cache and API at-block cache
200
+ * @param keepMetadataCache Keep the metadata cache, only clear other caches.
195
201
  */
196
- async clearCache() {
197
- await this._localCache?.clear();
202
+ async clearCache(keepMetadataCache = false) {
203
+ if (!keepMetadataCache) {
204
+ await this._localCache?.clear();
205
+ }
206
+ this._apiAtCache.clear();
198
207
  }
199
208
  async doConnect() {
200
209
  // @ts-ignore
@@ -21,7 +21,6 @@ class DedotClient// prettier-end-here
21
21
  _chainSpec;
22
22
  _archive;
23
23
  _txBroadcaster;
24
- #apiAtCache = {};
25
24
  /**
26
25
  * Use factory methods (`create`, `new`) to create `DedotClient` instances.
27
26
  *
@@ -147,7 +146,14 @@ class DedotClient// prettier-end-here
147
146
  this._chainSpec = undefined;
148
147
  this._archive = undefined;
149
148
  this._txBroadcaster = undefined;
150
- this.#apiAtCache = {};
149
+ }
150
+ /**
151
+ * @description Clear local cache, API at-block cache, and ChainHead cache
152
+ * @param keepMetadataCache Keep the metadata cache, only clear other caches.
153
+ */
154
+ async clearCache(keepMetadataCache = false) {
155
+ await super.clearCache(keepMetadataCache);
156
+ this._chainHead?.clearCache();
151
157
  }
152
158
  get query() {
153
159
  return (0, proxychain_js_1.newProxyChain)({
@@ -177,8 +183,9 @@ class DedotClient// prettier-end-here
177
183
  * @param hash
178
184
  */
179
185
  async at(hash) {
180
- if (this.#apiAtCache[hash])
181
- return this.#apiAtCache[hash];
186
+ const cached = this._apiAtCache.get(hash);
187
+ if (cached)
188
+ return cached;
182
189
  let targetVersion;
183
190
  // Try to get block info from ChainHead first (for pinned blocks)
184
191
  const targetBlock = this.chainHead.findBlock(hash);
@@ -192,10 +199,10 @@ class DedotClient// prettier-end-here
192
199
  else {
193
200
  // Block not pinned, try via Archive fallback if supported
194
201
  if (this._archive && (await this._archive.supported())) {
195
- console.warn(`Block ${hash} is not pinned, using Archive for historical access`);
196
202
  try {
197
203
  // Fetch runtime version via Archive
198
204
  const runtimeRaw = await this._archive.call('Core_version', '0x', hash);
205
+ (0, utils_1.assert)(runtimeRaw, 'Runtime Version Not Found');
199
206
  targetVersion = this.toSubstrateRuntimeVersion(codecs_1.$RuntimeVersion.tryDecode(runtimeRaw));
200
207
  }
201
208
  catch (error) {
@@ -228,7 +235,7 @@ class DedotClient// prettier-end-here
228
235
  api.query = (0, proxychain_js_1.newProxyChain)({ executor: new index_js_1.StorageQueryExecutorV2(api, this.chainHead) });
229
236
  api.call = (0, proxychain_js_1.newProxyChain)({ executor: new index_js_1.RuntimeApiExecutorV2(api, this.chainHead) });
230
237
  api.view = (0, proxychain_js_1.newProxyChain)({ executor: new index_js_1.ViewFunctionExecutorV2(api, this.chainHead) });
231
- this.#apiAtCache[hash] = api;
238
+ this._apiAtCache.set(hash, api);
232
239
  return api;
233
240
  }
234
241
  getStorageQuery() {
@@ -49,7 +49,6 @@ class LegacyClient// prettier-end-here
49
49
  extends BaseSubstrateClient_js_1.BaseSubstrateClient {
50
50
  #runtimeSubscriptionUnsub;
51
51
  #healthTimer;
52
- #apiAtCache = {};
53
52
  /**
54
53
  * Use factory methods (`create`, `new`) to create `Dedot` instances.
55
54
  *
@@ -96,7 +95,6 @@ class LegacyClient// prettier-end-here
96
95
  }
97
96
  cleanUp() {
98
97
  super.cleanUp();
99
- this.#apiAtCache = {};
100
98
  this.#healthTimer = undefined;
101
99
  this.#runtimeSubscriptionUnsub = undefined;
102
100
  }
@@ -232,8 +230,9 @@ class LegacyClient// prettier-end-here
232
230
  * @param hash
233
231
  */
234
232
  async at(hash) {
235
- if (this.#apiAtCache[hash])
236
- return this.#apiAtCache[hash];
233
+ const cached = this._apiAtCache.get(hash);
234
+ if (cached)
235
+ return cached;
237
236
  const targetVersion = await this.#getRuntimeVersion(hash);
238
237
  let metadata = this.metadata;
239
238
  let registry = this.registry;
@@ -256,7 +255,7 @@ class LegacyClient// prettier-end-here
256
255
  api.call = (0, proxychain_js_1.newProxyChain)({ executor: new index_js_1.RuntimeApiExecutor(api) });
257
256
  api.events = (0, proxychain_js_1.newProxyChain)({ executor: new index_js_1.EventExecutor(api) });
258
257
  api.errors = (0, proxychain_js_1.newProxyChain)({ executor: new index_js_1.ErrorExecutor(api) });
259
- this.#apiAtCache[hash] = api;
258
+ this._apiAtCache.set(hash, api);
260
259
  return api;
261
260
  }
262
261
  getStorageQuery() {
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Archive = void 0;
4
4
  const utils_1 = require("@dedot/utils");
5
5
  const JsonRpcGroup_js_1 = require("./JsonRpcGroup.js");
6
+ const ARCHIVE_CACHE_CAPACITY = 256;
7
+ const ARCHIVE_CACHE_TTL = 60_000; // 1 minutes - archive data is immutable
6
8
  /**
7
9
  * @name Archive
8
10
  * Archive JSON-RPC methods for accessing historical blockchain data.
@@ -16,7 +18,7 @@ class Archive extends JsonRpcGroup_js_1.JsonRpcGroup {
16
18
  #cache;
17
19
  constructor(client, options) {
18
20
  super(client, { prefix: 'archive', supportedVersions: ['unstable', 'v1'], ...options });
19
- this.#cache = new Map();
21
+ this.#cache = new utils_1.LRUCache(ARCHIVE_CACHE_CAPACITY, ARCHIVE_CACHE_TTL);
20
22
  }
21
23
  /**
22
24
  * Retrieves the body (list of transactions) of a given block.
@@ -38,8 +40,9 @@ class Archive extends JsonRpcGroup_js_1.JsonRpcGroup {
38
40
  async body(hash) {
39
41
  const blockHash = hash || (await this.finalizedHash());
40
42
  const cacheKey = `${blockHash}::body`;
41
- if (this.#cache.has(cacheKey)) {
42
- return this.#cache.get(cacheKey);
43
+ const cached = this.#cache.get(cacheKey);
44
+ if (cached !== null) {
45
+ return cached;
43
46
  }
44
47
  const result = await this.send('body', blockHash);
45
48
  this.#cache.set(cacheKey, result);
@@ -77,8 +80,9 @@ class Archive extends JsonRpcGroup_js_1.JsonRpcGroup {
77
80
  async header(hash) {
78
81
  const blockHash = hash || (await this.finalizedHash());
79
82
  const cacheKey = `${blockHash}::header`;
80
- if (this.#cache.has(cacheKey)) {
81
- return this.#cache.get(cacheKey);
83
+ const cached = this.#cache.get(cacheKey);
84
+ if (cached !== null) {
85
+ return cached;
82
86
  }
83
87
  const result = await this.send('header', blockHash);
84
88
  this.#cache.set(cacheKey, result);
@@ -141,8 +145,9 @@ class Archive extends JsonRpcGroup_js_1.JsonRpcGroup {
141
145
  async call(func, params, hash) {
142
146
  const blockHash = hash || (await this.finalizedHash());
143
147
  const cacheKey = `${blockHash}::call::${func}::${params}`;
144
- if (this.#cache.has(cacheKey)) {
145
- return this.#cache.get(cacheKey);
148
+ const cached = this.#cache.get(cacheKey);
149
+ if (cached !== null) {
150
+ return cached;
146
151
  }
147
152
  const result = await this.send('call', blockHash, func, params);
148
153
  if (!result.success) {
@@ -189,8 +194,9 @@ class Archive extends JsonRpcGroup_js_1.JsonRpcGroup {
189
194
  // Generate cache key
190
195
  const cacheKey = `${blockHash}::storage::${JSON.stringify(items)}::${childTrie ?? null}`;
191
196
  // Check cache
192
- if (this.#cache.has(cacheKey)) {
193
- return resolve(this.#cache.get(cacheKey));
197
+ const cached = this.#cache.get(cacheKey);
198
+ if (cached !== null) {
199
+ return resolve(cached);
194
200
  }
195
201
  this.#storageSubscription(items, childTrie || null, (event) => {
196
202
  switch (event.event) {
@@ -7,6 +7,8 @@ const JsonRpcGroup_js_1 = require("../JsonRpcGroup.js");
7
7
  const BlockUsage_js_1 = require("./BlockUsage.js");
8
8
  const error_js_1 = require("./error.js");
9
9
  exports.MIN_FINALIZED_QUEUE_SIZE = 10; // finalized queue size
10
+ const CHAINHEAD_CACHE_CAPACITY = 256;
11
+ const CHAINHEAD_CACHE_TTL = 30_000; // 30 seconds
10
12
  class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
11
13
  #unsub;
12
14
  #subscriptionId;
@@ -43,7 +45,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
43
45
  this.#followResponseQueue = new utils_1.AsyncQueue();
44
46
  this.#retryQueue = new utils_1.AsyncQueue();
45
47
  this.#blockUsage = new BlockUsage_js_1.BlockUsage();
46
- this.#cache = new Map();
48
+ this.#cache = new utils_1.LRUCache(CHAINHEAD_CACHE_CAPACITY, CHAINHEAD_CACHE_TTL);
47
49
  // This helps us to not accidentally putting too much stress on the JSON-RPC server, especially smoldot/light-client
48
50
  this.#operationQueue = new utils_1.ThrottleQueue(this.#__unsafe__isSmoldot() ? 25 : 250);
49
51
  }
@@ -245,8 +247,9 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
245
247
  if (!this.isPinned(hash))
246
248
  return;
247
249
  delete this.#pinnedBlocks[hash];
248
- // clear cache
249
- Array.from(this.#cache.keys())
250
+ // Clear cache entries related to the pruned block
251
+ // Filter and remove only cache entries for this specific block
252
+ this.#cache.keys()
250
253
  .filter((key) => key.startsWith(`${hash}::`))
251
254
  .forEach((key) => this.#cache.delete(key));
252
255
  });
@@ -394,7 +397,6 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
394
397
  catch (error) {
395
398
  if (error instanceof error_js_1.ChainHeadBlockNotPinnedError && this.#archive) {
396
399
  const errorHash = error.hash;
397
- console.warn(`Block ${errorHash} not pinned in ChainHead, falling back to Archive`);
398
400
  return await fallback(this.#archive, errorHash);
399
401
  }
400
402
  throw error;
@@ -454,7 +456,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
454
456
  this.#followResponseQueue.clear();
455
457
  this.#retryQueue.clear();
456
458
  this.#blockUsage.clear();
457
- this.#cache.clear();
459
+ this.clearCache();
458
460
  this.#operationQueue.cancel();
459
461
  }
460
462
  async #ensureFollowed() {
@@ -532,8 +534,9 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
532
534
  const operation = async () => {
533
535
  const atHash = this.#ensurePinnedHash(at);
534
536
  const cacheKey = `${atHash}::body`;
535
- if (this.#cache.has(cacheKey)) {
536
- return this.#cache.get(cacheKey);
537
+ const cached = this.#cache.get(cacheKey);
538
+ if (cached) {
539
+ return cached;
537
540
  }
538
541
  const bodyOperation = async () => {
539
542
  await this.#ensureFollowed();
@@ -571,8 +574,9 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
571
574
  const operation = async () => {
572
575
  const atHash = this.#ensurePinnedHash(at);
573
576
  const cacheKey = `${atHash}::call::${func}::${params}`;
574
- if (this.#cache.has(cacheKey)) {
575
- return this.#cache.get(cacheKey);
577
+ const cached = this.#cache.get(cacheKey);
578
+ if (cached) {
579
+ return cached;
576
580
  }
577
581
  const callOperation = async () => {
578
582
  await this.#ensureFollowed();
@@ -603,8 +607,9 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
603
607
  const operation = async () => {
604
608
  const hash = this.#ensurePinnedHash(at);
605
609
  const cacheKey = `${hash}::header`;
606
- if (this.#cache.has(cacheKey)) {
607
- return this.#cache.get(cacheKey);
610
+ const cached = this.#cache.get(cacheKey);
611
+ if (cached) {
612
+ return cached;
608
613
  }
609
614
  const resp = await this.#getHeader(hash);
610
615
  this.#cache.set(cacheKey, resp);
@@ -627,8 +632,9 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
627
632
  try {
628
633
  // JSON.stringify(items) might get big, we probably should do a twox hashing in such case
629
634
  const cacheKey = `${hash}::storage::${JSON.stringify(items)}::${childTrie ?? null}`;
630
- if (this.#cache.has(cacheKey)) {
631
- return this.#cache.get(cacheKey);
635
+ const cached = this.#cache.get(cacheKey);
636
+ if (cached) {
637
+ return cached;
632
638
  }
633
639
  this.#blockUsage.use(hash);
634
640
  let results = [];
@@ -713,5 +719,19 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
713
719
  // @ts-ignore a trick internally to check whether a provider is using smoldot connection
714
720
  return typeof this.client.provider['chain'] === 'function';
715
721
  }
722
+ /**
723
+ * Clears the internal cache used for storing query results (both chainHead & archive instances)
724
+ * This can be useful for memory management or when you want to force fresh data retrieval.
725
+ *
726
+ * @example
727
+ * ```typescript
728
+ * // Clear all cached results
729
+ * chainHead.clearCache();
730
+ * ```
731
+ */
732
+ clearCache() {
733
+ this.#cache.clear();
734
+ this.#archive?.clearCache();
735
+ }
716
736
  }
717
737
  exports.ChainHead = ChainHead;
@@ -2,7 +2,7 @@ import { BlockHash, Hash, Metadata, PortableRegistry, RuntimeVersion } from '@de
2
2
  import type { JsonRpcProvider } from '@dedot/providers';
3
3
  import { type IStorage } from '@dedot/storage';
4
4
  import { Callback, GenericStorageQuery, GenericSubstrateApi, InjectedSigner, Query, QueryFnResult, RpcVersion, Unsub, VersionedGenericSubstrateApi } from '@dedot/types';
5
- import { Deferred } from '@dedot/utils';
5
+ import { Deferred, LRUCache } from '@dedot/utils';
6
6
  import type { SubstrateApi } from '../chaintypes/index.js';
7
7
  import { JsonRpcClient } from '../json-rpc/index.js';
8
8
  import { BaseStorageQuery } from '../storage/index.js';
@@ -21,6 +21,7 @@ export declare abstract class BaseSubstrateClient<Rv extends RpcVersion, ChainAp
21
21
  protected _runtimeVersion?: SubstrateRuntimeVersion;
22
22
  protected _localCache?: IStorage;
23
23
  protected _runtimeUpgrading?: Deferred<void>;
24
+ protected _apiAtCache: LRUCache;
24
25
  protected constructor(rpcVersion: RpcVersion, options: JsonRpcClientOptions | JsonRpcProvider);
25
26
  protected normalizeOptions(options: ApiOptions | JsonRpcProvider): ApiOptions;
26
27
  protected initializeLocalCache(): Promise<void>;
@@ -37,9 +38,10 @@ export declare abstract class BaseSubstrateClient<Rv extends RpcVersion, ChainAp
37
38
  protected fetchMetadata(hash?: BlockHash, runtime?: SubstrateRuntimeVersion): Promise<Metadata>;
38
39
  protected cleanUp(): void;
39
40
  /**
40
- * @description Clear local cache
41
+ * @description Clear local cache and API at-block cache
42
+ * @param keepMetadataCache Keep the metadata cache, only clear other caches.
41
43
  */
42
- clearCache(): Promise<void>;
44
+ clearCache(keepMetadataCache?: boolean): Promise<void>;
43
45
  protected doConnect(): Promise<this>;
44
46
  protected onConnected: () => Promise<void>;
45
47
  protected onDisconnected: () => Promise<void>;
@@ -1,12 +1,14 @@
1
1
  import { $Metadata, PortableRegistry } from '@dedot/codecs';
2
2
  import { LocalStorage } from '@dedot/storage';
3
- import { calcRuntimeApiHash, deferred, ensurePresence as _ensurePresence, u8aToHex } from '@dedot/utils';
3
+ import { calcRuntimeApiHash, deferred, ensurePresence as _ensurePresence, u8aToHex, LRUCache, } from '@dedot/utils';
4
4
  import { ConstantExecutor, ErrorExecutor, EventExecutor } from '../executor/index.js';
5
5
  import { isJsonRpcProvider, JsonRpcClient } from '../json-rpc/index.js';
6
6
  import { newProxyChain } from '../proxychain.js';
7
7
  import { QueryableStorage } from '../storage/index.js';
8
8
  const SUPPORTED_METADATA_VERSIONS = [16, 15, 14];
9
9
  const MetadataApiHash = calcRuntimeApiHash('Metadata'); // 0x37e397fc7c91f5e4
10
+ const API_AT_CACHE_CAPACITY = 64;
11
+ const API_AT_CACHE_TTL = 300_000; // 5 minutes
10
12
  const MESSAGE = 'Make sure to call `.connect()` method first before using the API interfaces.';
11
13
  export function ensurePresence(value) {
12
14
  return _ensurePresence(value, MESSAGE);
@@ -24,10 +26,12 @@ export class BaseSubstrateClient extends JsonRpcClient {
24
26
  _runtimeVersion;
25
27
  _localCache;
26
28
  _runtimeUpgrading;
29
+ _apiAtCache;
27
30
  constructor(rpcVersion, options) {
28
31
  super(options);
29
32
  this.rpcVersion = rpcVersion;
30
33
  this._options = this.normalizeOptions(options);
34
+ this._apiAtCache = new LRUCache(API_AT_CACHE_CAPACITY, API_AT_CACHE_TTL);
31
35
  }
32
36
  /// --- Internal logics
33
37
  normalizeOptions(options) {
@@ -185,12 +189,17 @@ export class BaseSubstrateClient extends JsonRpcClient {
185
189
  this._genesisHash = undefined;
186
190
  this._runtimeVersion = undefined;
187
191
  this._localCache = undefined;
192
+ this._apiAtCache.clear();
188
193
  }
189
194
  /**
190
- * @description Clear local cache
195
+ * @description Clear local cache and API at-block cache
196
+ * @param keepMetadataCache Keep the metadata cache, only clear other caches.
191
197
  */
192
- async clearCache() {
193
- await this._localCache?.clear();
198
+ async clearCache(keepMetadataCache = false) {
199
+ if (!keepMetadataCache) {
200
+ await this._localCache?.clear();
201
+ }
202
+ this._apiAtCache.clear();
194
203
  }
195
204
  async doConnect() {
196
205
  // @ts-ignore
@@ -50,6 +50,11 @@ export declare class DedotClient<ChainApi extends VersionedGenericSubstrateApi =
50
50
  protected beforeDisconnect(): Promise<void>;
51
51
  protected onDisconnected: () => Promise<void>;
52
52
  protected cleanUp(): void;
53
+ /**
54
+ * @description Clear local cache, API at-block cache, and ChainHead cache
55
+ * @param keepMetadataCache Keep the metadata cache, only clear other caches.
56
+ */
57
+ clearCache(keepMetadataCache?: boolean): Promise<void>;
53
58
  get query(): ChainApi[RpcV2]['query'];
54
59
  get view(): ChainApi[RpcV2]['view'];
55
60
  get call(): ChainApi[RpcV2]['call'];
@@ -18,7 +18,6 @@ export class DedotClient// prettier-end-here
18
18
  _chainSpec;
19
19
  _archive;
20
20
  _txBroadcaster;
21
- #apiAtCache = {};
22
21
  /**
23
22
  * Use factory methods (`create`, `new`) to create `DedotClient` instances.
24
23
  *
@@ -144,7 +143,14 @@ export class DedotClient// prettier-end-here
144
143
  this._chainSpec = undefined;
145
144
  this._archive = undefined;
146
145
  this._txBroadcaster = undefined;
147
- this.#apiAtCache = {};
146
+ }
147
+ /**
148
+ * @description Clear local cache, API at-block cache, and ChainHead cache
149
+ * @param keepMetadataCache Keep the metadata cache, only clear other caches.
150
+ */
151
+ async clearCache(keepMetadataCache = false) {
152
+ await super.clearCache(keepMetadataCache);
153
+ this._chainHead?.clearCache();
148
154
  }
149
155
  get query() {
150
156
  return newProxyChain({
@@ -174,8 +180,9 @@ export class DedotClient// prettier-end-here
174
180
  * @param hash
175
181
  */
176
182
  async at(hash) {
177
- if (this.#apiAtCache[hash])
178
- return this.#apiAtCache[hash];
183
+ const cached = this._apiAtCache.get(hash);
184
+ if (cached)
185
+ return cached;
179
186
  let targetVersion;
180
187
  // Try to get block info from ChainHead first (for pinned blocks)
181
188
  const targetBlock = this.chainHead.findBlock(hash);
@@ -189,10 +196,10 @@ export class DedotClient// prettier-end-here
189
196
  else {
190
197
  // Block not pinned, try via Archive fallback if supported
191
198
  if (this._archive && (await this._archive.supported())) {
192
- console.warn(`Block ${hash} is not pinned, using Archive for historical access`);
193
199
  try {
194
200
  // Fetch runtime version via Archive
195
201
  const runtimeRaw = await this._archive.call('Core_version', '0x', hash);
202
+ assert(runtimeRaw, 'Runtime Version Not Found');
196
203
  targetVersion = this.toSubstrateRuntimeVersion($RuntimeVersion.tryDecode(runtimeRaw));
197
204
  }
198
205
  catch (error) {
@@ -225,7 +232,7 @@ export class DedotClient// prettier-end-here
225
232
  api.query = newProxyChain({ executor: new StorageQueryExecutorV2(api, this.chainHead) });
226
233
  api.call = newProxyChain({ executor: new RuntimeApiExecutorV2(api, this.chainHead) });
227
234
  api.view = newProxyChain({ executor: new ViewFunctionExecutorV2(api, this.chainHead) });
228
- this.#apiAtCache[hash] = api;
235
+ this._apiAtCache.set(hash, api);
229
236
  return api;
230
237
  }
231
238
  getStorageQuery() {
@@ -46,7 +46,6 @@ export class LegacyClient// prettier-end-here
46
46
  extends BaseSubstrateClient {
47
47
  #runtimeSubscriptionUnsub;
48
48
  #healthTimer;
49
- #apiAtCache = {};
50
49
  /**
51
50
  * Use factory methods (`create`, `new`) to create `Dedot` instances.
52
51
  *
@@ -93,7 +92,6 @@ export class LegacyClient// prettier-end-here
93
92
  }
94
93
  cleanUp() {
95
94
  super.cleanUp();
96
- this.#apiAtCache = {};
97
95
  this.#healthTimer = undefined;
98
96
  this.#runtimeSubscriptionUnsub = undefined;
99
97
  }
@@ -229,8 +227,9 @@ export class LegacyClient// prettier-end-here
229
227
  * @param hash
230
228
  */
231
229
  async at(hash) {
232
- if (this.#apiAtCache[hash])
233
- return this.#apiAtCache[hash];
230
+ const cached = this._apiAtCache.get(hash);
231
+ if (cached)
232
+ return cached;
234
233
  const targetVersion = await this.#getRuntimeVersion(hash);
235
234
  let metadata = this.metadata;
236
235
  let registry = this.registry;
@@ -253,7 +252,7 @@ export class LegacyClient// prettier-end-here
253
252
  api.call = newProxyChain({ executor: new RuntimeApiExecutor(api) });
254
253
  api.events = newProxyChain({ executor: new EventExecutor(api) });
255
254
  api.errors = newProxyChain({ executor: new ErrorExecutor(api) });
256
- this.#apiAtCache[hash] = api;
255
+ this._apiAtCache.set(hash, api);
257
256
  return api;
258
257
  }
259
258
  getStorageQuery() {
@@ -1,5 +1,7 @@
1
- import { DedotError } from '@dedot/utils';
1
+ import { DedotError, LRUCache } from '@dedot/utils';
2
2
  import { JsonRpcGroup } from './JsonRpcGroup.js';
3
+ const ARCHIVE_CACHE_CAPACITY = 256;
4
+ const ARCHIVE_CACHE_TTL = 60_000; // 1 minutes - archive data is immutable
3
5
  /**
4
6
  * @name Archive
5
7
  * Archive JSON-RPC methods for accessing historical blockchain data.
@@ -13,7 +15,7 @@ export class Archive extends JsonRpcGroup {
13
15
  #cache;
14
16
  constructor(client, options) {
15
17
  super(client, { prefix: 'archive', supportedVersions: ['unstable', 'v1'], ...options });
16
- this.#cache = new Map();
18
+ this.#cache = new LRUCache(ARCHIVE_CACHE_CAPACITY, ARCHIVE_CACHE_TTL);
17
19
  }
18
20
  /**
19
21
  * Retrieves the body (list of transactions) of a given block.
@@ -35,8 +37,9 @@ export class Archive extends JsonRpcGroup {
35
37
  async body(hash) {
36
38
  const blockHash = hash || (await this.finalizedHash());
37
39
  const cacheKey = `${blockHash}::body`;
38
- if (this.#cache.has(cacheKey)) {
39
- return this.#cache.get(cacheKey);
40
+ const cached = this.#cache.get(cacheKey);
41
+ if (cached !== null) {
42
+ return cached;
40
43
  }
41
44
  const result = await this.send('body', blockHash);
42
45
  this.#cache.set(cacheKey, result);
@@ -74,8 +77,9 @@ export class Archive extends JsonRpcGroup {
74
77
  async header(hash) {
75
78
  const blockHash = hash || (await this.finalizedHash());
76
79
  const cacheKey = `${blockHash}::header`;
77
- if (this.#cache.has(cacheKey)) {
78
- return this.#cache.get(cacheKey);
80
+ const cached = this.#cache.get(cacheKey);
81
+ if (cached !== null) {
82
+ return cached;
79
83
  }
80
84
  const result = await this.send('header', blockHash);
81
85
  this.#cache.set(cacheKey, result);
@@ -138,8 +142,9 @@ export class Archive extends JsonRpcGroup {
138
142
  async call(func, params, hash) {
139
143
  const blockHash = hash || (await this.finalizedHash());
140
144
  const cacheKey = `${blockHash}::call::${func}::${params}`;
141
- if (this.#cache.has(cacheKey)) {
142
- return this.#cache.get(cacheKey);
145
+ const cached = this.#cache.get(cacheKey);
146
+ if (cached !== null) {
147
+ return cached;
143
148
  }
144
149
  const result = await this.send('call', blockHash, func, params);
145
150
  if (!result.success) {
@@ -186,8 +191,9 @@ export class Archive extends JsonRpcGroup {
186
191
  // Generate cache key
187
192
  const cacheKey = `${blockHash}::storage::${JSON.stringify(items)}::${childTrie ?? null}`;
188
193
  // Check cache
189
- if (this.#cache.has(cacheKey)) {
190
- return resolve(this.#cache.get(cacheKey));
194
+ const cached = this.#cache.get(cacheKey);
195
+ if (cached !== null) {
196
+ return resolve(cached);
191
197
  }
192
198
  this.#storageSubscription(items, childTrie || null, (event) => {
193
199
  switch (event.event) {
@@ -77,4 +77,15 @@ export declare class ChainHead extends JsonRpcGroup<ChainHeadEvent> {
77
77
  * @protected
78
78
  */
79
79
  protected unpin(hashes: BlockHash | BlockHash[]): Promise<void>;
80
+ /**
81
+ * Clears the internal cache used for storing query results (both chainHead & archive instances)
82
+ * This can be useful for memory management or when you want to force fresh data retrieval.
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * // Clear all cached results
87
+ * chainHead.clearCache();
88
+ * ```
89
+ */
90
+ clearCache(): void;
80
91
  }
@@ -1,9 +1,11 @@
1
1
  import { $Header } from '@dedot/codecs';
2
- import { assert, AsyncQueue, deferred, ensurePresence, noop, ThrottleQueue, waitFor, } from '@dedot/utils';
2
+ import { assert, AsyncQueue, deferred, ensurePresence, noop, ThrottleQueue, waitFor, LRUCache, } from '@dedot/utils';
3
3
  import { JsonRpcGroup } from '../JsonRpcGroup.js';
4
4
  import { BlockUsage } from './BlockUsage.js';
5
5
  import { ChainHeadBlockNotPinnedError, ChainHeadBlockPrunedError, ChainHeadError, ChainHeadLimitReachedError, ChainHeadOperationError, ChainHeadOperationInaccessibleError, ChainHeadStopError, RetryStrategy, } from './error.js';
6
6
  export const MIN_FINALIZED_QUEUE_SIZE = 10; // finalized queue size
7
+ const CHAINHEAD_CACHE_CAPACITY = 256;
8
+ const CHAINHEAD_CACHE_TTL = 30_000; // 30 seconds
7
9
  export class ChainHead extends JsonRpcGroup {
8
10
  #unsub;
9
11
  #subscriptionId;
@@ -40,7 +42,7 @@ export class ChainHead extends JsonRpcGroup {
40
42
  this.#followResponseQueue = new AsyncQueue();
41
43
  this.#retryQueue = new AsyncQueue();
42
44
  this.#blockUsage = new BlockUsage();
43
- this.#cache = new Map();
45
+ this.#cache = new LRUCache(CHAINHEAD_CACHE_CAPACITY, CHAINHEAD_CACHE_TTL);
44
46
  // This helps us to not accidentally putting too much stress on the JSON-RPC server, especially smoldot/light-client
45
47
  this.#operationQueue = new ThrottleQueue(this.#__unsafe__isSmoldot() ? 25 : 250);
46
48
  }
@@ -242,8 +244,9 @@ export class ChainHead extends JsonRpcGroup {
242
244
  if (!this.isPinned(hash))
243
245
  return;
244
246
  delete this.#pinnedBlocks[hash];
245
- // clear cache
246
- Array.from(this.#cache.keys())
247
+ // Clear cache entries related to the pruned block
248
+ // Filter and remove only cache entries for this specific block
249
+ this.#cache.keys()
247
250
  .filter((key) => key.startsWith(`${hash}::`))
248
251
  .forEach((key) => this.#cache.delete(key));
249
252
  });
@@ -391,7 +394,6 @@ export class ChainHead extends JsonRpcGroup {
391
394
  catch (error) {
392
395
  if (error instanceof ChainHeadBlockNotPinnedError && this.#archive) {
393
396
  const errorHash = error.hash;
394
- console.warn(`Block ${errorHash} not pinned in ChainHead, falling back to Archive`);
395
397
  return await fallback(this.#archive, errorHash);
396
398
  }
397
399
  throw error;
@@ -451,7 +453,7 @@ export class ChainHead extends JsonRpcGroup {
451
453
  this.#followResponseQueue.clear();
452
454
  this.#retryQueue.clear();
453
455
  this.#blockUsage.clear();
454
- this.#cache.clear();
456
+ this.clearCache();
455
457
  this.#operationQueue.cancel();
456
458
  }
457
459
  async #ensureFollowed() {
@@ -529,8 +531,9 @@ export class ChainHead extends JsonRpcGroup {
529
531
  const operation = async () => {
530
532
  const atHash = this.#ensurePinnedHash(at);
531
533
  const cacheKey = `${atHash}::body`;
532
- if (this.#cache.has(cacheKey)) {
533
- return this.#cache.get(cacheKey);
534
+ const cached = this.#cache.get(cacheKey);
535
+ if (cached) {
536
+ return cached;
534
537
  }
535
538
  const bodyOperation = async () => {
536
539
  await this.#ensureFollowed();
@@ -568,8 +571,9 @@ export class ChainHead extends JsonRpcGroup {
568
571
  const operation = async () => {
569
572
  const atHash = this.#ensurePinnedHash(at);
570
573
  const cacheKey = `${atHash}::call::${func}::${params}`;
571
- if (this.#cache.has(cacheKey)) {
572
- return this.#cache.get(cacheKey);
574
+ const cached = this.#cache.get(cacheKey);
575
+ if (cached) {
576
+ return cached;
573
577
  }
574
578
  const callOperation = async () => {
575
579
  await this.#ensureFollowed();
@@ -600,8 +604,9 @@ export class ChainHead extends JsonRpcGroup {
600
604
  const operation = async () => {
601
605
  const hash = this.#ensurePinnedHash(at);
602
606
  const cacheKey = `${hash}::header`;
603
- if (this.#cache.has(cacheKey)) {
604
- return this.#cache.get(cacheKey);
607
+ const cached = this.#cache.get(cacheKey);
608
+ if (cached) {
609
+ return cached;
605
610
  }
606
611
  const resp = await this.#getHeader(hash);
607
612
  this.#cache.set(cacheKey, resp);
@@ -624,8 +629,9 @@ export class ChainHead extends JsonRpcGroup {
624
629
  try {
625
630
  // JSON.stringify(items) might get big, we probably should do a twox hashing in such case
626
631
  const cacheKey = `${hash}::storage::${JSON.stringify(items)}::${childTrie ?? null}`;
627
- if (this.#cache.has(cacheKey)) {
628
- return this.#cache.get(cacheKey);
632
+ const cached = this.#cache.get(cacheKey);
633
+ if (cached) {
634
+ return cached;
629
635
  }
630
636
  this.#blockUsage.use(hash);
631
637
  let results = [];
@@ -710,4 +716,18 @@ export class ChainHead extends JsonRpcGroup {
710
716
  // @ts-ignore a trick internally to check whether a provider is using smoldot connection
711
717
  return typeof this.client.provider['chain'] === 'function';
712
718
  }
719
+ /**
720
+ * Clears the internal cache used for storing query results (both chainHead & archive instances)
721
+ * This can be useful for memory management or when you want to force fresh data retrieval.
722
+ *
723
+ * @example
724
+ * ```typescript
725
+ * // Clear all cached results
726
+ * chainHead.clearCache();
727
+ * ```
728
+ */
729
+ clearCache() {
730
+ this.#cache.clear();
731
+ this.#archive?.clearCache();
732
+ }
713
733
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dedot/api",
3
- "version": "0.15.3-next.65898ecf.10+65898ecf",
3
+ "version": "0.16.0",
4
4
  "description": "A delightful JavaScript/TypeScript client for Polkadot & Substrate",
5
5
  "author": "Thang X. Vu <thang@dedot.dev>",
6
6
  "homepage": "https://dedot.dev",
@@ -13,13 +13,13 @@
13
13
  "type": "module",
14
14
  "sideEffects": false,
15
15
  "dependencies": {
16
- "@dedot/codecs": "0.15.3-next.65898ecf.10+65898ecf",
17
- "@dedot/providers": "0.15.3-next.65898ecf.10+65898ecf",
18
- "@dedot/runtime-specs": "0.15.3-next.65898ecf.10+65898ecf",
19
- "@dedot/shape": "0.15.3-next.65898ecf.10+65898ecf",
20
- "@dedot/storage": "0.15.3-next.65898ecf.10+65898ecf",
21
- "@dedot/types": "0.15.3-next.65898ecf.10+65898ecf",
22
- "@dedot/utils": "0.15.3-next.65898ecf.10+65898ecf"
16
+ "@dedot/codecs": "0.16.0",
17
+ "@dedot/providers": "0.16.0",
18
+ "@dedot/runtime-specs": "0.16.0",
19
+ "@dedot/shape": "0.16.0",
20
+ "@dedot/storage": "0.16.0",
21
+ "@dedot/types": "0.16.0",
22
+ "@dedot/utils": "0.16.0"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json",
@@ -48,7 +48,7 @@
48
48
  "node": ">=18"
49
49
  },
50
50
  "license": "Apache-2.0",
51
- "gitHead": "65898ecf2226162fc6632d3f4f64ad5fe1179643",
51
+ "gitHead": "3e78a8eebb8977de0c5fb956154e5b09c4448f31",
52
52
  "module": "./index.js",
53
53
  "types": "./index.d.ts"
54
54
  }
@@ -1,6 +1,6 @@
1
- import { ISubstrateClient, ISubstrateClientAt } from '@dedot/api/types';
2
1
  import { BlockHash, StorageData, StorageKey } from '@dedot/codecs';
3
2
  import type { Callback, Unsub } from '@dedot/types';
3
+ import { ISubstrateClient, ISubstrateClientAt } from '../types.js';
4
4
  /**
5
5
  * @name BaseStorageQuery
6
6
  * @description
@@ -1,6 +1,6 @@
1
- import { DedotClient } from '@dedot/api/client';
2
1
  import { BlockHash, StorageData, StorageKey } from '@dedot/codecs';
3
2
  import type { Callback, Unsub } from '@dedot/types';
3
+ import { DedotClient } from '../client/DedotClient.js';
4
4
  import { BaseStorageQuery } from './BaseStorageQuery.js';
5
5
  /**
6
6
  * @name NewStorageQuery
package/types.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { ChainHeadEvent } from '@dedot/api/json-rpc';
2
1
  import { BlockHash, Hash, Metadata, PortableRegistry } from '@dedot/codecs';
3
2
  import type { ConnectionStatus, JsonRpcProvider, ProviderEvent } from '@dedot/providers';
4
3
  import type { AnyShape } from '@dedot/shape';
@@ -6,6 +5,7 @@ import type { IStorage } from '@dedot/storage';
6
5
  import type { Callback, GenericStorageQuery, GenericSubstrateApi, InjectedSigner, Query, QueryFnResult, RpcVersion, RuntimeApiName, RuntimeApiSpec, Unsub } from '@dedot/types';
7
6
  import type { HashFn, HexString, IEventEmitter } from '@dedot/utils';
8
7
  import type { AnySignedExtension } from './extrinsic/index.js';
8
+ import type { ChainHeadEvent } from './json-rpc/index.js';
9
9
  export type MetadataKey = `RAW_META/${string}`;
10
10
  export type SubscribeMethod = string;
11
11
  export type UnsubscribeMethod = string;