DOMException: Failed to execute 'importScripts' on 'WorkerGlobalScope':

8,068 views
Skip to first unread message

Robbi

unread,
Dec 30, 2022, 7:42:05 PM12/30/22
to Chromium Extensions
The script at 'chrome-extension://<extId>/script/main.js' failed to load.
at chrome-extension://<extId>/worker_wrapper.js:33

Hi folk,
I can't figure out where I'm going wrong.
I'd like to get all items in chrome.storage (local + session + sync) and merge these 3 objects into one simple javascript object.
In the same worker wrapper I should also import one (or more script).
if I settle for getting only one storage (i.e. chrome.session) then the script works and the external scripts is imported without any errors.
Why?
TIA


This is the code:

/* manifet.json */
{
    "manifest_version": 3,
    "name": "Test",
    "description": "fooBar",
    "version": "0.0.0.1",
    "background": {
        "service_worker": "worker_wrapper.js"
    },
    "permissions": ["storage"]
     ...
}

/* worker_wrapper.js */
var mergedStorage;
try {
    //1 - THIS WORKS!!!
    (async s => {
        let np = new Promise((ok, ko) => {
            chrome.storage.session.get(null, se => {
                if (chrome.runtime.lastError)
                    ko(chrome.runtime.lastError);
                else
                    ok({...se})
            })
        });
        mergedStorage = await np;
        importScripts(s);
    })('/script/main.js');
   
    //2 - IT DOESN'T WORK
    (async s => {
        let np = new Promise((ok, ko) => {
            var arrProm = [];
            arrProm.push(chrome.storage.session.get(null));
            arrProm.push(chrome.storage.local.get(null));
            arrProm.push(chrome.storage.sync.get(null));
            Promise.all(arrProm)
                .then(v => ok({...v[0], ...v[1], ...v[2]}))
        });
        mergedStorage = await np;
        importScripts(s);    //*** Error at this line
    })('/script/main.js');
   
    //3 - IT DOESN'T WORK
    (async s => {
        var np = new Promise(ok => {
            chrome.storage.session.get(null, se =>
                chrome.storage.local.get(null, lo =>
                    chrome.storage.sync.get(null, sy =>
                        ok({...se, ...lo, ...sy})
                    )
                )
            )
        });
        mergedStorage = await np;
        importScripts(s);    //*** Error at this line
    })('/script/main.js');
} catch (e) {
    console.log(e)
}

/*/script/main.js*/
console.log(mergedStorage)

wOxxOm

unread,
Dec 31, 2022, 5:44:25 AM12/31/22
to Chromium Extensions, Robbi
Per specification service worker can't use importScripts asynchronously unless you duplicate the same in self.oninstall event listener.
This is yet another evidence that choosing such a developer-hostile and limited environment for ManifestV3 was a mistake.

Here's an example:

const importedScripts = [];

function tryImport(...fileNames) {
  try {
    const toRun = new Set(fileNames.filter(f => !importedScripts.includes(f)));
    if (toRun.length) {
      importedScripts.push(...toRun);
      importScripts(...toRun);
    }
    return true;
  } catch (e) {
    console.error(e);
  }
}

self.oninstall = () => {
  // The imported script shouldn't do anything, but only declare a global function
  // (someComplexScriptAsyncHandler) or use an analog of require() to register a module
  tryImport('/js/some-complex-script.js');
};

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.action === 'somethingComplex') {
    if (tryImport('/js/some-complex-script.js')) {
      // calling a global function from some-complex-script.js
      someComplexScriptAsyncHandler(msg, sender, sendResponse);
      return true;
    }
  }
});


wOxxOm

unread,
Dec 31, 2022, 5:52:02 AM12/31/22
to Chromium Extensions, wOxxOm, Robbi
I forgot to mention: the difference between the attempts is that when you use "await" with something like chrome.storage that returns results in a separate event loop cycle the subsequent code runs in the next event loop cycle, i.e. what I called "use importScripts asynchronously", which violates the specification. Also note that if you import this code unconditionally there's no need for the oninstall trickery, you can simply move importScripts to the beginning of the SW.

Robbi

unread,
Dec 31, 2022, 2:28:21 PM12/31/22
to Chromium Extensions, wOxxOm, Robbi
Hi wOxxOm, thank you for your answer.
I see your code is similar\same to what you had given as an answer in this thread on SO 
I don't think I understand very well the purpose of that code or perhaps the correlation with my problem.

If I understood well, your code allows, against the preventive import of all the scripts that we expect to use in the future inside the self.oninstall event handler,
to be able to import the necessary scripts at a later time through a simple exchange of messages.

When the SW will go to sleep after installing or upgrading, all scripts "declared" inside the self.oninstall event handler will be unloaded
and, when the right message will land to the chrome.runtime.onMessage handler will be imported only the scripts needed in that precise context.
This message can only come from a script external to the SW, such as an extension page.
Did I understand it right so far?

I added to your code a chrome.action.onClicked handler which opens a tab which sends a message back to the SW and I was able to verify that this mechanism works fine.
I saved that SO thread in my favorites because I guess it will come in handy in the future (you're a genius).
I only changed "(toRun.length)" to "(toRun.size)" since "length" returns undefined with Set

Now let's get back to my problem.
Maybe I forgot to make a premise (but I think you've already guessed it).
My goal is to be able to use a handy object variable inside the scripts I've imported with "importScripts"
and I need to use this object in the main thread  (so it must be available immediately).
An alternative could be to move the self-invoking function to the main thread of the script I'm importing (/script/main.js).
I fixed things a bit like this, but I'm not very satisfied with it:

/*----------------------------------------------------------*/
/*worker_wrappwer*/
var mainProm, mainPromBusy;
try {
    mainProm = new Promise((ok, ko) => {
        var arrProm = [];
        arrProm.push(chrome.storage.session.get());
        arrProm.push(chrome.storage.local.get());
        arrProm.push(chrome.storage.sync.get());
        mainPromBusy = Promise.all(arrProm)
            .then(v => {
                ok({...v[0], ...v[1], ...v[2] /*, 'fooBar': 123*/ });
                mainPromBusy = null
            })
            .catch(e => ko(e))
    });

    importScripts('script/main.js')
} catch (e) {
    console.log(e)
}

/*script/main.js*/
(async _ => {
    if (mainPromBusy != null)
        mergedStores = await mainProm;
    console.log(mergedStores)
})()

/*----------------------------------------------------------*/

Lastly, could you please elaborate on your explanation why my first attempt worked while the others didn't?
I don't think I understood correctly.

Thanks you and...
best wishes for the new year

wOxxOm

unread,
Dec 31, 2022, 3:32:28 PM12/31/22
to Chromium Extensions, Robbi, wOxxOm
Note that messaging is just an example of importing at a later time, but the underlying problem is exactly the same as yours with "await chrome.storage...." i.e. importScripts is not called in the first turn of the event loop of SW lifetime, but later.

>  why my first attempt worked while the others didn't?

Because it calls importScripts in the same "JS event loop task", whereas the other examples do it after "await" with an API that returns in a separate event loop task, see my second comment above.

> When the SW will go to sleep after installing or upgrading, all scripts "declared" inside the self.oninstall event handler will be unloaded

Words like "sleep" or "unload" are pretty vague because they often mean resumable hybernation, but it's not what actually happens: the entire JS environment is simply terminated like you kill a process in the task manager, so everything that was running inside just evaporates immediately.

Robbi

unread,
Jan 1, 2023, 10:23:44 AM1/1/23
to Chromium Extensions, wOxxOm, Robbi
Ok, i mulled a bit and now i get it (both 2 topics).
The message exchange had led me off track.

However, I think this solution is not ideal for my specific case.
Please see my rethinked code:

/*------------------------------------------------------------*/
//worker_wrapper.js
var mergedStores;
var importedScripts = [];
function tryImport(t, ...fileNames) {    /* first parameter now is the "caller" */
    try {
        const toRun = new Set(fileNames.filter(f => {return !importedScripts.includes(f)}));
        if (toRun.size) {

            importedScripts.push(...toRun);
            importScripts(...toRun)
        }
        return true;
    } catch (e) {
        console.error(e)
    }
}

/*
- Using "tryImport" I will never see "mergedStores" set correctly
- Using "importScripts", "mergedStores" will be undefined at first time, but will be set correctly at second time

*/
self.oninstall = _ => {
    /* tryImport('selfOninstall', '/main.js') */
    importScripts('/main.js')
};


var arrProm = [];
arrProm.push(chrome.storage.session.get());
arrProm.push(chrome.storage.local.get());
arrProm.push(chrome.storage.sync.get());
Promise.all(arrProm).then(v => {
         mergedStores  = {...v[0], ...v[1], ...v[2], 'fooBar':123};
  
         console.log('mergedStores has been set');

         /* tryImport('asyncFunc', '/main.js') */
         importScripts('/main.js')
});

//main.js
console.log(mergedStores)

/*------------------------------------------------------------*/

I mean, the purpose of the "tryImport" function is to not import the same script twice. Did I get it right?
That function allows the script to be imported synchronously into self.oninstall (when "mergeStorage" is not ready)
but it does not allow a second import of the same script into my async function where I get and merge all storages together.

If I don't make use of the "tryImport" function and use "importScripts" directly both in self.oninstall
and in my asynchronous function, then I will have to expect two imports of the same script where,
on the first import the mergedStorage object will be undefined, while on the second time I will be valued correctly.
How can I fix that?

wOxxOm

unread,
Jan 1, 2023, 12:02:44 PM1/1/23
to Chromium Extensions, Robbi, wOxxOm
See the suggestion in my example:

// The imported script shouldn't do anything, but only declare a global function
// (someComplexScriptAsyncHandler) or use an analog of require() to register a module

This is why importing just once is fine.
Message has been deleted

Robbi

unread,
Jan 1, 2023, 3:28:46 PM1/1/23
to Chromium Extensions, wOxxOm, Robbi
Is this where you wanted to take me?


//worker_wrapper.js
var mergedStores;
var importedScripts = [];
function tryImport(t, ...fileNames) {    //first parameter now is the "caller"
    console.log('caller', t);

    try {
        const toRun = new Set(fileNames.filter(f => { return !importedScripts.includes(f) }));
        if (toRun.size) {
            importedScripts.push(...toRun);
            importScripts(...toRun)
        }
        return true
    } catch (e) {
        console.error(e)
    }
}

self.oninstall = _ => {

     tryImport('asyncFunc', '/main.js')
};

var arrProm = [];
arrProm.push(chrome.storage.session.get());
arrProm.push(chrome.storage.local.get());
arrProm.push(chrome.storage.sync.get());
Promise.all(arrProm).then(v => {
    mergedStores  = {...v[0], ...v[1], ...v[2]};

    console.log('mergedStores has been set');
    tryImport('asyncFunc', '/main.js');
    fooBar()
});

/* main.js */
async function fooBar() {
    // impMod = await import('other.js');    //Import() is disallowed on ServiceWorkerGlobalScope by the HTML specification.
                                                                       //Service workers can't require() modules as well
    console.log(mergedStores)
}

wOxxOm

unread,
Jan 1, 2023, 4:31:58 PM1/1/23
to Chromium Extensions, Robbi, wOxxOm
Yeah, this should work.

As for require() that I mentioned in my example, it generally works only if you use a bundler like webpack or you manually specified the loader function for require that incorporates oninstall+tryImport.

Robbi

unread,
Jan 1, 2023, 5:29:05 PM1/1/23
to Chromium Extensions, wOxxOm, Robbi
Ok, we had good academic speeches and, at least I, learned something (how to import script asynchronously in SW).
But do you realize that all this frameset could have been avoided by building the script I want to include
(with the getting and the merging of the 3 storages) as a self-invoked function in this simple way ?

/**--------------------------------------------------------------------------------------/
/* worker_wrapper.js */
var mergedStorage;
try {
    importScripts('/main.js')
} catch (e) {
    console.error(e)
}

/* main.js */
(async _ => {
    //a lot of stuff before

    let np = new Promise(async (ok, ko) => {

        var arrProm = [];
        arrProm.push(chrome.storage.session.get());
        arrProm.push(chrome.storage.local.get());
        arrProm.push(chrome.storage.sync.get());
        var v = await Promise.all(arrProm);
        ok({...v[0], ...v[1], ...v[2]})

    });
    mergedStorage = await np;
    console.log(mergedStorage)
   
    //a lot of stuff after
})()
/*-------------------------------------------------------------------*/

So why try so hard?
At this point, for my specific use case, this is the most "elegant" solution.
Thanks

Robbi

unread,
Jan 1, 2023, 5:32:21 PM1/1/23
to Chromium Extensions, Robbi, wOxxOm
I mean, in main,js there is always a global function in any case (that was the main thing I wanted to avoid)

wOxxOm

unread,
Jan 2, 2023, 3:08:16 AM1/2/23
to Chromium Extensions, Robbi, wOxxOm
Yes, that's what I mentioned in my second comment: "if you import this code unconditionally there's no need for the oninstall trickery, you can simply move importScripts to the beginning of the SW."
Reply all
Reply to author
Forward
0 new messages