Nicholas DiPiazza
Nicholas DiPiazza

Reputation: 10595

How to share a unique tab ID between content and background scripts without an asynchronous delay?

I have built a chrome extension and I'm getting hit by a race condition that I need help with.

If you see the answer chrome extension: sharing an object between content scripts and background script it tells us that you cannot share a variable between content and background scripts.

My goal is to generate a unique ID per-browser tab and then share it in between the content.js and the background.js. Then I need to use this value in a content injected javascript as explained in this answer: In Chrome extensions, can you force some javascript to be injected before everything?

The only way I have been able to figure out how to do this is by doing the following async code then I just use the tab ID as the unique ID:

content.js

await pingBackground();
async function pingBackground() {
    var info;
    await new Promise(function (resolve, reject) {
        chrome.runtime.sendMessage({ type: 1 }, function (response) {
            if (response !== undefined) {
                info = response;
                resolve();
            }
            else {
                reject();
            }
        });
    });
    console.log("Id is " + info.id);
}

background.js

chrome.runtime.onMessage.addListener(messageHandler);
function messageHandler(message, sender, reply) {
    switch (message.type) {
        case 1:
        reply({ 'id': sender['tab'].id, 'active': true });
        break;
    }
}

manifest.json

{
    "name": "oogi",
    "version": "0.1",
    "manifest_version": 2,
    "background": {
        "scripts": [
            "common.js",
            "background.js"
        ],
        "persistent": true
    },
    "content_scripts": [
        {
            "matches": ["*://*/*"],
            "js": ["content.js"],
            "run_at": "document_start"
        }
    ],
    "permissions": [
        "contentSettings",
        "webRequest",
        "webRequestBlocking",
        "*://*/*"
    ]
}

But the problem with this is by the time I get the tab ID from background js, the script's content has already been loaded.

Is there some way to make it so this variable can be asynchronously shared between background.js and content.js? Or is this simply impossible?

Can I switch it around and have background.js load a variable from content.js asynchronously?

UPDATE:

A terrible hack which works is to do this in the foreground of the content.js:

var sleepScript = document.createElement('script');
var sleepCode = `function sleep (ms) {
    var start = new Date().getTime();
    while (new Date() < start + ms) {}
    return 0;
}
sleep(500);`;
sleepScript.textContent = sleepCode;
(document.head || document.documentElement).appendChild(sleepScript);

This will force the page to wait for a bit giving the time to query the background before running the inline dom.

It works but that's awful.

Upvotes: 1

Views: 387

Answers (1)

Nicholas DiPiazza
Nicholas DiPiazza

Reputation: 10595

This question was already answered previously, although it is hard to tell that this is the same issue at first glance.

https://stackoverflow.com/a/45105934

The answer is pretty descriptive so give it a read.

Here is the script changes that make it work:

// background.js
function addSeedCookie(details) {
  details.responseHeaders.push({
    name: "Set-Cookie",
    value: `tab_id=${details.tabId}; Max-Age=2`
  });
  return {
    responseHeaders: details.responseHeaders
  };
}

chrome.webRequest.onHeadersReceived.addListener(
  addSeedCookie, {urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]
);

// inject.js
function getCookie(cookie) { // https://stackoverflow.com/a/19971550/934239
  return document.cookie.split(';').reduce(function(prev, c) {
    var arr = c.split('=');
    return (arr[0].trim() === cookie) ? arr[1] : prev;
  }, undefined);
}

var tabId = getCookie("tab_id");

Upvotes: 1

Related Questions