Supabaseのソースコードから学ぶJavaScriptでのロックの実装方法

JavaScript で非同期処理を扱うとき、「**並列で走ってはいけない処理**」や「**同時実行を制御したい**」ケースがよくあります。
そういったときに有効なのが「**ロック**」の仕組み。

この記事では、**supabase-js の内部実装から学べる async / Promise ベースのロック機構**を紹介しつつ、
僕が実際に読んでいて つまずいたポイントも交えながら詳しく解説します。
## 対象コード：_acquireLock
今回ご紹介するコードはsupabase-jsのcreateClientで生成されるSupabaseClientの認証に関するメソッドを実装しているGoTrueClientから抜粋した private async _acquireLock(...) という関数を紹介します：
```js
private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
  this._debug('#_acquireLock', 'begin', acquireTimeout)

  try {
    if (this.lockAcquired) {
      const last = this.pendingInLock.length
        ? this.pendingInLock[this.pendingInLock.length - 1]
        : Promise.resolve()

      const result = (async () => {
        await last
        return await fn()
      })()

      this.pendingInLock.push(
        (async () => {
          try {
            await result
          } catch (e: any) {}
        })()
      )

      return result
    }

    return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
      this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)

      try {
        this.lockAcquired = true

        const result = fn()

        this.pendingInLock.push(
          (async () => {
            try {
              await result
            } catch (e: any) {}
          })()
        )

        await result

        // keep draining the queue until there's nothing to wait on
        while (this.pendingInLock.length) {
          const waitOn = [...this.pendingInLock]
          await Promise.all(waitOn)
          this.pendingInLock.splice(0, waitOn.length)
        }

        return await result
      } finally {
        this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
        this.lockAcquired = false
      }
    })
  } finally {
    this._debug('#_acquireLock', 'end')
  }
}
```
### 全体像の理解
ざっくり要約すると…
* **同時に複数の処理を走らせたくないときの制御**
* lockAcquired で「**今ロック中かどうか**」を判定
* ロックが取れていないなら、**先に処理を実行しつつ、後続をキューで待たせる**
* pendingInLock という **Promise の配列**を使って、**順番待ちを実現している**

### 最初の処理：ロック未取得の場合
まず、最初のリクエスト（ロックがまだ取られていない）では lockAcquired は false。
以下のブロックが実行されます：
```js
return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
  this.lockAcquired = true
  const result = fn()

  this.pendingInLock.push((async () => { await result })())
  await result

  while (this.pendingInLock.length) {
    const waitOn = [...this.pendingInLock]
    await Promise.all(waitOn)
    this.pendingInLock.splice(0, waitOn.length)
  }

  return await result
})
```
ここがポイント：
| 行                              | 説明                                      |
| ------------------------------ | --------------------------------------- |
| `this.lockAcquired = true`     | ロックを取得済みにする                             |
| `const result = fn()`          | 最初の処理を実行（Promise）                       |
| `this.pendingInLock.push(...)` | **自分自身をキューに登録**                         |
| `await result`                 | 自分の処理が終わるのを待つ                           |
| `while (...)`                  | **追加された処理（あとから来たリクエスト）を順番に全て終わらせるまで待機** |

### 僕がつまずいたポイント：「なんでwhileでキューに何か入ってるの？」
最初はこう思いました：

「ロックがまだ取得されてない＝キューに何もないはずなのに、while(this.pendingInLock.length) が true になるのおかしくない？」

🔍 **理由1：自分自身が push している**
```js
this.pendingInLock.push((async () => {
  try {
    await result
  } catch {}
})())
```

→ 自分自身の処理 result を push しているので、**キューは1件ある状態になる。**
→ これは await result した直後にはすぐに resolve 済みなので、while ではすぐ抜ける。

🔍 **理由2：後続が先に if (this.lockAcquired) に入って追加される可能性**
lockAcquired が true になった時点で、**次の呼び出しは if (this.lockAcquired) 側に入る**。
```js
const last = this.pendingInLock[this.pendingInLock.length - 1]
const result = (async () => {
  await last
  return await fn()
})()
```
→ **このとき、last に今実行中の Promise が入っているので、後続は自然と順番待ちになる。**
そして result が完成した後、this.pendingInLock.push(...) によりまたキューに追加される。

ちなみにこのawait last は pendingInLock の最後に追加された すでに実行済みの Promise を await している。**ここで再度実行を始めているわけではないです。**

### イメージ図（順番）
```txt
// 初回
requestA:
- lockAcquired = false → ロック取得して fn 実行 → 自分をキューに入れる → while 実行

// 次の呼び出し（同時に発生）
requestB:
- lockAcquired = true → last = requestA.result → await last → fn 実行 → 自分をキューに

requestC:
- lockAcquired = true → last = requestB.result → await last → ...
```
→ **最初に来た requestA がすべての完了を見届ける。**
→ requestB / C は順番に result を待ち、同様に push される。

##  まとめ
lockAcquired フラグと pendingInLock 配列を組み合わせて**非同期のロック制御**をしている。

**最初にロックを取った処理が、すべての順番待ち処理の完了まで責任を持つ設計**。

while (this.pendingInLock.length) が機能するのは、自分自身＋後続がそこに push しているから。

## 最後に
このような非同期制御は、普通の JavaScript アプリでも DB アクセス制御やキャッシュ更新の時に活躍します。

Supabase の実装から学べるように、「Promise の使いまわし」「キューで順番待ちを再現」「非同期のロック解除タイミング」など、非常に学びが多いコードでした。