
class ExtStore {
  constructor(values, defaults) {
    this._isInit = false;

    this._init = async function () {
      if (this._isInit) return;
  
      await Promise.all(Object.keys(values).map((key) => {
        return this.setIfNull(values[key], defaults[key]);
      }));
  
      this._isInit = true;
    }

    this._setLocal = function (obj) {
      return new Promise(function (resolve) {
        const entries = Object.entries(obj);

        if (entries.length > 0) {
          const [key, value] = entries[0];
          window.localStorage.setItem(key, value);
          resolve();
        }
      });
    }

    this._getLocal = function (key) {
      return new Promise(function (resolve) {
        const value = window.localStorage.getItem(key);
        resolve({[key]: value});
      });
    };

    this._removeLocal = promisify(window.localStorage, 'removeItem');

    // Safari hasn't supported storage at extension level yet. Fallback to localstorage
    if (isSafari()) {
      this._setAsync = this._setLocal;
      this._getAsync = this._getLocal;
      this._removeAsync = this._removeLocal;
    } else {
      this._setAsync = promisify(chrome.storage.local, 'set');
      this._getAsync = promisify(chrome.storage.local, 'get');
      this._removeAsync = promisify(chrome.storage.local, 'remove');
    }
  }

  async _innerGet (key) {
    const result = key.endsWith('local')
      ? await this._getLocal(key)
      : await this._getAsync(key);

    return result[key];
  }

  _innerSet (key, value) {
    const payload = {[key]: value};
    return key.endsWith('local')
      ? this._setLocal(payload)
      : this._setAsync(payload);
  }

  async set(key, value) {
    if (!this._isInit) await this._init();

    return this._innerSet(key, value);
  }

  async get(key) {
    if (!this._isInit) await this._init();

    return this._innerGet(key);
  }

  remove(key) {
    return this._removeAsync(key);
  }

  async setIfNull(key, val) {
    const existingVal = await this._innerGet(key);
    if (existingVal == null) {
      await this._innerSet(key, val);
    }
  }
}

/**
 *
 * @param {context function} fn
 * @param {method in context function} method
 *
 * Passing method to call context function in order to remain the context execution
 */
function promisify(fn, method) {
  if (typeof fn[method] !== 'function') {
    throw new Error(`promisify: fn does not have ${method} method`);
  }

  return function(...args) {
    return new Promise(function(resolve, reject) {
      fn[method](...args, function(res) {
        // No method for detecting error in Safari extension
        if (isSafari()) {
          return resolve(res);
        }

        if (chrome.runtime.lastError) {
          reject(chrome.runtime.lastError);
        } else {
          resolve(res);
        }
      });
    });
  };
}

function isSafari() {
  return typeof safari !== 'undefined' && safari.self && typeof safari.self.addEventListener === 'function';
}

window.extStore = new ExtStore(STORE, DEFAULTS)

@@ -314,505 +314,8 @@
  %7D%0A%0A
-    this._setLocal = function (obj) %7B%0A      return new Promise(function (resolve) %7B%0A        const entries = Object.entries(obj);%0A%0A        if (entries.length %3E 0) %7B%0A          const %5Bkey, value%5D = entries%5B0%5D;%0A          window.localStorage.setItem(key, value);%0A          resolve();%0A        %7D%0A      %7D);%0A    %7D%0A%0A    this._getLocal = function (key) %7B%0A      return new Promise(function (resolve) %7B%0A        const value = window.localStorage.getItem(key);%0A        resolve(%7B%5Bkey%5D: value%7D);%0A      %7D);%0A    %7D;%0A%0A
     
@@ -829,24 +829,838 @@
 %0A    %7D%0A  %7D%0A%0A
+  _getLocal (key) %7B%0A    return new Promise(function (resolve) %7B%0A      var value = parse(localStorage.getItem(key));%0A      resolve(%7B%5Bkey%5D: value%7D);%0A    %7D);%0A%0A    function parse(val) %7B%0A      try %7B%0A        return JSON.parse(val);%0A      %7D catch (e) %7B%0A        return val;%0A      %7D%0A    %7D%0A  %7D%0A%0A  _setLocal (obj) %7B%0A    return new Promise(function (resolve) %7B%0A      const entries = Object.entries(obj);%0A%0A      if (entries.length %3E 0) %7B%0A        const %5Bkey, value%5D = entries%5B0%5D;%0A        try %7B%0A          localStorage.setItem(key, JSON.stringify(value));%0A        %7D catch (e) %7B%0A          const msg =%0A            'Octotree cannot save its settings. ' +%0A            'If the local storage for this domain is full, please clean it up and try again.';%0A          console.error(msg, e);%0A        %7D%0A        resolve();%0A      %7D%0A    %7D);%0A  %7D%0A%0A
   async _inn

Allow saving object to storage (#836)