diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..a127f3d2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +Copyright (c) 2017 Ably + +Copyright 2016 Ably Real-time Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..489b2700 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node server.js diff --git a/README.md b/README.md index a288919b..47769aef 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,38 @@ -[![Ably](https://s3.amazonaws.com/files.ably.io/logo-with-type.png)](https://www.ably.io) +# [Ably](https://www.ably.io) Ably Reactor Queue and Wolfram Alpha demo ---- +Ably is a hugely scalable, superfast and secure hosted real-time messaging service for web-enabled devices. [Find out more about Ably](https://www.ably.io). -# Tutorials repository +This demo uses realtime pub/sub to publish questions and subscribe to answers, and uses the [Ably Reactor Queues](https://www.ably.io/reactor) to subscribe to realtime data from a worker server over AMQP. When the worker receives a question, it sends the request to Wolfram Alpha to get the answer, and publishes the answer on a channel so that the browser receives it. -This repository contains the working code for many of the [Ably tutorials](https://www.ably.io/tutorials). +Want to try this demo now? Deploy to Heroku for free: -See [https://www.ably.io/tutorials](https://www.ably.io/tutorials) for a complete list of Ably tutorials. The source code for each tutorial exists as a branch in this repo, see [a complete list of branches in this repo](https://github.com/ably/tutorials/branches). +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/ably/tutorials/tree/queue-wolfram-alpha-nodejs) -To find out more Ably and our realtime data delivery platform, visit [https://www.ably.io](https://www.ably.io) +# Setting up the demo on Heroku + +Once the app has been deployed to Heroku using the button above, there a few quick steps needed to get this demo running: + +* [Sign up for a free Developer AppId with Wolfram Alpha](http://developer.wolframalpha.com/) +* Configure an environment variable with the Wolfram AppId (replace `[your wolfram app id]` with the AppId from the previous step): `heroku config:set WOLFRAM_APP_ID=[your wolfram app id] --app [heroku app name you assigned for this demo]` +* Log in to your Ably dashboard `heroku addons:open ably --app [heroku app name you assigned for this demo]` +* Set up a queue (in the Queues tab) with the name `wolfram` in the `US East (Virgina)` area. +* Set up a queue rule (button to add rules is further down the page within the Queues tab) with the following: + * Queue - choose the `wolfram` queue you just set up + * Source - choose "Message" + * Channel Filter - enter `"^wolfram:questions"` to ensure that all questions published to the `wolfram:questions` channel are republished into the `wolfram` queue + +You are now ready to run the demo: `heroku open --app [heroku app name you assigned for this demo]` + +# Ably Reactor + +The Ably Reactor provides Queues to consume realtime data, Events to trigger server-side code or functions in respons to realtime data, and Firehose to stream events to other queue or streaming services. + +[Find out more about the Ably Reactor](https://www.ably.io/reactor) + +# Questions + +Please visit http://support.ably.io/ for access to our knowledgebase and to ask for any assistance. + +# License + +Copyright (c) 2017 Ably Real-time Ltd, Licensed under the Apache License, Version 2.0. Refer to [LICENSE](./LICENSE) for the license terms. diff --git a/app.json b/app.json new file mode 100644 index 00000000..4731bebf --- /dev/null +++ b/app.json @@ -0,0 +1,13 @@ +{ + "name": "Ably Wolfram Queue demo", + "description": "A simple demo to demonstrate using Ably Reactor Queues with Wolfram Alpha", + "repository": "https://github.com/ably/tutorials/tree/queue-wolfram-alpha-nodejs", + "logo": "https://files.ably.io/logo-70x70.png", + "keywords": ["Ably", "AMQP", "Wolfram Alpha", "realtime"], + "success_url": "/post-install.html", + "addons": [ + { + "plan": "ably:test" + } + ] +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..e85c1219 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "ably-tutorials", + "author": "Ably ", + "description": "Node.js Reactor Queue and Wolfram Alpha tutorial", + "repository": "https://github.com/ably/tutorials", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + }, + "scripts": { + "start": "node ./server.js" + }, + "dependencies": { + "ably": ">=0.9.0-beta", + "amqplib": "^0.4", + "express": ">=4.14.0", + "request": ">=2.79.0" + } +} diff --git a/public/app.js b/public/app.js new file mode 100644 index 00000000..5c906d7f --- /dev/null +++ b/public/app.js @@ -0,0 +1,49 @@ +$(function() { + /* Set up realtime library and authenticate using token issued from server */ + var ably = new Ably.Realtime({ authUrl: '/auth' }); + var answersChannel = ably.channels.get('wolfram:answers'); + var questionsChannel = ably.channels.get('wolfram:questions'); + + var $answers = $('#answers'), + $status = $('#status'), + $askButton = $('#ask-btn'), + $question = $('#question'); + + /* Subscribe to answers published on this channel */ + answersChannel.subscribe(function(message) { + var $question = $('
').text(message.data.question); + var $answer = $('
').text(message.data.answer); + var $stat = $('
').text("Wolfram took " + message.data.wolframTime + "ms"); + $answers.prepend($('
').append($stat).append($question).append($answer)); + }); + + $askButton.on('click', function() { + var question = $question.val(); + if (question.replace(' ') != '') { + /* Publish question to the Ably channel so that the queue worker receives it via queues */ + questionsChannel.publish('question', question, function(err) { + if (err) { + showStatus('Failed to publish question!'); + $question.val(question); + return; + } + clearStatus(); + }); + showStatus('Sending question...'); + $question.val(''); + } + }); + + ably.connection.on('connecting', function() { showStatus('Connecting to Ably...'); }); + ably.connection.on('connected', function() { clearStatus(); }); + ably.connection.on('disconnected', function() { showStatus('Disconnected from Ably...'); }); + ably.connection.on('suspended', function() { showStatus('Disconnected from Ably for a while...'); }); + + function showStatus(text) { + $status.text(text).show(); + } + + function clearStatus() { + $status.fadeOut(750); + } +}); diff --git a/public/index.html b/public/index.html new file mode 100644 index 00000000..e7e34210 --- /dev/null +++ b/public/index.html @@ -0,0 +1,24 @@ + + + Ably Reactor Queue and Wolfram Alpha demo + + + + + + +

Wolfram Alpha

+
+

Questions

+ + +

+ Pressing this button will publish a message to a channel, which will in turn using an Ably Reactor Queue rule to republish the message into a queue. The node.js worker server will then consume this message in real time, send the question to Wolfram Alpha using its REST API, and publish the answer back to the client using an Ably channel. +

+
+
+

Answers

+
+
+ + diff --git a/public/post-install.html b/public/post-install.html new file mode 100644 index 00000000..d000038b --- /dev/null +++ b/public/post-install.html @@ -0,0 +1,40 @@ + + + Ably Reactor Queue and Wolfram Alpha demo + + + + +

Finalizing the setup of this demo

+ +

Great, the app is now installed! However we need to you to follow few quick steps to get this demo running:

+ +
    +
  • Sign up for a free Developer AppId with Wolfram Alpha
  • +
  • Configure an environment variable with the Wolfram AppId (replace [your wolfram app id] with the AppId from the previous step): heroku config:set WOLFRAM_APP_ID=[your wolfram app id] --app [heroku app name you assigned for this demo]
  • +
  • Log in to your Ably dashboard: heroku addons:open ably --app [heroku app name you assigned for this demo]
  • +
  • Set up a queue (in the Queues tab) with the name wolfram in the US East (Virgina) area.
  • +
  • Set up a queue rule (button to add rules is further down the page within the Queues tab) with the following: + +
      +
    • Queue - choose the wolfram queue you just set up
    • +
    • Source - choose "Message"
    • +
    • Channel Filter - enter "^wolfram:questions" to ensure that all questions published to the wolfram:questions channel are published into the wolfram queue
    • +
    +
  • +
+ +

All done!

+ +

+ View the Reactor Queue and Wolfram Alpha Demo » +

+ + + + diff --git a/public/style.css b/public/style.css new file mode 100644 index 00000000..af116c57 --- /dev/null +++ b/public/style.css @@ -0,0 +1,94 @@ +body { + font-family: Arial, Helvetica, sans-serif; + font-size: 0.85em; +} + +h2 { + font-size: 1.2em; +} + +.col { + width: 50%; + float: left; + box-sizing: border-box; + padding: 0 1em; +} + +textarea#question { + width: 100%; + height: 4em; + margin-bottom: 1em; +} + +#answers > div { + box-shadow: 0 0 0.5em #bbb; + margin-bottom: 1.5em; +} + +#answers > div .question { + color: orange; + font-style: italic; + padding: 5px; + float: left; +} + +#answers > div .stat { + color: #999; + font-size: 0.8em; + float: right; + padding: 5px; +} + +#answers > div .answer { + font-weight: bold; + padding: 5px; + clear: both; +} + +#status { + color: orange; + padding-left: 1em; +} + +button { + -moz-box-shadow: 0px 1px 0px 0px #fff6af; + -webkit-box-shadow: 0px 1px 0px 0px #fff6af; + box-shadow: 0px 1px 0px 0px #fff6af; + background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #ffec64), color-stop(1, #ffab23)); + background:-moz-linear-gradient(top, #ffec64 5%, #ffab23 100%); + background:-webkit-linear-gradient(top, #ffec64 5%, #ffab23 100%); + background:-o-linear-gradient(top, #ffec64 5%, #ffab23 100%); + background:-ms-linear-gradient(top, #ffec64 5%, #ffab23 100%); + background:linear-gradient(to bottom, #ffec64 5%, #ffab23 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffec64', endColorstr='#ffab23',GradientType=0); + background-color:#ffec64; + -moz-border-radius:6px; + -webkit-border-radius:6px; + border-radius:6px; + border:1px solid #ffaa22; + display:inline-block; + cursor:pointer; + color:#333333; + font-family:Arial; + font-size:15px; + font-weight:bold; + padding:6px 24px; + text-decoration:none; + text-shadow:0px 1px 0px #ffee66; +} + +button:hover { + background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #ffab23), color-stop(1, #ffec64)); + background:-moz-linear-gradient(top, #ffab23 5%, #ffec64 100%); + background:-webkit-linear-gradient(top, #ffab23 5%, #ffec64 100%); + background:-o-linear-gradient(top, #ffab23 5%, #ffec64 100%); + background:-ms-linear-gradient(top, #ffab23 5%, #ffec64 100%); + background:linear-gradient(to bottom, #ffab23 5%, #ffec64 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffab23', endColorstr='#ffec64',GradientType=0); + background-color:#ffab23; +} + +button:active { + position:relative; + top:1px; +} diff --git a/server.js b/server.js new file mode 100644 index 00000000..4f7fc781 --- /dev/null +++ b/server.js @@ -0,0 +1,42 @@ +const Ably = require('ably'); +const Express = require('express'); +const ServerPort = process.env.PORT || 3000; +const worker = require('./worker'); + +const ApiKey = process.env.ABLY_API_KEY || 'INSERT-YOUR-API-KEY-HERE'; /* Add your API key here */ +if (ApiKey.indexOf('INSERT') === 0) { throw('Cannot run without an Ably API key. Add your key to server.js'); } + +const WolframAppId = process.env.WOLFRAM_APP_ID || 'INSERT-YOUR-WOLFRAM-APP-ID-HERE'; /* Add your Wolfram AppID here */ +if (WolframAppId.indexOf('INSERT') === 0) { console.warn('Cannot run without a Wolfram AppID. Add your AppID to server.js'); } + +/* Instance the Ably library */ +const rest = new Ably.Rest({ key: ApiKey }); + +/* Start a web server */ +const app = Express(); + +/* Issue token requests to browser clients sending a request to the /auth endpoint */ +app.get('/auth', function (req, res) { + rest.auth.createTokenRequest(function(err, tokenRequest) { + if (err) { + res.status(500).send('Error requesting token: ' + JSON.stringify(err)); + } else { + res.setHeader('Content-Type', 'application/json'); + res.send(JSON.stringify(tokenRequest)); + } + }); +}); + +if (WolframAppId.indexOf('INSERT') === 0) { + app.get('/', function (req, res) { + res.status(500).send('WolframAppId is not set. You need to configure an environment variable WOLFRAM_APP_ID'); + }); +} + +/* Server static HTML files from /public folder */ +app.use(Express.static('public')); +app.listen(ServerPort); + +worker.start(ApiKey, WolframAppId, 'wolfram:answers', 'wolfram', 'us-east-1-a-queue.ably.io:5671/shared'); + +console.log('Open the Wolfram demo in your browser: https://localhost:' + ServerPort + '/'); diff --git a/worker.js b/worker.js new file mode 100644 index 00000000..77fb8695 --- /dev/null +++ b/worker.js @@ -0,0 +1,78 @@ +'use strict'; + +const amqp = require('amqplib/callback_api'); +const Ably = require('ably'); +const request = require('request'); +const querystring = require("querystring"); +const WolframEndpoint = 'http://api.wolframalpha.com/v1/result'; + +/* Send question over HTTP to Wolfram to find the answer */ +function getAnswerAndPublish(ablyChannel, wolframUrl, question) { + console.log('worker:', 'Received question', question, ' - about to ask Wolfram'); + var timeNow = Date.now(); + request(wolframUrl, function (error, response, body) { + var timePassed = Date.now() - timeNow; + if (!error && response.statusCode == 200) { + publishAnswer(ablyChannel, question, body, timePassed) + } else { + if (body) { + publishAnswer(ablyChannel, question, "Wolfram couldn't compute: " + body, timePassed); + } else { + publishAnswer(ablyChannel, question, "Wolfram error: " + JSON.stringify(error), timePassed); + } + } + }); +} + +function publishAnswer(ablyChannel, question, answer, wolframTime) { + ablyChannel.publish('answer', { question: question, answer: answer, wolframTime: wolframTime }, function(err) { + if (err) { + console.error('worker:', 'Failed to publish question', question, ' - err:', JSON.stringify(err)); + } + }) +} + +/* Start the worker that consumes from the AMQP QUEUE */ +exports.start = function(apiKey, wolframApiKey, answersChannelName, queueName, queueEndpoint) { + const appId = apiKey.split('.')[0]; + const queue = appId + ":" + queueName; + const endpoint = queueEndpoint; + const url = 'amqps://' + apiKey + '@' + endpoint; + const rest = new Ably.Rest({ key: apiKey }); + const answersChannel = rest.channels.get(answersChannelName); + + /* Connect to Ably queue */ + amqp.connect(url, (err, conn) => { + if (err) { + console.error('worker:', 'Queue error!', err); + return; + } + console.log('worker:', 'Connected to AMQP endpoint', endpoint); + + /* Create a communication channel */ + conn.createChannel((err, ch) => { + if (err) { + console.error('worker:', 'Queue error!', err); + return; + } + console.log('worker:', 'Waiting for messages'); + + /* Wait for messages published to the Ably Reactor queue */ + ch.consume(queue, (item) => { + const decodedEnvelope = JSON.parse(item.content); + + const messages = Ably.Realtime.Message.fromEncodedArray(decodedEnvelope.messages); + messages.forEach(function(message) { + var question = message.data; + var url = WolframEndpoint + '?' + querystring.stringify({ appid: wolframApiKey, i: question}); + getAnswerAndPublish(answersChannel, url, question); + }); + + /* Remove message from queue */ + ch.ack(item); + }); + }); + + conn.on('error', function(err) { console.error('worker:', 'Connection error!', err); }); + }); +};