@dedot/api 0.9.5-next.20891389.0 → 0.9.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.
@@ -22,6 +22,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
22
22
  #recovering;
23
23
  #blockUsage;
24
24
  #cache;
25
+ #operationQueue;
25
26
  constructor(client, options) {
26
27
  super(client, { prefix: 'chainHead', supportedVersions: ['unstable', 'v1'], ...options });
27
28
  this.#handlers = {};
@@ -32,6 +33,8 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
32
33
  this.#retryQueue = new utils_1.AsyncQueue();
33
34
  this.#blockUsage = new BlockUsage_js_1.BlockUsage();
34
35
  this.#cache = new Map();
36
+ // This helps us to not accidentally putting too much stress on the JSON-RPC server, especially smoldot/light-client
37
+ this.#operationQueue = new utils_1.ThrottleQueue(this.#__unsafe__isSmoldot() ? 25 : 250);
35
38
  }
36
39
  async runtimeVersion() {
37
40
  await this.#ensureFollowed();
@@ -80,11 +83,18 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
80
83
  const defer = (0, utils_1.deferred)();
81
84
  try {
82
85
  this.#unsub && this.#unsub().catch(utils_1.noop); // ensure unfollowed
86
+ let signals = 0;
83
87
  this.#unsub = await this.send('follow', true, (event, subscription) => {
84
88
  this.#followResponseQueue
85
89
  .enqueue(async () => {
86
90
  await this.#onFollowEvent(event, subscription);
87
- if (event.event == 'initialized') {
91
+ if (signals >= 2)
92
+ return;
93
+ signals += 1;
94
+ // Sometime smoldot send a `stop` event right after `initialized`
95
+ // So requests sending between `initialized` and `stop` will be on pruned hashes -> throwing out errors
96
+ // Here we make sure to receive at least the first 2 signals to resolve
97
+ if (signals >= 2) {
88
98
  defer.resolve();
89
99
  }
90
100
  })
@@ -229,7 +239,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
229
239
  .then(() => {
230
240
  // 3. Resolve the recovering promise
231
241
  // This means to continue all pending requests while the chainHead started recovering mode at step 1.
232
- this.#recovering.resolve();
242
+ this.#recovering?.resolve();
233
243
  // 4. Recover stale operations
234
244
  // 4.1. Operations that's going on & waiting to receiving its operationId
235
245
  // will eventually get an `limitedReached` error for using a stale followSubscriptionId
@@ -245,7 +255,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
245
255
  .catch((e) => {
246
256
  console.error(e);
247
257
  // TODO we should retry a few attempts
248
- this.#recovering.reject(new error_js_1.ChainHeadError('Cannot recover from stop event!'));
258
+ this.#recovering?.reject(new error_js_1.ChainHeadError('Cannot recover from stop event!'));
249
259
  Object.values(this.#handlers).forEach(({ defer, operationId }) => {
250
260
  defer.reject(new error_js_1.ChainHeadError('Cannot recover from stop event!'));
251
261
  delete this.#handlers[operationId];
@@ -393,6 +403,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
393
403
  this.#retryQueue.clear();
394
404
  this.#blockUsage.clear();
395
405
  this.#cache.clear();
406
+ this.#operationQueue.cancel();
396
407
  }
397
408
  async #ensureFollowed() {
398
409
  if (this.#recovering) {
@@ -422,17 +433,15 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
422
433
  async #retryOperation(strategy, retry) {
423
434
  try {
424
435
  return await new Promise((resolve, reject) => {
425
- setTimeout(() => {
426
- if (strategy === error_js_1.RetryStrategy.NOW) {
427
- retry().then(resolve).catch(reject);
428
- }
429
- else if (strategy === error_js_1.RetryStrategy.QUEUED) {
430
- this.#retryQueue.enqueue(retry).then(resolve).catch(reject);
431
- }
432
- else {
433
- throw new Error('Invalid retry strategy');
434
- }
435
- }); // retry again in the next tick
436
+ if (strategy === error_js_1.RetryStrategy.NOW) {
437
+ retry().then(resolve).catch(reject);
438
+ }
439
+ else if (strategy === error_js_1.RetryStrategy.QUEUED) {
440
+ this.#retryQueue.enqueue(retry).then(resolve).catch(reject);
441
+ }
442
+ else {
443
+ throw new Error('Invalid retry strategy');
444
+ }
436
445
  });
437
446
  }
438
447
  catch (e) {
@@ -480,7 +489,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
480
489
  const resp = await this.send('body', this.#subscriptionId, hash);
481
490
  return this.#awaitOperation(resp, hash);
482
491
  };
483
- const resp = await this.#performOperationWithRetry(operation, atHash);
492
+ const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(operation, atHash));
484
493
  this.#cache.set(cacheKey, resp);
485
494
  return resp;
486
495
  }
@@ -509,7 +518,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
509
518
  const resp = await this.send('call', this.#subscriptionId, hash, func, params);
510
519
  return this.#awaitOperation(resp, hash);
511
520
  };
512
- const resp = await this.#performOperationWithRetry(operation, atHash);
521
+ const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(operation, atHash));
513
522
  this.#cache.set(cacheKey, resp);
514
523
  return resp;
515
524
  }
@@ -552,9 +561,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
552
561
  }
553
562
  this.#blockUsage.use(hash);
554
563
  let results = [];
555
- // @ts-ignore a trick internally to check whether a provider is using smoldot connection
556
- const isSmoldot = typeof this.client.provider['chain'] === 'function';
557
- if (isSmoldot) {
564
+ if (this.#__unsafe__isSmoldot()) {
558
565
  const fetchItem = async (item) => {
559
566
  const [batch, newDiscardedItems] = await this.#getStorage([item], childTrie ?? null, hash);
560
567
  if (newDiscardedItems.length > 0) {
@@ -587,7 +594,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
587
594
  }
588
595
  async #getStorage(items, childTrie, at) {
589
596
  const operation = () => this.#getStorageOperation(items, childTrie, this.#ensurePinnedHash(at));
590
- return this.#performOperationWithRetry(operation, at);
597
+ return this.#operationQueue.add(() => this.#performOperationWithRetry(operation, at));
591
598
  }
592
599
  async #getStorageOperation(items, childTrie, at) {
593
600
  await this.#ensureFollowed();
@@ -624,5 +631,9 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
624
631
  return;
625
632
  await this.send('unpin', this.#subscriptionId, hashes);
626
633
  }
634
+ #__unsafe__isSmoldot() {
635
+ // @ts-ignore a trick internally to check whether a provider is using smoldot connection
636
+ return typeof this.client.provider['chain'] === 'function';
637
+ }
627
638
  }
628
639
  exports.ChainHead = ChainHead;
@@ -83,7 +83,13 @@ class NewStorageQuery extends BaseStorageQuery_js_1.BaseStorageQuery {
83
83
  await pull(best);
84
84
  // Subscribe to best block events
85
85
  const unsub = this.client.on('bestBlock', (block) => {
86
- pullQueue.enqueue(() => pull(block)).catch(console.error);
86
+ // Here we're handling each pull one by one,
87
+ // If the queue get too long, it might take a long time for us to get the fresh & latest data
88
+ // This is a precaution in such case, if the queue size >= 3 we skip all the pending pull and jump to the latest pull
89
+ if (pullQueue.size >= 3)
90
+ pullQueue.clear();
91
+ // TODO timing out for a pull to prevent it took too long to fetch
92
+ pullQueue.enqueue(() => pull(block)).catch(utils_1.noop);
87
93
  });
88
94
  return async () => {
89
95
  unsub();
@@ -1,5 +1,5 @@
1
1
  import { $Header } from '@dedot/codecs';
2
- import { assert, AsyncQueue, deferred, ensurePresence, noop, waitFor } from '@dedot/utils';
2
+ import { assert, AsyncQueue, deferred, ensurePresence, noop, ThrottleQueue, waitFor, } 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';
@@ -19,6 +19,7 @@ export class ChainHead extends JsonRpcGroup {
19
19
  #recovering;
20
20
  #blockUsage;
21
21
  #cache;
22
+ #operationQueue;
22
23
  constructor(client, options) {
23
24
  super(client, { prefix: 'chainHead', supportedVersions: ['unstable', 'v1'], ...options });
24
25
  this.#handlers = {};
@@ -29,6 +30,8 @@ export class ChainHead extends JsonRpcGroup {
29
30
  this.#retryQueue = new AsyncQueue();
30
31
  this.#blockUsage = new BlockUsage();
31
32
  this.#cache = new Map();
33
+ // This helps us to not accidentally putting too much stress on the JSON-RPC server, especially smoldot/light-client
34
+ this.#operationQueue = new ThrottleQueue(this.#__unsafe__isSmoldot() ? 25 : 250);
32
35
  }
33
36
  async runtimeVersion() {
34
37
  await this.#ensureFollowed();
@@ -77,11 +80,18 @@ export class ChainHead extends JsonRpcGroup {
77
80
  const defer = deferred();
78
81
  try {
79
82
  this.#unsub && this.#unsub().catch(noop); // ensure unfollowed
83
+ let signals = 0;
80
84
  this.#unsub = await this.send('follow', true, (event, subscription) => {
81
85
  this.#followResponseQueue
82
86
  .enqueue(async () => {
83
87
  await this.#onFollowEvent(event, subscription);
84
- if (event.event == 'initialized') {
88
+ if (signals >= 2)
89
+ return;
90
+ signals += 1;
91
+ // Sometime smoldot send a `stop` event right after `initialized`
92
+ // So requests sending between `initialized` and `stop` will be on pruned hashes -> throwing out errors
93
+ // Here we make sure to receive at least the first 2 signals to resolve
94
+ if (signals >= 2) {
85
95
  defer.resolve();
86
96
  }
87
97
  })
@@ -226,7 +236,7 @@ export class ChainHead extends JsonRpcGroup {
226
236
  .then(() => {
227
237
  // 3. Resolve the recovering promise
228
238
  // This means to continue all pending requests while the chainHead started recovering mode at step 1.
229
- this.#recovering.resolve();
239
+ this.#recovering?.resolve();
230
240
  // 4. Recover stale operations
231
241
  // 4.1. Operations that's going on & waiting to receiving its operationId
232
242
  // will eventually get an `limitedReached` error for using a stale followSubscriptionId
@@ -242,7 +252,7 @@ export class ChainHead extends JsonRpcGroup {
242
252
  .catch((e) => {
243
253
  console.error(e);
244
254
  // TODO we should retry a few attempts
245
- this.#recovering.reject(new ChainHeadError('Cannot recover from stop event!'));
255
+ this.#recovering?.reject(new ChainHeadError('Cannot recover from stop event!'));
246
256
  Object.values(this.#handlers).forEach(({ defer, operationId }) => {
247
257
  defer.reject(new ChainHeadError('Cannot recover from stop event!'));
248
258
  delete this.#handlers[operationId];
@@ -390,6 +400,7 @@ export class ChainHead extends JsonRpcGroup {
390
400
  this.#retryQueue.clear();
391
401
  this.#blockUsage.clear();
392
402
  this.#cache.clear();
403
+ this.#operationQueue.cancel();
393
404
  }
394
405
  async #ensureFollowed() {
395
406
  if (this.#recovering) {
@@ -419,17 +430,15 @@ export class ChainHead extends JsonRpcGroup {
419
430
  async #retryOperation(strategy, retry) {
420
431
  try {
421
432
  return await new Promise((resolve, reject) => {
422
- setTimeout(() => {
423
- if (strategy === RetryStrategy.NOW) {
424
- retry().then(resolve).catch(reject);
425
- }
426
- else if (strategy === RetryStrategy.QUEUED) {
427
- this.#retryQueue.enqueue(retry).then(resolve).catch(reject);
428
- }
429
- else {
430
- throw new Error('Invalid retry strategy');
431
- }
432
- }); // retry again in the next tick
433
+ if (strategy === RetryStrategy.NOW) {
434
+ retry().then(resolve).catch(reject);
435
+ }
436
+ else if (strategy === RetryStrategy.QUEUED) {
437
+ this.#retryQueue.enqueue(retry).then(resolve).catch(reject);
438
+ }
439
+ else {
440
+ throw new Error('Invalid retry strategy');
441
+ }
433
442
  });
434
443
  }
435
444
  catch (e) {
@@ -477,7 +486,7 @@ export class ChainHead extends JsonRpcGroup {
477
486
  const resp = await this.send('body', this.#subscriptionId, hash);
478
487
  return this.#awaitOperation(resp, hash);
479
488
  };
480
- const resp = await this.#performOperationWithRetry(operation, atHash);
489
+ const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(operation, atHash));
481
490
  this.#cache.set(cacheKey, resp);
482
491
  return resp;
483
492
  }
@@ -506,7 +515,7 @@ export class ChainHead extends JsonRpcGroup {
506
515
  const resp = await this.send('call', this.#subscriptionId, hash, func, params);
507
516
  return this.#awaitOperation(resp, hash);
508
517
  };
509
- const resp = await this.#performOperationWithRetry(operation, atHash);
518
+ const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(operation, atHash));
510
519
  this.#cache.set(cacheKey, resp);
511
520
  return resp;
512
521
  }
@@ -549,9 +558,7 @@ export class ChainHead extends JsonRpcGroup {
549
558
  }
550
559
  this.#blockUsage.use(hash);
551
560
  let results = [];
552
- // @ts-ignore a trick internally to check whether a provider is using smoldot connection
553
- const isSmoldot = typeof this.client.provider['chain'] === 'function';
554
- if (isSmoldot) {
561
+ if (this.#__unsafe__isSmoldot()) {
555
562
  const fetchItem = async (item) => {
556
563
  const [batch, newDiscardedItems] = await this.#getStorage([item], childTrie ?? null, hash);
557
564
  if (newDiscardedItems.length > 0) {
@@ -584,7 +591,7 @@ export class ChainHead extends JsonRpcGroup {
584
591
  }
585
592
  async #getStorage(items, childTrie, at) {
586
593
  const operation = () => this.#getStorageOperation(items, childTrie, this.#ensurePinnedHash(at));
587
- return this.#performOperationWithRetry(operation, at);
594
+ return this.#operationQueue.add(() => this.#performOperationWithRetry(operation, at));
588
595
  }
589
596
  async #getStorageOperation(items, childTrie, at) {
590
597
  await this.#ensureFollowed();
@@ -621,4 +628,8 @@ export class ChainHead extends JsonRpcGroup {
621
628
  return;
622
629
  await this.send('unpin', this.#subscriptionId, hashes);
623
630
  }
631
+ #__unsafe__isSmoldot() {
632
+ // @ts-ignore a trick internally to check whether a provider is using smoldot connection
633
+ return typeof this.client.provider['chain'] === 'function';
634
+ }
624
635
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dedot/api",
3
- "version": "0.9.5-next.20891389.0+2089138",
3
+ "version": "0.9.5",
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.9.5-next.20891389.0+2089138",
17
- "@dedot/providers": "0.9.5-next.20891389.0+2089138",
18
- "@dedot/runtime-specs": "0.9.5-next.20891389.0+2089138",
19
- "@dedot/shape": "0.9.5-next.20891389.0+2089138",
20
- "@dedot/storage": "0.9.5-next.20891389.0+2089138",
21
- "@dedot/types": "0.9.5-next.20891389.0+2089138",
22
- "@dedot/utils": "0.9.5-next.20891389.0+2089138"
16
+ "@dedot/codecs": "0.9.5",
17
+ "@dedot/providers": "0.9.5",
18
+ "@dedot/runtime-specs": "0.9.5",
19
+ "@dedot/shape": "0.9.5",
20
+ "@dedot/storage": "0.9.5",
21
+ "@dedot/types": "0.9.5",
22
+ "@dedot/utils": "0.9.5"
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": "208913895242cc9c16cb8bb9bcb880393cb61e6b",
51
+ "gitHead": "18f4c23ad45330f9de1b3be70d4ce7581eaccb20",
52
52
  "module": "./index.js",
53
53
  "types": "./index.d.ts"
54
54
  }
@@ -1,4 +1,4 @@
1
- import { AsyncQueue } from '@dedot/utils';
1
+ import { AsyncQueue, noop } from '@dedot/utils';
2
2
  import { BaseStorageQuery } from './BaseStorageQuery.js';
3
3
  /**
4
4
  * @name NewStorageQuery
@@ -80,7 +80,13 @@ export class NewStorageQuery extends BaseStorageQuery {
80
80
  await pull(best);
81
81
  // Subscribe to best block events
82
82
  const unsub = this.client.on('bestBlock', (block) => {
83
- pullQueue.enqueue(() => pull(block)).catch(console.error);
83
+ // Here we're handling each pull one by one,
84
+ // If the queue get too long, it might take a long time for us to get the fresh & latest data
85
+ // This is a precaution in such case, if the queue size >= 3 we skip all the pending pull and jump to the latest pull
86
+ if (pullQueue.size >= 3)
87
+ pullQueue.clear();
88
+ // TODO timing out for a pull to prevent it took too long to fetch
89
+ pullQueue.enqueue(() => pull(block)).catch(noop);
84
90
  });
85
91
  return async () => {
86
92
  unsub();