Compare commits

...

No commits in common. "master" and "gh-pages" have entirely different histories.

30 changed files with 857 additions and 862 deletions

16
.gitignore vendored
View File

@ -1,16 +0,0 @@
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
npm-debug.log
node_modules
activemq-data

20
LICENSE
View File

@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 Lookout
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,77 +0,0 @@
# activepush
Web push service built around ActiveMQ (or any STOMP broker).
ActivePush subscribes to a STOMP broker and relays messages with a specific `push_id` header to subscribed Socket.io clients. The message bodies are opaque to ActivePush, so the service is useful in a variety of applications.
## Usage
Production:
npm install
bin/activepush [environment]
Development:
npm install -d
bin/activepush
Or, to auto-reload on changes:
node-dev activepush.coffee
The optional `environment` argument corresponds to a configuration file in `configure`, which defaults to `development` and overrides values in the "default.yml" configuration file.
By default ActivePush expects a STOMP broker (e.x. ActiveMQ) to be running on `localhost:61613`.
If ActiveMQ is installed you can start it with the following command:
activemq start broker:stomp://localhost:61613
## Configuration
Configuration files are located in the `config` directory. `default.yml` is loaded first, and an environment-specific configuration (defaulting to `development.yml`) overrides the defaults.
The environment can be specified as the first argument the the `activepush` executable, or in the `NODE_ENV` environment variable.
## Production
All HTTP endpoints except those under the "/socket.io" URL should not be exposed externally as they could expose private information (e.g. "/health") or capabilities (e.g. "/send"). Alternatively, those endpoints could easily be moved to a separate port (see commented out code in the `start` method of ActivePush)
Multiple instances of ActivePush can be load-balanced, keeping the following in mind:
* A STOMP message bus rather than a queue should be used to ensure all instances recieve all messages
* Socket.io needs clients to be "pinned" to the same backend across multiple requests (perhaps using cookies or the source IP address), in particular for the XHR polling transport.
## Architecture
The ActivePush class in `activepush.coffee` creates the server using the StompProducer and SocketIOConsumer. This could be made configurable, or even support multiple simultaneous producer/consumers.
The STOMP and WebSocket components are implemented in `consumers.coffee` and `producers.coffee`, respectively. Other producers/consumers could be implemented.
The producer/consumer components share a SubscriptionBroker `subscriptions` object, which is essentially an EventEmitter they can listen or emit `push_id` events to.
ActivePush makes extensive use of "promises" (specifically Q promises, which are compatible with the Promises/A+ standard) in both the application and tests.
## Testing
npm test
Or:
mocha --compilers coffee:coffee-script test
The integration tests assume a STOMP broker is running on `localhost:61613`. ActiveMQ can be started with the following command:
activemq start broker:stomp://localhost:61613
They also assume Selenium/WebDriver is running on `localhost:4444`. If you want to disable the WebDriver tests (they are significantly slower) comment out the last line of `test/integration-webdriver.coffee`, or run:
mocha --compilers coffee:coffee-script test/integration-socketio-client.coffee
`integration-common.coffee` implements the logic of the tests while `integration-socketio-client.coffee` and `integration-webdriver.coffee` implement a common API to create either an in-process `socket.io-client` or a remote WebDriver instance running the demo.html page (which stores messages it receives in `window.messages` for introspection by the test)
Unfortunately there is no easy way to tell when all messages have propagated from the running test to the ActiveMQ queue back to the ActivePush server and through the Socket.io client, so we currently have short delays before testing that messages have been received. This leads to occasional non-deterministic test failures. Increasing the delays reduces the frequency at a cost of longer running tests.
## Contributors
* Tom Robinson <tlrobinson@gmail.com>

View File

@ -1,118 +0,0 @@
Q = require "q"
http = require "http"
express = require "express"
winston = require "winston"
optimist = require "optimist"
{ SubscriptionBroker } = require "./lib/subscription-broker"
{ SocketIOConsumer } = require "./lib/consumers"
{ StompProducer } = require "./lib/producers"
config = exports.config = require "./lib/config"
class exports.ActivePush
constructor: (config) ->
@config = config
@logger = @config.logger or winston
# Configure logging
if @config.logging.file
@logger.remove logger.transports.Console
@logger.add logger.transports.File,
filename: @config.logging.file
@logger.level = (@config.logging.level or "info").toLowerCase()
start: ->
@subscriptions = new SubscriptionBroker()
# Create STOMP producer
@producer = new StompProducer
logger: @logger
subscriptions: @subscriptions
inbox: @config.stomp.inbox
host: @config.stomp.hosts[0].host
port: @config.stomp.hosts[0].port
# Create Socket.io consumer
@app = express()
@server = http.createServer @app
@consumer = new SocketIOConsumer @server,
logger: @logger
subscriptions: @subscriptions
port: @config.http.port
@_initializePrivateEndpoints(@app)
# Put these on another port by doing the following:
# privateApp = express()
# privateApp.listen(@config.http.port + 1)
# @_initializePrivateEndpoints(privateApp)
Q.all([
@consumer.start()
@producer.start()
]).then =>
@
stop: ->
Q.all([
@consumer.stop()
@producer.stop()
])
_initializePrivateEndpoints: (app) ->
# Health endpoint
@app.get "/health", @_healthEndpoint
# Demo page and sending endpoint.
@app.get "/", (req, res) ->
res.sendfile "#{__dirname}/demo.html"
@app.post "/send", express.json(), (req, res) =>
@producer.publish req.body.push_id, req.body.message
res.send 200
_healthEndpoint: (req, res) =>
health =
log:
enabled: true
level: @config.logging.level
filename: @config.logging.file
for component in [@subscriptions, @producer, @consumer]
object = component.getHealth()
health[object.name] = object
delete object.name
res.json health
exports.main = ->
options = optimist
.usage("Start ActivePush server.\nUsage: activepush [OPTIONS] [ENVIRONMENT]")
.boolean(["h", "v"])
.alias("c", "config")
.describe("c", "Specify a configuration file")
.alias("h", "help")
.describe("h", "Show command line options and exit")
.alias("v", "version")
.describe("v", "Show version and exit")
if options.argv.help
options.showHelp()
process.exit()
if options.argv.version
console.log "v" + require("#{__dirname}/package.json").version
process.exit()
configName = options.argv.config or options.argv._[0]
configuration = config.loadConfiguration(configName)
activepush = new exports.ActivePush(configuration)
activepush.start().then ->
activepush.logger.info "Started with environment: #{activepush.config.environment}"
process.on "SIGINT", ->
activepush.logger.info "Shutting down..."
activepush.stop().then ->
process.exit(1)
.done()
.done()
if require.main is module
exports.main()

View File

@ -1,3 +0,0 @@
#!/usr/bin/env coffee
require("../activepush").main()

View File

@ -1,12 +0,0 @@
#!/usr/bin/env coffee
config = require "../lib/config"
{ StompProducer } = require "../lib/producers"
configuration = config.loadConfiguration process.argv[2]
push_id = process.argv[3] or "demo"
message = process.argv[4] or "hello world"
console.log { configuration, push_id, message }
StompProducer.publish configuration.stomp.hosts[0], configuration.stomp.inbox, push_id, message

View File

@ -1,15 +0,0 @@
stomp:
inbox: "/topic/activepush"
hosts:
- host: "localhost"
port: 61613
ssl: false
login: "guest"
passcode: "guest"
logging:
level: "WARN"
file: "log/activepush.log"
http:
port: 9191

View File

@ -1,4 +0,0 @@
logging:
level: "DEBUG"
file: null

View File

@ -1,5 +0,0 @@
http:
port: 6789
logging:
file: null

View File

@ -1,79 +0,0 @@
<html>
<head>
<title>ActivePush Demo Page</title>
</head>
<body>
<div>
<label for="push-id">push id:</label>
<input type="text" name="push-id" id="push-id" value="">
<label for="message">message:</label>
<input type="text" name="message" id="message" value="hello world">
<input type="submit" id="send" value="Send">
</div>
<hr>
<pre id="log"></pre>
<hr>
<pre id="health"></pre>
<script src="/socket.io/socket.io.js"></script>
<script>
var push_id = window.location.hash.slice(1) || "demo"
window.location.hash = "#" + push_id;
// Sample client
var socket = io.connect("http://localhost");
socket.on("connect", function () {
console.log("connected");
socket.emit("subscribe", push_id);
console.log("subscribed to push_id:", push_id);
// Used in integration tests:
window.messages = [];
});
socket.on("disconnect", function () {
console.log("disconnected");
});
socket.on("error", function (error) {
console.log("error", error);
});
socket.on("message", function (data) {
messages.push(data)
console.log("message:", JSON.stringify(data, null, 2));
});
// Demo form
document.getElementById("push-id").value = push_id;
document.getElementById("send").onclick = function() {
var xhr = new XMLHttpRequest();
xhr.open("POST", "send");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({
push_id: document.getElementById("push-id").value,
message: document.getElementById("message").value
}));
}
// Auto-refresh /health
health = document.getElementById("health");
setInterval(function() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/health");
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
health.innerText = xhr.responseText;
}
}
xhr.send();
}, 1000)
// Also pipe log to page for demo purposes
console._log = console.log;
console.log = function() {
console._log.apply(console, arguments);
document.getElementById("log").textContent += new Date() + ": " + Array.prototype.join.call(arguments, " ") + "\n";
}
</script>
</body>
</html>

BIN
images/code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
images/pattern.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
images/tar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
images/top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
images/zip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

154
index.html Normal file
View File

@ -0,0 +1,154 @@
<!doctype html>
<!-- The Time Machine GitHub pages theme was designed and developed by Jon Rohan, on Feb 7, 2012. -->
<!-- Follow him for fun. http://twitter.com/jonrohan. Tail his code on http://github.com/jonrohan -->
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<link rel="stylesheet" href="stylesheets/stylesheet.css" media="screen"/>
<link rel="stylesheet" href="stylesheets/pygment_trac.css"/>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script type="text/javascript" src="javascripts/script.js"></script>
<title>Activepush</title>
<meta name="description" content="A socket.io push service built around ActiveMQ">
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
<div class="wrapper">
<header>
<h1 class="title">Activepush</h1>
</header>
<div id="container">
<p class="tagline">A socket.io push service built around ActiveMQ</p>
<div id="main" role="main">
<div class="download-bar">
<div class="inner">
<a href="https://github.com/lookout/activepush/tarball/master" class="download-button tar"><span>Download</span></a>
<a href="https://github.com/lookout/activepush/zipball/master" class="download-button zip"><span>Download</span></a>
<a href="https://github.com/lookout/activepush" class="code">View Activepush on GitHub</a>
</div>
<span class="blc"></span><span class="trc"></span>
</div>
<article class="markdown-body">
<p>Web push service built around ActiveMQ (or any STOMP broker).</p>
<p>ActivePush subscribes to a STOMP broker and relays messages with a specific <code>push_id</code> header to subscribed Socket.io clients. The message bodies are opaque to ActivePush, so the service is useful in a variety of applications.</p>
<h2>
<a name="usage" class="anchor" href="#usage"><span class="octicon octicon-link"></span></a>Usage</h2>
<p>Production:</p>
<pre><code>npm install
bin/activepush [environment]
</code></pre>
<p>Development:</p>
<pre><code>npm install -d
bin/activepush
</code></pre>
<p>Or, to auto-reload on changes:</p>
<pre><code>node-dev activepush.coffee
</code></pre>
<p>The optional <code>environment</code> argument corresponds to a configuration file in <code>configure</code>, which defaults to <code>development</code> and overrides values in the "default.yml" configuration file.</p>
<p>By default ActivePush expects a STOMP broker (e.x. ActiveMQ) to be running on <code>localhost:61613</code>.</p>
<p>If ActiveMQ is installed you can start it with the following command:</p>
<pre><code>activemq start broker:stomp://localhost:61613
</code></pre>
<h2>
<a name="configuration" class="anchor" href="#configuration"><span class="octicon octicon-link"></span></a>Configuration</h2>
<p>Configuration files are located in the <code>config</code> directory. <code>default.yml</code> is loaded first, and an environment-specific configuration (defaulting to <code>development.yml</code>) overrides the defaults.</p>
<p>The environment can be specified as the first argument the the <code>activepush</code> executable, or in the <code>NODE_ENV</code> environment variable.</p>
<h2>
<a name="production" class="anchor" href="#production"><span class="octicon octicon-link"></span></a>Production</h2>
<p>All HTTP endpoints except those under the "/socket.io" URL should not be exposed externally as they could expose private information (e.g. "/health") or capabilities (e.g. "/send"). Alternatively, those endpoints could easily be moved to a separate port (see commented out code in the <code>start</code> method of ActivePush)</p>
<p>Multiple instances of ActivePush can be load-balanced, keeping the following in mind:</p>
<ul>
<li>A STOMP message bus rather than a queue should be used to ensure all instances recieve all messages</li>
<li>Socket.io needs clients to be "pinned" to the same backend across multiple requests (perhaps using cookies or the source IP address), in particular for the XHR polling transport.</li>
</ul><h2>
<a name="architecture" class="anchor" href="#architecture"><span class="octicon octicon-link"></span></a>Architecture</h2>
<p>The ActivePush class in <code>activepush.coffee</code> creates the server using the StompProducer and SocketIOConsumer. This could be made configurable, or even support multiple simultaneous producer/consumers.</p>
<p>The STOMP and WebSocket components are implemented in <code>consumers.coffee</code> and <code>producers.coffee</code>, respectively. Other producers/consumers could be implemented.</p>
<p>The producer/consumer components share a SubscriptionBroker <code>subscriptions</code> object, which is essentially an EventEmitter they can listen or emit <code>push_id</code> events to.</p>
<p>ActivePush makes extensive use of "promises" (specifically Q promises, which are compatible with the Promises/A+ standard) in both the application and tests.</p>
<h2>
<a name="testing" class="anchor" href="#testing"><span class="octicon octicon-link"></span></a>Testing</h2>
<pre><code>npm test
</code></pre>
<p>Or:</p>
<pre><code>mocha --compilers coffee:coffee-script test
</code></pre>
<p>The integration tests assume a STOMP broker is running on <code>localhost:61613</code>. ActiveMQ can be started with the following command:</p>
<pre><code>activemq start broker:stomp://localhost:61613
</code></pre>
<p>They also assume Selenium/WebDriver is running on <code>localhost:4444</code>. If you want to disable the WebDriver tests (they are significantly slower) comment out the last line of <code>test/integration-webdriver.coffee</code>, or run:</p>
<pre><code>mocha --compilers coffee:coffee-script test/integration-socketio-client.coffee
</code></pre>
<p><code>integration-common.coffee</code> implements the logic of the tests while <code>integration-socketio-client.coffee</code> and <code>integration-webdriver.coffee</code> implement a common API to create either an in-process <code>socket.io-client</code> or a remote WebDriver instance running the demo.html page (which stores messages it receives in <code>window.messages</code> for introspection by the test)</p>
<p>Unfortunately there is no easy way to tell when all messages have propagated from the running test to the ActiveMQ queue back to the ActivePush server and through the Socket.io client, so we currently have short delays before testing that messages have been received. This leads to occasional non-deterministic test failures. Increasing the delays reduces the frequency at a cost of longer running tests.</p>
<h2>
<a name="contributors" class="anchor" href="#contributors"><span class="octicon octicon-link"></span></a>Contributors</h2>
<ul>
<li>Tom Robinson <a href="mailto:tlrobinson@gmail.com">tlrobinson@gmail.com</a>
</li>
</ul>
</article>
</div>
</div>
<footer>
<div class="owner">
<p><a href="https://github.com/lookout" class="avatar"><img src="https://secure.gravatar.com/avatar/ee9f2b5fc43be14aedb09b84b30c5a2c?s=30&amp;d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png" width="48" height="48"/></a> <a href="https://github.com/lookout">lookout</a> maintains <a href="https://github.com/lookout/activepush">Activepush</a></p>
</div>
<div class="creds">
<small>This page generated using <a href="https://pages.github.com/">GitHub Pages</a><br/>theme by <a href="https://twitter.com/jonrohan/">Jon Rohan</a></small>
</div>
</footer>
</div>
<div class="current-section">
<a href="#top">Scroll to top</a>
<a href="https://github.com/lookout/activepush/tarball/master" class="tar">tar</a><a href="https://github.com/lookout/activepush/zipball/master" class="zip">zip</a><a href="" class="code">source code</a>
<p class="name"></p>
</div>
</body>
</html>

52
javascripts/script.js Normal file
View File

@ -0,0 +1,52 @@
(function($) {
$(document).ready(function(){
// putting lines by the pre blocks
$("pre").each(function(){
var pre = $(this).text().split("\n");
var lines = new Array(pre.length+1);
for(var i = 0; i < pre.length; i++) {
var wrap = Math.floor(pre[i].split("").length / 70)
if (pre[i]==""&&i==pre.length-1) {
lines.splice(i, 1);
} else {
lines[i] = i+1;
for(var j = 0; j < wrap; j++) {
lines[i] += "\n";
}
}
}
$(this).before("<pre class='lines'>" + lines.join("\n") + "</pre>");
});
var headings = [];
var collectHeaders = function(){
headings.push({"top":$(this).offset().top - 15,"text":$(this).text()});
}
if($(".markdown-body h1").length > 1) $(".markdown-body h1").each(collectHeaders)
else if($(".markdown-body h2").length > 1) $(".markdown-body h2").each(collectHeaders)
else if($(".markdown-body h3").length > 1) $(".markdown-body h3").each(collectHeaders)
$(window).scroll(function(){
if(headings.length==0) return true;
var scrolltop = $(window).scrollTop() || 0;
if(headings[0] && scrolltop < headings[0].top) {
$(".current-section").css({"opacity":0,"visibility":"hidden"});
return false;
}
$(".current-section").css({"opacity":1,"visibility":"visible"});
for(var i in headings) {
if(scrolltop >= headings[i].top) {
$(".current-section .name").text(headings[i].text);
}
}
});
$(".current-section a").click(function(){
$(window).scrollTop(0);
return false;
})
});
})(jQuery)

View File

@ -1,13 +0,0 @@
{ EventEmitter } = require "events"
class exports.Module extends EventEmitter
constructor: (options) ->
EventEmitter.call @
@options = options
@logger = options.logger or nullLogger
@subscriptions = options.subscriptions
nullLogger = {}
nullLogger.error = nullLogger.warn = nullLogger.info = nullLogger.debug = (->)

View File

@ -1,16 +0,0 @@
require "js-yaml"
path = require "path"
merge = require "deepmerge"
# If name matches ^\w+$ then use one of the built-in configurations,
# otherwise assume it's a path.
exports.loadConfiguration = (name) ->
name = name or process.env["NODE_ENV"] or "development"
if /^\w+$/.test(name)
config = path.resolve "#{__dirname}/../config/#{name}.yml"
else
config = path.resolve ".", name
deflt = require("#{__dirname}/../config/default.yml") or {}
overlay = require(config) or {}
merge(merge(deflt, overlay), environment: name)

View File

@ -1,43 +0,0 @@
Q = require "q"
Consumer = require("./common").Module
socket_io = require "socket.io"
class SocketIOConsumer extends Consumer
constructor: (server, options = {}) ->
Consumer.call @, options
@server = server
start: ->
@server = require("http").createServer() unless @server?
@io = socket_io.listen @server, log: false, transports: ["websocket", "xhr-polling"]
@io.sockets.on "connection", @_onConnection
Q.ninvoke(@server, "listen", @options.port).then =>
@logger.info "SOCKET.IO listening on port %s", @options.port
stop: ->
Q.try =>
@server.close()
_onConnection: (socket) =>
@logger.debug "SOCKET.IO connection"
socket.on "error", (error) =>
@logger.warn "SOCKET.IO error: %s", error
socket.on "subscribe", (push_id) =>
@logger.info "SOCKET.IO subscribed: %s", push_id
listener = (message) =>
@logger.debug "SOCKET.IO send push_id=%s message=%s", push_id, message
socket.send message
@subscriptions.addListener push_id, listener
socket.on "disconnect", =>
@logger.info "SOCKET.IO disconnected: %s", push_id
@subscriptions.removeListener push_id, listener
getHealth: ->
name: "socketio"
clients: @io.sockets.clients().length
module.exports = { Consumer, SocketIOConsumer }

View File

@ -1,116 +0,0 @@
Q = require "q"
merge = require "deepmerge"
Producer = require("./common").Module
class StompProducer extends Producer
constructor: (options) ->
Producer.call @, options
getHealth: ->
name: "stomp"
host: @options.host
port: @options.port
inbox: @options.inbox
{ Stomp } = require "stomp"
{ ReconnectingStomp } = require "./reconnecting-stomp"
# Library #1: stomp
class NodeStompProducer extends StompProducer
start: ->
deferred = Q.defer()
options = merge(@options.host, debug: false)
# @stomp = new Stomp options
@stomp = new ReconnectingStomp options
@stomp.connect()
@stomp.on "connected", =>
@_onConnected()
deferred.resolve()
@stomp.on "disconnected", =>
@logger.error "STOMP disconnected: %s:%s", @options.host, @options.port
deferred.reject()
@stomp.on "error", (error) =>
@logger.error "STOMP error: %s", error
deferred.promise
stop: ->
Q.try =>
@stomp.disconnect()
_onConnected: =>
@logger.info "STOMP connected: %s:%s%s", @options.host, @options.port, @options.inbox
@stomp.subscribe { destination: @options.inbox, ack: "client" }, @_onMessage
_onMessage: (body, headers) =>
push_id = headers.push_id
message = body[0]
@logger.debug "STOMP receive push_id=%s message=%s", push_id, message
@subscriptions.emit push_id, message
publish: (push_id, message) ->
@stomp.send
destination: @options.inbox
push_id: push_id
body: message
persistent: false
, false
NodeStompProducer.publish = (options, push_id, message) ->
console.log "options", options
stomp = new NodeStompProducer options
stomp.start().then ->
stomp.publish(push_id, message)
stomp.stop()
StompProducer = NodeStompProducer
# Library #2: stompit
# stompit = require "stompit"
# class StompitProducer extends StompProducer
# start: ->
# deferred = Q.defer()
# @stomp = stompit.connect host: @options.host, port: @options.port, =>
# @_onConnected()
# deferred.resolve()
# deferred.promise
# stop: ->
# Q.try =>
# @stomp.disconnect()
# _onConnected: =>
# @stomp.subscribe { destination: @options.inbox }, @_onMessage
# @logger.info "STOMP connected: %s:%s%s", @options.host, @options.port, @options.inbox
# _onMessage: (message) =>
# body = ""
# message.on "data", (data) ->
# body += data.toString("utf-8")
# message.on "end", =>
# push_id = message.headers.push_id
# @logger.debug "STOMP receive push_id=%s message=%s", push_id, body
# @subscriptions.emit push_id, body
# publish: (push_id, message) ->
# frame = @stomp.send(
# destination: @options.inbox
# push_id: push_id
# persistent: false
# )
# Q.ninvoke(frame, "end", message)
# StompitProducer.publish = (options, push_id, message) ->
# console.log "options", options
# stomp = new StompitProducer options
# stomp.start().then ->
# stomp.publish(push_id, message)
# .then ->
# stomp.stop()
# StompProducer = StompitProducer
module.exports = { Producer, StompProducer }

View File

@ -1,41 +0,0 @@
{ Stomp } = require "stomp"
# Subclass of Stomp that automatically tries to reconnect, similar options to Ruby STOMP gem
class exports.ReconnectingStomp extends Stomp
constructor: (args) ->
Stomp.apply @, arguments
@initial_reconnect_delay = args.initial_reconnect_delay or 1
@max_reconnect_delay = args.max_reconnect_delay or 30.0
@use_exponential_back_off = if args.use_exponential_back_off? then args.use_exponential_back_off else true
@back_off_multiplier = args.back_off_multiplier or 2
@max_reconnect_attempts = args.max_reconnect_attempts or 0
@_resetReconnection()
@on "connected", @_resetReconnection
# Hook emit to intercept "disconnect" error.
# Kind of hacky. Should we wrap the class instead?
emit: (name, object) ->
if name is "disconnected"
unless @max_reconnect_attempts > 0 and @reconnectCount >= @max_reconnect_attempts
@_reconnect()
return
Stomp::emit.apply @, arguments
_reconnect: =>
if @reconnectTimer?
return
if @use_exponential_back_off
@reconnectDelay = Math.min(@max_reconnect_delay * 1000, @reconnectDelay * @back_off_multiplier)
@reconnectTimer = setTimeout =>
@max_reconnect_attempts++
@connect()
delete @reconnectTimer
, @reconnectDelay
_resetReconnection: =>
@reconnectCount = 0
@reconnectTimer = null
@reconnectDelay = @initial_reconnect_delay * 1000

View File

@ -1,23 +0,0 @@
{ EventEmitter } = require "events"
class exports.SubscriptionBroker extends EventEmitter
constructor: ->
EventEmitter.call @
@messageCount = 0
emit: ->
@messageCount++
EventEmitter::emit.apply @, arguments
getHealth: ->
health =
name: "subscriptions"
push_ids: {}
subscriptions: 0
messages: @messageCount
for name, value of @_events
count = if Array.isArray(value) then value.length else 1
health.push_ids[name] = count
health.total += count
health

View File

@ -1,42 +0,0 @@
{
"name": "activepush",
"version": "1.0.3",
"description": "Web push service built around ActiveMQ (or any STOMP broker)",
"main": "activepush.coffee",
"bin": {
"activepush": "./bin/activepush",
"activepush-client-example": "./bin/example-client"
},
"scripts": {
"start": "node_modules/.bin/coffee activepush.coffee",
"test": "node_modules/.bin/mocha --compilers coffee:coffee-script test"
},
"repository": "git://github.com:lookout/activepush.git",
"author": "Lookout (https://www.lookout.com/)",
"license": "MIT",
"dependencies": {
"q": "~0.9.6",
"coffee-script": "~1.6.3",
"socket.io": "~0.9.16",
"stomp": "~0.1.1",
"js-yaml": "~2.1.0",
"express": "~3.3.4",
"deepmerge": "~0.2.7",
"winston": "~0.7.2",
"stompit": "~0.9.0",
"optimist": "~0.6.0"
},
"devDependencies": {
"mocha": "~1.12.0",
"mocha-as-promised": "~1.4.0",
"chai": "~1.7.2",
"chai-as-promised": "~3.3.1",
"q": "~0.9.6",
"q-step": "0.0.1",
"q-proxy": "0.0.1",
"node-uuid": "~1.4.0",
"socket.io-client": "~0.9.16",
"wd": "0.0.33",
"node-dev": "latest"
}
}

1
params.json Normal file
View File

@ -0,0 +1 @@
{"name":"Activepush","tagline":"A socket.io push service built around ActiveMQ","body":"Web push service built around ActiveMQ (or any STOMP broker).\r\n\r\nActivePush subscribes to a STOMP broker and relays messages with a specific `push_id` header to subscribed Socket.io clients. The message bodies are opaque to ActivePush, so the service is useful in a variety of applications.\r\n\r\n## Usage\r\n\r\nProduction:\r\n\r\n npm install\r\n bin/activepush [environment]\r\n\r\nDevelopment:\r\n\r\n npm install -d\r\n bin/activepush\r\n\r\nOr, to auto-reload on changes:\r\n\r\n node-dev activepush.coffee\r\n\r\nThe optional `environment` argument corresponds to a configuration file in `configure`, which defaults to `development` and overrides values in the \"default.yml\" configuration file.\r\n\r\nBy default ActivePush expects a STOMP broker (e.x. ActiveMQ) to be running on `localhost:61613`.\r\n\r\nIf ActiveMQ is installed you can start it with the following command:\r\n\r\n activemq start broker:stomp://localhost:61613\r\n\r\n## Configuration\r\n\r\nConfiguration files are located in the `config` directory. `default.yml` is loaded first, and an environment-specific configuration (defaulting to `development.yml`) overrides the defaults.\r\n\r\nThe environment can be specified as the first argument the the `activepush` executable, or in the `NODE_ENV` environment variable.\r\n\r\n## Production\r\n\r\nAll HTTP endpoints except those under the \"/socket.io\" URL should not be exposed externally as they could expose private information (e.g. \"/health\") or capabilities (e.g. \"/send\"). Alternatively, those endpoints could easily be moved to a separate port (see commented out code in the `start` method of ActivePush)\r\n\r\nMultiple instances of ActivePush can be load-balanced, keeping the following in mind:\r\n\r\n* A STOMP message bus rather than a queue should be used to ensure all instances recieve all messages\r\n* Socket.io needs clients to be \"pinned\" to the same backend across multiple requests (perhaps using cookies or the source IP address), in particular for the XHR polling transport.\r\n\r\n## Architecture\r\n\r\nThe ActivePush class in `activepush.coffee` creates the server using the StompProducer and SocketIOConsumer. This could be made configurable, or even support multiple simultaneous producer/consumers.\r\n\r\nThe STOMP and WebSocket components are implemented in `consumers.coffee` and `producers.coffee`, respectively. Other producers/consumers could be implemented.\r\n\r\nThe producer/consumer components share a SubscriptionBroker `subscriptions` object, which is essentially an EventEmitter they can listen or emit `push_id` events to.\r\n\r\nActivePush makes extensive use of \"promises\" (specifically Q promises, which are compatible with the Promises/A+ standard) in both the application and tests.\r\n\r\n## Testing\r\n\r\n npm test\r\n\r\nOr:\r\n\r\n mocha --compilers coffee:coffee-script test\r\n\r\nThe integration tests assume a STOMP broker is running on `localhost:61613`. ActiveMQ can be started with the following command:\r\n\r\n activemq start broker:stomp://localhost:61613\r\n\r\nThey also assume Selenium/WebDriver is running on `localhost:4444`. If you want to disable the WebDriver tests (they are significantly slower) comment out the last line of `test/integration-webdriver.coffee`, or run:\r\n\r\n mocha --compilers coffee:coffee-script test/integration-socketio-client.coffee\r\n\r\n`integration-common.coffee` implements the logic of the tests while `integration-socketio-client.coffee` and `integration-webdriver.coffee` implement a common API to create either an in-process `socket.io-client` or a remote WebDriver instance running the demo.html page (which stores messages it receives in `window.messages` for introspection by the test)\r\n\r\nUnfortunately there is no easy way to tell when all messages have propagated from the running test to the ActiveMQ queue back to the ActivePush server and through the Socket.io client, so we currently have short delays before testing that messages have been received. This leads to occasional non-deterministic test failures. Increasing the delays reduces the frequency at a cost of longer running tests.\r\n## Contributors\r\n\r\n* Tom Robinson <tlrobinson@gmail.com>\r\n","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."}

View File

@ -0,0 +1,69 @@
.highlight { background: #ffffff; }
.highlight .c { color: #999988; font-style: italic } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { font-weight: bold } /* Keyword */
.highlight .o { font-weight: bold } /* Operator */
.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */
.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */
.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #999999 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { font-weight: bold } /* Keyword.Constant */
.highlight .kd { font-weight: bold } /* Keyword.Declaration */
.highlight .kn { font-weight: bold } /* Keyword.Namespace */
.highlight .kp { font-weight: bold } /* Keyword.Pseudo */
.highlight .kr { font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #009999 } /* Literal.Number */
.highlight .s { color: #d14 } /* Literal.String */
.highlight .na { color: #008080 } /* Name.Attribute */
.highlight .nb { color: #0086B3 } /* Name.Builtin */
.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */
.highlight .no { color: #008080 } /* Name.Constant */
.highlight .ni { color: #800080 } /* Name.Entity */
.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */
.highlight .nn { color: #555555 } /* Name.Namespace */
.highlight .nt { color: #000080 } /* Name.Tag */
.highlight .nv { color: #008080 } /* Name.Variable */
.highlight .ow { font-weight: bold } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mf { color: #009999 } /* Literal.Number.Float */
.highlight .mh { color: #009999 } /* Literal.Number.Hex */
.highlight .mi { color: #009999 } /* Literal.Number.Integer */
.highlight .mo { color: #009999 } /* Literal.Number.Oct */
.highlight .sb { color: #d14 } /* Literal.String.Backtick */
.highlight .sc { color: #d14 } /* Literal.String.Char */
.highlight .sd { color: #d14 } /* Literal.String.Doc */
.highlight .s2 { color: #d14 } /* Literal.String.Double */
.highlight .se { color: #d14 } /* Literal.String.Escape */
.highlight .sh { color: #d14 } /* Literal.String.Heredoc */
.highlight .si { color: #d14 } /* Literal.String.Interpol */
.highlight .sx { color: #d14 } /* Literal.String.Other */
.highlight .sr { color: #009926 } /* Literal.String.Regex */
.highlight .s1 { color: #d14 } /* Literal.String.Single */
.highlight .ss { color: #990073 } /* Literal.String.Symbol */
.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */
.highlight .vc { color: #008080 } /* Name.Variable.Class */
.highlight .vg { color: #008080 } /* Name.Variable.Global */
.highlight .vi { color: #008080 } /* Name.Variable.Instance */
.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */
.type-csharp .highlight .k { color: #0000FF }
.type-csharp .highlight .kt { color: #0000FF }
.type-csharp .highlight .nf { color: #000000; font-weight: normal }
.type-csharp .highlight .nc { color: #2B91AF }
.type-csharp .highlight .nn { color: #000000 }
.type-csharp .highlight .s { color: #A31515 }
.type-csharp .highlight .sc { color: #A31515 }

581
stylesheets/stylesheet.css Normal file
View File

@ -0,0 +1,581 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* Style */
body {
font-size: 15px;
font-family: Arial, Arial, Helvetica, sans-serif;
line-height: 1.5;
background: #D1D1D1;
}
a {
color: #63a52a;
text-decoration: none;
-webkit-transition: color ease-in-out 0.3s;
}
a:hover {
text-decoration: underline;
color: #90D355;
}
h1.title {
margin: 30px 20px 10px;
font-size: 60px;
font-weight: bold;
font-style: italic;
font-family:Georgia, serif;
text-align: center;
}
.wrapper {
width: 675px;
margin: 0 auto;
}
#container {
border: 1px solid #2a2a2a;
background: #ddd url(../images/pattern.png);
box-shadow: 0 0 5px #b1b1b1;
}
p.tagline {
padding: 20px 20px 0;
color: #fff;
font-size: 17px;
}
#main {
margin-top: 20px;
padding: 0 20px 90px;
background-color: #fff;
}
.download-bar {
background: #222;
border: 5px solid #444;
padding: 10px;
margin: 0 -35px 20px;
position: relative;
}
.download-bar .inner {
overflow: hidden;
}
.download-bar .watch-fork iframe {
display: block;
float: left;
border-right: 1px solid #ddd;
padding-right: 5px;
}
.download-bar .watch-fork iframe.last {
border-right: 0 none;
padding-right: 0;
padding-left: 5px;
border-left: 1px solid #fff;
}
.download-bar .watch-fork {
overflow: hidden;
float: right;
background-color: #eee;
padding: 5px;
border-radius: 3px;
}
.download-bar .blc {
border: 10px solid black;
border-color: transparent transparent black;
width: 0;
height: 0;
display: block;
position: absolute;
bottom: -15px;
left: 0;
-moz-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
}
.download-bar .trc {
border: 10px solid black;
border-color: black transparent transparent;
width: 0;
height: 0;
display: block;
position: absolute;
top: -15px;
right: 0;
-moz-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
}
.download-bar .avatar {
border: 1px solid black;
display: block;
padding: 4px;
float: left;
}
.download-bar .avatar img {
display: block;
}
.download-bar a.code {
background: transparent url(../images/code.png) no-repeat 0 2px;
padding-left: 35px;
margin-top: 8px;
display: block;
float: left;
text-indent: 0;
width: auto;
height: auto;
opacity: 1;
-moz-opacity: 1;
filter:alpha(opacity=1);
}
.current-section {
position: fixed;
top: 0;
left: 50%;
width: 693px;
margin-left: -352px;
background: #222;
border: 5px solid #444;
color: #fff;
opacity: 0;
visibility: hidden;
-webkit-transition: opacity ease-in-out 0.3s;
}
.current-section p {
padding: 5px 27px;
font-size: 24px;
font-weight: bold;
}
.current-section a {
float: right;
text-indent: -10000px;
background: transparent url(../images/top.png) no-repeat 0 0;
width: 20px;
height: 20px;
opacity: 0.8;
margin-right: 12px;
margin-top: 12px;
-moz-opacity: 0.8;
filter:alpha(opacity=8);
-webkit-transition: opacity ease-in-out 0.3s;
}
.current-section a:hover {
opacity: 1;
-moz-opacity: 1;
filter:alpha(opacity=1);
}
.current-section a.zip {
margin-right: 8px;
}
a.zip,
a.zip span {
background: transparent url(../images/zip.png) no-repeat 0 0;
width: 30px;
height: 21px;
opacity: 0.8;
display: inline-block;
text-indent: -10000px;
-moz-opacity: 0.8;
filter:alpha(opacity=8);
-webkit-transition: opacity ease-in-out 0.3s;
}
a.tar,
a.tar span {
background: transparent url(../images/tar.png) no-repeat 0 0;
width: 30px;
height: 21px;
opacity: 0.8;
display: inline-block;
text-indent: -10000px;
-moz-opacity: 0.8;
filter:alpha(opacity=8);
-webkit-transition: opacity ease-in-out 0.3s;
}
a.code {
background: transparent url(../images/code.png) no-repeat 0 2px;
width: 30px;
height: 21px;
display: block;
opacity: 0.8;
display: inline-block;
text-indent: -10000px;
-moz-opacity: 0.8;
filter:alpha(opacity=8);
-webkit-transition: opacity ease-in-out 0.3s;
}
a.zip:hover,
a.tar:hover,
a.code:hover {
opacity: 1;
-moz-opacity: 1;
filter:alpha(opacity=1);
}
a.download-button {
border: 1px solid black;
border-radius: 3px;
display: inline-block;
text-indent: 0!important;
width: auto;
float: right;
background: #999; /* for non-css3 browsers */
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#37ADD4', endColorstr='#1B657E'); /* for IE */
background: -webkit-gradient(linear, left top, left bottom, from(#37ADD4), to(#1B657E)); /* for webkit browsers */
background: -moz-linear-gradient(top, #37ADD4, #1B657E); /* for firefox 3.6+ */
height: auto;
margin-left: 10px;
}
a.download-button span {
background-position: 10px 5px;
width: auto;
height: auto;
padding: 5px 10px;
padding-left: 45px;
display: inline-block;
text-indent: 0!important;
color: #fff;
}
footer {
margin-bottom: 60px;
padding-bottom: 60px;
}
footer .owner {
background: #222;
border: 5px solid #444;
padding: 5px 15px;
margin: -67px -10px 35px;
color: #d6d6d6;
}
footer .creds small {
float: right;
font-size: 10px;
text-align: right;
margin-left: 15px;
}
footer .owner .avatar {
background-color: #666;
display: block;
margin: -19px 10px 0 0;
width: 60px;
float: left;
}
footer .owner img {
display: block;
border: 1px solid #2a2a2a;
margin: 5px;
}
footer .owner p {
font-family:Georgia, serif;
}
footer .owner p a {
font-size: 16px;
font-style: italic;
}
/* Markdown */
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6,
.markdown-body p,
.markdown-body pre,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body blockquote {
margin-bottom: 20px;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
font-weight: bold;
}
.markdown-body h1 {
font-size: 28px;
}
.markdown-body h2 {
font-size: 24px;
color: #557398;
}
.markdown-body h3 {
font-size: 20px;
}
.markdown-body h4 {
font-size: 18px;
}
.markdown-body h5 {
font-size: 16px;
}
.markdown-body pre {
padding: 10px 70px 10px 0;
margin-left: -20px;
margin-right: -20px;
font-family: 'Monaco', 'Lucida Console', monospace;
font-size: 13px;
line-height: 20px;
box-shadow: inset 0 0 5px #000;
word-wrap: break-word;
background-color:#3b3b3b;
color: #d6d6d6;
}
.markdown-body pre.lines {
font-size: 12px;
margin:0 10px 0 -20px;
padding: 10px;
float: left;
display: block;
text-align: right;
box-shadow: none;
background-color:#2a2a2a;
color: #d6d6d6;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 30px;
}
.markdown-body ul {
list-style-type: disc;
}
.markdown-body li,
.markdown-body li p,
.markdown-body dd,
.markdown-body dd p {
margin-bottom: 10px;
}
.markdown-body li pre,
.markdown-body li pre.lines,
.markdown-body dd pre,
.markdown-body dd pre.lines {
margin-left: -35px;
}
.markdown-body dt {
font-weight: bold;
font-style: italic;
}
.markdown-body dd {
margin-left: 15px;
}
.markdown-body table {
width: 673px;
margin-left: -20px;
margin-right: -20px;
}
.markdown-body tbody {
border-top: 2px solid #557398;
border-bottom: 2px solid #557398;
background-color: #EBEFF4;
}
.markdown-body table td * {
margin: 0;
}
.markdown-body td {
border-right: 1px solid #557398;
border-bottom: 1px solid #557398;
padding: 5px;
}
.markdown-body td:first-child,
.markdown-body th:first-child {
width: 30%;
padding-left: 20px;
}
.markdown-body td:last-child {
border-right: 0 none;
}
.markdown-body th {
font-size: 18px;
font-weight: bold;
text-align: left;
padding: 5px;
}
.markdown-body tt {
background-color:#3b3b3b;
color: #d6d6d6;
padding: 2px 3px;
}
.markdown-body blockquote {
font-style: italic;
font-family:Georgia, serif;
font-size: 17px;
border-top: 3px solid #333;
border-bottom: 3px solid #333;
padding: 10px 20px;
padding-left: 50px;
}
.markdown-body blockquote:before {
font-style: italic;
font-family: Georgia, serif;
font-size: 90px;
height: 90px;
margin-left: -60px;
margin-top: -25px;
content: "‟";
display: block;
float: left;
}
.markdown-body img {
max-width: 100%;
@include box-sizing(border-box);
}
.highlight { background: #ffffff; }
.highlight .c { color: #999988; font-style: italic } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { font-weight: bold } /* Keyword */
.highlight .o { font-weight: bold } /* Operator */
.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */
.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */
.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #999999 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { font-weight: bold } /* Keyword.Constant */
.highlight .kd { font-weight: bold } /* Keyword.Declaration */
.highlight .kn { font-weight: bold } /* Keyword.Namespace */
.highlight .kp { font-weight: bold } /* Keyword.Pseudo */
.highlight .kr { font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #009999 } /* Literal.Number */
.highlight .s { color: #d14 } /* Literal.String */
.highlight .na { color: #008080 } /* Name.Attribute */
.highlight .nb { color: #0086B3 } /* Name.Builtin */
.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */
.highlight .no { color: #008080 } /* Name.Constant */
.highlight .ni { color: #800080 } /* Name.Entity */
.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */
.highlight .nn { color: #555555 } /* Name.Namespace */
.highlight .nt { color: #000080 } /* Name.Tag */
.highlight .nv { color: #008080 } /* Name.Variable */
.highlight .ow { font-weight: bold } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mf { color: #009999 } /* Literal.Number.Float */
.highlight .mh { color: #009999 } /* Literal.Number.Hex */
.highlight .mi { color: #009999 } /* Literal.Number.Integer */
.highlight .mo { color: #009999 } /* Literal.Number.Oct */
.highlight .sb { color: #d14 } /* Literal.String.Backtick */
.highlight .sc { color: #d14 } /* Literal.String.Char */
.highlight .sd { color: #d14 } /* Literal.String.Doc */
.highlight .s2 { color: #d14 } /* Literal.String.Double */
.highlight .se { color: #d14 } /* Literal.String.Escape */
.highlight .sh { color: #d14 } /* Literal.String.Heredoc */
.highlight .si { color: #d14 } /* Literal.String.Interpol */
.highlight .sx { color: #d14 } /* Literal.String.Other */
.highlight .sr { color: #009926 } /* Literal.String.Regex */
.highlight .s1 { color: #d14 } /* Literal.String.Single */
.highlight .ss { color: #990073 } /* Literal.String.Symbol */
.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */
.highlight .vc { color: #008080 } /* Name.Variable.Class */
.highlight .vg { color: #008080 } /* Name.Variable.Global */
.highlight .vi { color: #008080 } /* Name.Variable.Instance */
.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */

View File

@ -1,130 +0,0 @@
require("mocha-as-promised")()
chai = require "chai"
chai.use require "chai-as-promised"
{ expect } = chai
Q = require "q"
merge = require "deepmerge"
uuid = require "node-uuid"
QStep = require "q-step"
activepush = require "../activepush"
{ ActivePush } = activepush
TIMEOUT = 20000
createServer = (config) ->
new ActivePush(config).start()
# These tests can be run either using socket.io-client within Node to simulate a browser, or using WebDriver to test real browsers
exports.initIntegrationTests = (options) ->
{ name, createClient } = options
config = activepush.config.loadConfiguration "test"
# config.logging.level = "DEBUG"
describe "Single ActivePush instance (#{name})", ->
@timeout TIMEOUT
server = null
before ->
# Use a unique inbox in case we're running multiple tests using the same ActiveMQ server concurrently etc
inbox = uniqueInbox()
createServer(merge(config, stomp:inbox:inbox)).then (activePush) ->
server = activePush
after ->
server.stop()
it "should not buffer messages (treat as transient)", ->
expected = uniqueMessage("NO")
QStep(
() -> server.producer.publish "my_push_id", expected
() -> createClient server.config.http.port, "my_push_id"
(getMessages) -> getMessages()
(receivedMessages) -> expect(receivedMessages).to.deep.equal []
)
it "should relay the correct messages to a single client", ->
expected = uniqueMessage("YES")
QStep(
() -> createClient server.config.http.port, "my_push_id"
(getMessages) ->
@getMessages = getMessages
Q.all [
server.producer.publish "my_push_id", expected
server.producer.publish "other_push_id", uniqueMessage("NO")
]
() -> @getMessages()
(receivedMessages) -> expect(receivedMessages).to.deep.equal [expected]
)
it "should relay the correct messages to multiple clients", (done) ->
expected = uniqueMessage("YES")
QStep(
() -> Q.all(createClient(server.config.http.port, "my_push_id") for index in [0..1])
(allGetMessages) ->
@allGetMessages = allGetMessages
Q.all [
server.producer.publish "my_push_id", expected
server.producer.publish "other_push_id", uniqueMessage("NO")
]
() -> Q.all(getMessages() for getMessages in @allGetMessages)
(allReceivedMessages) -> expect(allReceivedMessages).to.deep.equal [[expected], [expected]]
)
it "should relay multiple messages when using XHR transport", ->
expected = uniqueMessage("YES")
QStep(
() -> createClient(server.config.http.port, "my_push_id", transports: ["xhr-polling"], 'try multiple transports': false)
(getMessages) ->
@getMessages = getMessages
Q.all [
server.producer.publish "my_push_id", expected
server.producer.publish "my_push_id", expected
]
() -> @getMessages()
(receivedMessages) -> expect(receivedMessages).to.deep.equal [expected, expected]
)
describe "Multiple ActivePush instances (#{name})", ->
@timeout TIMEOUT
servers = null
before ->
# Use a unique inbox in case we're running multiple tests using the same ActiveMQ server concurrently etc
inbox = uniqueInbox()
Q.all(for index in [0..1]
createServer(merge(config,
stomp:inbox: inbox
http:port: config.http.port + index + 1 # Don't re-use the same port from first test
))
).then (_servers) ->
servers = _servers
after ->
Q.all(server.stop() for server in servers)
it "should relay the correct messages to multiple clients", (done) ->
QStep(
() -> Q.all(createClient(server.config.http.port, "my_push_id") for server in servers)
(allGetMessages) ->
@allGetMessages = allGetMessages
promises = []
@expected = for server, index in servers
promises.push server.producer.publish "my_push_id", (msg = uniqueMessage("YES#{index}"))
promises.push server.producer.publish "other_push_id", uniqueMessage("NO")
msg
Q.all(promises)
() -> Q.all(getMessages() for getMessages in @allGetMessages)
(allReceivedMessages) ->
allReceivedMessages = (messages.sort() for messages in allReceivedMessages)
allExpectedMessages = (@expected.sort() for messages in allReceivedMessages)
expect(allReceivedMessages).to.deep.equal allExpectedMessages
)
uniqueInbox = ->
"/topic/activepush-test-"+uuid.v1()
# Use unique messages to ensure we don't messages from other tests, etc.
uniqueMessage = (prefix="") ->
prefix+(if prefix then "-" else "")+uuid.v1()

View File

@ -1,35 +0,0 @@
Q = require "q"
io = require "socket.io-client"
merge = require "deepmerge"
integration = require "./integration-common"
# HACK: Delay before checking received messages to ensure all messages get delivered.
# Increase this value if tests are failiing non-deterministically.
# TODO: better way to detect all messages have been delivered?
WAIT_TIME = 200
exports.initIntegrationTests = ->
integration.initIntegrationTests
name: "socket.io"
createClient: (port, push_id, options = {}) ->
deferred = Q.defer()
socket = io.connect "http://localhost:#{port}", merge(options, "force new connection": true)
# For some reason socket.io-client doesn't respect the "transports" option so we have to set it manually
socket.socket.options.transports = options.transports if options.transports?
socket.on "connect", ->
socket.emit "subscribe", push_id
messages = []
socket.on "message", (message) ->
messages.push message
deferred.resolve ->
Q.delay(WAIT_TIME).then -> messages
# FIXME: figure out how to remove this delay (only required when using the XHR transport)
if socket.socket.options.transports[0] is "xhr-polling" and socket.socket.options.transports.length is 1
deferred.promise.delay(100)
else
deferred.promise
exports.initIntegrationTests()

View File

@ -1,54 +0,0 @@
Q = require "q"
QStep = require "q-step"
wd = require "wd"
merge = require "deepmerge"
integration = require "./integration-common"
# HACK: Delay before checking received messages to ensure all messages get delivered.
# Increase this value if tests are failiing non-deterministically.
# TODO: better way to detect all messages have been delivered?
WAIT_TIME = 400
DEFAULT_CONFIG =
browser:
browserName: "firefox"
LOCAL_CONFIG =
host: "localhost"
port: 4444
SAUCE_CONFIG =
host: "ondemand.saucelabs.com"
port: 80
username: process.env["SAUCE_USER"]
password: process.env["SAUCE_KEY"]
if process.env["SAUCE_USER"]?
CONFIG = merge(DEFAULT_CONFIG, SAUCE_CONFIG)
else
CONFIG = merge(DEFAULT_CONFIG, LOCAL_CONFIG)
exports.initIntegrationTests = (config = {}) ->
config = merge(CONFIG, config)
integration.initIntegrationTests
name: "webdriver-#{config.browser.browserName}"
createClient: (port, push_id) ->
browser = wd.promiseRemote(config.host, config.port, config.username, config.password)
QStep(
-> browser.init(config.browser)
-> browser.get("http://localhost:#{port}/\##{push_id}")
-> browser.waitForCondition("!!window.messages", 5000)
->
->
Q.delay(WAIT_TIME).then ->
browser.eval("window.messages").then (messages) ->
messages
.fin ->
browser.quit()
)
# exports.initIntegrationTests()
exports.initIntegrationTests(browser: browserName: "chrome")