Inconsistency when writing synchronous to localStorage from multiple tabs - javascript

tl;dr I noticed inconsistent behaviour between browsers when writing to localStorage at the exact same time.
Requirement: Even when multiple tabs are open, a specific action (POST-request to refresh OAuth session) should be executed only once. Which tab executes the action does not matter. The point in time to do the refresh derives from the expiration time of the session and is the exact same in all tabs.
Approch: All tabs generate a random number, store it and write to localStorage. They then read the localStorage and if both are the same, then the tab is allowed to execute the action.
let tab = Math.random();
localStorage.setItem('tab',tab);
if(JSON.parse(localStorage.getItem('tab')) === tab) {
console.log('aquired lock');
} else {
console.log('did not aquire lock');
}
JSFiddle - In order to test the behaviour you need to open the fiddle in two tabs and then press Run in both. The timeout is calculated to execute at the next full 10-seconds. (Second 0, 10, 20, 30, 40, 50)
Expectation: Tabs A and B set localStorage['tab'] to a random value, on retriving the value only one tab retrieves the same value as it randomly generated and therefore is allowed to execute the action.
Result: Both A and B still retrieve their own generated value.
I added some timeout the let the memory-dust settle:
let tab = Math.random();
localStorage.setItem('tab',tab);
setTimeout(function(){
if(JSON.parse(localStorage.getItem('tab')) === tab) {
console.log('aquired lock');
} else {
console.log('did not aquire lock');
}
}, 1000);
JSFiddle
Result (Firefox): Tab A retrieves value tab B generated, and vice versa. So no tab is allowed to execute the action.
This is where I got a bit spooked. I checked for console-timestamps, which where exactly the same, and the localStorage in the dev-tools, which showed different values in the different tabs. (Even reloading the tab did show different values in different tabs.)
If writing the value later on (e.g. via the console) all tabs update the value accordingly.
Result (Chrome, Edge): Only one tab logs aquired lock as expected.
Is there any explaination why Firefox can have different values in localStorage per tab?
I already solved the problem by subscribing to the StorageEvent. The tab with the smallest random-number gets to executed the action.
Used browsers:
Firefox 68.0 and 60.8.0esr (both 64bit)
Chrome 75.0.3770.142 (64bit)
Edge 44.18362.1.0

This is a fairly standard situation when dealing with shared memory. Each thread accessing shared memory is allowed to keep its own copy (a "cache") for performance reasons until/unless some synchronization occurs, at which point the local copy must be reconciled with the shared copy.
The old storage specification talked about acquiring a storage mutex on every storage operation:
Whenever the properties of a localStorage attribute's Storage object are to be examined, returned, set, or deleted, whether as part of a direct property access, when checking for the presence of a property, during property enumeration, when determining the number of properties present, or as part of the execution of any of the methods or attributes defined on the Storage interface, the user agent must first obtain the storage mutex.
But that specification has been subsumed into the WHAT-WG "HTML" specification (which is about a lot more than HTML) in §11 ("Web Storage") and the requirement that every operation must acquire a storage mutex has been dropped. (I don't know why, but I would guess for performance reasons.) The current specification says:
Warning: The localStorage attribute provides access to shared state. This specification does not define the interaction with other browsing contexts in a multiprocess user agent, and authors are encouraged to assume that there is no locking mechanism.
The specification also doesn't discuss synchronization of storage across browsing contexts. That means implementations are free to optimize.
Looking into it with a modified version of your script, it looks like Firefox optimizes by having a local copy of local storage for each browsing context (tab) which it appears to update based on the storage event from other contexts (tabs). But if both tabs set the value (generating a storage event for the other tab) before the storage event from the other tab is processed, they both get and process the storage event from the other, updating with that value (the other tab's value), causing the behavior you describe.
Side note: The operation writing to persistent storage (what a third tab would see if you opened it after doing all this) also appears to be asynchronous, and the two tabs are in a race to see which one writes last (a race that is not always won by the last one writing to its local copy!).
This is effectively a large-scale version of what happens with shared memory between threads when there's only loose synchronization between the threads and no locking semantics, which the spec no longer requires.
Chrome would appear to be doing locking or similar.

Related

Is it possible to get a tab (or window) identifier that stays the same across page reloads?

Question:
Is there an api or way to determine some kind of an id/uuid of the current tab/window the page is displayed in that is stable when the page is refreshed?
To give you more context why this is important to me:
My use case is to allow my single page app (SPA) to be opened in multiple tabs at the same time.
Right now I can't do it because while the current state is persisted in a "currentModel" property in localStorage, unfortunately not all urls of the SPA are "self-contained" . By "self-contained" I mean that the url itself should be sufficient to reload that state from the backend (or localStorage) because it contains some kind of id/uuid (this flaw of my SPA has grown historically and I know it's not what HTML was meant to be used, however as there's a team of 60+ developers working on that SPA you can imagine that this cannot be refactored in a single day).
So what I have are some urls like this "/search" that display a search form, but also information about the current context (i.e. think business object).
Now if I want to allow the SPA to be opened simultaneously in multiple tabs, I can no longer use a simple "currentModel" property. Instead I need to make it a hashmap with the keys being tab/window ids so that the context of a tab can be restored in case the user hits F5/refresh in one of the tabs. Of course this id whatsoever would need to be stable across page reloads.
Workarounds that I know of
Writing a custom browser extension that has access to tabs
Updating the url to include a "tab"-id created when the SPA is bootstrapped
The window.name property (at least in modern Firefox and Chrome) maintains its value across an F5 page reload. Thus, code can check window.name for navigational hints or whatever else might be useful in order for it to "get its bearings" in the absence of information in the URL itself.
Thus, a newly-loaded tab will be able to detect that it is in fact newly loaded, and can insert and update the name property of the window whenever it wants. Upon a reload, code can sniff the name and request additional stuff from the server (or do whatever) if it sees that sort of information already present in the window name.
I have been able to put any sort of string into that property, in quite limited tests, but it's possible IE is picky about the name having to have some particular form (like a valid identifier or something).
You may still be able to leverage localStorage but instead of keeping one object representing your state, maybe you should keep state on a route-by-route basis?
const state = {
"/": { state: {} },
"/page-2": { state: {} }
}
// persist state
localStorage.setItem('state', JSON.stringify(state))
// unwrap state for route /
const routeState = (JSON.parse(localStorage.getItem('state')))['/']
You can try leveraging the window.onbeforeunload action to save state for a page when a user refreshes the page. You could do something like:
window.onbeforeunload = function(event) {
localStorage.setItem('lastState', JSON.stringify(state))
}
And then when a page reloads, check if state exists in localStorage, load it, and remove it. This method assumes that users won't be reloading pages and opening new ones at the same time. Otherwise, you'd need some other identifier when saving the state which puts you back in your original predicament.

What happens if a write to localStorage is canceled?

Let's say I make a call to local storage like so:
window.localStorage.setItem("key", bigJsonObject);
And immediately afterwards, the user closes their web browser. What will be the result of
window.localStorage.getItem("key")
Will the bigJsonObject be partially written? Or will the whole write fail? Is their any way to guarantee that there will be no partial writes?
Refer to §4.1 of the "web storage" specification:
The setItem() and removeItem() methods must be atomic with respect to failure. In the case of failure, the method does nothing. That is, changes to the data storage area must either be successful, or the data storage area must not be changed at all.
However, there have (historically) been browser bugs in this regard, e.g. some time before Chrome 21 until some time before Chrome 29.

Using localstorage checks in a different window

I'm trying to make a basic webapp. It's basically a puzzle that appears over time, when you find certain links or URLs.
The puzzle has 8 pieces, and they appear when you visit a certain hash. The hashes are setup using backbone.js, and they each trigger a function that shows the piece that corresponds with it.
The hashes go like this - "index.html#hide/one", "index.html#hide/two", up to "index.html#hide/eight".
Every time a hash is triggered, it shows a piece using a JavaScript function that simply adds a class to the element. Easy enough, right?
The problem is, the hashes open in a different window. The main window is just "index.html#hide". So I need to create a localstorage value for each piece, constantly check it on the main page, and if it's set to "yes", execute a function.
Is this possible? And if so, how could I go about doing it?
Thanks in advance,
-Mitchyl
Edit - Here's the source code if anyone's interested. I'm not quite sure what's relevant and what's not, so here's the code in it's entirety. http://pastebin.com/Q4hpJtQ8
Rather than polling LocalStorage from the main window, it's probably better to use some sort of direct communication between the windows.
If all your windows are the same origin (same protocol, domain and port) and one window opens the others, then you can directly call JS functions from one window to another as long as you save the window handle. For windows that your main window opened, the main window will be in window.opener so you could just call a globally defined function in the main window like:
window.opener.updatePuzzle(data);
You can also use window.postMessage() to exchange data between windows which seems (on the surface) a bit cleaner and isn't as restrictive about same origin (because it requires two cooperating windows), but there are limitations with postMessage() in IE before IE11 which still make it difficult to rely on (thanks IE).
If your main window did not open the other windows in any way (e.g. the user just typed the other URLs), then your windows cannot directly communicate with one another within the browser. In that case, they'd either have to exchange data via a server or poll for data via LocalStorage.
Here's a pretty good tutorial on LocalStorage: http://www.sitepoint.com/an-overview-of-the-web-storage-api/:
From one window:
localStorage.setItem("hash4", true);
From the other window:
setInterval(function() {
var maxHash = 5;
for (var i = 0; i < maxHash; i++) {
var val = localStorage.getItem("hash" + i)
if (val) {
// hash value i was found to be true
}
}
}, 1000);
As it sounds like you're doing this on mobile, you should know that polling continuously is not ideal on mobile for a bunch of reasons. First off, it's a battery drain when it's allowed to run. Second off, the mobile device will probably not let it run continuously (as it tries to save your battery).
You could also be a bit more sophisticated about how you store and rather than use N separate keys, you could store JSON in one key that represented an object that contained all the values in one LocalStorage key.

How to clean chrome in-memory cache?

I'm developing an extension in chrome and I'm trying to perform an action each time a user searches in Google. Currently I'm using chrome.webRequest onBeforeRequest listener. It works perfectly most of the cases but some of the requests are done through the cache and doesn't perform any call. I've found this in the API documentation about caching:
Chrome employs two caches — an on-disk cache and a very fast in-memory cache. The lifetime of an in-memory cache is attached to the lifetime of a render process, which roughly corresponds to a tab. Requests that are answered from the in-memory cache are invisible to the web request API. If a request handler changes its behavior (for example, the behavior according to which requests are blocked), a simple page refresh might not respect this changed behavior. To make sure the behavior change goes through, call handlerBehaviorChanged() to flush the in-memory cache. But don't do it often; flushing the cache is a very expensive operation. You don't need to call handlerBehaviorChanged() after registering or unregistering an event listener.
I've tried using the handlerBehaviorChanged() method to empty the in-memory cache, but there was no difference. Although it's not recommended I've even tried to call it after every request.
This is my code:
chrome.webRequest.MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES = 1000;
chrome.webRequest.onBeforeRequest.addListener(function (details) {
//perform action
chrome.webRequest.handlerBehaviorChanged();
} {
urls: ["*://*.google.com/*"]
});
Is there any way to empty/disable this in-memory cache from the extension?
I asume the "Caching" is performed by the Google-Website with some crazy JavaScript in Objects, Arrays,... so emptying the browser in Memory-Cache won't help.
My first thought was that the data was Stored in the sessionStorage (due to the fact that the Values had the search-term in them [here I searched for test] and are updated/created on every request/change of the selected "search-word"
)
I tried clearing the Sessionstorage (even periodicaly), but it didn't really change the "not"-loading, further more the storage was recreated and even without the storage, the different results were displayed.
Due to this Information and the fact that I can't check several 1000 lines of minfied JavaScript Code, I just can asume that the website does the caching of the requests. I hope this Information can point you in the right direction.

How to set the Returning Property in KISSMetrics?

I am integrating KISSMetrics into my rails app via Javascript and Rails itself (depending on the event type).
How do I set the Returning Property for visitors in KISSMetrics?
The documentation implies that it's automatically set by KISS, as long as my app identifies the user consistently. I've confirmed that KISS is able to identify the user consistently via the Live dashboard, which shows that KISS has logged the anonymized user ID number, URL and referrer property for the visit.
However, despite repeat visits to my test site with the same user, I do not see any Returning property being set. It's not shown in the Live dashboard. Nor is it available as a property in reports or metrics.
Am I missing a step here? Or do I need to wait a day for? I've already given it about 8 hours, whereas the guidance on delays in KISS seems to be anywhere from a few minutes to 6 hours.
Returning parameter is set automatically by the system - nothing needs to be configured. The confusion is that there is a significant delay between when the event first happens (a returning visitor) and when it is reported by their system. This is in contrast to Rails-fired events (seen almost immediately).

Categories

Resources