MV3 Extension Service Worker Async Init When Waking Up

587 views
Skip to first unread message

avishai lazar

unread,
Nov 24, 2022, 8:59:00 AM11/24/22
to Chromium Extensions
TL;DR

Is it possible to systematically delay an event processing until async init work is done? I know I can check if a global init work promise was resolved on every listener callback before proceeding to the execution but it is tedious and easy to forget when the next event listener callback will be introduced.

THE LONGER VERSION

Our service worker is doing some async work during its init. Only when init is done we register all the listeners so we will be able to assume when the listener will be called, everything is ready.

but, what happens when the service worker becomes inactive and then being wake up by an event?

The "main" background.js will run and right after it, the listener will run. Every async work done during the init will happen "in parallel" to the listener execution which causes a nasty race condition between the async init work done and the actual execution of the listener of the event that woke the SW.

I'm looking for a clean and systematically solution that will ensure that our async init is done before staring handling the event that woke up the service worker.

Thanks! 

wOxxOm

unread,
Nov 24, 2022, 10:04:36 AM11/24/22
to Chromium Extensions, avish...@gmail.com
Since top-level await is intentionally disabled for service workers in Chrome the only solution is to wait inside each event listener.

To avoid forgetting you can write a rule for ESLint using `no-restricted-syntax` (no runtime overhead) or make a runtime Proxy wrapper around `chrome` object that automatically augments the addListener function:

let busy = (async () => {
  await new Promise(setTimeout);
  busy = null;
})();

self.Chrome = (() => {
  const handler = {
    get: (src, key) => {
      const val = src[key];
      if (key === 'addListener' && typeof val === 'function') {
        return (fn, ...filters) => {
          src[key](
            (...res) => busy ? busy.then(() => fn(...res)) : fn(...res),
            ...filters
          );
        };
      }
      return val && typeof val === 'object' && /^[a-z]/.test(key)
        ? new Proxy(val, handler)
        : val;
    },
  };
  return new Proxy(chrome, handler);
})();

wOxxOm

unread,
Nov 24, 2022, 10:06:01 AM11/24/22
to Chromium Extensions, wOxxOm, avish...@gmail.com
Typo: it should be self.chrome not self.Chrome

wOxxOm

unread,
Nov 24, 2022, 10:14:23 AM11/24/22
to Chromium Extensions, wOxxOm, avish...@gmail.com
Hey, it can be optimized to restore the original `chrome` when initialization is complete:

let busy = (async () => {
  const {chrome} = self;
  const chromeHandler = {
    has: (src, key) => {

      const val = src[key];
      if (key === 'addListener' && typeof val === 'function') {
        return (fn, ...filters) => {
          src[key](async (...res) => (await busy, fn(...res)), ...filters);

        };
      }
      return val && typeof val === 'object' && /^[a-z]/.test(key)
        ? new Proxy(val, chromeHandler)
        : val;
    },
  };
  self.chrome = new Proxy(chrome, chromeHandler);
  await initFoo();
  await initBar();
  busy = null;
  self.chrome = chrome;
})();

avishai lazar

unread,
Nov 24, 2022, 10:23:39 AM11/24/22
to Chromium Extensions, wOxxOm, avishai lazar
Thanks for your detailed answer. I was about to comment here with another solution I found to monkey patch addEventListener of every API we use but it looks like your solution should "catch-all". Now I'm fighting to try and make it work nicely with TypeScript...

Jackie Han

unread,
Nov 24, 2022, 11:20:20 AM11/24/22
to avishai lazar, Chromium Extensions, wOxxOm
Last year, I initiated a discussion on this issue. I hope there is a "setup" stage in service worker. Currently, I still use `await initPromise` in listeners to handle it.

A global init promise is still useful for some cases. For example, when a user changes a setting, you need to update the init promise to a new promise (do "initPromise = init()" again).

--
You received this message because you are subscribed to the Google Groups "Chromium Extensions" group.
To unsubscribe from this group and stop receiving emails from it, send an email to chromium-extens...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/chromium-extensions/2362694c-c84f-40f1-9951-e8cd08893e5fn%40chromium.org.

avishai lazar

unread,
Nov 27, 2022, 8:57:22 AM11/27/22
to Chromium Extensions, wOxxOm, avishai lazar
Sadly your interesting approach doesn't work since chrome checks for some APIs that you trigger the function on a specific object type.

For example, when I'm proxying chrome.storage.local and then trigger the get function, since chrome.storage.local is now a Proxy object I'm getting the error: Illegal invocation: Function must be called on an object of type StorageArea.

Any idea how I can get around it with catch all solution?

On Thursday, November 24, 2022 at 5:14:23 PM UTC+2 wOxxOm wrote:

wOxxOm

unread,
Nov 27, 2022, 9:05:10 AM11/27/22
to Chromium Extensions, avish...@gmail.com, wOxxOm
There might be a typo in my example, but the Proxy approach is definitely working (including chrome.storage) and is used in live extensions e.g. Stylus or Violentmonkey. BTW you don't have to use a Proxy, you can recursively enumerate the entire `chrome` object tree and just patch all addListener properties.

Appliances Choices

unread,
Nov 27, 2022, 1:31:15 PM11/27/22
to Chromium Extensions, Chromium Extensions
i am also facing same issue on my site pickleballhop pickleballhop is providing reviews and guides on pickleball.

avishai lazar

unread,
Nov 27, 2022, 3:29:17 PM11/27/22
to Chromium Extensions, wOxxOm, avishai lazar
I went with a super slim version of your solution just to demonstrate the problem. You can copy paste it into any background SW console and then call chrome.storage.local.get([]) :

```javascript
const chromeHandler = {
  get: (target, property) => {
    if (typeof target[property] === 'function') {
      return target[property];
    }

    return new Proxy(target[property], chromeHandler);
  },
};

chrome = new Proxy(chrome, chromeHandler);
```

I've also checked the extension you mentioned. They did some not trivial stuff to make it work.

Anyway, I went with the wrapper solution, it looks like it works well. I had also to maintain a book keeping between the original callbacks and the actual wrappers so removeListener will work.

wOxxOm

unread,
Nov 27, 2022, 3:40:06 PM11/27/22
to Chromium Extensions, avish...@gmail.com, wOxxOm
Looking at their source code I see just one additional thing - they bind each function property to the original `target`.
Given the amount of overhead for unrelated methods it might be better to simply enumerate and patch all addListener properties once.

avishai lazar

unread,
Nov 30, 2022, 5:06:10 AM11/30/22
to Chromium Extensions, wOxxOm, avishai lazar
to wrap things up, I've decided not to proceed with this approach since during the init flow we do need to call addListener which creates a deadlock so this implicit approach is not good to my usecase.

avishai lazar

unread,
Nov 30, 2022, 5:08:40 AM11/30/22
to Chromium Extensions, avishai lazar, wOxxOm
to clarify my previous comment, we need to call addListener and waiting to the event and running the listener is part of the init flow. but we can't run the listener because the init flow was not completed (we are in the middle of it).

wOxxOm

unread,
Nov 30, 2022, 9:25:45 AM11/30/22
to Chromium Extensions, avish...@gmail.com, wOxxOm
You can patch addEventListener separately, before initialization.

wOxxOm

unread,
Nov 30, 2022, 9:26:04 AM11/30/22
to Chromium Extensions, wOxxOm, avish...@gmail.com
typo: addListener*

Imperishable Night

unread,
Nov 30, 2022, 8:33:26 PM11/30/22
to Chromium Extensions, wOxxOm, avish...@gmail.com
If I use this approach, will each event be handled "atomically" and in the order in which they fired in the first place? I'm writing a stateful application and the order of events may matter a lot.

Imperishable Night

unread,
Nov 30, 2022, 8:50:36 PM11/30/22
to Chromium Extensions, Imperishable Night, wOxxOm

Also, I guess if my app registers every listener in one top-level file then there is no need to use Proxy, as I can simply define my own addListener function and make sure to use that?

wOxxOm

unread,
Dec 1, 2022, 3:24:52 AM12/1/22
to Chromium Extensions, mprsh...@gmail.com, wOxxOm
Patching addListener doesn't call it, so there's no problem in doing it separately before initialization.
Using a dedicated addListener is also a solution, of course.

Reply all
Reply to author
Forward
0 new messages