In which order `storage` event is received?

268 views Asked by At

In which order storage event is received?

For example, tab_1 executes code

localStorage.set('key', 1);
localStorage.set('key', 2);

and tab_2 executes code

localStorage.set('key', 3);
localStorage.set('key', 4);

They execute code simultaneously.

Questions:

  • Can 2 be stored before 1?
  • Can 3 or 4 be stored between 1 and 2?
  • Can tab_3 receive storage events not in same order as values were stored in?(like if we stored 3, 4, 1, 2, can we receive 1, 2, 3, 4, or even in random order like 4, 1, 2, 3)
3

There are 3 answers

0
Kaiido On BEST ANSWER

As already quoted in the previous answer, the specs say

This specification does not define the interaction with other agent clusters in a multiprocess user agent, and authors are encouraged to assume that there is no locking mechanism.

Then, each Document will have its own Storage object set as its local storage holder.
This Storage object is supposed to be a wrapper around a storage proxy map which itself should act directly over its backing map which is supposed to be directly the storage bottle's map, i.e. where the data is actually stored, and thus this should be the same for all Documents.

So... that doesn't help much to know what should happen in case two same-origin Documents in two different agent clusters (and thus possibly running in parallel) would write to the same bucket at the same time.

All the specs say is that as an author you should not expect any locking mechanism.

So in your scenario, according to the specs,

Can 2 be stored before 1?

It's a definitive no. setItem() updates the map synchronously there is no way 2 would be set before 1 from the same agent.

Can 3 or 4 be stored between 1 and 2?

Yes it can. If your tab_1 is locked between these two lines there is nothing in the specs that prevents tab_2 from writing to the same map.

Can tab_3 receive storage events not in same order as values were stored in?

setItem() is responsible of broadcasting the storage, which will queue a task on every global object whose Document's Storage's map will point to the same bottle's map, and in order to queue that task, it must use the one and only DOM manipulation task source, so it's ensured that all the events will be fired in the order their respective tasks have been queued.


Now as to what implementations actually do, it seems a bit complex.

To test this, we need to have same-origin documents running in parallel. The best for it is to open separate browser windows (not just tabs) and navigate through a common origin from there. Then we can try to write to tab_1's Storage, and begin a busy loop that will read the same key that it just set. While tab_1 is performing its busy loop, we can ask tab_2 to also write to the same key as tab_1 did. All this while listening to storage event in a tab_3. If they were really updating the same map, we'd see the change during the busy loop in tab_1, or tab_2 would have to somehow wait until tab_1 is done with its busy loop.

I prepared these page tests:

Please make sure they all run in their own agent cluster by checking that tab_2's content is updated even while tab_1 is busy (red background).

tab_1

document.querySelector("button").onclick = async (evt) => {
  document.body.style.backgroundColor = "red"; // Make it visible when the page is locked
  log("begin test");
  await new Promise((res) => setTimeout(res, 10));
  localStorage.test = "1";
  let a = localStorage.test;
  const t1 = performance.now();
  while(performance.now() - t1 < 3000) {
    if (a !== localStorage.test) { // Check whether the value has changed
      log("value changed during busy loop");
      a = localStorage.test;
    }
  }
  localStorage.test = "2";
  log("end test " + localStorage.test);
  document.body.style.backgroundColor = null;
};
onstorage = ({ newValue, key }) => {
  log("received a new storage event");
  const currentValue = localStorage.getItem(key);
  log({ newValue, currentValue, key });
}

tab_2

document.querySelector("button").onclick = async (evt) => {
  log("setting values");
  localStorage.test = "3";
  localStorage.test = "4";
  log("values set " + localStorage.test);
};

tab_3

onstorage = ({ newValue, key, url }) => {
  log("received a new storage event");
  const currentValue = localStorage.getItem(key);
  log({ newValue, currentValue, key, url });
}

The actual results are a bit surprising. In Chrome we can see in tab_3 that it received the first event from tab_1, then the 2 events from tab_2, and the last event from tab_1 (1, 3, 4, 2). So this confirms the fact that 3 and 4 can be written in between 1 and 2. The surprising part is that tab_1 didn't see the value change at all during its busy loop.
This would mean they don't actually update the main bottle's map, but just the wrapper or a local version of the map, and somehow wait on something to update the actual map or every local versions, even though this isn't in accordance with the specs. And while tab_1 also receives the events from tab_2, in the next tasks, the stored value at the time it receives the event is actually 2 and not 4 like the event's newValue says it is.

In Firefox the behavior is yet a bit farther from the specs since they do apparently broadcast the storage only at the end of the task. So there in tab_3, we actually receive the two events from tab_2, and then the two events from tab_1 (3, 4, 1, 2).
So the answer to your third question is actually also a yes...

19
Lajos Arpad On

Documentation: https://developer.mozilla.org/en-US/docs/Web/API/Storage

Can 2 be stored before 1?

Javascript uses a single thread and executes the commands sequentially. So, if you have

localStorage.setItem('key', 1);
localStorage.setItem('key', 2);

then my expectation would be that 1 is stored before 2 and 2 overrides 1, because setItem seems to be a simple function call and I don't think it is involving async data. I am saying "I don't think" because I do not know where its source-code is, but given the fact that it's native code:

enter image description here

it's up to the browser developers to implement it or integrate an implementation. I understand you want to factually find this out, so here's a test:

let one = 0;
let two = 0;
for (let i = 0; i < 10000; i++) {
    localStorage.setItem('key', 1);
    localStorage.setItem('key', 2);
    if (localStorage.getItem('key') == 1) one++;
    else two++;
}
console.log('one ' + one + ' two ' + two);

Output in FireFox:

one 0 two 10000

and the same in Chrome. So the answer is no.

Can 3 or 4 be stored between 1 and 2?

If Javascript in two different tabs can run at the same time, then yes. Let's write a test for that:

setTimeout(function() {alert("the other tab")}, 5000);

Run this code in a tab and quickly drift away to another tab. If in 5 seconds you see an alert while you are on the other tab, then the answer is yes. If not, then the answer is no. Running this in FireFox and drifting to another tab, I can see a red dot appearing on the now inactive tab, signifying that the alert happened there. The same happened at Chrome, but there the dot was orange. So the answer is yes.

Can tab_3 receive storage events not in same order as values were stored in?(like if we stored 3, 4, 1, 2, can we receive 1, 2, 3, 4, or even in random order like 4, 1, 2, 3)

This largely depends on the implementation, but I expect the loading of the data stored in localStorage to be behaving in the same manner upon each call. You can test it by running your codes in tab1 and tab2, respectively, possibly using setTimeout and opening further tabs, either before or after running your codes and check the value of localStorage in each. If they are the same, then the test's result suggests that it is reliable indeed. If not, then the test disproved my hypothesis about reliability.

11
meriton On

JavaScript being single threaded (in the absence of Web Workers, who do not communicate through shared memory), the processing order within a browser tab is well defined.

As for ordering across tabs, the spec cautions:

The localStorage getter provides access to shared state. This specification does not define the interaction with other agent clusters in a multiprocess user agent, and authors are encouraged to assume that there is no locking mechanism. A site could, for instance, try to read the value of a key, increment its value, then write it back out, using the new value as a unique identifier for the session; if the site does this twice in two different browser windows at the same time, it might end up using the same "unique" identifier for both sessions, with potentially disastrous effects.

HTML Spec Issue 403 requests that the concurrency behavior of localStorage be specified better, but a solution has yet to be defined at this time ...

Implementation in Current Browsers

If I open 3 browser windows for the same origin, and execute the following in each of them:

function test(increment) {
    let x = localStorage.test;
    console.log("polling for changes, initial value is", x);
    for (let i = 0; i < 50000000; i++) {
        let y = localStorage.test;
        if (x != y) {
            console.log("seeing new value", y);
            x = y;
        }
        if (i % 10000000 == 0) {
            let n = parseInt(x) + increment;
            console.log("setting new value", n);
            localStorage.test = n;
        }
    }
}
window.onstorage = e => console.log(
    `receiving storage event ${e.oldValue} -> ${e.newValue} while reading ${localStorage.test}`
);

And then in the first browser window:

localStorage.test = 0;
test(1);

and while that is executing, in the second browser window:

test(2);

Output in Chrome, first tab:

polling for changes, initial value is 0
setting new value 1
seeing new value 1
setting new value 2
seeing new value 2
setting new value 3
seeing new value 3
setting new value 4
seeing new value 4
setting new value 5
seeing new value 5
undefined
receiving storage event 1 -> 3 while reading 11
receiving storage event 2 -> 5 while reading 11
receiving storage event 3 -> 7 while reading 11
receiving storage event 4 -> 9 while reading 11
receiving storage event 5 -> 11 while reading 11

second tab

receiving storage event 0 -> 1 while reading 1
test(2);
polling for changes, initial value is 1
setting new value 3
seeing new value 3
setting new value 5
seeing new value 5
setting new value 7
seeing new value 7
setting new value 9
seeing new value 9
setting new value 11
seeing new value 11
undefined
receiving storage event 3 -> 2 while reading 11
receiving storage event 5 -> 3 while reading 11
receiving storage event 7 -> 4 while reading 11
receiving storage event 9 -> 5 while reading 11

third tab

receiving storage event 0 -> 1 while reading 1
receiving storage event 1 -> 3 while reading 3
receiving storage event 3 -> 2 while reading 2
receiving storage event 2 -> 5 while reading 5
receiving storage event 5 -> 3 while reading 3
receiving storage event 3 -> 7 while reading 7
receiving storage event 7 -> 4 while reading 4
receiving storage event 4 -> 9 while reading 9
receiving storage event 9 -> 5 while reading 5
receiving storage event 5 -> 11 while reading 11

Doing the same in current Firefox, we get:

polling for changes, initial value is 0
setting new value 1
seeing new value 1
setting new value 2
seeing new value 2
setting new value 3
seeing new value 3
setting new value 4
seeing new value 4
setting new value 5
seeing new value 5
undefined
receiving storage event 0 -> 2 while reading 10
receiving storage event 2 -> 4 while reading 10
receiving storage event 4 -> 6 while reading 10
receiving storage event 6 -> 8 while reading 10
receiving storage event 8 -> 10 while reading 10

second tab:

polling for changes, initial value is 0 
setting new value 2
seeing new value 2
setting new value 4
seeing new value 4
setting new value 6
seeing new value 6
setting new value 8
seeing new value 8
setting new value 10
seeing new value 10
receiving storage event 0 -> 1 while reading 10
receiving storage event 1 -> 2 while reading 10
receiving storage event 2 -> 3 while reading 10
receiving storage event 3 -> 4 while reading 10
receiving storage event 4 -> 5 while reading 10
undefined

third tab

receiving storage event 0 -> 1 while reading 5
receiving storage event 1 -> 2 while reading 5
receiving storage event 2 -> 3 while reading 5
receiving storage event 3 -> 4 while reading 5
receiving storage event 4 -> 5 while reading 5
receiving storage event 0 -> 2 while reading 10
receiving storage event 2 -> 4 while reading 10
receiving storage event 4 -> 6 while reading 10
receiving storage event 6 -> 8 while reading 10
receiving storage event 8 -> 10 while reading 10

It appears that:

  • In both browsers, concurrent modifications only become visible once the current task has finished executing, not during the task. If two tasks read and write concurrently, they don't see each other, but overwrite each other's work.
  • In both browsers, it is possible to receive obsolete storage events, i.e. events about a state that has already been overwritten.
  • In both browsers, all windows see the events in the same order, and the latest (visible) write wins.
  • In Chrome, writes are broadcast to other windows as they occur, while Firefox broadcasts them once the task completes.