browser.storage.onChanged doesn't appear to work on Safari

I'm trying to use browser.storage.onChanged to detect when changes are made to browser.storage.local where I save all the preferences for my Safari Web Extension.

This works on Chrome, Firefox, Edge, and Opera. But I can't get it to work on Safari. When I call browser.storage.local.set, the onChanged listener is not called.

Below is the code I'm using to test this in Safari. This has to be in an extension with the Storage permission.

Given this works in all other browsers and I'm following the documentation, I'm inclined to believe this is a bug in Safari. Am I missing something?

Code Block
/* Function to handle changes to storage */
function handleChange(changes) {
for (var key in changes) {
var storageChange = changes[key];
console.log('onChanged triggered. '+
'Storage key "%s" changed. ' +
'Old value was "%s", new value is "%s".',
key,
storageChange.oldValue,
storageChange.newValue);
}
}
/* Add listener for storage changing */
browser.storage.onChanged.addListener(handleChange)
/* Confirm that the listener has been attached (expect "true" to be returned)*/
browser.storage.onChanged.hasListener(handleChange)
/* Set value for ['testKey] (should trigger onChanged event which calls handleChange) */
browser.storage.local.set({testKey: true}, () => { console.log('Storage updated'); });
/* Get current value for ['testKey] in storage.local, should return {testKey: true}*/
browser.storage.local.get(['testKey'], (result) => { console.log(result); });
/* Change value for ['testKey] (should trigger onChanged event which calls handleChange) */
browser.storage.local.set({testKey: false}, () => { console.log('Storage updated'); });


  • UPDATE: I ended up filing a bug Apple via the Feedback Assistant and they eventually fixed the bug (in Jan 2022). This now works as expected: the content script can listen for changes to storage on an options page with browser.storage.onChanged.

Add a Comment

Replies

I've been dealing with the same issue! As temporary solution, I had to fall back to polling the storage for safari:
Code Block javascript
const isSafari = chrome.runtime.getURL('').startsWith('safari-web-extension://');
if (isSafari) {
// browser.storage.onChanged.addListener
} else {
// setInterval browser.storage.sync.get
}


Yet this is no way to work with storage. Is there a better way to detect if this is supported or not? (especially once safari fixed the issue)

I've run into this same issue as well – any progress here?

I'm not able to reproduce this problem when using browser.storage.onChanged from a background page, it works as expected for me there. I copied your sample code verbatim.

Are you seeing this failing when registering the event handler from some other context (e.g. from an injected content script in other extension content like a popup)?
Same here.

We have listeners in the content script. In Chrome, when I set a new entry in the background page, the content script logs the change. In Safari, it doesn't work.
I am having the same exact issue. One workaround I have found for the time being is to use browser.tabs.sendMessage or browser.runtime.sendMessage.

For example if popup.js is updating the local storage and your content.js wants to listen to those changes you could send a message after updating the storage or even pass in the new storage itself.

Code Block
// popup.js
// notify current tab with new settings
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
  browser.tabs.sendMessage(tabs[0].id, newSettings)
})

Afterward, you can listen to this message notifications like so:

Code Block
// content.js
browser.storage.local.get(['settings'], (result) => {
    console.log(result.newSettings)
})

Hopefully everyone is seeing this fixed now?

Thanks for the follow-up Leggett!

Looks like it works well on Content Scripts, however, it doesn't work in extension pages embedded in iframes.

I have an html page (that lives inside the extension) that I inject in the target page inside an iframe. The browser.storage.local.onChanged.addListener is never triggered in that page.

Note: This works in Chrome, Firefox, Edge, and Opera

It looks something like this:

<!-- Target page -->
<body>
   <iframe src="safari-web-extension://<ID>/my-page.html">
      <html>
        <script type="text/javascript">
          chrome.storage.local.onChanged.addListener((details) => {
            // Is never triggered
            console.log(details)
          })
        </script>
      </html>

   </iframe>
</body>

Had to fallback to polling only for Safari.

It'd be good to know if this is a permissions issue or an actual bug in Safari.

Looks like it works well on Content Scripts, however, it doesn't work in extension pages embedded in iframes.

I have an html page (that lives inside the extension) that I inject in the target page inside an iframe. The browser.storage.local.onChanged.addListener is never triggered in that page. Note: This works in Chrome, Firefox, Edge, and Opera.

It looks something like this:

<!-- Target page -->
<body>
   <iframe src="safari-web-extension://<ID>/my-page.html">
      <html>
        <script type="text/javascript">
          chrome.storage.local.onChanged.addListener((details) => {
            // Is never triggered
            console.log(details)
          })
        </script>
      </html>

   </iframe>
</body>

Had to fallback to polling only for Safari.

It'd be good to know if this is a permissions issue or an actual bug in Safari.