QR Send

スマホとPCのChrome間で、URLやテキストをQRコード1回のペアリングで送り合える自作Chrome拡張。アカウント不要・無料。

使ってみる →

Chrome拡張 Manifest V3のservice workerはすぐ寝る:chrome.alarmsで定期処理を成立させる

個人開発

background.jsのsetIntervalが、いつの間にか止まっている

Chrome拡張のbackground.jsでsetIntervalを使って定期処理を組んだつもりが、リリースしてしばらく経つと通知が全然来ない。ログを見ても特にエラーは出ていない。Manifest V3(MV3)の拡張を書いていれば一度はハマるやつだと思います。私も最初これで丸一日溶かしました。

原因はManifest V2までの前提を引きずっていたことです。MV2まではbackground.htmlのような常時起動しているページを裏に1枚持たせておけて、そこでsetIntervalを回しておけば延々とポーリングし続けられました。ところがMV3ではこの永続ページ自体が廃止されています。

service workerは寝る

MV3のbackground処理は「service worker」というイベント駆動の仕組みに置き換わっています。イベントが来たときだけ起動し、何もイベントがなければ数十秒ほどで自動的に終了(terminate)してしまいます。常に生きている前提でコードを書くと、ここで静かに壊れます。

// これはMV3では動作が保証されない
setInterval(() => {
  checkForNewMessages();
}, 30000);

service workerが起動している間はこのタイマーも動くのですが、数十秒操作がないとservice worker自体が終了し、setIntervalで仕込んだタイマーも一緒に消えてしまいます。次に何らかのイベントでservice workerが再起動しても、setIntervalはもう一度呼び直さない限り復活しません。テスト中は一応動いているように見えるので気づきにくく、リリース後にしばらく放置していたら発覚する、という厄介なバグです。

chrome.alarmsで確実に起こしてもらう

MV3で定期処理をやりたいなら、chrome.alarms APIを使いましょう。OS側のアラーム機構でservice workerを指定した間隔で起こしてくれる仕組みで、service workerが休止していても指定時刻になればChromeがきちんと叩き起こしてくれます。

ここで注意です。chrome.alarmsの最短周期は30秒で、それより短い間隔を指定しても30秒に丸められます。数秒おきのリアルタイム処理には向きませんが、「一定間隔でサーバーを確認する」程度のポーリング用途なら十分です。

まずmanifest.jsonでalarms権限とservice workerを宣言します。コピペで大丈夫です。

{
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  },
  "permissions": ["alarms", "storage"]
}

background.js側では、拡張のインストール時にアラームを登録しておきます。

chrome.runtime.onInstalled.addListener(() => {
  chrome.alarms.create("poll-check", { periodInMinutes: 0.5 });
});

periodInMinutes: 0.5が30秒周期にあたります。これでChromeが30秒ごとにservice workerを起こし、chrome.alarms.onAlarmイベントを発火してくれます。chrome://extensionsのservice workerのログに30秒おきの起動が出ていれば成功です。

service workerはグローバル変数を覚えていてくれない

ここで見落としがちなのが、service workerはグローバル変数に状態を持たせても、休止のたびに消えてしまう点です。前回のチェックでどこまで処理したかというカーソルやタイムスタンプをただの変数に入れていると、次に起こされたときには初期化された状態からスタートしてしまいます。

これを避けるには、保持したい状態は毎回chrome.storage.localに書き込んでおき、起こされるたびにそこから読み直す設計にする必要があります。service worker自身は「状態を持たない実行環境」だと割り切って、状態の置き場所はstorageに寄せてしまいましょう。

実例:一定間隔でサーバーの新着を確認する

自作のChrome拡張「QR Send」(https://qrsend.app)でも、この構成をそのまま使っています。スマホから送られてきたURLを中継サーバー経由で受け取る仕組みで、PC側の拡張がservice workerとして30秒おきに起こされ、新着がないか確認し、あれば新しいタブとして開くという流れです。

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name !== "poll-check") return;

  const { lastCheckedAt } = await chrome.storage.local.get("lastCheckedAt");
  const since = lastCheckedAt || 0;

  const res = await fetch(`https://example.com/messages?since=${since}`);
  const messages = await res.json();

  for (const message of messages) {
    chrome.tabs.create({ url: message.url });
  }

  await chrome.storage.local.set({ lastCheckedAt: Date.now() });
});

ポイントは、アラームが発火するたびにchrome.storage.localから前回チェック時刻を読み直し、処理が終わったら次回のために書き戻しているところです。service worker自体がその間に何度休止と再起動を繰り返していても、状態はstorageの中にあるので影響を受けません。新着があればタブが開く、というところまで確認できたら成功です。

通知を出したい場合はchrome.notificationsを、新着をバッジで示したい場合はchrome.action.setBadgeTextを、同じonAlarmリスナーの中に足していくだけで済みます。

まとめ

MV3のbackgroundはイベント駆動のservice workerに変わり、数十秒で自動終了する。setIntervalはその休止に巻き込まれて消えるため定期処理には使えず、休止していても指定間隔で起こしてくれるchrome.alarms(最短30秒)で代替する。あわせて、グローバル変数に持たせた状態も休止のたびに消えるので、保持したい値はchrome.storage.localに書き込んで起こされるたびに読み直す設計にしておく。この2つを押さえておけば、ポーリング系の処理はMV3でも普通に組める。

コメント

タイトルとURLをコピーしました