@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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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
|
-
|
|
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();
|