同期エンジンの仕組み
このページでは、WCPOS v1.10.0 で導入された同期エンジンについて説明します。それ以前のバージョンは、画面ごとに異なるレプリケーションモデルを使用しています。ページ末尾のv1.10.0での変更点をご覧ください。
WCPOSはローカルファーストです。すべての画面はデバイス上のデータベースに対して読み書きを行い、同期エンジンがそのデータベースとWooCommerceストアをバックグラウンドで収束させ続けます。このページでは、エンジンが何を、いつ取得するかをどのように判断し、売上がどのようにサーバーへ戻るのかを説明します。開発者、インテグレーター、そしてPOSがホスティングに与える負荷を理解したいストアオーナーに役立つ詳細度でまとめています。
ストアとキャッシャーごとに1つのエンジン
POSは各デバイス上で、サイト+ストア+キャッシャーの組み合わせごとに1つの同期エンジンを実行します。エンジンは自分専用のローカルデータベースを保有するため、ストアやキャッシャーを切り替えると、共有された1つのレコードの山をフィルターするのではなく、データプレーン全体が切り替わります。キャッシャーごとの分離は意図的な設計です。同じデバイス上の2人のキャッシャーがローカルデータを共有することはありません。
アップグレード時にローカルデータベースがその場で移行されることはありません。アプリは新しいデータベースを作成し、常に正となるコピーを保持しているサーバーからダウンロードし直します(v1.9からのアップグレードを参照)。
Proのマルチストア構成では、すべての同期リクエストが対象のストアを識別します。そのため、レジで編集した価格はWebストアの価格ではなく、そのストアの価格を更新します。
バックグラウンドで実行される処理
エンジンがスケジュールに沿って行う処理はすべてレーンです。レーンとは、名前が付けられ、範囲が限定されたバックグラウンド処理の単位です。レーンは3つのグループに分かれます。
| グループ | レーン | 既定の実行間隔 |
|---|---|---|
| プル | 変更チェック(変更シグナル) | 10秒〜5分(同期プリセットで設定) |
| 最近の注文、商品カタログのシード、参照データのシード | 約5分 | |
| 顧客のトリクル(アイドル時のみ) | 約5分 | |
| プッシュ | 書き込みドレイン — キューに入ったローカルの変更を送信 | 約10秒 |
| メンテナンス | 整合性および削除の監査、サーバー合計数の更新 | 数分〜約17分ごと |
すべてのレーンには、2つの性質が共通しています。
- 各レーンは1回の実行あたりのリクエスト上限を宣言します。 どのレーンも1回の実行で無制限にリクエストを増やすことはできません。大きなジョブは、再開可能なカーソルを使った上限付きのバッチで処理されます。これは中核的な設計上の不変条件であり、同期のパフォーマンスで詳しく説明しています。
- メンテナンスはキャッシャーに道を譲ります。 監査やバックグラウンドの事前読み込みは、レジが販売できる状態になるまでの対話的な処理の後に実行され、その前に実行されることはありません。また、サーバーに負荷の兆候が見られたときに最初に一時停止されるのもこれらの処理です。
上記の間隔は既定値です。チェック間隔とリクエストあたりのレコード数は、POSの ストアヘルス → パフォーマンス からデバイスごとに販売者が調整できます。ストアヘルスをご覧ください。
POSが変更を検知する仕組み
エンジンは、変更があったかどうかを調べるためにデータを再ダウンロードすることはありません。サーバーは変更ログを保持しており、POSは「前回の位置以降に何か動きがあったか?」という1つの問いにだけ答える軽量な変更チェックをポーリングします。
- 1回のチェックで8つのコレクションを網羅します。 商品、バリエーション、税率、顧客、クーポン、カテゴリー、ブランド、タグが対象です。注文は意図的に変更チェックの対象外です。注文の鮮度は専用の「最近の注文」レーンが担っており、そのため商品と注文の更新は異なるリズムで届くことがあります。
- アイドル状態のレジのコストはほぼゼロです。 エンジンは条件付きリクエストを送信します。何も変更がなければ、サーバーはボディのない
304 Not Modifiedを1つ返すだけです。動きのないストアでは、1回のチェックにつき小さなレスポンスが1つに落ち着きます。 - チェックには±20%のジッターがかかります。 そのため、1つのサイト上の複数のレジは同期したバーストでサーバーに集中するのではなく、少しずつずれていきます。
- アイドル時の減衰: 10分間操作がないと、チェック間隔が延びます(下限は60秒)。タッチ、キー入力、バーコードスキャンの受理といった実際の操作があれば、すぐに通常の間隔に戻り、即座にキャッチアップのチェックが実行されます。減衰は間隔を延ばすだけで、設定値より速くポーリングすることはありません。
- 数日間閉じられていたレジは、履歴を再生するのではなくベースラインを取り直します。 変更ログがレジの最後の位置から離れすぎている場合、すべての行を再生すると数百件のリクエストが必要になります。そこでエンジンはカーソルを先頭までジャンプさせ、デバイスがすでに保持しているデータの現在のサーバー上の状態を確認し直します。これにより、コストはレジが離れていた期間ではなく、ローカルコピーのサイズに比例します。
画面がデータを取得する仕組み
v1.10.0では、画面が独自に同期を実行することはありません。画面は表示している内容(検索語、フィルター、並び順、ページ)を宣言し、そもそもリクエストが必要かどうかはエンジンが判断します。 各宣言は、次の3つのいずれかに解決されます。
- 取得(Fetched) — エンジンが宣言を満たすために通信を行いました。
- ローカルで応答(Served locally) — デバイスがすでに答えを保持していたか、直近の同一の取得結果でカバーできました。
- 上書き(Superseded) — 画面が先へ進み(スクロールした、フィルターを変更した)、より新しい宣言に置き換えられました。これは通常の動作であり、エラーではありません。
ローカルだけで答えられるフィルターがサーバーまで届くことはありません。また、宣言が成功したからといって、そのコレクションが完全にダウンロードされたことにはなりません。完全性は別途追跡されます(次のセクション)。
すべてが先読みでダウンロードされるわけではなく、起動時にデバイス上に何があるかはコレクションによって異なります。
- シードされるもの — 商品は上限付きのカタログシードによって充填されます。税率は起動時に取得されます(税率がなければPOSはカートの計算ができません)。
- オンデマンド+アイドル時のトリクル — 顧客には先読みのシードがありません。顧客のトリクルはアイドルの間隔ごとに小さなバッチをダウンロードし、キャッシャーが操作中のときは完全にスキップします。新規または変更された顧客は変更チェックを通じて届きます。
- 初回オープン時に取得されるもの — カテゴリー、タグ、ブランド、クーポンは、キャッシャーが最初に開いたときに取得されます。誰も開かないコレクションが、リクエストを発生させることは一切ありません。
さらにバリエーションの選択画面では、開くたびに価格と在庫が1回更新されます。そのため、デバイス上にあるバリエーションは即座に表示されつつ、数日前の古い在庫が表示されることはありません。
「すべてダウンロードされていますか?」— 誠実なカバレッジ
*「5,000件中1,240件の商品」*のような表示には、分母となるサーバー側の合計数が必要です。エンジンはコレクションごとに合計数を管理し(およそ15分ごとに更新されます。通常のsyncレスポンスにすでに合計数が含まれているため、専用のリクエストが必要になることはめったになく、たいていコストはかかりません)、次の厳格な誠実性のルールに従います。
- サーバー合計数が古い、または取得できない場合は*確認中…*と表示され、ローカルの件数がひそかに代用されることはありません。ローカルの分母を使えば常に100%と表示され、この数値が明らかにすべきギャップそのものを隠してしまうからです。
- エンジンが完全性を保証できない場合、判定は不明となり、UIはその数値をローカルの件数として表示します。
- 注文のカバレッジバーは、サーバー上のすべての注文履歴に対して測定されます。一方でレジは意図的に未処理の注文と最近の注文のみを保持します。そのため、正常なレジでも注文は部分的と表示されますが、これは設計どおりです。
これらの数値は ストアヘルス → データベース に表示されます。あわせて、最初の商品がデバイスに届いた時点で切り替わる販売準備完了のマイルストーンも表示されます。オフラインであっても妨げにはなりません。オフラインで販売できることこそが目的だからです。ストアヘルスをご覧ください。
変更がストアに戻る仕組み
ローカルでの書き込み(売上、商品の編集、顧客の更新)はすべて、まずデバイス上の永続的な送信キューに入ります。そしてドレインレーンが数秒ごとにキューをWooCommerceへ送信します。これがオフライン販売を安全にしている仕組みです。接続がない状態で計上した売上はキューに留まり、接続が戻った時点で送信されます。
重要な詳細は次のとおりです。
- カートへの書き込みは注文ごとに直列化されます。 スキャナーの高速な連続読み取りも1件ずつ適用され、同じ商品を繰り返し追加した場合は、2行目をキューに入れるのではなく、重複する行にマージされます。
- 注文の確認応答ではサーバー側のコピーを採用します。 WooCommerceは注文の作成時に明細行へIDを割り当てます。エンジンは確認応答された注文を採用するため、以降の更新は重複行を追加するのではなく、それらの明細行に一致します。採用は保守的に行われ、サーバーがまだ認識していないローカルの編集を上書きすることはなく、注文にのみ適用されます。
- サーバーが恒久的に拒否した書き込みが、ひそかに再試行されることはありません。 その書き込みはサーバー自身が示した理由とともに保留され、ストアヘルス → データベース に*「変更がサーバーに届きませんでした」*として表示されます。選べる操作は明示的な2つです。再送信(現在のレコードの内容からリクエストを組み立て直すため、その後の修正が反映されます)と破棄です。自動的な再試行ループはありません。復旧は常に、目に見える意図的な操作です。販売者向けの手順はストアヘルスをご覧ください。
複数のブラウザータブ
同じストアのWeb版POSを複数のタブで実行することはサポートされています。どのタブからでも売上を計上でき、書き込みは共有キューに追加されますが、送信を行うのは選出された1つのタブだけです(ストア+キャッシャーのスコープごと)。そのタブが閉じられると、ブラウザーは自動的に次のタブを昇格させます。異なるキャッシャーでサインインした2つのタブは別々のスコープとなり、それぞれが自分のキューを管理します。
ローカルストレージ
WebアプリとデスクトップアプリはデータをOPFS(Origin Private File System)ワーカー経由で保存します。iOSアプリとAndroidアプリは、ファイルシステムエンジンを通じて同じオンディスク形式を使用します。4つのプラットフォームすべてが1つのストレージ形式と、同じ破損復旧ツールを共有しています。(それ以前のバージョンでは、WebでIndexedDB、ネイティブでSQLiteを使用していました。)
クエリはデータベース層の内部で実行されます(セレクター、並び替え、ページング)。そのため、表示中のページの行だけがアプリ側に渡ります。1万件の注文からなる合成フィクスチャーでは、このプッシュダウンによって、購読中の書き込み1件あたりの更新コストが約27ミリ秒から約0.06ミリ秒に短縮されました。
v1.9からのアップグレード
v1.10.0はローカルデータベースを移行せず、コールド再同期を実行します。
- 初回起動時、アプリは新しいローカルデータベースを開き、ストアからダウンロードし直します。すべてのデバイスで一度だけ、完全な再ダウンロードが発生します。
- データが失われることはありません。同期されるすべてのデータについて、正となるコピーはWooCommerceサーバーです。未送信の変更は、古いデータが整理される前に送信されるか、画面に表示されます。アップグレードによって未送信の売上が失われることはありません。
- アプリとプラグインは足並みをそろえてリリースされます。 v1.10.0のクライアントは、プラグインのv2同期APIを使用します。どちらか一方だけをアップグレードすると、リクエストは
rest_no_routeエラーで失敗します。このエラーは「もう一方も更新してください」という意味であり、インストールが壊れているわけではありません。
v1.10.0での変更点
| v1.9.x | v1.10.0 | |
|---|---|---|
| 同期の単位 | マウントされた画面のクエリごとに1つのレプリケーションストリーム | サイト+ストア+キャッシャーごとに1つのエンジン |
| 取得のきっかけ | 画面のマウント | 画面が必要なものを宣言すること |
| 変更検出 | 定期的なポーリング+1時間ごとの完全監査 | 条件付き 304 レスポンスを使った変更ログのカーソル |
| サーバー合計数 | 追跡なし | コレクションごとの合計数と、誠実な*確認中…*の状態 |
| 失敗した書き込み | 不透明に再試行 | 永続キュー、目に見える復旧パネル、ひそかな再試行なし |
| Webのマルチタブ | 協調なし | スコープごとに1つの送信タブを選出 |
| ローカル検索 | 単語の前方一致 | 部分一致(3文字以上) |
| ストレージ | IndexedDB(Web)、SQLite(ネイティブ) | 全プラットフォームでOPFS形式のストレージ |
| 同期のチューニング | 固定 | ストアヘルス内のデバイスごとのプリセットとダイヤル |
エンジンの背後にあるパフォーマンスの考え方(リクエスト上限、サーバー負荷時のバックオフ、実測値)については、同期のパフォーマンスをご覧ください。