Added support to detect and stop providers spinning on intitial network detection (#4015).

This commit is contained in:
Richard Moore 2023-06-06 17:58:06 -04:00
parent 7dcecfa47d
commit f37a52da28
2 changed files with 51 additions and 11 deletions

@ -421,6 +421,8 @@ export class AbstractProvider implements Provider {
// null=unpaused, true=paused+dropWhilePaused, false=paused // null=unpaused, true=paused+dropWhilePaused, false=paused
#pausedState: null | boolean; #pausedState: null | boolean;
#destroyed: boolean;
#networkPromise: null | Promise<Network>; #networkPromise: null | Promise<Network>;
readonly #anyNetwork: boolean; readonly #anyNetwork: boolean;
@ -462,6 +464,8 @@ export class AbstractProvider implements Provider {
this.#plugins = new Map(); this.#plugins = new Map();
this.#pausedState = null; this.#pausedState = null;
this.#destroyed = false;
this.#nextTimer = 1; this.#nextTimer = 1;
this.#timers = new Map(); this.#timers = new Map();
@ -1445,9 +1449,20 @@ export class AbstractProvider implements Provider {
return this.off(event, listener); return this.off(event, listener);
} }
/**
* If this provider has been destroyed using the [[destroy]] method.
*
* Once destroyed, all resources are reclaimed, internal event loops
* and timers are cleaned up and no further requests may be sent to
* the provider.
*/
get destroyed(): boolean {
return this.#destroyed;
}
/** /**
* Sub-classes may use this to shutdown any sockets or release their * Sub-classes may use this to shutdown any sockets or release their
* resources. * resources and reject any pending requests.
* *
* Sub-classes **must** call ``super.destroy()``. * Sub-classes **must** call ``super.destroy()``.
*/ */
@ -1459,6 +1474,8 @@ export class AbstractProvider implements Provider {
for (const timerId of this.#timers.keys()) { for (const timerId of this.#timers.keys()) {
this._clearTimeout(timerId); this._clearTimeout(timerId);
} }
this.#destroyed = true;
} }
/** /**

@ -495,6 +495,11 @@ export abstract class JsonRpcApiProvider extends AbstractProvider {
// Process results in batch order // Process results in batch order
for (const { resolve, reject, payload } of batch) { for (const { resolve, reject, payload } of batch) {
if (this.destroyed) {
reject(makeError("provider destroyed; cancelled request", "UNSUPPORTED_OPERATION", { operation: payload.method }));
continue;
}
// Find the matching result // Find the matching result
const resp = result.filter((r) => (r.id === payload.id))[0]; const resp = result.filter((r) => (r.id === payload.id))[0];
@ -506,7 +511,6 @@ export abstract class JsonRpcApiProvider extends AbstractProvider {
// The response is an error // The response is an error
if ("error" in resp) { if ("error" in resp) {
return reject(this.getRpcError(payload, resp)); return reject(this.getRpcError(payload, resp));
} }
// All good; send the result // All good; send the result
@ -578,13 +582,6 @@ export abstract class JsonRpcApiProvider extends AbstractProvider {
* Sub-classes **MUST** override this. * Sub-classes **MUST** override this.
*/ */
abstract _send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult | JsonRpcError>>; abstract _send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult | JsonRpcError>>;
/*
{
assert(false, "sub-classes must override _send", "UNSUPPORTED_OPERATION", {
operation: "jsonRpcApiProvider._send"
});
}
*/
/** /**
@ -678,11 +675,12 @@ export abstract class JsonRpcApiProvider extends AbstractProvider {
(async () => { (async () => {
// Bootstrap the network // Bootstrap the network
while (this.#network == null) { while (this.#network == null && !this.destroyed) {
try { try {
this.#network = await this._detectNetwork(); this.#network = await this._detectNetwork();
} catch (error) { } catch (error) {
console.log("JsonRpcProvider failed to startup; retry in 1s"); console.log("JsonRpcProvider failed to detect network and cannot start up; retry in 1s (perhaps the URL is wrong or the node is not started)");
this.emit("error", makeError("failed to bootstrap network detection", "NETWORK_ERROR", { event: "initial-network-discovery", info: { error } }));
await stall(1000); await stall(1000);
} }
} }
@ -973,6 +971,11 @@ export abstract class JsonRpcApiProvider extends AbstractProvider {
send(method: string, params: Array<any> | Record<string, any>): Promise<any> { send(method: string, params: Array<any> | Record<string, any>): Promise<any> {
// @TODO: cache chainId?? purge on switch_networks // @TODO: cache chainId?? purge on switch_networks
// We have been destroyed; no operations are supported anymore
if (this.destroyed) {
return Promise.reject(makeError("provider destroyed; cancelled request", "UNSUPPORTED_OPERATION", { operation: method }));
}
const id = this.#nextId++; const id = this.#nextId++;
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
this.#payloads.push({ this.#payloads.push({
@ -1031,6 +1034,26 @@ export abstract class JsonRpcApiProvider extends AbstractProvider {
const accounts: Array<string> = await this.send("eth_accounts", [ ]); const accounts: Array<string> = await this.send("eth_accounts", [ ]);
return accounts.map((a) => new JsonRpcSigner(this, a)); return accounts.map((a) => new JsonRpcSigner(this, a));
} }
destroy(): void {
// Stop processing requests
if (this.#drainTimer) {
clearTimeout(this.#drainTimer);
this.#drainTimer = null;
}
// Cancel all pending requests
for (const { payload, reject } of this.#payloads) {
reject(makeError("provider destroyed; cancelled request", "UNSUPPORTED_OPERATION", { operation: payload.method }));
}
this.#payloads = [ ];
// Parent clean-up
super.destroy();
}
} }
export abstract class JsonRpcApiPollingProvider extends JsonRpcApiProvider { export abstract class JsonRpcApiPollingProvider extends JsonRpcApiProvider {