chrome.offscreen.hasDocument(), what happened to it?

1,639 views
Skip to first unread message

Robbi

unread,
Dec 22, 2022, 7:02:15 PM12/22/22
to Chromium Extensions
I'm playing with chrome.offscreen + web worker using Chrome Canary.
I realized the hasDocument method is not present in the official documentation but I am using it without any problem.
Assuming the team decided to remove that method, what would be the best way to determine if the offscreen document already exists?
Since creating a second document throws an error, an alternative might be to place its creation in a try catch, but I find that inelegant.

TIA

wOxxOm

unread,
Dec 23, 2022, 4:18:55 AM12/23/22
to Chromium Extensions, Robbi
Quoting https://crsrc.org/extensions/common/api/offscreen.idl;l=63

> Determines whether the extension has an active document. TODO(https://crbug.com/1339382): This probably isn't something we want to ship in its current form (hence the nodoc). Instead of this, we should integrate offscreen documents into a service worker-compatible getViews() alternative. But this is pretty useful in testing environments. 

I guess you can provide your feedback in https://crbug.com/1339382

Robbi

unread,
Dec 23, 2022, 1:19:01 PM12/23/22
to Chromium Extensions, wOxxOm, Robbi
Maybe it's better to wait for things to settle down a bit in order to not to run the risk of doing like Penelope...
It would be nice to have some reassurance today about how "justification" and "reasons" will be used in order to not have surprises later when our extensions will be published.
In my opinion, the rule should be: "Today I can indicate a "reason" that is not well tailored to the context, but tomorrow, if this reason becames "a problem" I have time enough to fix things through the system of long-term scheduled deprecations .
By "a problem" I mean either:
  • the rejection of the review process
  • the closing\"not opening" of offscreen document whenever there is no correlation between the stated "reason" and the code used.

Another thing to ask yourself is: "Can I use the same offscreen document for several "reasons"?"
In an extensions of mine I'm using the same html+js for "audio_playback", "dom_parser" (I also use that OSD to manage a web worker).
Could it be an imprudent/contestable choice?
One last question, to display an alert and\or a confirm can DOM_PARSE be fine as "reason"?

wOxxOm

unread,
Dec 23, 2022, 1:24:03 PM12/23/22
to Chromium Extensions, Robbi, wOxxOm
These are valid and important concerns that were quite likely overlooked during design of the API, so it's best if you post your feedback there until it's too late. Once an API settles down it's inordinately hard to convince them to change it even if it's objectively problematic.

Jackie Han

unread,
Feb 6, 2023, 12:31:42 AM2/6/23
to wOxxOm, Chromium Extensions, Robbi
I tried the offscreen api yesterday. I also have the same doubt, how to detect if it exists? There are multiple ways at the moment.

1. try catch
try {
  await chrome.offscreen.createDocument(...)
} catch(e) {
  // doc already there, since it only support one "at present"
}

This way is not forward compatible. If Chrome supports creating multiple offscreen documents in the future, this code will cause serious problems.

2. use private(unstable) hasDocument() method
if (!chrome.offscreen.hasDocument()) {
  await chrome.offscreen.createDocument(...)
}

Using private methods is also risky!

Note: chrome.offscreen.hasDocument() doesn't support a url as parameter.
In my opinion, the signature of this method should be chrome.offscreen.hasDocument(url)

3. send a message
chrome.runtime.sendMessage('hi offscreen, are you there?')
if (no response) { create it; }

4. use Clients api
// in service worker
clients.matchAll().then((clientList) => {
  let exist = false;
  for (const client of clientList) {
    if (client.url === "offscreen.html") {
      exist = true;
      break;
    }
  }

  if (!exist) {
    await chrome.offscreen.createDocument(...);
  }
});



--
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/7a1e24b2-9fb6-49db-86e5-0425b14455efn%40chromium.org.

Robbi

unread,
Feb 6, 2023, 6:09:46 AM2/6/23
to Chromium Extensions, Jackie Han, Chromium Extensions, Robbi, wOxxOm
I created a wrapper function and I am using try-catch logic (although try-catch should always be a last resort).
If they decide to confirm "hasDocument" method, I can quickly modify this function.

function OSCreateDocument(u, j, r) {
    return new Promise((ok, ko) => {
        chrome.offscreen.createDocument({
            'url': u,
            'justification': j,
            'reasons': r
        }).then(ok).catch(e => ko(e))
    })

Jackie Han

unread,
May 12, 2023, 5:26:31 AM5/12/23
to wOxxOm, Chromium Extensions, Robbi
Update a more mature version to avoid concurrency issues.

// Clients api version
let creating;
async function setupOffscreen() {
  let exist = false;
  let clientList = await clients.matchAll();

  for (const client of clientList) {
    if (client.url.endsWith("off_screen.html")) {
      exist = true;
      break;
    }
  }

  if (!exist) {
    if (creating) {
      await creating;
    } else {
      creating = chrome.offscreen.createDocument(...);
      await creating;
      creating = null;
    }
  }
}

async function sendMessageToOffscreenDocument(data) {
  await setupOffscreen();
  chrome.runtime.sendMessage(data);
}


// runtime.getContexts() version, only available in Chrome Canary now.
let creating;
async function setupOffscreen() {
  let contexts = await chrome.runtime.getContexts({contextTypes: ["OFFSCREEN_DOCUMENT"]});
  if (contexts.length === 0) {
    if (creating) {
      await creating;
    } else {
      creating = chrome.offscreen.createDocument(...);
      await creating;
      creating = null;
    }
  }
}

async function sendMessageToOffscreenDocument(data) {
  await setupOffscreen();
  chrome.runtime.sendMessage(data);
}

Sebastian Benz

unread,
May 12, 2023, 8:52:45 AM5/12/23
to Jackie Han, wOxxOm, Chromium Extensions, Robbi
Hey Jackie,

this is fantastic (and thanks a lot for flagging the concurrency issues in our docs)! Would you be interested in creating a PR to update our docs with your sample code?

Thanks!

Sebastian



Dr. Sebastian Benz

Developer Programs Engineer


Google Germany GmbH

Erika-Mann-Straße 33

80636 München


Geschäftsführer: Paul Manicle, Liana Sebastian

Registergericht und -nummer: Hamburg, HRB 86891

Sitz der Gesellschaft: Hamburg


Diese E-Mail ist vertraulich. Falls Sie diese fälschlicherweise erhalten haben sollten, leiten Sie diese bitte nicht an jemand anderes weiter, löschen Sie alle Kopien und Anhänge davon und lassen Sie mich bitte wissen, dass die E-Mail an die falsche Person gesendet wurde. 

     

This e-mail is confidential. If you received this communication by mistake, please don't forward it to anyone else, please erase all copies and attachments, and please let me know that it has gone to the wrong person.


Jackie Han

unread,
May 12, 2023, 9:20:14 AM5/12/23
to Sebastian Benz, wOxxOm, Chromium Extensions, Robbi
Hi Sebastian,

If someone else doesn't come up with a better way, I can create a PR later.

wOxxOm

unread,
May 12, 2023, 9:27:13 AM5/12/23
to Chromium Extensions, Jackie Han, wOxxOm, Chromium Extensions, Robbi, Sebastian Benz
The best way is a simple try-catch, there's no need for all this byzantine trickery because there's no need for multiple offscreen documents, and even in a fantastical scenario it'll be implemented it can be backward-compatible by default (replacing the current document) unless a new option is specified like newInstance: true.

wOxxOm

unread,
May 12, 2023, 9:35:04 AM5/12/23
to Chromium Extensions, wOxxOm, Jackie Han, Chromium Extensions, Robbi, Sebastian Benz
Correction, the current behavior is "keeping the current document and throwing an exception".

Jackie Han

unread,
May 12, 2023, 10:40:51 AM5/12/23
to wOxxOm, Chromium Extensions, Robbi, Sebastian Benz
chrome.offscreen.createDocument({
  url: 'off_screen.html',
  reasons: ['DOM_SCRAPING'],
  justification: 'reason for needing the document',
  // true: allow create multiple docs,
  // false: only allow one, try to create more than one will throw error,
  // default is false.
  multiple: true
});


I understand your design like above. try-catch is easy, but Chrome extension team did not explicitly answer when and how to support multiple offscreen docs (e.g. supporting multiple documents, but with a maximum limit). So the root of the problem is that this is an unfinished design.

Sebastian Benz

unread,
May 12, 2023, 10:44:51 AM5/12/23
to Jackie Han, wOxxOm, Chromium Extensions, Robbi
Another reason not to use try/catch is that createDocument might fail for other reasons as well, which makes getContext / clients.matchAll a better solution. 

@Jackie Han: I'm not aware of a better way to do this - thanks for creating a PR!

Jackie Han

unread,
May 12, 2023, 11:55:13 AM5/12/23
to Sebastian Benz, wOxxOm, Chromium Extensions, Robbi
Ok. I'll try to create a PR this weekend. This version doesn't fix everything, but is a little more robust than the existing version.

wOxxOm

unread,
May 12, 2023, 1:17:43 PM5/12/23
to Chromium Extensions, Jackie Han, wOxxOm, Chromium Extensions, Robbi, Sebastian Benz
> Another reason not to use try/catch is that createDocument might fail for other reasons as well, which makes getContext / clients.matchAll a better solution. 

getContexts is reliable, but clients.matchAll is not because it'll fail if the user/author opens this html file in a tab chrome-extension://id/file.html for debugging/hacking purposes. Just because it won't happen frequently doesn't mean this use case can be ignored.

The try-catch is still the simplest solution and it'll be reliable if you ignore the caught error when its message is 'Only a single offscreen document may be created' and rethrow it otherwise.

wOxxOm

unread,
May 12, 2023, 1:19:39 PM5/12/23
to Chromium Extensions, wOxxOm, Jackie Han, Chromium Extensions, Robbi, Sebastian Benz
On the other hand checking for error message is not elegant as the phrasing may be different across browsers, so yeah getContexts is the sole winner.

Sebastian Benz

unread,
May 16, 2023, 3:16:04 PM5/16/23
to wOxxOm, Chromium Extensions, Jackie Han, Robbi
The updated example is now live. Thanks a lot @Jackie Han for the PR!

Jackie Han

unread,
May 17, 2023, 3:13:36 AM5/17/23
to Sebastian Benz, wOxxOm, Chromium Extensions, Robbi
Hi You're welcome. I'm happy to help improve the official documentation.

In addition to creating offscreen documents, I would also like to share my thoughts on closing offscreen documents here.

How to close offscreen documents depends on the functionality of your extension. Some people may be inclined to leave it open. But when you are sure that you will not use it in a short time, it is best to close it to reduce the occupation of user resources.

To close the offscreen doc, in addition to calling `chrome.offscreen.closeDocument()` outside the offscreen doc, you can also call `window.close()` inside the offscreen doc.

If your extension's behavior is based on events, I recommend using the following method to automatically close the offscreen doc:

// offscreen.js inside offscreen.html
let timeoutId = 0;
chrome.runtime.onMessage.addListener(
  (message, sender, sendResponse) => {
    // close this document if there are no new messages within one minute
    // you can adjust the delay time according to your extension 
    clearTimeout(
timeoutId);// clear timer
    
timeoutId = setTimeout(window.close, 60000);// reset timer
   
    // do something and send response
  }
);



Bekir Ozturk

unread,
Jun 5, 2023, 5:37:14 PM6/5/23
to Chromium Extensions, Sebastian Benz, Chromium Extensions, Jackie Han, Robbi, wOxxOm
Am I doing something wrong? My code is like this. Got it from the docs. Doesn't this check if it has an offscreen document?
Why am I getting this error? Uncaught (in promise) Error: Only a single offscreen document may be created.

// background.js

let creating;

createOffscreen();

async function createOffscreen(path="offscreen.html") {
    const offscreenUrl=chrome.runtime.getURL(path);
    const matchedClients=await clients.matchAll();

    for (const client of matchedClients) {
        if (client.url===offscreenUrl) {
            return;
        }
    }

    if (creating) {
        await creating;
    }

    else {
        creating=chrome.offscreen.createDocument({
            url: path,
            reasons: ["DOM_SCRAPING"],
            justification: "set icon theme",
        });
        await creating;
        creating=null;
    }
}

Jackie Han

unread,
Jun 5, 2023, 8:50:17 PM6/5/23
to Bekir Ozturk, Chromium Extensions, Sebastian Benz, Robbi, wOxxOm
Hi Bekir,

I just tested on Chrome 114 stable and Chrome Canary.  The problem is that `await clients.matchAll()` returns an empty array [] now. In other words, it doesn't return an offscreen document client. Obviously, the behavior of the browser has changed recently. It may be a Chrome bug.

Jackie Han

unread,
Jun 5, 2023, 9:17:16 PM6/5/23
to Bekir Ozturk, Chromium Extensions, Sebastian Benz, Robbi, wOxxOm
I reported this bug at https://crbug.com/1451659

Jackie Han

unread,
Jun 5, 2023, 9:32:33 PM6/5/23
to Bekir Ozturk, Chromium Extensions, Sebastian Benz, Robbi, wOxxOm
Hi Bekir,

Update: I found this bug only happens when you put the `createOffscreen()` at service worker startup. If you use it latter, `clients.matchAll()` can return offscreen doc, but when it runs at startup, it doesn't return offscreen doc.

To workaround it, you can move `createOffscreen()` from top level to where you need to use an offscreen doc.

Jackie Han

unread,
Jun 5, 2023, 10:20:40 PM6/5/23
to Bekir Ozturk, Chromium Extensions, Sebastian Benz, Robbi, wOxxOm
This bug is a problem between Clients API and chrome.offscreen API.

In the future, there is a new api `runtime.getContexts()` to do it.

// check offscreen docs by Clients API (for now)
const offscreenUrl = chrome.runtime.getURL(path);
const matchedClients = await clients.matchAll();
for (const client of matchedClients) {
  if (client.url === offscreenUrl) {
    return;
  }
}

// check offscreen docs by Contexts API (in the future)
const contexts = await chrome.runtime.getContexts({contextTypes: ["OFFSCREEN_DOCUMENT"]});
if (contexts.length > 0) {
  return;
}



wOxxOm

unread,
Jun 5, 2023, 11:22:43 PM6/5/23
to Chromium Extensions, Jackie Han, Chromium Extensions, Sebastian Benz, Robbi, wOxxOm, Bekir Ozturk
Just use try/catch and check the error text. It'd also handle the case of calling createDocument a second time while the first time is still being created (AFAIK the current code in docs can't handle it).

Jackie Han

unread,
Jun 6, 2023, 12:25:13 AM6/6/23
to wOxxOm, Chromium Extensions, Sebastian Benz, Robbi, Bekir Ozturk
Just use try/catch and check the error text.
 
If the offscreen api supports creating multiple docs, then it will not throw this error, and may throw another error which text is "you can't create more than 5 offscreen documents".

wOxxOm

unread,
Jun 6, 2023, 12:28:56 AM6/6/23
to Chromium Extensions, Jackie Han, Chromium Extensions, Sebastian Benz, Robbi, Bekir Ozturk, wOxxOm
It doesn't support multiple documents.

wOxxOm

unread,
Jun 6, 2023, 12:31:07 AM6/6/23
to Chromium Extensions, wOxxOm, Jackie Han, Chromium Extensions, Sebastian Benz, Robbi, Bekir Ozturk
...and even if it will, the API should behave the same unless the user explicitly specified the desire to open another instance via an additional parameter. We've already discussed it earlier.

Jackie Han

unread,
Jun 6, 2023, 12:39:35 AM6/6/23
to wOxxOm, Chromium Extensions, Sebastian Benz, Robbi, Bekir Ozturk
It doesn't support multiple documents.
But the blog explicitly said it may support it.

if it will, the API should behave the same……
It better be, but they're not making any promises.

wOxxOm

unread,
Jun 6, 2023, 12:49:07 AM6/6/23
to Chromium Extensions, Jackie Han, Chromium Extensions, Sebastian Benz, Robbi, Bekir Ozturk, wOxxOm
Checking the error text as I suggested would differentiate between the two errors even if multiple documents are supported and Chrome developers unexpectedly turn out to be so bad at designing the API that they make it backward-incompatible. Either way, try-catch + error checking is the only method I see that handles the case of calling createDocument during an ongoing initialization due to a previous call of createDocument.

Bekir Ozturk

unread,
Jun 6, 2023, 5:03:12 AM6/6/23
to Chromium Extensions, wOxxOm, Jackie Han, Chromium Extensions, Sebastian Benz, Robbi, Bekir Ozturk

Thank you both for answering so fast.

As far as I know, getting an error doesn't prevent the extension from running. So, it's not a big deal, but it's annoying.

All I wanted to see was the changing action icon. It seems like only Firefox has it in the manifest file.
To detect the color scheme change, I used CSS to change the color of the body of the offscreen. I was checking its color, but it made this error.

You said, don't call it the first thing. When can I? I have an init function and some listeners in the global scope of the background. The init gets the storage.local and then the offscreen.Should I put some kind of setTimeout(), like for a second, or just try/catch it?

It's not a big project. I and a few other people that I shared it with are using it. I don't think they even realized the icon changes color when you change your system settings. Who changes it frequently?
That's why just putting in a try/catch would be an easy solution if there are none.

Jackie Han

unread,
Jun 6, 2023, 6:37:17 AM6/6/23
to Bekir Ozturk, Chromium Extensions, wOxxOm, Sebastian Benz, Robbi
All I wanted to see was the changing action icon. It seems like only Firefox has it in the manifest file.
To detect the color scheme change, I used CSS to change the color of the body of the offscreen.

Yes, Chrome doesn't support extension's icon for dark/light themes. It may support it in the future. More details see https://github.com/w3c/webextensions/issues/229

You can create an offscreen document. In the offscreen doc, you use the following code:
    let query = window.matchMedia('(prefers-color-scheme: dark)');
    let isDarkMode = query.matches;
    query.addEventListener("change", a-change-listener);

    // then sendMessage to background, then setIcon()
 
In your case, you need a persistent offscreen, never close it. You can init it in below listeners:

chrome.runtime.onInstalled.addListener(createOffscreen);
chrome.runtime.onStartup.addListener(createOffscreen);

In this way, your createOffscreen will not throw errors. Yes, try-catch also works for you.

Message has been deleted

Bekir Ozturk

unread,
Jun 6, 2023, 2:36:44 PM6/6/23
to Chromium Extensions, Jackie Han, Chromium Extensions, wOxxOm, Sebastian Benz, Robbi, Bekir Ozturk
Tried to share code between triple ` symbol but didn't work well, deleted the message.

I've tried window.matchMedia() before. I don't know why but listener doesn't get called. I don't think I do anything wrong but here's the code. Even if it is, I tried variations. Just checked if it even ever gets called. It doesn't. First sendMessage is called and the info is true. Then nothing happens. That's why I used a CSS and checked that CSS with setInterval.

let query = window.matchMedia('(prefers-color-scheme: dark)');
let isDarkMode = query.matches;

chrome.runtime.sendMessage({
    type: "message",
    message: `isDarkMode: ${isDarkMode}`
});

query.addEventListener("change", (event) => {
    isDarkMode = event.matches;

    chrome.runtime.sendMessage({
        type: "message",
        message: `isDarkMode: ${isDarkMode}; event: ${event}`
    });

    chrome.runtime.sendMessage({
        type: isDarkMode ? "dark" : "light"
    });
});

I put it in onStartup listener. Thanks for the tip.

wOxxOm

unread,
Jun 6, 2023, 2:37:49 PM6/6/23
to Chromium Extensions, Bekir Ozturk, Jackie Han, Chromium Extensions, wOxxOm, Sebastian Benz, Robbi
matchMedia doesn't work in non-visible contexts like the MV2 background page or MV3 offscreen document.

Bekir Ozturk

unread,
Jun 6, 2023, 2:45:12 PM6/6/23
to Chromium Extensions, wOxxOm, Bekir Ozturk, Jackie Han, Chromium Extensions, Sebastian Benz, Robbi
It makes sense now. But to be correct, it detects whether it's dark or not, but it doesn't trigger the listener. That's what I've experienced.

Jackie Han

unread,
Jun 6, 2023, 3:06:34 PM6/6/23
to Bekir Ozturk, Chromium Extensions, wOxxOm, Sebastian Benz, Robbi
 it detects whether it's dark or not, but it doesn't trigger the listener.

Yes, in offscreen document, query.matches works, but the change listener doesn't trigger.
In fact, in a browser tab (not offscreen doc), the change listener work only when the tab is activated, if the tab is not activated, the change listener doesn't trigger util the tab is activated.
So, I think the same reason causes the change listener doesn't work in offscreen doc, because it is always not activated.
 

Jackie Han

unread,
Jun 7, 2023, 1:32:54 PM6/7/23
to Bekir Ozturk, Chromium Extensions, wOxxOm
(off topic again)
The spec doesn't say that MediaQueryList.onchange can't work when the document is invisible. Chrome and Safari don't trigger the event, but Firefox still fires the event after switching to another tab. So I think this is an optimization for browsers to take advantage of deferred rendering.

Kein Zantezuken

unread,
Jun 28, 2023, 5:35:18 PM6/28/23
to Chromium Extensions, Jackie Han, Chromium Extensions, Sebastian Benz, Robbi, wOxxOm, Bekir Ozturk
I've tried every single one of the suggested solutions here:

1. Simple Try/Catch
2. Try/Catch via Promise's .then\.catch
3. Simple and Complex `Clients` API checks
4. Attempt to send message

None of them sole the problem of errors piling up. In case of #1-#3 it is damned:
`Uncaught (in promise) Error: Only a single offscreen document may be created.`
In case of #4 it is some connection communication error.

Either way I'm still with the same issue - these error pile up in extension's log and widget on the extension page. They should not be there.

Jackie Han

unread,
Jun 29, 2023, 12:21:30 AM6/29/23
to Kein Zantezuken, Chromium Extensions, Sebastian Benz, Robbi, wOxxOm, Bekir Ozturk
Kein,

Since you didn't give any reproduced code, I guess you may have made some beginner mistakes. For example,
  try {
    await chrome.offscreen.createDocument({...}); // do you forget the await keyword?
  } catch(e) {}


Reply all
Reply to author
Forward
0 new messages