ほりひログ

所属組織の製品 (Azure とか) に関連する内容が多めだけど、個人の見解であって、所属組織を代表する公式情報ではないです。

Azure Functions で Top-Level await は使えるのか?

f:id:horihiro:20210212080153p:plain

ことのいきさつ

AWS Lambda が Node.js v14 をサポートしたらしいです。
そのニュースを見て、「そういえば Azure Functions はどのバージョンが動いているんだっけ?」と確認してみると、既に Node.js v14.15.4 が最新のようでした。

でも Node.js v14 って何ができるんだっけ?と思い、また調べてみると、Top-Level await のサポートがあるんですね。

で、ここで表題の疑問、

「Azure Functions の関数コードで Node.js の Top-Level await は使えるのか?」

が湧いた訳です。

結論

使える。

ちゃんと書く

Top-Level await については、下記 Qiita の記事がわかりやすいと思うのでお任せして。

qiita.com qiita.com

確認したいことは、関数コードで module.exports の外で await が使えるかどうか、です。
# 単なる技術的興味なので、何に使えるかはこの際忘れましょう。

こんな感じ。await する関数 sleep は、指定のミリ秒数待機する単純な例です。

[index.js]

const sleep = (time) => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, time);
    });
}

const now = Date.now();
let responseMessage = `before await at ${Date.now() - now}
`;
await sleep(60000); // <-- このコードは動くのか?
responseMessage +=`after await at ${Date.now() - now}`;

module.exports = async function (context, req) {
    context.res = {
        // status: 200, /* Defaults to 200 */
        body: responseMessage
    };
}

実行してみます。

f:id:horihiro:20210211204555p:plain

やっぱり「await は async 関数の中だけ。」と怒られます。

Top-Level await は、ES modules の仕様なので、関数コードが ES modules として扱われる必要があります。

github.com

Azure Functions の Node.js Worker では、デフォルト (ファイル拡張子が .mjs 以外) だと関数コードを ( import ではなく) require しているので、CommonJS として使用することが想定されています。

[https://github.com/Azure/azure-functions-nodejs-worker/blob/v2.x/src/FunctionLoader.ts#L42:embed:cite] f:id:horihiro:20210212073027p:plain

なので、この関数コードを何とかして ES modules として読ませてやればよさそうです。
具体的には下記 2 つの変更をします。

  1. ファイル拡張子を .mjs に変更
    index.js -> index.mjs
  2. エクスポートの書式を ES module 化
    module.exports = -> export default

[index.mjs]

const sleep = (time) => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, time);
    });
}

const now = Date.now();
let responseMessage = `before await at ${Date.now() - now}
`;
await sleep(60000); // <-- こいつ・・・動くぞ!
responseMessage +=`after await at ${Date.now() - now}`;

export default async function (context, req) {
    context.res = {
        // status: 200, /* Defaults to 200 */
        body: responseMessage
    };
}

実行してみると、きちんとエクスポートした関数内から、関数の外で await した (60000 ミリ秒経過) 結果を参照することができています。

f:id:horihiro:20210211213054p:plain

await 中、例えば、20000 ミリ秒経過後に上記の HTTP 関数に対してアクセスすると、残りの 40000 ミリ秒待機した後にレスポンスが返却されます。

ちなみに、ES modules としてモジュールをロードする方法として、 package.json 内に "type": "module" を追加する方法もあります。 しかし、上記の通り Azure Functions の Node.js Worker は、ファイル拡張子が .mjs の時のみ ES modules として import する実装なので、上記のような拡張子の変更が必須です。

TypeScript は?

Azure Functions Core Tools では TypeScript で書かれた関数コードのテンプレートも作ってくれます。

この TypeScript のコードは、デプロイ時に tsc でトランスパイルされるので、実質 JavaScript で実行されます。

なので、TypeScript でも Top-Level await が使えそうですが、結果的には下記 2 つの追加作業が必要です。

  1. トランスパイルの設定の変更
    tsc で Top-Level await を通すよう tsconfig.json を以下のように修正します。
    f:id:horihiro:20210212121313p:plain
  2. tsc の出力結果の修正
    tsc の出力結果は、デフォルトでは ./dist/<関数名>/index.js に出力されますが、上記のとおり .js のままだと ES modules としてロードされないので、.mjs に拡張子を変更して、function.json の中の scriptFile の値も mjs ファイルを変更します。

ただ 2. は tsc を実行するたびにやらないといけないです。ちょっと面倒ですね。。。

結局何に使えるのか。

パッと思いつく使い方としては、エクスポートした関数の外で (つまり当該関数コード ファイルのロード時 1 回だけ) node-fetchaxios などで外部リソースを await で取得し、関数コード内で取得した外部リソースを再利用する、てことはできそうです。
# 副作用までは追いかけていませんが。

Azure Functions の関数コード用テンプレートはここで公開されているので、需要があるなら issue や PR を挙げてもよいかもしれませんね。

github.com