mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 06:12:17 +03:00
`stop()` is the normal lifecycle for a `follow: true` stream, not an edge case, so the `signal.aborted` early return skipping `clearTimeout` leaked one armed timer per stop. Cancelling the reader on the early loop exit closes the second half. I reformatted the changelog fragment to the `changelog.d` convention (`- **fix(scope):** …`) before merging — `check:changelog-integrity` rejects a fragment that does not start with a markdown bullet, which is the same gate your #13158 was about. Wording is yours, unchanged in substance. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them. - `typecheck:core` clean - complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline - 71 focused assertions green across the 13 test files this batch adds or touches ⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff. Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import http from "node:http";
|
|
import type { AddressInfo } from "node:net";
|
|
|
|
import { createLogStream } from "../../src/lib/cli-helper/log-streamer.ts";
|
|
|
|
function armedTimers(): number {
|
|
return process.getActiveResourcesInfo().filter((r) => r === "Timeout").length;
|
|
}
|
|
|
|
async function startServer(): Promise<{ port: number; close: () => Promise<void> }> {
|
|
const open: http.ServerResponse[] = [];
|
|
const server = http.createServer((_req, res) => {
|
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
res.write("log line\n");
|
|
// Deliberately left open: stop() must land while the stream is still live.
|
|
open.push(res);
|
|
});
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const { port } = server.address() as AddressInfo;
|
|
return {
|
|
port,
|
|
close: async () => {
|
|
for (const res of open) res.end();
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
},
|
|
};
|
|
}
|
|
|
|
test("stop() clears the stream timeout timer", async () => {
|
|
const server = await startServer();
|
|
try {
|
|
const before = armedTimers();
|
|
|
|
const streams = Array.from({ length: 8 }, () =>
|
|
createLogStream({
|
|
baseUrl: `http://127.0.0.1:${server.port}`,
|
|
follow: true,
|
|
// Long enough that a leaked timer is still armed when we measure.
|
|
timeout: 120_000,
|
|
})
|
|
);
|
|
|
|
// Begin consuming so start() runs and the fetch is in flight.
|
|
for (const s of streams) {
|
|
void s.stream
|
|
.getReader()
|
|
.read()
|
|
.catch(() => {});
|
|
}
|
|
await new Promise((r) => setTimeout(r, 300));
|
|
|
|
for (const s of streams) s.stop();
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
|
|
const after = armedTimers();
|
|
assert.ok(
|
|
after <= before,
|
|
`stopping 8 streams retained ${after - before} armed timer(s) ` +
|
|
`(before=${before} after=${after}); stop() must clear the timeout`
|
|
);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
test("a stream that ends normally still clears its timer", async () => {
|
|
const finished = http.createServer((_req, res) => {
|
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
res.end("done\n");
|
|
});
|
|
await new Promise<void>((resolve) => finished.listen(0, "127.0.0.1", resolve));
|
|
const { port } = finished.address() as AddressInfo;
|
|
|
|
try {
|
|
const before = armedTimers();
|
|
const { stream } = createLogStream({
|
|
baseUrl: `http://127.0.0.1:${port}`,
|
|
follow: false,
|
|
timeout: 120_000,
|
|
});
|
|
|
|
const reader = stream.getReader();
|
|
while (true) {
|
|
const { done } = await reader.read();
|
|
if (done) break;
|
|
}
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
|
|
assert.ok(
|
|
armedTimers() <= before,
|
|
"a normally-completed stream must not leave its timeout armed"
|
|
);
|
|
} finally {
|
|
await new Promise<void>((resolve) => finished.close(() => resolve()));
|
|
}
|
|
});
|