KDP(電子出版)のメモ 急急如律令

Amazon Kindleダイレクト・パブリッシングでの電子出版や電子書籍の作成販売について、文章やイラストの作成や編集方法について書いています。

async await汚染を減らしたい

AozoraEpub3Converter.jsから実行したgetImageFilePathでthis.bookInfoがnullとなる。なので、getImageFilePathの引数にthis.bookInfoを追加した。

            if (this.imageIndex - 1 === this.bookInfo.coverImageIndex) {
                                                      ^

TypeError: Cannot read properties of null (reading 'coverImageIndex') at Epub3Writer.getImageFilePath

this.IMAGES_PATHをEpub3Writer.IMAGES_PATHに置き換えた。IMAGES_PATHというのはEPUBに画像を格納するフォルダー名。

return ../${Epub3Writer.IMAGES_PATH}${outImageFileName};

で画像ファイルのHTMLのsrcをリターンする。object Promiseなので、awaitが必要。

画像

async await汚染

You:

asyncを使わずthenで書くことは可能か

import fs from "fs";
import sharp from "sharp";

export default class ImageInfo {
  /** 画像の情報を生成
   * @param {string} ext png jpg gif webp など
   */
  constructor(ext, width, height) {
    this.id = null;                // ファイルID
    this.outFileName = null;       // 出力ファイル名
    this.ext = ext ? ext.toLowerCase() : null;

    this.width = width ?? -1;
    this.height = height ?? -1;

    this.outWidth = -1;
    this.outHeight = -1;

    this.isCover = false;
    this.rotateAngle = 0;
  }

  // =========================
  // static 生成メソッド
  // =========================

  /** ファイルから画像情報を生成 */
  static async getImageInfoFromFile(imageFile) {
    const buffer = await fs.promises.readFile(imageFile);
    return await ImageInfo.getImageInfo(buffer);
  }

  /** 画像ストリーム / Buffer から画像情報を生成 */
  static async getImageInfo(input) {
    if (!input) return null;

    try {
      const metadata = await sharp(input).metadata();

      if (!metadata || !metadata.format) return null;

      return new ImageInfo(
        metadata.format,      // png jpeg webp gif avif など
        metadata.width,
        metadata.height
      );

    } catch (e) {
      return null;
    }
  }

  /** 既に幅高さが分かっている場合 */
  static getImageInfoFromRaw(ext, width, height) {
    return new ImageInfo(ext, width, height);
  }

  // =========================
  // getter / setter
  // =========================

  getId() {
    return this.id;
  }

  setId(id) {
    this.id = id;
  }

  getOutFileName() {
    return this.outFileName;
  }

  setOutFileName(file) {
    this.outFileName = file;
  }

  setExt(ext) {
    this.ext = ext?.toLowerCase();
  }

  getExt() {
    return this.ext;
  }

  /** mime形式(image/png)を返す */
  getFormat() {
    if (!this.ext) return null;
    return "image/" + (this.ext === "jpg" ? "jpeg" : this.ext);
  }

  getIsCover() {
    return this.isCover;
  }

  setIsCover(isCover) {
    this.isCover = isCover;
  }

  getWidth() {
    return this.width;
  }

  setWidth(width) {
    this.width = width;
  }

  getHeight() {
    return this.height;
  }

  setHeight(height) {
    this.height = height;
  }

  getOutWidth() {
    return this.outWidth;
  }

  setOutWidth(outWidth) {
    this.outWidth = outWidth;
  }

  getOutHeight() {
    return this.outHeight;
  }

  setOutHeight(outHeight) {
    this.outHeight = outHeight;
  }
}

ChatGPT:

結論から言うと 可能です(完全に then チェーンに書き換えできます)
async/await は単なるシンタックスシュガーなので、すべて Promise.then() に展開できます。

あなたのコードを then版に書き換えるとこうなります👇


🔁 then版(書き換え)

JavaScript

import fs from "fs";  
import sharp from "sharp";  
  
export default class ImageInfo {  
  
  constructor(ext, width, height) {  
    this.id \= null;  
    this.outFileName \= null;  
    this.ext \= ext ? ext.toLowerCase() : null;  
  
    this.width \= width ?? \-1;  
    this.height \= height ?? \-1;  
  
    this.outWidth \= \-1;  
    this.outHeight \= \-1;  
  
    this.isCover \= false;  
    this.rotateAngle \= 0;  
  }  
  
  // =========================  
  // static 生成メソッド  
  // =========================  
  
  /\*\* ファイルから画像情報を生成 \*/  
  static getImageInfoFromFile(imageFile) {  
    return fs.promises.readFile(imageFile)  
      .then(buffer => {  
        return ImageInfo.getImageInfo(buffer);  
      });  
  }  
  
  /\*\* 画像ストリーム / Buffer から画像情報を生成 \*/  
  static getImageInfo(input) {  
    if (!input) return Promise.resolve(null);  
  
    return sharp(input).metadata()  
      .then(metadata => {  
        if (!metadata || !metadata.format) return null;  
  
        return new ImageInfo(  
          metadata.format,  
          metadata.width,  
          metadata.height  
        );  
      })  
      .catch(() => {  
        return null;  
      });  
  }  
  
  /\*\* 既に幅高さが分かっている場合 \*/  
  static getImageInfoFromRaw(ext, width, height) {  
    return new ImageInfo(ext, width, height);  
  }  
  
  // =========================  
  // getter / setter  
  // =========================  
  
  getId() { return this.id; }  
  setId(id) { this.id \= id; }  
  
  getOutFileName() { return this.outFileName; }  
  setOutFileName(file) { this.outFileName \= file; }  
  
  setExt(ext) { this.ext \= ext?.toLowerCase(); }  
  getExt() { return this.ext; }  
  
  getFormat() {  
    if (!this.ext) return null;  
    return "image/" + (this.ext \=== "jpg" ? "jpeg" : this.ext);  
  }  
  
  getIsCover() { return this.isCover; }  
  setIsCover(isCover) { this.isCover \= isCover; }  
  
  getWidth() { return this.width; }  
  setWidth(width) { this.width \= width; }  
  
  getHeight() { return this.height; }  
  setHeight(height) { this.height \= height; }  
  
  getOutWidth() { return this.outWidth; }  
  setOutWidth(outWidth) { this.outWidth \= outWidth; }  
  
  getOutHeight() { return this.outHeight; }  
  setOutHeight(outHeight) { this.outHeight \= outHeight; }  
}

⚠️ 注意点(重要)

① 戻り値は Promise のまま

JavaScript

const info \= ImageInfo.getImageInfoFromFile(path);

これは Promise なので、

JavaScript

info.then(i => console.log(i));

のように使う必要があります。


awaitthen の対応関係

async/await then
const x = await A A.then(x => ...)
try/catch .catch()
return return Promise.resolve(...) or then内return

③ null返しの扱い

JavaScript

if (!input) return Promise.resolve(null);

これを書かないと 同期値とPromiseが混ざるので注意。


👍 どっちが良いか(実務視点)

正直に言うと:

  • async/await → 可読性が高い(推奨)

  • then → コールバック地獄になりやすい

ただし今回みたいに:

  • 既存コードが then ベース

  • async がバグ原因(無限ループなど)

👉 こういう場合は then に統一するのはアリです


💡 追加アドバイス(今回の文脈的に重要)

あなたの過去ログを見ると:

async/awaitで無限ループになった

これはかなりの確率で👇

  • await漏れ

  • 非同期関数を同期的に再帰

  • Promiseをreturnしてない

が原因です。


必要なら

  • 「async版のままでバグ直す」

  • 「全体をthenに統一」

  • 「非同期処理の設計見直し」

このへん一緒に整理できます。

You:

async awaitの汚染を減らすには

ChatGPT:

いい質問です。これ、Node.jsで規模が大きくなるとかなり重要な設計ポイントです。

結論から言うと
「非同期を“境界に閉じ込める”」のが一番効きます。


🧠 async/await汚染の正体

一度 async を使うと:

JavaScript

function A() {  
  return B(); // BがasyncならPromiseになる  
}

👉 呼び出し元も全部 async にしないといけない

これが「汚染」です。


🧩 対策①:非同期を“入口だけ”にする(最重要)

❌ 悪い例(汚染拡大)

JavaScript

async function parse() {  
  const info \= await ImageInfo.getImageInfoFromFile(path);  
  return process(info);  
}  
  
async function main() {  
  const result \= await parse();  
}

✅ 良い例(境界で止める)

JavaScript

function parse(buffer) {  
  // ここは完全同期  
  return process(buffer);  
}  
  
function main() {  
  return ImageInfo.getImageInfoFromFile(path)  
    .then(buffer => parse(buffer));  
}

👉 非同期は外側だけ
👉 中は全部同期関数にする


🧩 対策②:データ取得と処理を分離

これかなり効きます👇

❌ よくある構造

JavaScript

async function convert(path) {  
  const info \= await getImageInfo(path);  
  return resize(info);  
}

✅ 分離する

JavaScript

function resize(info) {  
  return ... // 同期処理  
}  
  
function convert(path) {  
  return getImageInfo(path)  
    .then(info => resize(info));  
}

👉 I/O(非同期)とロジック(同期)を分離


🧩 対策③:Promiseを“返すだけ”の関数にする

JavaScript

function getInfo(path) {  
  return fs.promises.readFile(path)  
    .then(buf => sharp(buf).metadata());  
}

👉 asyncを使わないことで
👉 呼び出し側が「awaitするかthenするか」選べる


🧩 対策④:トップレベルだけ async

JavaScript

async function main() {  
  const info \= await getInfo(path);  
  console.log(info);  
}  
  
main();

👉 内部は全部thenでもOK
👉 asyncはここだけ


🧩 対策⑤:同期風APIを作る(ラッパー)

頻出パターンはこれ👇

JavaScript

function getImageInfoSafe(path) {  
  return getImageInfo(path).then(x => x ?? defaultValue);  
}

👉 呼び出し側の分岐を減らす
👉 async汚染も減る


🧩 対策⑥:バッチ化(かなり効く)

JavaScript

Promise.all(files.map(f => getImageInfo(f)))  
  .then(results => {  
    // ここは同期っぽく処理できる  
  });

👉 await連鎖を減らせる


🎯 実務でのベスト構成

あなたのケース(画像処理+EPUB生成)だと👇

\[非同期層\]  
  - ファイル読み込み  
  - 画像メタ取得(sharp)  
  
\[同期層\]  
  - EPUB構造生成  
  - HTML生成  
  - 文字処理

👉 ImageInfo は「非同期層の端」に置くのがベスト


🚨 よくあるNGパターン

① asyncの連鎖

JavaScript

async function A() {  
  return await B(); // ←無意味  
}

👉 これはただの汚染


② await in ループ

JavaScript

for (const f of files) {  
  await process(f);  
}

👉 並列にできるのに直列になる


③ Promise返してない

JavaScript

function A() {  
  fs.promises.readFile(...).then(...);  
}

👉 呼び出し元が制御不能


💡まとめ

汚染を減らす5原則

  1. 非同期は入口・出口だけ

  2. ロジックは全部同期関数に分離

  3. asyncよりthenを境界で使う

  4. Promiseをそのまま返す

  5. 並列化できるところはPromise.all


🔧 あなたのコードに対する最適解

今回の ImageInfo はかなり良い位置にいて:

👉 getImageInfo だけ非同期
👉 他は全部同期

→ すでに理想構造に近いです


もし希望あれば:

  • EPUB全体の非同期設計レビュー

  • 無限ループの原因特定

  • sharp周りの最適化(ここ地雷多い)

このあたりかなり深く見れます。

Is this conversation helpful so far?