Browser extension storage "get" return value if nothing found

Hello, according to: Browser storage get on MDN - the return value is not “null” or “empty” depending on the request parameters for the storage.

I am working on a extension which heavily relies on the storage data. To ease development, I wrote a small helper get/set function, here only the “get” function of the extension (Flag Cookies “get Storage” function on Github).

The real issue with the current “get” function in the browser extension, at least in my case/understanding, there is no “null” or “empty” or something along that line. I would expect, if nothing is found, nothing will be returned by the “get call”. Simple as that. :slight_smile:

Or is there another way to use the “promise” based approach but return nothing to a calling “real time” function in the extension? Perhaps when using “await” and a “async function” call?

Any help would be appreciated!

To distinguish between cases “key-value pair not found” and “key found, but value is set to null/undefined”, they created API that always returns an object - with, or without the key/value pair.

And your code is already checking that: if (Object.keys(value).length === 0) {

Additionally, by returning an object, you can return more than one key-value pair!
Which is used when you ask for multiple values (you can pass an array of strings and you will get object with multiple keys/values).

Note that you can also use destructing:

const {hello} = await browser.storage.local.get('hello');
// hello will be undefined

And you can also use default values:

const {hello = 'hi'} = await browser.storage.local.get('hello');
// hello will be set to 'hi'

AND you can even rename it if you don’t like it:

const {hello: hi = 'hi'} = await browser.storage.local.get('hello');
// hi will be set to 'hi'

:open_mouth:

NOTE: Firefox supports also Chrome namespace, so you can just use “chrome” everywhere. Although, it’s likely better to use “browser” everywhere and use webextension-polyfill in Chrome.

Sorry for the delayed response.

And here is the exact problem, if “no matching element is defined” the API always, after testing your examples, returns something but nothing what is desired. From what I understand at present there is simply no “false/empty/null” or what so ever value returned.

Setting a default for the variable does not work out (as I tried to do) and always returns at least the request keys.

I do not exactly understand why returning nothing is such a difficult thing.

As a suggestion on what I would do:
Provide the “get” storage API function a additional and optional parameter which is returned if nothing matches the keys.

This would make sense in such a case that it would be easy possible to have a default if really needed and let it be any value required. Either a bool, a string or another object or whatever.

If you request a single key and that key is not found, the get() call will return an empty object. In JavaScript, you can distinguish between an undefined property and a defined property with the value undefined. There are a few different ways to do this, but I’d suggest using Object.hasOwn.

let data;
// This storage area is empty
data = await browser.storage.local.get(null);
console.log("data is", data);                   // data is {}

// Check for a known key that has NOT been set
data = await browser.storage.local.get("greeting");
console.log("data is", data);                   // data is {}
console.log("Is 'greeting' set?");              // Is 'greeting' set?
console.log(Object.hasOwn(data, "greeting"));   // false

// Check for a known key that HAS been set
await browser.storage.local.set({greeting: "Hello, world!"});
data = await browser.storage.local.get("greeting");
console.log("data is", data);                   // data is {greeting: "Hello, world!"}
console.log("Is 'greeting' set?");              // Is 'greeting' set?
console.log(Object.hasOwn(data, "greeting"));   // true

The API supports something similar to what you’ve suggested, but more granular. By calling the get() method with an object, you can set the default value for each key that doesn’t have a value in the storage area.

data = await browser.storage.local.get({
  "greeting": "Hello!",
  "username": "DEFAULT"
});

// Default value is NOT used (value has already been set)
console.log("greeting", data.greeting); // Hello, world!

// Default value IS used (value has not been set)
console.log("username", data.username); // DEFAULT

Thank you for your suggestions and help.

There is one issue which I am uncertain about, if the “request” (Object) is exactly the same as what is “delivered”, there is no way to test if the “value” was retrieved from the store or if it is just a simple return in case the value was not found.

According to: get - Return value

[…] If keys is an object, keys that are not found in the storage area will have their values given by the keys object.

The point, if “keys” is an object the value provided by the “get call” is returned, “if not found”.

  • I have always a “Object get request”.

This means, if the request is the same as what “might be” expected the “get call” will always return something, even so the value is not present in the storage.

For example this code parts:

grafik

In both parts the request object is populated with the input of the function.

But how do I apply a sane solution to something like this?
request = { [keyStore]: { [key]: targetValue } };
request = { [keyStore]: key };

If the values are present, it should exactly return the values reqeuested. “But” if not present it exactly returns the same “object” as stated in the (get - Return value) link above.

Do I miss anything here?

Correct. If you want to see if storage contains a value for a given key, then you should not call get() with an object. Instead, you should call get() with a string to retrieve one value from storage or an array of strings to retrieve multiple values. Once the get() call resolves, you can use Object.hasOwn() to check if a given key is defined on the object and respond appropriately.

Apologies, but I don’t follow the problem you’re trying to solve in your getStoreValue() function. If you could provide some examples of the call pattern you’d like and the return value you expect, I may be able to show how to implement the function.

Hello,

following is the data structure for “google.de” only on a fresh session.
Requested with:
await browser.storage.local.get();

The data is build up as follows:

“firefox-default” = name of the current (contextual) profile, may change and is not static depending on the user context settings!
www.google.de” = the domain the rules are placed on.
“.google.de” a domain where cookies originated from inside “www.google.de”.

“flagCookies_logged” is the main container for “profile” cookies data
It contains the profile, domain, and “target website name”.

  {
    "firefox-default": {
      "www.google.de": {
        ".google.de": {
          "AEC": true,
          "SOCS": false
        }
      }
    },
    "flagCookies_logged": {
      "firefox-default": {
        "www.google.de": {
          ".google.de": {
            "__Secure-ENID": true
          }
        }
      }
    }
  }

What I now want to archive, request for example only “www.google.de” of the context “firefox-default” inside “flagCookies_logged” and see if there are any rules present or does nothing exist.

The following screenshot shows what has been “activated” in FlagCookies - creating the above settings in the store:

Note: Do not be fooled by the UI when reopening the FlagCookies window, it seems something is utterly broken of the display…

Thanks for the additional info. I think I understand your data model and what you’re trying to do. I’m assuming that the JSON object you provided represents the data in storage.local and that the strings firefox-default and www.google.de are they keys used to access those values. Also, if I understand correctly, you’re going to be accessing the firefox-default and www.google.de storage keys separately.

If I’ve got that right, then you could do something like this

async function getSiteRules({container, profile, domain}={}) {
    const storageKey = container || profile;
    const data = await browser.storage.local.get([storageKey]);
    if (container) {
        data = data[container];
    }
    return data?.[profile]?.[domain];
}

// Usage examples

await getSiteRules({
    container: "flagCookies_logged",
    profile: "firefox-default",
    domain: "www.google.de",
});
// Returns
// {
//   ".google.de": {
//     "__Secure-ENID": true
//   }
// }

await getSiteRules({
    profile: "firefox-default",
    domain: "www.google.de",
});
// Returns: 
// {
//   ".google.de": {
//     "AEC": true,
//     "SOCS": false
//   }
// }

await getSiteRules({
    profile: "unset",
    domain: "www.google.de",
});
// Returns
// undefined

If you’re doing a lot of work with deeply nested objects, you might want to take a look at a library like Lodash. It has a number of very handy utilities like _.get() to retrieve a deeply nested property of an object or _.merge() to perform a deep merge of two objects. You can use them to, for example, get all of the rules for a given site in one go:

async function getAllSiteRules({container, profile, domain} = {}) {
  const storageData = browser.storage.local.get([profile, container]);
  
  const globalValue = _.get(storageData, [profile, domain]);
  const containerValue = _.get(storageData, [container, profile, domain]);
  
  const final = _.merge(globalValue, containerValue);
  return final;
}

// Usage example

await getAllSiteRules(({
    container: "flagCookies_logged",
    profile: "firefox-default",
    domain: "www.google.de",
});
// Returns
// {
//   ".google.de": {
//     "AEC": true,
//     "SOCS": false,
//     "__Secure-ENID": true
//   }
// }

Hello @dotproto - thank you for providing a possible solution.
Unfortunately it does not solve the initial issue I am facing.

If I understand correctly, above code, no matter what, will return something, even if nothing is found in the storage falling back to what was requested by “[storageKey]” because it is a object (key)?

The only “real” solution I can thing of, saving a value inside the data for each domain, which is only returned and visible if the data was pulled out of the browser.storage.local - so to speak to request a object and simply test for a boolean, sort of, which only is returned if data is present in the storage. Does this make sense what I mean? :slight_smile:

From point of implementation this would make sense, but in general it does not make sense that the requested data is “lazy” returned if nothing is found in the particular storage. I do not understand why this might be a “useful” default behavior.

Adding a additional value (as proof of returned data in the store) to the store, would just blow up the data amount for each ruleset, even if it just a single byte for each entry/domain requested.