From 6cbb15bd154052285401a666cbe4f79d1fb9feb7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 12 Jun 2014 20:47:15 +0200 Subject: [PATCH 01/13] js: Update dev env instructions --- js/README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/js/README.md b/js/README.md index 54bdc502..31d030d1 100644 --- a/js/README.md +++ b/js/README.md @@ -47,13 +47,9 @@ See the [Mopidy.js documentation](http://docs.mopidy.com/en/latest/api/js/). Building from source -------------------- -1. Install [Node.js](http://nodejs.org/) and npm. There is a PPA if you're - running Ubuntu: +1. Install [Node.js](http://nodejs.org/) and npm. If you're running Ubuntu: - sudo apt-get install python-software-properties - sudo add-apt-repository ppa:chris-lea/node.js - sudo apt-get update - sudo apt-get install nodejs + sudo apt-get install nodejs-legacy npm 2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies: From 7b71e64553180245045668f36657360af4ca53b7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 12 Jun 2014 20:52:53 +0200 Subject: [PATCH 02/13] js: Update test deps --- js/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/package.json b/js/package.json index d16cfaa9..159bad83 100644 --- a/js/package.json +++ b/js/package.json @@ -19,14 +19,14 @@ "when": "~2.7.1" }, "devDependencies": { - "buster": "~0.7.8", - "grunt": "~0.4.2", + "buster": "~0.7.13", + "grunt": "~0.4.5", "grunt-buster": "~0.3.1", - "grunt-browserify": "~1.3.0", + "grunt-browserify": "~1.3.2", "grunt-contrib-jshint": "~0.8.0", "grunt-contrib-uglify": "~0.2.7", "grunt-contrib-watch": "~0.5.3", - "phantomjs": "~1.9.2-6" + "phantomjs": "~1.9.7-8" }, "scripts": { "test": "grunt test", From 0167986b98df1f8badcbd0886a70fe5c24621e22 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Jun 2014 10:12:18 +0200 Subject: [PATCH 03/13] js: Pass grunt-browserify errors on --- js/Gruntfile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/Gruntfile.js b/js/Gruntfile.js index 812ecec4..437ad9d0 100644 --- a/js/Gruntfile.js +++ b/js/Gruntfile.js @@ -26,7 +26,7 @@ module.exports = function (grunt) { }, options: { postBundleCB: function (err, src, next) { - next(null, grunt.template.process("<%= meta.banner %>") + src); + next(err, grunt.template.process("<%= meta.banner %>") + src); }, standalone: "Mopidy" } @@ -45,7 +45,7 @@ module.exports = function (grunt) { }, options: { postBundleCB: function (err, src, next) { - next(null, grunt.template.process("<%= meta.banner %>") + src); + next(err, grunt.template.process("<%= meta.banner %>") + src); }, standalone: "Mopidy" } From c40c70a90b9d3b6c1d33489d01a568defc0de0cf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Jun 2014 10:16:39 +0200 Subject: [PATCH 04/13] js: Update dev dependencies --- js/package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/js/package.json b/js/package.json index 159bad83..4bf9fcb0 100644 --- a/js/package.json +++ b/js/package.json @@ -20,12 +20,13 @@ }, "devDependencies": { "buster": "~0.7.13", + "browserify": "~3", "grunt": "~0.4.5", "grunt-buster": "~0.3.1", "grunt-browserify": "~1.3.2", - "grunt-contrib-jshint": "~0.8.0", - "grunt-contrib-uglify": "~0.2.7", - "grunt-contrib-watch": "~0.5.3", + "grunt-contrib-jshint": "~0.10.0", + "grunt-contrib-uglify": "~0.5.0", + "grunt-contrib-watch": "~0.6.1", "phantomjs": "~1.9.7-8" }, "scripts": { From eec51a1e83b5018576b96a09ee07915cedab9313 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Jun 2014 10:38:27 +0200 Subject: [PATCH 05/13] js: Upgrade to when.js 3 --- docs/changelog.rst | 7 +++++++ js/README.md | 8 ++++++++ js/package.json | 4 ++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b10575ed..e0baee0f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -78,6 +78,13 @@ Feature release. Mopidy's HTTP server among other Zeroconf-published HTTP servers on the local network. +- Update Mopidy.js to use when.js 3. If you maintain a Mopidy client, you + should review the `differences between when.js 2 and 3 + `_ + and the `when.js debugging guide + `_. + This version has been released to npm as Mopidy.js v0.3.0. + **MPD frontend** - Proper command tokenization for MPD requests. This replaces the old regex diff --git a/js/README.md b/js/README.md index 31d030d1..9f5bf9e6 100644 --- a/js/README.md +++ b/js/README.md @@ -80,6 +80,14 @@ To run other [grunt](http://gruntjs.com/) targets which isn't predefined in Changelog --------- +### 0.3.0 (UNRELEASED) + +- Upgrade to when.js 3, which brings great performance improvements and better + debugging facilities. If you maintain a Mopidy client, you should review the + [differences between when.js 2 and 3](https://github.com/cujojs/when/blob/master/docs/api.md#upgrading-to-30-from-2x) + and the + [when.js debugging guide](https://github.com/cujojs/when/blob/master/docs/api.md#debugging-promises). + ### 0.2.0 (2014-01-04) - **Backwards incompatible change for Node.js users:** diff --git a/js/package.json b/js/package.json index 4bf9fcb0..4082afb9 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "mopidy", - "version": "0.2.0", + "version": "0.3.0", "description": "Client lib for controlling a Mopidy music server over a WebSocket", "homepage": "http://www.mopidy.com/", "author": { @@ -16,7 +16,7 @@ "dependencies": { "bane": "~1.1.0", "faye-websocket": "~0.7.2", - "when": "~2.7.1" + "when": "~3.2.3" }, "devDependencies": { "buster": "~0.7.13", From cb04b81bf43ad70906e2af4d22f67448822de047 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Jun 2014 12:16:35 +0200 Subject: [PATCH 06/13] js: Simplify _send() rejections --- js/src/mopidy.js | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/js/src/mopidy.js b/js/src/mopidy.js index 1667f9b1..32ae7e8e 100644 --- a/js/src/mopidy.js +++ b/js/src/mopidy.js @@ -141,33 +141,22 @@ Mopidy.prototype._handleWebSocketError = function (error) { }; Mopidy.prototype._send = function (message) { - var deferred = when.defer(); - switch (this._webSocket.readyState) { case Mopidy.WebSocket.CONNECTING: - deferred.resolver.reject({ - message: "WebSocket is still connecting" - }); - break; + return when.reject({message: "WebSocket is still connecting"}); case Mopidy.WebSocket.CLOSING: - deferred.resolver.reject({ - message: "WebSocket is closing" - }); - break; + return when.reject({message: "WebSocket is closing"}); case Mopidy.WebSocket.CLOSED: - deferred.resolver.reject({ - message: "WebSocket is closed" - }); - break; + return when.reject({message: "WebSocket is closed"}); default: + var deferred = when.defer(); message.jsonrpc = "2.0"; message.id = this._nextRequestId(); this._pendingRequests[message.id] = deferred.resolver; this._webSocket.send(JSON.stringify(message)); this.emit("websocket:outgoingMessage", message); + return deferred.promise; } - - return deferred.promise; }; Mopidy.prototype._nextRequestId = (function () { From 4a609ce95d6d593657a672c5f0dc521d20303732 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Jun 2014 12:17:47 +0200 Subject: [PATCH 07/13] js: Add TODOs for rejecting promises with Error This is recommended to get proper stack traces, according to https://github.com/cujojs/when/blob/master/docs/api.md#a-note-on-javascript-errors --- js/src/mopidy.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/js/src/mopidy.js b/js/src/mopidy.js index 32ae7e8e..d1afc289 100644 --- a/js/src/mopidy.js +++ b/js/src/mopidy.js @@ -102,6 +102,8 @@ Mopidy.prototype._cleanup = function (closeEvent) { Object.keys(this._pendingRequests).forEach(function (requestId) { var resolver = this._pendingRequests[requestId]; delete this._pendingRequests[requestId]; + // TODO Mopidy.js 1.0 should reject with an Error object to produce + // usable stack traces resolver.reject({ message: "WebSocket closed", closeEvent: closeEvent @@ -143,10 +145,16 @@ Mopidy.prototype._handleWebSocketError = function (error) { Mopidy.prototype._send = function (message) { switch (this._webSocket.readyState) { case Mopidy.WebSocket.CONNECTING: + // TODO Mopidy.js 1.0 should reject with an Error object to produce + // usable stack traces return when.reject({message: "WebSocket is still connecting"}); case Mopidy.WebSocket.CLOSING: + // TODO Mopidy.js 1.0 should reject with an Error object to produce + // usable stack traces return when.reject({message: "WebSocket is closing"}); case Mopidy.WebSocket.CLOSED: + // TODO Mopidy.js 1.0 should reject with an Error object to produce + // usable stack traces return when.reject({message: "WebSocket is closed"}); default: var deferred = when.defer(); @@ -203,9 +211,13 @@ Mopidy.prototype._handleResponse = function (responseMessage) { if (responseMessage.hasOwnProperty("result")) { resolver.resolve(responseMessage.result); } else if (responseMessage.hasOwnProperty("error")) { + // TODO Mopidy.js 1.0 should reject with an Error object to produce + // usable stack traces resolver.reject(responseMessage.error); this._console.warn("Server returned error:", responseMessage.error); } else { + // TODO Mopidy.js 1.0 should reject with an Error object to produce + // usable stack traces resolver.reject({ message: "Response without 'result' or 'error' received", data: {response: responseMessage} From 026fdb38a320c69750429295954e329a6fa22970 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Jun 2014 12:17:47 +0200 Subject: [PATCH 08/13] js: Reject promises using Error objects This is recommended to get proper stack traces, according to https://github.com/cujojs/when/blob/master/docs/api.md#a-note-on-javascript-errors. The new Error objects has the exact same properties as the objects we used to reject promises previously, thus this should be fully backwards compatible, but improve debuggability. --- js/README.md | 4 ++ js/src/mopidy.js | 38 +++++------- js/test/mopidy-test.js | 135 +++++++++++++++++++++++++++++------------ 3 files changed, 115 insertions(+), 62 deletions(-) diff --git a/js/README.md b/js/README.md index 9f5bf9e6..dcaa57ac 100644 --- a/js/README.md +++ b/js/README.md @@ -88,6 +88,10 @@ Changelog and the [when.js debugging guide](https://github.com/cujojs/when/blob/master/docs/api.md#debugging-promises). +- All promise rejection values are now of the Error type. This ensures that all + JavaScript VMs will show a useful stack trace if a rejected promise's value + is used to throw an exception. + ### 0.2.0 (2014-01-04) - **Backwards incompatible change for Node.js users:** diff --git a/js/src/mopidy.js b/js/src/mopidy.js index d1afc289..473d81a5 100644 --- a/js/src/mopidy.js +++ b/js/src/mopidy.js @@ -102,12 +102,9 @@ Mopidy.prototype._cleanup = function (closeEvent) { Object.keys(this._pendingRequests).forEach(function (requestId) { var resolver = this._pendingRequests[requestId]; delete this._pendingRequests[requestId]; - // TODO Mopidy.js 1.0 should reject with an Error object to produce - // usable stack traces - resolver.reject({ - message: "WebSocket closed", - closeEvent: closeEvent - }); + var error = new Error("WebSocket closed"); + error.closeEvent = closeEvent; + resolver.reject(error); }.bind(this)); this.emit("state:offline"); @@ -145,17 +142,11 @@ Mopidy.prototype._handleWebSocketError = function (error) { Mopidy.prototype._send = function (message) { switch (this._webSocket.readyState) { case Mopidy.WebSocket.CONNECTING: - // TODO Mopidy.js 1.0 should reject with an Error object to produce - // usable stack traces - return when.reject({message: "WebSocket is still connecting"}); + return when.reject(new Error("WebSocket is still connecting")); case Mopidy.WebSocket.CLOSING: - // TODO Mopidy.js 1.0 should reject with an Error object to produce - // usable stack traces - return when.reject({message: "WebSocket is closing"}); + return when.reject(new Error("WebSocket is closing")); case Mopidy.WebSocket.CLOSED: - // TODO Mopidy.js 1.0 should reject with an Error object to produce - // usable stack traces - return when.reject({message: "WebSocket is closed"}); + return when.reject(new Error("WebSocket is closed")); default: var deferred = when.defer(); message.jsonrpc = "2.0"; @@ -205,23 +196,22 @@ Mopidy.prototype._handleResponse = function (responseMessage) { return; } + var error; var resolver = this._pendingRequests[responseMessage.id]; delete this._pendingRequests[responseMessage.id]; if (responseMessage.hasOwnProperty("result")) { resolver.resolve(responseMessage.result); } else if (responseMessage.hasOwnProperty("error")) { - // TODO Mopidy.js 1.0 should reject with an Error object to produce - // usable stack traces - resolver.reject(responseMessage.error); + error = new Error(responseMessage.error.message); + error.code = responseMessage.error.code; + error.data = responseMessage.error.data; + resolver.reject(error); this._console.warn("Server returned error:", responseMessage.error); } else { - // TODO Mopidy.js 1.0 should reject with an Error object to produce - // usable stack traces - resolver.reject({ - message: "Response without 'result' or 'error' received", - data: {response: responseMessage} - }); + error = new Error("Response without 'result' or 'error' received"); + error.data = {response: responseMessage}; + resolver.reject(error); this._console.warn( "Response without 'result' or 'error' received. Message was:", responseMessage); diff --git a/js/test/mopidy-test.js b/js/test/mopidy-test.js index 9f2509fc..97525ba7 100644 --- a/js/test/mopidy-test.js +++ b/js/test/mopidy-test.js @@ -169,12 +169,16 @@ buster.testCase("Mopidy", { this.mopidy._cleanup(closeEvent); assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0); - when.join(promise1, promise2).then(done(function () { - assert(false, "Promises should be rejected"); - }), done(function (error) { - assert.equals(error.message, "WebSocket closed"); - assert.same(error.closeEvent, closeEvent); - })); + when.join(promise1, promise2).done( + done(function () { + assert(false, "Promises should be rejected"); + }), + done(function (error) { + assert(error instanceof Error); + assert.equals(error.message, "WebSocket closed"); + assert.same(error.closeEvent, closeEvent); + }) + ); }, "emits 'state:offline' event when done": function () { @@ -388,12 +392,16 @@ buster.testCase("Mopidy", { var promise = this.mopidy._send({method: "foo"}); refute.called(this.mopidy._webSocket.send); - promise.then(done(function () { - assert(false); - }), done(function (error) { - assert.equals( - error.message, "WebSocket is still connecting"); - })); + promise.done( + done(function () { + assert(false); + }), + done(function (error) { + assert(error instanceof Error); + assert.equals( + error.message, "WebSocket is still connecting"); + }) + ); }, "immediately rejects request if CLOSING": function (done) { @@ -402,12 +410,15 @@ buster.testCase("Mopidy", { var promise = this.mopidy._send({method: "foo"}); refute.called(this.mopidy._webSocket.send); - promise.then(done(function () { - assert(false); - }), done(function (error) { - assert.equals( - error.message, "WebSocket is closing"); - })); + promise.done( + done(function () { + assert(false); + }), + done(function (error) { + assert(error instanceof Error); + assert.equals(error.message, "WebSocket is closing"); + }) + ); }, "immediately rejects request if CLOSED": function (done) { @@ -416,12 +427,15 @@ buster.testCase("Mopidy", { var promise = this.mopidy._send({method: "foo"}); refute.called(this.mopidy._webSocket.send); - promise.then(done(function () { - assert(false); - }), done(function (error) { - assert.equals( - error.message, "WebSocket is closed"); - })); + promise.done( + done(function () { + assert(false); + }), + done(function (error) { + assert(error instanceof Error); + assert.equals(error.message, "WebSocket is closed"); + }) + ); } }, @@ -544,7 +558,11 @@ buster.testCase("Mopidy", { "rejects and logs requests which get errors back": function (done) { var stub = this.stub(this.mopidy._console, "warn"); var promise = this.mopidy._send({method: "bar"}); - var responseError = {message: "Error", data: {}}; + var responseError = { + code: -32601, + message: "Method not found", + data: {} + }; var responseMessage = { jsonrpc: "2.0", id: Object.keys(this.mopidy._pendingRequests)[0], @@ -555,11 +573,48 @@ buster.testCase("Mopidy", { assert.calledOnceWith(stub, "Server returned error:", responseError); - promise.then(done(function () { - assert(false); - }), done(function (error) { - assert.equals(error, responseError); - })); + promise.done( + done(function () { + assert(false); + }), + done(function (error) { + assert(error instanceof Error); + assert.equals(error.code, responseError.code); + assert.equals(error.message, responseError.message); + assert.equals(error.data, responseError.data); + }) + ); + }, + + "rejects and logs requests which get errors without data": function (done) { + var stub = this.stub(this.mopidy._console, "warn"); + var promise = this.mopidy._send({method: "bar"}); + var responseError = { + code: -32601, + message: "Method not found" + // 'data' key intentionally missing + }; + var responseMessage = { + jsonrpc: "2.0", + id: Object.keys(this.mopidy._pendingRequests)[0], + error: responseError + }; + + this.mopidy._handleResponse(responseMessage); + + assert.calledOnceWith(stub, + "Server returned error:", responseError); + promise.done( + done(function () { + assert(false); + }), + done(function (error) { + assert(error instanceof Error); + assert.equals(error.code, responseError.code); + assert.equals(error.message, responseError.message); + refute.defined(error.data); + }) + ); }, "rejects and logs responses without result or error": function (done) { @@ -575,14 +630,18 @@ buster.testCase("Mopidy", { assert.calledOnceWith(stub, "Response without 'result' or 'error' received. Message was:", responseMessage); - promise.then(done(function () { - assert(false); - }), done(function (error) { - assert.equals( - error.message, - "Response without 'result' or 'error' received"); - assert.equals(error.data.response, responseMessage); - })); + promise.done( + done(function () { + assert(false); + }), + done(function (error) { + assert(error instanceof Error); + assert.equals( + error.message, + "Response without 'result' or 'error' received"); + assert.equals(error.data.response, responseMessage); + }) + ); } }, From 30471bab743becca2c07addd7f277f4fb6498887 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Jun 2014 22:51:12 +0200 Subject: [PATCH 09/13] js: Add ServerError and ConnectionError types --- docs/changelog.rst | 7 +++++++ js/README.md | 4 +++- js/src/mopidy.js | 27 ++++++++++++++++++++++----- js/test/mopidy-test.js | 5 +++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e0baee0f..dd57d8ef 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -85,6 +85,13 @@ Feature release. `_. This version has been released to npm as Mopidy.js v0.3.0. +- All of Mopidy.js' promise rejection values are now of the Error type. This + ensures that all JavaScript VMs will show a useful stack trace if a rejected + promise's value is used to throw an exception. To allow catch clauses to + handle different errors differently, server side errors are of the type + ``Mopidy.ServerError``, and connection related errors are of the type + ``Mopidy.ConnectionError``. + **MPD frontend** - Proper command tokenization for MPD requests. This replaces the old regex diff --git a/js/README.md b/js/README.md index dcaa57ac..966e9b2e 100644 --- a/js/README.md +++ b/js/README.md @@ -90,7 +90,9 @@ Changelog - All promise rejection values are now of the Error type. This ensures that all JavaScript VMs will show a useful stack trace if a rejected promise's value - is used to throw an exception. + is used to throw an exception. To allow catch clauses to handle different + errors differently, server side errors are of the type `Mopidy.ServerError`, + and connection related errors are of the type `Mopidy.ConnectionError`. ### 0.2.0 (2014-01-04) diff --git a/js/src/mopidy.js b/js/src/mopidy.js index 473d81a5..ff0a64ee 100644 --- a/js/src/mopidy.js +++ b/js/src/mopidy.js @@ -24,6 +24,20 @@ function Mopidy(settings) { } } +Mopidy.ConnectionError = function (message) { + this.name = "ConnectionError"; + this.message = message; +}; +Mopidy.ConnectionError.prototype = new Error(); +Mopidy.ConnectionError.prototype.constructor = Mopidy.ConnectionError; + +Mopidy.ServerError = function (message) { + this.name = "ServerError"; + this.message = message; +}; +Mopidy.ServerError.prototype = new Error(); +Mopidy.ServerError.prototype.constructor = Mopidy.ServerError; + Mopidy.WebSocket = websocket.Client; Mopidy.prototype._configure = function (settings) { @@ -102,7 +116,7 @@ Mopidy.prototype._cleanup = function (closeEvent) { Object.keys(this._pendingRequests).forEach(function (requestId) { var resolver = this._pendingRequests[requestId]; delete this._pendingRequests[requestId]; - var error = new Error("WebSocket closed"); + var error = new Mopidy.ConnectionError("WebSocket closed"); error.closeEvent = closeEvent; resolver.reject(error); }.bind(this)); @@ -142,11 +156,14 @@ Mopidy.prototype._handleWebSocketError = function (error) { Mopidy.prototype._send = function (message) { switch (this._webSocket.readyState) { case Mopidy.WebSocket.CONNECTING: - return when.reject(new Error("WebSocket is still connecting")); + return when.reject( + new Mopidy.ConnectionError("WebSocket is still connecting")); case Mopidy.WebSocket.CLOSING: - return when.reject(new Error("WebSocket is closing")); + return when.reject( + new Mopidy.ConnectionError("WebSocket is closing")); case Mopidy.WebSocket.CLOSED: - return when.reject(new Error("WebSocket is closed")); + return when.reject( + new Mopidy.ConnectionError("WebSocket is closed")); default: var deferred = when.defer(); message.jsonrpc = "2.0"; @@ -203,7 +220,7 @@ Mopidy.prototype._handleResponse = function (responseMessage) { if (responseMessage.hasOwnProperty("result")) { resolver.resolve(responseMessage.result); } else if (responseMessage.hasOwnProperty("error")) { - error = new Error(responseMessage.error.message); + error = new Mopidy.ServerError(responseMessage.error.message); error.code = responseMessage.error.code; error.data = responseMessage.error.data; resolver.reject(error); diff --git a/js/test/mopidy-test.js b/js/test/mopidy-test.js index 97525ba7..4b4a9f60 100644 --- a/js/test/mopidy-test.js +++ b/js/test/mopidy-test.js @@ -175,6 +175,7 @@ buster.testCase("Mopidy", { }), done(function (error) { assert(error instanceof Error); + assert(error instanceof Mopidy.ConnectionError); assert.equals(error.message, "WebSocket closed"); assert.same(error.closeEvent, closeEvent); }) @@ -398,6 +399,7 @@ buster.testCase("Mopidy", { }), done(function (error) { assert(error instanceof Error); + assert(error instanceof Mopidy.ConnectionError); assert.equals( error.message, "WebSocket is still connecting"); }) @@ -416,6 +418,7 @@ buster.testCase("Mopidy", { }), done(function (error) { assert(error instanceof Error); + assert(error instanceof Mopidy.ConnectionError); assert.equals(error.message, "WebSocket is closing"); }) ); @@ -433,6 +436,7 @@ buster.testCase("Mopidy", { }), done(function (error) { assert(error instanceof Error); + assert(error instanceof Mopidy.ConnectionError); assert.equals(error.message, "WebSocket is closed"); }) ); @@ -610,6 +614,7 @@ buster.testCase("Mopidy", { }), done(function (error) { assert(error instanceof Error); + assert(error instanceof Mopidy.ServerError); assert.equals(error.code, responseError.code); assert.equals(error.message, responseError.message); refute.defined(error.data); From ee9d19fc6e6bfd25901729b284399a39296ad299 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Jun 2014 23:01:54 +0200 Subject: [PATCH 10/13] js: Fix unhandled promise rejection in test --- js/test/mopidy-test.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/js/test/mopidy-test.js b/js/test/mopidy-test.js index 4b4a9f60..0dc95a45 100644 --- a/js/test/mopidy-test.js +++ b/js/test/mopidy-test.js @@ -169,15 +169,16 @@ buster.testCase("Mopidy", { this.mopidy._cleanup(closeEvent); assert.equals(Object.keys(this.mopidy._pendingRequests).length, 0); - when.join(promise1, promise2).done( - done(function () { - assert(false, "Promises should be rejected"); - }), - done(function (error) { - assert(error instanceof Error); - assert(error instanceof Mopidy.ConnectionError); - assert.equals(error.message, "WebSocket closed"); - assert.same(error.closeEvent, closeEvent); + when.settle([promise1, promise2]).done( + done(function (descriptors) { + assert.equals(descriptors.length, 2); + descriptors.forEach(function (d) { + assert.equals(d.state, "rejected"); + assert(d.reason instanceof Error); + assert(d.reason instanceof Mopidy.ConnectionError); + assert.equals(d.reason.message, "WebSocket closed"); + assert.same(d.reason.closeEvent, closeEvent); + }); }) ); }, From 13205bee5ffe2b5418c51831311f902f175b7987 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Jun 2014 23:18:12 +0200 Subject: [PATCH 11/13] js: Connect to /mopidy/ws without trailing slash This is the recommended URL since the switch to Tornado as web server. --- js/src/mopidy.js | 2 +- js/test/mopidy-test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/js/src/mopidy.js b/js/src/mopidy.js index ff0a64ee..8586d231 100644 --- a/js/src/mopidy.js +++ b/js/src/mopidy.js @@ -44,7 +44,7 @@ Mopidy.prototype._configure = function (settings) { var currentHost = (typeof document !== "undefined" && document.location.host) || "localhost"; settings.webSocketUrl = settings.webSocketUrl || - "ws://" + currentHost + "/mopidy/ws/"; + "ws://" + currentHost + "/mopidy/ws"; if (settings.autoConnect !== false) { settings.autoConnect = true; diff --git a/js/test/mopidy-test.js b/js/test/mopidy-test.js index 0dc95a45..4485bc32 100644 --- a/js/test/mopidy-test.js +++ b/js/test/mopidy-test.js @@ -48,7 +48,7 @@ buster.testCase("Mopidy", { document.location.host || "localhost"; assert.calledOnceWith(this.webSocketConstructorStub, - "ws://" + currentHost + "/mopidy/ws/"); + "ws://" + currentHost + "/mopidy/ws"); }, "does not connect when autoConnect is false": function () { @@ -84,7 +84,7 @@ buster.testCase("Mopidy", { document.location.host || "localhost"; assert.calledOnceWith(this.webSocketConstructorStub, - "ws://" + currentHost + "/mopidy/ws/"); + "ws://" + currentHost + "/mopidy/ws"); }, "does nothing when the WebSocket is open": function () { From 76d13b6efd32f2b9bdf13d71da98bfd3e05704f3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Jun 2014 23:40:06 +0200 Subject: [PATCH 12/13] docs: Document Mopidy.js settings (fixes #702) --- docs/api/js.rst | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/api/js.rst b/docs/api/js.rst index 3044e4ec..cdcaab16 100644 --- a/docs/api/js.rst +++ b/docs/api/js.rst @@ -74,7 +74,7 @@ development setup in the ``js/`` dir in our repo. The instructions in Creating an instance ==================== -Once you got Mopidy.js loaded, you need to create an instance of the wrapper: +Once you have Mopidy.js loaded, you need to create an instance of the wrapper: .. code-block:: js @@ -100,6 +100,31 @@ later: // ... do other stuff, like hooking up events ... mopidy.connect(); +When creating an instance, you can specify the following settings: + +``autoConnect`` + Whether or not to connect to the WebSocket on instance creation. Defaults + to true. + +``backoffDelayMin`` + The minimum number of milliseconds to wait after a connection error before + we try to reconnect. For every failed attempt, the backoff delay is doubled + until it reaches ``backoffDelayMax``. Defaults to 1000. + +``backoffDelayMax`` + The maximum number of milliseconds to wait after a connection error before + we try to reconnect. Defaults to 64000. + +``webSocket`` + An existing WebSocket object to be used instead of creating a new + WebSocket. Defaults to undefined. + +``webSocketUrl`` + URL used when creating new WebSocket objects. Defaults to + ``ws:///mopidy/ws``, or + ``ws://localhost/mopidy/ws`` if ``document.location.host`` isn't + available, like it is in the browser environment. + Hooking up to events ==================== From 681c2d15600879b42caf9ff991ed0bb42c22fc08 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Jun 2014 00:39:38 +0200 Subject: [PATCH 13/13] docs: Improve Mopidy.js Promise usage examples --- docs/api/js.rst | 104 ++++++++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/docs/api/js.rst b/docs/api/js.rst index cdcaab16..e314360d 100644 --- a/docs/api/js.rst +++ b/docs/api/js.rst @@ -170,7 +170,8 @@ Once your Mopidy.js object has connected to the Mopidy server and emits the Any calls you make before the ``state:online`` event is emitted will fail. If you've hooked up an errback (more on that a bit later) to the promise returned -from the call, the errback will be called with an error message. +from the call, the errback will be called with a ``Mopidy.ConnectionError`` +instance. All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. The core API attributes is *not* available, but that shouldn't be a problem as we've @@ -229,22 +230,34 @@ Instead, typical usage will look like this: } }; - mopidy.playback.getCurrentTrack().then( - printCurrentTrack, console.error.bind(console)); + mopidy.playback.getCurrentTrack() + .done(printCurrentTrack); -The first function passed to ``then()``, ``printCurrentTrack``, is the callback -that will be called if the method call succeeds. The second function, -``console.error``, is the errback that will be called if anything goes wrong. -If you don't hook up an errback, debugging will be hard as errors will silently -go missing. +The function passed to ``done()``, ``printCurrentTrack``, is the callback +that will be called if the method call succeeds. If anything goes wrong, +``done()`` will throw an exception. -For debugging, you may be interested in errors from function without -interesting return values as well. In that case, you can pass ``null`` as the -callback: +If you want to explicitly handle any errors and avoid an exception being +thrown, you can register an error handler function anywhere in a promise +chain. The function will be called with the error object as the only argument: .. code-block:: js - mopidy.playback.next().then(null, console.error.bind(console)); + mopidy.playback.getCurrentTrack() + .catch(console.error.bind(console)); + .done(printCurrentTrack); + +You can also register the error handler at the end of the promise chain by +passing it as the second argument to ``done()``: + +.. code-block:: js + + mopidy.playback.getCurrentTrack() + .done(printCurrentTrack, console.error.bind(console)); + +If you don't hook up an error handler function and never call ``done()`` on the +promise object, when.js will log warnings to the console that you have +unhandled errors. In general, unhandled errors will not go silently missing. The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A `_ standard. We use the @@ -308,44 +321,38 @@ Example to get started with .. code-block:: js - var consoleError = console.error.bind(console); - var trackDesc = function (track) { return track.name + " by " + track.artists[0].name + " from " + track.album.name; }; - var queueAndPlayFirstPlaylist = function () { - mopidy.playlists.getPlaylists().then(function (playlists) { - var playlist = playlists[0]; + var queueAndPlay = function (playlistNum, trackNum) { + playlistNum = playlistNum || 0; + trackNum = trackNum || 0; + mopidy.playlists.getPlaylists().done(function (playlists) { + var playlist = playlists[playlistNum]; console.log("Loading playlist:", playlist.name); - mopidy.tracklist.add(playlist.tracks).then(function (tlTracks) { - mopidy.playback.play(tlTracks[0]).then(function () { - mopidy.playback.getCurrentTrack().then(function (track) { + mopidy.tracklist.add(playlist.tracks).done(function (tlTracks) { + mopidy.playback.play(tlTracks[trackNum]).done(function () { + mopidy.playback.getCurrentTrack().done(function (track) { console.log("Now playing:", trackDesc(track)); - }, consoleError); - }, consoleError); - }, consoleError); - }, consoleError); + }); + }); + }); + }); }; var mopidy = new Mopidy(); // Connect to server mopidy.on(console.log.bind(console)); // Log all events - mopidy.on("state:online", queueAndPlayFirstPlaylist); + mopidy.on("state:online", queueAndPlay); Approximately the same behavior in a more functional style, using chaining - of promisies. + of promises. .. code-block:: js - var consoleError = console.error.bind(console); - - var getFirst = function (list) { - return list[0]; - }; - - var extractTracks = function (playlist) { - return playlist.tracks; + var get = function (key, object) { + return object[key]; }; var printTypeAndName = function (model) { @@ -364,33 +371,36 @@ Example to get started with // By returning any arguments we get, the function can be inserted // anywhere in the chain. var args = arguments; - return mopidy.playback.getCurrentTrack().then(function (track) { - console.log("Now playing:", trackDesc(track)); - return args; - }); + return mopidy.playback.getCurrentTrack() + .done(function (track) { + console.log("Now playing:", trackDesc(track)); + return args; + }); }; - var queueAndPlayFirstPlaylist = function () { + var queueAndPlay = function (playlistNum, trackNum) { + playlistNum = playlistNum || 0; + trackNum = trackNum || 0; mopidy.playlists.getPlaylists() // => list of Playlists - .then(getFirst, consoleError) + .fold(get, playlistNum) // => Playlist - .then(printTypeAndName, consoleError) + .then(printTypeAndName) // => Playlist - .then(extractTracks, consoleError) + .fold(get, 'tracks') // => list of Tracks - .then(mopidy.tracklist.add, consoleError) + .then(mopidy.tracklist.add) // => list of TlTracks - .then(getFirst, consoleError) + .fold(get, trackNum) // => TlTrack - .then(mopidy.playback.play, consoleError) + .then(mopidy.playback.play) // => null - .then(printNowPlaying, consoleError); + .done(printNowPlaying, console.error.bind(console)); }; var mopidy = new Mopidy(); // Connect to server mopidy.on(console.log.bind(console)); // Log all events - mopidy.on("state:online", queueAndPlayFirstPlaylist); + mopidy.on("state:online", queueAndPlay); 9. The web page should now queue and play your first playlist every time your load it. See the browser's console for output from the function, any errors,