@subql/node-ethereum 2.9.3-2 → 2.9.3-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.
@@ -3,6 +3,10 @@ import { JsonRpcProvider } from '@ethersproject/providers';
3
3
  import { Networkish } from '@ethersproject/networks';
4
4
  import { ConnectionInfo } from './web';
5
5
  export declare class JsonRpcBatchProvider extends JsonRpcProvider {
6
+ private batchSize;
7
+ private successfulBatchCount;
8
+ private failedBatchCount;
9
+ private batchSizeAdjustmentInterval;
6
10
  _pendingBatchAggregator: NodeJS.Timer;
7
11
  _pendingBatch: Array<{
8
12
  request: {
@@ -16,7 +20,9 @@ export declare class JsonRpcBatchProvider extends JsonRpcProvider {
16
20
  }>;
17
21
  _chainIdCache: string | null;
18
22
  constructor(url: string | ConnectionInfo, network?: Networkish);
23
+ setBatchSize(batchSize: number): void;
19
24
  send(method: string, params: Array<any>): Promise<any>;
20
25
  flush(): void;
21
26
  private runRequests;
27
+ private adjustBatchSize;
22
28
  }
@@ -11,8 +11,15 @@ const logger = (0, node_core_1.getLogger)('JsonRpcBatchProvider');
11
11
  class JsonRpcBatchProvider extends providers_1.JsonRpcProvider {
12
12
  constructor(url, network) {
13
13
  super(url, network);
14
+ this.batchSize = 1;
15
+ this.successfulBatchCount = 0;
16
+ this.failedBatchCount = 0;
17
+ this.batchSizeAdjustmentInterval = 10; // Adjust batch size after every 10 batches
14
18
  this._chainIdCache = null;
15
19
  }
20
+ setBatchSize(batchSize) {
21
+ this.batchSize = batchSize;
22
+ }
16
23
  send(method, params) {
17
24
  if (method === 'eth_chainId' && this._chainIdCache !== null) {
18
25
  return Promise.resolve(this._chainIdCache);
@@ -38,7 +45,7 @@ class JsonRpcBatchProvider extends providers_1.JsonRpcProvider {
38
45
  this.runRequests();
39
46
  }, 1);
40
47
  }
41
- if (this._pendingBatch.length > 10) {
48
+ if (this._pendingBatch.length > this.batchSize) {
42
49
  this.flush();
43
50
  }
44
51
  return promise;
@@ -64,6 +71,7 @@ class JsonRpcBatchProvider extends providers_1.JsonRpcProvider {
64
71
  });
65
72
  return (0, web_1.fetchJson)(this.connection, JSON.stringify(request))
66
73
  .then((result) => {
74
+ var _a;
67
75
  this.emit('debug', {
68
76
  action: 'response',
69
77
  request: request,
@@ -79,6 +87,9 @@ class JsonRpcBatchProvider extends providers_1.JsonRpcProvider {
79
87
  batch.forEach((inflightRequest) => {
80
88
  inflightRequest.reject(error);
81
89
  });
90
+ if (((_a = result.error) === null || _a === void 0 ? void 0 : _a.message) === 'Batch size is too large') {
91
+ this.adjustBatchSize(false);
92
+ }
82
93
  return;
83
94
  }
84
95
  // BSC returns a 200 response with this when being rate limited, we need a special case for it
@@ -99,6 +110,7 @@ class JsonRpcBatchProvider extends providers_1.JsonRpcProvider {
99
110
  var _a;
100
111
  inflightRequest.reject(new Error((_a = result[0].error) === null || _a === void 0 ? void 0 : _a.message));
101
112
  });
113
+ this.adjustBatchSize(false);
102
114
  return;
103
115
  }
104
116
  const resultMap = result.reduce((resultMap, payload) => {
@@ -116,6 +128,13 @@ class JsonRpcBatchProvider extends providers_1.JsonRpcProvider {
116
128
  const error = new Error(payload.error.message);
117
129
  error.code = payload.error.code;
118
130
  error.data = payload.error.data;
131
+ if (payload.error.message === 'Batch size limit exceeded' || // onfinality
132
+ payload.error.message === 'exceeded project rate limit' || // infura
133
+ payload.error.message.includes('Failed to buffer the request body') ||
134
+ payload.error.message.includes('Too Many Requests') ||
135
+ payload.error.message.includes('Request Entity Too Large')) {
136
+ this.adjustBatchSize(false);
137
+ }
119
138
  inflightRequest.reject(error);
120
139
  }
121
140
  else {
@@ -125,6 +144,7 @@ class JsonRpcBatchProvider extends providers_1.JsonRpcProvider {
125
144
  inflightRequest.resolve(payload.result);
126
145
  }
127
146
  });
147
+ this.adjustBatchSize(true);
128
148
  })
129
149
  .catch((error) => {
130
150
  this.emit('debug', {
@@ -137,8 +157,26 @@ class JsonRpcBatchProvider extends providers_1.JsonRpcProvider {
137
157
  batch.forEach((inflightRequest) => {
138
158
  inflightRequest.reject(error);
139
159
  });
160
+ //this.adjustBatchSize(false);
140
161
  });
141
162
  }
163
+ adjustBatchSize(success) {
164
+ success ? this.successfulBatchCount++ : this.failedBatchCount++;
165
+ const totalBatches = this.successfulBatchCount + this.failedBatchCount;
166
+ if (totalBatches % this.batchSizeAdjustmentInterval === 0) {
167
+ const successRate = this.successfulBatchCount / totalBatches;
168
+ // Adjust the batch size based on the success rate.
169
+ if (successRate < 0.9 && this.batchSize > 1) {
170
+ this.batchSize--;
171
+ }
172
+ else if (successRate > 0.95 && this.batchSize < 10) {
173
+ this.batchSize++;
174
+ }
175
+ // Reset the counters
176
+ this.successfulBatchCount = 0;
177
+ this.failedBatchCount = 0;
178
+ }
179
+ }
142
180
  }
143
181
  exports.JsonRpcBatchProvider = JsonRpcBatchProvider;
144
182
  //# sourceMappingURL=json-rpc-batch-provider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"json-rpc-batch-provider.js","sourceRoot":"","sources":["../../../src/ethereum/ethers/json-rpc-batch-provider.ts"],"names":[],"mappings":";;;AAAA,oBAAoB;AACpB,0DAAqD;AAErD,wDAA2D;AAE3D,+BAAkD;AAClD,gDAA6C;AAE7C,MAAM,MAAM,GAAG,IAAA,qBAAS,EAAC,sBAAsB,CAAC,CAAC;AAajD,eAAe;AAEf,MAAa,oBAAqB,SAAQ,2BAAe;IAUvD,YAAY,GAA4B,EAAE,OAAoB;QAC5D,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAHtB,kBAAa,GAAkB,IAAI,CAAC;IAIpC,CAAC;IAED,IAAI,CAAC,MAAc,EAAE,MAAkB;QACrC,IAAI,MAAM,KAAK,aAAa,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,EAAE;YAC3D,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;SAC5C;QAED,MAAM,OAAO,GAAG;YACd,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,MAAM;YACd,EAAE,EAAE,IAAI,CAAC,OAAO,EAAE;YAClB,OAAO,EAAE,KAAK;SACf,CAAC;QAEF,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,EAAE;YAC9B,IAAI,CAAC,aAAa,GAAG,EAAE,CAAC;SACzB;QAED,MAAM,eAAe,GAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;QAEtE,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC9C,eAAe,CAAC,OAAO,GAAG,OAAO,CAAC;YAClC,eAAe,CAAC,MAAM,GAAG,MAAM,CAAC;QAClC,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAEzC,IAAI,CAAC,IAAI,CAAC,uBAAuB,EAAE;YACjC,sDAAsD;YACtD,IAAI,CAAC,uBAAuB,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC7C,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,CAAC,EAAE,CAAC,CAAC,CAAC;SACP;QAED,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,GAAG,EAAE,EAAE;YAClC,IAAI,CAAC,KAAK,EAAE,CAAC;SACd;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,uBAAuB,EAAE;YAChC,YAAY,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;YAC3C,IAAI,CAAC,WAAW,EAAE,CAAC;SACpB;IACH,CAAC;IAEO,WAAW;QACjB,sDAAsD;QACtD,yBAAyB;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC;QACjC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,CAAC,uBAAuB,GAAG,IAAI,CAAC;QAEpC,0CAA0C;QAC1C,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAE1D,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;YACjB,MAAM,EAAE,cAAc;YACtB,OAAO,EAAE,IAAA,qBAAQ,EAAC,OAAO,CAAC;YAC1B,QAAQ,EAAE,IAAI;SACf,CAAC,CAAC;QAEH,OAAO,IAAA,eAAS,EAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;aACvD,IAAI,CAAC,CAAC,MAAmB,EAAE,EAAE;YAC5B,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;gBACjB,MAAM,EAAE,UAAU;gBAClB,OAAO,EAAE,OAAO;gBAChB,QAAQ,EAAE,MAAM;gBAChB,QAAQ,EAAE,IAAI;aACf,CAAC,CAAC;YAEH,gCAAgC;YAChC,uBAAuB;YACvB,IAAI;YAEJ,mDAAmD;YACnD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;gBAC1B,MAAM,KAAK,GAAG,IAAI,KAAK,CACrB,qBAAqB,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAC/C,CAAC;gBACF,KAAK,CAAC,OAAO,CAAC,CAAC,eAAe,EAAE,EAAE;oBAChC,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAChC,CAAC,CAAC,CAAC;gBACH,OAAO;aACR;YAED,8FAA8F;YAC9F;;;;;;;;;;;cAWE;YACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,EAAE;gBAChD,KAAK,CAAC,OAAO,CAAC,CAAC,eAAe,EAAE,EAAE;;oBAChC,eAAe,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAA,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,0CAAE,OAAO,CAAC,CAAC,CAAC;gBAC9D,CAAC,CAAC,CAAC;gBACH,OAAO;aACR;YAED,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE;gBACrD,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC;gBAChC,OAAO,SAAS,CAAC;YACnB,CAAC,EAAE,EAA+B,CAAC,CAAC;YAEpC,6DAA6D;YAC7D,uCAAuC;YACvC,KAAK,CAAC,OAAO,CAAC,CAAC,eAAe,EAAE,EAAE;gBAChC,MAAM,OAAO,GAAG,SAAS,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACtD,IAAI,CAAC,OAAO,EAAE;oBACZ,eAAe,CAAC,MAAM,CACpB,IAAI,KAAK,CACP,2CAA2C,eAAe,CAAC,OAAO,CAAC,EAAE,EAAE,CACxE,CACF,CAAC;iBACH;qBAAM,IAAI,OAAO,CAAC,KAAK,EAAE;oBACxB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;oBACzC,KAAM,CAAC,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;oBACjC,KAAM,CAAC,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;oBACvC,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;iBAC/B;qBAAM;oBACL,IAAI,eAAe,CAAC,OAAO,CAAC,MAAM,KAAK,aAAa,EAAE;wBACpD,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;qBACrC;oBACD,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;iBACzC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YACf,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;gBACjB,MAAM,EAAE,UAAU;gBAClB,KAAK,EAAE,KAAK;gBACZ,OAAO,EAAE,OAAO;gBAChB,QAAQ,EAAE,IAAI;aACf,CAAC,CAAC;YAEH,sBAAsB;YAEtB,KAAK,CAAC,OAAO,CAAC,CAAC,eAAe,EAAE,EAAE;gBAChC,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAChC,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;CACF;AAnKD,oDAmKC","sourcesContent":["/* eslint-disable */\nimport { deepCopy } from '@ethersproject/properties';\n\nimport { JsonRpcProvider } from '@ethersproject/providers';\nimport { Networkish } from '@ethersproject/networks';\nimport { ConnectionInfo, fetchJson } from './web';\nimport { getLogger } from '@subql/node-core';\n\nconst logger = getLogger('JsonRpcBatchProvider');\n\ninterface RpcResult {\n jsonrpc: '2.0';\n id: number;\n result?: string;\n error?: {\n code: number;\n message: string;\n data?: any;\n };\n}\n\n// Experimental\n\nexport class JsonRpcBatchProvider extends JsonRpcProvider {\n _pendingBatchAggregator: NodeJS.Timer;\n _pendingBatch: Array<{\n request: { method: string; params: Array<any>; id: number; jsonrpc: '2.0' };\n resolve: (result: any) => void;\n reject: (error: Error) => void;\n }>;\n\n _chainIdCache: string | null = null;\n\n constructor(url: string | ConnectionInfo, network?: Networkish) {\n super(url, network);\n }\n\n send(method: string, params: Array<any>): Promise<any> {\n if (method === 'eth_chainId' && this._chainIdCache !== null) {\n return Promise.resolve(this._chainIdCache);\n }\n\n const request = {\n method: method,\n params: params,\n id: this._nextId++,\n jsonrpc: '2.0',\n };\n\n if (this._pendingBatch == null) {\n this._pendingBatch = [];\n }\n\n const inflightRequest: any = { request, resolve: null, reject: null };\n\n const promise = new Promise((resolve, reject) => {\n inflightRequest.resolve = resolve;\n inflightRequest.reject = reject;\n });\n\n this._pendingBatch.push(inflightRequest);\n\n if (!this._pendingBatchAggregator) {\n // Schedule batch for next event loop + short duration\n this._pendingBatchAggregator = setTimeout(() => {\n this.runRequests();\n }, 1);\n }\n\n if (this._pendingBatch.length > 10) {\n this.flush();\n }\n\n return promise;\n }\n\n flush(): void {\n if (this._pendingBatchAggregator) {\n clearTimeout(this._pendingBatchAggregator);\n this.runRequests();\n }\n }\n\n private runRequests() {\n // Get teh current batch and clear it, so new requests\n // go into the next batch\n const batch = this._pendingBatch;\n this._pendingBatch = null;\n this._pendingBatchAggregator = null;\n\n // Get the request as an array of requests\n const request = batch.map((inflight) => inflight.request);\n\n this.emit('debug', {\n action: 'requestBatch',\n request: deepCopy(request),\n provider: this,\n });\n\n return fetchJson(this.connection, JSON.stringify(request))\n .then((result: RpcResult[]) => {\n this.emit('debug', {\n action: 'response',\n request: request,\n response: result,\n provider: this,\n });\n\n // if (!Array.isArray(result)) {\n // result = [result];\n // }\n\n // https://github.com/ethers-io/ethers.js/pull/2657\n if (!Array.isArray(result)) {\n const error = new Error(\n 'Invalid response \\n' + JSON.stringify(result),\n );\n batch.forEach((inflightRequest) => {\n inflightRequest.reject(error);\n });\n return;\n }\n\n // BSC returns a 200 response with this when being rate limited, we need a special case for it\n /*\n [\n {\n jsonrpc: '2.0',\n id: null,\n error: {\n code: -32005,\n message: 'method eth_getLogs in batch triggered rate limit'\n }\n }\n ]\n */\n if (result.length === 1 && result[0].id === null) {\n batch.forEach((inflightRequest) => {\n inflightRequest.reject(new Error(result[0].error?.message));\n });\n return;\n }\n\n const resultMap = result.reduce((resultMap, payload) => {\n resultMap[payload.id] = payload;\n return resultMap;\n }, {} as Record<number, RpcResult>);\n\n // For each result, feed it to the correct Promise, depending\n // on whether it was a success or error\n batch.forEach((inflightRequest) => {\n const payload = resultMap[inflightRequest.request.id];\n if (!payload) {\n inflightRequest.reject(\n new Error(\n `Missing payload in response for request ${inflightRequest.request.id}`,\n ),\n );\n } else if (payload.error) {\n const error = new Error(payload.error.message);\n (<any>error).code = payload.error.code;\n (<any>error).data = payload.error.data;\n inflightRequest.reject(error);\n } else {\n if (inflightRequest.request.method === 'eth_chainId') {\n this._chainIdCache = payload.result;\n }\n inflightRequest.resolve(payload.result);\n }\n });\n })\n .catch((error) => {\n this.emit('debug', {\n action: 'response',\n error: error,\n request: request,\n provider: this,\n });\n\n //logger.error(error);\n\n batch.forEach((inflightRequest) => {\n inflightRequest.reject(error);\n });\n });\n }\n}\n"]}
1
+ {"version":3,"file":"json-rpc-batch-provider.js","sourceRoot":"","sources":["../../../src/ethereum/ethers/json-rpc-batch-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable */\nimport { deepCopy } from '@ethersproject/properties';\n\nimport { JsonRpcProvider } from '@ethersproject/providers';\nimport { Networkish } from '@ethersproject/networks';\nimport { ConnectionInfo, fetchJson } from './web';\nimport { getLogger } from '@subql/node-core';\n\nconst logger = getLogger('JsonRpcBatchProvider');\n\ninterface RpcResult {\n jsonrpc: '2.0';\n id: number;\n result?: string;\n error?: {\n code: number;\n message: string;\n data?: any;\n };\n}\n\n// Experimental\n\nexport class JsonRpcBatchProvider extends JsonRpcProvider {\n private batchSize = 1;\n private successfulBatchCount = 0;\n private failedBatchCount = 0;\n private batchSizeAdjustmentInterval = 10; // Adjust batch size after every 10 batches\n\n _pendingBatchAggregator: NodeJS.Timer;\n _pendingBatch: Array<{\n request: { method: string; params: Array<any>; id: number; jsonrpc: '2.0' };\n resolve: (result: any) => void;\n reject: (error: Error) => void;\n }>;\n\n _chainIdCache: string | null = null;\n\n constructor(url: string | ConnectionInfo, network?: Networkish) {\n super(url, network);\n }\n\n setBatchSize(batchSize: number) {\n this.batchSize = batchSize;\n }\n\n send(method: string, params: Array<any>): Promise<any> {\n if (method === 'eth_chainId' && this._chainIdCache !== null) {\n return Promise.resolve(this._chainIdCache);\n }\n\n const request = {\n method: method,\n params: params,\n id: this._nextId++,\n jsonrpc: '2.0',\n };\n\n if (this._pendingBatch == null) {\n this._pendingBatch = [];\n }\n\n const inflightRequest: any = { request, resolve: null, reject: null };\n\n const promise = new Promise((resolve, reject) => {\n inflightRequest.resolve = resolve;\n inflightRequest.reject = reject;\n });\n\n this._pendingBatch.push(inflightRequest);\n\n if (!this._pendingBatchAggregator) {\n // Schedule batch for next event loop + short duration\n this._pendingBatchAggregator = setTimeout(() => {\n this.runRequests();\n }, 1);\n }\n\n if (this._pendingBatch.length > this.batchSize) {\n this.flush();\n }\n\n return promise;\n }\n\n flush(): void {\n if (this._pendingBatchAggregator) {\n clearTimeout(this._pendingBatchAggregator);\n this.runRequests();\n }\n }\n\n private runRequests() {\n // Get teh current batch and clear it, so new requests\n // go into the next batch\n const batch = this._pendingBatch;\n this._pendingBatch = null;\n this._pendingBatchAggregator = null;\n\n // Get the request as an array of requests\n const request = batch.map((inflight) => inflight.request);\n\n this.emit('debug', {\n action: 'requestBatch',\n request: deepCopy(request),\n provider: this,\n });\n\n return fetchJson(this.connection, JSON.stringify(request))\n .then((result: RpcResult[]) => {\n this.emit('debug', {\n action: 'response',\n request: request,\n response: result,\n provider: this,\n });\n\n // if (!Array.isArray(result)) {\n // result = [result];\n // }\n\n // https://github.com/ethers-io/ethers.js/pull/2657\n if (!Array.isArray(result)) {\n const error = new Error(\n 'Invalid response \\n' + JSON.stringify(result),\n );\n batch.forEach((inflightRequest) => {\n inflightRequest.reject(error);\n });\n if (\n (result as RpcResult).error?.message === 'Batch size is too large'\n ) {\n this.adjustBatchSize(false);\n }\n return;\n }\n\n // BSC returns a 200 response with this when being rate limited, we need a special case for it\n /*\n [\n {\n jsonrpc: '2.0',\n id: null,\n error: {\n code: -32005,\n message: 'method eth_getLogs in batch triggered rate limit'\n }\n }\n ]\n */\n if (result.length === 1 && result[0].id === null) {\n batch.forEach((inflightRequest) => {\n inflightRequest.reject(new Error(result[0].error?.message));\n });\n this.adjustBatchSize(false);\n return;\n }\n\n const resultMap = result.reduce((resultMap, payload) => {\n resultMap[payload.id] = payload;\n return resultMap;\n }, {} as Record<number, RpcResult>);\n\n // For each result, feed it to the correct Promise, depending\n // on whether it was a success or error\n batch.forEach((inflightRequest) => {\n const payload = resultMap[inflightRequest.request.id];\n if (!payload) {\n inflightRequest.reject(\n new Error(\n `Missing payload in response for request ${inflightRequest.request.id}`,\n ),\n );\n } else if (payload.error) {\n const error = new Error(payload.error.message);\n (<any>error).code = payload.error.code;\n (<any>error).data = payload.error.data;\n if (\n payload.error.message === 'Batch size limit exceeded' || // onfinality\n payload.error.message === 'exceeded project rate limit' || // infura\n payload.error.message.includes(\n 'Failed to buffer the request body',\n ) ||\n payload.error.message.includes('Too Many Requests') ||\n payload.error.message.includes('Request Entity Too Large')\n ) {\n this.adjustBatchSize(false);\n }\n inflightRequest.reject(error);\n } else {\n if (inflightRequest.request.method === 'eth_chainId') {\n this._chainIdCache = payload.result;\n }\n inflightRequest.resolve(payload.result);\n }\n });\n\n this.adjustBatchSize(true);\n })\n .catch((error) => {\n this.emit('debug', {\n action: 'response',\n error: error,\n request: request,\n provider: this,\n });\n\n //logger.error(error);\n\n batch.forEach((inflightRequest) => {\n inflightRequest.reject(error);\n });\n\n //this.adjustBatchSize(false);\n });\n }\n\n private adjustBatchSize(success: boolean) {\n success ? this.successfulBatchCount++ : this.failedBatchCount++;\n const totalBatches = this.successfulBatchCount + this.failedBatchCount;\n\n if (totalBatches % this.batchSizeAdjustmentInterval === 0) {\n const successRate = this.successfulBatchCount / totalBatches;\n\n // Adjust the batch size based on the success rate.\n if (successRate < 0.9 && this.batchSize > 1) {\n this.batchSize--;\n } else if (successRate > 0.95 && this.batchSize < 10) {\n this.batchSize++;\n }\n\n // Reset the counters\n this.successfulBatchCount = 0;\n this.failedBatchCount = 0;\n }\n }\n}\n"]}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /* eslint-disable */
4
+ const json_rpc_batch_provider_1 = require("./json-rpc-batch-provider");
5
+ describe('JsonRpcBatchProvider', () => {
6
+ let batchProvider;
7
+ let fetchJsonMock;
8
+ beforeEach(() => {
9
+ // Create a new instance of the JsonRpcBatchProvider before each test
10
+ batchProvider = new json_rpc_batch_provider_1.JsonRpcBatchProvider('http://localhost:8545');
11
+ // Mock the fetchJson function from the ethers package to simulate server responses
12
+ fetchJsonMock = jest.spyOn(require('./web'), 'fetchJson');
13
+ });
14
+ afterEach(() => {
15
+ // Reset the fetchJson mock after each test
16
+ fetchJsonMock.mockRestore();
17
+ });
18
+ test('adjustBatchSize properly adjusts batch size based on success rate', async () => {
19
+ // Mock fetchJson to return a successful response
20
+ fetchJsonMock.mockImplementation(async (connection, payload) => {
21
+ const requests = JSON.parse(payload);
22
+ return requests.map((request) => ({
23
+ id: request.id,
24
+ jsonrpc: '2.0',
25
+ result: '0x1',
26
+ }));
27
+ });
28
+ // Execute the send method multiple times to simulate successful requests
29
+ const requestCount = 20;
30
+ const promises = [];
31
+ for (let i = 0; i < requestCount; i++) {
32
+ const promise = batchProvider.send('eth_call', []);
33
+ promises.push(promise);
34
+ }
35
+ await Promise.all(promises);
36
+ // Check if the batch size has increased due to the success rate
37
+ expect(batchProvider['batchSize']).toBeGreaterThan(1);
38
+ // Now, mock fetchJson to return an error response
39
+ fetchJsonMock.mockImplementation(async (connection, payload) => {
40
+ const requests = JSON.parse(payload);
41
+ return requests.map((request) => ({
42
+ id: request.id,
43
+ jsonrpc: '2.0',
44
+ error: { code: -32603, message: 'Batch size limit exceeded' },
45
+ }));
46
+ });
47
+ // Execute the send method multiple times to simulate failed requests
48
+ const failedPromises = [];
49
+ for (let i = 0; i < requestCount + 10; i++) {
50
+ const failedPromise = batchProvider.send('eth_call', []);
51
+ failedPromises.push(failedPromise);
52
+ }
53
+ try {
54
+ await Promise.all(failedPromises);
55
+ }
56
+ catch (_a) {
57
+ // ignore error
58
+ }
59
+ // Check if the batch size has decreased due to the failure rate
60
+ expect(batchProvider['batchSize']).toBeLessThan(2);
61
+ });
62
+ });
63
+ //# sourceMappingURL=json-rpc-batch-provider.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-rpc-batch-provider.spec.js","sourceRoot":"","sources":["../../../src/ethereum/ethers/json-rpc-batch-provider.spec.ts"],"names":[],"mappings":";;AAAA,oBAAoB;AACpB,uEAAiE;AAGjE,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,IAAI,aAAmC,CAAC;IACxC,IAAI,aAA+B,CAAC;IAEpC,UAAU,CAAC,GAAG,EAAE;QACd,qEAAqE;QACrE,aAAa,GAAG,IAAI,8CAAoB,CAAC,uBAAuB,CAAC,CAAC;QAElE,mFAAmF;QACnF,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,2CAA2C;QAC3C,aAAa,CAAC,WAAW,EAAE,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;QACnF,iDAAiD;QACjD,aAAa,CAAC,kBAAkB,CAC9B,KAAK,EAAE,UAA0B,EAAE,OAAe,EAAE,EAAE;YACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACrC,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAY,EAAE,EAAE,CAAC,CAAC;gBACrC,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,OAAO,EAAE,KAAK;gBACd,MAAM,EAAE,KAAK;aACd,CAAC,CAAC,CAAC;QACN,CAAC,CACF,CAAC;QAEF,yEAAyE;QACzE,MAAM,YAAY,GAAG,EAAE,CAAC;QACxB,MAAM,QAAQ,GAAG,EAAE,CAAC;QACpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,EAAE,CAAC,EAAE,EAAE;YACrC,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YACnD,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;SACxB;QACD,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAE5B,gEAAgE;QAChE,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAEtD,kDAAkD;QAClD,aAAa,CAAC,kBAAkB,CAC9B,KAAK,EAAE,UAA0B,EAAE,OAAe,EAAE,EAAE;YACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACrC,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAY,EAAE,EAAE,CAAC,CAAC;gBACrC,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,2BAA2B,EAAE;aAC9D,CAAC,CAAC,CAAC;QACN,CAAC,CACF,CAAC;QAEF,qEAAqE;QACrE,MAAM,cAAc,GAAG,EAAE,CAAC;QAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE;YAC1C,MAAM,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YACzD,cAAc,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;SACpC;QAED,IAAI;YACF,MAAM,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;SACnC;QAAC,WAAM;YACN,eAAe;SAChB;QAED,gEAAgE;QAChE,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* eslint-disable */\nimport { JsonRpcBatchProvider } from './json-rpc-batch-provider';\nimport { ConnectionInfo } from './web';\n\ndescribe('JsonRpcBatchProvider', () => {\n let batchProvider: JsonRpcBatchProvider;\n let fetchJsonMock: jest.SpyInstance;\n\n beforeEach(() => {\n // Create a new instance of the JsonRpcBatchProvider before each test\n batchProvider = new JsonRpcBatchProvider('http://localhost:8545');\n\n // Mock the fetchJson function from the ethers package to simulate server responses\n fetchJsonMock = jest.spyOn(require('./web'), 'fetchJson');\n });\n\n afterEach(() => {\n // Reset the fetchJson mock after each test\n fetchJsonMock.mockRestore();\n });\n\n test('adjustBatchSize properly adjusts batch size based on success rate', async () => {\n // Mock fetchJson to return a successful response\n fetchJsonMock.mockImplementation(\n async (connection: ConnectionInfo, payload: string) => {\n const requests = JSON.parse(payload);\n return requests.map((request: any) => ({\n id: request.id,\n jsonrpc: '2.0',\n result: '0x1',\n }));\n },\n );\n\n // Execute the send method multiple times to simulate successful requests\n const requestCount = 20;\n const promises = [];\n for (let i = 0; i < requestCount; i++) {\n const promise = batchProvider.send('eth_call', []);\n promises.push(promise);\n }\n await Promise.all(promises);\n\n // Check if the batch size has increased due to the success rate\n expect(batchProvider['batchSize']).toBeGreaterThan(1);\n\n // Now, mock fetchJson to return an error response\n fetchJsonMock.mockImplementation(\n async (connection: ConnectionInfo, payload: string) => {\n const requests = JSON.parse(payload);\n return requests.map((request: any) => ({\n id: request.id,\n jsonrpc: '2.0',\n error: { code: -32603, message: 'Batch size limit exceeded' },\n }));\n },\n );\n\n // Execute the send method multiple times to simulate failed requests\n const failedPromises = [];\n for (let i = 0; i < requestCount + 10; i++) {\n const failedPromise = batchProvider.send('eth_call', []);\n failedPromises.push(failedPromise);\n }\n\n try {\n await Promise.all(failedPromises);\n } catch {\n // ignore error\n }\n\n // Check if the batch size has decreased due to the failure rate\n expect(batchProvider['batchSize']).toBeLessThan(2);\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@subql/node-ethereum",
3
- "version": "2.9.3-2",
3
+ "version": "2.9.3-3",
4
4
  "description": "",
5
5
  "author": "Ian He",
6
6
  "license": "Apache-2.0",
@@ -67,5 +67,5 @@
67
67
  "/dist",
68
68
  "/bin"
69
69
  ],
70
- "stableVersion": "2.9.3-1"
70
+ "stableVersion": "2.9.3-2"
71
71
  }