Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

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 = {
running: false,
stack: [],
runCallStack: function () {
if (this.stack.length > 0 && this.running === false) {
this.stack.shift().call(this);
}
},
addProcessToStack: function (process) {
var that = this;
return new Promise(function(resolve, reject) {
var task = function () {
var that = this;
this.running = true;
var resolveWrapper = function () {
that.running = false;
resolve.apply(this, arguments);
that.runCallStack();
};
var rejectWrapper = function () {
that.running = false;
reject.apply(this, arguments);
that.runCallStack();
};
process(resolveWrapper, rejectWrapper);
//process.call(that, resolve, reject);
//that.runCallStack();
}
that.stack.push(task);
that.runCallStack();
});
},
get name() {
return this._name;
},
get: function(key) {
var that = this;
return this.addProcessToStack(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 this.addProcessToStack(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 this.addProcessToStack(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 this.addProcessToStack(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 this.addProcessToStack(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 this.addProcessToStack(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 this.addProcessToStack(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 this.addProcessToStack(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));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment