From bc78a65fff1f5f52d95bb61f9526053e5d0d82db Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Mar 2013 13:42:07 +0200 Subject: [PATCH] js: Upgrade when.js from 1.8.1 to 2.0.0 --- docs/changes.rst | 2 +- js/Gruntfile.js | 1 + js/buster.js | 12 +- js/lib/{when-1.8.1.js => when-2.0.0.js} | 868 ++++++++++----------- js/lib/when-define-shim.js | 11 + js/package.json | 2 +- js/src/mopidy.js | 2 +- js/test/mopidy-test.js | 10 +- mopidy/frontends/http/data/mopidy.js | 916 ++++++++++++----------- mopidy/frontends/http/data/mopidy.min.js | 4 +- 10 files changed, 967 insertions(+), 861 deletions(-) rename js/lib/{when-1.8.1.js => when-2.0.0.js} (51%) create mode 100644 js/lib/when-define-shim.js diff --git a/docs/changes.rst b/docs/changes.rst index c315922b..df4caa86 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -71,7 +71,7 @@ v0.13.0 (in development) - Upgrade Mopidy.js' build system Grunt from 0.3 to 0.4. -- Upgrade Mopidy.js' dependencies when.js from 1.6.1 to 1.8.1. +- Upgrade Mopidy.js' dependencies when.js from 1.6.1 to 2.0.0. - Expose :meth:`mopidy.core.Core.get_uri_schemes` to HTTP clients. It is available through Mopidy.js as ``mopidy.getUriSchemes()``. diff --git a/js/Gruntfile.js b/js/Gruntfile.js index 195decd6..3039e98c 100644 --- a/js/Gruntfile.js +++ b/js/Gruntfile.js @@ -24,6 +24,7 @@ module.exports = function (grunt) { files: { "<%= meta.files.concat %>": [ "lib/bane-*.js", + "lib/when-define-shim.js", "lib/when-*.js", "src/mopidy.js" ] diff --git a/js/buster.js b/js/buster.js index 37f41d8a..1cc517c8 100644 --- a/js/buster.js +++ b/js/buster.js @@ -2,7 +2,11 @@ var config = module.exports; config.browser_tests = { environment: "browser", - libs: ["lib/**/*.js"], + libs: [ + "lib/bane-*.js", + "lib/when-define-shim.js", + "lib/when-*.js" + ], sources: ["src/**/*.js"], testHelpers: ["test/**/*-helper.js"], tests: ["test/**/*-test.js"] @@ -10,7 +14,11 @@ config.browser_tests = { config.node_tests = { environment: "node", - libs: ["lib/**/*.js"], + libs: [ + "lib/bane-*.js", + "lib/when-define-shim.js", + "lib/when-*.js" + ], sources: ["src/**/*.js"], testHelpers: ["test/**/*-helper.js"], tests: ["test/**/*-test.js"] diff --git a/js/lib/when-1.8.1.js b/js/lib/when-2.0.0.js similarity index 51% rename from js/lib/when-1.8.1.js rename to js/lib/when-2.0.0.js index 05c5a429..78249532 100644 --- a/js/lib/when-1.8.1.js +++ b/js/lib/when-2.0.0.js @@ -9,34 +9,27 @@ * * @author Brian Cavalier * @author John Hann - * - * @version 1.8.1 + * @version 2.0.0 */ - (function(define) { 'use strict'; define(function () { - var reduceArray, slice, undef; - // // Public API - // - when.defer = defer; // Create a deferred - when.resolve = resolve; // Create a resolved promise - when.reject = reject; // Create a rejected promise + when.defer = defer; // Create a deferred + when.resolve = resolve; // Create a resolved promise + when.reject = reject; // Create a rejected promise - when.join = join; // Join 2 or more promises + when.join = join; // Join 2 or more promises - when.all = all; // Resolve a list of promises - when.map = map; // Array.map() for promises - when.reduce = reduce; // Array.reduce() for promises + when.all = all; // Resolve a list of promises + when.map = map; // Array.map() for promises + when.reduce = reduce; // Array.reduce() for promises - when.any = any; // One-winner race - when.some = some; // Multi-winner race + when.any = any; // One-winner race + when.some = some; // Multi-winner race - when.chain = chain; // Make a promise trigger another resolver - - when.isPromise = isPromise; // Determine if a thing is a promise + when.isPromise = isPromise; // Determine if a thing is a promise /** * Register an observer for a promise or immediate value. @@ -59,77 +52,6 @@ define(function () { return resolve(promiseOrValue).then(onFulfilled, onRejected, onProgress); } - /** - * Returns promiseOrValue if promiseOrValue is a {@link Promise}, a new Promise if - * promiseOrValue is a foreign promise, or a new, already-fulfilled {@link Promise} - * whose value is promiseOrValue if promiseOrValue is an immediate value. - * - * @param {*} promiseOrValue - * @returns {Promise} Guaranteed to return a trusted Promise. If promiseOrValue - * is trusted, returns promiseOrValue, otherwise, returns a new, already-resolved - * when.js promise whose resolution value is: - * * the resolution value of promiseOrValue if it's a foreign promise, or - * * promiseOrValue if it's a value - */ - function resolve(promiseOrValue) { - var promise; - - if(promiseOrValue instanceof Promise) { - // It's a when.js promise, so we trust it - promise = promiseOrValue; - - } else if(isPromise(promiseOrValue)) { - // Assimilate foreign promises - promise = assimilate(promiseOrValue); - } else { - // It's a value, create a fulfilled promise for it. - promise = fulfilled(promiseOrValue); - } - - return promise; - } - - /** - * Assimilate an untrusted thenable by introducing a trusted middle man. - * Not a perfect strategy, but possibly the best we can do. - * IMPORTANT: This is the only place when.js should ever call an untrusted - * thenable's then() on an. Don't expose the return value to the untrusted thenable - * - * @param {*} thenable - * @param {function} thenable.then - * @returns {Promise} - */ - function assimilate(thenable) { - var d = defer(); - - // TODO: Enqueue this for future execution in 2.0 - try { - thenable.then( - function(value) { d.resolve(value); }, - function(reason) { d.reject(reason); }, - function(update) { d.progress(update); } - ); - } catch(e) { - d.reject(e); - } - - return d.promise; - } - - /** - * Returns a rejected promise for the supplied promiseOrValue. The returned - * promise will be rejected with: - * - promiseOrValue, if it is a value, or - * - if promiseOrValue is a promise - * - promiseOrValue's value after it is fulfilled - * - promiseOrValue's reason after it is rejected - * @param {*} promiseOrValue the rejected value of the returned {@link Promise} - * @return {Promise} rejected {@link Promise} - */ - function reject(promiseOrValue) { - return when(promiseOrValue, rejected); - } - /** * Trusted Promise constructor. A Promise created from this constructor is * a trusted when.js promise. Any other duck-typed promise is considered @@ -142,18 +64,6 @@ define(function () { } Promise.prototype = { - /** - * Register a callback that will be called when a promise is - * fulfilled or rejected. Optionally also register a progress handler. - * Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress) - * @param {function?} [onFulfilledOrRejected] - * @param {function?} [onProgress] - * @return {Promise} - */ - always: function(onFulfilledOrRejected, onProgress) { - return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress); - }, - /** * Register a rejection handler. Shortcut for .then(undefined, onRejected) * @param {function?} onRejected @@ -163,6 +73,26 @@ define(function () { return this.then(undef, onRejected); }, + /** + * Ensures that onFulfilledOrRejected will be called regardless of whether + * this promise is fulfilled or rejected. onFulfilledOrRejected WILL NOT + * receive the promises' value or reason. Any returned value will be disregarded. + * onFulfilledOrRejected may throw or return a rejected promise to signal + * an additional error. + * @param {function} onFulfilledOrRejected handler to be called regardless of + * fulfillment or rejection + * @returns {Promise} + */ + ensure: function(onFulfilledOrRejected) { + var self = this; + + return this.then(injectHandler, injectHandler).yield(self); + + function injectHandler() { + return resolve(onFulfilledOrRejected()); + } + }, + /** * Shortcut for .then(function() { return value; }) * @param {*} value @@ -191,201 +121,291 @@ define(function () { return onFulfilled.apply(undef, array); }); }); + }, + + /** + * Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected) + * @deprecated + */ + always: function(onFulfilledOrRejected, onProgress) { + return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress); } }; /** - * Create an already-resolved promise for the supplied value - * @private + * Returns a resolved promise. The returned promise will be + * - fulfilled with promiseOrValue if it is a value, or + * - if promiseOrValue is a promise + * - fulfilled with promiseOrValue's value after it is fulfilled + * - rejected with promiseOrValue's reason after it is rejected + * @param {*} value + * @return {Promise} + */ + function resolve(value) { + return promise(function(resolve) { + resolve(value); + }); + } + + /** + * Returns a rejected promise for the supplied promiseOrValue. The returned + * promise will be rejected with: + * - promiseOrValue, if it is a value, or + * - if promiseOrValue is a promise + * - promiseOrValue's value after it is fulfilled + * - promiseOrValue's reason after it is rejected + * @param {*} promiseOrValue the rejected value of the returned {@link Promise} + * @return {Promise} rejected {@link Promise} + */ + function reject(promiseOrValue) { + return when(promiseOrValue, rejected); + } + + /** + * Creates a new Deferred with fully isolated resolver and promise parts, + * either or both of which may be given out safely to consumers. + * The resolver has resolve, reject, and progress. The promise + * only has then. * + * @return {{ + * promise: Promise, + * resolver: { + * resolve: function:Promise, + * reject: function:Promise, + * notify: function:Promise + * }}} + */ + function defer() { + var deferred, pending, resolved; + + // Optimize object shape + deferred = { + promise: undef, resolve: undef, reject: undef, notify: undef, + resolver: { resolve: undef, reject: undef, notify: undef } + }; + + deferred.promise = pending = promise(makeDeferred); + + return deferred; + + function makeDeferred(resolvePending, rejectPending, notifyPending) { + deferred.resolve = deferred.resolver.resolve = function(value) { + if(resolved) { + return resolve(value); + } + resolved = true; + resolvePending(value); + return pending; + }; + + deferred.reject = deferred.resolver.reject = function(reason) { + if(resolved) { + return resolve(rejected(reason)); + } + resolved = true; + rejectPending(reason); + return pending; + }; + + deferred.notify = deferred.resolver.notify = function(update) { + notifyPending(update); + return update; + }; + } + } + + /** + * Creates a new promise whose fate is determined by resolver. + * @private (for now) + * @param {function} resolver function(resolve, reject, notify) + * @returns {Promise} promise whose fate is determine by resolver + */ + function promise(resolver) { + var value, handlers = []; + + // Call the provider resolver to seal the promise's fate + try { + resolver(promiseResolve, promiseReject, promiseNotify); + } catch(e) { + promiseReject(e); + } + + // Return the promise + return new Promise(then); + + /** + * Register handlers for this promise. + * @param [onFulfilled] {Function} fulfillment handler + * @param [onRejected] {Function} rejection handler + * @param [onProgress] {Function} progress handler + * @return {Promise} new Promise + */ + function then(onFulfilled, onRejected, onProgress) { + return promise(function(resolve, reject, notify) { + handlers + // Call handlers later, after resolution + ? handlers.push(function(value) { + value.then(onFulfilled, onRejected, onProgress) + .then(resolve, reject, notify); + }) + // Call handlers soon, but not in the current stack + : enqueue(function() { + value.then(onFulfilled, onRejected, onProgress) + .then(resolve, reject, notify); + }); + }); + } + + /** + * Transition from pre-resolution state to post-resolution state, notifying + * all listeners of the ultimate fulfillment or rejection + * @param {*|Promise} val resolution value + */ + function promiseResolve(val) { + if(!handlers) { + return; + } + + value = coerce(val); + scheduleHandlers(handlers, value); + + handlers = undef; + } + + /** + * Reject this promise with the supplied reason, which will be used verbatim. + * @param {*} reason reason for the rejection + */ + function promiseReject(reason) { + promiseResolve(rejected(reason)); + } + + /** + * Issue a progress event, notifying all progress listeners + * @param {*} update progress event payload to pass to all listeners + */ + function promiseNotify(update) { + if(handlers) { + scheduleHandlers(handlers, progressing(update)); + } + } + } + + /** + * Coerces x to a trusted Promise + * + * @private + * @param {*} x thing to coerce + * @returns {Promise} Guaranteed to return a trusted Promise. If x + * is trusted, returns x, otherwise, returns a new, trusted, already-resolved + * Promise whose resolution value is: + * * the resolution value of x if it's a foreign promise, or + * * x if it's a value + */ + function coerce(x) { + if(x instanceof Promise) { + return x; + } else if (x !== Object(x)) { + return fulfilled(x); + } + + return promise(function(resolve, reject, notify) { + enqueue(function() { + try { + // We must check and assimilate in the same tick, but not the + // current tick, careful only to access promiseOrValue.then once. + var untrustedThen = x.then; + + if(typeof untrustedThen === 'function') { + fcall(untrustedThen, x, resolve, reject, notify); + } else { + // It's a value, create a fulfilled wrapper + resolve(fulfilled(x)); + } + + } catch(e) { + // Something went wrong, reject + reject(e); + } + }); + }); + } + + /** + * Create an already-fulfilled promise for the supplied value + * @private * @param {*} value * @return {Promise} fulfilled promise */ function fulfilled(value) { - var p = new Promise(function(onFulfilled) { + var self = new Promise(function (onFulfilled) { try { - return resolve(typeof onFulfilled == 'function' ? onFulfilled(value) : value); - } catch(e) { + return typeof onFulfilled == 'function' + ? coerce(onFulfilled(value)) : self; + } catch (e) { return rejected(e); } }); - return p; + return self; } /** - * Create an already-rejected {@link Promise} with the supplied - * rejection reason. + * Create an already-rejected promise with the supplied rejection reason. * @private - * * @param {*} reason * @return {Promise} rejected promise */ function rejected(reason) { - var p = new Promise(function(_, onRejected) { + var self = new Promise(function (_, onRejected) { try { - return resolve(typeof onRejected == 'function' ? onRejected(reason) : rejected(reason)); - } catch(e) { + return typeof onRejected == 'function' + ? coerce(onRejected(reason)) : self; + } catch (e) { return rejected(e); } }); - return p; + return self; } /** - * Creates a new, Deferred with fully isolated resolver and promise parts, - * either or both of which may be given out safely to consumers. - * The Deferred itself has the full API: resolve, reject, progress, and - * then. The resolver has resolve, reject, and progress. The promise - * only has then. - * - * @return {Deferred} + * Create a progress promise with the supplied update. + * @private + * @param {*} update + * @return {Promise} progress promise */ - function defer() { - var deferred, promise, handlers, progressHandlers, - _then, _notify, _resolve; - - /** - * The promise for the new deferred - * @type {Promise} - */ - promise = new Promise(then); - - /** - * The full Deferred object, with {@link Promise} and {@link Resolver} parts - * @class Deferred - * @name Deferred - */ - deferred = { - then: then, // DEPRECATED: use deferred.promise.then - resolve: promiseResolve, - reject: promiseReject, - progress: promiseNotify, // DEPRECATED: use deferred.notify - notify: promiseNotify, - - promise: promise, - - resolver: { - resolve: promiseResolve, - reject: promiseReject, - progress: promiseNotify, // DEPRECATED: use deferred.notify - notify: promiseNotify + function progressing(update) { + var self = new Promise(function (_, __, onProgress) { + try { + return typeof onProgress == 'function' + ? progressing(onProgress(update)) : self; + } catch (e) { + return progressing(e); } - }; + }); - handlers = []; - progressHandlers = []; - - /** - * Pre-resolution then() that adds the supplied callback, errback, and progback - * functions to the registered listeners - * @private - * - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler - */ - _then = function(onFulfilled, onRejected, onProgress) { - var deferred, progressHandler; - - deferred = defer(); - - progressHandler = typeof onProgress === 'function' - ? function(update) { - try { - // Allow progress handler to transform progress event - deferred.notify(onProgress(update)); - } catch(e) { - // Use caught value as progress - deferred.notify(e); - } - } - : function(update) { deferred.notify(update); }; - - handlers.push(function(promise) { - promise.then(onFulfilled, onRejected) - .then(deferred.resolve, deferred.reject, progressHandler); - }); - - progressHandlers.push(progressHandler); - - return deferred.promise; - }; - - /** - * Issue a progress event, notifying all progress listeners - * @private - * @param {*} update progress event payload to pass to all listeners - */ - _notify = function(update) { - processQueue(progressHandlers, update); - return update; - }; - - /** - * Transition from pre-resolution state to post-resolution state, notifying - * all listeners of the resolution or rejection - * @private - * @param {*} value the value of this deferred - */ - _resolve = function(value) { - // Replace _then with one that directly notifies with the result. - _then = value.then; - // Replace _resolve so that this Deferred can only be resolved once - _resolve = resolve; - // Make _progress a noop, to disallow progress for the resolved promise. - _notify = identity; - - // Notify handlers - processQueue(handlers, value); - - // Free progressHandlers array since we'll never issue progress events - progressHandlers = handlers = undef; - - return value; - }; - - return deferred; - - /** - * Wrapper to allow _then to be replaced safely - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler - * @return {Promise} new promise - */ - function then(onFulfilled, onRejected, onProgress) { - // TODO: Promises/A+ check typeof onFulfilled, onRejected, onProgress - return _then(onFulfilled, onRejected, onProgress); - } - - /** - * Wrapper to allow _resolve to be replaced - */ - function promiseResolve(val) { - return _resolve(resolve(val)); - } - - /** - * Wrapper to allow _reject to be replaced - */ - function promiseReject(err) { - return _resolve(rejected(err)); - } - - /** - * Wrapper to allow _notify to be replaced - */ - function promiseNotify(update) { - return _notify(update); - } + return self; } /** - * Determines if promiseOrValue is a promise or not. Uses the feature - * test from http://wiki.commonjs.org/wiki/Promises/A to determine if - * promiseOrValue is a promise. + * Schedule a task that will process a list of handlers + * in the next queue drain run. + * @private + * @param {Array} handlers queue of handlers to execute + * @param {*} value passed as the only arg to each handler + */ + function scheduleHandlers(handlers, value) { + enqueue(function() { + var handler, i = 0; + while (handler = handlers[i++]) { + handler(value); + } + }); + } + + /** + * Determines if promiseOrValue is a promise or not * * @param {*} promiseOrValue anything * @returns {boolean} true if promiseOrValue is a {@link Promise} @@ -407,8 +427,8 @@ define(function () { * @param {function?} [onRejected] rejection handler * @param {function?} [onProgress] progress handler * @returns {Promise} promise that will resolve to an array of howMany values that - * resolved first, or will reject with an array of (promisesOrValues.length - howMany) + 1 - * rejection reasons. + * resolved first, or will reject with an array of + * (promisesOrValues.length - howMany) + 1 rejection reasons. */ function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) { @@ -416,62 +436,56 @@ define(function () { return when(promisesOrValues, function(promisesOrValues) { - var toResolve, toReject, values, reasons, deferred, fulfillOne, rejectOne, notify, len, i; + return promise(resolveSome).then(onFulfilled, onRejected, onProgress); - len = promisesOrValues.length >>> 0; + function resolveSome(resolve, reject, notify) { + var toResolve, toReject, values, reasons, fulfillOne, rejectOne, len, i; - toResolve = Math.max(0, Math.min(howMany, len)); - values = []; + len = promisesOrValues.length >>> 0; - toReject = (len - toResolve) + 1; - reasons = []; + toResolve = Math.max(0, Math.min(howMany, len)); + values = []; - deferred = defer(); + toReject = (len - toResolve) + 1; + reasons = []; - // No items in the input, resolve immediately - if (!toResolve) { - deferred.resolve(values); + // No items in the input, resolve immediately + if (!toResolve) { + resolve(values); - } else { - notify = deferred.notify; + } else { + rejectOne = function(reason) { + reasons.push(reason); + if(!--toReject) { + fulfillOne = rejectOne = noop; + reject(reasons); + } + }; - rejectOne = function(reason) { - reasons.push(reason); - if(!--toReject) { - fulfillOne = rejectOne = noop; - deferred.reject(reasons); - } - }; + fulfillOne = function(val) { + // This orders the values based on promise resolution order + values.push(val); + if (!--toResolve) { + fulfillOne = rejectOne = noop; + resolve(values); + } + }; - fulfillOne = function(val) { - // This orders the values based on promise resolution order - // Another strategy would be to use the original position of - // the corresponding promise. - values.push(val); - - if (!--toResolve) { - fulfillOne = rejectOne = noop; - deferred.resolve(values); - } - }; - - for(i = 0; i < len; ++i) { - if(i in promisesOrValues) { - when(promisesOrValues[i], fulfiller, rejecter, notify); + for(i = 0; i < len; ++i) { + if(i in promisesOrValues) { + when(promisesOrValues[i], fulfiller, rejecter, notify); + } } } + + function rejecter(reason) { + rejectOne(reason); + } + + function fulfiller(val) { + fulfillOne(val); + } } - - return deferred.promise.then(onFulfilled, onRejected, onProgress); - - function rejecter(reason) { - rejectOne(reason); - } - - function fulfiller(val) { - fulfillOne(val); - } - }); } @@ -529,50 +543,50 @@ define(function () { * input to contain {@link Promise}s and/or values, and mapFunc may return * either a value or a {@link Promise} * - * @param {Array|Promise} promise array of anything, may contain a mix + * @param {Array|Promise} array array of anything, may contain a mix * of {@link Promise}s and values * @param {function} mapFunc mapping function mapFunc(value) which may return * either a {@link Promise} or value * @returns {Promise} a {@link Promise} that will resolve to an array containing * the mapped output values. */ - function map(promise, mapFunc) { - return when(promise, function(array) { - var results, len, toResolve, resolve, i, d; + function map(array, mapFunc) { + return when(array, function(array) { - // Since we know the resulting length, we can preallocate the results - // array to avoid array expansions. - toResolve = len = array.length >>> 0; - results = []; - d = defer(); + return promise(resolveMap); - if(!toResolve) { - d.resolve(results); - } else { + function resolveMap(resolve, reject, notify) { + var results, len, toResolve, resolveOne, i; - resolve = function resolveOne(item, i) { - when(item, mapFunc).then(function(mapped) { - results[i] = mapped; + // Since we know the resulting length, we can preallocate the results + // array to avoid array expansions. + toResolve = len = array.length >>> 0; + results = []; - if(!--toResolve) { - d.resolve(results); + if(!toResolve) { + resolve(results); + } else { + + resolveOne = function(item, i) { + when(item, mapFunc).then(function(mapped) { + results[i] = mapped; + + if(!--toResolve) { + resolve(results); + } + }, reject, notify); + }; + + // Since mapFunc may be async, get all invocations of it into flight + for(i = 0; i < len; i++) { + if(i in array) { + resolveOne(array[i], i); + } else { + --toResolve; } - }, d.reject); - }; - - // Since mapFunc may be async, get all invocations of it into flight - for(i = 0; i < len; i++) { - if(i in array) { - resolve(array[i], i); - } else { - --toResolve; } } - } - - return d.promise; - }); } @@ -590,7 +604,7 @@ define(function () { * @returns {Promise} that will resolve to the final reduced value */ function reduce(promise, reduceFunc /*, initialValue */) { - var args = slice.call(arguments, 1); + var args = fcall(slice, arguments, 1); return when(promise, function(array) { var total; @@ -611,102 +625,94 @@ define(function () { }); } - /** - * Ensure that resolution of promiseOrValue will trigger resolver with the - * value or reason of promiseOrValue, or instead with resolveValue if it is provided. - * - * @param promiseOrValue - * @param {Object} resolver - * @param {function} resolver.resolve - * @param {function} resolver.reject - * @param {*} [resolveValue] - * @returns {Promise} - */ - function chain(promiseOrValue, resolver, resolveValue) { - var useResolveValue = arguments.length > 2; - - return when(promiseOrValue, - function(val) { - val = useResolveValue ? resolveValue : val; - resolver.resolve(val); - return val; - }, - function(reason) { - resolver.reject(reason); - return rejected(reason); - }, - function(update) { - typeof resolver.notify === 'function' && resolver.notify(update); - return update; - } - ); - } - // - // Utility functions + // Utilities, etc. // - /** - * Apply all functions in queue to value - * @param {Array} queue array of functions to execute - * @param {*} value argument passed to each function - */ - function processQueue(queue, value) { - var handler, i = 0; + var reduceArray, slice, fcall, nextTick, handlerQueue, + timeout, funcProto, call, arrayProto, undef; - while (handler = queue[i++]) { - handler(value); + // + // Shared handler queue processing + // + // Credit to Twisol (https://github.com/Twisol) for suggesting + // this type of extensible queue + trampoline approach for + // next-tick conflation. + + handlerQueue = []; + + /** + * Enqueue a task. If the queue is not currently scheduled to be + * drained, schedule it. + * @param {function} task + */ + function enqueue(task) { + if(handlerQueue.push(task) === 1) { + scheduleDrainQueue(); } } /** - * Helper that checks arrayOfCallbacks to ensure that each element is either - * a function, or null or undefined. - * @private - * @param {number} start index at which to start checking items in arrayOfCallbacks - * @param {Array} arrayOfCallbacks array to check - * @throws {Error} if any element of arrayOfCallbacks is something other than - * a functions, null, or undefined. + * Schedule the queue to be drained in the next tick. */ - function checkCallbacks(start, arrayOfCallbacks) { - // TODO: Promises/A+ update type checking and docs - var arg, i = arrayOfCallbacks.length; - - while(i > start) { - arg = arrayOfCallbacks[--i]; - - if (arg != null && typeof arg != 'function') { - throw new Error('arg '+i+' must be a function'); - } - } + function scheduleDrainQueue() { + nextTick(drainQueue); } /** - * No-Op function used in method replacement - * @private + * Drain the handler queue entirely or partially, being careful to allow + * the queue to be extended while it is being processed, and to continue + * processing until it is truly empty. */ - function noop() {} + function drainQueue() { + var task, i = 0; - slice = [].slice; + while(task = handlerQueue[i++]) { + task(); + } + + handlerQueue = []; + } + + // + // Capture function and array utils + // + /*global setImmediate:true*/ + + // capture setTimeout to avoid being caught by fake timers used in time based tests + timeout = setTimeout; + nextTick = typeof setImmediate === 'function' + ? typeof window === 'undefined' + ? setImmediate + : setImmediate.bind(window) + : typeof process === 'object' + ? process.nextTick + : function(task) { timeout(task, 0); }; + + // Safe function calls + funcProto = Function.prototype; + call = funcProto.call; + fcall = funcProto.bind + ? call.bind(call) + : function(f, context) { + return f.apply(context, slice.call(arguments, 2)); + }; + + // Safe array ops + arrayProto = []; + slice = arrayProto.slice; // ES5 reduce implementation if native not available // See: http://es5.github.com/#x15.4.4.21 as there are many - // specifics and edge cases. - reduceArray = [].reduce || + // specifics and edge cases. ES5 dictates that reduce.length === 1 + // This implementation deviates from ES5 spec in the following ways: + // 1. It does not check if reduceFunc is a Callable + reduceArray = arrayProto.reduce || function(reduceFunc /*, initialValue */) { /*jshint maxcomplexity: 7*/ - - // ES5 dictates that reduce.length === 1 - - // This implementation deviates from ES5 spec in the following ways: - // 1. It does not check if reduceFunc is a Callable - var arr, args, reduced, len, i; i = 0; - // This generates a jshint warning, despite being valid - // "Missing 'new' prefix when invoking a constructor." - // See https://github.com/jshint/jshint/issues/392 arr = Object(this); len = arr.length >>> 0; args = arguments; @@ -734,7 +740,6 @@ define(function () { // Do the actual reduce for(;i < len; ++i) { - // Skip holes if(i in arr) { reduced = reduceFunc(reduced, arr[i], i, arr); } @@ -743,17 +748,40 @@ define(function () { return reduced; }; + // + // Utility functions + // + + /** + * Helper that checks arrayOfCallbacks to ensure that each element is either + * a function, or null or undefined. + * @private + * @param {number} start index at which to start checking items in arrayOfCallbacks + * @param {Array} arrayOfCallbacks array to check + * @throws {Error} if any element of arrayOfCallbacks is something other than + * a functions, null, or undefined. + */ + function checkCallbacks(start, arrayOfCallbacks) { + // TODO: Promises/A+ update type checking and docs + var arg, i = arrayOfCallbacks.length; + + while(i > start) { + arg = arrayOfCallbacks[--i]; + + if (arg != null && typeof arg != 'function') { + throw new Error('arg '+i+' must be a function'); + } + } + } + + function noop() {} + function identity(x) { return x; } return when; }); -})(typeof define == 'function' && define.amd - ? define - : function (factory) { typeof exports === 'object' - ? (module.exports = factory()) - : (this.when = factory()); - } - // Boilerplate for AMD, Node, and browser global +})( + typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(); } ); diff --git a/js/lib/when-define-shim.js b/js/lib/when-define-shim.js new file mode 100644 index 00000000..ad135517 --- /dev/null +++ b/js/lib/when-define-shim.js @@ -0,0 +1,11 @@ +if (typeof window !== "undefined") { + window.define = function (factory) { + try { + delete window.define; + } catch (e) { + window.define = void 0; // IE + } + window.when = factory(); + }; + window.define.amd = {}; +} diff --git a/js/package.json b/js/package.json index d3398ca0..f950aee8 100644 --- a/js/package.json +++ b/js/package.json @@ -16,7 +16,7 @@ "dependencies": { "bane": "~0.4.0", "faye-websocket": "~0.4.4", - "when": "~1.8.1" + "when": "~2.0.0" }, "devDependencies": { "buster": "~0.6.12", diff --git a/js/src/mopidy.js b/js/src/mopidy.js index 011aec09..980256b5 100644 --- a/js/src/mopidy.js +++ b/js/src/mopidy.js @@ -242,7 +242,7 @@ Mopidy.prototype._handleEvent = function (eventMessage) { }; Mopidy.prototype._getApiSpec = function () { - this._send({method: "core.describe"}) + return this._send({method: "core.describe"}) .then(this._createApi.bind(this), this._handleWebSocketError) .then(null, this._handleWebSocketError); }; diff --git a/js/test/mopidy-test.js b/js/test/mopidy-test.js index b694fd7e..0bf97f60 100644 --- a/js/test/mopidy-test.js +++ b/js/test/mopidy-test.js @@ -610,16 +610,16 @@ buster.testCase("Mopidy", { assert.calledOnceWith(stub); }, - "gets Api description from server and calls _createApi": function () { + "gets Api description from server and calls _createApi": function (done) { var methods = {}; var sendStub = this.stub(this.mopidy, "_send"); sendStub.returns(when.resolve(methods)); var _createApiStub = this.stub(this.mopidy, "_createApi"); - this.mopidy._getApiSpec(); - - assert.calledOnceWith(sendStub, {method: "core.describe"}); - assert.calledOnceWith(_createApiStub, methods); + this.mopidy._getApiSpec().then(done(function () { + assert.calledOnceWith(sendStub, {method: "core.describe"}); + assert.calledOnceWith(_createApiStub, methods); + })); } }, diff --git a/mopidy/frontends/http/data/mopidy.js b/mopidy/frontends/http/data/mopidy.js index 20e43014..1669eaff 100644 --- a/mopidy/frontends/http/data/mopidy.js +++ b/mopidy/frontends/http/data/mopidy.js @@ -1,4 +1,4 @@ -/*! Mopidy.js - built 2013-03-12 +/*! Mopidy.js - built 2013-03-31 * http://www.mopidy.com/ * Copyright (c) 2013 Stein Magnus Jodal and contributors * Licensed under the Apache License, Version 2.0 */ @@ -166,6 +166,18 @@ return { createEventEmitter: createEventEmitter }; }); +if (typeof window !== "undefined") { + window.define = function (factory) { + try { + delete window.define; + } catch (e) { + window.define = void 0; // IE + } + window.when = factory(); + }; + window.define.amd = {}; +} + /** * A lightweight CommonJS Promises/A and when() implementation * when is part of the cujo.js family of libraries (http://cujojs.com/) @@ -175,34 +187,27 @@ * * @author Brian Cavalier * @author John Hann - * - * @version 1.8.1 + * @version 2.0.0 */ - (function(define) { 'use strict'; define(function () { - var reduceArray, slice, undef; - // // Public API - // - when.defer = defer; // Create a deferred - when.resolve = resolve; // Create a resolved promise - when.reject = reject; // Create a rejected promise + when.defer = defer; // Create a deferred + when.resolve = resolve; // Create a resolved promise + when.reject = reject; // Create a rejected promise - when.join = join; // Join 2 or more promises + when.join = join; // Join 2 or more promises - when.all = all; // Resolve a list of promises - when.map = map; // Array.map() for promises - when.reduce = reduce; // Array.reduce() for promises + when.all = all; // Resolve a list of promises + when.map = map; // Array.map() for promises + when.reduce = reduce; // Array.reduce() for promises - when.any = any; // One-winner race - when.some = some; // Multi-winner race + when.any = any; // One-winner race + when.some = some; // Multi-winner race - when.chain = chain; // Make a promise trigger another resolver - - when.isPromise = isPromise; // Determine if a thing is a promise + when.isPromise = isPromise; // Determine if a thing is a promise /** * Register an observer for a promise or immediate value. @@ -225,77 +230,6 @@ define(function () { return resolve(promiseOrValue).then(onFulfilled, onRejected, onProgress); } - /** - * Returns promiseOrValue if promiseOrValue is a {@link Promise}, a new Promise if - * promiseOrValue is a foreign promise, or a new, already-fulfilled {@link Promise} - * whose value is promiseOrValue if promiseOrValue is an immediate value. - * - * @param {*} promiseOrValue - * @returns {Promise} Guaranteed to return a trusted Promise. If promiseOrValue - * is trusted, returns promiseOrValue, otherwise, returns a new, already-resolved - * when.js promise whose resolution value is: - * * the resolution value of promiseOrValue if it's a foreign promise, or - * * promiseOrValue if it's a value - */ - function resolve(promiseOrValue) { - var promise; - - if(promiseOrValue instanceof Promise) { - // It's a when.js promise, so we trust it - promise = promiseOrValue; - - } else if(isPromise(promiseOrValue)) { - // Assimilate foreign promises - promise = assimilate(promiseOrValue); - } else { - // It's a value, create a fulfilled promise for it. - promise = fulfilled(promiseOrValue); - } - - return promise; - } - - /** - * Assimilate an untrusted thenable by introducing a trusted middle man. - * Not a perfect strategy, but possibly the best we can do. - * IMPORTANT: This is the only place when.js should ever call an untrusted - * thenable's then() on an. Don't expose the return value to the untrusted thenable - * - * @param {*} thenable - * @param {function} thenable.then - * @returns {Promise} - */ - function assimilate(thenable) { - var d = defer(); - - // TODO: Enqueue this for future execution in 2.0 - try { - thenable.then( - function(value) { d.resolve(value); }, - function(reason) { d.reject(reason); }, - function(update) { d.progress(update); } - ); - } catch(e) { - d.reject(e); - } - - return d.promise; - } - - /** - * Returns a rejected promise for the supplied promiseOrValue. The returned - * promise will be rejected with: - * - promiseOrValue, if it is a value, or - * - if promiseOrValue is a promise - * - promiseOrValue's value after it is fulfilled - * - promiseOrValue's reason after it is rejected - * @param {*} promiseOrValue the rejected value of the returned {@link Promise} - * @return {Promise} rejected {@link Promise} - */ - function reject(promiseOrValue) { - return when(promiseOrValue, rejected); - } - /** * Trusted Promise constructor. A Promise created from this constructor is * a trusted when.js promise. Any other duck-typed promise is considered @@ -308,18 +242,6 @@ define(function () { } Promise.prototype = { - /** - * Register a callback that will be called when a promise is - * fulfilled or rejected. Optionally also register a progress handler. - * Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress) - * @param {function?} [onFulfilledOrRejected] - * @param {function?} [onProgress] - * @return {Promise} - */ - always: function(onFulfilledOrRejected, onProgress) { - return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress); - }, - /** * Register a rejection handler. Shortcut for .then(undefined, onRejected) * @param {function?} onRejected @@ -329,6 +251,26 @@ define(function () { return this.then(undef, onRejected); }, + /** + * Ensures that onFulfilledOrRejected will be called regardless of whether + * this promise is fulfilled or rejected. onFulfilledOrRejected WILL NOT + * receive the promises' value or reason. Any returned value will be disregarded. + * onFulfilledOrRejected may throw or return a rejected promise to signal + * an additional error. + * @param {function} onFulfilledOrRejected handler to be called regardless of + * fulfillment or rejection + * @returns {Promise} + */ + ensure: function(onFulfilledOrRejected) { + var self = this; + + return this.then(injectHandler, injectHandler).yield(self); + + function injectHandler() { + return resolve(onFulfilledOrRejected()); + } + }, + /** * Shortcut for .then(function() { return value; }) * @param {*} value @@ -357,201 +299,291 @@ define(function () { return onFulfilled.apply(undef, array); }); }); + }, + + /** + * Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected) + * @deprecated + */ + always: function(onFulfilledOrRejected, onProgress) { + return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress); } }; /** - * Create an already-resolved promise for the supplied value - * @private + * Returns a resolved promise. The returned promise will be + * - fulfilled with promiseOrValue if it is a value, or + * - if promiseOrValue is a promise + * - fulfilled with promiseOrValue's value after it is fulfilled + * - rejected with promiseOrValue's reason after it is rejected + * @param {*} value + * @return {Promise} + */ + function resolve(value) { + return promise(function(resolve) { + resolve(value); + }); + } + + /** + * Returns a rejected promise for the supplied promiseOrValue. The returned + * promise will be rejected with: + * - promiseOrValue, if it is a value, or + * - if promiseOrValue is a promise + * - promiseOrValue's value after it is fulfilled + * - promiseOrValue's reason after it is rejected + * @param {*} promiseOrValue the rejected value of the returned {@link Promise} + * @return {Promise} rejected {@link Promise} + */ + function reject(promiseOrValue) { + return when(promiseOrValue, rejected); + } + + /** + * Creates a new Deferred with fully isolated resolver and promise parts, + * either or both of which may be given out safely to consumers. + * The resolver has resolve, reject, and progress. The promise + * only has then. * + * @return {{ + * promise: Promise, + * resolver: { + * resolve: function:Promise, + * reject: function:Promise, + * notify: function:Promise + * }}} + */ + function defer() { + var deferred, pending, resolved; + + // Optimize object shape + deferred = { + promise: undef, resolve: undef, reject: undef, notify: undef, + resolver: { resolve: undef, reject: undef, notify: undef } + }; + + deferred.promise = pending = promise(makeDeferred); + + return deferred; + + function makeDeferred(resolvePending, rejectPending, notifyPending) { + deferred.resolve = deferred.resolver.resolve = function(value) { + if(resolved) { + return resolve(value); + } + resolved = true; + resolvePending(value); + return pending; + }; + + deferred.reject = deferred.resolver.reject = function(reason) { + if(resolved) { + return resolve(rejected(reason)); + } + resolved = true; + rejectPending(reason); + return pending; + }; + + deferred.notify = deferred.resolver.notify = function(update) { + notifyPending(update); + return update; + }; + } + } + + /** + * Creates a new promise whose fate is determined by resolver. + * @private (for now) + * @param {function} resolver function(resolve, reject, notify) + * @returns {Promise} promise whose fate is determine by resolver + */ + function promise(resolver) { + var value, handlers = []; + + // Call the provider resolver to seal the promise's fate + try { + resolver(promiseResolve, promiseReject, promiseNotify); + } catch(e) { + promiseReject(e); + } + + // Return the promise + return new Promise(then); + + /** + * Register handlers for this promise. + * @param [onFulfilled] {Function} fulfillment handler + * @param [onRejected] {Function} rejection handler + * @param [onProgress] {Function} progress handler + * @return {Promise} new Promise + */ + function then(onFulfilled, onRejected, onProgress) { + return promise(function(resolve, reject, notify) { + handlers + // Call handlers later, after resolution + ? handlers.push(function(value) { + value.then(onFulfilled, onRejected, onProgress) + .then(resolve, reject, notify); + }) + // Call handlers soon, but not in the current stack + : enqueue(function() { + value.then(onFulfilled, onRejected, onProgress) + .then(resolve, reject, notify); + }); + }); + } + + /** + * Transition from pre-resolution state to post-resolution state, notifying + * all listeners of the ultimate fulfillment or rejection + * @param {*|Promise} val resolution value + */ + function promiseResolve(val) { + if(!handlers) { + return; + } + + value = coerce(val); + scheduleHandlers(handlers, value); + + handlers = undef; + } + + /** + * Reject this promise with the supplied reason, which will be used verbatim. + * @param {*} reason reason for the rejection + */ + function promiseReject(reason) { + promiseResolve(rejected(reason)); + } + + /** + * Issue a progress event, notifying all progress listeners + * @param {*} update progress event payload to pass to all listeners + */ + function promiseNotify(update) { + if(handlers) { + scheduleHandlers(handlers, progressing(update)); + } + } + } + + /** + * Coerces x to a trusted Promise + * + * @private + * @param {*} x thing to coerce + * @returns {Promise} Guaranteed to return a trusted Promise. If x + * is trusted, returns x, otherwise, returns a new, trusted, already-resolved + * Promise whose resolution value is: + * * the resolution value of x if it's a foreign promise, or + * * x if it's a value + */ + function coerce(x) { + if(x instanceof Promise) { + return x; + } else if (x !== Object(x)) { + return fulfilled(x); + } + + return promise(function(resolve, reject, notify) { + enqueue(function() { + try { + // We must check and assimilate in the same tick, but not the + // current tick, careful only to access promiseOrValue.then once. + var untrustedThen = x.then; + + if(typeof untrustedThen === 'function') { + fcall(untrustedThen, x, resolve, reject, notify); + } else { + // It's a value, create a fulfilled wrapper + resolve(fulfilled(x)); + } + + } catch(e) { + // Something went wrong, reject + reject(e); + } + }); + }); + } + + /** + * Create an already-fulfilled promise for the supplied value + * @private * @param {*} value * @return {Promise} fulfilled promise */ function fulfilled(value) { - var p = new Promise(function(onFulfilled) { + var self = new Promise(function (onFulfilled) { try { - return resolve(typeof onFulfilled == 'function' ? onFulfilled(value) : value); - } catch(e) { + return typeof onFulfilled == 'function' + ? coerce(onFulfilled(value)) : self; + } catch (e) { return rejected(e); } }); - return p; + return self; } /** - * Create an already-rejected {@link Promise} with the supplied - * rejection reason. + * Create an already-rejected promise with the supplied rejection reason. * @private - * * @param {*} reason * @return {Promise} rejected promise */ function rejected(reason) { - var p = new Promise(function(_, onRejected) { + var self = new Promise(function (_, onRejected) { try { - return resolve(typeof onRejected == 'function' ? onRejected(reason) : rejected(reason)); - } catch(e) { + return typeof onRejected == 'function' + ? coerce(onRejected(reason)) : self; + } catch (e) { return rejected(e); } }); - return p; + return self; } /** - * Creates a new, Deferred with fully isolated resolver and promise parts, - * either or both of which may be given out safely to consumers. - * The Deferred itself has the full API: resolve, reject, progress, and - * then. The resolver has resolve, reject, and progress. The promise - * only has then. - * - * @return {Deferred} + * Create a progress promise with the supplied update. + * @private + * @param {*} update + * @return {Promise} progress promise */ - function defer() { - var deferred, promise, handlers, progressHandlers, - _then, _notify, _resolve; - - /** - * The promise for the new deferred - * @type {Promise} - */ - promise = new Promise(then); - - /** - * The full Deferred object, with {@link Promise} and {@link Resolver} parts - * @class Deferred - * @name Deferred - */ - deferred = { - then: then, // DEPRECATED: use deferred.promise.then - resolve: promiseResolve, - reject: promiseReject, - progress: promiseNotify, // DEPRECATED: use deferred.notify - notify: promiseNotify, - - promise: promise, - - resolver: { - resolve: promiseResolve, - reject: promiseReject, - progress: promiseNotify, // DEPRECATED: use deferred.notify - notify: promiseNotify + function progressing(update) { + var self = new Promise(function (_, __, onProgress) { + try { + return typeof onProgress == 'function' + ? progressing(onProgress(update)) : self; + } catch (e) { + return progressing(e); } - }; + }); - handlers = []; - progressHandlers = []; - - /** - * Pre-resolution then() that adds the supplied callback, errback, and progback - * functions to the registered listeners - * @private - * - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler - */ - _then = function(onFulfilled, onRejected, onProgress) { - var deferred, progressHandler; - - deferred = defer(); - - progressHandler = typeof onProgress === 'function' - ? function(update) { - try { - // Allow progress handler to transform progress event - deferred.notify(onProgress(update)); - } catch(e) { - // Use caught value as progress - deferred.notify(e); - } - } - : function(update) { deferred.notify(update); }; - - handlers.push(function(promise) { - promise.then(onFulfilled, onRejected) - .then(deferred.resolve, deferred.reject, progressHandler); - }); - - progressHandlers.push(progressHandler); - - return deferred.promise; - }; - - /** - * Issue a progress event, notifying all progress listeners - * @private - * @param {*} update progress event payload to pass to all listeners - */ - _notify = function(update) { - processQueue(progressHandlers, update); - return update; - }; - - /** - * Transition from pre-resolution state to post-resolution state, notifying - * all listeners of the resolution or rejection - * @private - * @param {*} value the value of this deferred - */ - _resolve = function(value) { - // Replace _then with one that directly notifies with the result. - _then = value.then; - // Replace _resolve so that this Deferred can only be resolved once - _resolve = resolve; - // Make _progress a noop, to disallow progress for the resolved promise. - _notify = identity; - - // Notify handlers - processQueue(handlers, value); - - // Free progressHandlers array since we'll never issue progress events - progressHandlers = handlers = undef; - - return value; - }; - - return deferred; - - /** - * Wrapper to allow _then to be replaced safely - * @param {function?} [onFulfilled] resolution handler - * @param {function?} [onRejected] rejection handler - * @param {function?} [onProgress] progress handler - * @return {Promise} new promise - */ - function then(onFulfilled, onRejected, onProgress) { - // TODO: Promises/A+ check typeof onFulfilled, onRejected, onProgress - return _then(onFulfilled, onRejected, onProgress); - } - - /** - * Wrapper to allow _resolve to be replaced - */ - function promiseResolve(val) { - return _resolve(resolve(val)); - } - - /** - * Wrapper to allow _reject to be replaced - */ - function promiseReject(err) { - return _resolve(rejected(err)); - } - - /** - * Wrapper to allow _notify to be replaced - */ - function promiseNotify(update) { - return _notify(update); - } + return self; } /** - * Determines if promiseOrValue is a promise or not. Uses the feature - * test from http://wiki.commonjs.org/wiki/Promises/A to determine if - * promiseOrValue is a promise. + * Schedule a task that will process a list of handlers + * in the next queue drain run. + * @private + * @param {Array} handlers queue of handlers to execute + * @param {*} value passed as the only arg to each handler + */ + function scheduleHandlers(handlers, value) { + enqueue(function() { + var handler, i = 0; + while (handler = handlers[i++]) { + handler(value); + } + }); + } + + /** + * Determines if promiseOrValue is a promise or not * * @param {*} promiseOrValue anything * @returns {boolean} true if promiseOrValue is a {@link Promise} @@ -573,8 +605,8 @@ define(function () { * @param {function?} [onRejected] rejection handler * @param {function?} [onProgress] progress handler * @returns {Promise} promise that will resolve to an array of howMany values that - * resolved first, or will reject with an array of (promisesOrValues.length - howMany) + 1 - * rejection reasons. + * resolved first, or will reject with an array of + * (promisesOrValues.length - howMany) + 1 rejection reasons. */ function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) { @@ -582,62 +614,56 @@ define(function () { return when(promisesOrValues, function(promisesOrValues) { - var toResolve, toReject, values, reasons, deferred, fulfillOne, rejectOne, notify, len, i; + return promise(resolveSome).then(onFulfilled, onRejected, onProgress); - len = promisesOrValues.length >>> 0; + function resolveSome(resolve, reject, notify) { + var toResolve, toReject, values, reasons, fulfillOne, rejectOne, len, i; - toResolve = Math.max(0, Math.min(howMany, len)); - values = []; + len = promisesOrValues.length >>> 0; - toReject = (len - toResolve) + 1; - reasons = []; + toResolve = Math.max(0, Math.min(howMany, len)); + values = []; - deferred = defer(); + toReject = (len - toResolve) + 1; + reasons = []; - // No items in the input, resolve immediately - if (!toResolve) { - deferred.resolve(values); + // No items in the input, resolve immediately + if (!toResolve) { + resolve(values); - } else { - notify = deferred.notify; + } else { + rejectOne = function(reason) { + reasons.push(reason); + if(!--toReject) { + fulfillOne = rejectOne = noop; + reject(reasons); + } + }; - rejectOne = function(reason) { - reasons.push(reason); - if(!--toReject) { - fulfillOne = rejectOne = noop; - deferred.reject(reasons); - } - }; + fulfillOne = function(val) { + // This orders the values based on promise resolution order + values.push(val); + if (!--toResolve) { + fulfillOne = rejectOne = noop; + resolve(values); + } + }; - fulfillOne = function(val) { - // This orders the values based on promise resolution order - // Another strategy would be to use the original position of - // the corresponding promise. - values.push(val); - - if (!--toResolve) { - fulfillOne = rejectOne = noop; - deferred.resolve(values); - } - }; - - for(i = 0; i < len; ++i) { - if(i in promisesOrValues) { - when(promisesOrValues[i], fulfiller, rejecter, notify); + for(i = 0; i < len; ++i) { + if(i in promisesOrValues) { + when(promisesOrValues[i], fulfiller, rejecter, notify); + } } } + + function rejecter(reason) { + rejectOne(reason); + } + + function fulfiller(val) { + fulfillOne(val); + } } - - return deferred.promise.then(onFulfilled, onRejected, onProgress); - - function rejecter(reason) { - rejectOne(reason); - } - - function fulfiller(val) { - fulfillOne(val); - } - }); } @@ -695,50 +721,50 @@ define(function () { * input to contain {@link Promise}s and/or values, and mapFunc may return * either a value or a {@link Promise} * - * @param {Array|Promise} promise array of anything, may contain a mix + * @param {Array|Promise} array array of anything, may contain a mix * of {@link Promise}s and values * @param {function} mapFunc mapping function mapFunc(value) which may return * either a {@link Promise} or value * @returns {Promise} a {@link Promise} that will resolve to an array containing * the mapped output values. */ - function map(promise, mapFunc) { - return when(promise, function(array) { - var results, len, toResolve, resolve, i, d; + function map(array, mapFunc) { + return when(array, function(array) { - // Since we know the resulting length, we can preallocate the results - // array to avoid array expansions. - toResolve = len = array.length >>> 0; - results = []; - d = defer(); + return promise(resolveMap); - if(!toResolve) { - d.resolve(results); - } else { + function resolveMap(resolve, reject, notify) { + var results, len, toResolve, resolveOne, i; - resolve = function resolveOne(item, i) { - when(item, mapFunc).then(function(mapped) { - results[i] = mapped; + // Since we know the resulting length, we can preallocate the results + // array to avoid array expansions. + toResolve = len = array.length >>> 0; + results = []; - if(!--toResolve) { - d.resolve(results); + if(!toResolve) { + resolve(results); + } else { + + resolveOne = function(item, i) { + when(item, mapFunc).then(function(mapped) { + results[i] = mapped; + + if(!--toResolve) { + resolve(results); + } + }, reject, notify); + }; + + // Since mapFunc may be async, get all invocations of it into flight + for(i = 0; i < len; i++) { + if(i in array) { + resolveOne(array[i], i); + } else { + --toResolve; } - }, d.reject); - }; - - // Since mapFunc may be async, get all invocations of it into flight - for(i = 0; i < len; i++) { - if(i in array) { - resolve(array[i], i); - } else { - --toResolve; } } - } - - return d.promise; - }); } @@ -756,7 +782,7 @@ define(function () { * @returns {Promise} that will resolve to the final reduced value */ function reduce(promise, reduceFunc /*, initialValue */) { - var args = slice.call(arguments, 1); + var args = fcall(slice, arguments, 1); return when(promise, function(array) { var total; @@ -777,102 +803,94 @@ define(function () { }); } - /** - * Ensure that resolution of promiseOrValue will trigger resolver with the - * value or reason of promiseOrValue, or instead with resolveValue if it is provided. - * - * @param promiseOrValue - * @param {Object} resolver - * @param {function} resolver.resolve - * @param {function} resolver.reject - * @param {*} [resolveValue] - * @returns {Promise} - */ - function chain(promiseOrValue, resolver, resolveValue) { - var useResolveValue = arguments.length > 2; - - return when(promiseOrValue, - function(val) { - val = useResolveValue ? resolveValue : val; - resolver.resolve(val); - return val; - }, - function(reason) { - resolver.reject(reason); - return rejected(reason); - }, - function(update) { - typeof resolver.notify === 'function' && resolver.notify(update); - return update; - } - ); - } - // - // Utility functions + // Utilities, etc. // - /** - * Apply all functions in queue to value - * @param {Array} queue array of functions to execute - * @param {*} value argument passed to each function - */ - function processQueue(queue, value) { - var handler, i = 0; + var reduceArray, slice, fcall, nextTick, handlerQueue, + timeout, funcProto, call, arrayProto, undef; - while (handler = queue[i++]) { - handler(value); + // + // Shared handler queue processing + // + // Credit to Twisol (https://github.com/Twisol) for suggesting + // this type of extensible queue + trampoline approach for + // next-tick conflation. + + handlerQueue = []; + + /** + * Enqueue a task. If the queue is not currently scheduled to be + * drained, schedule it. + * @param {function} task + */ + function enqueue(task) { + if(handlerQueue.push(task) === 1) { + scheduleDrainQueue(); } } /** - * Helper that checks arrayOfCallbacks to ensure that each element is either - * a function, or null or undefined. - * @private - * @param {number} start index at which to start checking items in arrayOfCallbacks - * @param {Array} arrayOfCallbacks array to check - * @throws {Error} if any element of arrayOfCallbacks is something other than - * a functions, null, or undefined. + * Schedule the queue to be drained in the next tick. */ - function checkCallbacks(start, arrayOfCallbacks) { - // TODO: Promises/A+ update type checking and docs - var arg, i = arrayOfCallbacks.length; - - while(i > start) { - arg = arrayOfCallbacks[--i]; - - if (arg != null && typeof arg != 'function') { - throw new Error('arg '+i+' must be a function'); - } - } + function scheduleDrainQueue() { + nextTick(drainQueue); } /** - * No-Op function used in method replacement - * @private + * Drain the handler queue entirely or partially, being careful to allow + * the queue to be extended while it is being processed, and to continue + * processing until it is truly empty. */ - function noop() {} + function drainQueue() { + var task, i = 0; - slice = [].slice; + while(task = handlerQueue[i++]) { + task(); + } + + handlerQueue = []; + } + + // + // Capture function and array utils + // + /*global setImmediate:true*/ + + // capture setTimeout to avoid being caught by fake timers used in time based tests + timeout = setTimeout; + nextTick = typeof setImmediate === 'function' + ? typeof window === 'undefined' + ? setImmediate + : setImmediate.bind(window) + : typeof process === 'object' + ? process.nextTick + : function(task) { timeout(task, 0); }; + + // Safe function calls + funcProto = Function.prototype; + call = funcProto.call; + fcall = funcProto.bind + ? call.bind(call) + : function(f, context) { + return f.apply(context, slice.call(arguments, 2)); + }; + + // Safe array ops + arrayProto = []; + slice = arrayProto.slice; // ES5 reduce implementation if native not available // See: http://es5.github.com/#x15.4.4.21 as there are many - // specifics and edge cases. - reduceArray = [].reduce || + // specifics and edge cases. ES5 dictates that reduce.length === 1 + // This implementation deviates from ES5 spec in the following ways: + // 1. It does not check if reduceFunc is a Callable + reduceArray = arrayProto.reduce || function(reduceFunc /*, initialValue */) { /*jshint maxcomplexity: 7*/ - - // ES5 dictates that reduce.length === 1 - - // This implementation deviates from ES5 spec in the following ways: - // 1. It does not check if reduceFunc is a Callable - var arr, args, reduced, len, i; i = 0; - // This generates a jshint warning, despite being valid - // "Missing 'new' prefix when invoking a constructor." - // See https://github.com/jshint/jshint/issues/392 arr = Object(this); len = arr.length >>> 0; args = arguments; @@ -900,7 +918,6 @@ define(function () { // Do the actual reduce for(;i < len; ++i) { - // Skip holes if(i in arr) { reduced = reduceFunc(reduced, arr[i], i, arr); } @@ -909,21 +926,50 @@ define(function () { return reduced; }; + // + // Utility functions + // + + /** + * Helper that checks arrayOfCallbacks to ensure that each element is either + * a function, or null or undefined. + * @private + * @param {number} start index at which to start checking items in arrayOfCallbacks + * @param {Array} arrayOfCallbacks array to check + * @throws {Error} if any element of arrayOfCallbacks is something other than + * a functions, null, or undefined. + */ + function checkCallbacks(start, arrayOfCallbacks) { + // TODO: Promises/A+ update type checking and docs + var arg, i = arrayOfCallbacks.length; + + while(i > start) { + arg = arrayOfCallbacks[--i]; + + if (arg != null && typeof arg != 'function') { + throw new Error('arg '+i+' must be a function'); + } + } + } + + function noop() {} + function identity(x) { return x; } return when; }); -})(typeof define == 'function' && define.amd - ? define - : function (factory) { typeof exports === 'object' - ? (module.exports = factory()) - : (this.when = factory()); - } - // Boilerplate for AMD, Node, and browser global +})( + typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(); } ); +if (typeof module === "object" && typeof require === "function") { + var bane = require("bane"); + var websocket = require("faye-websocket"); + var when = require("when"); +} + function Mopidy(settings) { if (!(this instanceof Mopidy)) { return new Mopidy(settings); @@ -944,9 +990,17 @@ function Mopidy(settings) { } } +if (typeof module === "object" && typeof require === "function") { + Mopidy.WebSocket = websocket.Client; +} else { + Mopidy.WebSocket = window.WebSocket; +} + Mopidy.prototype._configure = function (settings) { + var currentHost = (typeof document !== "undefined" && + document.location.host) || "localhost"; settings.webSocketUrl = settings.webSocketUrl || - "ws://" + document.location.host + "/mopidy/ws/"; + "ws://" + currentHost + "/mopidy/ws/"; if (settings.autoConnect !== false) { settings.autoConnect = true; @@ -959,7 +1013,7 @@ Mopidy.prototype._configure = function (settings) { }; Mopidy.prototype._getConsole = function () { - var console = window.console || {}; + var console = typeof console !== "undefined" && console || {}; console.log = console.log || function () {}; console.warn = console.warn || function () {}; @@ -987,7 +1041,7 @@ Mopidy.prototype._delegateEvents = function () { Mopidy.prototype.connect = function () { if (this._webSocket) { - if (this._webSocket.readyState === WebSocket.OPEN) { + if (this._webSocket.readyState === Mopidy.WebSocket.OPEN) { return; } else { this._webSocket.close(); @@ -995,7 +1049,7 @@ Mopidy.prototype.connect = function () { } this._webSocket = this._settings.webSocket || - new WebSocket(this._settings.webSocketUrl); + new Mopidy.WebSocket(this._settings.webSocketUrl); this._webSocket.onclose = function (close) { this.emit("websocket:close", close); @@ -1060,17 +1114,17 @@ Mopidy.prototype._send = function (message) { var deferred = when.defer(); switch (this._webSocket.readyState) { - case WebSocket.CONNECTING: + case Mopidy.WebSocket.CONNECTING: deferred.resolver.reject({ message: "WebSocket is still connecting" }); break; - case WebSocket.CLOSING: + case Mopidy.WebSocket.CLOSING: deferred.resolver.reject({ message: "WebSocket is closing" }); break; - case WebSocket.CLOSED: + case Mopidy.WebSocket.CLOSED: deferred.resolver.reject({ message: "WebSocket is closed" }); @@ -1152,7 +1206,7 @@ Mopidy.prototype._handleEvent = function (eventMessage) { }; Mopidy.prototype._getApiSpec = function () { - this._send({method: "core.describe"}) + return this._send({method: "core.describe"}) .then(this._createApi.bind(this), this._handleWebSocketError) .then(null, this._handleWebSocketError); }; @@ -1204,3 +1258,7 @@ Mopidy.prototype._snakeToCamel = function (name) { return match.toUpperCase().replace("_", ""); }); }; + +if (typeof exports === "object") { + exports.Mopidy = Mopidy; +} diff --git a/mopidy/frontends/http/data/mopidy.min.js b/mopidy/frontends/http/data/mopidy.min.js index fd8a0baf..08ee1dac 100644 --- a/mopidy/frontends/http/data/mopidy.min.js +++ b/mopidy/frontends/http/data/mopidy.min.js @@ -1,5 +1,5 @@ -/*! Mopidy.js - built 2013-03-12 +/*! Mopidy.js - built 2013-03-31 * http://www.mopidy.com/ * Copyright (c) 2013 Stein Magnus Jodal and contributors * Licensed under the Apache License, Version 2.0 */ -function Mopidy(e){return this instanceof Mopidy?(this._settings=this._configure(e||{}),this._console=this._getConsole(),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,bane.createEventEmitter(this),this._delegateEvents(),this._settings.autoConnect&&this.connect(),void 0):new Mopidy(e)}("function"==typeof define&&define.amd&&function(e){define(e)}||"object"==typeof module&&function(e){module.exports=e()}||function(e){this.bane=e()})(function(){"use strict";function e(e,t,n){var o,r=n.length;if(r>0)for(o=0;r>o;++o)n[o](e,t);else setTimeout(function(){throw t.message=e+" listener threw error: "+t.message,t},0)}function t(e){if("function"!=typeof e)throw new TypeError("Listener is not function");return e}function n(e){return e.supervisors||(e.supervisors=[]),e.supervisors}function o(e,t){return e.listeners||(e.listeners={}),t&&!e.listeners[t]&&(e.listeners[t]=[]),t?e.listeners[t]:e.listeners}function r(e){return e.errbacks||(e.errbacks=[]),e.errbacks}function i(i){function c(t,n,o){try{n.listener.apply(n.thisp||i,o)}catch(s){e(t,s,r(i))}}return i=i||{},i.on=function(e,r,i){return"function"==typeof e?n(this).push({listener:e,thisp:r}):(o(this,e).push({listener:t(r),thisp:i}),void 0)},i.off=function(e,t){var i,s,c,f;if(!e){i=n(this),i.splice(0,i.length),s=o(this);for(c in s)s.hasOwnProperty(c)&&(i=o(this,c),i.splice(0,i.length));return i=r(this),i.splice(0,i.length),void 0}if("function"==typeof e?(i=n(this),t=e):i=o(this,e),!t)return i.splice(0,i.length),void 0;for(c=0,f=i.length;f>c;++c)if(i[c].listener===t)return i.splice(c,1),void 0},i.once=function(e,t,n){var o=function(){i.off(e,o),t.apply(this,arguments)};i.on(e,o,n)},i.bind=function(e,t){var n,o,r;if(t)for(o=0,r=t.length;r>o;++o){if("function"!=typeof e[t[o]])throw Error("No such method "+t[o]);this.on(t[o],e[t[o]],e)}else for(n in e)"function"==typeof e[n]&&this.on(n,e[n],e);return e},i.emit=function(e){var t,r,i=n(this),f=s.call(arguments);for(t=0,r=i.length;r>t;++t)c(e,i[t],f);for(i=o(this,e).slice(),f=s.call(arguments,1),t=0,r=i.length;r>t;++t)c(e,i[t],f)},i.errback=function(e){this.errbacks||(this.errbacks=[]),this.errbacks.push(t(e))},i}var s=Array.prototype.slice;return{createEventEmitter:i}}),function(e){"use strict";e(function(){function e(e,n,o,r){return t(e).then(n,o,r)}function t(e){var t;return t=e instanceof r?e:f(e)?n(e):i(e)}function n(e){var t=c();try{e.then(function(e){t.resolve(e)},function(e){t.reject(e)},function(e){t.progress(e)})}catch(n){t.reject(n)}return t.promise}function o(t){return e(t,s)}function r(e){this.then=e}function i(e){var n=new r(function(n){try{return t("function"==typeof n?n(e):e)}catch(o){return s(o)}});return n}function s(e){var n=new r(function(n,o){try{return t("function"==typeof o?o(e):s(e))}catch(r){return s(r)}});return n}function c(){function e(e,t,n){return l(e,t,n)}function n(e){return d(t(e))}function o(e){return d(s(e))}function i(e){return p(e)}var f,u,a,h,l,p,d;return u=new r(e),f={then:e,resolve:n,reject:o,progress:i,notify:i,promise:u,resolver:{resolve:n,reject:o,progress:i,notify:i}},a=[],h=[],l=function(e,t,n){var o,r;return o=c(),r="function"==typeof n?function(e){try{o.notify(n(e))}catch(t){o.notify(t)}}:function(e){o.notify(e)},a.push(function(n){n.then(e,t).then(o.resolve,o.reject,r)}),h.push(r),o.promise},p=function(e){return b(h,e),e},d=function(e){return l=e.then,d=t,p=_,b(a,e),h=a=m,e},f}function f(e){return e&&"function"==typeof e.then}function u(t,n,o,r,i){return g(2,arguments),e(t,function(t){function s(e){y(e)}function f(e){d(e)}var u,a,h,l,p,d,y,b,g,_;if(g=t.length>>>0,u=Math.max(0,Math.min(n,g)),h=[],a=g-u+1,l=[],p=c(),u)for(b=p.notify,y=function(e){l.push(e),--a||(d=y=k,p.reject(l))},d=function(e){h.push(e),--u||(d=y=k,p.resolve(h))},_=0;g>_;++_)_ in t&&e(t[_],f,s,b);else p.resolve(h);return p.promise.then(o,r,i)})}function a(e,t,n,o){function r(e){return t?t(e[0]):e[0]}return u(e,1,r,n,o)}function h(e,t,n,o){return g(1,arguments),p(e,_).then(t,n,o)}function l(){return p(arguments,_)}function p(t,n){return e(t,function(t){var o,r,i,s,f,u;if(i=r=t.length>>>0,o=[],u=c(),i)for(s=function(t,r){e(t,n).then(function(e){o[r]=e,--i||u.resolve(o)},u.reject)},f=0;r>f;f++)f in t?s(t[f],f):--i;else u.resolve(o);return u.promise})}function d(t,n){var o=w.call(arguments,1);return e(t,function(t){var r;return r=t.length,o[0]=function(t,o,i){return e(t,function(t){return e(o,function(e){return n(t,e,i,r)})})},v.apply(t,o)})}function y(t,n,o){var r=arguments.length>2;return e(t,function(e){return e=r?o:e,n.resolve(e),e},function(e){return n.reject(e),s(e)},function(e){return"function"==typeof n.notify&&n.notify(e),e})}function b(e,t){for(var n,o=0;n=e[o++];)n(t)}function g(e,t){for(var n,o=t.length;o>e;)if(n=t[--o],null!=n&&"function"!=typeof n)throw Error("arg "+o+" must be a function")}function k(){}function _(e){return e}var v,w,m;return e.defer=c,e.resolve=t,e.reject=o,e.join=l,e.all=h,e.map=p,e.reduce=d,e.any=a,e.some=u,e.chain=y,e.isPromise=f,r.prototype={always:function(e,t){return this.then(e,e,t)},otherwise:function(e){return this.then(m,e)},yield:function(e){return this.then(function(){return e})},spread:function(e){return this.then(function(t){return h(t,function(t){return e.apply(m,t)})})}},w=[].slice,v=[].reduce||function(e){var t,n,o,r,i;if(i=0,t=Object(this),r=t.length>>>0,n=arguments,1>=n.length)for(;;){if(i in t){o=t[i++];break}if(++i>=r)throw new TypeError}else o=n[1];for(;r>i;++i)i in t&&(o=e(o,t[i],i,t));return o},e})}("function"==typeof define&&define.amd?define:function(e){"object"==typeof exports?module.exports=e():this.when=e()}),Mopidy.prototype._configure=function(e){return e.webSocketUrl=e.webSocketUrl||"ws://"+document.location.host+"/mopidy/ws/",e.autoConnect!==!1&&(e.autoConnect=!0),e.backoffDelayMin=e.backoffDelayMin||1e3,e.backoffDelayMax=e.backoffDelayMax||64e3,e},Mopidy.prototype._getConsole=function(){var e=window.console||{};return e.log=e.log||function(){},e.warn=e.warn||function(){},e.error=e.error||function(){},e},Mopidy.prototype._delegateEvents=function(){this.off("websocket:close"),this.off("websocket:error"),this.off("websocket:incomingMessage"),this.off("websocket:open"),this.off("state:offline"),this.on("websocket:close",this._cleanup),this.on("websocket:error",this._handleWebSocketError),this.on("websocket:incomingMessage",this._handleMessage),this.on("websocket:open",this._resetBackoffDelay),this.on("websocket:open",this._getApiSpec),this.on("state:offline",this._reconnect)},Mopidy.prototype.connect=function(){if(this._webSocket){if(this._webSocket.readyState===WebSocket.OPEN)return;this._webSocket.close()}this._webSocket=this._settings.webSocket||new WebSocket(this._settings.webSocketUrl),this._webSocket.onclose=function(e){this.emit("websocket:close",e)}.bind(this),this._webSocket.onerror=function(e){this.emit("websocket:error",e)}.bind(this),this._webSocket.onopen=function(){this.emit("websocket:open")}.bind(this),this._webSocket.onmessage=function(e){this.emit("websocket:incomingMessage",e)}.bind(this)},Mopidy.prototype._cleanup=function(e){Object.keys(this._pendingRequests).forEach(function(t){var n=this._pendingRequests[t];delete this._pendingRequests[t],n.reject({message:"WebSocket closed",closeEvent:e})}.bind(this)),this.emit("state:offline")},Mopidy.prototype._reconnect=function(){this.emit("reconnectionPending",{timeToAttempt:this._backoffDelay}),setTimeout(function(){this.emit("reconnecting"),this.connect()}.bind(this),this._backoffDelay),this._backoffDelay=2*this._backoffDelay,this._backoffDelay>this._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},Mopidy.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},Mopidy.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},Mopidy.prototype._handleWebSocketError=function(e){this._console.warn("WebSocket error:",e.stack||e)},Mopidy.prototype._send=function(e){var t=when.defer();switch(this._webSocket.readyState){case WebSocket.CONNECTING:t.resolver.reject({message:"WebSocket is still connecting"});break;case WebSocket.CLOSING:t.resolver.reject({message:"WebSocket is closing"});break;case WebSocket.CLOSED:t.resolver.reject({message:"WebSocket is closed"});break;default:e.jsonrpc="2.0",e.id=this._nextRequestId(),this._pendingRequests[e.id]=t.resolver,this._webSocket.send(JSON.stringify(e)),this.emit("websocket:outgoingMessage",e)}return t.promise},Mopidy.prototype._nextRequestId=function(){var e=-1;return function(){return e+=1}}(),Mopidy.prototype._handleMessage=function(e){try{var t=JSON.parse(e.data);t.hasOwnProperty("id")?this._handleResponse(t):t.hasOwnProperty("event")?this._handleEvent(t):this._console.warn("Unknown message type received. Message was: "+e.data)}catch(n){if(!(n instanceof SyntaxError))throw n;this._console.warn("WebSocket message parsing failed. Message was: "+e.data)}},Mopidy.prototype._handleResponse=function(e){if(!this._pendingRequests.hasOwnProperty(e.id))return this._console.warn("Unexpected response received. Message was:",e),void 0;var t=this._pendingRequests[e.id];delete this._pendingRequests[e.id],e.hasOwnProperty("result")?t.resolve(e.result):e.hasOwnProperty("error")?(t.reject(e.error),this._console.warn("Server returned error:",e.error)):(t.reject({message:"Response without 'result' or 'error' received",data:{response:e}}),this._console.warn("Response without 'result' or 'error' received. Message was:",e))},Mopidy.prototype._handleEvent=function(e){var t=e.event,n=e;delete n.event,this.emit("event:"+this._snakeToCamel(t),n)},Mopidy.prototype._getApiSpec=function(){this._send({method:"core.describe"}).then(this._createApi.bind(this),this._handleWebSocketError).then(null,this._handleWebSocketError)},Mopidy.prototype._createApi=function(e){var t=function(e){return function(){var t=Array.prototype.slice.call(arguments);return this._send({method:e,params:t})}.bind(this)}.bind(this),n=function(e){var t=e.split(".");return t.length>=1&&"core"===t[0]&&(t=t.slice(1)),t},o=function(e){var t=this;return e.forEach(function(e){e=this._snakeToCamel(e),t[e]=t[e]||{},t=t[e]}.bind(this)),t}.bind(this),r=function(r){var i=n(r),s=this._snakeToCamel(i.slice(-1)[0]),c=o(i.slice(0,-1));c[s]=t(r),c[s].description=e[r].description,c[s].params=e[r].params}.bind(this);Object.keys(e).forEach(r),this.emit("state:online")},Mopidy.prototype._snakeToCamel=function(e){return e.replace(/(_[a-z])/g,function(e){return e.toUpperCase().replace("_","")})}; \ No newline at end of file +function Mopidy(e){return this instanceof Mopidy?(this._settings=this._configure(e||{}),this._console=this._getConsole(),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,bane.createEventEmitter(this),this._delegateEvents(),this._settings.autoConnect&&this.connect(),void 0):new Mopidy(e)}if(("function"==typeof define&&define.amd&&function(e){define(e)}||"object"==typeof module&&function(e){module.exports=e()}||function(e){this.bane=e()})(function(){"use strict";function e(e,t,n){var o,i=n.length;if(i>0)for(o=0;i>o;++o)n[o](e,t);else setTimeout(function(){throw t.message=e+" listener threw error: "+t.message,t},0)}function t(e){if("function"!=typeof e)throw new TypeError("Listener is not function");return e}function n(e){return e.supervisors||(e.supervisors=[]),e.supervisors}function o(e,t){return e.listeners||(e.listeners={}),t&&!e.listeners[t]&&(e.listeners[t]=[]),t?e.listeners[t]:e.listeners}function i(e){return e.errbacks||(e.errbacks=[]),e.errbacks}function r(r){function c(t,n,o){try{n.listener.apply(n.thisp||r,o)}catch(s){e(t,s,i(r))}}return r=r||{},r.on=function(e,i,r){return"function"==typeof e?n(this).push({listener:e,thisp:i}):(o(this,e).push({listener:t(i),thisp:r}),void 0)},r.off=function(e,t){var r,s,c,f;if(!e){r=n(this),r.splice(0,r.length),s=o(this);for(c in s)s.hasOwnProperty(c)&&(r=o(this,c),r.splice(0,r.length));return r=i(this),r.splice(0,r.length),void 0}if("function"==typeof e?(r=n(this),t=e):r=o(this,e),!t)return r.splice(0,r.length),void 0;for(c=0,f=r.length;f>c;++c)if(r[c].listener===t)return r.splice(c,1),void 0},r.once=function(e,t,n){var o=function(){r.off(e,o),t.apply(this,arguments)};r.on(e,o,n)},r.bind=function(e,t){var n,o,i;if(t)for(o=0,i=t.length;i>o;++o){if("function"!=typeof e[t[o]])throw Error("No such method "+t[o]);this.on(t[o],e[t[o]],e)}else for(n in e)"function"==typeof e[n]&&this.on(n,e[n],e);return e},r.emit=function(e){var t,i,r=n(this),f=s.call(arguments);for(t=0,i=r.length;i>t;++t)c(e,r[t],f);for(r=o(this,e).slice(),f=s.call(arguments,1),t=0,i=r.length;i>t;++t)c(e,r[t],f)},r.errback=function(e){this.errbacks||(this.errbacks=[]),this.errbacks.push(t(e))},r}var s=Array.prototype.slice;return{createEventEmitter:r}}),"undefined"!=typeof window&&(window.define=function(e){try{delete window.define}catch(t){window.define=void 0}window.when=e()},window.define.amd={}),function(e){"use strict";e(function(){function e(e,t,o,i){return n(e).then(t,o,i)}function t(e){this.then=e}function n(e){return r(function(t){t(e)})}function o(t){return e(t,f)}function i(){function e(e,r,s){t.resolve=t.resolver.resolve=function(t){return i?n(t):(i=!0,e(t),o)},t.reject=t.resolver.reject=function(e){return i?n(f(e)):(i=!0,r(e),o)},t.notify=t.resolver.notify=function(e){return s(e),e}}var t,o,i;return t={promise:R,resolve:R,reject:R,notify:R,resolver:{resolve:R,reject:R,notify:R}},t.promise=o=r(e),t}function r(e){function n(e,t,n){return r(function(o,i,r){p?p.push(function(s){s.then(e,t,n).then(o,i,r)}):k(function(){h.then(e,t,n).then(o,i,r)})})}function o(e){p&&(h=s(e),a(p,h),p=R)}function i(e){o(f(e))}function c(e){p&&a(p,u(e))}var h,p=[];try{e(o,i,c)}catch(l){i(l)}return new t(n)}function s(e){return e instanceof t?e:e!==Object(e)?c(e):r(function(t,n,o){k(function(){try{var i=e.then;"function"==typeof i?j(i,e,t,n,o):t(c(e))}catch(r){n(r)}})})}function c(e){var n=new t(function(t){try{return"function"==typeof t?s(t(e)):n}catch(o){return f(o)}});return n}function f(e){var n=new t(function(t,o){try{return"function"==typeof o?s(o(e)):n}catch(i){return f(i)}});return n}function u(e){var n=new t(function(t,o,i){try{return"function"==typeof i?u(i(e)):n}catch(r){return u(r)}});return n}function a(e,t){k(function(){for(var n,o=0;n=e[o++];)n(t)})}function h(e){return e&&"function"==typeof e.then}function p(t,n,o,i,s){return m(2,arguments),e(t,function(t){function c(o,i,r){function s(e){l(e)}function c(e){p(e)}var f,u,a,h,p,l,d,y;if(d=t.length>>>0,f=Math.max(0,Math.min(n,d)),a=[],u=d-f+1,h=[],f)for(l=function(e){h.push(e),--u||(p=l=v,i(h))},p=function(e){a.push(e),--f||(p=l=v,o(a))},y=0;d>y;++y)y in t&&e(t[y],c,s,r);else o(a)}return r(c).then(o,i,s)})}function l(e,t,n,o){function i(e){return t?t(e[0]):e[0]}return p(e,1,i,n,o)}function d(e,t,n,o){return m(1,arguments),b(e,M).then(t,n,o)}function y(){return b(arguments,M)}function b(t,n){return e(t,function(t){function o(o,i,r){var s,c,f,u,a;if(f=c=t.length>>>0,s=[],f)for(u=function(t,c){e(t,n).then(function(e){s[c]=e,--f||o(s)},i,r)},a=0;c>a;a++)a in t?u(t[a],a):--f;else o(s)}return r(o)})}function w(t,n){var o=j(E,arguments,1);return e(t,function(t){var i;return i=t.length,o[0]=function(t,o,r){return e(t,function(t){return e(o,function(e){return n(t,e,r,i)})})},S.apply(t,o)})}function k(e){1===W.push(e)&&g()}function g(){D(_)}function _(){for(var e,t=0;e=W[t++];)e();W=[]}function m(e,t){for(var n,o=t.length;o>e;)if(n=t[--o],null!=n&&"function"!=typeof n)throw Error("arg "+o+" must be a function")}function v(){}function M(e){return e}e.defer=i,e.resolve=n,e.reject=o,e.join=y,e.all=d,e.map=b,e.reduce=w,e.any=l,e.some=p,e.isPromise=h,t.prototype={otherwise:function(e){return this.then(R,e)},ensure:function(e){function t(){return n(e())}var o=this;return this.then(t,t).yield(o)},yield:function(e){return this.then(function(){return e})},spread:function(e){return this.then(function(t){return d(t,function(t){return e.apply(R,t)})})},always:function(e,t){return this.then(e,e,t)}};var S,E,j,D,W,O,q,C,x,R;return W=[],O=setTimeout,D="function"==typeof setImmediate?"undefined"==typeof window?setImmediate:setImmediate.bind(window):"object"==typeof process?process.nextTick:function(e){O(e,0)},q=Function.prototype,C=q.call,j=q.bind?C.bind(C):function(e,t){return e.apply(t,E.call(arguments,2))},x=[],E=x.slice,S=x.reduce||function(e){var t,n,o,i,r;if(r=0,t=Object(this),i=t.length>>>0,n=arguments,1>=n.length)for(;;){if(r in t){o=t[r++];break}if(++r>=i)throw new TypeError}else o=n[1];for(;i>r;++r)r in t&&(o=e(o,t[r],r,t));return o},e})}("function"==typeof define&&define.amd?define:function(e){module.exports=e()}),"object"==typeof module&&"function"==typeof require)var bane=require("bane"),websocket=require("faye-websocket"),when=require("when");Mopidy.WebSocket="object"==typeof module&&"function"==typeof require?websocket.Client:window.WebSocket,Mopidy.prototype._configure=function(e){var t="undefined"!=typeof document&&document.location.host||"localhost";return e.webSocketUrl=e.webSocketUrl||"ws://"+t+"/mopidy/ws/",e.autoConnect!==!1&&(e.autoConnect=!0),e.backoffDelayMin=e.backoffDelayMin||1e3,e.backoffDelayMax=e.backoffDelayMax||64e3,e},Mopidy.prototype._getConsole=function(){var e=e!==void 0&&e||{};return e.log=e.log||function(){},e.warn=e.warn||function(){},e.error=e.error||function(){},e},Mopidy.prototype._delegateEvents=function(){this.off("websocket:close"),this.off("websocket:error"),this.off("websocket:incomingMessage"),this.off("websocket:open"),this.off("state:offline"),this.on("websocket:close",this._cleanup),this.on("websocket:error",this._handleWebSocketError),this.on("websocket:incomingMessage",this._handleMessage),this.on("websocket:open",this._resetBackoffDelay),this.on("websocket:open",this._getApiSpec),this.on("state:offline",this._reconnect)},Mopidy.prototype.connect=function(){if(this._webSocket){if(this._webSocket.readyState===Mopidy.WebSocket.OPEN)return;this._webSocket.close()}this._webSocket=this._settings.webSocket||new Mopidy.WebSocket(this._settings.webSocketUrl),this._webSocket.onclose=function(e){this.emit("websocket:close",e)}.bind(this),this._webSocket.onerror=function(e){this.emit("websocket:error",e)}.bind(this),this._webSocket.onopen=function(){this.emit("websocket:open")}.bind(this),this._webSocket.onmessage=function(e){this.emit("websocket:incomingMessage",e)}.bind(this)},Mopidy.prototype._cleanup=function(e){Object.keys(this._pendingRequests).forEach(function(t){var n=this._pendingRequests[t];delete this._pendingRequests[t],n.reject({message:"WebSocket closed",closeEvent:e})}.bind(this)),this.emit("state:offline")},Mopidy.prototype._reconnect=function(){this.emit("reconnectionPending",{timeToAttempt:this._backoffDelay}),setTimeout(function(){this.emit("reconnecting"),this.connect()}.bind(this),this._backoffDelay),this._backoffDelay=2*this._backoffDelay,this._backoffDelay>this._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},Mopidy.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},Mopidy.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},Mopidy.prototype._handleWebSocketError=function(e){this._console.warn("WebSocket error:",e.stack||e)},Mopidy.prototype._send=function(e){var t=when.defer();switch(this._webSocket.readyState){case Mopidy.WebSocket.CONNECTING:t.resolver.reject({message:"WebSocket is still connecting"});break;case Mopidy.WebSocket.CLOSING:t.resolver.reject({message:"WebSocket is closing"});break;case Mopidy.WebSocket.CLOSED:t.resolver.reject({message:"WebSocket is closed"});break;default:e.jsonrpc="2.0",e.id=this._nextRequestId(),this._pendingRequests[e.id]=t.resolver,this._webSocket.send(JSON.stringify(e)),this.emit("websocket:outgoingMessage",e)}return t.promise},Mopidy.prototype._nextRequestId=function(){var e=-1;return function(){return e+=1}}(),Mopidy.prototype._handleMessage=function(e){try{var t=JSON.parse(e.data);t.hasOwnProperty("id")?this._handleResponse(t):t.hasOwnProperty("event")?this._handleEvent(t):this._console.warn("Unknown message type received. Message was: "+e.data)}catch(n){if(!(n instanceof SyntaxError))throw n;this._console.warn("WebSocket message parsing failed. Message was: "+e.data)}},Mopidy.prototype._handleResponse=function(e){if(!this._pendingRequests.hasOwnProperty(e.id))return this._console.warn("Unexpected response received. Message was:",e),void 0;var t=this._pendingRequests[e.id];delete this._pendingRequests[e.id],e.hasOwnProperty("result")?t.resolve(e.result):e.hasOwnProperty("error")?(t.reject(e.error),this._console.warn("Server returned error:",e.error)):(t.reject({message:"Response without 'result' or 'error' received",data:{response:e}}),this._console.warn("Response without 'result' or 'error' received. Message was:",e))},Mopidy.prototype._handleEvent=function(e){var t=e.event,n=e;delete n.event,this.emit("event:"+this._snakeToCamel(t),n)},Mopidy.prototype._getApiSpec=function(){return this._send({method:"core.describe"}).then(this._createApi.bind(this),this._handleWebSocketError).then(null,this._handleWebSocketError)},Mopidy.prototype._createApi=function(e){var t=function(e){return function(){var t=Array.prototype.slice.call(arguments);return this._send({method:e,params:t})}.bind(this)}.bind(this),n=function(e){var t=e.split(".");return t.length>=1&&"core"===t[0]&&(t=t.slice(1)),t},o=function(e){var t=this;return e.forEach(function(e){e=this._snakeToCamel(e),t[e]=t[e]||{},t=t[e]}.bind(this)),t}.bind(this),i=function(i){var r=n(i),s=this._snakeToCamel(r.slice(-1)[0]),c=o(r.slice(0,-1));c[s]=t(i),c[s].description=e[i].description,c[s].params=e[i].params}.bind(this);Object.keys(e).forEach(i),this.emit("state:online")},Mopidy.prototype._snakeToCamel=function(e){return e.replace(/(_[a-z])/g,function(e){return e.toUpperCase().replace("_","")})},"object"==typeof exports&&(exports.Mopidy=Mopidy); \ No newline at end of file