Skip to content

Instantly share code, notes, and snippets.

@bakoushin
Created November 13, 2018 14:30
Show Gist options
  • Save bakoushin/273d3b5821fb8874b8aba910ebe369a4 to your computer and use it in GitHub Desktop.
Save bakoushin/273d3b5821fb8874b8aba910ebe369a4 to your computer and use it in GitHub Desktop.
Simple DB

SimpleDB - Like Indexed DB, but Simple

A simple asynchronous data store.

STATUS: This is a thought experiment, not a serious proposal. Would basic async storage like this be useful? With this plus some locking primitive, could you build Indexed DB?

Like Indexed DB:

  • rich value types - store anything you can structured clone
  • rich key types - Number, String, Date, Array (of other key types)
  • key/value enumeration
  • asynchronous, to avoid jank
  • origin-scoped, with named partitions within each origin
  • A.C.I.D.

Unlike Indexed DB:

  • Promise-based
  • no schemas - a database is a single key/value mapping, no stores/indexes/keypaths/generators
  • no transactions - all operations are FIFO
  • no events
  • no cursors - just callbacks
[NoInterfaceObject] interface GlobalSimpleDB {
readonly attribute SimpleDBFactory simpleDB;
};
Window implements GlobalSimpleDB;
Worker implements GlobalSimpleDB;
interface SimpleDBFactory {
Promise<SimpleDB> open(DOMString name);
Promise<void> delete(DOMString name);
// TODO: Enumerate?
};
// Same as Indexed DB keys:
typedef (double or Date or DOMString or sequence<SimpleDBKey>) SimpleDBKey;
typedef IDBKeyRange SimpleDBKeyRange;
enum SimpleDBIterationDirection {
"forward",
"reverse"
};
// TODO: Allow key-only iteration.
dictionary SimpleDBForEachOptions {
SimpleDBKeyRange range = null;
SimpleDBIterationDirection direction = "forward";
};
dictionary SimpleDBKeyValue {
SimpleDBKey key;
any value;
};
callback SimpleDBForEachCallback = bool (SimpleDBKey key, optional any value);
interface SimpleDB {
static short cmp(SimpleDBKey a, SimpleDBKey b);
Promise<any> get(SimpleDBKey key);
Promise<void> set(SimpleDBKey key, any value);
Promise<void> delete(SimpleDBKey key);
// TODO: Premature optimization?
Promise<sequence<any>> getMany(sequence<SimpleDBKey> keys);
Promise<void> setMany(sequence<SimpleDBKeyValue> entries);
Promise<void> deleteMany(sequence<SimpleDBKey> keys);
Promise<void> clear();
// Can't mutate while iterating
// Return true to terminate iteration
Promise<void> forEach(SimpleDBForEachCallback callback, options SimpleDBForEachOptions options);
readonly attribute DOMString name;
};
(function(global) {
var SECRET = Object.create(null);
var DB_PREFIX = '$SimpleDB$';
var STORE = 'store';
function SimpleDBFactory(secret) {
if (secret !== SECRET) throw TypeError('Invalid constructor');
}
SimpleDBFactory.prototype = {
open: function(name) {
return new Promise(function(resolve, reject) {
var request = indexedDB.open(DB_PREFIX + name);
request.onupgradeneeded = function() {
var db = request.result;
db.createObjectStore(STORE);
};
request.onsuccess = function() {
var db = request.result;
resolve(new SimpleDB(SECRET, name, db));
};
request.onerror = function() {
reject(request.error);
};
});
},
delete: function(name) {
return new Promise(function(resolve, reject) {
var request = indexedDB.deleteDatabase(DB_PREFIX + name);
request.onsuccess = function() {
resolve(undefined);
};
request.onerror = function() {
reject(request.error);
};
});
}
};
function SimpleDB(secret, name, db) {
if (secret !== SECRET) throw TypeError('Invalid constructor');
this._name = name;
this._db = db;
}
SimpleDB.cmp = indexedDB.cmp;
SimpleDB.prototype = {
get name() {
return this._name;
},
get: function(key) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var req = store.get(key);
// NOTE: Could also use req.onsuccess/onerror
tx.oncomplete = function() { resolve(req.result); };
tx.onabort = function() { reject(tx.error); };
});
},
set: function(key, value) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var req = store.put(value, key);
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
},
delete: function(key) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var req = store.delete(key);
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
},
clear: function() {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var request = store.clear();
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
},
forEach: function(callback, options) {
var that = this;
return new Promise(function(resolve, reject) {
options = options || {};
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var request = store.openCursor(
options.range,
options.direction === 'reverse' ? 'prev' : 'next');
request.onsuccess = function() {
var cursor = request.result;
if (!cursor) return;
try {
var terminate = callback(cursor.key, cursor.value);
if (!terminate) cursor.continue();
} catch (ex) {
tx.abort(); // ???
}
};
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
},
getMany: function(keys) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var results = [];
for (var key of keys) {
store.get(key).onsuccess(function(result) {
results.push(result);
});
}
tx.oncomplete = function() { resolve(results); };
tx.onabort = function() { reject(tx.error); };
});
},
setMany: function(entries) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
for (var entry of entries) {
store.put(entry.value, entry.key);
}
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
},
deleteMany: function(keys) {
var that = this;
return new Promise(function(resolve, reject) {
var tx = that._db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
for (var key of keys)
store.delete(key);
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function() { reject(tx.error); };
});
}
};
global.simpleDB = new SimpleDBFactory(SECRET);
global.SimpleDBKeyRange = IDBKeyRange;
}(self));
<!DOCTYPE html>
<script src="simpledb_polyfill.js"></script>
<script>
var db;
simpleDB.open('foo' + Date.now())
.then(function(result) {
console.log('opened: ' + result.name);
db = result;
return db.get('key');
})
.then(function(result) {
console.log('get got: ' + result);
return db.set('key', 'value');
})
.then(function(result) {
return db.get('key');
})
.then(function(result) {
console.log('get got: ' + result);
var pairs = [];
for (var i = 0; i < 10; ++i)
pairs.push({key: i, value: 'v' + i});
return db.setMany(pairs);
})
.then(function() {
return db.forEach(function(k, v) {
console.log('iteration: ', k, v);
if (k === 3) { console.log('terminating'); return true; }
}, {range: SimpleDBKeyRange.bound(-Infinity, Infinity),
direction: 'reverse'});
})
.then(function() {
console.log('done iteration');
})
.catch(function(reason) {
console.warn('failure: ', reason.message);
console.warn(reason.stack);
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment