Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define how localStorage is synchronized between browser tabs/windows #403

Open
foolip opened this issue Dec 15, 2015 · 10 comments
Open

Define how localStorage is synchronized between browser tabs/windows #403

foolip opened this issue Dec 15, 2015 · 10 comments

Comments

@foolip
Copy link
Member

foolip commented Dec 15, 2015

From #335 (comment) and later comments it sounds like implementations of localStorage are not simply synchronous like document.cookie, but maintains an in-process cache that is synchronized with the real storage when returning to the event loop, or some similar condition.

The spec now says that "This specification does not define the interaction with other browsing contexts in a multiprocess user agent, and authors are encourages to assume that there is no locking mechanism."

If the way this is actually implemented is roughly interoperable, we should spec that. Needs thorough investigation.

@nathanhammond
Copy link

nathanhammond commented Apr 17, 2018

Here is some sample code that demonstrates seeming guarantees about when sync occurs between two processes.

I'm using the storage event as a proxy for having updated the local in-process cache, and a double event loop turn to guarantee that I'm behind all scheduled storage events. My conclusion is that I believe the hypothesis of synchronization at event loop turns is correct. (Tested in Chrome, Safari, and Firefox.)

I'm showing my work so that these statements may be corrected or expanded upon.

/cc @inexorabletash @rocallahan @jdm @annevk

<!--parent.html-->
<script>
  if (!window.localStorage.getItem('id')) {
    window.localStorage.setItem('id', '0');
  }
</script>
<!--Repeat line below dozens of times:-->
<iframe style="width: 50px; height: 2em;" src="consumer.html"></iframe>
<!--consumer.html-->
<script>
function domManipulationTaskSourceCallback(handler) {
  const styleElem = document.createElement('style');
  styleElem.innerHTML = 'body {}';
  styleElem.addEventListener('load', () => {
    document.body.removeChild(styleElem);
    handler();
  }, { once: true });

  document.body.appendChild(styleElem);
}

var idMessages = [];
window.addEventListener('storage', (event) => {
  if (event.key === 'id') {
    idMessages.push(event.newValue);
    console.count('id');
  }
});

function generateId() {
  return new Promise((resolve, reject) => {
    const current = window.localStorage.getItem('id');
    const next = (parseInt(current, 10) + 1).toString();

    window.localStorage.setItem('id', next);
    domManipulationTaskSourceCallback(() => {
      domManipulationTaskSourceCallback(() => {

        if (idMessages.includes(next)) {
          resolve(generateId());
        } else {
          resolve(next);
        }
      });
    });
  });
}

window.addEventListener('pageshow', (event) => {
  // Register.
  generateId().then((result) => {
    document.body.innerHTML = result;
  });
});
</script>

@annevk
Copy link
Member

annevk commented Apr 18, 2018

Since timers use the "timer task source" and localStorage uses the "DOM manipulation task source" there's no such guarantees in theory. A user agent is free to drain timers before doing storage work.

@nathanhammond
Copy link

nathanhammond commented Apr 18, 2018

@annevk is disputing this portion, emphasis added:

I'm using the storage event as a proxy for having updated the local in-process cache, and a double event loop turn to guarantee that I'm behind all scheduled storage events.

Reference for @annevk's comment.

In order to make my test valid, this means that I need to convert my double turn to be triggered by something which is queued in the DOM manipulation task source.

@nathanhammond
Copy link

nathanhammond commented Apr 18, 2018

New code which triggers in the DOM Manipulation Task Source (I've also modified the above code in my original comment to use this function instead of setTimeout):

function domManipulationTaskSourceCallback(handler) {
  const styleElem = document.createElement('style');
  styleElem.innerHTML = 'body {}';
  styleElem.addEventListener('load', () => {
    document.body.removeChild(styleElem);
    handler();
  }, { once: true });

  document.body.appendChild(styleElem);
}

With this scenario I'm trivially able to trigger localStorage collisions across processes in Safari and Firefox. I'm not able to trigger collisions across processes in Chrome.

To @foolip's original point,

If the way this is actually implemented is roughly interoperable, we should spec that. Needs thorough investigation.

My research implies to me that we are not roughly interoperable and that implementations do not presently have schedule guarantees about when the Storage object will be reconciled across processes.

@nathanhammond
Copy link

Thinking a bit more, there may exist a task source (possibly timer, given that I couldn't create collisions using it) between whose tasks we're guaranteed to have synchronized the Storage object. That would be valuable to specify if it exists.

@nathanhammond
Copy link

I've been unable to make this collide across processes:

function generateId() {
  return new Promise((resolve, reject) => {
    const current = window.localStorage.getItem('id');
    const next = (parseInt(current, 10) + 1).toString();

    window.localStorage.setItem('id', next);
    domManipulationTaskSourceCallback(() => {
      setTimeout(() => {
        domManipulationTaskSourceCallback(() => {
          if (idMessages.includes(next)) {
            resolve(generateId());
          } else {
            resolve(next);
          }
        });
      }, 0);
    });
  });
}

@annevk
Copy link
Member

annevk commented Apr 17, 2020

@foolip in whatwg/storage#18 I took a stab recently at defining the underpinnings of storage and decided to leave this aspect implementation-defined. If you have thoughts on how to improve upon that they'd be most welcome. Thanks!

@foolip
Copy link
Member Author

foolip commented May 13, 2020

Thanks @annevk! I see you already have reviewers on whatwg/storage#86, and I don't think I have much to add beyond "it would be good if this were defined" (this issue) but great to see it being fixed.

@annevk
Copy link
Member

annevk commented May 13, 2020

So to be clear, this issue is not being fixed, but many others are. It does provide a better foundation from which this issue could perhaps be tackled by someone willing to do the work though.

@asutherland
Copy link

asutherland commented Jan 22, 2022

The lack of clarity has come up in Firefox/Gecko in https://bugzilla.mozilla.org/show_bug.cgi?id=1740144 where the bug reporter provides a test case using BroadcastChannel which uses the DOM manipulation task source along with LocalStorage (which also uses the DOM manipulation task source).

As I conveyed in a recent-ish discussion on SessionStorage and events, and similar to what the first comment in the thread mentions, Firefox now uses a snapshot based mechanism that is flushed/committed at the end of the task when control returns to the event loop. This will also trigger "storage" events appropriately in other processes. (There is an optimization that will coalesce mutations, but if there are any "storage" listeners in other processes, mutations will not be coalesced so that we can provide full fidelity "storage" events like the key "foo" transitioning through a sequence of values rather than just receiving the final write value.)

We run into trouble in a situation like:

localStorage["foo"] = "justBeforeBroadcastChannel";
broadcastChannel.postMessage("message!");

In our implementation, the broadcast channel message is queued/broadcast immediately and logic in the other process can end up processing the message before "foo" takes on its new value of "justBeforeBroadcastChannel" (because the LocalStorage propagation happens at the end of the task which is strictly later).

We're tentatively considering that we would defer BroadcastChannel messages once we have a pending LocalStorage snapshot so that, at the end of the task, we would commit the snapshot, then send any BroadcastChannel messages, etc. This allows us to maintain run-to-completion semantics and relative task source ordering while decreasing the chance for content to experience inconsistencies, and without making LocalStorage even more powerful (ex: if it ended up being equivalent to SharedArrayBuffer across agent clusters!).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

4 participants