From 203143fe8860ef217a8d19c99c902e7bcb9c8cce Mon Sep 17 00:00:00 2001 From: David Fahlander Date: Fri, 30 Sep 2016 14:21:54 +0200 Subject: [PATCH] Hopefully resolves #315 by introducing an alternate tranx method that replaces window.Promise withing the transaction scope only. NOTE: It replaces window.Promise temporarly only for code that executes within the transaction scope, so it won't disturb neither Zone.js or other libraries that depend on window.Promise. This will only affect your DB-facing code and code you call from withing your transaction scope. --- src/Dexie.js | 20 +++++++++ src/Promise.js | 105 +++++++------------------------------------- test/tests-all.js | 1 + test/tests-tranx.js | 51 +++++++++++++++++++++ 4 files changed, 89 insertions(+), 88 deletions(-) create mode 100644 test/tests-tranx.js diff --git a/src/Dexie.js b/src/Dexie.js index f3fa7d7a87..72a013429f 100644 --- a/src/Dexie.js +++ b/src/Dexie.js @@ -696,6 +696,26 @@ export default function Dexie(dbName, options) { db.on("populate").fire(db._createTransaction(READWRITE, dbStoreNames, globalSchema)); db.on("error").fire(new Error()); }); + + this.tranx = function (mode, tableInstances, scopeFunc) { + var l = arguments.length, + args = new Array(l), + i = l; + while (i--) args[i] = arguments[i]; + var func = args[l-1]; + args[l-1] = function () { + PSD.onenter = function () { + this.Promise = _global.Promise; + _global.Promise = Promise; + } + PSD.onleave = function () { + _global.Promise = this.Promise; + } + PSD.onenter(); + return func.call(this, this); // Give transaction as first argument instead of table instances. + }; + return this.transaction.apply(this, args); + } this.transaction = function (mode, tableInstances, scopeFunc) { /// diff --git a/src/Promise.js b/src/Promise.js index 90b36e6044..b3f813941c 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -94,6 +94,8 @@ export var globalPSD = { ref: 0, unhandleds: [], onunhandled: globalError, + onenter: null, + onleave: null, //env: null, // Will be set whenever leaving a scope using wrappers.snapshot() finalize: function () { this.unhandleds.forEach(uh => { @@ -110,30 +112,6 @@ export var microtickQueue = []; // Callbacks to call in this or next physical ti export var numScheduledCalls = 0; // Number of listener-calls left to do in this physical tick. export var tickFinalizers = []; // Finalizers to call when there are no more async calls scheduled within current physical tick. -// Wrappers are not being used yet. Their framework is functioning and can be used -// to replace environment during a PSD scope (a.k.a. 'zone'). -/* **KEEP** export var wrappers = (() => { - var wrappers = []; - - return { - snapshot: () => { - var i = wrappers.length, - result = new Array(i); - while (i--) result[i] = wrappers[i].snapshot(); - return result; - }, - restore: values => { - var i = wrappers.length; - while (i--) wrappers[i].restore(values[i]); - }, - wrap: () => wrappers.map(w => w.wrap()), - add: wrapper => { - wrappers.push(wrapper); - } - }; -})(); -*/ - export default function Promise(fn) { if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new'); this._listeners = []; @@ -431,9 +409,9 @@ function callListener (cb, promise, listener) { var psd = listener.psd; try { if (psd !== outerScope) { - // **KEEP** outerScope.env = wrappers.snapshot(); // Snapshot outerScope's environment. PSD = psd; - // **KEEP** wrappers.restore(psd.env); // Restore PSD's environment. + outerScope.onleave && outerScope.onleave(); + psd.onenter && psd.onenter(); } // Set static variable currentFulfiller to the promise that is being fullfilled, @@ -459,7 +437,8 @@ function callListener (cb, promise, listener) { // Restore PSD, env and currentFulfiller. if (psd !== outerScope) { PSD = outerScope; - // **KEEP** wrappers.restore(outerScope.env); // Restore outerScope's environment + psd.onleave && psd.onleave(); + outerScope.onenter && outerScope.onenter(); } currentFulfiller = null; if (--numScheduledCalls === 0) finalizePhysicalTick(); @@ -481,7 +460,7 @@ function getStack (promise, stacks, limit) { stack = prettyStack(failure, 0); } else { errorName = failure; // If error is undefined or null, show that. - message = "" + message = ""; } stacks.push(errorName + (message ? ": " + message : "") + stack); } @@ -602,9 +581,9 @@ export function wrap (fn, errorCatcher) { try { if (outerScope !== psd) { - // **KEEP** outerScope.env = wrappers.snapshot(); // Snapshot outerScope's environment PSD = psd; - // **KEEP** wrappers.restore(psd.env); // Restore PSD's environment. + outerScope.onleave && outerScope.onleave(); + psd.onenter && psd.onenter(); } return fn.apply(this, arguments); } catch (e) { @@ -612,7 +591,8 @@ export function wrap (fn, errorCatcher) { } finally { if (outerScope !== psd) { PSD = outerScope; - // **KEEP** wrappers.restore(outerScope.env); // Restore outerScope's environment + psd.onleave && psd.onleave(); + outerScope.onenter && outerScope.onenter(); } if (wasRootExec) endMicroTickScope(); } @@ -625,7 +605,8 @@ export function newScope (fn, a1, a2, a3) { psd.parent = parent; psd.ref = 0; psd.global = false; - // **KEEP** psd.env = wrappers.wrap(psd); + psd.onenter = null; + psd.onleave = null; // unhandleds and onunhandled should not be specifically set here. // Leave them on parent prototype. @@ -644,15 +625,16 @@ export function usePSD (psd, fn, a1, a2, a3) { var outerScope = PSD; try { if (psd !== outerScope) { - // **KEEP** outerScope.env = wrappers.snapshot(); // snapshot outerScope's environment. PSD = psd; - // **KEEP** wrappers.restore(psd.env); // Restore PSD's environment. + outerScope.onleave && outerScope.onleave(); + psd.onenter && psd.onenter(); } return fn(a1, a2, a3); } finally { if (psd !== outerScope) { PSD = outerScope; - // **KEEP** wrappers.restore(outerScope.env); // Restore outerScope's environment. + psd.onleave && psd.onleave(); + outerScope.onenter && outerScope.onenter(); } } } @@ -667,59 +649,6 @@ function globalError(err, promise) { } catch (e) {} } -/* **KEEP** - -export function wrapPromise(PromiseClass) { - var proto = PromiseClass.prototype; - var origThen = proto.then; - - wrappers.add({ - snapshot: () => proto.then, - restore: value => {proto.then = value;}, - wrap: () => patchedThen - }); - - function patchedThen (onFulfilled, onRejected) { - var promise = this; - var onFulfilledProxy = wrap(function(value){ - var rv = value; - if (onFulfilled) { - rv = onFulfilled(rv); - if (rv && typeof rv.then === 'function') rv.then(); // Intercept that promise as well. - } - --PSD.ref || PSD.finalize(); - return rv; - }); - var onRejectedProxy = wrap(function(err){ - promise._$err = err; - var unhandleds = PSD.unhandleds; - var idx = unhandleds.length, - rv; - while (idx--) if (unhandleds[idx]._$err === err) break; - if (onRejected) { - if (idx !== -1) unhandleds.splice(idx, 1); // Mark as handled. - rv = onRejected(err); - if (rv && typeof rv.then === 'function') rv.then(); // Intercept that promise as well. - } else { - if (idx === -1) unhandleds.push(promise); - rv = PromiseClass.reject(err); - rv._$nointercept = true; // Prohibit eternal loop. - } - --PSD.ref || PSD.finalize(); - return rv; - }); - - if (this._$nointercept) return origThen.apply(this, arguments); - ++PSD.ref; - return origThen.call(this, onFulfilledProxy, onRejectedProxy); - } -} - -// Global Promise wrapper -if (_global.Promise) wrapPromise(_global.Promise); - -*/ - doFakeAutoComplete(() => { // Simplify the job for VS Intellisense. This piece of code is one of the keys to the new marvellous intellisense support in Dexie. asap = (fn, args) => { diff --git a/test/tests-all.js b/test/tests-all.js index 2e0c4be494..9f10802f1e 100644 --- a/test/tests-all.js +++ b/test/tests-all.js @@ -7,6 +7,7 @@ import "./tests-table.js"; import "./tests-extendability.js"; import "./tests-promise.js"; import "./tests-transaction.js"; +import "./tests-tranx.js"; import "./tests-open.js"; import "./tests-exception-handling.js"; import "./tests-upgrading.js"; diff --git a/test/tests-tranx.js b/test/tests-tranx.js new file mode 100644 index 0000000000..e1ea79f2df --- /dev/null +++ b/test/tests-tranx.js @@ -0,0 +1,51 @@ +import Dexie from 'dexie'; +import {module, stop, start, asyncTest, equal, ok} from 'QUnit'; +import {resetDatabase, spawnedTest} from './dexie-unittest-utils'; + +"use strict"; + +var db = new Dexie("TestDBTranx"); +db.version(1).stores({ + items: "id" +}); + +module("tranx", { + setup: function () { + stop(); + resetDatabase(db).catch(function (e) { + ok(false, "Error resetting database: " + e.stack); + }).finally(start); + }, + teardown: function () { + } +}); + +asyncTest("Should be able to use global Promise within transaction scopes", function(){ + db.tranx('rw', db.items, trans => { + return window.Promise.resolve().then(()=> { + ok(Dexie.currentTransaction == trans, "Transaction scopes should persist through Promise.resolve()"); + return db.items.add({ id: "foobar" }); + }).then(()=>{ + return Promise.resolve(); + }).then(()=>{ + ok(Dexie.currentTransaction == trans, "Transaction scopes should persist through Promise.resolve()"); + return db.items.get('foobar'); + }); + }).then (function(foobar){ + equal(foobar.id, 'foobar', "Transaction should have lived throughout the Promise.resolve() chain"); + }).catch (e => { + ok(false, `Error: ${e.stack || e}`); + }).finally(start); +}); + + +spawnedTest("Should use Promise.all where applicable", function* (){ + yield db.tranx('rw', db.items, function* () { + let x = yield Promise.resolve(3); + yield db.items.bulkAdd([{id: 'a'}, {id: 'b'}]); + let all = yield Promise.all(db.items.get('a'), db.items.get('b')); + equal (all.length, 2); + equal (all[0].id, 'a'); + equal (all[1].id, 'b'); + }); +});