Web Locks API
複数タブが同じIndexedDBやトークン更新を同時に触って壊す事故を防げる。ロック取得の待機規則とリーダー選出への応用を内部動作から整理します。
- 1.navigator.locks.requestは名前付きロックをオリジン単位で管理し、コールバックの実行完了(Promise解決)まで自動的に保持・解放するスコープ付きロックである。
- 2.exclusive/sharedモードとキュー順(原則FIFO)、ifAvailable・stealオプションによる待機回避・強制奪取が可能で、tabが閉じられても確実に解放される。
- 3.同時実行するタブの中から1つだけ処理を担当させるリーダー選出パターンに使え、状態同報が目的のBroadcastChannelとは役割が異なる。
なぜブラウザに排他制御が要るのか
同じオリジンのアプリを複数タブで開くと、各タブは独立した実行コンテキストを持ちながら、IndexedDB や localStorage、同じリフレッシュトークンといった共有資源へ同時にアクセスします。トークンのリフレッシュを全タブが同時に走らせればリフレッシュエンドポイントを多重に叩き、キャッシュの再構築を並走させれば無駄な二重書き込みが起きます。従来はこれを localStorage への書き込みとポーリングや、独自のミューテックス擬似実装で回避してきましたが、いずれもタブがクラッシュした場合の解放漏れや、原子性の欠如が課題でした。Web Locks API(navigator.locks)は、この「オリジン内・複数コンテキスト間の排他制御」をブラウザ本体の責務として提供する専用APIです。
スコープ付きロックという設計
Web Locks API の中心は navigator.locks.request(name, callback) です。特徴的なのは、ロックの取得・解放を明示的な lock()/unlock() 呼び出しではなく、コールバックのライフタイムに紐づけるスコープ付きロックとして設計している点です。
await navigator.locks.request("token-refresh", async (lock) => {
// このコールバックの実行中だけロックを保持
const token = await refreshAccessToken();
await saveToken(token);
}); // コールバックがresolve/rejectした時点で自動解放
callback が返す Promise が解決(成功・失敗いずれも)すると、ブラウザは自動的にロックを解放します。呼び出し側が解放を忘れるという事故が構造的に起こりません。同様に、ロックを保持しているタブが閉じられる・クラッシュする・ページ遷移すると、そのタブに属する実行コンテキストが破棄された時点でブラウザがロックを強制解放します。デッドロックが半永久に残るリスクは、他の自作排他制御より小さくなっています。
ロック名の名前空間はオリジン単位です。同一オリジンであれば、タブ・iframe・Web Worker・Service Workerのどのコンテキストから navigator.locks.request を呼んでも同じロック集合を奪い合います。クロスオリジンには影響しません。
exclusiveとshared、キューの規則
request の第2引数(省略可能なオプション)で mode を指定できます。既定は "exclusive"(排他)で、同名ロックを同時に1コンテキストしか保持できません。"shared" を指定すると、読み取り専用処理のように同時保持を許す共有ロックになります。
// 複数タブが同時に読み取り可能
navigator.locks.request("cache-read", { mode: "shared" }, async () => {
return readFromCache();
});
// 書き込みは排他。shared保持者がいる間は待機する
navigator.locks.request("cache-read", { mode: "exclusive" }, async () => {
return rebuildCache();
});
同じロック名に対する要求はキューに積まれ、**原則として要求された順(FIFO)**で許可されますが、仕様は「実装がリソースの公平性のために順序を変えてよい」余地も残しており、厳密なFIFOを前提にした設計は避けるべきです。sharedモード同士は同時に許可され、exclusiveの要求はキュー内の既存の保持者・先行する要求がすべてはけるまで待たされます。
| モード | 同時保持 | 典型用途 |
|---|---|---|
| exclusive(既定) | 1コンテキストのみ | 書き込み・更新処理の直列化 |
| shared | 複数コンテキスト可 | 読み取り専用処理の並行実行 |
待機を避けるifAvailableと、強制奪取のsteal
既定の request はロックが空くまで待機しますが、待たずに「今すぐ空いていなければ諦める」判定も可能です。ifAvailable: true を指定すると、ロックが即座に取れない場合コールバックには lock 引数として null が渡され、待機せずに即座に解決します。
await navigator.locks.request("token-refresh", { ifAvailable: true }, async (lock) => {
if (lock === null) {
return; // 他のタブが既にリフレッシュ中なので何もしない
}
await refreshAccessToken();
});
もう一つの特殊オプションが steal: true です。これは既存の保持者・待機中のキューをすべて破棄し、強制的にロックを奪取します。奪われた側のコールバックは中断されず実行され続けますが、そのロックへの参照はもはや有効な保持を意味しません。名前の通り緊急避難的な機能で、通常のアプリケーションロジックでは ifAvailable までにとどめ、steal は「明らかに固まった処理を強制的にリセットしたい」限定的な場面に留めるのが妥当です。
steal: true は既存の保持者に通知せず奪います。奪われた側の処理が資源への書き込み途中だった場合、データ破損の危険があります。基本的には ifAvailable や、タイムアウトを自前で AbortController と組み合わせる方式を優先してください。
リーダー選出パターンへの応用
Web Locks API の代表的な応用が**リーダー選出(leader election)**です。複数タブが起動している中で「バックグラウンド同期やWebSocket接続を担当するのは1タブだけにしたい」という要求に対し、各タブが同じ排他ロックを取り合い、取れたタブがリーダーとして振る舞う、という単純な仕組みで実現できます。
function becomeLeaderForever() {
navigator.locks.request("app-leader", async () => {
startWebSocketConnection();
// ロックを握ったまま解放しないPromiseを返し続ける
return new Promise(() => {}); // タブが閉じるまで保持
});
}
becomeLeaderForever();
ここでのポイントは、コールバックがあえて解決しないPromiseを返すことです。ロックはコールバックの生存期間に紐づくため、意図的に解決させなければリーダーであるタブがロックを保持し続け、そのタブが閉じられた瞬間に自動解放され、キューで待っていた次のタブへ即座にリーダー権が移ります。明示的な「離脱通知」やハートビートを自作する必要がなく、ブラウザのコンテキスト破棄検知に選出の継続性を委ねられる点が、この応用の実務的な強みです。
BroadcastChannelとの使い分け
BroadcastChannelとWeb Locks APIは、しばしば同じ「マルチタブ調整」の文脈で語られますが、解決する問題が異なります。BroadcastChannelは状態やイベントを全コンテキストへ同報する通信路であり、誰が処理を担当するかという排他制御の仕組みは持ちません。全タブが同じメッセージを受け取って各自処理すれば、むしろ多重実行になります。一方Web Locks APIは**「誰か1つだけが処理する」という相互排他**を保証しますが、処理結果を他のタブへ伝える通信機能は持ちません。
| 観点 | Web Locks API | BroadcastChannel |
|---|---|---|
| 解決する問題 | 排他制御・直列化・リーダー選出 | 状態やイベントの同報 |
| 同時実行の制御 | する(exclusive/shared) | しない(全員に届く) |
| データの伝達 | なし(コールバック引数のみ) | 構造化複製されたペイロード |
| 典型的な組み合わせ | リーダーだけがWebSocket受信 | 受信結果を全タブへ配布 |
「Web Locks APIでリーダーを1つ選び、リーダーが取得したデータをBroadcastChannelで他タブへ配布する」構成は、Service Workerでのバックグラウンド同期やIndexedDBの一括更新でよく使われる組み合わせです。排他と同報を1つのAPIに背負わせず、役割ごとに分けると設計が単純になります。
まとめ
Web Locks APIは、navigator.locks.request(name, callback) によりコールバックの生存期間へロックの保持・自動解放を紐づける、オリジン単位のスコープ付き排他制御です。既定はexclusiveで同時保持は1つ、sharedを指定すれば読み取り系を並行させられます。キューは原則FIFOですが厳密な順序保証はなく、ifAvailable で待機を回避、steal で強制奪取できますが後者はデータ破損リスクを伴う最終手段です。コールバックを解決させずに保持し続ければ、タブのクラッシュや終了時にブラウザが自動解放してくれる性質を利用して、単純なリーダー選出を実装できます。状態を全タブへ配るBroadcastChannelとは役割が異なり、多くの実務設計では両者を併用します。
Web/フロントエンド Article
Web Locks APIを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
Web Locks API
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 6
導入後に効く点
exclusive/sharedモードとキュー順(原則FIFO)、ifAvailable・stealオプションによる待機回避・強制奪取が可能で、tabが閉じられても確実に解放される。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 6
判断チェックリスト
- 自社の用途が「Web Locks API / 排他制御」に近いか確認する。
- 強みである「navigator.locks.requestは名前付きロックをオリジン単位で管理し、コールバックの実行完了(Promise解決)まで自動的に保持・解放するスコープ付きロックである。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。