SAP HANA XS Advanced - Asynchronous Non-Blocking I/O within Node.js SAP HANA applications
you will learn about the fundaments of the asynchronous nature of Node.js You will so see how this asynchronous capability allows for non-blocking input and output. This technique is one of the basic things that makes Node.js development different from other JavaScript development and also creates one of the reasons for its growing popularity. We will see how these techniques are applied to common operations like HTTP web service calls or even SAP HANA database access.
Prerequisites
- This tutorial is designed for SAP HANA on premise and SAP HANA, express edition. It is not designed for SAP HANA Cloud.
- Proficiency: Intermediate
- Tutorials: HANA Database Access from Node.js
- Step 1
Return to the Node.js module and the
index.js
source file. Add a second module require statement for./exerciseAsync
JavaScriptCopy/*eslint no-console: 0, no-unused-vars: 0, no-undef:0, no-process-exit:0*/ /*eslint-env node, es6 */ "use strict"; module.exports = (app, server) => { app.use("/node", require("./routes/myNode")()); app.use("/node/excAsync", require("./routes/exerciseAsync")(server)); app.use( (err, req, res, next) => { console.error(JSON.stringify(err)); res.status(500).send(`System Error ${JSON.stringify(err)}`); }); };
- Step 2
From the code you have just added, you can guess you will need a new file called
exerciseAsync.js
inside yourroutes
folder. This is just a test framework.JavaScriptCopy/*eslint no-console: 0, no-unused-vars: 0, new-cap:0 */ /*eslint-env node, es6 */ "use strict"; var express = require("express"); var WebSocketServer = require("ws").Server; module.exports = function (server) { var app = express.Router(); const asyncLib = require(global.__base + "async/async.js"); const dbAsync = require(global.__base + "async/databaseAsync.js"); const dbAsync2 = require(global.__base + "async/databaseAsync2.js"); const fileSync = require(global.__base + "async/fileSync.js"); const fileAsync = require(global.__base + "async/fileAsync.js"); const httpClient = require(global.__base + "async/httpClient.js"); app.use((req, res) => { var output = `<H1>Asynchronous Examples</H1></br> <a href="/exerciseAsync">/exerciseAsync</a> - Test Framework for Async Examples</br>` + require(global.__base + "utils/exampleTOC").fill(); res.type("text/html").status(200).send(output); }); try { var wss = new WebSocketServer({ //server: server noServer: true }); server.on("upgrade", function upgrade(request, socket, head) { const url = require("url"); const pathname = url.parse(request.url).pathname; if (pathname === "/node/excAsync") { wss.handleUpgrade(request, socket, head, function done(ws) { wss.emit("connection", ws, request); }); } }); wss.broadcast = (data) => { var message = JSON.stringify({ text: data }); wss.clients.forEach(function each(client) { try { client.send(message, function ack(error) { if (typeof error !== "undefined") { console.log(`Send Error: ${error.toString()}`); } }); } catch (e) { console.log(`Broadcast Error: ${e.toString()}`); } }); console.log(`Sent: ${message}`); }; wss.on("error", (error) => { console.log(`Web Socket Server Error: ${error.toString()}`); }); wss.on("connection", (ws) => { console.log("Connected"); ws.on("message", (message) => { console.log(`Received: ${message}`); var data = JSON.parse(message); switch (data.action) { case "async": asyncLib.asyncDemo(wss); break; case "fileSync": fileSync.fileDemo(wss); break; case "fileAsync": fileAsync.fileDemo(wss); break; case "httpClient": httpClient.callService(wss); break; case "dbAsync": dbAsync.dbCall(wss); break; case "dbAsync2": dbAsync2.dbCall(wss); break; default: wss.broadcast(`Error: Undefined Action: ${data.action}`); break; } }); ws.on("close", () => { console.log("Closed"); }); ws.on("error", (error) => { console.log(`Web Socket Error: ${error.toString()}`); }); ws.send(JSON.stringify({ text: "Connected to Exercise 3" }), function ack(error) { if (typeof error !== "undefined") { console.log(`Send Error: ${error.toString()}`); } }); }); } catch (e) { console.log(`General Error: ${e.toString()}`); } return app; };
- Step 3
You will create a quick web user interface within your existing web module to use your test framework with. Create a new folder called
exerciseAsync
in yourweb->resources
file.The first file will be the
Component.js
, with the following code:JavaScriptCopy/*eslint no-console: 0, no-unused-vars: 0, no-use-before-define: 0, no-redeclare: 0*/ sap.ui.define([ "sap/ui/core/UIComponent" ], function (UIComponent) { "use strict"; return UIComponent.extend("sap.xs.exerciseAsync.Component", { metadata: { manifest: "json" }, init: function () { sap.ui.core.UIComponent.prototype.init.apply( this, arguments); // Chat Model var oModel = this.getModel("chatModel"); oModel.setData({ chat: "", message: "" }); }, destroy: function () { // call the base component's destroy function UIComponent.prototype.destroy.apply(this, arguments); } }); });
Create the
manifest.json
file. You would generally declare the internationalization model here (i18n) but you will hard code the texts in English as it is not the focus of this exercise.JSONCopy{ "_version": "1.4.0", "start_url": "index.html", "sap.app": { "_version": "1.4.0", "type": "application", "resources": "resources.json", "i18n": "i18n/i18n.properties", "id": "exerciseAsync", "title": "{{appTitle}}", "description": "{{appDescription}}", "applicationVersion": { "version": "${project.version}" } }, "sap.fiori": { "_version": "2.0.0", "registrationIds": [], "archeType": "transactional" }, "sap.ui": { "_version": "1.60.0", "technology": "UI5", "icons": { "icon": "/images/favicon.ico", "favIcon": "/images/favicon.ico" }, "deviceTypes": { "desktop": true, "tablet": true, "phone": true }, "supportedThemes": [ "sap_belize", "sap_belize_plus" ] }, "sap.ui5": { "config": { "sapFiori2Adaptation": true }, "rootView": { "viewName": "sap.xs.exerciseAsync.view.App", "type": "XML", "id": "app", "async": true }, "dependencies": { "minUI5Version": "1.60.0", "libs": { "sap.ui.core": { "minVersion": "1.60.0" }, "sap.m": { "minVersion": "1.60.0" }, "sap.ui.layout": { "minVersion": "1.60.0" } } }, "contentDensities": { "compact": true, "cozy": true }, "handleValidation": true, "models": { "chatModel": { "type": "sap.ui.model.json.JSONModel", "settings": { "defaultBindingMode": "TwoWay" } }, "config": { "type": "sap.ui.model.json.JSONModel" }, "i18n": { "type": "sap.ui.model.resource.ResourceModel", "settings": { "bundleUrl": "./i18n/i18n.properties" } } } } }
Create an
index.html
file with the following content:htmlCopy<!DOCTYPE html> <html> <head> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <!-- <script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js" --> <script id="sap-ui-bootstrap" src="{{{sapui5_sb.url}}}/resources/sap-ui-core.js" data-sap-ui-theme="sap_belize_plus" data-sap-ui-xx-bindingSyntax="complex" data-sap-ui-compatVersion="edge" data-sap-ui-preload="async" data-sap-ui-language="en" data-sap-ui-resourceroots='{ "sap.xs.exerciseAsync": "./", "view": "./view" }' data-sap-ui-libs="sap.m,sap.ui.comp,sap.ui.core,sap.ui.layout,sap.ui.unified"> </script> <script type="text/javascript" src="../common/startup.js"></script> <script> localShellStartup("sap.xs.exerciseAsync"); </script> </head> <body class="sapUiBody" role="application"> <div id="content"></div> </body> </html>
You will now create the views and controller in their corresponding folders within the
exerciseAsync
folder and the following code, respectively. The name of the components is referenced asApp
as you can see in the root view specified inmanifest.json
view/App.view.xml
XMLCopy<mvc:View controllerName="sap.xs.exerciseAsync.controller.App" xmlns="sap.m" xmlns:l="sap.ui.layout" xmlns:mvc="sap.ui.core.mvc" height="100%"> <Page title="{i18n>appTitle}"> <content> <ScrollContainer height="100%" width="100%" horizontal="true" vertical="true"> <Panel headerText="Node.js Async Test Framework" expandable="true" expanded="true"> <l:VerticalLayout class="sapUiContentPadding" width="100%"> <l:content> <TextArea id="chatInfo" value="{chatModel>/chat}" cols="60" rows="8" editable="false"/> </l:content> </l:VerticalLayout> <Button text="Basic Async" press="sendBasic"/> <Button text="File Sync" press="sendFileS"/> <Button text="File Async" press="sendFileA"/> <Button text="HTTP Client" press="sendHTTP"/> <Button text="DB1" press="sendDB1"/> <Button text="DB2" press="sendDB2"/> </Panel> </ScrollContainer> </content> </Page> </mvc:View>
controller/App.controller.js
:JavaScriptCopy/*eslint no-console: 0, no-unused-vars: 0, no-use-before-define: 0, no-redeclare: 0, no-undef: 0, no-sequences: 0, no-unused-expressions: 0*/ //To use a javascript controller its name must end with .controller.js sap.ui.define([ "sap/xs/exerciseAsync/controller/BaseController", "sap/ui/model/json/JSONModel" ], function(BaseController, JSONModel) { "use strict"; jQuery.sap.require("sap.ui.core.ws.WebSocket"); var connection = new sap.ui.core.ws.WebSocket("/node/excAsync"); return BaseController.extend("sap.xs.exerciseAsync.controller.App", { onInit: function() { this.getView().addStyleClass("sapUiSizeCompact"); // make everything inside this View appear in Compact mode // connection opened connection.attachOpen(function(oControlEvent) { sap.m.MessageToast.show("connection opened"); }); // server messages connection.attachMessage(function(oControlEvent) { var oModel = sap.ui.getCore().getComponent("comp").getModel("chatModel"); var result = oModel.getData(); var data = jQuery.parseJSON(oControlEvent.getParameter("data")); var msg = data.text, lastInfo = result.chat; if (lastInfo.length > 0) { lastInfo += "\r\n"; } oModel.setData({ chat: lastInfo + msg }, true); // scroll to textarea bottom to show new messages // $("#comp---app--chatInfo-inner").scrollTop($("#comp---app--chatInfo-inner")[0].scrollHeight); }); // error handling connection.attachError(function(oControlEvent) { sap.m.MessageToast.show("Websocket connection error"); }); // onConnectionClose connection.attachClose(function(oControlEvent) { sap.m.MessageToast.show("Websocket connection closed"); }); }, // send message sendBasic: function() { var oModel = this.getOwnerComponent().getModel("chatModel"); oModel.setData({ chat: "" }, true); connection.send(JSON.stringify({ action: "async" })); }, sendFileS: function() { var oModel = this.getOwnerComponent().getModel("chatModel"); oModel.setData({ chat: "" }, true); connection.send(JSON.stringify({ action: "fileSync" })); }, sendFileA: function() { var oModel = this.getOwnerComponent().getModel("chatModel"); oModel.setData({ chat: "" }, true); connection.send(JSON.stringify({ action: "fileAsync" })); }, sendHTTP: function() { var oModel = this.getOwnerComponent().getModel("chatModel"); oModel.setData({ chat: "" }, true); connection.send(JSON.stringify({ action: "httpClient" })); }, sendDB1: function() { var oModel = this.getOwnerComponent().getModel("chatModel"); oModel.setData({ chat: "" }, true); connection.send(JSON.stringify({ action: "dbAsync" })); }, sendDB2: function() { var oModel = this.getOwnerComponent().getModel("chatModel"); oModel.setData({ chat: "" }, true); connection.send(JSON.stringify({ action: "dbAsync2" })); }, onErrorCall: function(oError) { if (oError.statusCode === 500 || oError.statusCode === 400 || oError.statusCode === "500" || oError.statusCode === "400") { var errorRes = JSON.parse(oError.responseText); if (!errorRes.error.innererror) { sap.m.MessageBox.alert(errorRes.error.message.value); } else { if (!errorRes.error.innererror.message) { sap.m.MessageBox.alert(errorRes.error.innererror.toString()); } else { sap.m.MessageBox.alert(errorRes.error.innererror.message); } } return; } else { sap.m.MessageBox.alert(oError.response.statusText); return; } } }); });
controller/BaseController.controller.js
:JavaScriptCopy/*global history */ sap.ui.define([ "sap/ui/core/mvc/Controller", "sap/ui/core/routing/History" ], function (Controller, History) { "use strict"; return Controller.extend("sap.xs.exerciseAsync.controller.BaseController", { /** * Convenience method for accessing the router in every controller of the application. * @public * @returns {sap.ui.core.routing.Router} the router for this component */ getRouter : function () { return this.getOwnerComponent().getRouter(); }, /** * Convenience method for getting the view model by name in every controller of the application. * @public * @param {string} sName the model name * @returns {sap.ui.model.Model} the model instance */ getModel : function (sName) { return this.getView().getModel(sName); }, /** * Convenience method for setting the view model in every controller of the application. * @public * @param {sap.ui.model.Model} oModel the model instance * @param {string} sName the model name * @returns {sap.ui.mvc.View} the view instance */ setModel : function (oModel, sName) { return this.getView().setModel(oModel, sName); }, /** * Convenience method for getting the resource bundle. * @public * @returns {sap.ui.model.resource.ResourceModel} the resourceModel of the component */ getResourceBundle : function () { return this.getOwnerComponent().getModel("i18n").getResourceBundle(); }, /** * Event handler for navigating back. * It there is a history entry we go one step back in the browser history * If not, it will replace the current entry of the browser history with the master route. * @public */ onNavBack : function() { var sPreviousHash = History.getInstance().getPreviousHash(); if (sPreviousHash !== undefined) { history.go(-1); } else { this.getRouter().navTo("master", {}, true); } } }); } );
Now we need to create one reusable JavaScript library that will be utilized by several frontend applications. In your web/resources folder create a new folder named common. Within the common folder create a file named startup.js.
web/resources/common/startup.js
JavaScriptCopy/*eslint no-console: 0, no-unused-vars: 0, no-use-before-define: 0, no-redeclare: 0, no-shadow:0*/ function onLoadSession(myJSON) { try { var result = JSON.parse(myJSON); if (result.session.length > 0) { if (result.session[0].familyName !== "") { return result.session[0].givenName + " " + result.session[0].familyName; } else { return result.session[0].UserName; } } } catch (e) { return ""; } return ""; } function getSessionInfo() { var aUrl = "/node/getSessionInfo"; return onLoadSession( jQuery.ajax({ url: aUrl, method: "GET", dataType: "json", async: false }).responseText); } function localShellStartup(name) { sap.ui.getCore().attachInit(function () { var ComponentContainer = new sap.ui.core.ComponentContainer({ height: "100%" }); var username = "Test User"; // create a shell new sap.ui.unified.Shell({ id: "myShell", icon: "/images/sap_18.png", headEndItems: new sap.ui.unified.ShellHeadItem({ icon: "sap-icon://log", tooltip: "Logoff", press: function () { window.location.href = "/my/logout"; } }), user: new sap.ui.unified.ShellHeadUserItem({ image: "sap-icon://person-placeholder", username: username }), content: ComponentContainer }).placeAt("content"); var oComponent = sap.ui.component({ id: "comp", name: name, manifestFirst: true, async: true }).then(function (oComponent) { ComponentContainer.setComponent(oComponent); }); }); }
- Step 4
Add the following under the
authenticationMethod
inxs-app.json
:"websockets": { "enabled": true },
- Step 5
Create a folder under
core_node
calledasync
. This is where you will create the rest of the files in this section.You will create 7 files. Use the following names and the code snippets to create them within the
core_node->async
folder:async.js
JavaScriptCopy/*eslint-env node, es6 */ "use strict"; module.exports = { asyncDemo: (wss) => { wss.broadcast("Start"); setTimeout(() => { wss.broadcast("Wait Timer Over"); }, 3000); wss.broadcast("End"); } };
database.js
JavaScriptCopy/*eslint no-console: 0, no-unused-vars: 0, no-undef: 0, no-shadow: 0*/ "use strict"; var hdb = require("@sap/hdbext"); var xsenv = require("@sap/xsenv"); var async = require("async"); var hanaOptions = xsenv.getServices({ hana: { plan: "hdi-shared" } }); hanaOptions.hana.pooling = true; module.exports = { callHANA: function (wss) { hdb.createConnection(hanaOptions.hana, function (error, client) { if (error) { console.error(error); } if (client) { wss.broadcast("Database Connected"); client.exec("select TOP 25 * from \"PurchaseOrder.Header\"", function (err, res, cb) { if (err) { return ("ERROR: " + err); } wss.broadcast("Database Call Complete"); for (var i = 0; i < res.length; i++) { wss.broadcast(res[i].PURCHASEORDERID + ": " + res[i].GROSSAMOUNT + "\n"); } client.disconnect(function (cb) { wss.broadcast("Database Disconnected"); // pool.release(client); }); return null; }); } //End if client }); //end create connection cb(); }, //end callHANA callHANA1: function (cb, wss) { hdb.createConnection(hanaOptions.hana, function (error, client) { if (error) { console.error(error); } if (client) { async.waterfall([ function execute(callback) { wss.broadcast("Database Connected #1"); client.exec("select TOP 25 * from \"PurchaseOrder.Header\"", function (err, res) { if (err) { return ("ERROR: " + err); } callback(null, err, res); return null; }); return null; }, function processResults(err, res, callback) { if (err) { return ("ERROR: " + err); } wss.broadcast("Database Call #1"); wss.broadcast("--PO Header"); for (var i = 0; i < res.length; i++) { wss.broadcast(res[i].PURCHASEORDERID + ": " + res[i].GROSSAMOUNT); } wss.broadcast("\n"); client.disconnect(); wss.broadcast("Database Disconnected #1"); wss.broadcast("End Waterfall #1"); //pool.release(client); cb(); return null; }, function disconnectDone(callback) { wss.broadcast("Database Disconnected #1"); wss.broadcast("End Waterfall #1"); // pool.release(client); cb(); } ], function (err, result) { wss.broadcast(err || "done"); wss.broadcast("Error Occurred disrupting flow of Waterfall for #1"); // pool.release(client); cb(); }); //end Waterfall } //end if client }); //end create connection }, //end callHANA1 callHANA2: function (cb, wss) { hdb.createConnection(hanaOptions.hana, function (error, client) { if (error) { console.error(error); } if (client) { async.waterfall([ function execute(callback) { wss.broadcast("Database Connected #2"); client.exec("select TOP 25 * from \"PurchaseOrder.Item\"", function (err, res) { if (err) { return ("ERROR: " + err); } callback(null, err, res); return null; }); }, function processResults(err, res, callback) { if (err) { return ("ERROR: " + err); } wss.broadcast("Database Call #2"); wss.broadcast("--PO Items"); for (var i = 0; i < res.length; i++) { wss.broadcast(res[i]["POHeader.PURCHASEORDERID"] + ": " + res[i]["PRODUCT"]); } wss.broadcast("\n"); client.disconnect(); wss.broadcast("Database Disconnected #2"); wss.broadcast("End Waterfall #2"); // pool.release(client); cb(); return null; }, function disconnectDone(callback) { wss.broadcast("Database Disconnected #2"); wss.broadcast("End Waterfall #2"); // pool.release(client); cb(); } ], function (err, result) { wss.broadcast(err || "done"); wss.broadcast("Error Occurred disrupting flow of Waterfall for #2"); // pool.release(client); cb(); }); //end Waterfall } //end if client }); //end create connection } //end callHANA2 };
databaseAsync.js
JavaScriptCopy/*eslint-env node, es6 */ "use strict"; var hana = require("./database"); module.exports = { dbCall: (wss) => { function dummy() {} wss.broadcast("Before Database Call"); hana.callHANA1(dummy, wss); hana.callHANA2(dummy, wss); wss.broadcast("After Database Call"); } };
fileAsync.js
JavaScriptCopy/*eslint-env node, es6 */ "use strict"; var fs = require("fs"); module.exports = { fileDemo: (wss) => { fs.readFile("./async/file.txt", "utf8", (error, text) => { wss.broadcast(text); }); wss.broadcast("After First Read\n"); fs.readFile("./async/file2.txt", "utf8", (error, text) => { wss.broadcast(text); }); wss.broadcast("After Second Read\n"); } };
fileSync.js
JavaScriptCopy/*eslint-env node, es6 */ "use strict"; var fs = require("fs"); module.exports = { fileDemo: (wss) => { var text = fs.readFileSync("./async/file.txt", "utf8"); wss.broadcast(text); wss.broadcast("After First Read\n"); text = fs.readFileSync("./async/file2.txt", "utf8"); wss.broadcast(text); wss.broadcast("After Second Read\n"); } };
httpClient.js
JavaScriptCopy/*eslint-env node, es6 */ "use strict"; var http = require("http"); module.exports = { callService: (wss) => { wss.broadcast("Before HTTP Call\n"); try { http.get({ path: "http://www.loc.gov/pictures/search/?fo=json&q=SAP&", host: "www.loc.gov", port: "80", headers: { host: "www.loc.gov" } }, (response) => { response.setEncoding("utf8"); response.on("data", (data) => { wss.broadcast(data.substring(0, 100)); }); response.on("error", wss.broadcast); }); } catch (err) { wss.broadcast(err.toString()); } wss.broadcast("After HTTP Call\n"); } };
databaseAsync2.js
JavaScriptCopy/*eslint no-console: 0, no-unused-vars: 0, no-undef: 0, no-shadow: 0*/ /*eslint-env node, es6 */ "use strict"; var hana = require("./database"); var async = require("async"); module.exports = { dbCall: (wss) => { async.parallel([ function(cb) { wss.broadcast("Before Database Call"); cb(); }, function(cb) { hana.callHANA1(cb, wss); }, function(cb) { hana.callHANA2(cb, wss); }, function(cb) { wss.broadcast("After Database Call"); cb(); } ], function(err) { wss.broadcast("---Everything's Really Done Now. Go Home!---"); }); } };
- Step 6
Take a look at the code you have just created. You will notice the file modules are using two text files. Create them inside the
async
folder using simple lines likefile.txt
withThis is my first file
andfile2.txt
withThis is my second file
- Step 7
You can now run the
core_node
moduleYou should see that the build and deploy was successful and that the application is running. Also run the web module
In the running tab, you should see the
index.html
with theHello World
button from earlier.Now change the path in the browser to
/exerciseAsync
. You should see the test framework for this exercise. - Step 8
What do you expect this code will output? From many other programming languages you would expect sequential processing and therefore the End output wouldn’t come until after the timer expired. However part of the power of Node.js is asynchronous non-blocking execution of many core elements. In test UI, press the Basic
Async
button.Perhaps a timer seemed like an obvious asynchronous operation. However this asynchronous nature of Node.js is often used when programs must wait on input or output. When asynchronous processing is applied to these operations, you can keep from blocking execution of other logic while you wait on things like file access, HTTP requests or even database query execution. Now look at the difference between synchronous and asynchronous file operations.
The
fileSync.js
is using thefs
library and the functionreadFileSync
to read each of the two text files. After each read operation output there is a message.JavaScriptCopy/*eslint-env node, es6 */ "use strict"; var fs = require("fs"); module.exports = { fileDemo: (wss) => { var text = fs.readFileSync("./async/file.txt", "utf8"); wss.broadcast(text); wss.broadcast("After First Read\n"); text = fs.readFileSync("./async/file2.txt", "utf8"); wss.broadcast(text); wss.broadcast("After Second Read\n"); } };
Test your
fileSync.js
from the UI test tool. As you might expect, everything is output in exactly the same order as the lines of code were listed in the application because all operations were synchronous. Program execution didn’t continue until each read operation had completely finished. - Step 9
Now to look at
fileAsync.js
. Notice that the message output now is embedded as an in-line callback function. It doesn’t get executed until the read operation is complete, but the rest of the program flow continues and isn’t blocked by the file operation.JavaScriptCopy/*eslint-env node, es6 */ "use strict"; var fs = require("fs"); module.exports = { fileDemo: (wss) => { fs.readFile("./async/file.txt", "utf8", (error, text) => { wss.broadcast(text); }); wss.broadcast("After First Read\n"); fs.readFile("./async/file2.txt", "utf8", (error, text) => { wss.broadcast(text); }); wss.broadcast("After Second Read\n"); } };
Now run
fileAsync.js
from the test UI. The output of this exercise gives us very different results. Both after comments are outputted before either of the file contents. Also if the first file had been significantly larger than the second, it’s possible that the second might have finished and output first. This has powerful implications to how you code applications.Similar to file operations, HTTP requests are another area where your programs must often wait on an external response. In this section you can see how Node.js also makes calling external HTTP services non-blocking. The HTTP library you used in earlier sections can also be used to make HTTP requests. Use the get function of the HTTP library to call to
http://www.loc.gov/pictures/search/?fo=json&q=SAP
. This will call the US Library of Congress Image Search (a REST API which requires no authentication or API Key to keep the exercise simple). Issue a message before and after the HTTP request.JavaScriptCopy/*eslint-env node, es6 */ "use strict"; var http = require("http"); module.exports = { callService: (wss) => { wss.broadcast("Before HTTP Call\n"); try { http.get({ path: "http://www.loc.gov/pictures/search/?fo=json&q=SAP&", host: "www.loc.gov", port: "80", headers: { host: "www.loc.gov" } }, (response) => { response.setEncoding("utf8"); response.on("data", (data) => { wss.broadcast(data.substring(0, 100)); }); response.on("error", wss.broadcast); }); } catch (err) { wss.broadcast(err.toString()); } wss.broadcast("After HTTP Call\n"); } };
- Step 10
Test your
httpClient.js
from the test UI. Similar to the earlier file exercise, the after HTTP call console message is output before the response from the HTTP request.Perhaps most interesting is that this non-blocking concept can also be extended to database access. This allows you to issue multiple requests to the underlying HANA database in parallel and without stopping the processing flow of the JavaScript application logic. Earlier in this section, you learned about making database access to HANA. For this section you’ve already coded the database requests in a reusable module, so you can concentrate on the asynchronous flow. First you have
databaseAsync.js
. This Issues a message, then calls two functions (callHANA1
andcallHANA2
), then issues another message. This will execute two different queries in the HANA database.JavaScriptCopy/*eslint-env node, es6 */ "use strict"; var hana = require("./database"); module.exports = { dbCall: (wss) => { function dummy() {} wss.broadcast("Before Database Call"); hana.callHANA1(dummy, wss); hana.callHANA2(dummy, wss); wss.broadcast("After Database Call"); } };
Test your
databaseAsync.js
from test UI. As you are hopefully learning to expect, the messages you issued after the database requests are actually output first. Only then are the database query results returned. There is also no guarantee that query 1 will finish before query 2. - Step 11
But what if you want more control over the flow of program execution. Maybe you want several database operations to happen in parallel, but then some logic to execute only after all queries are complete. This is one of things the
async
library in Node.js can make easier. IndatabaseAsync2.js
you adjust the logic to use theasync.parallel
function. This allows some of the commands to execute in parallel as before, but then have a sync point once all operations are complete to allow further processing. You will output one final message after everything is done.JavaScriptCopy/*eslint no-console: 0, no-unused-vars: 0, no-undef: 0, no-shadow: 0*/ /*eslint-env node, es6 */ "use strict"; var hana = require("./database"); var async = require("async"); module.exports = { dbCall: (wss) => { async.parallel([ function(cb) { wss.broadcast("Before Database Call"); cb(); }, function(cb) { hana.callHANA1(cb, wss); }, function(cb) { hana.callHANA2(cb, wss); }, function(cb) { wss.broadcast("After Database Call"); cb(); } ], function(err) { wss.broadcast("---Everything's Really Done Now. Go Home!---"); }); } };
Test your
databaseAsync2.js
from the test UI. The execution is similar to before, but now we have the final message after all queries are complete. Because we have a sync point after all parallel execution is complete, we can output this final message after both queries are complete.