Wiki_JS_AsyncLocalStorage - inoueshinichi/Wiki_Web GitHub Wiki

AsyncLocalStorageの基本

  • 非同期な文脈で状態の共有を行う方法
  • ReactのuseContextと同じ機能

参考

AsyncLocalStorage

  • 非同期処理のコンテキストを保持できる.
  • 元来Node.jsの機能だが, WinterCGによる標準化が進められている.
  • cloudflare, wokers, denoの実行環境でもOK
  • サーバーサイドの機能であり、クライアントサイド(ブラウザー)環境では使えない.
  • 同じ非同期コールスタックに属する非同期処理専用のメモリ領域
  • スレッドローカルな変数やメモリ領域と同じ機能.
  • 上記をReactのuseContextフックと同じように使えるようにしたもの.
  • 非同期関数(スコープ)に一つのAsyncLocalStorageが割り当てられる

各Asyncコールスタックで値を共有する方法

  1. Reactのpropsの様な横断型変数(visitorパターン)を各コールスタックの引数に定義する
  2. AsyncLocalStorage機能を使う
  • 保持したい値1つに対してAsyncLocalStorageインスタンスを生成
  • run()メソッドで値を格納
  • getState()メソッドで値を取得
  • AsyncLocalStorageは, 非同期コールスタックが深くなると有効な手段.

AsyncLocalStorageを使って取引IDを共有する方法

  • transferMoney=(withdraw, deposit, notify)
// JS

const transactionIdStorage = new AsyncLocalStorage<string>();

// この中でコールされる非同期処理はtransactionIdStorageに保持された値を参照できる
transactionIdStorage.run("12345", async () => { await transferMoney("Alice", "Bob"); });

async function transferMoney(sender: string, reciever: string) {
  // 非同期処理内部でtransactionIdStorageに紐づいたオブジェクト("12345")にアクセスできる.
  // ReactのuseContext的.
  // React.Provider = transactionIdStorage.run(~)
  const transactionId = transactionIdStorage.getStore();

  // withdraw,deposit,notifyからもtransactionIdを参照可能
  await withdraw(sender, 1000);
  await deposit(reciever, 1000);
  await notify(`${sender} sent 1000 yen to ${reciever} (transactionId: ${transactionId}`);
}

AsyncLocalStorageの使い所

構造化ロギング(例)

  • ログとは、メイン処理と直行するもの(本来なくても良い処理)
  • メイン処理に対してクリティカルでない&どこでも存在するもの&付随物
// JS
const logMetaDataStorage = new AsyncLocalStorage<Record<string, unknown>>();

logMetaDataStorage.run({ transactionId: "12345" }, async () => {
  await transferMoney("Alice", "Bob");
});

/* log出力 */
function log(message: string) {
  const logMetaData = logMetaDataStorage.getStore(); // transferMoney内部でコールされるのでOK
  console.log({...logMetaData, message);
}

async function transferMoney(sender: string, receiver: string) {
  const currentMetaData = logMetaDataStorage.getStore();
  log("Transferring Money");
}

依存性の注入(Dependency Insert) (例)

  • 引数などインターフェースを変更することなく必要な所にピンポイントで共通参照状態を注入できるところがAsyncLocalStorageのメリット
  • AsyncLocalStorageを使用しない.
  • タイムスタンプが現在時刻から1分以上前ならエラーとなる処理.
// JS

// 依存性注入前の実装
async function getLastestDataAndVerify() {
  const latestData = await fetchLatestData();
  if (latestData.timestamp < Date.now() - 1000 * 60) {
    throw new Error("Data is too old");
  }
}

// 依存性注入によるAsyncLocalStrageの活用
const nowStorage = AsyncLocalStorage<() => number>();

// 注入された関数を取得(何も注入されていなければDate.now())
const getNowFunction = () => nowStorage.getState() ?? Date.now;

async function getLatestDataAndVerify() {
  const latestData = await fetchLatestData();
  const nowFunction = getNowFunction();
  if (latestData.timestamp < nowFunction() - 1000 * 60) {
    throw new Error('Data is too old');
  }
}

AsyncLocalStorageを制御するとネストが発生するので注意

  • コールバック地獄の温床になるので使うのは控えめに
// JS
const logMetaDataStorage = new AsyncLocalStorage<Record<string,unknown>>();

// ログ出力
function log(message: string) {
  const logMetaData = logMetaDataStorage.getState();
  console.log({...logMetaData, message });
}

// コールバック地獄が発生する例
async function transferMoney(sender: string, receiver: string) {
  
  // 状態取得
  const currentMetaData: Record<string, unknown> = logMetaDataStorage.getStore();
  
  // 1. withdraw
  await logMetaDataStorage.run({...currentMetaData, operation: 'withdraw'}, async () => {
    // このスコープでAsyncLocalStorageの制御を入れるとコールバック地獄発生の始まり
    await withdraw(sender, 1000);
  });
  
  // 2. deposit
  await logMetaDataStorage.run({...currentMetaData, operation: 'deposit'}, async () => {
    // このスコープでAsyncLocalStorageの制御を入れるとコールバック地獄発生の始まり
    await deposit(receiver, 1000);
  });

  // 3. notify
  await logMetaDataStorage.run({...currentMetaData, operation: 'notify'}, async () => {
    // このスコープでAsyncLocalStorageの制御を入れるとコールバック地獄発生の始まり
    await notify(`${sender} sent 1000 yen to ${receiver}`);
  });
}
⚠️ **GitHub.com Fallback** ⚠️