1import * as NodeFSP from "node:fs/promises";
2import * as NodeNet from "node:net";
3import * as NodePath from "node:path";
4import * as NodeTimersPromises from "node:timers/promises";
5
6const defaultTcpHosts = ["127.0.0.1", "localhost", "::1"];
7
8async function fileExists(filePath) {
9 try {
10 await NodeFSP.access(filePath);
11 return true;
12 } catch {
13 return false;
14 }
15}
16
17function tcpPortIsReady({ host, port, connectTimeoutMs = 500 }) {
18 return new Promise((resolveReady) => {
19 const socket = NodeNet.createConnection({ host, port });
20 let settled = false;
21
22 const finish = (ready) => {
23 if (settled) {
24 return;
25 }
26
27 settled = true;
28 socket.removeAllListeners();
29 socket.destroy();
30 resolveReady(ready);
31 };
32
33 socket.once("connect", () => {
34 finish(true);
35 });
36 socket.once("timeout", () => {
37 finish(false);
38 });
39 socket.once("error", () => {
40 finish(false);
41 });
42 socket.setTimeout(connectTimeoutMs);
43 });
44}
45
46async function resolvePendingResources({ baseDir, files, tcpPort, tcpHosts, connectTimeoutMs }) {
47 const pendingFiles = [];
48
49 for (const relativeFilePath of files) {
50 const ready = await fileExists(NodePath.resolve(baseDir, relativeFilePath));
51 if (!ready) {
52 pendingFiles.push(relativeFilePath);
53 }
54 }
55
56 let tcpReady = false;
57 for (const host of tcpHosts) {
58 tcpReady = await tcpPortIsReady({
59 host,
60 port: tcpPort,
61 connectTimeoutMs,
62 });
63 if (tcpReady) {
64 break;
65 }
66 }
67
68 return {
69 pendingFiles,
70 tcpReady,
71 };
72}
73
74export async function waitForResources({
75 baseDir,
76 files = [],
77 intervalMs = 100,
78 timeoutMs = 120_000,
79 tcpHost,
80 tcpPort,
81 connectTimeoutMs = 500,
82}) {
83 if (!Number.isInteger(tcpPort) || tcpPort <= 0) {
84 throw new TypeError("waitForResources requires a positive integer tcpPort");
85 }
86
87 const startedAt = Date.now();
88 const tcpHosts = tcpHost ? [tcpHost] : defaultTcpHosts;
89
90 while (true) {
91 const { pendingFiles, tcpReady } = await resolvePendingResources({
92 baseDir,
93 files,
94 tcpPort,
95 tcpHosts,
96 connectTimeoutMs,
97 });
98
99 if (pendingFiles.length === 0 && tcpReady) {
100 return;
101 }
102
103 if (Date.now() - startedAt >= timeoutMs) {
104 const pendingResources = [];
105 if (!tcpReady) {
106 pendingResources.push(tcpHost ? `tcp:${tcpHost}:${tcpPort}` : `tcp:${tcpPort}`);
107 }
108 for (const filePath of pendingFiles) {
109 pendingResources.push(`file:${filePath}`);
110 }
111
112 throw new Error(
113 `Timed out waiting for desktop dev resources after ${timeoutMs}ms: ${pendingResources.join(", ")}`,
114 );
115 }
116
117 await NodeTimersPromises.setTimeout(intervalMs);
118 }
119}