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.
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
};
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.
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.
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]);
}
}
};
}
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);
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);
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