diff --git a/.gitignore b/.gitignore index 8ff5b8130..b38369109 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules lib package-lock.json .idea +*.heapsnapshot \ No newline at end of file diff --git a/simple-git-hooks.js b/simple-git-hooks.js index e3a569199..32f540bf7 100644 --- a/simple-git-hooks.js +++ b/simple-git-hooks.js @@ -1,4 +1,4 @@ module.exports = { - 'prepare-commit-msg': `grep -qE '^[^#]' .git/COMMIT_EDITMSG || (exec < /dev/tty && yarn cz --hook || true)`, - 'commit-msg': 'yarn commitlint --edit $1', + 'prepare-commit-msg': `grep -qE '^[^#]' .git/COMMIT_EDITMSG || (exec < /dev/tty && pnpm cz --hook || true)`, + 'commit-msg': 'pnpm commitlint --edit $1', } diff --git a/src/interceptors/ClientRequest/MockHttpSocket.ts b/src/interceptors/ClientRequest/MockHttpSocket.ts index 45cf6b5dd..e5b83c92a 100644 --- a/src/interceptors/ClientRequest/MockHttpSocket.ts +++ b/src/interceptors/ClientRequest/MockHttpSocket.ts @@ -128,7 +128,13 @@ export class MockHttpSocket extends MockSocket { // Once the socket is finished, nothing can write to it // anymore. It has also flushed any buffered chunks. - this.once('finish', () => this.requestParser.free()) + this.once('finish', () => { + this.requestParser.free() + // @ts-ignore + this.parser = null + // @ts-ignore + this._httpMessage.parser = null + }) if (this.baseUrl.protocol === 'https:') { Reflect.set(this, 'encrypted', true) diff --git a/test/modules/http/http-memory-worker.js b/test/modules/http/http-memory-worker.js new file mode 100644 index 000000000..6b82c0659 --- /dev/null +++ b/test/modules/http/http-memory-worker.js @@ -0,0 +1,33 @@ +const v8 = require('node:v8') +const path = require('node:path') +const http = require('node:http') +const { workerData, parentPort } = require('node:worker_threads') +const { + ClientRequestInterceptor, +} = require('../../../lib/interceptors/ClientRequest') + +const interceptor = new ClientRequestInterceptor() +interceptor.apply() + +const pendingRequests = [] + +for (let i = 0; i < workerData.requestCount; i++) { + pendingRequests.push( + new Promise((resolve, reject) => { + http + .get(workerData.serverUrl, (response) => { + response.on('data', () => {}) + response.on('error', (error) => reject(error)) + response.on('end', () => resolve()) + }) + .on('error', (error) => reject(error)) + }) + ) +} + +globalThis.gc?.() +v8.writeHeapSnapshot(workerData.snapshotPath) + +Promise.allSettled(pendingRequests).then(() => { + parentPort.postMessage('complete') +}) diff --git a/test/modules/http/http-memory.test.ts b/test/modules/http/http-memory.test.ts new file mode 100644 index 000000000..e51179cd9 --- /dev/null +++ b/test/modules/http/http-memory.test.ts @@ -0,0 +1,50 @@ +// @vitest-environment node +import fs from 'node:fs' +import path from 'node:path' +import { Worker } from 'node:worker_threads' +import { HttpServer } from '@open-draft/test-server/http' +import { it, expect, beforeAll, afterAll } from 'vitest' +import { DeferredPromise } from '@open-draft/deferred-promise' + +const server = new HttpServer((app) => { + app.get('/', (_req, res) => res.status(200).end()) +}) + +beforeAll(async () => { + await server.listen() +}) + +afterAll(async () => { + await server.close() +}) + +it( + 'does not retain the MockHttpSocket instance', + async () => { + const snapshotPath = path.resolve(__dirname, 'http-memory.heapsnapshot') + + // Spawn the usage scenario in a worker so the test doesn't affect the heap. + const worker = new Worker( + path.resolve(__dirname, './http-memory-worker.js'), + { + workerData: { + requestCount: 10_000, + serverUrl: server.http.url('/'), + snapshotPath, + }, + stderr: true, + stdout: true, + } + ) + + const completePromise = new DeferredPromise() + worker.once('message', () => completePromise.resolve()) + worker.on('error', (error) => completePromise.reject(error)) + + await completePromise + + const snapshotStats = await fs.promises.stat(snapshotPath) + expect(snapshotStats.size / 1_000_000).toBeLessThanOrEqual(100) + }, + { timeout: 10_000 } +)