Skip to content

Instantly share code, notes, and snippets.

@WebReflection
Last active August 29, 2015 13:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save WebReflection/9010248 to your computer and use it in GitHub Desktop.
Save WebReflection/9010248 to your computer and use it in GitHub Desktop.
DOM NamedStorage Interface Proposal with polyfills for ES3, ES5, and ES6

DOM NamedStorage Interface Proposal

Multiple scripts and multiple libraries have usually a common need of the W3C Web Storage, either the local or the session one.

However, it is extremely easy and very hard to prevent a not so infrequent storage.clear() call, able to remove any sort of data and for any kind of lbirary in the same realm.

A Possible Solution

This proposal aim is to harmonize multiple libraries preserving same interface but enriching the constructor.

Please note that current WebIDL is almost identical to the one already recommended by W3C.

[NamedStorage(
   DOMString name,  // the database name
   DOMString? type  // "session" or "local", the default
 )]
interface NamedStorage {
  readonly attribute unsigned long length;
  DOMString? key(unsigned long index);
  getter DOMString? getItem(DOMString key);
  setter creator void setItem(DOMString key, DOMString value);
  deleter void removeItem(DOMString key);
  void clear();

  // a backward compatible .length proposal
  unsigned long size(); // same as storage.length
  // not that important

};

About the NamedStorage name

In the following polyfills the name is used as prefix for all keys. A hopefully not common char as \x01 is is used to separate the prefix, containing such name, from the rest of the key. This will (weakly) ensure that no conflicts should happen when more instances of the storage are created around.

The Storage Size

This proposal aim is not to suggest multiple Storage instances or 5MB or more space per each of them, this proposal aim is to distribute via namespaces tasks performed within localStorage or sessionStorage although limiting the risk to compromise other libraries operating with these globally available objects.

ES3 Implementation Example

In this example, the returned object methods are implicitly bound to the storage reference in order to avoid exposing it and/or change it outside the closure. It is also easier later on to extend this private property through more modern versions of EcmaScript.

function NamedStorage(name, type) {
  var keys = function () {
        var i = storage.length,
            list = [],
            k;
        while (i--) {
          k = storage.key(i);
          if (!k.indexOf(prefix)) {
            list.push(k);
          }
        }
        return list;
      },
      storage = window[(type || 'local') + 'Storage'],
      prefix  = name +
                '\x01';
  return {
    size: function () {
      return keys().length;
    },
    key: function (index) {
      var k = storage.key(index);
      return k && k.slice(prefix.length);
    },
    getItem: function (key) {
      return storage.getItem(prefix + key);
    },
    setItem: function (key, value) {
      storage.setItem(prefix + key, value);
    },
    removeItem: function (key) {
      storage.removeItem(prefix + key);
    },
    clear: function () {
      var remove = keys(),
          i = remove.length;
      while (i--) {
        storage.removeItem(remove[i]);
      }
    }
  };
}

ES5 Implementation Example

Recycling what already written for ES3, the ES5 version behaves closer to the WebIDL specification exposing length and flagging properties and methods as not enumerable.

var NamedStorage = function(NamedStorage){
  function set() {
    throw new Error('storage.length is read only');
  }
  function defineProperty(key) {
    Object.defineProperty(
      this, key, {
        configurable: false,
        enumerable:false,
        writable:false,
        value: this[key]
      }
    );
  }
  return function ES5NamedStorage(name, type) {
    var storage = NamedStorage(name, type);
    Object.keys(storage).forEach(
      // make them all not enumerable
      defineProperty, storage
    );
    return Object.defineProperty(
      storage,
      'length', {
        configurable: false,
        enumerable:false,
        get: storage.size,
        set: set
      }
    );
  };
}(NamedStorage);

ES6 Implementation Example

Still recycling code up to the ES3 version, most modern browsers could simulate the same intent of the proposed WebIDL through an ES6 Proxy as showed below:

var NamedStorage = function(NamedStorage){
  var
    keys = function (storage) {
      for (var list = [], i = storage.length; i--;) {
        list[i] = storage.key(i);
      }
      return list;
    },
    has = function (storage, key) {
      return -1 < keys(storage).indexOf(key);
    },
    handler = {
      deleteProperty: function (storage, key) {
        // yes, always true
        return !storage.removeItem(key);
      },
      has: has,
      hasOwn: has,
      get: function (storage, key, receiver) {
        return storage.hasOwnProperty(key) ?
          storage[key] : storage.getItem(key);
      },
      set: function (storage, key, value, receiver) {
        storage.setItem(key, value);
      },
      enumerate: function (storage) {
        return keys(storage);
      }
    }
  ;
  return function ES6NamedStorage(name, type) {
    return new Proxy(NamedStorage(name, type), handler);
  };
}(NamedStorage);

Example

The following example should hopefully represent the intent of this proposal.

In this case, there are two concurrent NamedStorage instances using indirectly the sessionStorage instead of the default localStorage one.

Please note how these two instances are unable to directly break each other values.

var ns1 = new NamedStorage('ns1', 'session'),
    ns2 = new NamedStorage('ns2', 'session');

alert([
  (ns1.clear(), ns1.size()),        // 0
  (ns2.clear(), ns2.size()),        // 0
  (ns1.setItem('key', 'value-ns1'),
    ns1.size()),                    // 1
  (ns2.setItem('key', 'value-ns2'),
    ns2.size()),                    // 1
  ns1.getItem('key'),               // value-ns1
  ns2.getItem('key'),               // value-ns2
  (ns1.clear(),
    ns1.getItem('key')),            // undefined
  ns2.getItem('key'),               // value-ns2
  ns1.size(),                       // 0
  ns2.size(),                       // 1
  ns2.length                        // 1 in modern browsers
].join('\n'));

for(var k in ns1) alert(k);         // nothing in modern browsers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment