Permalink
Browse files

Support Promises/callbacks from request handlers (#71)

  • Loading branch information...
1 parent 7266a47 commit 04678a37c6a5185a08e1f36980aede94881d9e0d @ajcrites ajcrites committed Jan 27, 2017
Showing with 194 additions and 21 deletions.
  1. +4 −0 .eslintrc.yaml
  2. +63 −21 index.js
  3. +66 −0 test/test_alexa_app_intent_request.js
  4. +61 −0 test/test_alexa_app_launch_request.js
View
@@ -7,6 +7,10 @@ env:
globals:
require: false
module: false
+ describe: true
+ context: true
+ beforeEach: true
+ it: true
rules:
semi: 2
no-console: 0
View
@@ -345,6 +345,44 @@ alexa.app = function(name, endpoint) {
return new Promise(function(resolve, reject) {
var request = new alexa.request(request_json);
var response = new alexa.response(request.getSession());
+
+ // Error handling when a request fails in any way
+ var handleError = function (e) {
+ if (typeof self.error == "function") {
+ self.error(e, request, response);
+ } else if (typeof e == "string" && self.messages[e]) {
+ response.say(self.messages[e]);
+ response.send(e);
+ }
+ if (!response.resolved) {
+ if (e.message) {
+ response.fail("Unhandled exception: " + e.message + ".", e);
+ } else {
+ response.fail("Unhandled exception.", e);
+ }
+ }
+ };
+
+ // Prevent callback handler (request resolution) from being called
+ // multiple times
+ var callbackHandlerCalled = false;
+ // Sends the request or handles an error if an error is passed in
+ // to the callback.
+ var callbackHandler = function (e) {
+ if (callbackHandlerCalled) {
+ console.warn("Response has already been sent");
+ return;
+ }
+ callbackHandlerCalled = true;
+
+ if (e) {
+ handleError(e);
+ }
+ else {
+ response.send();
+ }
+ };
+
var postExecuted = false;
// Attach Promise resolve/reject functions to the response object
response.send = function(exception) {
@@ -378,32 +416,48 @@ alexa.app = function(name, endpoint) {
if ("IntentRequest" === requestType) {
var intent = request_json.request.intent.name;
if (typeof self.intents[intent] != "undefined" && typeof self.intents[intent]["function"] == "function") {
- if (false !== self.intents[intent]["function"](request, response)) {
- response.send();
+ var intentResult = self.intents[intent]["function"](request, response, callbackHandler);
+ if (intentResult instanceof Promise) {
+ intentResult.asCallback(callbackHandler);
+ }
+ else if (false !== intentResult) {
+ callbackHandler();
}
} else {
throw "NO_INTENT_FOUND";
}
} else if ("LaunchRequest" === requestType) {
if (typeof self.launchFunc == "function") {
- if (false !== self.launchFunc(request, response)) {
- response.send();
+ var launchResult = self.launchFunc(request, response, callbackHandler);
+ if (launchResult instanceof Promise) {
+ launchResult.asCallback(callbackHandler);
+ }
+ else if (false !== launchResult) {
+ callbackHandler();
}
} else {
throw "NO_LAUNCH_FUNCTION";
}
} else if ("SessionEndedRequest" === requestType) {
if (typeof self.sessionEndedFunc == "function") {
- if (false !== self.sessionEndedFunc(request, response)) {
- response.send();
+ var sessionEndedResult = self.sessionEndedFunc(request, response, callbackHandler);
+ if (sessionEndedResult instanceof Promise) {
+ sessionEndedResult.asCallback(callbackHandler);
+ }
+ else if (false !== sessionEndedResult) {
+ callbackHandler();
}
}
} else if (requestType && 0 === requestType.indexOf("AudioPlayer.")) {
var event = requestType.slice(12);
var eventHandlerObject = self.audioPlayerEventHandlers[event];
if (typeof eventHandlerObject != "undefined" && typeof eventHandlerObject["function"] == "function") {
- if (false !== eventHandlerObject["function"](request, response)) {
- response.send();
+ var eventHandlerResult = eventHandlerObject["function"](request, response, callbackHandler);
+ if (eventHandlerObject instanceof Promise) {
+ eventHandlerResult.asCallback(callbackHandler);
+ }
+ else if (false !== eventHandlerResult) {
+ callbackHandler();
}
} else {
response.send();
@@ -413,19 +467,7 @@ alexa.app = function(name, endpoint) {
}
}
} catch (e) {
- if (typeof self.error == "function") {
- self.error(e, request, response);
- } else if (typeof e == "string" && self.messages[e]) {
- response.say(self.messages[e]);
- response.send(e);
- }
- if (!response.resolved) {
- if (e.message) {
- response.fail("Unhandled exception: " + e.message + ".", e);
- } else {
- response.fail("Unhandled exception.", e);
- }
- }
+ handleError(e);
}
});
};
@@ -3,6 +3,7 @@
var chai = require("chai");
var chaiAsPromised = require("chai-as-promised");
var mockHelper = require("./helpers/mock_helper");
+var Promise = require("bluebird");
chai.use(chaiAsPromised);
var expect = chai.expect;
chai.config.includeStack = true;
@@ -203,6 +204,46 @@ describe("Alexa", function() {
});
});
+ it("responds with expected message for callback", function() {
+ var intentHandler = function(req, res, cb) {
+ res.say(expectedMessage);
+ cb();
+ return false;
+ };
+
+ testApp.intent("airportInfoIntent", {}, intentHandler);
+
+ var subject = testApp.request(mockRequest);
+ subject = subject.then(function(response) {
+ return response.response.outputSpeech;
+ });
+
+ return expect(subject).to.eventually.become({
+ ssml: "<speak>" + expectedMessage + "</speak>",
+ type: "SSML"
+ });
+ });
+
+ it("responds with expected message for promise", function() {
+ var intentHandler = function(req, res) {
+ return Promise.resolve().then(function() {
+ res.say(expectedMessage);
+ });
+ };
+
+ testApp.intent("airportInfoIntent", {}, intentHandler);
+
+ var subject = testApp.request(mockRequest);
+ subject = subject.then(function(response) {
+ return response.response.outputSpeech;
+ });
+
+ return expect(subject).to.eventually.become({
+ ssml: "<speak>" + expectedMessage + "</speak>",
+ type: "SSML"
+ });
+ });
+
it("retrieves a slot value", function() {
testApp.intent("airportInfoIntent", {}, function(req, res) {
res.say(req.slot("AirportCode"));
@@ -282,6 +323,31 @@ describe("Alexa", function() {
return expect(subject).to.be.rejectedWith("Unhandled exception: whoops.");
});
});
+
+ context("when an error is passed to the callback", function() {
+ it("reports failure", function() {
+ testApp.intent("airportInfoIntent", {},
+ function(req, res, cb) {
+ cb(new Error("whoops"));
+ return false;
+ });
+
+ var subject = testApp.request(mockRequest);
+ return expect(subject).to.be.rejectedWith("Unhandled exception: whoops.");
+ });
+ });
+
+ context("when a response promise is rejected", function() {
+ it("reports failure", function() {
+ testApp.intent("airportInfoIntent", {},
+ function(req, res) {
+ return Promise.reject(new Error("whoops"));
+ });
+
+ var subject = testApp.request(mockRequest);
+ return expect(subject).to.be.rejectedWith("Unhandled exception: whoops.");
+ });
+ });
});
});
});
@@ -3,6 +3,7 @@
var chai = require("chai");
var chaiAsPromised = require("chai-as-promised");
var mockHelper = require("./helpers/mock_helper");
+var Promise = require("bluebird");
chai.use(chaiAsPromised);
var expect = chai.expect;
chai.config.includeStack = true;
@@ -102,6 +103,66 @@ describe("Alexa", function() {
type: "SSML"
});
});
+
+ it("responds with expected message for callback", function() {
+ testApp.launch(function(req, res, cb) {
+ res.say(expectedMessage);
+
+ cb();
+
+ return false;
+ });
+
+ var subject = testApp.request(mockRequest).then(function(response) {
+ return response.response.outputSpeech;
+ });
+
+ return expect(subject).to.eventually.become({
+ ssml: "<speak>" + expectedMessage + "</speak>",
+ type: "SSML"
+ });
+ });
+
+ it("handles error for callback", function() {
+ testApp.launch(function(req, res, cb) {
+ res.say(expectedMessage);
+
+ cb(new Error("callback failure"));
+
+ return false;
+ });
+
+ var subject = testApp.request(mockRequest);
+
+ return expect(subject).to.be.rejectedWith("callback failure");
+ });
+
+ it("responds with expected message for promise", function() {
+ testApp.launch(function(req, res) {
+ return Promise.resolve().then(function() {
+ res.say(expectedMessage);
+ });
+ });
+
+ var subject = testApp.request(mockRequest).then(function(response) {
+ return response.response.outputSpeech;
+ });
+
+ return expect(subject).to.eventually.become({
+ ssml: "<speak>" + expectedMessage + "</speak>",
+ type: "SSML"
+ });
+ });
+
+ it("handles error for promise", function() {
+ testApp.launch(function(req, res) {
+ return Promise.reject(new Error("promise failure"));
+ });
+
+ var subject = testApp.request(mockRequest);
+
+ return expect(subject).to.be.rejectedWith("promise failure");
+ });
});
});
});

0 comments on commit 04678a3

Please sign in to comment.