Project

General

Profile

Actions

Internal utilities

In the extension several helper mechanisms are used that might deserve a more thorough explanation.

Locks

It's no more than a mutex, defined in common/lock.js. Its use might be non-obvious because javascript doesn't have thread. However, javascript does have asynchronous execution. Let's consider a situation with 2 async functions, A and B:

async function A()
{
    await operation1();
    await operation2();
    await operation3();
}

async function B()
{
    await operation4();
    await operation5();
    await operation6();
}

async function operation1() {console.log("x");}
async function operation2() {console.log("y");}
async function operation3() {console.log("z");}
async function operation4() {console.log("x");}
async function operation5() {console.log("y");}
async function operation6() {console.log("z");}

operation[1-6]() shall represent some unspecified functions that return promises. Now, what is going to happen if we execute the following?

A(); B();

We get this output:

x
x
y
y
z
z

Operations within A() got interleaved with those in B() because they are asynchronous. Sometimes this is actually what we want. Other times, we might need operation[1-3]() to happen in a row, uninterrupted. Using our lock implementation, this is fairly easy to achieve. Assuming common/lock.js has been evaluated in out current context, we can do:

let mutex = window.make_lock();

async function A()
{
    await window.lock(mutex);
    await operation1();
    await operation2();
    await operation3();
    await window.unlock(mutex);
}

async function B()
{
    await window.lock(mutex);
    await operation4();
    await operation5();
    await operation6();
    await window.unlock(mutex);
}

A(); B();

This yields us:

x
y
z
x
y
z

This might be useful when using either asynchronous WebExtensions APIs (e.g. local storage) or some asynchronous functions defined within the extension.

make_once()

When javascript modules were used within the extension, a problem regarding asynchronous execution showed up. In some cases module A depended on some function f() exported from module B, but that function itself depended on some object p that was not available immediately (for example because it was being obtained through resolution of some promise). This was the case, among others, with settings storage. Let's illustrate this with some simplified code.

in B:

let p = undefined;

(async () => {p = await obtain_p();})();

function f()
{
    do_something_with(p);
}

/* f gets exported */

in A:

/* f gets imported */

f();

This could easily fail if A managed to get executed before await obtain_p() resolved. Some way is needed to wait in A until p gets initialized. We could try making f() asynchronous, we could try passing p to f(). But the solution that actually got implemented was to not export f() at all. Instead, we export an asynchronous function get_f() that waits for p to get initialized and then resolves to f() (i.e. it returns a function object). The advantage is that we don't change the semantics of f().

Consider the modified version of previous code.

in B:

let p = undefined;

(async () => {p = await obtain_p();})();

function f()
{
    do_something_with(p);
}

async function get_f()
{
    /* waits for p */

    return f;
}

/* get_f gets exported */

in A:

/* get_f gets imported */

(async () => {
   let f = await get_f();
   f();
})();

It might not seem so at first glance, but this is the right approach. Now, one might be wondering how get_f() exactly performs its wait for p. Using setTimeout() to periodically check is obviously not something we'd approve. The truth is, the code that sets p should be merged into get_f(). But then we have to make sure obtain_p() is only called once, even though code in A or other modules might issue multiple calls to get_f() before the first of them resolves.

To handle this we have to create a new Promise object on each get_f() call that sees p uninitialized and store continuation callbacks in some data structure... All this would be needed in multiple modules of the extension, so it made sense to implement it once and put that implementation in common/once.js. Other code just uses the make_once() function defined there to create this kind of "getter".

Consider the following, final version of B:

/* make_once gets imported */

let p = undefined;

function f()
{
    do_something_with(p);
}

async function make_f()
{
    p = await obtain_p();

    return f;
}

make_f(), is only supposed to be executed once. After that, all code is supposed to call the f() that got returned. Writing make_f() as a one-time function allows us to keep it very concise. It would be difficult to access f() from multiple places, though, so we now use our facility to turn make_f() into a "getter". Still in B:

let get_f = make_once(make_f);

/* get_f gets exported */

Now, in A:

/* get_f gets imported */

(async () => {
   let f = await get_f();
   f();
})();

Convenient and simple. No? Don't worry, promises tend to be so abstract even someone who writes the code doesn't always know how and why it works. Just use make_once() as instructed here and you will be OK.

This mechanism was employed back when ES6 modules were used. After the transition back to conventional script files more approaches to such problems became valid. However, the current solution is sufficient and right now there's no need to look for a replacement one.

Updated by jahoti 5 months ago · 2 revisions