diff --git a/backbone.js b/backbone.js index 6b566e943..6f383e21f 100644 --- a/backbone.js +++ b/backbone.js @@ -433,7 +433,7 @@ // Proxy `Backbone.sync` by default -- but override this if you need // custom syncing semantics for *this* particular model. sync: function() { - return Backbone.sync.apply(this, arguments); + return Promise.resolve(Backbone.sync.apply(this, arguments)); }, // Get the value of an attribute. @@ -587,15 +587,21 @@ fetch: function(options) { options = _.extend({parse: true}, options); var model = this; - var success = options.success; - options.success = function(resp) { - var serverAttrs = options.parse ? model.parse(resp, options) : resp; - if (!model.set(serverAttrs, options)) return false; - if (success) success.call(options.context, model, resp, options); - model.trigger('sync', model, resp, options); - }; - wrapError(this, options); - return this.sync('read', this, options); + var success = options.success || _.identity; + var error = options.error; + delete options.success; + delete options.error; + return wrapError(this.sync('read', this, options), this, options, error) + .then(function(resp) { + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (model.set(serverAttrs, options)) + return Promise.resolve(success.call(options.context, model, resp, options)) + .then(function(delivered) { + model.trigger('sync', model, resp, options); + return delivered; + }); + return model; + }); }, // Set a hash of model attributes, and sync the model to the server. @@ -613,43 +619,48 @@ options = _.extend({validate: true, parse: true}, options); var wait = options.wait; + var success = options.success || _.identity; + var error = options.error; + delete options.success; + delete options.error; // If we're not waiting and attributes exist, save acts as // `set(attr).save(null, opts)` with validation. Otherwise, check if // the model will be valid when the attributes, if any, are set. if (attrs && !wait) { - if (!this.set(attrs, options)) return false; + if (!this.set(attrs, options)) return Promise.reject(this.validationError); } else { - if (!this._validate(attrs, options)) return false; + if (!this._validate(attrs, options)) return Promise.reject(this.validationError); } // After a successful server-side save, the client is (optionally) // updated with the server-side state. var model = this; - var success = options.success; var attributes = this.attributes; - options.success = function(resp) { - // Ensure attributes are restored during synchronous saves. - model.attributes = attributes; - var serverAttrs = options.parse ? model.parse(resp, options) : resp; - if (wait) serverAttrs = _.extend({}, attrs, serverAttrs); - if (serverAttrs && !model.set(serverAttrs, options)) return false; - if (success) success.call(options.context, model, resp, options); - model.trigger('sync', model, resp, options); - }; - wrapError(this, options); // Set temporary attributes if `{wait: true}` to properly find new ids. if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); if (method === 'patch' && !options.attrs) options.attrs = attrs; - var xhr = this.sync(method, this, options); + var promise = wrapError(this.sync(method, this, options), this, options, error); // Restore attributes. this.attributes = attributes; - return xhr; + return promise.then(function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (wait) serverAttrs = _.extend({}, attrs, serverAttrs); + if (!serverAttrs || model.set(serverAttrs, options)) + return Promise.resolve(success.call(options.context, model, resp, options)) + .then(function(delivered) { + model.trigger('sync', model, resp, options); + return delivered; + }); + return model; + }); }, // Destroy this model on the server if it was already persisted. @@ -658,29 +669,33 @@ destroy: function(options) { options = options ? _.clone(options) : {}; var model = this; - var success = options.success; var wait = options.wait; + var success = options.success; + var error = options.error; + delete options.success; + delete options.error; var destroy = function() { model.stopListening(); model.trigger('destroy', model, model.collection, options); }; - options.success = function(resp) { - if (wait) destroy(); - if (success) success.call(options.context, model, resp, options); - if (!model.isNew()) model.trigger('sync', model, resp, options); - }; - - var xhr = false; - if (this.isNew()) { - _.defer(options.success); - } else { - wrapError(this, options); - xhr = this.sync('delete', this, options); - } + var isNew = model.isNew(); + var promise = isNew ? Promise.resolve(model.attributes) : + wrapError(this.sync('delete', this, options), this, options, error); if (!wait) destroy(); - return xhr; + return promise.then(function(resp) { + var promise = Promise.resolve(model); + if (wait) destroy(); + if (success) + promise = Promise.resolve(success.call(options.context, model, resp, options)); + if (!isNew) + promise = promise.then(function(delivered) { + model.trigger('sync', model, resp, options); + return delivered; + }); + return promise; + }); }, // Default URL for the model's representation on the server -- if you're @@ -693,7 +708,12 @@ urlError(); if (this.isNew()) return base; var id = this.get(this.idAttribute); - return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); + var consume = function(base) { + return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); + }; + if (isPromise(base)) + return base.then(consume); + return consume(base); }, // **parse** converts a response into the hash of attributes to be `set` on @@ -783,7 +803,7 @@ // Proxy `Backbone.sync` by default. sync: function() { - return Backbone.sync.apply(this, arguments); + return Promise.resolve(Backbone.sync.apply(this, arguments)); }, // Add a model, or list of models to the set. `models` may be Backbone @@ -1004,16 +1024,21 @@ // data will be passed through the `reset` method instead of `set`. fetch: function(options) { options = _.extend({parse: true}, options); - var success = options.success; var collection = this; - options.success = function(resp) { - var method = options.reset ? 'reset' : 'set'; - collection[method](resp, options); - if (success) success.call(options.context, collection, resp, options); - collection.trigger('sync', collection, resp, options); - }; - wrapError(this, options); - return this.sync('read', this, options); + var success = options.success || _.identity; + var error = options.error; + delete options.success; + delete options.error; + return wrapError(this.sync('read', this, options), this, options, error) + .then(function(resp) { + var method = options.reset ? 'reset' : 'set'; + collection[method](resp, options); + return Promise.resolve(success.call(options.context, collection, resp, options)) + .then(function(delivered) { + collection.trigger('sync', collection, resp, options); + return delivered; + }); + }); }, // Create a new instance of a model in this collection. Add the model to the @@ -1026,13 +1051,18 @@ if (!model) return false; if (!wait) this.add(model, options); var collection = this; - var success = options.success; - options.success = function(model, resp, callbackOpts) { - if (wait) collection.add(model, callbackOpts); - if (success) success.call(callbackOpts.context, model, resp, callbackOpts); + var success = options.success || _.identity; + var error = options.error; + delete options.error; + options.success = function(_, resp, opts) { + options = opts; + return resp; }; - model.save(null, options); - return model; + return model.save(null, options) + .then(function(resp) { + if (wait) collection.add(model, options); + return Promise.resolve(success.call(options.context, model, resp, options)); + }); }, // **parse** converts a response into a list of models to be added to the @@ -1351,9 +1381,7 @@ var params = {type: type, dataType: 'json'}; // Ensure that we have a URL. - if (!options.url) { - params.url = _.result(model, 'url') || urlError(); - } + params.url = options.url || _.result(model, 'url') || urlError(); // Ensure that we have the appropriate request data. if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { @@ -1385,17 +1413,21 @@ } // Pass along `textStatus` and `errorThrown` from jQuery. - var error = options.error; options.error = function(xhr, textStatus, errorThrown) { options.textStatus = textStatus; options.errorThrown = errorThrown; - if (error) error.call(options.context, xhr, textStatus, errorThrown); }; // Make the request, allowing the user to override any Ajax options. - var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); - model.trigger('request', model, xhr, options); - return xhr; + var consume = function(url) { + params.url = url || urlError(); + var promise = Promise.resolve(options.xhr = Backbone.ajax(_.extend(params, _.omit(options, 'url')))); + model.trigger('request', model, promise, options); + return promise; + }; + if (isPromise(params.url)) + return params.url.then(consume); + return consume(params.url); }; // Map from CRUD to HTTP for our default `Backbone.sync` implementation. @@ -1455,11 +1487,18 @@ var router = this; Backbone.history.route(route, function(fragment) { var args = router._extractParameters(route, fragment); - if (router.execute(callback, args, name) !== false) { - router.trigger.apply(router, ['route:' + name].concat(args)); - router.trigger('route', name, args); - Backbone.history.trigger('route', router, name, args); - } + var result = router.execute(callback, args, name); + var consume = function(result) { + if (result !== false) { + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + } + }; + if (isPromise(result)) + result.then(consume); + else + consume(result); }); return this; }, @@ -1467,7 +1506,23 @@ // Execute a route handler with the provided parameters. This is an // excellent place to do pre-route setup or post-route cleanup. execute: function(callback, args, name) { - if (callback) callback.apply(this, args); + var router = this; + var error = function(err) { + router.trigger('error', err, name, args, callback); + }; + if (callback) { + try { + var route = callback.apply(this, args); + if (isPromise(route)) { + return route['catch'](error); + } + return route; + } catch(e) { + error(e); + } + } else { + error(); + } }, // Simple proxy to `Backbone.history` to save a fragment into the history. @@ -1709,8 +1764,8 @@ // Add a route to be tested when the fragment changes. Routes added later // may override previous routes. - route: function(route, callback) { - this.handlers.unshift({route: route, callback: callback}); + route: function(route, callback, meta) { + this.handlers.unshift(_.defaults({route: route, callback: callback}, meta)); }, // Checks the current URL to see if it has changed, and if it has, @@ -1865,15 +1920,26 @@ throw new Error('A "url" property or function must be specified'); }; - // Wrap an optional error callback with a fallback error event. - var wrapError = function(model, options) { - var error = options.error; - options.error = function(resp) { - if (error) error.call(options.context, model, resp, options); - model.trigger('error', model, resp, options); - }; + // Allow a callback to resolve a rejected promise, otherwise promise + // will remain rejected and an `error` event will be triggered on model. + var wrapError = function(promise, model, options, callback) { + return Promise.resolve(promise) + ['catch'](function(resp) { + if (callback) + return callback.call(options.context, model, resp, options); + throw resp; + }) + ['catch'](function(resp) { + model.trigger('error', model, resp, options); + throw resp; + }); }; + // Allow any `then`able (and `catch`able) to be considered a `Promise` + var isPromise = function(o) { + return o && (typeof o === 'object' || typeof o === 'function') && typeof o.then === 'function' && typeof o['catch'] === 'function'; + } + return Backbone; })); diff --git a/examples/backbone.localStorage.js b/examples/backbone.localStorage.js index 0aab24784..150a4bd74 100644 --- a/examples/backbone.localStorage.js +++ b/examples/backbone.localStorage.js @@ -114,54 +114,43 @@ _.extend(Backbone.LocalStorage.prototype, { Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) { var store = model.localStorage || model.collection.localStorage; - var resp, errorMessage, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it. - - try { - - switch (method) { - case "read": - resp = model.id != undefined ? store.find(model) : store.findAll(); - break; - case "create": - resp = store.create(model); - break; - case "update": - resp = store.update(model); - break; - case "delete": - resp = store.destroy(model); - break; + var resp, errorMessage; + + return new Promise(function(resolve, reject) { + try { + + switch (method) { + case "read": + resp = model.id != undefined ? store.find(model) : store.findAll(); + break; + case "create": + resp = store.create(model); + break; + case "update": + resp = store.update(model); + break; + case "delete": + resp = store.destroy(model); + break; + } + + } catch(error) { + if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0) + errorMessage = "Private browsing is unsupported"; + else + errorMessage = error.message; } - } catch(error) { - if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0) - errorMessage = "Private browsing is unsupported"; - else - errorMessage = error.message; - } - - if (resp) { - model.trigger("sync", model, resp, options); - if (options && options.success) - options.success(resp); - if (syncDfd) - syncDfd.resolve(resp); - - } else { - errorMessage = errorMessage ? errorMessage - : "Record Not Found"; - - if (options && options.error) - options.error(errorMessage); - if (syncDfd) - syncDfd.reject(errorMessage); - } + if (resp) { + model.trigger("sync", model, resp, options); + resolve(resp); + } else { + errorMessage = errorMessage ? errorMessage + : "Record Not Found"; - // add compatibility with $.ajax - // always execute callback for success and error - if (options && options.complete) options.complete(resp); - - return syncDfd && syncDfd.promise(); + reject(errorMessage); + } + }); }; Backbone.ajaxSync = Backbone.sync; @@ -177,7 +166,7 @@ Backbone.getSyncMethod = function(model) { // Override 'Backbone.sync' to default to localSync, // the original 'Backbone.sync' is still available in 'Backbone.ajaxSync' Backbone.sync = function(method, model, options) { - return Backbone.getSyncMethod(model).apply(this, [method, model, options]); + return Backbone.getSyncMethod(model).call(this, method, model, options); }; return Backbone.LocalStorage; diff --git a/examples/todos/index.html b/examples/todos/index.html index e737fd2c2..aa31c585a 100644 --- a/examples/todos/index.html +++ b/examples/todos/index.html @@ -43,6 +43,7 @@

Todos

+ diff --git a/index.html b/index.html index 187dbeee2..5c25c98f0 100644 --- a/index.html +++ b/index.html @@ -4957,6 +4957,7 @@

Change Log

+ diff --git a/karma.conf-sauce.js b/karma.conf-sauce.js index beb8cf92a..09f8df14e 100644 --- a/karma.conf-sauce.js +++ b/karma.conf-sauce.js @@ -64,6 +64,7 @@ module.exports = function(config) { 'test/vendor/jquery.js', 'test/vendor/json2.js', 'test/vendor/underscore.js', + 'test/vendor/promise.js', 'backbone.js', 'test/setup/*.js', 'test/*.js' diff --git a/karma.conf.js b/karma.conf.js index b7b3db0f7..75856f564 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -12,6 +12,7 @@ module.exports = function(config) { 'test/vendor/jquery.js', 'test/vendor/json2.js', 'test/vendor/underscore.js', + 'test/vendor/promise.js', 'backbone.js', 'test/setup/*.js', 'test/*.js' @@ -51,4 +52,4 @@ module.exports = function(config) { } } }); -}; \ No newline at end of file +}; diff --git a/test/collection.js b/test/collection.js index c691bc70e..e9d64d3af 100644 --- a/test/collection.js +++ b/test/collection.js @@ -441,7 +441,7 @@ test("model destroy removes from all collections", 3, function() { var e = new Backbone.Model({id: 5, title: 'Othello'}); - e.sync = function(method, model, options) { options.success(); }; + e.sync = _.noop; var colE = new Backbone.Collection([e]); var colF = new Backbone.Collection([e]); e.destroy(); @@ -473,16 +473,28 @@ equal(this.syncArgs.options.parse, false); }); - test("fetch with an error response triggers an error event", 1, function () { + test("fetch with an error response triggers an error event", 1, function (assert) { + var done = assert.async(); var collection = new Backbone.Collection(); collection.on('error', function () { ok(true); }); - collection.sync = function (method, model, options) { options.error(); }; - collection.fetch(); + collection.sync = _.constant(Promise.reject()); + collection.fetch() + .then(done, done); + }); + + test("fetch with an error response rejects promise", 1, function (assert) { + var done = assert.async(); + var collection = new Backbone.Collection(); + collection.sync = _.constant(Promise.reject()); + collection.fetch() + ['catch'](function() { ok(true); }) + .then(done, done); }); - test("#3283 - fetch with an error response calls error with context", 1, function () { + test("#3283 - fetch with an error response calls error with context", 1, function (assert) { + var done = assert.async(); var collection = new Backbone.Collection(); var obj = {}; var options = { @@ -491,13 +503,12 @@ equal(this, obj); } }; - collection.sync = function (method, model, options) { - options.error.call(options.context); - }; - collection.fetch(options); + collection.sync = _.constant(Promise.reject()); + collection.fetch(options).then(done, done); }); - test("ensure fetch only parses once", 1, function() { + test("ensure fetch only parses once", 1, function(assert) { + var done = assert.async(); var collection = new Backbone.Collection; var counter = 0; collection.parse = function(models) { @@ -505,19 +516,26 @@ return models; }; collection.url = '/test'; - collection.fetch(); - this.syncArgs.options.success(); - equal(counter, 1); + collection.fetch() + .then(function() { + equal(counter, 1); + }) + .then(done, done); }); - test("create", 4, function() { + test("create", 4, function(assert) { + var done = assert.async(); + var env = this; var collection = new Backbone.Collection; collection.url = '/test'; - var model = collection.create({label: 'f'}, {wait: true}); - equal(this.syncArgs.method, 'create'); - equal(this.syncArgs.model, model); - equal(model.get('label'), 'f'); - equal(model.collection, collection); + collection.create({label: 'f'}, {wait: true}) + .then(function(model) { + equal(env.syncArgs.method, 'create'); + equal(env.syncArgs.model, model); + equal(model.get('label'), 'f'); + equal(model.collection, collection); + }) + .then(done, done); }); test("create with validate:true enforces validation", 3, function() { @@ -537,11 +555,11 @@ equal(col.create({"foo":"bar"}, {validate:true}), false); }); - test("create will pass extra options to success callback", 1, function () { + test("create will pass extra options to success callback", 1, function (assert) { + var done = assert.async(); var Model = Backbone.Model.extend({ sync: function (method, model, options) { _.extend(options, {specialSync: true}); - return Backbone.Model.prototype.sync.call(this, method, model, options); } }); @@ -556,12 +574,11 @@ ok(options.specialSync, "Options were passed correctly to callback"); }; - collection.create({}, {success: success}); - this.ajaxSettings.success(); - + collection.create({}, {success: success}).then(done, done); }); - test("create with wait:true should not call collection.parse", 0, function() { + test("create with wait:true should not call collection.parse", 0, function(assert) { + var done = assert.async(); var Collection = Backbone.Collection.extend({ url: '/test', parse: function () { @@ -571,23 +588,25 @@ var collection = new Collection; - collection.create({}, {wait: true}); - this.ajaxSettings.success(); + collection.create({}, {wait: true}) + .then(done, done); }); - test("a failing create returns model with errors", function() { + test("a failing create returns model with errors", function(assert) { + var done = assert.async(); var ValidatingModel = Backbone.Model.extend({ - validate: function(attrs) { - return "fail"; - } + validate: _.constant("fail") }); var ValidatingCollection = Backbone.Collection.extend({ model: ValidatingModel }); var col = new ValidatingCollection(); - var m = col.create({"foo":"bar"}); - equal(m.validationError, 'fail'); - equal(col.length, 1); + col.create({"foo":"bar"}) + ['catch'](function(validationError) { + equal(validationError, 'fail'); + equal(col.length, 1); + }) + .then(done, done); }); test("initialize", 1, function() { @@ -870,26 +889,26 @@ ok(colUndefined.comparator); }); - test("#1355 - `options` is passed to success callbacks", 2, function(){ + test("#1355 - `options` is passed to success callbacks", 2, function(assert){ + var done = _.after(2, assert.async()); var m = new Backbone.Model({x:1}); var col = new Backbone.Collection(); var opts = { - opts: true, + custom: true, success: function(collection, resp, options) { - ok(options.opts); + ok(options.custom); } }; - col.sync = m.sync = function( method, collection, options ){ - options.success({}); - }; - col.fetch(opts); - col.create(m, opts); + col.sync = m.sync = _.constant({}); + col.fetch(opts).then(done, done); + col.create(m, opts).then(done, done); }); - test("#1412 - Trigger 'request' and 'sync' events.", 4, function() { + test("#1412 - Trigger 'request' and 'sync' events.", 4, function(assert) { + var done = assert.async(); var collection = new Backbone.Collection; collection.url = '/test'; - Backbone.ajax = function(settings){ settings.success(); }; + Backbone.ajax = _.noop; collection.on('request', function(obj, xhr, options) { ok(obj === collection, "collection has correct 'request' event after fetching"); @@ -897,25 +916,28 @@ collection.on('sync', function(obj, response, options) { ok(obj === collection, "collection has correct 'sync' event after fetching"); }); - collection.fetch(); - collection.off(); + collection.fetch() + .then(function() { + collection.off(); - collection.on('request', function(obj, xhr, options) { - ok(obj === collection.get(1), "collection has correct 'request' event after one of its models save"); - }); - collection.on('sync', function(obj, response, options) { - ok(obj === collection.get(1), "collection has correct 'sync' event after one of its models save"); - }); - collection.create({id: 1}); - collection.off(); + collection.on('request', function(obj, xhr, options) { + ok(obj === collection.get(1), "collection has correct 'request' event after one of its models save"); + }); + collection.on('sync', function(obj, response, options) { + ok(obj === collection.get(1), "collection has correct 'sync' event after one of its models save"); + }); + return collection.create({id: 1}); + }) + .then(function() { + collection.off(); + }) + .then(done, done); }); - test("#3283 - fetch, create calls success with context", 2, function() { + test("#3283 - fetch, create calls success with context", 2, function(assert) { + var done = _.after(2, assert.async()); var collection = new Backbone.Collection; collection.url = '/test'; - Backbone.ajax = function(settings) { - settings.success.call(settings.context); - }; var obj = {}; var options = { context: obj, @@ -924,16 +946,18 @@ } }; - collection.fetch(options); - collection.create({id: 1}, options); + collection.fetch(options).then(done, done); + collection.create({id: 1}, options).then(done, done); }); - test("#1447 - create with wait adds model.", 1, function() { + test("#1447 - create with wait adds model.", 1, function(assert) { + var done = assert.async(); var collection = new Backbone.Collection; var model = new Backbone.Model; - model.sync = function(method, model, options){ options.success(); }; + model.sync = _.noop; collection.on('add', function(){ ok(true); }); - collection.create(model, {wait: true}); + collection.create(model, {wait: true}) + .then(done, done); }); test("#1448 - add sorts collection after merge.", 1, function() { @@ -983,7 +1007,8 @@ deepEqual(added, [1, 2]); }); - test("fetch parses models by default", 1, function() { + test("fetch parses models by default", 1, function(assert) { + var done = assert.async(); var model = {}; var Collection = Backbone.Collection.extend({ url: 'test', @@ -993,8 +1018,10 @@ } }) }); - new Collection().fetch(); - this.ajaxSettings.success([model]); + var collection = new Collection(); + collection.sync = _.constant(model); + collection.fetch() + .then(done, done); }); test("`sort` shouldn't always fire on `add`", 1, function() { @@ -1262,7 +1289,9 @@ collection.set(res, {parse: true}); }); - asyncTest("#1939 - `parse` is passed `options`", 1, function () { + test("#1939 - `parse` is passed `options`", 1, function (assert) { + var done = assert.async(); + Backbone.ajax = _.constant({ someHeader: 'headerValue' }); var collection = new (Backbone.Collection.extend({ url: '/', parse: function (data, options) { @@ -1270,23 +1299,15 @@ return data; } })); - var ajax = Backbone.ajax; - Backbone.ajax = function (params) { - _.defer(params.success); - return {someHeader: 'headerValue'}; - }; - collection.fetch({ - success: function () { start(); } - }); - Backbone.ajax = ajax; + collection.fetch().then(done, done); }); - test("fetch will pass extra options to success callback", 1, function () { + test("fetch will pass extra options to success callback", 1, function (assert) { + var done = assert.async(); var SpecialSyncCollection = Backbone.Collection.extend({ url: '/test', sync: function (method, collection, options) { _.extend(options, { specialSync: true }); - return Backbone.Collection.prototype.sync.call(this, method, collection, options); } }); @@ -1296,8 +1317,7 @@ ok(options.specialSync, "Options were passed correctly to callback"); }; - collection.fetch({ success: onSuccess }); - this.ajaxSettings.success(); + collection.fetch({ success: onSuccess }).then(done, done); }); test("`add` only `sort`s when necessary", 2, function () { @@ -1350,15 +1370,19 @@ equal(collection.length, 2); }); - test("#2606 - Collection#create, success arguments", 1, function() { + test("#2606 - Collection#create, success arguments", 2, function(assert) { + var done = assert.async(); + Backbone.ajax = _.constant('response'); var collection = new Backbone.Collection; collection.url = 'test'; collection.create({}, { + custom: true, success: function(model, resp, options) { strictEqual(resp, 'response'); + ok(options.custom); } - }); - this.ajaxSettings.success('response'); + }) + .then(done, done); }); test("#2612 - nested `parse` works with `Collection#set`", function() { diff --git a/test/index.html b/test/index.html index 3dad8c679..f8c3bc4ec 100644 --- a/test/index.html +++ b/test/index.html @@ -10,6 +10,7 @@ + diff --git a/test/model.js b/test/model.js index faaf61dda..8e76eb82a 100644 --- a/test/model.js +++ b/test/model.js @@ -480,21 +480,21 @@ model.set({lastName: 'Hicks'}); }); - test("validate after save", 2, function() { + test("validate after save", 2, function(assert) { + var done = assert.async(); var lastError, model = new Backbone.Model(); model.validate = function(attrs) { if (attrs.admin) return "Can't change admin status."; }; - model.sync = function(method, model, options) { - options.success.call(this, {admin: true}); - }; + model.sync = _.constant({admin: true}); model.on('invalid', function(model, error) { lastError = error; }); - model.save(null); - - equal(lastError, "Can't change admin status."); - equal(model.validationError, "Can't change admin status."); + model.save(null) + .then(function() { + equal(lastError, "Can't change admin status."); + equal(model.validationError, "Can't change admin status."); + }).then(done, done); }); test("save", 2, function() { @@ -503,20 +503,21 @@ ok(_.isEqual(this.syncArgs.model, doc)); }); - test("save, fetch, destroy triggers error event when an error occurs", 3, function () { + test("save, fetch, destroy triggers error event when an error occurs", 3, function (assert) { + var done = _.after(3, assert.async()); var model = new Backbone.Model(); model.on('error', function () { ok(true); + done(); }); - model.sync = function (method, model, options) { - options.error(); - }; + model.sync = _.constant(Promise.reject()); model.save({data: 2, id: 1}); model.fetch(); model.destroy(); }); - test("#3283 - save, fetch, destroy calls success with context", 3, function () { + test("#3283 - save, fetch, destroy calls success with context", 3, function (assert) { + var done = _.after(3, assert.async()); var model = new Backbone.Model(); var obj = {}; var options = { @@ -525,15 +526,14 @@ equal(this, obj); } }; - model.sync = function (method, model, options) { - options.success.call(options.context); - }; - model.save({data: 2, id: 1}, options); - model.fetch(options); - model.destroy(options); + model.sync = _.noop; + model.save({data: 2, id: 1}, options).then(done, done); + model.fetch(options).then(done, done); + model.destroy(options).then(done, done); }); - test("#3283 - save, fetch, destroy calls error with context", 3, function () { + test("#3283 - save, fetch, destroy calls error with context", 3, function (assert) { + var done = _.after(3, assert.async()); var model = new Backbone.Model(); var obj = {}; var options = { @@ -542,27 +542,35 @@ equal(this, obj); } }; - model.sync = function (method, model, options) { - options.error.call(options.context); - }; - model.save({data: 2, id: 1}, options); - model.fetch(options); - model.destroy(options); - }); - - test("#3470 - save and fetch with parse false", 2, function() { - var i = 0; - var model = new Backbone.Model(); - model.parse = function() { - ok(false); - }; - model.sync = function(method, model, options) { - options.success({i: ++i}); - }; - model.fetch({parse: false}); - equal(model.get('i'), i); - model.save(null, {parse: false}); - equal(model.get('i'), i); + model.sync = _.constant(Promise.reject()); + model.save({data: 2, id: 1}, options).then(done, done); + model.fetch(options).then(done, done); + model.destroy(options).then(done, done); + }); + + test("#3470 - save and fetch with parse false", 2, function(assert) { + var done = assert.async(); + new Promise(function(resolve) { + var i = 0; + var model = new Backbone.Model(); + model.parse = function() { + ok(false); + }; + model.sync = function() { + return {i: ++i}; + }; + model.fetch({parse: false}) + .then(function() { + equal(model.get('i'), 1); + }) + .then(function() { + return model.save(null, {parse: false}); + }) + .then(function() { + equal(model.get('i'), 2); + }) + .then(resolve); + }).then(done, done); }); test("save with PATCH", function() { @@ -587,26 +595,29 @@ deepEqual(doc.attributes, {b: 2, d: 4}); }); - test("save in positional style", 1, function() { + test("save in positional style", 1, function(assert) { + var done = assert.async(); var model = new Backbone.Model(); - model.sync = function(method, model, options) { - options.success(); - }; - model.save('title', 'Twelfth Night'); - equal(model.get('title'), 'Twelfth Night'); + model.sync = _.noop; + model.save('title', 'Twelfth Night') + .then(function() { + equal(model.get('title'), 'Twelfth Night'); + }).then(done, done); }); - test("save with non-object success response", 2, function () { + test("save with non-object success response", 2, function (assert) { + var done = _.after(2, assert.async()); var model = new Backbone.Model(); - model.sync = function(method, model, options) { - options.success('', options); - options.success(null, options); + var test = function(resp) { + model.sync = _.constant(resp); + model.save({testing:'empty'}) + .then(function () { + deepEqual(model.attributes, {testing:'empty'}); + }) + .then(done, done); }; - model.save({testing:'empty'}, { - success: function (model) { - deepEqual(model.attributes, {testing:'empty'}); - } - }); + test(''); + test(null); }); test("save with wait and supplied id", function() { @@ -618,7 +629,8 @@ equal(this.ajaxSettings.url, '/collection/42'); }); - test("save will pass extra options to success callback", 1, function () { + test("save will pass extra options to success callback", 1, function (assert) { + var done = assert.async(); var SpecialSyncModel = Backbone.Model.extend({ sync: function (method, model, options) { _.extend(options, { specialSync: true }); @@ -633,8 +645,8 @@ ok(options.specialSync, "Options were passed correctly to callback"); }; - model.save(null, { success: onSuccess }); - this.ajaxSettings.success(); + model.save(null, { success: onSuccess }) + .then(done, done); }); test("fetch", 2, function() { @@ -643,11 +655,11 @@ ok(_.isEqual(this.syncArgs.model, doc)); }); - test("fetch will pass extra options to success callback", 1, function () { + test("fetch will pass extra options to success callback", 1, function (assert) { + var done = assert.async(); var SpecialSyncModel = Backbone.Model.extend({ sync: function (method, model, options) { _.extend(options, { specialSync: true }); - return Backbone.Model.prototype.sync.call(this, method, model, options); }, urlRoot: '/test' }); @@ -658,24 +670,25 @@ ok(options.specialSync, "Options were passed correctly to callback"); }; - model.fetch({ success: onSuccess }); - this.ajaxSettings.success(); + model.fetch({ success: onSuccess }).then(done, done); }); - test("destroy", 3, function() { - doc.destroy(); - equal(this.syncArgs.method, 'delete'); - ok(_.isEqual(this.syncArgs.model, doc)); - - var newModel = new Backbone.Model; - equal(newModel.destroy(), false); + test("destroy", 2, function(assert) { + var done = assert.async(); + var env = this; + doc.destroy() + .then(function() { + equal(env.syncArgs.method, 'delete'); + ok(_.isEqual(env.syncArgs.model, doc)); + }) + .then(done, done); }); - test("destroy will pass extra options to success callback", 1, function () { + test("destroy will pass extra options to success callback", 1, function (assert) { + var done = assert.async(); var SpecialSyncModel = Backbone.Model.extend({ sync: function (method, model, options) { _.extend(options, { specialSync: true }); - return Backbone.Model.prototype.sync.call(this, method, model, options); }, urlRoot: '/test' }); @@ -686,8 +699,7 @@ ok(options.specialSync, "Options were passed correctly to callback"); }; - model.destroy({ success: onSuccess }); - this.ajaxSettings.success(); + model.destroy({ success: onSuccess }).then(done, done); }); test("non-persisted destroy", 1, function() { @@ -903,17 +915,21 @@ ok(this.syncArgs.model === model); }); - test("save without `wait` doesn't set invalid attributes", function () { + test("save without `wait` doesn't set invalid attributes", function (assert) { + var done = assert.async(); var model = new Backbone.Model(); - model.validate = function () { return 1; } - model.save({a: 1}); - equal(model.get('a'), void 0); + model.validate = _.constant(1); + model.save({a: 1}) + ['catch'](function(validationError) { + equal(model.get('a'), void 0); + }) + .then(done, done); }); test("save doesn't validate twice", function () { var model = new Backbone.Model(); var times = 0; - model.sync = function () {}; + model.sync = _.noop; model.validate = function () { ++times; } model.save({}); equal(times, 1); @@ -933,18 +949,22 @@ equal(model.previous(''), true); }); - test("`save` with `wait` sends correct attributes", 5, function() { + test("`save` with `wait` sends correct attributes", 5, function(assert) { + var done = assert.async(); + var env = this; var changed = 0; var model = new Backbone.Model({x: 1, y: 2}); model.url = '/test'; model.on('change:x', function() { changed++; }); - model.save({x: 3}, {wait: true}); - deepEqual(JSON.parse(this.ajaxSettings.data), {x: 3, y: 2}); + model.save({x: 3}, {wait: true}) + .then(function() { + deepEqual(JSON.parse(env.ajaxSettings.data), {x: 3, y: 2}); + equal(model.get('x'), 3); + equal(changed, 1); + }) + .then(done, done); equal(model.get('x'), 1); equal(changed, 0); - this.syncArgs.options.success({}); - equal(model.get('x'), 3); - equal(changed, 1); }); test("a failed `save` with `wait` doesn't leave attributes behind", 1, function() { @@ -954,14 +974,16 @@ equal(model.get('x'), void 0); }); - test("#1030 - `save` with `wait` results in correct attributes if success is called during sync", 2, function() { + test("#1030 - `save` with `wait` results in correct attributes if success is called during sync", 2, function(assert) { + var done = assert.async(); var model = new Backbone.Model({x: 1, y: 2}); - model.sync = function(method, model, options) { - options.success(); - }; + model.sync = _.noop; model.on("change:x", function() { ok(true); }); - model.save({x: 3}, {wait: true}); - equal(model.get('x'), 3); + model.save({x: 3}, {wait: true}) + .then(function() { + equal(model.get('x'), 3); + }) + .then(done, done); }); test("save with wait validates attributes", function() { @@ -1122,56 +1144,82 @@ ok(!options.unset); }); - test("#1355 - `options` is passed to success callbacks", 3, function() { + test("#1355 - `options` is passed to success callback, save", function(assert) { + var done = assert.async(); + var model = new Backbone.Model(); + var opts = { + success: function( model, resp, options ) { + ok(options); + } + }; + model.sync = _.noop; + model.save({id: 1}, opts).then(done, done); + }); + + test("#1355 - `options` is passed to success callback, fetch", function(assert) { + var done = assert.async(); var model = new Backbone.Model(); var opts = { success: function( model, resp, options ) { ok(options); } }; - model.sync = function(method, model, options) { - options.success(); + model.sync = _.noop; + model.fetch(opts).then(done, done); + }); + + test("#1355 - `options` is passed to success callback, destroy", function(assert) { + var done = assert.async(); + var model = new Backbone.Model(); + var opts = { + success: function( model, resp, options ) { + ok(options); + } }; - model.save({id: 1}, opts); - model.fetch(opts); - model.destroy(opts); + model.sync = _.noop; + model.destroy(opts).then(done, done); }); - test("#1412 - Trigger 'sync' event.", 3, function() { + test("#1412 - Trigger 'sync' event.", 3, function(assert) { + var done = _.after(3, assert.async()); var model = new Backbone.Model({id: 1}); - model.sync = function (method, model, options) { options.success(); }; + model.sync = _.noop; model.on('sync', function(){ ok(true); }); - model.fetch(); - model.save(); - model.destroy(); + model.fetch().then(done, done); + model.save().then(done, done); + model.destroy().then(done, done); }); - asyncTest("#1365 - Destroy: New models execute success callback.", 2, function() { + test("#1365 - Destroy: New models execute success callback.", 2, function(assert) { + var done = assert.async(); new Backbone.Model() .on('sync', function() { ok(false); }) .on('destroy', function(){ ok(true); }) - .destroy({ success: function(){ + .destroy() + .then(function() { ok(true); - start(); - }}); + }) + .then(done, done); }); - test("#1433 - Save: An invalid model cannot be persisted.", 1, function() { + test("#1433 - Save: An invalid model cannot be persisted.", function(assert) { + var done = assert.async(); var model = new Backbone.Model; - model.validate = function(){ return 'invalid'; }; + model.validate = _.constant(1); model.sync = function(){ ok(false); }; - strictEqual(model.save(), false); + model.save().then(done, done); + expect(0); }); - test("#1377 - Save without attrs triggers 'error'.", 1, function() { + test("#1377 - Save without attrs triggers 'error'.", 1, function(assert) { + var done = assert.async(); var Model = Backbone.Model.extend({ url: '/test/', - sync: function(method, model, options){ options.success(); }, - validate: function(){ return 'invalid'; } + validate: _.constant(1) }); var model = new Model({id: 1}); model.on('invalid', function(){ ok(true); }); - model.save(); + model.save().then(done, done); }); test("#1545 - `undefined` can be passed to a model constructor without coersion", function() { @@ -1185,18 +1233,15 @@ var undefinedattrs = new Model(undefined); }); - asyncTest("#1478 - Model `save` does not trigger change on unchanged attributes", 0, function() { + test("#1478 - Model `save` does not trigger change on unchanged attributes", 0, function(assert) { + var done = assert.async(); var Model = Backbone.Model.extend({ - sync: function(method, model, options) { - setTimeout(function(){ - options.success(); - start(); - }, 0); - } + sync: _.constant(Promise.resolve()) }); new Model({x: true}) - .on('change:x', function(){ ok(false); }) - .save(null, {wait: true}); + .on('change:x', function(){ ok(false); }) + .save(null, {wait: true}) + .then(done, done); }); test("#1664 - Changing from one value, silently to another, back to original triggers a change.", 1, function() { @@ -1309,4 +1354,28 @@ model.set({a: true}); }); + test("save, fetch, destroy propagates errors from event listeners", 3, function (assert) { + var done = _.after(3, assert.async()); + var model = new Backbone.Model(); + model.on('sync', function () { + throw "boom"; + }); + model.sync = _.constant(Promise.resolve()); + model.save({data: 2, id: 1}) + ['catch'](function(error) { + equal(error, "boom"); + done(); + }); + model.fetch() + ['catch'](function(error) { + equal(error, "boom"); + done(); + }); + model.destroy() + ['catch'](function(error) { + equal(error, "boom"); + done(); + }); + }); + })(); diff --git a/test/setup/environment.js b/test/setup/environment.js index 309f23c30..35b948def 100644 --- a/test/setup/environment.js +++ b/test/setup/environment.js @@ -26,7 +26,7 @@ model: model, options: options }; - sync.apply(this, arguments); + return sync.apply(this, arguments); }; }); diff --git a/test/sync.js b/test/sync.js index a1f68ac9e..ba3fcd343 100644 --- a/test/sync.js +++ b/test/sync.js @@ -153,13 +153,14 @@ Backbone.sync('create', model); }); - test("Call provided error callback on error.", 1, function() { + test("Call provided error callback on error.", 1, function(assert) { + var done = assert.async(); var model = new Backbone.Model; + Backbone.ajax = _.constant(Promise.reject()); model.url = '/test'; - Backbone.sync('read', model, { - error: function() { ok(true); } - }); - this.ajaxSettings.error(); + Backbone.sync('read', model) + ['catch'](function() { ok(true); }) + .then(done, done); }); test('Use Backbone.emulateHTTP as default.', 2, function() { @@ -207,15 +208,19 @@ strictEqual(this.ajaxSettings.beforeSend(xhr), false); }); - test('#2928 - Pass along `textStatus` and `errorThrown`.', 2, function() { + test('#2928 - Pass along `textStatus` and `errorThrown`.', 2, function(assert) { + var done = assert.async(); + Backbone.ajax = function(settings) { + settings.error({}, 'textStatus', 'errorThrown'); + return Promise.reject(); + }; var model = new Backbone.Model; model.url = '/test'; model.on('error', function(model, xhr, options) { strictEqual(options.textStatus, 'textStatus'); strictEqual(options.errorThrown, 'errorThrown'); }); - model.fetch(); - this.ajaxSettings.error({}, 'textStatus', 'errorThrown'); + model.fetch().then(done, done); }); })(); diff --git a/test/vendor/promise.js b/test/vendor/promise.js new file mode 100644 index 000000000..b2aa2e868 --- /dev/null +++ b/test/vendor/promise.js @@ -0,0 +1,746 @@ +(function e(t, n, r) { + function s(o, u) { + if (!n[o]) { + if (!t[o]) { + var a = typeof require == "function" && require; + if (!u && a) return a(o, !0); + if (i) return i(o, !0); + var f = new Error("Cannot find module '" + o + "'"); + throw f.code = "MODULE_NOT_FOUND", f; + } + var l = n[o] = { + exports: {} + }; + t[o][0].call(l.exports, function(e) { + var n = t[o][1][e]; + return s(n ? n : e); + }, l, l.exports, e, t, n, r); + } + return n[o].exports; + } + var i = typeof require == "function" && require; + for (var o = 0; o < r.length; o++) s(r[o]); + return s; +})({ + 1: [ function(require, module, exports) { + module.exports = function() { + var events = require("events"); + var domain = {}; + domain.createDomain = domain.create = function() { + var d = new events.EventEmitter(); + function emitError(e) { + d.emit("error", e); + } + d.add = function(emitter) { + emitter.on("error", emitError); + }; + d.remove = function(emitter) { + emitter.removeListener("error", emitError); + }; + d.bind = function(fn) { + return function() { + var args = Array.prototype.slice.call(arguments); + try { + fn.apply(null, args); + } catch (err) { + emitError(err); + } + }; + }; + d.intercept = function(fn) { + return function(err) { + if (err) { + emitError(err); + } else { + var args = Array.prototype.slice.call(arguments, 1); + try { + fn.apply(null, args); + } catch (err) { + emitError(err); + } + } + }; + }; + d.run = function(fn) { + try { + fn(); + } catch (err) { + emitError(err); + } + return this; + }; + d.dispose = function() { + this.removeAllListeners(); + return this; + }; + d.enter = d.exit = function() { + return this; + }; + return d; + }; + return domain; + }.call(this); + }, { + events: 2 + } ], + 2: [ function(require, module, exports) { + function EventEmitter() { + this._events = this._events || {}; + this._maxListeners = this._maxListeners || undefined; + } + module.exports = EventEmitter; + EventEmitter.EventEmitter = EventEmitter; + EventEmitter.prototype._events = undefined; + EventEmitter.prototype._maxListeners = undefined; + EventEmitter.defaultMaxListeners = 10; + EventEmitter.prototype.setMaxListeners = function(n) { + if (!isNumber(n) || n < 0 || isNaN(n)) throw TypeError("n must be a positive number"); + this._maxListeners = n; + return this; + }; + EventEmitter.prototype.emit = function(type) { + var er, handler, len, args, i, listeners; + if (!this._events) this._events = {}; + if (type === "error") { + if (!this._events.error || isObject(this._events.error) && !this._events.error.length) { + er = arguments[1]; + if (er instanceof Error) { + throw er; + } + throw TypeError('Uncaught, unspecified "error" event.'); + } + } + handler = this._events[type]; + if (isUndefined(handler)) return false; + if (isFunction(handler)) { + switch (arguments.length) { + case 1: + handler.call(this); + break; + + case 2: + handler.call(this, arguments[1]); + break; + + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + + default: + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) args[i - 1] = arguments[i]; + handler.apply(this, args); + } + } else if (isObject(handler)) { + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) args[i - 1] = arguments[i]; + listeners = handler.slice(); + len = listeners.length; + for (i = 0; i < len; i++) listeners[i].apply(this, args); + } + return true; + }; + EventEmitter.prototype.addListener = function(type, listener) { + var m; + if (!isFunction(listener)) throw TypeError("listener must be a function"); + if (!this._events) this._events = {}; + if (this._events.newListener) this.emit("newListener", type, isFunction(listener.listener) ? listener.listener : listener); + if (!this._events[type]) this._events[type] = listener; else if (isObject(this._events[type])) this._events[type].push(listener); else this._events[type] = [ this._events[type], listener ]; + if (isObject(this._events[type]) && !this._events[type].warned) { + var m; + if (!isUndefined(this._maxListeners)) { + m = this._maxListeners; + } else { + m = EventEmitter.defaultMaxListeners; + } + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error("(node) warning: possible EventEmitter memory " + "leak detected. %d listeners added. " + "Use emitter.setMaxListeners() to increase limit.", this._events[type].length); + if (typeof console.trace === "function") { + console.trace(); + } + } + } + return this; + }; + EventEmitter.prototype.on = EventEmitter.prototype.addListener; + EventEmitter.prototype.once = function(type, listener) { + if (!isFunction(listener)) throw TypeError("listener must be a function"); + var fired = false; + function g() { + this.removeListener(type, g); + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + g.listener = listener; + this.on(type, g); + return this; + }; + EventEmitter.prototype.removeListener = function(type, listener) { + var list, position, length, i; + if (!isFunction(listener)) throw TypeError("listener must be a function"); + if (!this._events || !this._events[type]) return this; + list = this._events[type]; + length = list.length; + position = -1; + if (list === listener || isFunction(list.listener) && list.listener === listener) { + delete this._events[type]; + if (this._events.removeListener) this.emit("removeListener", type, listener); + } else if (isObject(list)) { + for (i = length; i-- > 0; ) { + if (list[i] === listener || list[i].listener && list[i].listener === listener) { + position = i; + break; + } + } + if (position < 0) return this; + if (list.length === 1) { + list.length = 0; + delete this._events[type]; + } else { + list.splice(position, 1); + } + if (this._events.removeListener) this.emit("removeListener", type, listener); + } + return this; + }; + EventEmitter.prototype.removeAllListeners = function(type) { + var key, listeners; + if (!this._events) return this; + if (!this._events.removeListener) { + if (arguments.length === 0) this._events = {}; else if (this._events[type]) delete this._events[type]; + return this; + } + if (arguments.length === 0) { + for (key in this._events) { + if (key === "removeListener") continue; + this.removeAllListeners(key); + } + this.removeAllListeners("removeListener"); + this._events = {}; + return this; + } + listeners = this._events[type]; + if (isFunction(listeners)) { + this.removeListener(type, listeners); + } else { + while (listeners.length) this.removeListener(type, listeners[listeners.length - 1]); + } + delete this._events[type]; + return this; + }; + EventEmitter.prototype.listeners = function(type) { + var ret; + if (!this._events || !this._events[type]) ret = []; else if (isFunction(this._events[type])) ret = [ this._events[type] ]; else ret = this._events[type].slice(); + return ret; + }; + EventEmitter.listenerCount = function(emitter, type) { + var ret; + if (!emitter._events || !emitter._events[type]) ret = 0; else if (isFunction(emitter._events[type])) ret = 1; else ret = emitter._events[type].length; + return ret; + }; + function isFunction(arg) { + return typeof arg === "function"; + } + function isNumber(arg) { + return typeof arg === "number"; + } + function isObject(arg) { + return typeof arg === "object" && arg !== null; + } + function isUndefined(arg) { + return arg === void 0; + } + }, {} ], + 3: [ function(require, module, exports) { + var process = module.exports = {}; + var queue = []; + var draining = false; + function drainQueue() { + if (draining) { + return; + } + draining = true; + var currentQueue; + var len = queue.length; + while (len) { + currentQueue = queue; + queue = []; + var i = -1; + while (++i < len) { + currentQueue[i](); + } + len = queue.length; + } + draining = false; + } + process.nextTick = function(fun) { + queue.push(fun); + if (!draining) { + setTimeout(drainQueue, 0); + } + }; + process.title = "browser"; + process.browser = true; + process.env = {}; + process.argv = []; + process.version = ""; + process.versions = {}; + function noop() {} + process.on = noop; + process.addListener = noop; + process.once = noop; + process.off = noop; + process.removeListener = noop; + process.removeAllListeners = noop; + process.emit = noop; + process.binding = function(name) { + throw new Error("process.binding is not supported"); + }; + process.cwd = function() { + return "/"; + }; + process.chdir = function(dir) { + throw new Error("process.chdir is not supported"); + }; + process.umask = function() { + return 0; + }; + }, {} ], + 4: [ function(require, module, exports) { + "use strict"; + var asap = require("asap/raw"); + function noop() {} + var LAST_ERROR = null; + var IS_ERROR = {}; + function getThen(obj) { + try { + return obj.then; + } catch (ex) { + LAST_ERROR = ex; + return IS_ERROR; + } + } + function tryCallOne(fn, a) { + try { + return fn(a); + } catch (ex) { + LAST_ERROR = ex; + return IS_ERROR; + } + } + function tryCallTwo(fn, a, b) { + try { + fn(a, b); + } catch (ex) { + LAST_ERROR = ex; + return IS_ERROR; + } + } + module.exports = Promise; + function Promise(fn) { + if (typeof this !== "object") { + throw new TypeError("Promises must be constructed via new"); + } + if (typeof fn !== "function") { + throw new TypeError("not a function"); + } + this._32 = 0; + this._8 = null; + this._89 = []; + if (fn === noop) return; + doResolve(fn, this); + } + Promise._83 = noop; + Promise.prototype.then = function(onFulfilled, onRejected) { + if (this.constructor !== Promise) { + return safeThen(this, onFulfilled, onRejected); + } + var res = new Promise(noop); + handle(this, new Handler(onFulfilled, onRejected, res)); + return res; + }; + function safeThen(self, onFulfilled, onRejected) { + return new self.constructor(function(resolve, reject) { + var res = new Promise(noop); + res.then(resolve, reject); + handle(self, new Handler(onFulfilled, onRejected, res)); + }); + } + function handle(self, deferred) { + while (self._32 === 3) { + self = self._8; + } + if (self._32 === 0) { + self._89.push(deferred); + return; + } + asap(function() { + var cb = self._32 === 1 ? deferred.onFulfilled : deferred.onRejected; + if (cb === null) { + if (self._32 === 1) { + resolve(deferred.promise, self._8); + } else { + reject(deferred.promise, self._8); + } + return; + } + var ret = tryCallOne(cb, self._8); + if (ret === IS_ERROR) { + reject(deferred.promise, LAST_ERROR); + } else { + resolve(deferred.promise, ret); + } + }); + } + function resolve(self, newValue) { + if (newValue === self) { + return reject(self, new TypeError("A promise cannot be resolved with itself.")); + } + if (newValue && (typeof newValue === "object" || typeof newValue === "function")) { + var then = getThen(newValue); + if (then === IS_ERROR) { + return reject(self, LAST_ERROR); + } + if (then === self.then && newValue instanceof Promise) { + self._32 = 3; + self._8 = newValue; + finale(self); + return; + } else if (typeof then === "function") { + doResolve(then.bind(newValue), self); + return; + } + } + self._32 = 1; + self._8 = newValue; + finale(self); + } + function reject(self, newValue) { + self._32 = 2; + self._8 = newValue; + finale(self); + } + function finale(self) { + for (var i = 0; i < self._89.length; i++) { + handle(self, self._89[i]); + } + self._89 = null; + } + function Handler(onFulfilled, onRejected, promise) { + this.onFulfilled = typeof onFulfilled === "function" ? onFulfilled : null; + this.onRejected = typeof onRejected === "function" ? onRejected : null; + this.promise = promise; + } + function doResolve(fn, promise) { + var done = false; + var res = tryCallTwo(fn, function(value) { + if (done) return; + done = true; + resolve(promise, value); + }, function(reason) { + if (done) return; + done = true; + reject(promise, reason); + }); + if (!done && res === IS_ERROR) { + done = true; + reject(promise, LAST_ERROR); + } + } + }, { + "asap/raw": 8 + } ], + 5: [ function(require, module, exports) { + "use strict"; + var Promise = require("./core.js"); + var asap = require("asap/raw"); + module.exports = Promise; + var TRUE = valuePromise(true); + var FALSE = valuePromise(false); + var NULL = valuePromise(null); + var UNDEFINED = valuePromise(undefined); + var ZERO = valuePromise(0); + var EMPTYSTRING = valuePromise(""); + function valuePromise(value) { + var p = new Promise(Promise._83); + p._32 = 1; + p._8 = value; + return p; + } + Promise.resolve = function(value) { + if (value instanceof Promise) return value; + if (value === null) return NULL; + if (value === undefined) return UNDEFINED; + if (value === true) return TRUE; + if (value === false) return FALSE; + if (value === 0) return ZERO; + if (value === "") return EMPTYSTRING; + if (typeof value === "object" || typeof value === "function") { + try { + var then = value.then; + if (typeof then === "function") { + return new Promise(then.bind(value)); + } + } catch (ex) { + return new Promise(function(resolve, reject) { + reject(ex); + }); + } + } + return valuePromise(value); + }; + Promise.all = function(arr) { + var args = Array.prototype.slice.call(arr); + return new Promise(function(resolve, reject) { + if (args.length === 0) return resolve([]); + var remaining = args.length; + function res(i, val) { + if (val && (typeof val === "object" || typeof val === "function")) { + if (val instanceof Promise && val.then === Promise.prototype.then) { + while (val._32 === 3) { + val = val._8; + } + if (val._32 === 1) return res(i, val._8); + if (val._32 === 2) reject(val._8); + val.then(function(val) { + res(i, val); + }, reject); + return; + } else { + var then = val.then; + if (typeof then === "function") { + var p = new Promise(then.bind(val)); + p.then(function(val) { + res(i, val); + }, reject); + return; + } + } + } + args[i] = val; + if (--remaining === 0) { + resolve(args); + } + } + for (var i = 0; i < args.length; i++) { + res(i, args[i]); + } + }); + }; + Promise.reject = function(value) { + return new Promise(function(resolve, reject) { + reject(value); + }); + }; + Promise.race = function(values) { + return new Promise(function(resolve, reject) { + values.forEach(function(value) { + Promise.resolve(value).then(resolve, reject); + }); + }); + }; + Promise.prototype["catch"] = function(onRejected) { + return this.then(null, onRejected); + }; + }, { + "./core.js": 4, + "asap/raw": 8 + } ], + 6: [ function(require, module, exports) { + "use strict"; + var rawAsap = require("./raw"); + var freeTasks = []; + var pendingErrors = []; + var requestErrorThrow = rawAsap.makeRequestCallFromTimer(throwFirstError); + function throwFirstError() { + if (pendingErrors.length) { + throw pendingErrors.shift(); + } + } + module.exports = asap; + function asap(task) { + var rawTask; + if (freeTasks.length) { + rawTask = freeTasks.pop(); + } else { + rawTask = new RawTask(); + } + rawTask.task = task; + rawAsap(rawTask); + } + function RawTask() { + this.task = null; + } + RawTask.prototype.call = function() { + try { + this.task.call(); + } catch (error) { + if (asap.onerror) { + asap.onerror(error); + } else { + pendingErrors.push(error); + requestErrorThrow(); + } + } finally { + this.task = null; + freeTasks[freeTasks.length] = this; + } + }; + }, { + "./raw": 7 + } ], + 7: [ function(require, module, exports) { + (function(global) { + "use strict"; + module.exports = rawAsap; + function rawAsap(task) { + if (!queue.length) { + requestFlush(); + flushing = true; + } + queue[queue.length] = task; + } + var queue = []; + var flushing = false; + var requestFlush; + var index = 0; + var capacity = 1024; + function flush() { + while (index < queue.length) { + var currentIndex = index; + index = index + 1; + queue[currentIndex].call(); + if (index > capacity) { + for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) { + queue[scan] = queue[scan + index]; + } + queue.length -= index; + index = 0; + } + } + queue.length = 0; + index = 0; + flushing = false; + } + var BrowserMutationObserver = global.MutationObserver || global.WebKitMutationObserver; + if (typeof BrowserMutationObserver === "function") { + requestFlush = makeRequestCallFromMutationObserver(flush); + } else { + requestFlush = makeRequestCallFromTimer(flush); + } + rawAsap.requestFlush = requestFlush; + function makeRequestCallFromMutationObserver(callback) { + var toggle = 1; + var observer = new BrowserMutationObserver(callback); + var node = document.createTextNode(""); + observer.observe(node, { + characterData: true + }); + return function requestCall() { + toggle = -toggle; + node.data = toggle; + }; + } + function makeRequestCallFromTimer(callback) { + return function requestCall() { + var timeoutHandle = setTimeout(handleTimer, 0); + var intervalHandle = setInterval(handleTimer, 50); + function handleTimer() { + clearTimeout(timeoutHandle); + clearInterval(intervalHandle); + callback(); + } + }; + } + rawAsap.makeRequestCallFromTimer = makeRequestCallFromTimer; + }).call(this, typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}); + }, {} ], + 8: [ function(require, module, exports) { + (function(process) { + "use strict"; + var domain; + var hasSetImmediate = typeof setImmediate === "function"; + module.exports = rawAsap; + function rawAsap(task) { + if (!queue.length) { + requestFlush(); + flushing = true; + } + queue[queue.length] = task; + } + var queue = []; + var flushing = false; + var index = 0; + var capacity = 1024; + function flush() { + while (index < queue.length) { + var currentIndex = index; + index = index + 1; + queue[currentIndex].call(); + if (index > capacity) { + for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) { + queue[scan] = queue[scan + index]; + } + queue.length -= index; + index = 0; + } + } + queue.length = 0; + index = 0; + flushing = false; + } + rawAsap.requestFlush = requestFlush; + function requestFlush() { + var parentDomain = process.domain; + if (parentDomain) { + if (!domain) { + domain = require("domain"); + } + domain.active = process.domain = null; + } + if (flushing && hasSetImmediate) { + setImmediate(flush); + } else { + process.nextTick(flush); + } + if (parentDomain) { + domain.active = process.domain = parentDomain; + } + } + }).call(this, require("_process")); + }, { + _process: 3, + domain: 1 + } ], + 9: [ function(require, module, exports) { + if (typeof Promise.prototype.done !== "function") { + Promise.prototype.done = function(onFulfilled, onRejected) { + var self = arguments.length ? this.then.apply(this, arguments) : this; + self.then(null, function(err) { + setTimeout(function() { + throw err; + }, 0); + }); + }; + } + }, {} ], + 10: [ function(require, module, exports) { + var asap = require("asap"); + if (typeof Promise === "undefined") { + Promise = require("./lib/core.js"); + require("./lib/es6-extensions.js"); + } + require("./polyfill-done.js"); + }, { + "./lib/core.js": 4, + "./lib/es6-extensions.js": 5, + "./polyfill-done.js": 9, + asap: 6 + } ] +}, {}, [ 10 ]); +//# sourceMappingURL=/polyfills/promise-7.0.1.js.map \ No newline at end of file