paperclip/tests/runner-e2e/ports.ts

118 lines
3.5 KiB
TypeScript

import { createServer } from "node:net";
import { derivePaperclipViteHmrPort } from "../../packages/shared/src/runtime-exposure/ports.js";
export const RUNNER_E2E_EMBEDDED_POSTGRES_PORT = 54_329;
export interface LoopbackPortReservation {
port: number;
close(): Promise<void>;
}
export type OpenLoopbackPort = (
requestedPort: number,
) => Promise<LoopbackPortReservation>;
async function openLoopbackPort(
requestedPort: number,
): Promise<LoopbackPortReservation> {
const server = createServer();
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(requestedPort, "127.0.0.1", resolve);
});
const address = server.address();
if (!address || typeof address === "string") {
server.close();
throw new Error("Failed to reserve a loopback port");
}
return {
port: address.port,
close: () =>
new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
),
};
}
export function runnerE2EServerPortConflictsWithDatabase(
serverPort: number,
databasePort = RUNNER_E2E_EMBEDDED_POSTGRES_PORT,
) {
return (
serverPort === databasePort ||
derivePaperclipViteHmrPort(serverPort) === databasePort
);
}
function isAddressInUse(error: unknown) {
return (error as NodeJS.ErrnoException | undefined)?.code === "EADDRINUSE";
}
export async function reserveRunnerE2EServerPort(
options: {
databasePort?: number;
maxAttempts?: number;
openPort?: OpenLoopbackPort;
} = {},
) {
const databasePort =
options.databasePort ?? RUNNER_E2E_EMBEDDED_POSTGRES_PORT;
const maxAttempts = options.maxAttempts ?? 32;
const openPort = options.openPort ?? openLoopbackPort;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const serverReservation = await openPort(0);
let hmrReservation: LoopbackPortReservation | undefined;
try {
if (
runnerE2EServerPortConflictsWithDatabase(
serverReservation.port,
databasePort,
)
) {
continue;
}
const hmrPort = derivePaperclipViteHmrPort(serverReservation.port);
try {
hmrReservation = await openPort(hmrPort);
} catch (error) {
if (isAddressInUse(error)) continue;
throw error;
}
return serverReservation.port;
} finally {
await hmrReservation?.close();
await serverReservation.close();
}
}
throw new Error(
`Failed to reserve a conflict-free Paperclip/Vite port pair after ${maxAttempts} attempts`,
);
}
/** Hold an OS-assigned database port until the isolated server is ready to spawn.
* Using ephemeral allocation avoids concurrent cells scanning the same default
* Postgres fallback range. The child cannot inherit this socket, so an unrelated
* process can still claim it in the short close-to-spawn interval.
*/
export async function reserveRunnerE2EDatabasePort(
serverPort: number,
options: { maxAttempts?: number; openPort?: OpenLoopbackPort } = {},
): Promise<LoopbackPortReservation> {
const openPort = options.openPort ?? openLoopbackPort;
const maxAttempts = options.maxAttempts ?? 32;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const reservation = await openPort(0);
if (
!runnerE2EServerPortConflictsWithDatabase(serverPort, reservation.port)
) {
return reservation;
}
await reservation.close();
}
throw new Error(
`Failed to reserve an isolated database port after ${maxAttempts} attempts`,
);
}