🦔

実践: await using でリソース開放

2024/11/06に公開2

実践

いつ使うんだこれと思ってたら使う日が来たシリーズ。

https://zenn.dev/ventus/articles/ts5_2-using-preview

今回、Deno で使ったんですが、 Node.js やブラウザでも Polyfill を入れれば動きます。

https://github.com/mizchi/play-ts-using

try finally で puppeteer を終了したい

Deno で puppeteer を扱うために、こういうコードを書いてました。

// original
import puppeteer from "npm:puppeteer@23.6.1";
import chromeFinder from "npm:chrome-finder@1.0.7";
let browser: puppeteer.Browser | null = null;
try {
  browser = await puppeteer.launch({
    headless: false,
    executablePath: chromeFinder(),
  });
  const page = (await browser.pages())[0];
  await page.goto("https://google.com", {
    waitUntil: "networkidle0",
  });
  const title = await page.title();
  console.log(title);
} finally {
  browser?.close();
}

この browser.close() を呼び忘れると、イベントリスナーが開放されない判定になり、Deno/Node ではプロセスが終了しなくなります。
逆に、強制的に process.exit(0) や Deno.exit(0) しても、今度は Chrome が終了しなくなります。

なので必ず browser.close() を呼びたいんですが、 await puppteer.launch(...) 自体が非同期で例外が発生する可能性があり、スコープの外に let で初期化した動的なシンボルを置いています。

これは初期化は一回なので理想的には線形メモリ的に扱いたいんですが、JS にはその機能がありません。

await using 版

というわけでスコープを抜ける時に処理を予約する await using のための関数を定義して、puppeteer 初期化しつつ browser.close() を予約します。

import puppeteer from "npm:puppeteer@23.6.1";
import chromeFinder from "npm:chrome-finder@1.0.7";

async function useBrowserContext() {
  const browser = await puppeteer.launch({
    headless: false,
    executablePath: chromeFinder(),
  });
  const page = (await browser.pages())[0];
  return {
    browser,
    page,
    async [Symbol.asyncDispose]() {
      await browser.close();
    },
  };
}

{
  await using ctx = await useBrowserContext();
  await ctx.page.goto("https://google.com", {
    waitUntil: "networkidle0",
  });
  const title = await ctx.page.title();
  console.log(title);
  // このスコープを抜ける時に Symbol.asyncDispose() が呼ばれる
}
console.log("Chrome released!");

型を付けるならこういう感じ

async function useBrowserContext(): Promise<AsyncDisposable & {
  browser: puppeteer.Browser;
  page: puppeteer.Page;
}> {
///...
}

見慣れない書き方なのでちょっと頭の体操感があるんですが、仕組みを理解すれば「リソース確保時に開放処理が予約されている」と認識することができます。

存在は知ってたんですが、はじめて実践的に使えて嬉しくなって記事にしました。こういう新機能は早めに試しておくと、はじめて見た時にビックリせずに済みます。

Discussion

rithmetyrithmety

この記事の try finally 版だと puppeteer.launch に失敗したら代入もされないので let を使う必要がなく
await using 版だと browser.pages にコケた場合の挙動が元コードと異なるように思います

それはそれとして using を使っていると
開放に失敗する可能性がある値を複数使う場合に特に役に立ちます

const open = name => {
  const close = () => {
    console.log(`try to close ${name}.`)
    throw new Error(`failed closing ${name}.`)
  }
  return { name, close, [Symbol.dispose]: close }
}

// 今まで
let a, b, c
try {
  a = open('a')
  try {
    b = open('b')
    try {
      c = open('c')
      console.log(a, b, c) // a,b,c を使った処理
    } finally {
      c?.close()
    }
  } finally {
    b?.close() // c の close に失敗しても b の close に挑戦する
  }
} finally {
  a?.close()
}


// これから
using a = open('a')
using b = open('b')
using c = open('c')
console.log(a, b, c) // a,b,c を使った処理