Arseniy Klempner d803565b30
feat(browser-tests): simplify, refactor, update dockerized browser node (#2623)
* feat(browser-tests): simplify, refactor, update dockerized browser node

* Update packages/browser-tests/web/index.ts

* fix: remove comments and console.logs from tests

* fix: add temporary logging

* fix: debugging static sharding

* fix: replace console with logger

* fix: remove use of any

* fix: log dial error

* fix: replace any with libp2p options

* fix: remove unused logic around sourcing address.env

* fix: uncomment log

* fix: add more logging and fix tests

* feat: add types for test-config

* fix: add types to server.ts

* fix: remove more uses of any

* fix: remove use of any in endpoint handlers
2025-10-07 10:54:19 -07:00

245 lines
7.2 KiB
TypeScript

import { fileURLToPath } from "url";
import * as path from "path";
import cors from "cors";
import express, { Request, Response } from "express";
import { Logger } from "@waku/utils";
import wakuRouter from "./routes/waku.js";
import { initBrowser, getPage, closeBrowser } from "./browser/index.js";
import {
DEFAULT_CLUSTER_ID,
DEFAULT_NUM_SHARDS,
Protocols,
AutoSharding,
StaticSharding,
} from "@waku/interfaces";
import { CreateNodeOptions } from "@waku/sdk";
import type { WindowNetworkConfig } from "../types/global.js";
interface NodeError extends Error {
code?: string;
}
const log = new Logger("server");
const app = express();
app.use(cors());
app.use(express.json());
import * as fs from "fs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const distRoot = path.resolve(__dirname, "..");
const webDir = path.resolve(distRoot, "web");
app.get("/app/index.html", (_req: Request, res: Response) => {
try {
const htmlPath = path.join(webDir, "index.html");
let htmlContent = fs.readFileSync(htmlPath, "utf8");
const networkConfig: WindowNetworkConfig = {};
if (process.env.WAKU_CLUSTER_ID) {
networkConfig.clusterId = parseInt(process.env.WAKU_CLUSTER_ID, 10);
}
if (process.env.WAKU_SHARD) {
networkConfig.shards = [parseInt(process.env.WAKU_SHARD, 10)];
log.info("Using static shard:", networkConfig.shards);
}
const lightpushNode = process.env.WAKU_LIGHTPUSH_NODE || null;
const enrBootstrap = process.env.WAKU_ENR_BOOTSTRAP || null;
log.info("Network config on server start, pre headless:", networkConfig);
const configScript = ` <script>
window.__WAKU_NETWORK_CONFIG = ${JSON.stringify(networkConfig)};
window.__WAKU_LIGHTPUSH_NODE = ${JSON.stringify(lightpushNode)};
window.__WAKU_ENR_BOOTSTRAP = ${JSON.stringify(enrBootstrap)};
</script>`;
const originalPattern =
' <script type="module" src="./index.js"></script>';
const replacement = `${configScript}\n <script type="module" src="./index.js"></script>`;
htmlContent = htmlContent.replace(originalPattern, replacement);
res.setHeader("Content-Type", "text/html");
res.send(htmlContent);
} catch (error) {
log.error("Error serving dynamic index.html:", error);
res.status(500).send("Error loading page");
}
});
app.use("/app", express.static(webDir, { index: false }));
app.use(wakuRouter);
async function startAPI(requestedPort: number): Promise<number> {
try {
app.get("/", (_req: Request, res: Response) => {
res.json({ status: "Waku simulation server is running" });
});
app
.listen(requestedPort, () => {
log.info(`API server running on http://localhost:${requestedPort}`);
})
.on("error", (error: NodeError) => {
if (error.code === "EADDRINUSE") {
log.error(
`Port ${requestedPort} is already in use. Please close the application using this port and try again.`,
);
} else {
log.error("Error starting server:", error);
}
throw error;
});
return requestedPort;
} catch (error) {
log.error("Error starting server:", error);
throw error;
}
}
async function startServer(port: number = 3000): Promise<void> {
try {
const actualPort = await startAPI(port);
await initBrowser(actualPort);
try {
log.info("Auto-starting node with CLI configuration...");
const hasEnrBootstrap = Boolean(process.env.WAKU_ENR_BOOTSTRAP);
const networkConfig: AutoSharding | StaticSharding = process.env.WAKU_SHARD
? ({
clusterId: process.env.WAKU_CLUSTER_ID
? parseInt(process.env.WAKU_CLUSTER_ID, 10)
: DEFAULT_CLUSTER_ID,
shards: [parseInt(process.env.WAKU_SHARD, 10)],
} as StaticSharding)
: ({
clusterId: process.env.WAKU_CLUSTER_ID
? parseInt(process.env.WAKU_CLUSTER_ID, 10)
: DEFAULT_CLUSTER_ID,
numShardsInCluster: DEFAULT_NUM_SHARDS,
} as AutoSharding);
const createOptions: CreateNodeOptions = {
defaultBootstrap: false,
...(hasEnrBootstrap && {
discovery: {
dns: true,
peerExchange: true,
peerCache: true,
},
}),
networkConfig,
};
log.info(
`Bootstrap mode: ${hasEnrBootstrap ? "ENR-only (defaultBootstrap=false)" : "default bootstrap (defaultBootstrap=true)"}`,
);
if (hasEnrBootstrap) {
log.info(`ENR bootstrap peers: ${process.env.WAKU_ENR_BOOTSTRAP}`);
}
log.info(
`Network config: ${JSON.stringify(networkConfig)}`,
);
await getPage()?.evaluate((config) => {
return window.wakuApi.createWakuNode(config);
}, createOptions);
await getPage()?.evaluate(() => window.wakuApi.startNode());
try {
await getPage()?.evaluate(() =>
window.wakuApi.waitForPeers?.(5000, [Protocols.LightPush]),
);
log.info("Auto-start completed with bootstrap peers");
} catch (peerError) {
log.info(
"Auto-start completed (no bootstrap peers found - may be expected with test ENRs)",
);
}
} catch (e) {
log.warn("Auto-start failed:", e);
}
} catch (error) {
log.error("Error starting server:", error);
}
}
process.on("uncaughtException", (error) => {
log.error("Uncaught Exception:", error);
if (process.env.NODE_ENV !== "production") {
process.exit(1);
}
});
process.on("unhandledRejection", (reason, promise) => {
log.error("Unhandled Rejection at:", promise, "reason:", reason);
if (process.env.NODE_ENV !== "production") {
process.exit(1);
}
});
const gracefulShutdown = async (signal: string) => {
log.info(`Received ${signal}, gracefully shutting down...`);
try {
await closeBrowser();
} catch (e) {
log.warn("Error closing browser:", e);
}
process.exit(0);
};
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
function parseCliArgs() {
const args = process.argv.slice(2);
let clusterId: number | undefined;
let shard: number | undefined;
for (const arg of args) {
if (arg.startsWith("--cluster-id=")) {
clusterId = parseInt(arg.split("=")[1], 10);
if (isNaN(clusterId)) {
log.error("Invalid cluster-id value. Must be a number.");
process.exit(1);
}
} else if (arg.startsWith("--shard=")) {
shard = parseInt(arg.split("=")[1], 10);
if (isNaN(shard)) {
log.error("Invalid shard value. Must be a number.");
process.exit(1);
}
}
}
return { clusterId, shard };
}
const isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
if (isMainModule) {
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
const cliArgs = parseCliArgs();
if (cliArgs.clusterId !== undefined) {
process.env.WAKU_CLUSTER_ID = cliArgs.clusterId.toString();
log.info(`Using CLI cluster ID: ${cliArgs.clusterId}`);
}
if (cliArgs.shard !== undefined) {
process.env.WAKU_SHARD = cliArgs.shard.toString();
log.info(`Using CLI shard: ${cliArgs.shard}`);
}
void startServer(port);
}