diff --git a/.changeset/wild-pets-change.md b/.changeset/wild-pets-change.md new file mode 100644 index 00000000000..b6015300df3 --- /dev/null +++ b/.changeset/wild-pets-change.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Optimize the network connections to prevent memory leaks. diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts index 74d524acd72..ebbf7e5c3bb 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts @@ -165,6 +165,10 @@ export class EdrProvider extends BaseProvider { let edrProvider: EdrProvider; + // We use a WeakRef to the provider to prevent the subscriptionCallback + // below from creating a cycle and leaking the provider. + let edrProviderWeakRef: WeakRef | undefined; + // We need to catch errors here, as the provider creation can panic unexpectedly, // and we want to make sure such a crash is propagated as a ProviderError. try { @@ -191,13 +195,17 @@ export class EdrProvider extends BaseProvider { }, { subscriptionCallback: (event: SubscriptionEvent) => { - edrProvider.onSubscriptionEvent(event); + const deferredProvider = edrProviderWeakRef?.deref(); + if (deferredProvider !== undefined) { + deferredProvider.onSubscriptionEvent(event); + } }, }, contractDecoder, ); edrProvider = new EdrProvider(provider, jsonRpcRequestWrapper); + edrProviderWeakRef = new WeakRef(edrProvider); } catch (error) { ensureError(error); @@ -282,6 +290,7 @@ export class EdrProvider extends BaseProvider { } public async close(): Promise { + this.removeAllListeners(); // Clear the provider reference to help with garbage collection this.#provider = undefined; } diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/http-provider.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/http-provider.ts index db76abdb93a..de5e701e401 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/http-provider.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/http-provider.ts @@ -161,6 +161,7 @@ export class HttpProvider extends BaseProvider { } public async close(): Promise { + this.removeAllListeners(); if (this.#dispatcher !== undefined) { // See https://github.com/nodejs/undici/discussions/3522#discussioncomment-10498734 await this.#dispatcher.close(); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/edr-provider.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/edr-provider.ts index f829b2b9bb3..0a70e4e61d2 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/edr-provider.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/edr-provider.ts @@ -470,6 +470,17 @@ describe("edr-provider", () => { {}, ); }); + + it("should remove all listeners after closing", async () => { + const connection = await hre.network.connect(); + + connection.provider.on("notification", () => {}); + assert.equal(connection.provider.listenerCount("notification"), 1); + + await connection.provider.close(); + + assert.equal(connection.provider.listenerCount("notification"), 0); + }); }); describe("isDefaultEdrNetworkHDAccountsConfig", () => { diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/http-provider.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/http-provider.ts index 25a887e2879..f1aa8d2e3cb 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/http-provider.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/http-provider.ts @@ -579,5 +579,20 @@ describe("http-provider", () => { {}, ); }); + + it("should remove all listeners after closing", async () => { + const provider = await HttpProvider.create({ + url: "http://localhost", + networkName: "exampleNetwork", + timeout: 20_000, + }); + + provider.on("notification", () => {}); + assert.equal(provider.listenerCount("notification"), 1); + + await provider.close(); + + assert.equal(provider.listenerCount("notification"), 0); + }); }); });