🌽

Promiseの結果をキャッシュして効率的な非同期処理を実現する

に公開

Promiseのキャッシュを活用する方法について記載します。

Promiseは一度実行された結果を保持する特性があり、これを利用してキャッシュを実現できます。

Promiseの基本的な特性

Promiseは、一度実行されるとその結果(fulfilled または rejected)を保持します。

Promiseオブジェクトを生成し返す関数を実行した場合、毎回Promiseが実行されます

const createPromise = () => {
    return new Promise((resolve) => {
        console.log("処理を実行中...");
        setTimeout(() => resolve("結果"), 1000);
    });
};

const result1 = await createPromise(); // Promiseが実行される
const result2 = await createPromise(); // Promiseが実行される

Promiseオブジェクトを変数に保持して再利用する場合、二回目以降の呼び出しではPromiseは実行されず、キャッシュされた結果が返却されます。

// Promiseを変数に保持
const stockedPromise = new Promise((resolve) => {
    console.log("処理を実行中...");
    setTimeout(() => resolve("結果"), 1000);
});

const result1 = await stockedPromise; // Promiseが実行される
const result2 = await stockedPromise; // キャッシュされた結果が返却される

実践的な例:ユーザー取得APIのレスポンスキャッシュ

ユーザー取得APIのPromiseをキャッシュする例です。

class ApiService {
    private userCache = new Map<number, Promise<User>>();

    async getUser(id: number): Promise<User> {
        if (!this.userCache.has(id)) {
            this.userCache.set(id, 
                fetch(`/api/users/${id}`)
                    .then(res => res.json()).catch((error) => {
                        this.userCache.delete(id); // エラー時にキャッシュを削除
                        throw error; 
                    })
            );
        }
        return this.userCache.get(id)!;
    }
}

同じユーザーIDで複数回呼び出されたとしても、実際のAPIリクエストは1回だけ実行されます。

const apiService = new ApiService();

// 最初の呼び出し:APIリクエストが実行される
const user1 = await apiService.getUser(1);
// 2回目の呼び出し:キャッシュされた結果が返される
const user2 = await apiService.getUser(1);

注意点

Promiseのキャッシュを利用する際には以下の点に注意が必要です。

キャッシュの保持期間

キャッシュされたPromiseは、明示的にリセットされない限り保持されます。

必要に応じて有効期限を設定してください。

let cachedPromise: Promise<string> | null = null;
let cacheTimestamp: number | null = null;
const CACHE_DURATION = 60000; // キャッシュの有効期間(ミリ秒)

const fetchData = async (): Promise<string> => {
    const now = Date.now();
    if (!cachedPromise || !cacheTimestamp || now - cacheTimestamp > CACHE_DURATION) {
        cachedPromise = new Promise((resolve) => {
            setTimeout(() => resolve("データ取得完了"), 2000);
        });
        cacheTimestamp = now;
    }
    return await cachedPromise;
};

reject 状態の管理

一度 reject されたPromiseについても、その状態がキャッシュされてしまいます。

エラーが発生した場合はキャッシュをリセットして再試行するなどの工夫が必要です。

let cachedPromise: Promise<string> | null = null;

const fetchData = async (): Promise<string> => {
    if (!cachedPromise) {
        cachedPromise = new Promise((resolve, reject) => {
            reject(new Error("エラーが発生しました"));
        });
    }
    try {
        return await cachedPromise;
    } catch (error) {
        cachedPromise = null; // エラー時にキャッシュをリセット
        throw error;
    }
};

おまけ : DIとPromiseキャッシュ

DIを使ってPromiseを注入する場合の挙動について簡単に触れます。

ConstructorでPromiseを生成し、シングルトンとして管理する場合、キャッシュされた結果を再利用できます。

import { Injectable } from '@nestjs/common';

@Injectable()
export class CachedPromiseService {
    private readonly promise: Promise<string>;

    constructor() {
        this.promise = new Promise((resolve) => {
            console.log("処理を実行中...");
            setTimeout(() => resolve("結果"), 1000);
        });
    }

    async getPromiseResult(): Promise<string> {
        return await this.promise; // キャッシュされた結果を返す
    }
}

import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
    constructor(private readonly cachedPromiseService: CachedPromiseService) {}
    async getResult(): Promise<string> {
        const a = await this.cachedPromiseService.getPromiseResult(); // Promiseが実行される
        const b = await this.cachedPromiseService.getPromiseResult(); // キャッシュされた結果が返される
        return a;
    }
}

Promiseを管理しない場合は、メソッドを呼び出すたびに新しいPromiseを生成されるので、キャッシュは効きません。

import { Injectable } from '@nestjs/common';

@Injectable()
export class NonCachedPromiseService {
    constructor() {}

    async getPromiseResult(): Promise<string> {
        return new Promise((resolve) => {
            console.log("処理を実行中...");
            setTimeout(() => resolve("結果"), 1000);
        });
    }
}


import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
    constructor(private readonly nonCachedPromiseService: NonCachedPromiseService) {}
    async getResult(): Promise<string> {
        const a = await this.nonCachedPromiseService.getPromiseResult(); // Promiseが実行される
        const b = await this.nonCachedPromiseService.getPromiseResult(); // Promiseが実行される(キャッシュは効かない)
        return a;
    }
}

まとめ

  • Promiseは一度実行されると結果を保持する特性がある。
  • Promiseを変数に保存して再利用することでキャッシュを実現できる。

Discussion