アルパカログ

📅  2021-09-27

Notion Blogでビルド時にAPI制限を回避するためにキャッシュを実装する


Notion Blogを自分なりにカスタマイズして最新記事やおすすめ記事などを表示するようになると、APIをたくさんリクエストすることになり、気が付いたらAPIの制限に引っかかってビルドがエラーになってしまうということが起きます。

Notion APIの制限は、2021年9月時点では1秒間に平均3リクエストとなっています。

もしAPI制限に引っかかってしまった場合はエラーコード429 rate_limited となります。

このエントリでは、API制限を回避するためにビルド時にAPIレスポンスをキャッシュする実装を説明します。

キャッシュを使うことでHTTPリクエストが減るため多少の高速化も期待できます。

キャッシュを使ったビルドのフロー

Notion APIのうち、今回は全ての記事の情報をJSONでローカルファイルにキャッシュします。

そして、部分的な記事取得(例えば最新記事N件など)についてもキャッシュが存在する場合にはキャッシュを参照するようにします。

キャッシュストアはローカルファイルを使います。

通常ならファイルのロックを考慮しなければならないところですが、今回はビルドプロセスの最初にキャッシュを生成し終了後に破棄するためロックについては考えません。

ビルドの流れを整理すると下記のようになります。

  1. ビルドの最初にAPIから全記事を取得しローカルファイルにキャッシュする
  2. ビルド時、APIクライアントはキャッシュの有無を確認し、存在すればキャッシュを使う
  3. ビルドの最後でローカルファイルを削除する

キャッシュモジュールの実装

src/lib/notion/blog-index-cache.js というファイル名でキャッシュモジュールを実装していきます。

差分は下記を参照してください。

const fs = require('fs')

const {
  NOTION_API_SECRET,
  DATABASE_ID,
  BLOG_INDEX_CACHE,
} = require('./server-constants')
const { Client } = require('@notionhq/client')

const notionClient = new Client({
  auth: NOTION_API_SECRET,
})

exports.exists = function() {
  return fs.existsSync(BLOG_INDEX_CACHE)
}

exports.get = function() {
  return JSON.parse(fs.readFileSync(BLOG_INDEX_CACHE))
}

exports.set = async function() {
  let params = {
    database_id: DATABASE_ID,
    filter: {
      and: [
        {
          property: 'Published',
          checkbox: {
            equals: true,
          },
        },
        {
          property: 'Date',
          date: {
            on_or_before: new Date().toISOString(),
          },
        },
      ],
    },
    sorts: [
      {
        property: 'Date',
        timestamp: 'created_time',
        direction: 'descending',
      },
    ],
    page_size: 100,
  }

  let results = []

  while (true) {
    const data = await notionClient.databases.query(params)

    results = results.concat(data.results)

    if (!data.has_more) {
      break
    }

    params['start_cursor'] = data.next_cursor
  }

  fs.writeFileSync(BLOG_INDEX_CACHE, JSON.stringify(results))
  console.log(`Cached ${results.length} posts into ${BLOG_INDEX_CACHE}`)

  return
}

exports.expire = function() {
  return fs.unlinkSync(BLOG_INDEX_CACHE)
}

ポイントとしては、ビルドの最初、つまりTypeScriptのトランスパイル前に実行されるモジュールなので、JavaScriptで記述しなければならないということです。

そのためファイルの拡張子も .js になっています。

次に、ビルドの最初にキャッシュを生成するために呼び出すスクリプト scripts/set-blog-index-cache.js と、ビルド後にキャッシュを破棄するために呼び出すスクリプト scripts/expire-blog-index-cache.js を追加します。

差分は下記を参照してください。

const blogIndexCache = require('../src/lib/notion/blog-index-cache.js')

blogIndexCache.set()

const blogIndexCache = require('../src/lib/notion/blog-index-cache.js')

blogIndexCache.expire()

ビルドコマンドの変更

Next.js のビルドは、特に指定がなければ next build が使われますが package.jsonbuild コマンドを追加することで変更することができます。

そこで先ほど作成したキャッシュ生成・破棄のスクリプトをビルドコマンドに加えます。

ビルドコマンドは下記のようになります。これを package.json に追加します。

node scripts/set-blog-index-cache.js && next build && node scripts/expire-blog-index-cache.js

これでビルドの開始時にキャッシュが生成され、ビルド後にキャッシュが破棄されるようになりました。

ちなみに今回は .blog_index_data というファイル名でキャッシュしていますが、このファイル名は元々 Notion Blog で使われていたため、削除されないように next.config.js を修正しておきましょう。

- try {
-   fs.unlinkSync(path.resolve('.blog_index_data'))
- } catch (_) {
-   /* non fatal */
- }

APIクライアントの実装

ローカルファイルにキャッシュされるようになったので、キャッシュがある場合にキャッシュを使用するようにAPIクライアントを実装していきましょう。

このブログでは下記のように実装しています。

export async function getAllPosts() {
  let results = []

  if (blogIndexCache.exists()) {
    // キャッシュがある場合に参照する
    results = blogIndexCache.get()
    console.log('Found cached posts.')
  } else {
    // キャッシュがない場合にAPIリクエストする
  }
}

APIクライアントを実装したら、一度ローカルでスクリプト node scripts/set-blog-index-cache.js を実行しキャッシュを生成しましょう。

APIクライアントが上手く実装できていれば yarn dev などでローカルサーバーを起動してアクセスした際にキャッシュが使われているのが確認できるはずです(同じ実装なら Found cached posts. がログに現れる)。

ローカルで動作確認できればデプロイも行ってみてください。私の場合はデプロイも多少早くなりました。

以上です。

このエントリでは、API制限を回避するためにビルド時にAPIレスポンスをキャッシュする実装を説明しました。