@dedot/api 0.9.5-next.20891389.0 → 0.9.5-next.889fe7cd.3

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,7 @@ 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.#operationQueue = new utils_1.ThrottleQueue(this.#__unsafe__isSmoldot() ? 30 : 250);
35
37
  }
36
38
  async runtimeVersion() {
37
39
  await this.#ensureFollowed();
@@ -80,11 +82,18 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
80
82
  const defer = (0, utils_1.deferred)();
81
83
  try {
82
84
  this.#unsub && this.#unsub().catch(utils_1.noop); // ensure unfollowed
85
+ let signals = 0;
83
86
  this.#unsub = await this.send('follow', true, (event, subscription) => {
84
87
  this.#followResponseQueue
85
88
  .enqueue(async () => {
86
89
  await this.#onFollowEvent(event, subscription);
87
- if (event.event == 'initialized') {
90
+ if (signals >= 2)
91
+ return;
92
+ signals += 1;
93
+ // Sometime smoldot send a `stop` event right after `initialized`
94
+ // So requests sending between `initialized` and `stop` will be on pruned hashes -> throwing out errors
95
+ // Here we make sure to receive at least the first 2 signals to resolve
96
+ if (signals >= 2) {
88
97
  defer.resolve();
89
98
  }
90
99
  })
@@ -229,7 +238,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
229
238
  .then(() => {
230
239
  // 3. Resolve the recovering promise
231
240
  // This means to continue all pending requests while the chainHead started recovering mode at step 1.
232
- this.#recovering.resolve();
241
+ this.#recovering?.resolve();
233
242
  // 4. Recover stale operations
234
243
  // 4.1. Operations that's going on & waiting to receiving its operationId
235
244
  // will eventually get an `limitedReached` error for using a stale followSubscriptionId
@@ -245,7 +254,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
245
254
  .catch((e) => {
246
255
  console.error(e);
247
256
  // TODO we should retry a few attempts
248
- this.#recovering.reject(new error_js_1.ChainHeadError('Cannot recover from stop event!'));
257
+ this.#recovering?.reject(new error_js_1.ChainHeadError('Cannot recover from stop event!'));
249
258
  Object.values(this.#handlers).forEach(({ defer, operationId }) => {
250
259
  defer.reject(new error_js_1.ChainHeadError('Cannot recover from stop event!'));
251
260
  delete this.#handlers[operationId];
@@ -393,6 +402,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
393
402
  this.#retryQueue.clear();
394
403
  this.#blockUsage.clear();
395
404
  this.#cache.clear();
405
+ this.#operationQueue.cancel();
396
406
  }
397
407
  async #ensureFollowed() {
398
408
  if (this.#recovering) {
@@ -422,17 +432,15 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
422
432
  async #retryOperation(strategy, retry) {
423
433
  try {
424
434
  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
435
+ if (strategy === error_js_1.RetryStrategy.NOW) {
436
+ retry().then(resolve).catch(reject);
437
+ }
438
+ else if (strategy === error_js_1.RetryStrategy.QUEUED) {
439
+ this.#retryQueue.enqueue(retry).then(resolve).catch(reject);
440
+ }
441
+ else {
442
+ throw new Error('Invalid retry strategy');
443
+ }
436
444
  });
437
445
  }
438
446
  catch (e) {
@@ -480,7 +488,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
480
488
  const resp = await this.send('body', this.#subscriptionId, hash);
481
489
  return this.#awaitOperation(resp, hash);
482
490
  };
483
- const resp = await this.#performOperationWithRetry(operation, atHash);
491
+ const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(operation, atHash));
484
492
  this.#cache.set(cacheKey, resp);
485
493
  return resp;
486
494
  }
@@ -509,7 +517,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
509
517
  const resp = await this.send('call', this.#subscriptionId, hash, func, params);
510
518
  return this.#awaitOperation(resp, hash);
511
519
  };
512
- const resp = await this.#performOperationWithRetry(operation, atHash);
520
+ const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(operation, atHash));
513
521
  this.#cache.set(cacheKey, resp);
514
522
  return resp;
515
523
  }
@@ -552,9 +560,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
552
560
  }
553
561
  this.#blockUsage.use(hash);
554
562
  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) {
563
+ if (this.#__unsafe__isSmoldot()) {
558
564
  const fetchItem = async (item) => {
559
565
  const [batch, newDiscardedItems] = await this.#getStorage([item], childTrie ?? null, hash);
560
566
  if (newDiscardedItems.length > 0) {
@@ -587,7 +593,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
587
593
  }
588
594
  async #getStorage(items, childTrie, at) {
589
595
  const operation = () => this.#getStorageOperation(items, childTrie, this.#ensurePinnedHash(at));
590
- return this.#performOperationWithRetry(operation, at);
596
+ return this.#operationQueue.add(() => this.#performOperationWithRetry(operation, at));
591
597
  }
592
598
  async #getStorageOperation(items, childTrie, at) {
593
599
  await this.#ensureFollowed();
@@ -624,5 +630,9 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
624
630
  return;
625
631
  await this.send('unpin', this.#subscriptionId, hashes);
626
632
  }
633
+ #__unsafe__isSmoldot() {
634
+ // @ts-ignore a trick internally to check whether a provider is using smoldot connection
635
+ return typeof this.client.provider['chain'] === 'function';
636
+ }
627
637
  }
628
638
  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,7 @@ 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.#operationQueue = new ThrottleQueue(this.#__unsafe__isSmoldot() ? 30 : 250);
32
34
  }
33
35
  async runtimeVersion() {
34
36
  await this.#ensureFollowed();
@@ -77,11 +79,18 @@ export class ChainHead extends JsonRpcGroup {
77
79
  const defer = deferred();
78
80
  try {
79
81
  this.#unsub && this.#unsub().catch(noop); // ensure unfollowed
82
+ let signals = 0;
80
83
  this.#unsub = await this.send('follow', true, (event, subscription) => {
81
84
  this.#followResponseQueue
82
85
  .enqueue(async () => {
83
86
  await this.#onFollowEvent(event, subscription);
84
- if (event.event == 'initialized') {
87
+ if (signals >= 2)
88
+ return;
89
+ signals += 1;
90
+ // Sometime smoldot send a `stop` event right after `initialized`
91
+ // So requests sending between `initialized` and `stop` will be on pruned hashes -> throwing out errors
92
+ // Here we make sure to receive at least the first 2 signals to resolve
93
+ if (signals >= 2) {
85
94
  defer.resolve();
86
95
  }
87
96
  })
@@ -226,7 +235,7 @@ export class ChainHead extends JsonRpcGroup {
226
235
  .then(() => {
227
236
  // 3. Resolve the recovering promise
228
237
  // This means to continue all pending requests while the chainHead started recovering mode at step 1.
229
- this.#recovering.resolve();
238
+ this.#recovering?.resolve();
230
239
  // 4. Recover stale operations
231
240
  // 4.1. Operations that's going on & waiting to receiving its operationId
232
241
  // will eventually get an `limitedReached` error for using a stale followSubscriptionId
@@ -242,7 +251,7 @@ export class ChainHead extends JsonRpcGroup {
242
251
  .catch((e) => {
243
252
  console.error(e);
244
253
  // TODO we should retry a few attempts
245
- this.#recovering.reject(new ChainHeadError('Cannot recover from stop event!'));
254
+ this.#recovering?.reject(new ChainHeadError('Cannot recover from stop event!'));
246
255
  Object.values(this.#handlers).forEach(({ defer, operationId }) => {
247
256
  defer.reject(new ChainHeadError('Cannot recover from stop event!'));
248
257
  delete this.#handlers[operationId];
@@ -390,6 +399,7 @@ export class ChainHead extends JsonRpcGroup {
390
399
  this.#retryQueue.clear();
391
400
  this.#blockUsage.clear();
392
401
  this.#cache.clear();
402
+ this.#operationQueue.cancel();
393
403
  }
394
404
  async #ensureFollowed() {
395
405
  if (this.#recovering) {
@@ -419,17 +429,15 @@ export class ChainHead extends JsonRpcGroup {
419
429
  async #retryOperation(strategy, retry) {
420
430
  try {
421
431
  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
432
+ if (strategy === RetryStrategy.NOW) {
433
+ retry().then(resolve).catch(reject);
434
+ }
435
+ else if (strategy === RetryStrategy.QUEUED) {
436
+ this.#retryQueue.enqueue(retry).then(resolve).catch(reject);
437
+ }
438
+ else {
439
+ throw new Error('Invalid retry strategy');
440
+ }
433
441
  });
434
442
  }
435
443
  catch (e) {
@@ -477,7 +485,7 @@ export class ChainHead extends JsonRpcGroup {
477
485
  const resp = await this.send('body', this.#subscriptionId, hash);
478
486
  return this.#awaitOperation(resp, hash);
479
487
  };
480
- const resp = await this.#performOperationWithRetry(operation, atHash);
488
+ const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(operation, atHash));
481
489
  this.#cache.set(cacheKey, resp);
482
490
  return resp;
483
491
  }
@@ -506,7 +514,7 @@ export class ChainHead extends JsonRpcGroup {
506
514
  const resp = await this.send('call', this.#subscriptionId, hash, func, params);
507
515
  return this.#awaitOperation(resp, hash);
508
516
  };
509
- const resp = await this.#performOperationWithRetry(operation, atHash);
517
+ const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(operation, atHash));
510
518
  this.#cache.set(cacheKey, resp);
511
519
  return resp;
512
520
  }
@@ -549,9 +557,7 @@ export class ChainHead extends JsonRpcGroup {
549
557
  }
550
558
  this.#blockUsage.use(hash);
551
559
  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) {
560
+ if (this.#__unsafe__isSmoldot()) {
555
561
  const fetchItem = async (item) => {
556
562
  const [batch, newDiscardedItems] = await this.#getStorage([item], childTrie ?? null, hash);
557
563
  if (newDiscardedItems.length > 0) {
@@ -584,7 +590,7 @@ export class ChainHead extends JsonRpcGroup {
584
590
  }
585
591
  async #getStorage(items, childTrie, at) {
586
592
  const operation = () => this.#getStorageOperation(items, childTrie, this.#ensurePinnedHash(at));
587
- return this.#performOperationWithRetry(operation, at);
593
+ return this.#operationQueue.add(() => this.#performOperationWithRetry(operation, at));
588
594
  }
589
595
  async #getStorageOperation(items, childTrie, at) {
590
596
  await this.#ensureFollowed();
@@ -621,4 +627,8 @@ export class ChainHead extends JsonRpcGroup {
621
627
  return;
622
628
  await this.send('unpin', this.#subscriptionId, hashes);
623
629
  }
630
+ #__unsafe__isSmoldot() {
631
+ // @ts-ignore a trick internally to check whether a provider is using smoldot connection
632
+ return typeof this.client.provider['chain'] === 'function';
633
+ }
624
634
  }
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-next.889fe7cd.3+889fe7c",
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-next.889fe7cd.3+889fe7c",
17
+ "@dedot/providers": "0.9.5-next.889fe7cd.3+889fe7c",
18
+ "@dedot/runtime-specs": "0.9.5-next.889fe7cd.3+889fe7c",
19
+ "@dedot/shape": "0.9.5-next.889fe7cd.3+889fe7c",
20
+ "@dedot/storage": "0.9.5-next.889fe7cd.3+889fe7c",
21
+ "@dedot/types": "0.9.5-next.889fe7cd.3+889fe7c",
22
+ "@dedot/utils": "0.9.5-next.889fe7cd.3+889fe7c"
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": "889fe7cd88f69202d4634cfe9fd25900b41a8935",
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();