-
Notifications
You must be signed in to change notification settings - Fork 62
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
Inconsistency: StorageArea callback order #292
Comments
In Safari the storage is multi-threaded, database backed — truly async. Given the async nature of all of these calls, I don't think FIFO should be guaranteed. This is a simple fix on the extension side by doing: await browser.storage.local.set({kitten, monster});
let kitten = await browser.storage.local.get("kitten");
let monster = await browser.storage.local.get("monster"); Or: browser.storage.local.set({kitten, monster}).then(() => {
setItem();
browser.storage.local.get("kitten").then(gotKitten, onError);
browser.storage.local.get("monster").then(gotMonster, onError);
}, onError); |
A few notes before the issue is discussed by the group. I ported some working code from Chrome to Safari. I didn't discover the bug in my Safari extension for some time, because the code was more complex than the simple sample code given above, and the bug was more subtle. Consequently, the fix was somewhat different than the one suggested. The issues as I see them:
|
re: FIFO (first in, first out) of sequentially queued async operations of the same API (i.e. storage) StorageArea.remove() & StorageArea.clear() are also noteworthy. // normally there isn't any conflict & FIFO shouldn't matter in remove()
browser.storage.local.remove("kitten");
browser.storage.local.set({monster});
// although not logical but just in case, where FIFO does matter
let kitten = { name: 'Lucy' };
browser.storage.local.remove("kitten");
kitten = { name: 'Balla' };
browser.storage.local.set({kitten, monster});
// clearing storage and starting fresh, where FIFO does matter
browser.storage.local.clear();
browser.storage.local.set({kitten, monster}); |
The storage methods are asynchronous by design. So the Safari behaviour is expected and if the implementation of Chrome and Firefox changes, it could behave exactly like Safari without any note to developers. It comes down to this: Say you have some code like this: console.log('a');
browser.storage.local.set({
something: true
}, () => {
console.log('b');
});
console.log('c'); It would display this in the console: This is something The Safari behaviour also seems to result in faster responses for these API calls as it does not have to keep a queue in the background. An easy fix would be to keep a local reference to the stored value like such: let currentValue = "value";
function doSomethingWithStorage () {
console.log(currentValue);
}
async function restoreValue () {
const result = await browser.storage.local.get(['key']);
currentValue = result.key;
}
function setNewValue (newValue) {
currentValue = newValue;
browser.storage.local.set({key: newValue});
}
function changeAndRefresh (newValue) {
setNewValue(newValue);
doSomethingWithStorage();
}
async function init () {
await restoreValue();
doSomethingWithStorage();
} |
Everyone understands that the storage methods are async, and that this sample code would output "a", "c", b". So it doesn't actually come down to this. The |
@lapcat let me clarify by adding the get request here. console.log('a');
browser.storage.local.set({
something: true
}, () => {
console.log('b');
});
console.log('c');
browser.storage.local.get("something", () => {
console.log('d');
});
console.log('e'); From what is mentioned in the post. Chrome and Firefox would output: While Safari outputs: The point I was trying to make is by design, the APIs are asynchronous so if "b" gets returned before or after "d" is undetermined and something the programmer can not rely on. |
As I said in my last comment, everyone knows this. There's no confusion about what an async callback is. We're all professionals here.
This does not follow. There are async queues that are still FIFO. It's commonplace in computing, and what I'm trying to say is that it's an implicit assumption of the API that's actually already documented to an extent. |
From the perspective of database consistency, consistency means AFTER a successful write, update or delete of a Record, any read request immediately receives the latest value of the Record. So what means For Chrome's document, I think it is just an example for how to use get and set. The docs explicitly say it may fail, and set |
The set() and get() calls are both async (and could both fail), so "immediately" doesn't really apply here. According to the principle that the developer can't depend in any way on the order of the callbacks, there's no guarantee that the value isn't stale even if get() is called inside a set() callback, because there's no guarantee that other set() calls and successful callbacks occur between the time that get() is called and its callback is called. Safari's implementation is multi-threaded, but with multi-threaded code where no ordering is guaranteed, multiple competing threads have the ability to acquire a lock, and while the lock is held by a thread, no other thread can change the shared data. The extension storage API is not locking, though, which makes it difficult for the developer to reason about the consistency of the storage, and why FIFO makes sense. |
Maybe. At least, the callback means the set operation is complete. So if not in set's callback, there is no guaranteed the set operation is complete. If no other set operation, get() in set's callback is guaranteed to read the updated data. If this does not satisfy your needs, I think browser.storage is not suitable. Try localStorage or IndexedDB. |
I would caution against this, since
|
@lapcat If you need operation order coherence like you seem to describe, the best thing to do is to store your data in variables local to the current execution context (instant synchronous access) and then just write them into StorageArea asynchronously. In general, you might have a problem of very frequent storage writes (e.g., on sync storage). I have seen lots of people using simple debouncing to work around this. There is also this monstrosity which guarantees instant writing without data races: https://github.com/darkreader/darkreader/blob/main/src/utils/state-manager-impl.ts |
Well, this issue is simply a report of web browser inconsistency, not a support request or a feature request. I didn't even know that I "need" operation order coherence, because it just happens automatically in Chrome and Firefox. It wasn't until porting code to Safari that I eventually discovered a problem. I'm aware of Safari's unique behavior now, so I can deal with it in my own code, but I'm not sure how many other developers are aware. I don't think that advice is on topic here; the only questions are whether the browser implementations should be consistent, and whether the MDN documentation should change. |
Are you sure for this. Browsers usually don't clear extensions data by default.
|
From what I understand, this topic has not been about how to use Topic is simply enquiring about FIFO (in the same API i.e.
Simple FIFO Test:
browser.storage.local.set({name: 'John'});
browser.storage.local.set({name: 'Lucy'}); |
FIFO implies that all operations(get/set/remove) are in a queue waiting for previous operations to complete. No document say that. So I support the second comment in this issue. The callback is used to guaranteed the operation is complete. I suggest improving the documentation to clarify this. |
That depends on whether there is a locking mechanism or not. I am only guessing but often databases employ locking mechanism to prevent data corruption when multiples processes attempt to write to the same data location. The lock is released when a process is completed therefore, they end up in a queue. |
It seems logical any write operations (set, remove, clear) should be FIFO by using a queue. I checked in all browsers and this seems to be the case right now. It would be good to get a confirmation on this from the browser vendors. The question then is, should read operations (get) be on the same queue. Having FIFO for get as well: Personally I would be in favour of having the get operations not using a queue as it leads to higher performance in extensions. |
I think the question is whether the web browsers should be consistent with each other and with the documentation.
Has anyone actually measured a performance difference, and if so, how much is it? The call is async either way. In any case, it seems unlikely that both Chrome and Firefox would change their implementations to match Safari. And maybe Safari won't change its implementation to match Chrome and Firefox either. But at the very least, the API documentation is currently misleading. |
Back to the topic, I suggest only clearing up this misunderstanding in the documentation so that developers don't assume any order of execution results. If order is important, use await statement or nested callback. |
We discussed this during 2022-10-27 meeting. There was consensus on having no guarantee on the order of the calls except for write-calls. So it is probably best to proceed with updating the documentation in mdn. Can browser vendors confirm the write-calls actually do use a queue? |
I just want to point out that this will not hold true forever. With the move to "ServiceWorkers" caching data like this will be invalid. When the "ServiceWorker" hits the "execution limit" it will get killed and you lose all your state. This is actually a major reason that many are upset about the "ServiceWorker" switch (go read the other issues if you want more context). |
The typical implementation is for an API call to be processed in the order of invocation. If order is important, you should |
In Chromium and Firefox, StorageArea.get() and StorageArea.set() have FIFO behavior. In Safari, they do not.
The MDN documentation assumes FIFO:
The sample code on MDN works as expected in Firefox:
But not in Safari:
(It's not clear from the console log, but
gotKitten
andgotMonster
are actually called beforesetItem
.)The Chrome documentation also seems to assume FIFO:
If you use for example
const value = "testing";
then the sample code works in Chrome:But not in Safari:
The Safari team has said that this works as designed, and there's no guarantee of order, but Safari is inconsistent with Chrome and Firefox, and Safari is broken with the instructional sample code for the API on MDN. This situation can be confusing and unexpected to extension developers.
Whatever the community decides about this issue, the documentation and sample code ought to be updated to make the behavior clear, specifying whether it's undefined or defined FIFO.
The text was updated successfully, but these errors were encountered: