Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New tool: Dexie.waitFor() keeps transaction alive. #378

Merged
merged 1 commit into from
Nov 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions src/Dexie.js
Original file line number Diff line number Diff line change
Expand Up @@ -1387,13 +1387,13 @@ export default function Dexie(dbName, options) {
this.on = Events(this, "complete", "error", "abort");
this.parent = parent || null;
this.active = true;
this._tables = null;
this._reculock = 0;
this._blockedFuncs = [];
this._psd = null;
this._dbschema = dbschema;
this._resolve = null;
this._reject = null;
this._waitingFor = null;
this._waitingQueue = null;
this._spinCount = 0; // Just for debugging waitFor()
this._completion = new Promise ((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
Expand Down Expand Up @@ -1519,6 +1519,45 @@ export default function Dexie(dbName, options) {
}
},

_root: function () {
return this.parent ? this.parent._root() : this;
},

waitFor (promise) {
// Always operate on the root transaction (in case this is a sub stransaction)
var root = this._root();
// For stability reasons, convert parameter to promise no matter what type is passed to waitFor().
// (We must be able to call .then() on it.)
promise = Promise.resolve(promise);
if (root._waitingFor) {
// Already called waitFor(). Wait for both to complete.
root._waitingFor = root._waitingFor.then(()=>promise);
} else {
// We're not in waiting state. Start waiting state.
root._waitingFor = promise;
root._waitingQueue = [];
// Start interacting with indexedDB until promise completes:
var store = root.idbtrans.objectStore(root.storeNames[0]);
(function spin(){
++root._spinCount; // For debugging only
while (root._waitingQueue.length) (root._waitingQueue.shift())();
if (root._waitingFor) store.get(-Infinity).onsuccess = spin;
}());
}
var currentWaitPromise = root._waitingFor;
return new Promise ((resolve, reject) => {
promise.then (
res => root._waitingQueue.push(wrap(resolve.bind(null, res))),
err => root._waitingQueue.push(wrap(reject.bind(null, err)))
).finally(() => {
if (root._waitingFor === currentWaitPromise) {
// No one added a wait after us. Safe to stop the spinning.
root._waitingFor = null;
}
});
});
},

//
// Transaction Public Properties and Methods
//
Expand Down Expand Up @@ -3053,6 +3092,17 @@ props(Dexie, {
currentTransaction: {
get: () => PSD.trans || null
},

waitFor: function (promiseOrFunction, optionalTimeout) {
// If a function is provided, invoke it and pass the returning value to Transaction.waitFor()
var promise = Promise.resolve(
typeof promiseOrFunction === 'function' ? Dexie.ignoreTransaction(promiseOrFunction) : promiseOrFunction)
.timeout(optionalTimeout || 60000); // Default the timeout to one minute. Caller may specify Infinity if required.

// Run given promise on current transaction. If no current transaction, just return a Dexie promise based
// on given value.
return PSD.trans ? PSD.trans.waitFor(promise) : promise;
},

// Export our Promise implementation since it can be handy as a standalone Promise implementation
Promise: Promise,
Expand Down
11 changes: 10 additions & 1 deletion src/Promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {doFakeAutoComplete, tryCatch, props, setProp, _global,
getPropertyDescriptor, getArrayOf, extend} from './utils';
import {nop, callBoth, mirror} from './chaining-functions';
import {debug, prettyStack, getErrorWithStack} from './debug';
import {exceptions} from './errors';

//
// Promise and Zone (PSD) for Dexie library
Expand Down Expand Up @@ -251,6 +252,14 @@ props(Promise.prototype, {
stack_being_generated = false;
}
}
},

timeout: function (ms, msg) {
return ms < Infinity ?
new Promise((resolve, reject) => {
var handle = setTimeout(() => reject(new exceptions.Timeout(msg)), ms);
this.then(resolve, reject).finally(clearTimeout.bind(null, handle));
}) : this;
}
});

Expand Down Expand Up @@ -299,7 +308,7 @@ props (Promise, {
values.map(value => Promise.resolve(value).then(resolve, reject));
});
},

PSD: {
get: ()=>PSD,
set: value => PSD = value
Expand Down
88 changes: 87 additions & 1 deletion test/tests-transaction.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Dexie from 'dexie';
import {module, stop, start, asyncTest, equal, ok} from 'QUnit';
import {resetDatabase, spawnedTest} from './dexie-unittest-utils';
import {resetDatabase, spawnedTest, promisedTest} from './dexie-unittest-utils';

"use strict";

Expand Down Expand Up @@ -789,3 +789,89 @@ asyncTest("Dexie.currentTransaction in CRUD hooks", 53, function () {
start();
});
});

function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

promisedTest("waitFor()", async ()=>{
await db.transaction('rw', db.users, async trans =>{
// Wait for a promise:
await trans.waitFor(sleep(100));
// Do an operation on transaction
await trans.users.put({username: "testingtesting"});
await trans.waitFor(sleep(100));
let result = await trans.users.get("testingtesting");
ok(result && result.username === "testingtesting", "Should be able to continue transaction after waiting for non-indexedDB promise");
ok(true, `Waiting spin count:${trans._spinCount}`);

// With timeout
await Dexie.waitFor(sleep(2000), 10) // Timeout of 10 ms.
.then (()=>ok(false, "Should have timed out!"))
.catch('TimeoutError', ex => ok(true, "Timed out as expected"));

// Wait for function
await Dexie.waitFor(async ()=>{
ok(Dexie.currentTransaction === null,
"We should not be in the transaction zone here because transaction can be in a temporary inactive state here");
await sleep(10);
ok (true, "Slept 10 ms")
// Let's test if we can access the transaction from here.
// The transaction should be alive indeed but not in an active state.
await trans.users.count().then(()=>{
// This happens on IE11
ok(true, "Could access transaction within the wait callback. Nice for you, but you were just lucky!");
}).catch(ex => {
// This happens on Firefox and Chrome
ok(true, "Could NOT access transaction within the wait callback. As expected. Error: " + ex);
});
ok(Dexie.currentTransaction === null,
"We should not be in the transaction zone here because transaction can be in inactive state here");
});

result = await trans.users.get("testingtesting");
ok(result && result.username === "testingtesting", "Should still be able to operate on the transaction");
ok(true, `Waiting spin count:${trans._spinCount}`);
ok(Dexie.currentTransaction === trans, "Zone info should still be correct");

// Subtransaction
await db.transaction('r', db.users, function* (subTrans) {
ok(subTrans !== trans, "Should be in a sub transaction");
ok(Dexie.currentTransaction === subTrans, "Should be in a sub transaction");
let count = yield trans.users.count();
ok(true, "Should be able to operate on sub transaction. User count = " + count);
yield subTrans.waitFor(sleep(10));
ok(true, "Should be able to call waitFor() on sub transaction");
count = yield trans.users.count();
ok(true, "Should be able to operate on sub transaction. User count = " + count);
});

// Calling waitFor multiple times in parallell
await Promise.all([
trans.waitFor(sleep(10)),
trans.waitFor(sleep(10)),
trans.waitFor(sleep(10))]);
ok (true, "Could wait for several tasks in parallell");

result = await trans.users.get("testingtesting");
ok(result && result.username === "testingtesting", "Should still be able to operate on the transaction");
//await sleep(100);
//ok(true, `Waiting spin count:${trans._spinCount}`);
}).then(()=>ok(true, "Transaction committed"));
});

promisedTest("Dexie.waitFor() outside transaction", async ()=> {
// Test that waitFor can be called when not in a transaction as well.
// The meaning of this is that sometimes a function does db operations without
// a transaction, but should be able to call also within the caller's transaction.
// A function should therefore be able to call Dexie.waitFor() no matter if is executing
// within a transaction or not.
let result = await Dexie.waitFor(sleep(10).then(()=>true));
ok(result, "Could call waitFor outside a transaction as well");
let codeExecuted = false;
await Dexie.waitFor(async ()=>{
await sleep(10);
codeExecuted = true;
});
ok(codeExecuted, "Could call waitFor(function) outside a transation as well");
});