Skip to content

Instantly share code, notes, and snippets.

@inexorabletash
Last active January 19, 2024 05:50
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save inexorabletash/8791448 to your computer and use it in GitHub Desktop.
Save inexorabletash/8791448 to your computer and use it in GitHub Desktop.
IndexedDB with Promises Hackery

IndexedDB with Promises

This is a hacky prototype of what IDB-on-Promises could look like.

STATUS: See this updated proposal instead.

See also:

https://github.com/slightlyoff/Promises/tree/master/historical_interest/reworked_APIs/IndexedDB

Connections

indexedDB.open() and indexedDB.deleteDatabase() return Promise<IDBDatabase>

var connection = indexedDB.open('my-db', {
  version: 1, 

  // Version upgrade is an optional callback, not an event
  upgrade: function(db, oldVersion) {
    if (oldVersion < 1) {
      db.createObjectStore('my-store');
    }
  }
}).then(function(db) {
  window.db = db;
}).catch(function(reason) {
  // ...
});

Requests

Requests - put, add, update, delete, get, getKey, clear, count - return Promises.

var tx = db.transaction('my-store', 'readwrite');

var store = tx.objectStore('my-store');

store.put('value', 'key').then(function(key) {
  return store.get(key);
}).then(function(value) {
  alert('wrote and read back');
}).catch(function(reason) {
  alert('wtf?');
});

// Transaction is also a Promise
// TODO: This is sketchy - maybe have a getPromise() ?
tx.then(function() { alert("commit successful"), function(reason) { alert("aborted: " + reason); });

Cursors

Cursors are a bit weird, but given all() and forEach() convenience functions.

store.openCursor().then(function(cursor) {
  return cursor.forEach(function(r) {
    console.log(r.key, r.primaryKey, r.value);
  });
}).then(function() {
  console.log('cursor done');
});
// Promise wrappers for IndexedDB
(function(global){
// options.version may trigger an upgrade
// options.upgrade is called with (IDBDatabase, oldVersion) if necessary
// Returns Promise<IDBDatabase>
var $IDBFactory_prototype_open = IDBFactory.prototype.open;
IDBFactory.prototype.open = function(name, options) {
var version = Object(options).version;
var request;
if (version) // (undefined not yet treated the same as 'not passed')
request = $IDBFactory_prototype_open.call(this, name, version);
else
request = $IDBFactory_prototype_open.call(this, name);
var upgrade = Object(options).upgrade;
if (upgrade) request.onupgradeneeded = function(e) {
var storeName = "\x00(IndexedDB Promises Upgrade Hack)\x00";
try { request.result.createObjectStore(storeName); } catch (_) {}
makeAsyncTransactionQueue(request.transaction, storeName);
upgrade(request.result, e.oldVersion);
};
var blocked = Object(options).blocked;
if (blocked) request.onblocked = blocked;
return new Promise(function(resolve, reject) {
request.onsuccess = function() { resolve(request.result); };
request.onerror = function() { reject(request.error); };
});
};
// Returns Promise<undefined>
var $IDBFactory_prototype_deleteDatabase = IDBFactory.prototype.deleteDatabase;
IDBFactory.prototype.deleteDatabase = function(name, options) {
var request = $IDBFactory_prototype_deleteDatabase.call(indexedDB, name);
var blocked = Object(options).blocked;
if (blocked) request.onblocked = blocked;
return new Promise(function(resolve, reject) {
request.onsuccess = function() { resolve(request.result); };
request.onerror = function() { reject(request.error); };
});
};
// This is a terrible horrible, no good, very bad hack.
//
// Promise delivery is async, which means the result of e.g. a get()
// request is delivered outside the transaction callback; in IDB
// spec parlance, the transaction is not "active", and so additional
// requests fail and the transaction may already have committed due
// to a lack of additional requests.
//
// Hack around this by running a constant series of (battery
// draining) dummy requests against an arbitrary store and
// maintaining a custom request queue. Keep the loop alive as long
// as new requests keep coming in.
var $IDBObjectStore_prototype_get = IDBObjectStore.prototype.get;
function makeAsyncTransactionQueue(tx, storeName) {
tx._queue = [];
var store = tx.objectStore(storeName);
var MAX_AGE = 10; // ms
var last = Date.now();
(function spin() {
while (tx._queue.length) {
(tx._queue.shift())();
last = Date.now();
}
if ((Date.now() - last) < MAX_AGE)
$IDBObjectStore_prototype_get.call(store, -Infinity).onsuccess = spin;
}());
}
// Returns Promise<undefined> with the following properties:
// void abort()
// IDBObjectStore objectStore(name)
// IDBDatabase db
// IDBTransactionMode mode
var $IDBDatabase_prototype_transaction = IDBDatabase.prototype.transaction;
IDBDatabase.prototype.transaction = function(scope, mode) {
var tx = $IDBDatabase_prototype_transaction.apply(this, arguments);
var storeName = (typeof scope === 'string') ? scope : scope[0];
makeAsyncTransactionQueue(tx, storeName);
var p = new Promise(function(resolve, reject) {
tx.oncomplete = function() { resolve(undefined); };
tx.onabort = function(e) { reject(tx.error); };
});
p.abort = tx.abort.bind(tx);
p.objectStore = tx.objectStore.bind(tx);
p.db = tx.db;
p.mode = tx.mode;
return p;
};
// Simple request methods
// These all return Promise<T> where T is the result type from IDB.
[
[IDBObjectStore, ['put', 'add', 'delete', 'get', 'clear', 'count']],
[IDBIndex, ['get', 'getKey', 'count']],
[IDBCursor, ['update', 'delete']]
].forEach(function(typeAndMethods) {
var type = typeAndMethods[0],
methods = typeAndMethods[1];
methods.forEach(function(methodName) {
var method = type.prototype[methodName];
type.prototype[methodName] = function() {
var $this = this,
$arguments = arguments,
transaction = transactionFor(this);
return promiseForRequest($this, method, $arguments, transaction);
};
});
});
// Helper: IDBObjectStore or IDBIndex => IDBTransaction
function transactionFor(source) {
var store = ('objectStore' in source) ? source.objectStore : source;
return store.transaction;
}
// Helper: Wrap an IDBRequest in a Promise<T>
function promiseForRequest($this, method, $arguments, transaction) {
return new Promise(function(resolve, reject) {
// Defer request creation until transaction |active| flag is set.
transaction._queue.push(function() {
var request = method.apply($this, $arguments);
request.onsuccess = function() { resolve(request.result); };
request.onerror = function() { reject(request.error); };
});
});
}
// Cursor creation and continuation methods
// These return Promise<IDBCursor or null>
[
[IDBObjectStore, ['openCursor']],
[IDBIndex, ['openCursor', 'openKeyCursor']]
].forEach(function(typeAndMethods) {
var type = typeAndMethods[0],
methods = typeAndMethods[1];
methods.forEach(function(methodName) {
var method = type.prototype[methodName];
type.prototype[methodName] = function() {
var $this = this,
$arguments = arguments,
transaction = transactionFor(this);
return new Promise(function(resolve, reject) {
// Defer request creation until transaction |active| flag is set.
transaction._queue.push(function() {
var request = method.apply($this, $arguments);
request.onsuccess = function() {
// A cursor's request is re-used, so it must be captured.
if (request.result) request.result._request = request;
resolve(request.result);
};
request.onerror = function() { reject(request.error); };
});
});
};
});
});
// These return Promise<IDBCursor or null>
['continue', 'advance'].forEach(function(m) {
var method = IDBCursor.prototype[m];
IDBCursor.prototype[m] = function() {
var $this = this,
$arguments = arguments,
transaction = transactionFor(this.source);
return new Promise(function(resolve, reject) {
// Defer request creation until transaction |active| flag is set.
transaction._queue.push(function() {
method.apply($this, $arguments);
var request = $this._request;
request.onsuccess = function() { resolve(request.result); };
request.onerror = function() { reject(request.error); };
});
});
};
});
// Helpers to make the basic cursor iteration cases less grotesque with
// recursive Promises
// TODO: Consider separating cursor object (API) from cursor result (data)
// Returns Promise<[{key, primaryKey, value}]>
IDBCursor.prototype.all = function() {
var $this = this;
var request = $this._request;
return new Promise(function(resolve, reject) {
var results = [];
function iterate() {
if (!request.result) {
resolve(results);
return;
}
try {
results.push({key: $this.key,
primaryKey: $this.primaryKey,
value: $this.value});
$this.continue().then(iterate, reject);
} catch (e) {
reject(e);
}
}
request.onsuccess = iterate;
request.onerror = function() { reject(request.error); };
iterate();
});
};
// Callback is called with {key, primaryKey, value} until
// the cursor is exhausted.
// Returns Promise<undefined>
IDBCursor.prototype.forEach = function(callback) {
var $this = this;
var request = $this._request;
return new Promise(function(resolve, reject) {
function iterate() {
if (!request.result) {
resolve(undefined);
return;
}
try {
callback({key: $this.key,
primaryKey: $this.primaryKey,
value: $this.value});
$this.continue().then(iterate, reject);
} catch (e) {
reject(e);
}
}
request.onsuccess = iterate;
request.onerror = function() { reject(request.error); };
iterate();
});
};
}(this));
<!DOCTYPE html>
<script src="idbp.js"></script>
<script>
function log(x) { console.log('log: ' + x); }
function err(x) { console.error('err: ' + x); }
window.db = null;
indexedDB.open('db' + Date.now(), {
upgrade: function(db, oldVersion) {
console.log('oldVersion: ' + oldVersion);
var store = db.createObjectStore('store');
for (var i = 65; i < 75; ++i) {
console.log('putting...');
store.put(String.fromCharCode(i), i);
}
}
}).then(function(db) {
window.db = db;
var tx = db.transaction('store', 'readwrite');
var s = tx.objectStore('store');
s.put('value', 'key').then(
function(key) { return s.get(key); }
).then(log, err);
return tx.then(function() { console.log('tx commited'); });
}).then(function(old_tx) {
var tx = db.transaction('store');
var s = tx.objectStore('store');
s.openCursor().then(function(cursor) {
return cursor.all();
}).then(function(results) {
console.log(JSON.stringify(results));
}).then(function() {
console.log("done all");
});
s.openCursor().then(function(cursor) {
return cursor.forEach(function(result) {
console.log(result.key + ': ' + result.value);
});
}).then(function() {
console.log("done forEach");
});
}).catch(err);
</script>
@J-Cake
Copy link

J-Cake commented Jul 21, 2019

Nice

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment