Chapter 03無料公開

Web Budget API

uhyo
uhyo
2021.11.30に更新

いわゆるPWAを支えるWeb標準として、Service Workerが広く利用され、成功を収めています。PWAの特徴としては、オフラインの状況でも動作したり、「インストール」という操作が可能でユーザーによりネイティブアプリに近い体験を与えることができたりといったことが挙げられます。さらにネイティブアプリに近づけていこうという動きは継続しており、この章の執筆時点ではcapabilities projectとして、Googleを中心とした取り組みが進められています。

ネイティブアプリっぽい体験の一つとして、「ブラウザでタブを開いていないときでも裏で動作する」ということが挙げられます。代表的なものがPush APIであり、これはcapabilities projectよりも前からあるわりと古参なAPIです。Push APIにより、サーバーからプッシュされるメッセージに反応して、ブラウザが起動していない状態でも(Service Workerを通じて)JavaScriptコードを実行できます。主なユースケースとして、Notification APIと組み合わせることによって、PWAでいわゆる「プッシュ通知」が実現できます。

この章で紹介するWeb Budget APIは、この文脈の中で発生した提案の一つでした。これは、そのようなバックグラウンドの処理があとどれくらい許されているのか(これをbudgetと呼びます)を取得するためのAPIです。Webというのは(今となっては)セキュリティを気にするプラットフォームですから、いくらユーザーがPWAをインストールしたとはいえ、ユーザーがアプリを開いていないときに無尽蔵に処理が走るのは問題であるという考えでしょう。

Web Budget APIは一時期Blinkに実装されましたが、開発者も他のブラウザベンダも興味を示さなかったことを理由として仕様の検討は終了し、Blinkからも削除されました。そのため、ここで解説する内容は惜しくも役に立ちません。

仕様: Web Budget API. Editor’s Draft, 15 August 2018

Web Budget APIの使い方

Web Budget APIの入り口は、navigator.budgetとして取得できるBudgetServiceオブジェクトです。

const budgetService = navigator.budget;

budgetの取得

BudgetServiceを通じてできることは主に3つです。まず、getBudget()メソッドにより、現在および将来のbudget予測を取得できます。このメソッドの面白い点は、ただ単に現在の情報が得られるだけでなく、(ブラウザがそのような情報を用意してくれていれば)将来の情報も含めて取得できることです。筆者にはいまいち具体的なユースケースが想像できませんが、将来の情報が分かればできることもあるのでしょう。getBudget()の返り値の型は、TypeScriptっぽく書くとPromise<BudgetState[]>です。ただし、BudgetStateは次のようなオブジェクトです。

type BudgetState = {
  readonly budgetAt: number;
  readonly time: number; // DOMTimeStamp
};

このように、ある時点(time)でのbudgetの量(budgetAt)を取得できます。それぞれのBudgetStateは、timeで表される時刻まで有効です。具体的なデータがどんな感じなのかはよく分かりませんが、こんな感じだと想像されます(timeの具体的な数値は脳内で適宜補ってください)。

[
  { budgetAt: 10, time: (1分後) },
  { budgetAt: 20, time: (5分後) },
  { budgetAt: 1.5, time: (1時間) },
]

getBudgetにより返される配列には最低1個の要素があることが仕様上保証されています。ただし、budgetがない場合は最初の要素のbudgetAtが0となります。このことから、現在のbudgetを取得するコードは次のように書けるでしょう。

const [{ budgetAt: currentBudget }] = await navigator.budget.getBudget();

ただし、厳密に言えば、getBudgetが複数要素の配列を返す場合にその要素たちが時刻でソートされているかどうかは保証されていません。そのため、厳密な判定が必要なら事前にtimeでソートする必要がありそうです。

操作のコストの取得

上記のように取得できるbudgetは数値ですが、数値の絶対的な値には意味がありません。別途、やりたい操作のコストを取得し、それを現在のbudgetと比較することで意味を持ちます。操作のコストを取得するにはBudgetServiceのgetCost()メソッドを使用します。このメソッドには、やりたい操作を表す文字列を渡します。

const costForSilentPush = await navigator.budget.getCost("silent-push");

これにより、silent pushという操作(後述)のコストを取得できます。コストもやはり数値であり、これが現在のbudget以上ならばsilent pushを実行可能です。ちなみに、現在仕様で定義されている操作は"silent-push"のみです(現在といっても、もうこの仕様は放棄されたので追加されることはありませんが)。

if (currentBudget >= costForSilentPush) {
  doSilentPush();
}

あるいは、silent pushはバックグランドのタスクなので、今すぐではなくgetBudgetで取得したbudgetの期限に間に合うようにスケジューリングするといったことも可能でしょう。

ちなみに、getCostで得られる値は最大値であると定義されています。つまり、実際に操作を行なってみたらもっと少ないbudget消費で済むかもしれないということです。このことから、操作のコストというのは常に一定ではないということが分かります。例えば、コストというのはおおよそリソースの消費度合いを表すものですから、端末が電源に繋がれているかどうかによって変動するかもしれません(そういうのはbudgetの側の変動で表現される気もしますが)。

budgetの予約

BudgetServiceを通じてできることの最後は、budgetを予約することです。そのためにはreserve()メソッドを使用します。このメソッドには、getCostと同様に操作を表す文字列を渡します。返り値は真偽値で、予約できた場合はtrue、予約できなかった場合はfalseです。このメソッドで予約された操作のコストは残りのbudgetから減らされます。

await reserved = await navigator.budget.reserve("silent-push");

Silent Pushとは何か

これまでの例では、budgetを消費する操作の例としてsilent pushというものが使われてきました。これはWeb Budget APIの策定停止時点で唯一、APIの具体的な対象として想定されていたものです。Silent pushというのは、Push APIを通じてバックグラウンドでコードが実行されたのに、ユーザーに通知を出さないことを指します。これについて深追いすると役に立つ知識となってしまいますが、せっかくなので少し触れておきます。

Push APIは「サーバーからのpushに応じてコードが実行される」というものであり、Notification APIと組み合わせることでサーバーが指示したタイミングで通知を出せる「プッシュ通知」となる一方で、Notification APIを使わなければ「裏でコードが動いただけ」となり、ユーザーはコードが実行されたことに気づかないでしょう。この状況がsilent pushです。

Web Budget API仕様には次のような例が掲載されています。これは「budgetがあればsilent pushとし、budgetが足りなければ仕方ないのでユーザーに見える通知を出す」というコードです。直感的にはユーザーに見える通知を出すことにbudgetが必要なように思えますが、実際のコンセプトは逆で、「通知を出さない」ことでbudgetを消費します。ユーザーに気付かれずに動き続けることが問題であるという考え方ですね。

self.addEventListener('push', event => {
    // Execute the application-specific logic depending on the contents of the
    // received push message, for example by caching the latest content.

    event.waitUntil(
        navigator.budget.reserve('silent-push').then(reserved => {
            if (reserved)
                return;  // No need to show a notification.

            // Not enough budget is available, must show a notification.
            return registration.showNotification(...);
        })
    );
});

Budget APIがWICGに持ち込まれた時のdiscourseがこちらで、silent pushがユースケースとして念頭に置かれていたことが分かります。

https://discourse.wicg.io/t/proposal-budget-api/1717

ただし、このログはmartinthomson氏によるBudget APIのデザインに対する反対意見で締めくくられています。ここで挙げられているような問題が、Budget APIが大成しなかった理由なのかもしれません。

ちなみに、これに対応するようにPush APIにはuserVisibleOnlyというオプションが存在し、push subscriptionの作成時に指定することができます。これをtrueにすることで、サーバーからのpushを受けたら最終的にユーザーに見えるフィードバック(通知など)を発生させますという宣言です。

しかし、実のところ、userVisibleOnlyが指定されていたらどうなるかということはPush APIの仕様には(この章の執筆時点では)書かれていません。この問題を扱った次のissueも現時点ではopenであり、silent pushの仕様上の扱いが定まっていない状況です。

https://github.com/w3c/push-api/issues/3139

このissueによれば、ブラウザによってはそもそもuserVisibleOnlyfalseにすることを許可していなかったり、あるいはuserVisibleOnlyの値にかかわらずsilent pushが実質的に許可されないという挙動になるようです。「silent pushが許可されない」といっても、あるプログラムが通知を表示するかどうかはそのプログラムを実際に実行してみないと分からないので[1]、悪いプログラムがサーバーからpushを受けつつ通知を表示しないということを成し遂げてしまう可能性があります。そのため、そのような場合に事後的にペナルティを与える(ユーザーに強制的に通知を表示するとか、あるいはpush subscriptionを無効化するとか)ようになっているようです。

つまるところ、現状の実装としてはsilent pushは許可されておらず、Push APIは実質プッシュ通知専用であると考えてよいでしょう。

新たな希望: Periodic Background Sync API

Silent pushと似たユースケースを満たすための新たな希望として、Periodic Background Sync APIというのも考えられています。これは冒頭で紹介したcapabilities projectの一員で、まだ役に立つ可能性のある仕様です。

Periodic Background Sync APIは、バックグラウンドでService Workerが起動してプログラムを実行できる機能です。ただし、Push APIとは異なり、アプリケーション側の任意のタイミングで起動できるのではなく、起動のタイミングはブラウザが決定します。Background Syncという名が示す通り、裏で最新のデータを取得するというユースケースを想定しているため、デバイスがオンラインの場合にしか処理は起動しません。

バックグラウンド処理の起動タイミングに関してアプリケーション側が設定できるのはminIntervalだけです。これはその名の通り「最小実行間隔」を表す数値であり、最短でここで指定された間隔でプログラムがバックグランドで実行されます。

以下はPeriodic Background Syncを登録する例です(仕様から引用)。次の例ではminIntervalは1日ですから、最大で1日に1回fetch-newsのためにService Workerが起動します。

async function registerPeriodicNewsCheck() {
  const registration = await navigator.serviceWorker.ready;
  try {
    await registration.periodicSync.register('fetch-news', {
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch {
    console.log('Periodic Sync could not be registered!');
  }
}

「最小実行間隔」ということから分かるように、希望よりも長い間隔でService Workerを起動する権利がブラウザには与えられています。Chromeは、ユーザーからエンゲージメントを得ていないアプリに対しては実行間隔が長くなり、最終的にまったく起動されなくなることもあるとしています。このように、Server Pushはその名の通りpush型のアーキテクチャだったのに対し、Periodic Background Syncはpull型のアーキテクチャである点が特徴です。Silent pushをどのようにアプリケーションに認可するかという問題を、pull型にしてバックグラウンド実行の主導権をブラウザ側に持たせるという方法で解決しています。

Silent pushのユースケースをPeriodic Background Syncで全てカバーすることはできないでしょうが、ある程度はこちらのAPIで解決できそうですね。

まとめ

この章では、バックグラウンドの処理をアプリケーションに認可するための枠組みとして提案されたWeb Budget APIを紹介しました。残念ながらこのAPI自体は役に立たなくなってしまいましたが、バックグラウンドの処理というユースケース自体は存在しており、Periodic Background Sync APIといった仕様に受け継がれています。

余談: この章を書き終わってから気づいたのですが、JxckさんがすでにBudget APIの解説を書いていました(書かれた当時はまだ役に立つ可能性があるAPIだったわけですが)。そのため、この章は無料で見られる章に加えられました。

脚注
  1. これはいかなる手段で検証しても絶対に不可能です。詳しくは「停止性問題」で調べてみましょう。 ↩︎