├── .gitignore ├── Cakefile ├── LICENSE ├── README.md ├── TODO ├── demos ├── bridge │ ├── bridge.py │ ├── index.html │ ├── server.py │ ├── test.html │ └── test_sockets.js ├── presence │ ├── html │ │ ├── README.md │ │ ├── css │ │ │ ├── bootstrap-2.0.1.css │ │ │ └── bootstrap-responsive-2.0.1.css │ │ ├── img │ │ │ ├── glyphicons-halflings-white.png │ │ │ ├── glyphicons-halflings.png │ │ │ ├── hand_thumbsdown.png │ │ │ └── hand_thumbsup.png │ │ ├── index.html │ │ └── js │ │ │ ├── angular-0.9.19.js │ │ │ ├── bootstrap-2.0.1.js │ │ │ ├── nullmq-0.1.0.js │ │ │ ├── presence-0.1.0.js │ │ │ └── stomp-0.1.0.js │ ├── python │ │ ├── README.md │ │ ├── requirements.txt │ │ └── server.py │ └── ruby │ │ ├── Gemfile │ │ ├── Gemfile.lock │ │ ├── README.md │ │ ├── bin │ │ ├── chat_client │ │ ├── chat_server │ │ ├── presence_client │ │ ├── presence_server │ │ └── redirector │ │ └── lib │ │ ├── clone.rb │ │ └── clone │ │ ├── client.rb │ │ └── server.rb ├── reflector │ ├── index.html │ ├── server.py │ ├── test.html │ ├── test_pipeline.js │ ├── test_pubsub.js │ └── test_rr.js └── requirements.txt ├── dist ├── lib │ ├── jquery.min.js │ ├── qunit.css │ ├── qunit.js │ └── stomp.js └── nullmq.js ├── notes.txt ├── package.json ├── requirements.txt ├── src └── nullmq.coffee └── test ├── nullmq.spec.coffee ├── queue.spec.coffee ├── server.mock.coffee └── websocket.mock.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | dist/test 2 | node_modules/ -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | {exec} = require 'child_process' 3 | util = require 'util' 4 | 5 | task 'watch', 'Watch for changes in coffee files to build and test', -> 6 | util.log "Watching for changes in src and test" 7 | lastTest = 0 8 | watchDir 'src', -> 9 | invoke 'build:src' 10 | invoke 'build:test' 11 | watchDir 'test', -> 12 | invoke 'build:test' 13 | watchDir 'dist/test', (file)-> 14 | # We only want to run tests once (a second), 15 | # even if a bunch of test files change 16 | time = new Date().getTime() 17 | if (time-lastTest) > 1000 18 | lastTest = time 19 | invoke 'test' 20 | 21 | task 'test', 'Run the tests', -> 22 | util.log "Running tests..." 23 | exec "jasmine-node --nocolor dist/test", (err, stdout, stderr) -> 24 | if err 25 | handleError(parseTestResults(stdout), stderr) 26 | else 27 | displayNotification "Tests pass!" 28 | util.log lastLine(stdout) 29 | 30 | task 'build', 'Build source and tests', -> 31 | invoke 'build:src' 32 | invoke 'build:test' 33 | 34 | task 'build:src', 'Build the src files into lib', -> 35 | util.log "Compiling src..." 36 | exec "coffee -o dist/ -c src/", (err, stdout, stderr) -> 37 | handleError(err) if err 38 | 39 | task 'build:test', 'Build the test files into lib/test', -> 40 | util.log "Compiling test..." 41 | exec "coffee -o dist/test/ -c test/", (err, stdout, stderr) -> 42 | handleError(err) if err 43 | 44 | watchDir = (dir, callback) -> 45 | fs.readdir dir, (err, files) -> 46 | handleError(err) if err 47 | for file in files then do (file) -> 48 | fs.watchFile "#{dir}/#{file}", (curr, prev) -> 49 | if +curr.mtime isnt +prev.mtime 50 | callback "#{dir}/#{file}" 51 | 52 | parseTestResults = (data) -> 53 | lines = (line for line in data.split('\n') when line.length > 5) 54 | results = lines.pop() 55 | details = lines[1...lines.length-2].join('\n') 56 | results + '\n\n' + details + '\n' 57 | 58 | lastLine = (data) -> 59 | (line for line in data.split('\n') when line.length > 5).pop() 60 | 61 | handleError = (error, stderr) -> 62 | if stderr? and !error 63 | util.log stderr 64 | displayNotification stderr.match(/\n(Error:[^\n]+)/)?[1] 65 | else 66 | util.log error 67 | displayNotification error 68 | 69 | displayNotification = (message = '') -> 70 | options = { title: 'CoffeeScript' } 71 | try require('growl').notify message, options -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Jeff Lindsay 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NullMQ 2 | 3 | ZeroMQ semantics in the browser. For more information, check out [these 4 | slides](http://www.slideshare.net/progrium/nullmq-pdx). 5 | 6 | This is still very early. 7 | 8 | ## Installation 9 | 10 | * `git clone ...` 11 | * `cat requirements.txt | xargs npm install -g` 12 | * `cake build` 13 | 14 | ## Testing 15 | 16 | * `cake test` 17 | 18 | ## Demos 19 | 20 | Under demos there are two server implementations (bridge and reflector). 21 | There is also an example web application showing presence and chat using the 22 | clone pattern. 23 | 24 | * `pip install -r demos/requirements.txt` 25 | 26 | ## Contributors 27 | 28 | * Jeff Lindsay 29 | * Bulat Shakirzyanov 30 | 31 | ## License 32 | 33 | MIT 34 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | -Multipart 2 | -DEALER, ROUTER 3 | -Bridge integration tests 4 | -Reconnect 5 | -Subscribe Filters (filter on sending socket) 6 | -high watermark 7 | -------------------------------------------------------------------------------- /demos/bridge/bridge.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import gevent 4 | 5 | from stomp4py.server.websocket import StompHandler 6 | from stomp4py.server.base import Subscription 7 | 8 | from gevent_zeromq import zmq 9 | 10 | context = zmq.Context() 11 | 12 | class NullMQConnection(Subscription): 13 | def __init__(self, handler, frame): 14 | super(NullMQConnection, self).__init__(handler, frame) 15 | self.type = frame.headers.get('type', 'connect') 16 | self.socket_type = frame.headers.get('socket') 17 | self.uid = '%s-%s' % (frame.conn_id, self.id) 18 | self.filter = frame.headers.get('filter', '') 19 | self.listener = None 20 | 21 | StompHandler.subscription_class = NullMQConnection 22 | 23 | class ZeroMQBridge(object): 24 | def __init__(self, prefix): 25 | self.prefix = prefix 26 | self.connection_by_uid = {} 27 | self.connection_by_destination = collections.defaultdict(set) 28 | 29 | def __call__(self, frame, payload): 30 | print frame 31 | if frame is None or frame.command == 'DISCONNECT': 32 | for connection in payload: 33 | self.close(connection) 34 | elif frame.command == 'SUBSCRIBE': 35 | self.connect(payload) 36 | elif frame.command == 'UNSUBSCRIBE': 37 | self.close(payload) 38 | elif frame.command == 'SEND': 39 | self.send(frame) 40 | elif frame.command == 'COMMIT': 41 | for part in payload: 42 | self.send(part) 43 | 44 | def connect(self, connection): 45 | connection.socket = context.socket( 46 | getattr(zmq, connection.socket_type.upper())) 47 | connection.socket.connect('%s%s' % (self.prefix, connection.destination)) 48 | 49 | self.connection_by_uid[connection.uid] = connection 50 | self.connection_by_destination[connection.destination].add(connection) 51 | if connection.socket_type == 'sub': 52 | connection.socket.setsockopt(zmq.SUBSCRIBE, connection.filter) 53 | if connection.socket_type in ['sub', 'pull', 'dealer']: 54 | def listen_for_messages(): 55 | while connection.active: 56 | connection.send(connection.socket.recv()) 57 | gevent.spawn(listen_for_messages) 58 | 59 | def close(self, connection): 60 | if connection.listener: 61 | connection.listener.kill() 62 | connection.socket.close() 63 | del self.connection_by_uid[connection.uid] 64 | self.connection_by_destination[connection.destination].remove(connection) 65 | 66 | def send(self, frame): 67 | type = frame.headers.get('socket') 68 | if type in ['req', 'rep']: 69 | reply_to = frame.headers.get('reply-to') 70 | if type == 'req': 71 | uid = '%s-%s' % (frame.conn_id, reply_to) 72 | else: 73 | uid = reply_to 74 | conn = self.connection_by_uid[uid] 75 | conn.socket.send(frame.body) 76 | if type == 'req': 77 | def wait_for_reply(): 78 | conn.send(conn.socket.recv()) 79 | conn.listener = gevent.spawn(wait_for_reply) 80 | elif type in ['pub', 'push', 'dealer']: 81 | conns = list(self.connection_by_destination[frame.destination]) 82 | if len(conns): 83 | conns[0].socket.send(frame.body) 84 | -------------------------------------------------------------------------------- /demos/bridge/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | Run tests 15 | 16 | -------------------------------------------------------------------------------- /demos/bridge/server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | import mimetypes 4 | 5 | import gevent.server 6 | 7 | from ws4py.server.geventserver import WebSocketServer 8 | from stomp4py.server.websocket import StompHandler 9 | 10 | from gevent_zeromq import zmq 11 | 12 | from bridge import ZeroMQBridge 13 | 14 | context = zmq.Context() 15 | 16 | def control_socket(prefix): 17 | control = context.socket(zmq.REP) 18 | control.bind('%s/control' % prefix) 19 | sockets = None 20 | while True: 21 | cmd = control.recv() 22 | if cmd == 'setup': 23 | if sockets is None: 24 | sockets = dict( 25 | pub=context.socket(zmq.PUB), 26 | sub=context.socket(zmq.SUB), 27 | req=context.socket(zmq.REQ), 28 | rep=context.socket(zmq.REP), 29 | push=context.socket(zmq.PUSH), 30 | pull=context.socket(zmq.PULL),) 31 | for key in sockets: 32 | sockets[key].bind('%s/%s' % (prefix, key)) 33 | sockets['sub'].setsockopt(zmq.SUBSCRIBE, '') 34 | control.send("ok") 35 | elif cmd == 'teardown': 36 | #for key in sockets: 37 | # sockets[key].close() 38 | control.send("ok") 39 | elif cmd == 'pub': 40 | sockets[cmd].send("Foobar") 41 | control.send("Foobar") 42 | elif cmd == 'sub': 43 | msg = sockets[cmd].recv() 44 | control.send(msg) 45 | elif cmd == 'req': 46 | sockets[cmd].send("Foobar") 47 | reply = sockets[cmd].recv() 48 | control.send(reply) 49 | elif cmd == 'rep': 50 | request = sockets[cmd].recv() 51 | sockets[cmd].send("Foobar:%s" % request) 52 | control.send("Foobar:%s" % request) 53 | elif cmd == 'push': 54 | sockets[cmd].send("Foobar") 55 | control.send("Foobar") 56 | elif cmd == 'pull': 57 | msg = sockets[cmd].recv() 58 | control.send(msg) 59 | else: 60 | control.send("Unknown command") 61 | 62 | if __name__ == '__main__': 63 | prefix = sys.argv[1] if len(sys.argv) > 1 else 'ipc:///tmp' 64 | 65 | #gevent.spawn(control_socket, prefix) 66 | 67 | bridge = ZeroMQBridge(prefix) 68 | 69 | def websocket_handler(websocket, environ): 70 | if environ.get('PATH_INFO') == '/bridge': 71 | StompHandler(websocket, bridge).serve() 72 | else: 73 | websocket.close() 74 | 75 | def http_handler(environ, start_response): 76 | if '/dist' in environ['PATH_INFO'] and environ['PATH_INFO'].split('/')[1] == 'dist': 77 | filename = '../..%s' % environ['PATH_INFO'] 78 | else: 79 | filename = environ['PATH_INFO'].split('/')[-1] or 'index.html' 80 | if os.path.exists(filename): 81 | start_response('200 OK', [('Content-type', mimetypes.guess_type(filename)[0])]) 82 | return [open(filename).read()] 83 | else: 84 | start_response('404 Not found', [('Content-type', 'text/plain')]) 85 | return ["Not found"] 86 | 87 | server = WebSocketServer(('127.0.0.1', 9000), websocket_handler, fallback_app=http_handler) 88 | print "Starting NullMQ-ZeroMQ bridge on 9000 for prefix %s..." % prefix 89 | server.serve_forever() -------------------------------------------------------------------------------- /demos/bridge/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | NullMQ Bridge 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 36 | 37 |
38 |

NullMQ Bridge

39 |
40 |

Tests requires that a Bridge Server accepting Stomp WebSocket protocol is running with the configuration:

41 |
    42 |
  • URL: 43 |
  • User: / 44 |
45 |
46 | 47 |

48 |

49 |
    50 | 51 |
    
    52 |   
    53 | 54 | -------------------------------------------------------------------------------- /demos/bridge/test_sockets.js: -------------------------------------------------------------------------------- 1 | var ctx = null; 2 | var control = null; 3 | 4 | module("ZeroMQ test sockets", { 5 | setup: function() { 6 | ctx = new nullmq.Context(TEST.url); 7 | if (control == null) { 8 | control = new nullmq.Context(TEST.url).socket(nullmq.REQ); 9 | control.connect('/control'); 10 | } 11 | control.send("setup"); 12 | }, 13 | 14 | teardown: function() { 15 | control.send("teardown"); 16 | control.recv(function(rep) { }); 17 | ctx.term(); 18 | } 19 | }); 20 | 21 | function whenReady(fn) { 22 | control.recv(function(ready) { 23 | if (ready == 'ok') { 24 | fn(); 25 | } else { 26 | ok(false, "Remote setup failed"); 27 | } 28 | }); 29 | } 30 | 31 | test("publishing", function() { 32 | whenReady(function() { 33 | var message = "Foobar"; 34 | var pub = ctx.socket(nullmq.PUB); 35 | pub.connect('/sub'); 36 | pub.send(message); 37 | control.send('sub'); 38 | control.recv(function(msg) { 39 | start(); 40 | equals(msg, message); 41 | }); 42 | 43 | }); 44 | stop(TEST.timeout); 45 | }); 46 | 47 | test("subscribing", function() { 48 | whenReady(function() { 49 | var sub = ctx.socket(nullmq.SUB); 50 | sub.connect('/pub'); 51 | sub.recv(function(actual) { 52 | start(); 53 | control.send('pub'); 54 | control.recv(function(expected) { 55 | equals(actual, expected); 56 | }); 57 | }); 58 | }); 59 | stop(TEST.timeout); 60 | }); 61 | 62 | test("pushing", function() { 63 | whenReady(function() { 64 | var message = "Foobar"; 65 | var push = ctx.socket(nullmq.PUSH); 66 | push.connect('/pull'); 67 | push.send(message); 68 | control.send('push'); 69 | control.recv(function(msg) { 70 | start(); 71 | equals(msg, message); 72 | }); 73 | }); 74 | stop(TEST.timeout); 75 | }); 76 | 77 | test("pulling", function() { 78 | whenReady(function() { 79 | var pull = ctx.socket(nullmq.PULL); 80 | pull.connect('/push'); 81 | pull.recv(function(actual) { 82 | start(); 83 | control.send('push'); 84 | control.recv(function(expected) { 85 | equals(actual, expected); 86 | }); 87 | }); 88 | }); 89 | stop(TEST.timeout); 90 | }); 91 | 92 | test("requesting", function() { 93 | whenReady(function() { 94 | var req = ctx.socket(nullmq.REQ); 95 | req.connect('/rep'); 96 | req.send("Foobar"); 97 | control.send('rep'); 98 | control.recv(function(expected) { 99 | req.recv(function(actual) { 100 | start(); 101 | equals(actual, expected); 102 | }); 103 | 104 | }); 105 | }); 106 | stop(TEST.timeout); 107 | }); 108 | 109 | test("replying", function() { 110 | whenReady(function() { 111 | var expected = null; 112 | var rep = ctx.socket(nullmq.REP); 113 | rep.connect('/req'); 114 | rep.recvall(function(msg) { 115 | expected = msg + ":Foobar"; 116 | rep.send(expected); 117 | }); 118 | control.send('req'); 119 | control.recv(function(actual) { 120 | start(); 121 | equals(actual, expected); 122 | }); 123 | }); 124 | stop(TEST.timeout); 125 | }); 126 | -------------------------------------------------------------------------------- /demos/presence/html/README.md: -------------------------------------------------------------------------------- 1 | # Presence html frontend 2 | 3 | ## Run 4 | 5 | `python -m SimpleHTTPServer 8080` 6 | 7 | ## Use 8 | 9 | * make sure to start python bridge and ruby chat and presence servers first 10 | * go to `http://localhost:8080/` in your browser 11 | -------------------------------------------------------------------------------- /demos/presence/html/css/bootstrap-responsive-2.0.1.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .clearfix { 11 | *zoom: 1; 12 | } 13 | .clearfix:before, .clearfix:after { 14 | display: table; 15 | content: ""; 16 | } 17 | .clearfix:after { 18 | clear: both; 19 | } 20 | .hidden { 21 | display: none; 22 | visibility: hidden; 23 | } 24 | @media (max-width: 480px) { 25 | .nav-collapse { 26 | -webkit-transform: translate3d(0, 0, 0); 27 | } 28 | .page-header h1 small { 29 | display: block; 30 | line-height: 18px; 31 | } 32 | input[class*="span"], 33 | select[class*="span"], 34 | textarea[class*="span"], 35 | .uneditable-input { 36 | display: block; 37 | width: 100%; 38 | min-height: 28px; 39 | /* Make inputs at least the height of their button counterpart */ 40 | 41 | /* Makes inputs behave like true block-level elements */ 42 | 43 | -webkit-box-sizing: border-box; 44 | /* Older Webkit */ 45 | 46 | -moz-box-sizing: border-box; 47 | /* Older FF */ 48 | 49 | -ms-box-sizing: border-box; 50 | /* IE8 */ 51 | 52 | box-sizing: border-box; 53 | /* CSS3 spec*/ 54 | 55 | } 56 | .input-prepend input[class*="span"], .input-append input[class*="span"] { 57 | width: auto; 58 | } 59 | input[type="checkbox"], input[type="radio"] { 60 | border: 1px solid #ccc; 61 | } 62 | .form-horizontal .control-group > label { 63 | float: none; 64 | width: auto; 65 | padding-top: 0; 66 | text-align: left; 67 | } 68 | .form-horizontal .controls { 69 | margin-left: 0; 70 | } 71 | .form-horizontal .control-list { 72 | padding-top: 0; 73 | } 74 | .form-horizontal .form-actions { 75 | padding-left: 10px; 76 | padding-right: 10px; 77 | } 78 | .modal { 79 | position: absolute; 80 | top: 10px; 81 | left: 10px; 82 | right: 10px; 83 | width: auto; 84 | margin: 0; 85 | } 86 | .modal.fade.in { 87 | top: auto; 88 | } 89 | .modal-header .close { 90 | padding: 10px; 91 | margin: -10px; 92 | } 93 | .carousel-caption { 94 | position: static; 95 | } 96 | } 97 | @media (max-width: 767px) { 98 | .container { 99 | width: auto; 100 | padding: 0 20px; 101 | } 102 | .row-fluid { 103 | width: 100%; 104 | } 105 | .row { 106 | margin-left: 0; 107 | } 108 | .row > [class*="span"], .row-fluid > [class*="span"] { 109 | float: none; 110 | display: block; 111 | width: auto; 112 | margin: 0; 113 | } 114 | } 115 | @media (min-width: 768px) and (max-width: 979px) { 116 | .row { 117 | margin-left: -20px; 118 | *zoom: 1; 119 | } 120 | .row:before, .row:after { 121 | display: table; 122 | content: ""; 123 | } 124 | .row:after { 125 | clear: both; 126 | } 127 | [class*="span"] { 128 | float: left; 129 | margin-left: 20px; 130 | } 131 | .span1 { 132 | width: 42px; 133 | } 134 | .span2 { 135 | width: 104px; 136 | } 137 | .span3 { 138 | width: 166px; 139 | } 140 | .span4 { 141 | width: 228px; 142 | } 143 | .span5 { 144 | width: 290px; 145 | } 146 | .span6 { 147 | width: 352px; 148 | } 149 | .span7 { 150 | width: 414px; 151 | } 152 | .span8 { 153 | width: 476px; 154 | } 155 | .span9 { 156 | width: 538px; 157 | } 158 | .span10 { 159 | width: 600px; 160 | } 161 | .span11 { 162 | width: 662px; 163 | } 164 | .span12, .container { 165 | width: 724px; 166 | } 167 | .offset1 { 168 | margin-left: 82px; 169 | } 170 | .offset2 { 171 | margin-left: 144px; 172 | } 173 | .offset3 { 174 | margin-left: 206px; 175 | } 176 | .offset4 { 177 | margin-left: 268px; 178 | } 179 | .offset5 { 180 | margin-left: 330px; 181 | } 182 | .offset6 { 183 | margin-left: 392px; 184 | } 185 | .offset7 { 186 | margin-left: 454px; 187 | } 188 | .offset8 { 189 | margin-left: 516px; 190 | } 191 | .offset9 { 192 | margin-left: 578px; 193 | } 194 | .offset10 { 195 | margin-left: 640px; 196 | } 197 | .offset11 { 198 | margin-left: 702px; 199 | } 200 | .row-fluid { 201 | width: 100%; 202 | *zoom: 1; 203 | } 204 | .row-fluid:before, .row-fluid:after { 205 | display: table; 206 | content: ""; 207 | } 208 | .row-fluid:after { 209 | clear: both; 210 | } 211 | .row-fluid > [class*="span"] { 212 | float: left; 213 | margin-left: 2.762430939%; 214 | } 215 | .row-fluid > [class*="span"]:first-child { 216 | margin-left: 0; 217 | } 218 | .row-fluid > .span1 { 219 | width: 5.801104972%; 220 | } 221 | .row-fluid > .span2 { 222 | width: 14.364640883%; 223 | } 224 | .row-fluid > .span3 { 225 | width: 22.928176794%; 226 | } 227 | .row-fluid > .span4 { 228 | width: 31.491712705%; 229 | } 230 | .row-fluid > .span5 { 231 | width: 40.055248616%; 232 | } 233 | .row-fluid > .span6 { 234 | width: 48.618784527%; 235 | } 236 | .row-fluid > .span7 { 237 | width: 57.182320438000005%; 238 | } 239 | .row-fluid > .span8 { 240 | width: 65.74585634900001%; 241 | } 242 | .row-fluid > .span9 { 243 | width: 74.30939226%; 244 | } 245 | .row-fluid > .span10 { 246 | width: 82.87292817100001%; 247 | } 248 | .row-fluid > .span11 { 249 | width: 91.436464082%; 250 | } 251 | .row-fluid > .span12 { 252 | width: 99.999999993%; 253 | } 254 | input.span1, textarea.span1, .uneditable-input.span1 { 255 | width: 32px; 256 | } 257 | input.span2, textarea.span2, .uneditable-input.span2 { 258 | width: 94px; 259 | } 260 | input.span3, textarea.span3, .uneditable-input.span3 { 261 | width: 156px; 262 | } 263 | input.span4, textarea.span4, .uneditable-input.span4 { 264 | width: 218px; 265 | } 266 | input.span5, textarea.span5, .uneditable-input.span5 { 267 | width: 280px; 268 | } 269 | input.span6, textarea.span6, .uneditable-input.span6 { 270 | width: 342px; 271 | } 272 | input.span7, textarea.span7, .uneditable-input.span7 { 273 | width: 404px; 274 | } 275 | input.span8, textarea.span8, .uneditable-input.span8 { 276 | width: 466px; 277 | } 278 | input.span9, textarea.span9, .uneditable-input.span9 { 279 | width: 528px; 280 | } 281 | input.span10, textarea.span10, .uneditable-input.span10 { 282 | width: 590px; 283 | } 284 | input.span11, textarea.span11, .uneditable-input.span11 { 285 | width: 652px; 286 | } 287 | input.span12, textarea.span12, .uneditable-input.span12 { 288 | width: 714px; 289 | } 290 | } 291 | @media (max-width: 979px) { 292 | body { 293 | padding-top: 0; 294 | } 295 | .navbar-fixed-top { 296 | position: static; 297 | margin-bottom: 18px; 298 | } 299 | .navbar-fixed-top .navbar-inner { 300 | padding: 5px; 301 | } 302 | .navbar .container { 303 | width: auto; 304 | padding: 0; 305 | } 306 | .navbar .brand { 307 | padding-left: 10px; 308 | padding-right: 10px; 309 | margin: 0 0 0 -5px; 310 | } 311 | .navbar .nav-collapse { 312 | clear: left; 313 | } 314 | .navbar .nav { 315 | float: none; 316 | margin: 0 0 9px; 317 | } 318 | .navbar .nav > li { 319 | float: none; 320 | } 321 | .navbar .nav > li > a { 322 | margin-bottom: 2px; 323 | } 324 | .navbar .nav > .divider-vertical { 325 | display: none; 326 | } 327 | .navbar .nav .nav-header { 328 | color: #999999; 329 | text-shadow: none; 330 | } 331 | .navbar .nav > li > a, .navbar .dropdown-menu a { 332 | padding: 6px 15px; 333 | font-weight: bold; 334 | color: #999999; 335 | -webkit-border-radius: 3px; 336 | -moz-border-radius: 3px; 337 | border-radius: 3px; 338 | } 339 | .navbar .dropdown-menu li + li a { 340 | margin-bottom: 2px; 341 | } 342 | .navbar .nav > li > a:hover, .navbar .dropdown-menu a:hover { 343 | background-color: #222222; 344 | } 345 | .navbar .dropdown-menu { 346 | position: static; 347 | top: auto; 348 | left: auto; 349 | float: none; 350 | display: block; 351 | max-width: none; 352 | margin: 0 15px; 353 | padding: 0; 354 | background-color: transparent; 355 | border: none; 356 | -webkit-border-radius: 0; 357 | -moz-border-radius: 0; 358 | border-radius: 0; 359 | -webkit-box-shadow: none; 360 | -moz-box-shadow: none; 361 | box-shadow: none; 362 | } 363 | .navbar .dropdown-menu:before, .navbar .dropdown-menu:after { 364 | display: none; 365 | } 366 | .navbar .dropdown-menu .divider { 367 | display: none; 368 | } 369 | .navbar-form, .navbar-search { 370 | float: none; 371 | padding: 9px 15px; 372 | margin: 9px 0; 373 | border-top: 1px solid #222222; 374 | border-bottom: 1px solid #222222; 375 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 376 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 377 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 378 | } 379 | .navbar .nav.pull-right { 380 | float: none; 381 | margin-left: 0; 382 | } 383 | .navbar-static .navbar-inner { 384 | padding-left: 10px; 385 | padding-right: 10px; 386 | } 387 | .btn-navbar { 388 | display: block; 389 | } 390 | .nav-collapse { 391 | overflow: hidden; 392 | height: 0; 393 | } 394 | } 395 | @media (min-width: 980px) { 396 | .nav-collapse.collapse { 397 | height: auto !important; 398 | } 399 | } 400 | @media (min-width: 1200px) { 401 | .row { 402 | margin-left: -30px; 403 | *zoom: 1; 404 | } 405 | .row:before, .row:after { 406 | display: table; 407 | content: ""; 408 | } 409 | .row:after { 410 | clear: both; 411 | } 412 | [class*="span"] { 413 | float: left; 414 | margin-left: 30px; 415 | } 416 | .span1 { 417 | width: 70px; 418 | } 419 | .span2 { 420 | width: 170px; 421 | } 422 | .span3 { 423 | width: 270px; 424 | } 425 | .span4 { 426 | width: 370px; 427 | } 428 | .span5 { 429 | width: 470px; 430 | } 431 | .span6 { 432 | width: 570px; 433 | } 434 | .span7 { 435 | width: 670px; 436 | } 437 | .span8 { 438 | width: 770px; 439 | } 440 | .span9 { 441 | width: 870px; 442 | } 443 | .span10 { 444 | width: 970px; 445 | } 446 | .span11 { 447 | width: 1070px; 448 | } 449 | .span12, .container { 450 | width: 1170px; 451 | } 452 | .offset1 { 453 | margin-left: 130px; 454 | } 455 | .offset2 { 456 | margin-left: 230px; 457 | } 458 | .offset3 { 459 | margin-left: 330px; 460 | } 461 | .offset4 { 462 | margin-left: 430px; 463 | } 464 | .offset5 { 465 | margin-left: 530px; 466 | } 467 | .offset6 { 468 | margin-left: 630px; 469 | } 470 | .offset7 { 471 | margin-left: 730px; 472 | } 473 | .offset8 { 474 | margin-left: 830px; 475 | } 476 | .offset9 { 477 | margin-left: 930px; 478 | } 479 | .offset10 { 480 | margin-left: 1030px; 481 | } 482 | .offset11 { 483 | margin-left: 1130px; 484 | } 485 | .row-fluid { 486 | width: 100%; 487 | *zoom: 1; 488 | } 489 | .row-fluid:before, .row-fluid:after { 490 | display: table; 491 | content: ""; 492 | } 493 | .row-fluid:after { 494 | clear: both; 495 | } 496 | .row-fluid > [class*="span"] { 497 | float: left; 498 | margin-left: 2.564102564%; 499 | } 500 | .row-fluid > [class*="span"]:first-child { 501 | margin-left: 0; 502 | } 503 | .row-fluid > .span1 { 504 | width: 5.982905983%; 505 | } 506 | .row-fluid > .span2 { 507 | width: 14.529914530000001%; 508 | } 509 | .row-fluid > .span3 { 510 | width: 23.076923077%; 511 | } 512 | .row-fluid > .span4 { 513 | width: 31.623931624%; 514 | } 515 | .row-fluid > .span5 { 516 | width: 40.170940171000005%; 517 | } 518 | .row-fluid > .span6 { 519 | width: 48.717948718%; 520 | } 521 | .row-fluid > .span7 { 522 | width: 57.264957265%; 523 | } 524 | .row-fluid > .span8 { 525 | width: 65.81196581200001%; 526 | } 527 | .row-fluid > .span9 { 528 | width: 74.358974359%; 529 | } 530 | .row-fluid > .span10 { 531 | width: 82.905982906%; 532 | } 533 | .row-fluid > .span11 { 534 | width: 91.45299145300001%; 535 | } 536 | .row-fluid > .span12 { 537 | width: 100%; 538 | } 539 | input.span1, textarea.span1, .uneditable-input.span1 { 540 | width: 60px; 541 | } 542 | input.span2, textarea.span2, .uneditable-input.span2 { 543 | width: 160px; 544 | } 545 | input.span3, textarea.span3, .uneditable-input.span3 { 546 | width: 260px; 547 | } 548 | input.span4, textarea.span4, .uneditable-input.span4 { 549 | width: 360px; 550 | } 551 | input.span5, textarea.span5, .uneditable-input.span5 { 552 | width: 460px; 553 | } 554 | input.span6, textarea.span6, .uneditable-input.span6 { 555 | width: 560px; 556 | } 557 | input.span7, textarea.span7, .uneditable-input.span7 { 558 | width: 660px; 559 | } 560 | input.span8, textarea.span8, .uneditable-input.span8 { 561 | width: 760px; 562 | } 563 | input.span9, textarea.span9, .uneditable-input.span9 { 564 | width: 860px; 565 | } 566 | input.span10, textarea.span10, .uneditable-input.span10 { 567 | width: 960px; 568 | } 569 | input.span11, textarea.span11, .uneditable-input.span11 { 570 | width: 1060px; 571 | } 572 | input.span12, textarea.span12, .uneditable-input.span12 { 573 | width: 1160px; 574 | } 575 | .thumbnails { 576 | margin-left: -30px; 577 | } 578 | .thumbnails > li { 579 | margin-left: 30px; 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /demos/presence/html/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/nullmq/ec282393add9a47a89b26242c2ff974a9ae934ff/demos/presence/html/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /demos/presence/html/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/nullmq/ec282393add9a47a89b26242c2ff974a9ae934ff/demos/presence/html/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /demos/presence/html/img/hand_thumbsdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/nullmq/ec282393add9a47a89b26242c2ff974a9ae934ff/demos/presence/html/img/hand_thumbsdown.png -------------------------------------------------------------------------------- /demos/presence/html/img/hand_thumbsup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/nullmq/ec282393add9a47a89b26242c2ff974a9ae934ff/demos/presence/html/img/hand_thumbsup.png -------------------------------------------------------------------------------- /demos/presence/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Presence 6 | 7 | 8 | 26 | 27 | 28 |
    29 | 33 |
    34 |
    35 |
    36 |
    37 | 38 |

    Connecting...

    39 |

    You are connected as {{name}}

    40 |

    Disconnecting...

    41 |

    You are not connected

    42 |

    Client status: {{presenceClient.status}}

    43 |
    44 |
    45 | 46 | 47 | 48 | Disconnect 49 |
    50 |
    51 |
    52 |

    53 | 54 | online 55 | offline 56 | 57 | {{peer.name}} 58 | {{peer.text}} 59 |

    60 |
    61 |
    62 |
    63 |
    64 |

    No peers online

    65 |
    66 |
    67 |
    68 |
    69 |
    70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
    [{{message.timestamp|timestamp}}]<{{message.name}}>{{message.text}}
    79 |
    80 |
    81 |
    82 | 83 | 84 |
    85 |
    86 |
    87 |
    88 |
    89 |
    90 | 91 | 92 | 93 | 94 | 100 | 101 | -------------------------------------------------------------------------------- /demos/presence/html/js/nullmq-0.1.0.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Queue, Socket, Stomp, assert, nullmq, 3 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 4 | __indexOf = Array.prototype.indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 5 | 6 | nullmq = { 7 | PUB: 'pub', 8 | SUB: 'sub', 9 | REQ: 'req', 10 | REP: 'rep', 11 | XREQ: 'dealer', 12 | XREP: 'router', 13 | PULL: 'pull', 14 | PUSH: 'push', 15 | DEALER: 'dealer', 16 | ROUTER: 'router', 17 | HWM: 100, 18 | IDENTITY: 101, 19 | SUBSCRIBE: 102, 20 | UNSUBSCRIBE: 103, 21 | _SENDERS: ['req', 'dealer', 'push', 'pub', 'router', 'rep'] 22 | }; 23 | 24 | assert = function(description, condition) { 25 | if (condition == null) condition = false; 26 | if (!condition) throw Error("Assertion: " + description); 27 | }; 28 | 29 | Queue = (function() { 30 | 31 | function Queue(maxsize) { 32 | this.maxsize = maxsize != null ? maxsize : null; 33 | this.queue = []; 34 | this.offset = 0; 35 | this.watches = []; 36 | } 37 | 38 | Queue.prototype.getLength = function() { 39 | return this.queue.length - this.offset; 40 | }; 41 | 42 | Queue.prototype.isEmpty = function() { 43 | return this.queue.length === 0; 44 | }; 45 | 46 | Queue.prototype.isFull = function() { 47 | if (this.maxsize === null) return false; 48 | return this.getLength() >= this.maxsize; 49 | }; 50 | 51 | Queue.prototype.put = function(item) { 52 | var _base; 53 | if (!this.isFull()) { 54 | this.queue.push(item); 55 | if (typeof (_base = this.watches.shift()) === "function") _base(); 56 | return item; 57 | } else { 58 | 59 | } 60 | }; 61 | 62 | Queue.prototype.get = function() { 63 | var item; 64 | if (this.queue.length === 0) return; 65 | item = this.queue[this.offset]; 66 | if (++this.offset * 2 >= this.queue.length) { 67 | this.queue = this.queue.slice(this.offset); 68 | this.offset = 0; 69 | } 70 | return item; 71 | }; 72 | 73 | Queue.prototype.peek = function() { 74 | if (this.queue.length > 0) { 75 | return this.queue[this.offset]; 76 | } else { 77 | return; 78 | } 79 | }; 80 | 81 | Queue.prototype.watch = function(fn) { 82 | if (this.queue.length === 0) { 83 | return this.watches.push(fn); 84 | } else { 85 | return fn(); 86 | } 87 | }; 88 | 89 | return Queue; 90 | 91 | })(); 92 | 93 | nullmq.Context = (function() { 94 | 95 | function Context(url, onconnect) { 96 | var _this = this; 97 | this.url = url; 98 | if (onconnect == null) onconnect = function() {}; 99 | this.active = false; 100 | this.client = Stomp.client(this.url); 101 | this.client.connect("guest", "guest", function() { 102 | var op, _results; 103 | _this.active = true; 104 | _results = []; 105 | while (op = _this.pending_operations.shift()) { 106 | _results.push(op()); 107 | } 108 | return _results; 109 | }); 110 | this.pending_operations = [onconnect]; 111 | this.sockets = []; 112 | } 113 | 114 | Context.prototype.socket = function(type) { 115 | return new Socket(this, type); 116 | }; 117 | 118 | Context.prototype.term = function() { 119 | var _this = this; 120 | return this._when_connected(function() { 121 | var socket, _i, _len, _ref; 122 | assert("context is already connected", _this.client.connected); 123 | _ref = _this.sockets; 124 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 125 | socket = _ref[_i]; 126 | socket.close(); 127 | } 128 | return _this.client.disconnect(); 129 | }); 130 | }; 131 | 132 | Context.prototype._send = function(socket, destination, message) { 133 | var _this = this; 134 | return this._when_connected(function() { 135 | var headers, part, _i, _len; 136 | assert("context is already connected", _this.client.connected); 137 | headers = { 138 | 'socket': socket.type 139 | }; 140 | if (socket.type === nullmq.REQ) { 141 | headers['reply-to'] = socket.connections[destination]; 142 | } 143 | if (socket.type === nullmq.REP) { 144 | headers['reply-to'] = socket.last_recv.reply_to; 145 | } 146 | if (message instanceof Array) { 147 | headers['transaction'] = Math.random() + ''; 148 | _this.client.begin(transaction); 149 | for (_i = 0, _len = message.length; _i < _len; _i++) { 150 | part = message[_i]; 151 | _this.client.send(destination, headers, part); 152 | } 153 | return _this.client.commit(transaction); 154 | } else { 155 | return _this.client.send(destination, headers, message.toString()); 156 | } 157 | }); 158 | }; 159 | 160 | Context.prototype._subscribe = function(type, socket, destination) { 161 | var _this = this; 162 | return this._when_connected(function() { 163 | var id; 164 | assert("context is already connected", _this.client.connected); 165 | id = _this.client.subscribe(destination, function(frame) { 166 | var envelope; 167 | envelope = { 168 | 'message': frame.body, 169 | 'destination': frame.destination 170 | }; 171 | if (frame.headers['reply-to'] != null) { 172 | envelope['reply_to'] = frame.headers['reply-to']; 173 | } 174 | return socket.recv_queue.put(envelope); 175 | }, { 176 | 'socket': socket.type, 177 | 'type': type 178 | }); 179 | return socket.connections[destination] = id; 180 | }); 181 | }; 182 | 183 | Context.prototype._connect = function(socket, destination) { 184 | return this._subscribe('connect', socket, destination); 185 | }; 186 | 187 | Context.prototype._bind = function(socket, destination) { 188 | return this._subscribe('bind', socket, destination); 189 | }; 190 | 191 | Context.prototype._when_connected = function(op) { 192 | if (this.client.connected) { 193 | return op(); 194 | } else { 195 | return this.pending_operations.push(op); 196 | } 197 | }; 198 | 199 | return Context; 200 | 201 | })(); 202 | 203 | Socket = (function() { 204 | 205 | function Socket(context, type) { 206 | var _ref; 207 | this.context = context; 208 | this.type = type; 209 | this._dispatch_outgoing = __bind(this._dispatch_outgoing, this); 210 | this.client = this.context.client; 211 | this.closed = false; 212 | this.recv_queue = new Queue(); 213 | this.send_queue = new Queue(); 214 | this.identity = null; 215 | this.linger = -1; 216 | this.filters = []; 217 | this.connections = {}; 218 | this.rr_index = 0; 219 | this.last_recv = void 0; 220 | this.context.sockets.push(this); 221 | if (_ref = this.type, __indexOf.call(nullmq._SENDERS, _ref) >= 0) { 222 | this.send_queue.watch(this._dispatch_outgoing); 223 | } 224 | } 225 | 226 | Socket.prototype.connect = function(destination) { 227 | if (__indexOf.call(Object.keys(this.connections), destination) >= 0) return; 228 | return this.context._connect(this, destination); 229 | }; 230 | 231 | Socket.prototype.bind = function(destination) { 232 | if (__indexOf.call(Object.keys(this.connections), destination) >= 0) return; 233 | return this.context._bind(this, destination); 234 | }; 235 | 236 | Socket.prototype.setsockopt = function(option, value) { 237 | var _ref; 238 | switch (option) { 239 | case nullmq.HWM: 240 | return this.hwm = value; 241 | case nullmq.IDENTITY: 242 | return this._identity(value); 243 | case nullmq.LINGER: 244 | return this.linger = value; 245 | case nullmq.SUBSCRIBE: 246 | if (this.type !== nullmq.SUB) return; 247 | if (_ref = !value, __indexOf.call(this.filters, _ref) >= 0) { 248 | this.filters.push(value); 249 | } 250 | return value; 251 | case nullmq.UNSUBSCRIBE: 252 | if (this.type !== nullmq.SUB) return; 253 | if (__indexOf.call(this.filters, value) >= 0) { 254 | this.filters.splice(this.filters.indexOf(value), 1); 255 | } 256 | return value; 257 | default: 258 | return; 259 | } 260 | }; 261 | 262 | Socket.prototype.getsockopt = function(option) { 263 | switch (option) { 264 | case nullmq.HWM: 265 | return this.hwm; 266 | case nullmq.IDENTITY: 267 | return this.identity; 268 | case nullmq.LINGER: 269 | return this.linger; 270 | default: 271 | return; 272 | } 273 | }; 274 | 275 | Socket.prototype.close = function() { 276 | var destination, id, _ref; 277 | _ref = this.connections; 278 | for (destination in _ref) { 279 | id = _ref[destination]; 280 | this.client.unsubscribe(id); 281 | } 282 | this.connections = {}; 283 | return this.closed = true; 284 | }; 285 | 286 | Socket.prototype.send = function(message) { 287 | var _ref; 288 | if ((_ref = this.type) === nullmq.PULL || _ref === nullmq.SUB) { 289 | throw Error("Sending is not implemented for this socket type"); 290 | } 291 | return this.send_queue.put(message); 292 | }; 293 | 294 | Socket.prototype.recv = function(callback) { 295 | var _this = this; 296 | return this.recv_queue.watch(function() { 297 | return callback(_this._recv()); 298 | }); 299 | }; 300 | 301 | Socket.prototype.recvall = function(callback) { 302 | var watcher, 303 | _this = this; 304 | watcher = function() { 305 | callback(_this._recv()); 306 | return _this.recv_queue.watch(watcher); 307 | }; 308 | return this.recv_queue.watch(watcher); 309 | }; 310 | 311 | Socket.prototype._recv = function() { 312 | var envelope; 313 | envelope = this.recv_queue.get(); 314 | this.last_recv = envelope; 315 | return envelope.message; 316 | }; 317 | 318 | Socket.prototype._identity = function(value) { 319 | return this.identity = value; 320 | }; 321 | 322 | Socket.prototype._deliver_round_robin = function(message) { 323 | var connection_count, destination; 324 | destination = Object.keys(this.connections)[this.rr_index]; 325 | this.context._send(this, destination, message); 326 | connection_count = Object.keys(this.connections).length; 327 | return this.rr_index = ++this.rr_index % connection_count; 328 | }; 329 | 330 | Socket.prototype._deliver_fanout = function(message) { 331 | var destination, id, _ref, _results; 332 | _ref = this.connections; 333 | _results = []; 334 | for (destination in _ref) { 335 | id = _ref[destination]; 336 | _results.push(this.context._send(this, destination, message)); 337 | } 338 | return _results; 339 | }; 340 | 341 | Socket.prototype._deliver_routed = function(message) { 342 | var destination; 343 | destination = message.shift(); 344 | return this.context._send(this, destination, message); 345 | }; 346 | 347 | Socket.prototype._deliver_back = function(message) { 348 | return this.context._send(this, this.last_recv.destination, message); 349 | }; 350 | 351 | Socket.prototype._dispatch_outgoing = function() { 352 | var message; 353 | if (this.context.active) { 354 | message = this.send_queue.get(); 355 | switch (this.type) { 356 | case nullmq.REQ: 357 | case nullmq.DEALER: 358 | case nullmq.PUSH: 359 | this._deliver_round_robin(message); 360 | break; 361 | case nullmq.PUB: 362 | this._deliver_fanout(message); 363 | break; 364 | case nullmq.ROUTER: 365 | this._deliver_routed(message); 366 | break; 367 | case nullmq.REP: 368 | this._deliver_back(message); 369 | break; 370 | default: 371 | assert("outgoing dispatching shouldn't happen for this socket type"); 372 | } 373 | return this.send_queue.watch(this._dispatch_outgoing); 374 | } else { 375 | return setTimeout(this._dispatch_outgoing, 20); 376 | } 377 | }; 378 | 379 | return Socket; 380 | 381 | })(); 382 | 383 | if (typeof window !== "undefined" && window !== null) { 384 | window.nullmq = nullmq; 385 | if (!(window.Stomp != null)) { 386 | console.log("Required Stomp library not loaded."); 387 | } else { 388 | Stomp = window.Stomp; 389 | } 390 | } else { 391 | exports.nullmq = nullmq; 392 | exports.Queue = Queue; 393 | Stomp = require('./lib/stomp.js').Stomp; 394 | } 395 | 396 | }).call(this); 397 | -------------------------------------------------------------------------------- /demos/presence/html/js/presence-0.1.0.js: -------------------------------------------------------------------------------- 1 | MainController.$inject = ['$updateView']; 2 | function MainController($updateView) { 3 | this.STATUS_CONNECTING = Client.CONNECTING; 4 | this.STATUS_CONNECTED = Client.CONNECTED; 5 | this.STATUS_DISCONNECTED = Client.DISCONNECTED; 6 | this.STATUS_DISCONNECTING = Client.DISCONNECTING; 7 | 8 | this.peers = {}; 9 | 10 | this.getPeers = function() { 11 | return Object.keys(this.peers).map(function(name) { 12 | return this.peers[name] 13 | }.bind(this)) 14 | }.bind(this); 15 | 16 | this.scrollToBottom = function() { 17 | var objDiv = document.getElementById("chat_window"); 18 | objDiv.scrollTop = objDiv.scrollHeight; 19 | } 20 | 21 | this.presenceClient = new Client({ 22 | subscribe: "tcp://localhost:10001" 23 | , request: "tcp://localhost:10002" 24 | , push: "tcp://localhost:10003" 25 | }); 26 | this.presenceClient.onResponse = function(payload) { 27 | Object.merge(this.peers, JSON.parse(payload)); 28 | $updateView(); 29 | }.bind(this); 30 | this.presenceClient.onPublish = function(payload) { 31 | var peer = JSON.parse(payload); 32 | this.peers[peer['name']] = this.peers[peer['name']] || {} 33 | Object.merge(this.peers[peer['name']], peer); 34 | $updateView(); 35 | }.bind(this); 36 | var interval; 37 | this.presenceClient.onConnect = function() { 38 | interval = setInterval(function() { 39 | this.presenceClient.push(JSON.stringify({ 40 | name: this.name 41 | , online: true 42 | , text: this.text 43 | , timeout: 2 44 | })); 45 | }.bind(this), 1000); 46 | }.bind(this); 47 | this.presenceClient.onDisconnect = function() { 48 | clearInterval(interval); 49 | this.peers = {}; 50 | }.bind(this); 51 | 52 | this.messages = []; 53 | this.chatClient = new Client({ 54 | subscribe: "tcp://localhost:10004" 55 | , request: "tcp://localhost:10005" 56 | , push: "tcp://localhost:10006" 57 | }); 58 | this.chatClient.onResponse = function(payload) { 59 | JSON.parse(payload).forEach(function(msg) { 60 | this.messages.push(msg); 61 | }.bind(this)); 62 | this.$root.$eval(); 63 | this.scrollToBottom(); 64 | }.bind(this); 65 | this.chatClient.onPublish = function(payload) { 66 | var msg = JSON.parse(payload); 67 | this.messages.push(msg); 68 | this.$root.$eval(); 69 | this.scrollToBottom(); 70 | }.bind(this); 71 | this.chatClient.onDisconnect = function() { 72 | this.messages = [] 73 | }.bind(this); 74 | 75 | this.sendMessage = function() { 76 | if (this.message) { 77 | this.chatClient.push(JSON.stringify({ 78 | name: this.name 79 | , text: this.message 80 | })); 81 | this.message = ''; 82 | } 83 | }.bind(this) 84 | 85 | this.connect = function() { 86 | this.presenceClient.connect(); 87 | this.chatClient.connect(); 88 | $updateView(); 89 | }.bind(this); 90 | 91 | this.disconnect = function() { 92 | this.presenceClient.disconnect(); 93 | this.chatClient.disconnect(); 94 | $updateView(); 95 | }.bind(this); 96 | } 97 | 98 | Client.CONNECTING = 2; 99 | Client.CONNECTED = 0; 100 | Client.DISCONNECTED = 1; 101 | Client.DISCONNECTING = 3; 102 | 103 | function Client(options) { 104 | this.status = Client.DISCONNECTED; 105 | this.options = options; 106 | 107 | this.onResponse = function(payload) {}; 108 | this.onPublish = function(payload) {}; 109 | this.onConnect = function() {}; 110 | this.onDisconnect = function() {}; 111 | }; 112 | 113 | Client.prototype.connect = function() { 114 | if (this.status != Client.DISCONNECTED) { 115 | return; 116 | } 117 | 118 | this.context = new nullmq.Context('ws://'+window.location.hostname+':9000'); 119 | this.status = Client.CONNECTING; 120 | 121 | this.startSub(); 122 | this.doRequest(); 123 | this.startPush(); 124 | 125 | this.status = Client.CONNECTED; 126 | 127 | this.onConnect(); 128 | }; 129 | 130 | Client.prototype.disconnect = function() { 131 | if (this.status != Client.CONNECTED) { 132 | return; 133 | } 134 | 135 | this.status = Client.DISCONNECTING; 136 | 137 | this.stopSub(); 138 | this.stopPush(); 139 | this.context.term(); 140 | delete this.context; 141 | 142 | this.status = Client.DISCONNECTED; 143 | 144 | this.onDisconnect(); 145 | }; 146 | 147 | Client.prototype.doRequest = function() { 148 | var req = this.context.socket(nullmq.REQ); 149 | req.connect(this.options['request']); 150 | req.send(''); 151 | req.recv(this.onResponse); 152 | } 153 | 154 | Client.prototype.startSub = function() { 155 | this.subSock = this.context.socket(nullmq.SUB); 156 | 157 | this.subSock.connect(this.options['subscribe']); 158 | this.subSock.setsockopt(nullmq.SUBSCRIBE, ''); 159 | 160 | this.subSock.recvall(this.onPublish); 161 | } 162 | 163 | Client.prototype.stopSub = function() { 164 | (this.subSock.close || angular.noop)(); 165 | } 166 | 167 | Client.prototype.startPush = function() { 168 | this.pushSock = this.context.socket(nullmq.PUSH); 169 | this.pushSock.connect(this.options['push']); 170 | } 171 | 172 | Client.prototype.stopPush = function() { 173 | (this.pushSock.close || angular.noop)(); 174 | } 175 | 176 | Client.prototype.push = function(payload) { 177 | if (this.status != Client.CONNECTED) { 178 | return; 179 | } 180 | this.pushSock.send(payload); 181 | } 182 | 183 | Object.merge = function(destination, source) { 184 | for (var property in source) { 185 | if (source.hasOwnProperty(property)) { 186 | destination[property] = source[property]; 187 | } 188 | } 189 | return destination; 190 | }; 191 | -------------------------------------------------------------------------------- /demos/presence/html/js/stomp-0.1.0.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Client, Stomp; 3 | var __hasProp = Object.prototype.hasOwnProperty, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 4 | Stomp = { 5 | WebSocket: typeof WebSocket !== "undefined" && WebSocket !== null ? WebSocket : null, 6 | frame: function(command, headers, body) { 7 | if (headers == null) { 8 | headers = []; 9 | } 10 | if (body == null) { 11 | body = ''; 12 | } 13 | return { 14 | command: command, 15 | headers: headers, 16 | body: body, 17 | id: headers.id, 18 | receipt: headers.receipt, 19 | transaction: headers.transaction, 20 | destination: headers.destination, 21 | subscription: headers.subscription, 22 | error: null, 23 | toString: function() { 24 | var lines, name, value; 25 | lines = [command]; 26 | for (name in headers) { 27 | if (!__hasProp.call(headers, name)) continue; 28 | value = headers[name]; 29 | lines.push("" + name + ": " + value); 30 | } 31 | lines.push('\n' + body); 32 | return lines.join('\n'); 33 | } 34 | }; 35 | }, 36 | unmarshal: function(data) { 37 | var body, chr, command, divider, headerLines, headers, i, idx, line, trim, _ref, _ref2, _ref3; 38 | divider = data.search(/\n\n/); 39 | headerLines = data.substring(0, divider).split('\n'); 40 | command = headerLines.shift(); 41 | headers = {}; 42 | body = ''; 43 | trim = function(str) { 44 | return str.replace(/^\s+/g, '').replace(/\s+$/g, ''); 45 | }; 46 | line = idx = null; 47 | for (i = 0, _ref = headerLines.length; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) { 48 | line = headerLines[i]; 49 | idx = line.indexOf(':'); 50 | headers[trim(line.substring(0, idx))] = trim(line.substring(idx + 1)); 51 | } 52 | chr = null; 53 | for (i = _ref2 = divider + 2, _ref3 = data.length; _ref2 <= _ref3 ? i < _ref3 : i > _ref3; _ref2 <= _ref3 ? i++ : i--) { 54 | chr = data.charAt(i); 55 | if (chr === '\0') { 56 | break; 57 | } 58 | body += chr; 59 | } 60 | return Stomp.frame(command, headers, body); 61 | }, 62 | marshal: function(command, headers, body) { 63 | return Stomp.frame(command, headers, body).toString() + '\0'; 64 | }, 65 | client: function(url) { 66 | return new Client(url); 67 | } 68 | }; 69 | Client = (function() { 70 | function Client(url) { 71 | this.url = url; 72 | this.counter = 0; 73 | this.connected = false; 74 | this.subscriptions = {}; 75 | } 76 | Client.prototype._transmit = function(command, headers, body) { 77 | var out; 78 | out = Stomp.marshal(command, headers, body); 79 | if (typeof this.debug === "function") { 80 | this.debug(">>> " + out); 81 | } 82 | return this.ws.send(out); 83 | }; 84 | Client.prototype.connect = function(login_, passcode_, connectCallback, errorCallback) { 85 | if (typeof this.debug === "function") { 86 | this.debug("Opening Web Socket..."); 87 | } 88 | this.ws = new Stomp.WebSocket(this.url); 89 | this.ws.onmessage = __bind(function(evt) { 90 | var frame, onreceive; 91 | if (typeof this.debug === "function") { 92 | this.debug('<<< ' + evt.data); 93 | } 94 | frame = Stomp.unmarshal(evt.data); 95 | if (frame.command === "CONNECTED" && connectCallback) { 96 | this.connected = true; 97 | return connectCallback(frame); 98 | } else if (frame.command === "MESSAGE") { 99 | onreceive = this.subscriptions[frame.headers.subscription]; 100 | return typeof onreceive === "function" ? onreceive(frame) : void 0; 101 | } 102 | }, this); 103 | this.ws.onclose = __bind(function() { 104 | var msg; 105 | msg = "Whoops! Lost connection to " + this.url; 106 | if (typeof this.debug === "function") { 107 | this.debug(msg); 108 | } 109 | return typeof errorCallback === "function" ? errorCallback(msg) : void 0; 110 | }, this); 111 | this.ws.onopen = __bind(function() { 112 | if (typeof this.debug === "function") { 113 | this.debug('Web Socket Opened...'); 114 | } 115 | return this._transmit("CONNECT", { 116 | login: login_, 117 | passcode: passcode_ 118 | }); 119 | }, this); 120 | return this.connectCallback = connectCallback; 121 | }; 122 | Client.prototype.disconnect = function(disconnectCallback) { 123 | this._transmit("DISCONNECT"); 124 | this.ws.close(); 125 | this.connected = false; 126 | return typeof disconnectCallback === "function" ? disconnectCallback() : void 0; 127 | }; 128 | Client.prototype.send = function(destination, headers, body) { 129 | if (headers == null) { 130 | headers = {}; 131 | } 132 | if (body == null) { 133 | body = ''; 134 | } 135 | headers.destination = destination; 136 | return this._transmit("SEND", headers, body); 137 | }; 138 | Client.prototype.subscribe = function(destination, callback, headers) { 139 | var id; 140 | if (headers == null) { 141 | headers = {}; 142 | } 143 | id = "sub-" + this.counter++; 144 | headers.destination = destination; 145 | headers.id = id; 146 | this.subscriptions[id] = callback; 147 | this._transmit("SUBSCRIBE", headers); 148 | return id; 149 | }; 150 | Client.prototype.unsubscribe = function(id, headers) { 151 | if (headers == null) { 152 | headers = {}; 153 | } 154 | headers.id = id; 155 | delete this.subscriptions[id]; 156 | return this._transmit("UNSUBSCRIBE", headers); 157 | }; 158 | Client.prototype.begin = function(transaction, headers) { 159 | if (headers == null) { 160 | headers = {}; 161 | } 162 | headers.transaction = transaction; 163 | return this._transmit("BEGIN", headers); 164 | }; 165 | Client.prototype.commit = function(transaction, headers) { 166 | if (headers == null) { 167 | headers = {}; 168 | } 169 | headers.transaction = transaction; 170 | return this._transmit("COMMIT", headers); 171 | }; 172 | Client.prototype.abort = function(transaction, headers) { 173 | if (headers == null) { 174 | headers = {}; 175 | } 176 | headers.transaction = transaction; 177 | return this._transmit("ABORT", headers); 178 | }; 179 | Client.prototype.ack = function(message_id, headers) { 180 | if (headers == null) { 181 | headers = {}; 182 | } 183 | headers["message-id"] = message_id; 184 | return this._transmit("ACK", headers); 185 | }; 186 | return Client; 187 | })(); 188 | if (typeof window !== "undefined" && window !== null) { 189 | window.Stomp = Stomp; 190 | } else { 191 | exports.Stomp = Stomp; 192 | } 193 | }).call(this); 194 | -------------------------------------------------------------------------------- /demos/presence/python/README.md: -------------------------------------------------------------------------------- 1 | # Python presence and chat bridge 2 | 3 | ## Install 4 | 5 | `pip install -r demos/requirements.txt` 6 | 7 | ## Run 8 | 9 | `python server.py` 10 | -------------------------------------------------------------------------------- /demos/presence/python/requirements.txt: -------------------------------------------------------------------------------- 1 | git+git://github.com/progrium/stomp4py.git#egg=stomp4py 2 | git+git://github.com/progrium/WebSocket-for-Python#egg=ws4py 3 | git+git://github.com/progrium/gservice.git#egg=gservice 4 | gevent==0.13.3 5 | gevent_zeromq 6 | greenlet==0.3.1 7 | lockfile==0.9.1 8 | nose==1.1.2 9 | python-daemon==1.6 10 | setproctitle==1.1.2 11 | wsgiref==0.1.2 -------------------------------------------------------------------------------- /demos/presence/python/server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | import mimetypes 4 | import collections 5 | import gevent 6 | 7 | from ws4py.server.geventserver import WebSocketServer 8 | from stomp4py.server.websocket import StompHandler 9 | from stomp4py.server.base import Subscription 10 | 11 | from gevent_zeromq import zmq 12 | 13 | context = zmq.Context() 14 | 15 | class NullMQConnection(Subscription): 16 | def __init__(self, handler, frame): 17 | super(NullMQConnection, self).__init__(handler, frame) 18 | self.type = frame.headers.get('type', 'connect') 19 | self.socket_type = frame.headers.get('socket') 20 | self.uid = '%s-%s' % (frame.conn_id, self.id) 21 | self.filter = frame.headers.get('filter', '') 22 | self.listener = None 23 | 24 | StompHandler.subscription_class = NullMQConnection 25 | 26 | class ZeroMQBridge(object): 27 | def __init__(self): 28 | self.connection_by_uid = {} 29 | self.connection_by_destination = collections.defaultdict(set) 30 | 31 | def __call__(self, frame, payload): 32 | if frame is None or frame.command == 'DISCONNECT': 33 | for connection in payload: 34 | self.close(connection) 35 | elif frame.command == 'SUBSCRIBE': 36 | self.connect(payload) 37 | elif frame.command == 'UNSUBSCRIBE': 38 | self.close(payload) 39 | elif frame.command == 'SEND': 40 | self.send(frame) 41 | elif frame.command == 'COMMIT': 42 | for part in payload: 43 | self.send(part) 44 | 45 | def connect(self, connection): 46 | connection.socket = context.socket( 47 | getattr(zmq, connection.socket_type.upper())) 48 | connection.socket.connect('%s' % (connection.destination)) 49 | 50 | self.connection_by_uid[connection.uid] = connection 51 | self.connection_by_destination[connection.destination].add(connection) 52 | if connection.socket_type == 'sub': 53 | connection.socket.setsockopt(zmq.SUBSCRIBE, connection.filter) 54 | if connection.socket_type in ['sub', 'pull', 'dealer']: 55 | def listen_for_messages(): 56 | while connection.active: 57 | connection.send(connection.socket.recv()) 58 | gevent.spawn(listen_for_messages) 59 | 60 | def close(self, connection): 61 | if connection.listener: 62 | connection.listener.kill() 63 | connection.socket.close() 64 | del self.connection_by_uid[connection.uid] 65 | self.connection_by_destination[connection.destination].remove(connection) 66 | 67 | def send(self, frame): 68 | type = frame.headers.get('socket') 69 | if type in ['req', 'rep']: 70 | reply_to = frame.headers.get('reply-to') 71 | if type == 'req': 72 | uid = '%s-%s' % (frame.conn_id, reply_to) 73 | else: 74 | uid = reply_to 75 | conn = self.connection_by_uid[uid] 76 | conn.socket.send(frame.body) 77 | if type == 'req': 78 | def wait_for_reply(): 79 | conn.send(conn.socket.recv()) 80 | conn.listener = gevent.spawn(wait_for_reply) 81 | elif type in ['pub', 'push', 'dealer']: 82 | conns = list(self.connection_by_destination[frame.destination]) 83 | if len(conns): 84 | conns[0].socket.send(frame.body) 85 | 86 | if __name__ == '__main__': 87 | def websocket_handler(websocket, environ): 88 | StompHandler(websocket, ZeroMQBridge()).serve() 89 | 90 | server = WebSocketServer(('0.0.0.0', 9000), websocket_handler) 91 | print "Starting NullMQ-ZeroMQ bridge on 9000" 92 | server.serve_forever() -------------------------------------------------------------------------------- /demos/presence/ruby/Gemfile: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib/', __FILE__) 2 | $:.unshift lib unless $:.include?(lib) 3 | 4 | source "http://rubygems.org" 5 | 6 | gem "ffi-rzmq" 7 | -------------------------------------------------------------------------------- /demos/presence/ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | ffi (1.0.11) 5 | ffi-rzmq (0.9.3) 6 | ffi 7 | 8 | PLATFORMS 9 | ruby 10 | 11 | DEPENDENCIES 12 | ffi-rzmq 13 | -------------------------------------------------------------------------------- /demos/presence/ruby/README.md: -------------------------------------------------------------------------------- 1 | # Presense and chat server in Ruby using 0mq 2 | 3 | ## Installation 4 | 5 | * clone this repository 6 | * `bundle install` 7 | 8 | ## Start presence server 9 | 10 | `./bin/presence_server` 11 | 12 | ## Start chat server 13 | 14 | `./bin/chat_server` 15 | 16 | ## Start presence client 17 | 18 | `./bin/presence_client `, e.g. `./bin/presence_client bulat "I'm online"` 19 | 20 | ## Start chat client 21 | 22 | `./bin/chat_client `, e.g. `./bin/chat_client bulat` 23 | 24 | ### Using chat client 25 | 26 | When client is connected to server, type any message and hit enter to see it displayed to peers 27 | -------------------------------------------------------------------------------- /demos/presence/ruby/bin/chat_client: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rubygems" 4 | require "bundler/setup" 5 | 6 | require "ffi-rzmq" 7 | require "clone" 8 | require "json" 9 | 10 | @name = ARGV[0] 11 | 12 | client = Clone::Client.new(ZMQ::Context.new(1), { 13 | :subscribe => "tcp://localhost:10004", 14 | :request => "tcp://localhost:10005", 15 | :push => "tcp://localhost:10006" 16 | }) 17 | 18 | client.on_response do |payload| 19 | begin 20 | messages = JSON.parse(payload) 21 | messages.each do |msg| 22 | $stdout << sprintf("[%s] <%s> %s\r\n", msg['timestamp'], msg['name'], msg['text']) 23 | end 24 | rescue JSON::ParseError 25 | end 26 | end 27 | 28 | client.on_publish do |payload| 29 | begin 30 | msg = JSON.parse(payload) 31 | $stdout << sprintf("[%s] <%s> %s\r\n", msg['timestamp'], msg['name'], msg['text']) 32 | rescue JSON::ParseError 33 | end 34 | end 35 | 36 | begin 37 | $stdout << "connecting...\r\n" 38 | client.connect 39 | while msg = $stdin.gets.chomp 40 | $stdout << "\r" 41 | client.push(JSON.generate({ 42 | "text" => msg, 43 | "name" => @name 44 | })) 45 | end 46 | rescue Interrupt 47 | $stdout << "disconnecting...\r\n" 48 | client.disconnect 49 | end -------------------------------------------------------------------------------- /demos/presence/ruby/bin/chat_server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rubygems" 4 | require "bundler/setup" 5 | 6 | require "ffi-rzmq" 7 | require "clone" 8 | require "json" 9 | 10 | @messages = [] 11 | 12 | server = Clone::Server.new(ZMQ::Context.new(1), { 13 | :publish => "tcp://*:10004", 14 | :router => "tcp://*:10005", 15 | :pull => "tcp://*:10006" 16 | }) 17 | 18 | server.on_request do |payload| 19 | JSON.generate(@messages.dup) 20 | end 21 | 22 | server.on_push do |payload| 23 | begin 24 | message = JSON.parse(payload) 25 | message['timestamp'] = Time.now 26 | @messages << message 27 | server.publish(JSON.generate(message)) 28 | rescue JSON::ParserError 29 | end 30 | end 31 | 32 | begin 33 | puts "starting server...\r\n" 34 | server.start 35 | loop do 36 | # do nothing, since 37 | sleep() 38 | end 39 | rescue Interrupt 40 | puts "stopping server...\r\n" 41 | server.stop 42 | end -------------------------------------------------------------------------------- /demos/presence/ruby/bin/presence_client: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rubygems" 4 | require "bundler/setup" 5 | 6 | require "ffi-rzmq" 7 | require "clone" 8 | require "json" 9 | 10 | @peers = {} 11 | @name = ARGV[0] 12 | @text = ARGV[1] 13 | 14 | client = Clone::Client.new(ZMQ::Context.new(1), { 15 | :subscribe => "tcp://localhost:10001", 16 | :request => "tcp://localhost:10002", 17 | :push => "tcp://localhost:10003" 18 | }) 19 | 20 | client.on_response do |payload| 21 | begin 22 | peers = JSON.parse(payload) 23 | peers.each do |name, peer| 24 | @peers[name] = peer 25 | end 26 | rescue JSON::ParserError 27 | end 28 | end 29 | 30 | client.on_publish do |payload| 31 | begin 32 | peer = JSON.parse(payload) 33 | @peers[peer['name']] ||= {} 34 | @peers[peer['name']].merge!(peer) 35 | rescue JSON::ParserError 36 | end 37 | end 38 | 39 | begin 40 | $stdout << "connecting...\r\n" 41 | client.connect 42 | loop do 43 | client.push(JSON.generate({ 44 | "name" => @name, 45 | "text" => @text, 46 | "online" => true, 47 | "timeout" => 2 48 | })) 49 | sleep(1) 50 | end 51 | rescue Interrupt 52 | $stdout << "disconnecting...\r\n" 53 | client.disconnect 54 | end 55 | -------------------------------------------------------------------------------- /demos/presence/ruby/bin/presence_server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rubygems" 4 | require "bundler/setup" 5 | 6 | require "ffi-rzmq" 7 | require "clone" 8 | require "json" 9 | 10 | @clients = {} 11 | 12 | server = Clone::Server.new(ZMQ::Context.new(1), { 13 | :publish => "tcp://*:10001", 14 | :router => "tcp://*:10002", 15 | :pull => "tcp://*:10003" 16 | }) 17 | 18 | server.on_request do |payload| 19 | JSON.generate(Hash[*(@clients.dup.map do |k, v| 20 | [k, {"name" => k, "online" => v['online'], "text" => v['text']}] 21 | end.flatten)]) 22 | end 23 | 24 | server.on_push do |payload| 25 | begin 26 | data = JSON.parse(payload) 27 | client = @clients[data["name"]] || {} 28 | client["last_seen"] = Time.now 29 | if client["online"] != data["online"] 30 | server.publish(JSON.generate({ 31 | "name" => data["name"], 32 | "online" => data["online"] 33 | })) 34 | client["online"] = data["online"] 35 | end 36 | if client["text"] != data["text"] 37 | server.publish(JSON.generate({ 38 | "name" => data["name"], 39 | "text" => data["text"] 40 | })) 41 | client["text"] = data["text"] 42 | end 43 | client["timeout"] = data["timeout"] 44 | @clients[data["name"]] = client unless @clients[data["name"]] 45 | rescue JSON::ParserError 46 | end 47 | end 48 | 49 | begin 50 | puts "starting server..." 51 | server.start 52 | 53 | loop do 54 | @clients.dup.each do |name, client| 55 | if client['online'] && ((Time.now - client['last_seen']) > client['timeout']) 56 | @clients[name]['online'] = false 57 | server.publish(JSON.generate({ 58 | "name" => name, 59 | "online" => false 60 | })) 61 | end 62 | end 63 | end 64 | rescue Interrupt 65 | puts "stopping server..." 66 | server.stop 67 | end 68 | -------------------------------------------------------------------------------- /demos/presence/ruby/bin/redirector: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rubygems" 4 | require "bundler/setup" 5 | 6 | require "socket" 7 | 8 | server = TCPServer.new(8888) 9 | private_ipv4 = Socket.ip_address_list.detect{|intf| intf.ipv4_private?}.ip_address 10 | puts "starting redirector to #{private_ipv4}:8080" 11 | loop do 12 | client = server.accept 13 | headers = "HTTP/1.1 301 Moved\r\nDate: Tue, 14 Dec 2010 10:48:45 GMT\r\nServer: Ruby\r\nLocation: http://#{private_ipv4}:8080/\r\nContent-length: 0\r\n\r\n" 14 | client.puts headers 15 | client.close 16 | end 17 | -------------------------------------------------------------------------------- /demos/presence/ruby/lib/clone.rb: -------------------------------------------------------------------------------- 1 | module Clone 2 | autoload :Server, 'clone/server' 3 | autoload :Client, 'clone/client' 4 | end 5 | -------------------------------------------------------------------------------- /demos/presence/ruby/lib/clone/client.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | 3 | module Clone 4 | class Client 5 | def initialize(context, options, threads = Thread, push_queue = nil) 6 | @context = context 7 | @options = options 8 | @threads = threads 9 | @push_queue = push_queue || Queue.new 10 | end 11 | 12 | def connect 13 | @threads.abort_on_exception = true 14 | start_sub 15 | do_request 16 | start_push 17 | end 18 | 19 | def disconnect 20 | stop_sub 21 | stop_push 22 | @context.terminate 23 | end 24 | 25 | def on_response(&block) 26 | @request_handler = block 27 | end 28 | 29 | def on_publish(&block) 30 | @subscribe_handler = block 31 | end 32 | 33 | def push(msg) 34 | @push_queue << msg 35 | end 36 | 37 | private 38 | 39 | def start_sub 40 | @subscribed = false 41 | @sub = spawn_socket(@options[:subscribe], ZMQ::SUB) do |sock| 42 | @subscribed = sock.setsockopt(ZMQ::SUBSCRIBE, '') == 0 unless @subscribed 43 | sock.recv_string(change = '') 44 | @subscribe_handler.call(change) 45 | end 46 | end 47 | 48 | def stop_sub 49 | @sub[:stop] = true 50 | @threads.kill(@sub) 51 | end 52 | 53 | def do_request 54 | request = @context.socket(ZMQ::REQ) 55 | request.connect(@options[:request]) 56 | 57 | request.send_string('') 58 | request.recv_string(data = '') 59 | request.close 60 | 61 | @request_handler.call(data) 62 | end 63 | 64 | def start_push 65 | @push = spawn_socket(@options[:push], ZMQ::PUSH) do |sock| 66 | sock.send_string(@push_queue.pop) 67 | end 68 | end 69 | 70 | def stop_push 71 | @push[:stop] = true 72 | @threads.kill(@push) 73 | end 74 | 75 | def spawn_socket(endpoint, socket_type) 76 | @threads.new(@context.socket(socket_type)) do |sock| 77 | thread = @threads.current 78 | thread[:stop] = false 79 | 80 | sock.connect(endpoint) 81 | 82 | until thread[:stop] 83 | yield sock 84 | end 85 | 86 | sock.close 87 | end 88 | end 89 | end 90 | end -------------------------------------------------------------------------------- /demos/presence/ruby/lib/clone/server.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | 3 | module Clone 4 | class Server 5 | def initialize(context, options, threads = Thread, pub_queue = nil) 6 | @context = context 7 | @options = options 8 | @threads = threads 9 | @pub_queue = pub_queue || Queue.new 10 | end 11 | 12 | def start 13 | @threads.abort_on_exception = true 14 | start_pull 15 | start_pub 16 | start_router 17 | end 18 | 19 | def stop 20 | stop_pull 21 | stop_pub 22 | stop_router 23 | @context.terminate 24 | end 25 | 26 | def on_request(&block) 27 | @router_handler = block 28 | end 29 | 30 | def on_push(&block) 31 | @pull_handler = block 32 | end 33 | 34 | def publish(msg) 35 | @pub_queue << msg 36 | end 37 | 38 | private 39 | 40 | def start_pull 41 | @pull = spawn_socket(@options[:pull], ZMQ::PULL) do |sock| 42 | sock.recv_string(payload = '') 43 | @pull_handler.call(payload) 44 | end 45 | end 46 | 47 | def stop_pull 48 | @pull[:stop] = true 49 | @threads.kill(@pull) 50 | end 51 | 52 | def start_pub 53 | @pub = spawn_socket(@options[:publish], ZMQ::PUB) do |sock| 54 | sock.send_string(@pub_queue.pop) 55 | end 56 | end 57 | 58 | def stop_pub 59 | @pub[:stop] = true 60 | @threads.kill(@pub) 61 | end 62 | 63 | def start_router 64 | @router = spawn_socket(@options[:router], ZMQ::ROUTER) do |sock| 65 | sock.recv_string(address = '') 66 | sock.recv_string('') if sock.more_parts? 67 | sock.recv_string(data = '') if sock.more_parts? 68 | sock.send_string(address, ZMQ::SNDMORE) 69 | sock.send_string('', ZMQ::SNDMORE) 70 | sock.send_string(@router_handler.call(data)) 71 | end 72 | end 73 | 74 | def stop_router 75 | @router[:stop] = true 76 | @threads.kill(@router) 77 | end 78 | 79 | def spawn_socket(endpoint, socket_type) 80 | @threads.new(@context.socket(socket_type)) do |sock| 81 | sock.bind(endpoint) 82 | 83 | thread = @threads.current 84 | thread[:stop] = false 85 | 86 | until thread[:stop] 87 | yield sock 88 | end 89 | 90 | sock.close 91 | end 92 | end 93 | end 94 | end -------------------------------------------------------------------------------- /demos/reflector/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | Run tests 15 | 16 | -------------------------------------------------------------------------------- /demos/reflector/server.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import uuid 3 | import os.path 4 | import mimetypes 5 | 6 | import gevent.server 7 | 8 | from ws4py.server.geventserver import WebSocketServer 9 | from stomp4py.server.websocket import StompHandler 10 | from stomp4py.server.base import Subscription 11 | 12 | class NullMQConnection(Subscription): 13 | def __init__(self, handler, frame): 14 | super(NullMQConnection, self).__init__(handler, frame) 15 | self.type = frame.headers.get('type', 'connect') 16 | self.socket = frame.headers.get('socket') 17 | self.route_id = '%s-%s' % (frame.conn_id, self.id) 18 | 19 | StompHandler.subscription_class = NullMQConnection 20 | 21 | def socket_pairing(subscription, frame): 22 | return subscription.socket == {'pub': 'sub', 'req': 'rep', 23 | 'rep': 'req', 'push': 'pull',}[frame.headers.get('socket')] 24 | 25 | class NullMQReflector(object): 26 | def __init__(self): 27 | self.rr_index = 0 28 | self.channels = collections.defaultdict(set) 29 | self.subscriber_counts = collections.Counter() 30 | 31 | def __call__(self, frame, payload): 32 | print frame 33 | if frame is None or frame.command == 'DISCONNECT': 34 | for subscriber in payload: 35 | self.unsubscribe(subscriber.destination, subscriber) 36 | elif frame.command == 'SUBSCRIBE': 37 | self.subscribe(frame.destination, payload) 38 | elif frame.command == 'UNSUBSCRIBE': 39 | self.unsubscribe(frame.destination, payload) 40 | elif frame.command == 'SEND': 41 | self.route(frame) 42 | elif frame.command == 'COMMIT': 43 | for part in payload: 44 | self.route(part) 45 | 46 | def subscribe(self, destination, subscriber): 47 | self.subscriber_counts[destination] += 1 48 | self.channels[destination].add(subscriber) 49 | 50 | def unsubscribe(self, destination, subscriber): 51 | self.subscriber_counts[destination] -= 1 52 | self.channels[destination].remove(subscriber) 53 | 54 | # Clean up counts and channels with no subscribers 55 | self.subscriber_counts += collections.Counter() 56 | if not self.subscriber_counts[destination]: 57 | del self.channels[destination] 58 | 59 | def route(self, frame): 60 | # Core routing logic of the reflector 61 | socket = frame.headers.get('socket') 62 | if socket == 'pub': 63 | # Fanout 64 | for subscriber in self.channels[frame.destination]: 65 | if socket_pairing(subscriber, frame): 66 | subscriber.send(frame.body) 67 | elif socket == 'req': 68 | # Round robin w/ reply-to 69 | reps = [s for s in self.channels[frame.destination] if socket_pairing(s, frame)] 70 | self.rr_index = (self.rr_index + 1) % len(reps) 71 | reply_to = '%s-%s' % (frame.conn_id, frame.headers.get('reply-to')) 72 | reps[self.rr_index].send(frame.body, {'reply-to': reply_to}) 73 | elif socket == 'rep': 74 | # Reply-to lookup 75 | reply_to = frame.headers.get('reply-to') 76 | for subscriber in self.channels[frame.destination]: 77 | if subscriber.route_id == reply_to: 78 | subscriber.send(frame.body) 79 | elif socket == 'push': 80 | # Round robin 81 | reps = [s for s in self.channels[frame.destination] if socket_pairing(s, frame)] 82 | self.rr_index = (self.rr_index + 1) % len(reps) 83 | reps[self.rr_index].send(frame.body) 84 | 85 | if __name__ == '__main__': 86 | reflector = NullMQReflector() 87 | 88 | def websocket_handler(websocket, environ): 89 | if environ.get('PATH_INFO') == '/reflector': 90 | StompHandler(websocket, reflector).serve() 91 | else: 92 | websocket.close() 93 | 94 | def http_handler(environ, start_response): 95 | if '/dist' in environ['PATH_INFO'] and environ['PATH_INFO'].split('/')[1] == 'dist': 96 | filename = '../..%s' % environ['PATH_INFO'] 97 | else: 98 | filename = environ['PATH_INFO'].split('/')[-1] or 'index.html' 99 | if os.path.exists(filename): 100 | start_response('200 OK', [('Content-type', mimetypes.guess_type(filename)[0])]) 101 | return [open(filename).read()] 102 | else: 103 | start_response('404 Not found', [('Content-type', 'text/plain')]) 104 | return ["Not found"] 105 | 106 | server = WebSocketServer(('127.0.0.1', 9000), websocket_handler, fallback_app=http_handler) 107 | print "Starting NullMQ reflector on 9000..." 108 | server.serve_forever() -------------------------------------------------------------------------------- /demos/reflector/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | NullMQ Reflector 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 38 | 39 |
    40 |

    NullMQ Reflector

    41 |
    42 |

    Tests requires that a Reflector Server accepting Stomp WebSocket protocol is running with the configuration:

    43 |
      44 |
    • URL: 45 |
    • User: / 46 |
    47 |
    48 | 49 |

    50 |

    51 |
      52 | 53 |
      
      54 |   
      55 | 56 | -------------------------------------------------------------------------------- /demos/reflector/test_pipeline.js: -------------------------------------------------------------------------------- 1 | var ctx1 = null; 2 | var ctx2 = null; 3 | var ctx3 = null; 4 | 5 | module("Pipelining", { 6 | setup: function() { 7 | ctx1 = new nullmq.Context(TEST.url); 8 | ctx2 = new nullmq.Context(TEST.url); 9 | ctx3 = new nullmq.Context(TEST.url); 10 | }, 11 | 12 | teardown: function() { 13 | ctx1.term(); 14 | ctx2.term(); 15 | ctx3.term(); 16 | } 17 | }); 18 | 19 | test("basic push-pull", function() { 20 | var messages = []; 21 | var message = "Foobar"; 22 | 23 | var pull = ctx1.socket(nullmq.PULL); 24 | pull.connect('/endpoint'); 25 | pull.recvall(function(msg) { 26 | messages.push(msg); 27 | }); 28 | 29 | var push = ctx2.socket(nullmq.PUSH); 30 | push.connect('/endpoint'); 31 | push.send(message); 32 | push.send(message); 33 | 34 | var wait = setInterval(function() { 35 | if (messages.length > 1) { 36 | clearInterval(wait); 37 | start(); 38 | equals(messages.length, 2); 39 | notEqual(messages.indexOf(message), -1); 40 | } 41 | }, 20); 42 | 43 | stop(TEST.timeout); 44 | }); -------------------------------------------------------------------------------- /demos/reflector/test_pubsub.js: -------------------------------------------------------------------------------- 1 | var ctx1 = null; 2 | var ctx2 = null; 3 | var ctx3 = null; 4 | 5 | module("Publish-Subscribe", { 6 | setup: function() { 7 | ctx1 = new nullmq.Context(TEST.url); 8 | ctx2 = new nullmq.Context(TEST.url); 9 | ctx3 = new nullmq.Context(TEST.url); 10 | }, 11 | 12 | teardown: function() { 13 | ctx1.term(); 14 | ctx2.term(); 15 | ctx3.term(); 16 | } 17 | }); 18 | 19 | test("basic publish-subscribe", function() { 20 | var messages = []; 21 | var message = "Foobar"; 22 | 23 | // Subscriber 1 on Context 1 24 | var sub1 = ctx1.socket(nullmq.SUB); 25 | sub1.connect('/publisher'); 26 | sub1.recvall(function(msg) { 27 | messages.push(msg); 28 | }); 29 | 30 | // Subscriber 2 on Context 1 31 | var sub2 = ctx1.socket(nullmq.SUB); 32 | sub2.connect('/publisher'); 33 | sub2.recvall(function(msg) { 34 | messages.push(msg); 35 | }); 36 | 37 | // Subscriber 3 on Context 2 38 | var sub3 = ctx2.socket(nullmq.SUB); 39 | sub3.connect('/publisher'); 40 | sub3.recvall(function(msg) { 41 | messages.push(msg); 42 | }); 43 | 44 | // Publisher on Context 3 45 | var pub = ctx3.socket(nullmq.PUB); 46 | pub.bind('/publisher'); 47 | pub.send(message); 48 | 49 | var wait = setInterval(function() { 50 | if (messages.length > 2) { 51 | clearInterval(wait); 52 | start(); 53 | equals(messages.length, 3); 54 | notEqual(messages.indexOf(message), -1); 55 | } 56 | }, 20); 57 | 58 | stop(TEST.timeout); 59 | }); -------------------------------------------------------------------------------- /demos/reflector/test_rr.js: -------------------------------------------------------------------------------- 1 | var ctx1 = null; 2 | var ctx2 = null; 3 | var ctx3 = null; 4 | 5 | module("Request-Reply", { 6 | setup: function() { 7 | ctx1 = new nullmq.Context(TEST.url); 8 | ctx2 = new nullmq.Context(TEST.url); 9 | ctx3 = new nullmq.Context(TEST.url); 10 | }, 11 | 12 | teardown: function() { 13 | ctx1.term(); 14 | ctx2.term(); 15 | ctx3.term(); 16 | } 17 | }); 18 | 19 | test("basic request-reply", function() { 20 | var message = "Foobar"; 21 | 22 | var rep = ctx1.socket(nullmq.REP); 23 | rep.bind('/echo'); 24 | rep.recvall(function(msg) { 25 | rep.send(msg); 26 | }); 27 | 28 | var req = ctx2.socket(nullmq.REQ); 29 | req.connect('/echo'); 30 | req.send(message); 31 | req.recv(function(msg) { 32 | start(); 33 | equals(msg, message); 34 | }) 35 | 36 | stop(TEST.timeout); 37 | }); 38 | 39 | test("round robin across connected reply sockets", function() { 40 | var messages = []; 41 | 42 | var rep1 = ctx1.socket(nullmq.REP); 43 | rep1.bind('/reply1'); 44 | rep1.recvall(function(msg) { 45 | rep1.send('reply1'); 46 | }); 47 | 48 | var rep2 = ctx1.socket(nullmq.REP); 49 | rep2.bind('/reply2'); 50 | rep2.recvall(function(msg) { 51 | rep2.send('reply2'); 52 | }); 53 | 54 | var req = ctx2.socket(nullmq.REQ); 55 | req.connect('/reply1'); 56 | req.connect('/reply2'); 57 | req.send("First request"); 58 | req.recv(function(msg) { 59 | start(); 60 | messages.push(msg); 61 | 62 | req.send("Second request"); 63 | req.recv(function(msg) { 64 | messages.push(msg); 65 | 66 | notEqual(messages.indexOf('reply1'), -1); 67 | notEqual(messages.indexOf('reply2'), -1); 68 | }); 69 | }) 70 | 71 | stop(TEST.timeout); 72 | }); 73 | 74 | test("mixing connect and bind reply sockets in different contexts", function() { 75 | var messages = []; 76 | 77 | var rep1 = ctx1.socket(nullmq.REP); 78 | rep1.bind('/rep'); 79 | rep1.recvall(function(msg) { 80 | rep1.send('replyA'); 81 | }); 82 | 83 | var rep2 = ctx2.socket(nullmq.REP); 84 | rep2.connect('/req'); 85 | rep2.recvall(function(msg) { 86 | rep2.send('replyB'); 87 | }); 88 | 89 | var req = ctx3.socket(nullmq.REQ); 90 | req.connect('/rep'); 91 | req.bind('/req'); 92 | req.send("First request"); 93 | req.recv(function(msg) { 94 | start(); 95 | messages.push(msg); 96 | 97 | req.send("Second request"); 98 | req.recv(function(msg) { 99 | messages.push(msg); 100 | 101 | notEqual(messages.indexOf('replyA'), -1); 102 | notEqual(messages.indexOf('replyB'), -1); 103 | }); 104 | }) 105 | 106 | stop(TEST.timeout); 107 | }); -------------------------------------------------------------------------------- /demos/requirements.txt: -------------------------------------------------------------------------------- 1 | git+git://github.com/progrium/stomp4py.git#egg=stomp4py 2 | git+git://github.com/progrium/WebSocket-for-Python#egg=ws4py 3 | git+git://github.com/progrium/gservice.git#egg=gservice 4 | gevent==0.13.3 5 | gevent_zeromq 6 | greenlet==0.3.1 7 | lockfile==0.9.1 8 | nose==1.1.2 9 | python-daemon==1.6 10 | setproctitle==1.1.2 11 | wsgiref==0.1.2 -------------------------------------------------------------------------------- /dist/lib/qunit.css: -------------------------------------------------------------------------------- 1 | 2 | ol#qunit-tests { 3 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 4 | margin:0; 5 | padding:0; 6 | list-style-position:inside; 7 | 8 | font-size: smaller; 9 | } 10 | ol#qunit-tests li{ 11 | padding:0.4em 0.5em 0.4em 2.5em; 12 | border-bottom:1px solid #fff; 13 | font-size:small; 14 | list-style-position:inside; 15 | } 16 | ol#qunit-tests li ol{ 17 | box-shadow: inset 0px 2px 13px #999; 18 | -moz-box-shadow: inset 0px 2px 13px #999; 19 | -webkit-box-shadow: inset 0px 2px 13px #999; 20 | margin-top:0.5em; 21 | margin-left:0; 22 | padding:0.5em; 23 | background-color:#fff; 24 | border-radius:15px; 25 | -moz-border-radius: 15px; 26 | -webkit-border-radius: 15px; 27 | } 28 | ol#qunit-tests li li{ 29 | border-bottom:none; 30 | margin:0.5em; 31 | background-color:#fff; 32 | list-style-position: inside; 33 | padding:0.4em 0.5em 0.4em 0.5em; 34 | } 35 | 36 | ol#qunit-tests li li.pass{ 37 | border-left:26px solid #C6E746; 38 | background-color:#fff; 39 | color:#5E740B; 40 | } 41 | ol#qunit-tests li li.fail{ 42 | border-left:26px solid #EE5757; 43 | background-color:#fff; 44 | color:#710909; 45 | } 46 | ol#qunit-tests li.pass{ 47 | background-color:#D2E0E6; 48 | color:#528CE0; 49 | } 50 | ol#qunit-tests li.fail{ 51 | background-color:#EE5757; 52 | color:#000; 53 | } 54 | ol#qunit-tests li strong { 55 | cursor:pointer; 56 | } 57 | h1#qunit-header{ 58 | background-color:#0d3349; 59 | margin:0; 60 | padding:0.5em 0 0.5em 1em; 61 | color:#fff; 62 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 63 | border-top-right-radius:15px; 64 | border-top-left-radius:15px; 65 | -moz-border-radius-topright:15px; 66 | -moz-border-radius-topleft:15px; 67 | -webkit-border-top-right-radius:15px; 68 | -webkit-border-top-left-radius:15px; 69 | text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; 70 | } 71 | h2#qunit-banner{ 72 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 73 | height:5px; 74 | margin:0; 75 | padding:0; 76 | } 77 | h2#qunit-banner.qunit-pass{ 78 | background-color:#C6E746; 79 | } 80 | h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar { 81 | background-color:#EE5757; 82 | } 83 | #qunit-testrunner-toolbar { 84 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 85 | padding:0; 86 | /*width:80%;*/ 87 | padding:0em 0 0.5em 2em; 88 | font-size: small; 89 | } 90 | h2#qunit-userAgent { 91 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 92 | background-color:#2b81af; 93 | margin:0; 94 | padding:0; 95 | color:#fff; 96 | font-size: small; 97 | padding:0.5em 0 0.5em 2.5em; 98 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 99 | } 100 | p#qunit-testresult{ 101 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 102 | margin:0; 103 | font-size: small; 104 | color:#2b81af; 105 | border-bottom-right-radius:15px; 106 | border-bottom-left-radius:15px; 107 | -moz-border-radius-bottomright:15px; 108 | -moz-border-radius-bottomleft:15px; 109 | -webkit-border-bottom-right-radius:15px; 110 | -webkit-border-bottom-left-radius:15px; 111 | background-color:#D2E0E6; 112 | padding:0.5em 0.5em 0.5em 2.5em; 113 | } 114 | strong b.fail{ 115 | color:#710909; 116 | } 117 | strong b.pass{ 118 | color:#5E740B; 119 | } 120 | -------------------------------------------------------------------------------- /dist/lib/qunit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2009 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * and GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var QUnit = { 14 | 15 | // Initialize the configuration options 16 | init: function() { 17 | config = { 18 | stats: { all: 0, bad: 0 }, 19 | moduleStats: { all: 0, bad: 0 }, 20 | started: +new Date, 21 | blocking: false, 22 | autorun: false, 23 | assertions: [], 24 | filters: [], 25 | queue: [] 26 | }; 27 | 28 | var tests = id("qunit-tests"), 29 | banner = id("qunit-banner"), 30 | result = id("qunit-testresult"); 31 | 32 | if ( tests ) { 33 | tests.innerHTML = ""; 34 | } 35 | 36 | if ( banner ) { 37 | banner.className = ""; 38 | } 39 | 40 | if ( result ) { 41 | result.parentNode.removeChild( result ); 42 | } 43 | }, 44 | 45 | // call on start of module test to prepend name to all tests 46 | module: function(name, testEnvironment) { 47 | config.currentModule = name; 48 | 49 | synchronize(function() { 50 | if ( config.currentModule ) { 51 | QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); 52 | } 53 | 54 | config.currentModule = name; 55 | config.moduleTestEnvironment = testEnvironment; 56 | config.moduleStats = { all: 0, bad: 0 }; 57 | 58 | QUnit.moduleStart( name, testEnvironment ); 59 | }); 60 | }, 61 | 62 | asyncTest: function(testName, expected, callback) { 63 | if ( arguments.length === 2 ) { 64 | callback = expected; 65 | expected = 0; 66 | } 67 | 68 | QUnit.test(testName, expected, callback, true); 69 | }, 70 | 71 | test: function(testName, expected, callback, async) { 72 | var name = testName, testEnvironment, testEnvironmentArg; 73 | 74 | if ( arguments.length === 2 ) { 75 | callback = expected; 76 | expected = null; 77 | } 78 | // is 2nd argument a testEnvironment? 79 | if ( expected && typeof expected === 'object') { 80 | testEnvironmentArg = expected; 81 | expected = null; 82 | } 83 | 84 | if ( config.currentModule ) { 85 | name = config.currentModule + " module: " + name; 86 | } 87 | 88 | if ( !validTest(name) ) { 89 | return; 90 | } 91 | 92 | synchronize(function() { 93 | QUnit.testStart( testName ); 94 | 95 | testEnvironment = extend({ 96 | setup: function() {}, 97 | teardown: function() {} 98 | }, config.moduleTestEnvironment); 99 | if (testEnvironmentArg) { 100 | extend(testEnvironment,testEnvironmentArg); 101 | } 102 | 103 | // allow utility functions to access the current test environment 104 | QUnit.current_testEnvironment = testEnvironment; 105 | 106 | config.assertions = []; 107 | config.expected = expected; 108 | 109 | try { 110 | if ( !config.pollution ) { 111 | saveGlobal(); 112 | } 113 | 114 | testEnvironment.setup.call(testEnvironment); 115 | } catch(e) { 116 | QUnit.ok( false, "Setup failed on " + name + ": " + e.message ); 117 | } 118 | 119 | if ( async ) { 120 | QUnit.stop(); 121 | } 122 | 123 | try { 124 | callback.call(testEnvironment); 125 | } catch(e) { 126 | fail("Test " + name + " died, exception and test follows", e, callback); 127 | QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message ); 128 | // else next test will carry the responsibility 129 | saveGlobal(); 130 | 131 | // Restart the tests if they're blocking 132 | if ( config.blocking ) { 133 | start(); 134 | } 135 | } 136 | }); 137 | 138 | synchronize(function() { 139 | try { 140 | checkPollution(); 141 | testEnvironment.teardown.call(testEnvironment); 142 | } catch(e) { 143 | QUnit.ok( false, "Teardown failed on " + name + ": " + e.message ); 144 | } 145 | 146 | try { 147 | QUnit.reset(); 148 | } catch(e) { 149 | fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset); 150 | } 151 | 152 | if ( config.expected && config.expected != config.assertions.length ) { 153 | QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" ); 154 | } 155 | 156 | var good = 0, bad = 0, 157 | tests = id("qunit-tests"); 158 | 159 | config.stats.all += config.assertions.length; 160 | config.moduleStats.all += config.assertions.length; 161 | 162 | if ( tests ) { 163 | var ol = document.createElement("ol"); 164 | ol.style.display = "none"; 165 | 166 | for ( var i = 0; i < config.assertions.length; i++ ) { 167 | var assertion = config.assertions[i]; 168 | 169 | var li = document.createElement("li"); 170 | li.className = assertion.result ? "pass" : "fail"; 171 | li.appendChild(document.createTextNode(assertion.message || "(no message)")); 172 | ol.appendChild( li ); 173 | 174 | if ( assertion.result ) { 175 | good++; 176 | } else { 177 | bad++; 178 | config.stats.bad++; 179 | config.moduleStats.bad++; 180 | } 181 | } 182 | 183 | var b = document.createElement("strong"); 184 | b.innerHTML = name + " (" + bad + ", " + good + ", " + config.assertions.length + ")"; 185 | 186 | addEvent(b, "click", function() { 187 | var next = b.nextSibling, display = next.style.display; 188 | next.style.display = display === "none" ? "block" : "none"; 189 | }); 190 | 191 | addEvent(b, "dblclick", function(e) { 192 | var target = e && e.target ? e.target : window.event.srcElement; 193 | if ( target.nodeName.toLowerCase() === "strong" ) { 194 | var text = "", node = target.firstChild; 195 | 196 | while ( node.nodeType === 3 ) { 197 | text += node.nodeValue; 198 | node = node.nextSibling; 199 | } 200 | 201 | text = text.replace(/(^\s*|\s*$)/g, ""); 202 | 203 | if ( window.location ) { 204 | window.location.href = window.location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent(text); 205 | } 206 | } 207 | }); 208 | 209 | var li = document.createElement("li"); 210 | li.className = bad ? "fail" : "pass"; 211 | li.appendChild( b ); 212 | li.appendChild( ol ); 213 | tests.appendChild( li ); 214 | 215 | if ( bad ) { 216 | var toolbar = id("qunit-testrunner-toolbar"); 217 | if ( toolbar ) { 218 | toolbar.style.display = "block"; 219 | id("qunit-filter-pass").disabled = null; 220 | id("qunit-filter-missing").disabled = null; 221 | } 222 | } 223 | 224 | } else { 225 | for ( var i = 0; i < config.assertions.length; i++ ) { 226 | if ( !config.assertions[i].result ) { 227 | bad++; 228 | config.stats.bad++; 229 | config.moduleStats.bad++; 230 | } 231 | } 232 | } 233 | 234 | QUnit.testDone( testName, bad, config.assertions.length ); 235 | 236 | if ( !window.setTimeout && !config.queue.length ) { 237 | done(); 238 | } 239 | }); 240 | 241 | if ( window.setTimeout && !config.doneTimer ) { 242 | config.doneTimer = window.setTimeout(function(){ 243 | if ( !config.queue.length ) { 244 | done(); 245 | } else { 246 | synchronize( done ); 247 | } 248 | }, 13); 249 | } 250 | }, 251 | 252 | /** 253 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 254 | */ 255 | expect: function(asserts) { 256 | config.expected = asserts; 257 | }, 258 | 259 | /** 260 | * Asserts true. 261 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 262 | */ 263 | ok: function(a, msg) { 264 | QUnit.log(a, msg); 265 | 266 | config.assertions.push({ 267 | result: !!a, 268 | message: msg 269 | }); 270 | }, 271 | 272 | /** 273 | * Checks that the first two arguments are equal, with an optional message. 274 | * Prints out both actual and expected values. 275 | * 276 | * Prefered to ok( actual == expected, message ) 277 | * 278 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 279 | * 280 | * @param Object actual 281 | * @param Object expected 282 | * @param String message (optional) 283 | */ 284 | equal: function(actual, expected, message) { 285 | push(expected == actual, actual, expected, message); 286 | }, 287 | 288 | notEqual: function(actual, expected, message) { 289 | push(expected != actual, actual, expected, message); 290 | }, 291 | 292 | deepEqual: function(a, b, message) { 293 | push(QUnit.equiv(a, b), a, b, message); 294 | }, 295 | 296 | notDeepEqual: function(a, b, message) { 297 | push(!QUnit.equiv(a, b), a, b, message); 298 | }, 299 | 300 | strictEqual: function(actual, expected, message) { 301 | push(expected === actual, actual, expected, message); 302 | }, 303 | 304 | notStrictEqual: function(actual, expected, message) { 305 | push(expected !== actual, actual, expected, message); 306 | }, 307 | 308 | start: function() { 309 | // A slight delay, to avoid any current callbacks 310 | if ( window.setTimeout ) { 311 | window.setTimeout(function() { 312 | if ( config.timeout ) { 313 | clearTimeout(config.timeout); 314 | } 315 | 316 | config.blocking = false; 317 | process(); 318 | }, 13); 319 | } else { 320 | config.blocking = false; 321 | process(); 322 | } 323 | }, 324 | 325 | stop: function(timeout) { 326 | config.blocking = true; 327 | 328 | if ( timeout && window.setTimeout ) { 329 | config.timeout = window.setTimeout(function() { 330 | QUnit.ok( false, "Test timed out" ); 331 | QUnit.start(); 332 | }, timeout); 333 | } 334 | }, 335 | 336 | /** 337 | * Resets the test setup. Useful for tests that modify the DOM. 338 | */ 339 | reset: function() { 340 | if ( window.jQuery ) { 341 | jQuery("#main").html( config.fixture ); 342 | jQuery.event.global = {}; 343 | jQuery.ajaxSettings = extend({}, config.ajaxSettings); 344 | } 345 | }, 346 | 347 | /** 348 | * Trigger an event on an element. 349 | * 350 | * @example triggerEvent( document.body, "click" ); 351 | * 352 | * @param DOMElement elem 353 | * @param String type 354 | */ 355 | triggerEvent: function( elem, type, event ) { 356 | if ( document.createEvent ) { 357 | event = document.createEvent("MouseEvents"); 358 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 359 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 360 | elem.dispatchEvent( event ); 361 | 362 | } else if ( elem.fireEvent ) { 363 | elem.fireEvent("on"+type); 364 | } 365 | }, 366 | 367 | // Safe object type checking 368 | is: function( type, obj ) { 369 | return Object.prototype.toString.call( obj ) === "[object "+ type +"]"; 370 | }, 371 | 372 | // Logging callbacks 373 | done: function(failures, total) {}, 374 | log: function(result, message) {}, 375 | testStart: function(name) {}, 376 | testDone: function(name, failures, total) {}, 377 | moduleStart: function(name, testEnvironment) {}, 378 | moduleDone: function(name, failures, total) {} 379 | }; 380 | 381 | // Backwards compatibility, deprecated 382 | QUnit.equals = QUnit.equal; 383 | QUnit.same = QUnit.deepEqual; 384 | 385 | // Maintain internal state 386 | var config = { 387 | // The queue of tests to run 388 | queue: [], 389 | 390 | // block until document ready 391 | blocking: true 392 | }; 393 | 394 | // Load paramaters 395 | (function() { 396 | var location = window.location || { search: "", protocol: "file:" }, 397 | GETParams = location.search.slice(1).split('&'); 398 | 399 | for ( var i = 0; i < GETParams.length; i++ ) { 400 | GETParams[i] = decodeURIComponent( GETParams[i] ); 401 | if ( GETParams[i] === "noglobals" ) { 402 | GETParams.splice( i, 1 ); 403 | i--; 404 | config.noglobals = true; 405 | } else if ( GETParams[i].search('=') > -1 ) { 406 | GETParams.splice( i, 1 ); 407 | i--; 408 | } 409 | } 410 | 411 | // restrict modules/tests by get parameters 412 | config.filters = GETParams; 413 | 414 | // Figure out if we're running the tests from a server or not 415 | QUnit.isLocal = !!(location.protocol === 'file:'); 416 | })(); 417 | 418 | // Expose the API as global variables, unless an 'exports' 419 | // object exists, in that case we assume we're in CommonJS 420 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 421 | extend(window, QUnit); 422 | window.QUnit = QUnit; 423 | } else { 424 | extend(exports, QUnit); 425 | exports.QUnit = QUnit; 426 | } 427 | 428 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 429 | config.autorun = true; 430 | } 431 | 432 | addEvent(window, "load", function() { 433 | // Initialize the config, saving the execution queue 434 | var oldconfig = extend({}, config); 435 | QUnit.init(); 436 | extend(config, oldconfig); 437 | 438 | config.blocking = false; 439 | 440 | var userAgent = id("qunit-userAgent"); 441 | if ( userAgent ) { 442 | userAgent.innerHTML = navigator.userAgent; 443 | } 444 | 445 | var toolbar = id("qunit-testrunner-toolbar"); 446 | if ( toolbar ) { 447 | toolbar.style.display = "none"; 448 | 449 | var filter = document.createElement("input"); 450 | filter.type = "checkbox"; 451 | filter.id = "qunit-filter-pass"; 452 | filter.disabled = true; 453 | addEvent( filter, "click", function() { 454 | var li = document.getElementsByTagName("li"); 455 | for ( var i = 0; i < li.length; i++ ) { 456 | if ( li[i].className.indexOf("pass") > -1 ) { 457 | li[i].style.display = filter.checked ? "none" : ""; 458 | } 459 | } 460 | }); 461 | toolbar.appendChild( filter ); 462 | 463 | var label = document.createElement("label"); 464 | label.setAttribute("for", "qunit-filter-pass"); 465 | label.innerHTML = "Hide passed tests"; 466 | toolbar.appendChild( label ); 467 | 468 | var missing = document.createElement("input"); 469 | missing.type = "checkbox"; 470 | missing.id = "qunit-filter-missing"; 471 | missing.disabled = true; 472 | addEvent( missing, "click", function() { 473 | var li = document.getElementsByTagName("li"); 474 | for ( var i = 0; i < li.length; i++ ) { 475 | if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) { 476 | li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block"; 477 | } 478 | } 479 | }); 480 | toolbar.appendChild( missing ); 481 | 482 | label = document.createElement("label"); 483 | label.setAttribute("for", "qunit-filter-missing"); 484 | label.innerHTML = "Hide missing tests (untested code is broken code)"; 485 | toolbar.appendChild( label ); 486 | } 487 | 488 | var main = id('main'); 489 | if ( main ) { 490 | config.fixture = main.innerHTML; 491 | } 492 | 493 | if ( window.jQuery ) { 494 | config.ajaxSettings = window.jQuery.ajaxSettings; 495 | } 496 | 497 | QUnit.start(); 498 | }); 499 | 500 | function done() { 501 | if ( config.doneTimer && window.clearTimeout ) { 502 | window.clearTimeout( config.doneTimer ); 503 | config.doneTimer = null; 504 | } 505 | 506 | if ( config.queue.length ) { 507 | config.doneTimer = window.setTimeout(function(){ 508 | if ( !config.queue.length ) { 509 | done(); 510 | } else { 511 | synchronize( done ); 512 | } 513 | }, 13); 514 | 515 | return; 516 | } 517 | 518 | config.autorun = true; 519 | 520 | // Log the last module results 521 | if ( config.currentModule ) { 522 | QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); 523 | } 524 | 525 | var banner = id("qunit-banner"), 526 | tests = id("qunit-tests"), 527 | html = ['Tests completed in ', 528 | +new Date - config.started, ' milliseconds.
      ', 529 | '', config.stats.all - config.stats.bad, ' tests of ', config.stats.all, ' passed, ', config.stats.bad,' failed.'].join(''); 530 | 531 | if ( banner ) { 532 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 533 | } 534 | 535 | if ( tests ) { 536 | var result = id("qunit-testresult"); 537 | 538 | if ( !result ) { 539 | result = document.createElement("p"); 540 | result.id = "qunit-testresult"; 541 | result.className = "result"; 542 | tests.parentNode.insertBefore( result, tests.nextSibling ); 543 | } 544 | 545 | result.innerHTML = html; 546 | } 547 | 548 | QUnit.done( config.stats.bad, config.stats.all ); 549 | } 550 | 551 | function validTest( name ) { 552 | var i = config.filters.length, 553 | run = false; 554 | 555 | if ( !i ) { 556 | return true; 557 | } 558 | 559 | while ( i-- ) { 560 | var filter = config.filters[i], 561 | not = filter.charAt(0) == '!'; 562 | 563 | if ( not ) { 564 | filter = filter.slice(1); 565 | } 566 | 567 | if ( name.indexOf(filter) !== -1 ) { 568 | return !not; 569 | } 570 | 571 | if ( not ) { 572 | run = true; 573 | } 574 | } 575 | 576 | return run; 577 | } 578 | 579 | function push(result, actual, expected, message) { 580 | message = message || (result ? "okay" : "failed"); 581 | QUnit.ok( result, result ? message + ": " + expected : message + ", expected: " + QUnit.jsDump.parse(expected) + " result: " + QUnit.jsDump.parse(actual) ); 582 | } 583 | 584 | function synchronize( callback ) { 585 | config.queue.push( callback ); 586 | 587 | if ( config.autorun && !config.blocking ) { 588 | process(); 589 | } 590 | } 591 | 592 | function process() { 593 | while ( config.queue.length && !config.blocking ) { 594 | config.queue.shift()(); 595 | } 596 | } 597 | 598 | function saveGlobal() { 599 | config.pollution = []; 600 | 601 | if ( config.noglobals ) { 602 | for ( var key in window ) { 603 | config.pollution.push( key ); 604 | } 605 | } 606 | } 607 | 608 | function checkPollution( name ) { 609 | var old = config.pollution; 610 | saveGlobal(); 611 | 612 | var newGlobals = diff( old, config.pollution ); 613 | if ( newGlobals.length > 0 ) { 614 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 615 | config.expected++; 616 | } 617 | 618 | var deletedGlobals = diff( config.pollution, old ); 619 | if ( deletedGlobals.length > 0 ) { 620 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 621 | config.expected++; 622 | } 623 | } 624 | 625 | // returns a new Array with the elements that are in a but not in b 626 | function diff( a, b ) { 627 | var result = a.slice(); 628 | for ( var i = 0; i < result.length; i++ ) { 629 | for ( var j = 0; j < b.length; j++ ) { 630 | if ( result[i] === b[j] ) { 631 | result.splice(i, 1); 632 | i--; 633 | break; 634 | } 635 | } 636 | } 637 | return result; 638 | } 639 | 640 | function fail(message, exception, callback) { 641 | if ( typeof console !== "undefined" && console.error && console.warn ) { 642 | console.error(message); 643 | console.error(exception); 644 | console.warn(callback.toString()); 645 | 646 | } else if ( window.opera && opera.postError ) { 647 | opera.postError(message, exception, callback.toString); 648 | } 649 | } 650 | 651 | function extend(a, b) { 652 | for ( var prop in b ) { 653 | a[prop] = b[prop]; 654 | } 655 | 656 | return a; 657 | } 658 | 659 | function addEvent(elem, type, fn) { 660 | if ( elem.addEventListener ) { 661 | elem.addEventListener( type, fn, false ); 662 | } else if ( elem.attachEvent ) { 663 | elem.attachEvent( "on" + type, fn ); 664 | } else { 665 | fn(); 666 | } 667 | } 668 | 669 | function id(name) { 670 | return !!(typeof document !== "undefined" && document && document.getElementById) && 671 | document.getElementById( name ); 672 | } 673 | 674 | // Test for equality any JavaScript type. 675 | // Discussions and reference: http://philrathe.com/articles/equiv 676 | // Test suites: http://philrathe.com/tests/equiv 677 | // Author: Philippe Rathé 678 | QUnit.equiv = function () { 679 | 680 | var innerEquiv; // the real equiv function 681 | var callers = []; // stack to decide between skip/abort functions 682 | 683 | 684 | // Determine what is o. 685 | function hoozit(o) { 686 | if (QUnit.is("String", o)) { 687 | return "string"; 688 | 689 | } else if (QUnit.is("Boolean", o)) { 690 | return "boolean"; 691 | 692 | } else if (QUnit.is("Number", o)) { 693 | 694 | if (isNaN(o)) { 695 | return "nan"; 696 | } else { 697 | return "number"; 698 | } 699 | 700 | } else if (typeof o === "undefined") { 701 | return "undefined"; 702 | 703 | // consider: typeof null === object 704 | } else if (o === null) { 705 | return "null"; 706 | 707 | // consider: typeof [] === object 708 | } else if (QUnit.is( "Array", o)) { 709 | return "array"; 710 | 711 | // consider: typeof new Date() === object 712 | } else if (QUnit.is( "Date", o)) { 713 | return "date"; 714 | 715 | // consider: /./ instanceof Object; 716 | // /./ instanceof RegExp; 717 | // typeof /./ === "function"; // => false in IE and Opera, 718 | // true in FF and Safari 719 | } else if (QUnit.is( "RegExp", o)) { 720 | return "regexp"; 721 | 722 | } else if (typeof o === "object") { 723 | return "object"; 724 | 725 | } else if (QUnit.is( "Function", o)) { 726 | return "function"; 727 | } else { 728 | return undefined; 729 | } 730 | } 731 | 732 | // Call the o related callback with the given arguments. 733 | function bindCallbacks(o, callbacks, args) { 734 | var prop = hoozit(o); 735 | if (prop) { 736 | if (hoozit(callbacks[prop]) === "function") { 737 | return callbacks[prop].apply(callbacks, args); 738 | } else { 739 | return callbacks[prop]; // or undefined 740 | } 741 | } 742 | } 743 | 744 | var callbacks = function () { 745 | 746 | // for string, boolean, number and null 747 | function useStrictEquality(b, a) { 748 | if (b instanceof a.constructor || a instanceof b.constructor) { 749 | // to catch short annotaion VS 'new' annotation of a declaration 750 | // e.g. var i = 1; 751 | // var j = new Number(1); 752 | return a == b; 753 | } else { 754 | return a === b; 755 | } 756 | } 757 | 758 | return { 759 | "string": useStrictEquality, 760 | "boolean": useStrictEquality, 761 | "number": useStrictEquality, 762 | "null": useStrictEquality, 763 | "undefined": useStrictEquality, 764 | 765 | "nan": function (b) { 766 | return isNaN(b); 767 | }, 768 | 769 | "date": function (b, a) { 770 | return hoozit(b) === "date" && a.valueOf() === b.valueOf(); 771 | }, 772 | 773 | "regexp": function (b, a) { 774 | return hoozit(b) === "regexp" && 775 | a.source === b.source && // the regex itself 776 | a.global === b.global && // and its modifers (gmi) ... 777 | a.ignoreCase === b.ignoreCase && 778 | a.multiline === b.multiline; 779 | }, 780 | 781 | // - skip when the property is a method of an instance (OOP) 782 | // - abort otherwise, 783 | // initial === would have catch identical references anyway 784 | "function": function () { 785 | var caller = callers[callers.length - 1]; 786 | return caller !== Object && 787 | typeof caller !== "undefined"; 788 | }, 789 | 790 | "array": function (b, a) { 791 | var i; 792 | var len; 793 | 794 | // b could be an object literal here 795 | if ( ! (hoozit(b) === "array")) { 796 | return false; 797 | } 798 | 799 | len = a.length; 800 | if (len !== b.length) { // safe and faster 801 | return false; 802 | } 803 | for (i = 0; i < len; i++) { 804 | if ( ! innerEquiv(a[i], b[i])) { 805 | return false; 806 | } 807 | } 808 | return true; 809 | }, 810 | 811 | "object": function (b, a) { 812 | var i; 813 | var eq = true; // unless we can proove it 814 | var aProperties = [], bProperties = []; // collection of strings 815 | 816 | // comparing constructors is more strict than using instanceof 817 | if ( a.constructor !== b.constructor) { 818 | return false; 819 | } 820 | 821 | // stack constructor before traversing properties 822 | callers.push(a.constructor); 823 | 824 | for (i in a) { // be strict: don't ensures hasOwnProperty and go deep 825 | 826 | aProperties.push(i); // collect a's properties 827 | 828 | if ( ! innerEquiv(a[i], b[i])) { 829 | eq = false; 830 | } 831 | } 832 | 833 | callers.pop(); // unstack, we are done 834 | 835 | for (i in b) { 836 | bProperties.push(i); // collect b's properties 837 | } 838 | 839 | // Ensures identical properties name 840 | return eq && innerEquiv(aProperties.sort(), bProperties.sort()); 841 | } 842 | }; 843 | }(); 844 | 845 | innerEquiv = function () { // can take multiple arguments 846 | var args = Array.prototype.slice.apply(arguments); 847 | if (args.length < 2) { 848 | return true; // end transition 849 | } 850 | 851 | return (function (a, b) { 852 | if (a === b) { 853 | return true; // catch the most you can 854 | } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || hoozit(a) !== hoozit(b)) { 855 | return false; // don't lose time with error prone cases 856 | } else { 857 | return bindCallbacks(a, callbacks, [b, a]); 858 | } 859 | 860 | // apply transition with (1..n) arguments 861 | })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1)); 862 | }; 863 | 864 | return innerEquiv; 865 | 866 | }(); 867 | 868 | /** 869 | * jsDump 870 | * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com 871 | * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php) 872 | * Date: 5/15/2008 873 | * @projectDescription Advanced and extensible data dumping for Javascript. 874 | * @version 1.0.0 875 | * @author Ariel Flesler 876 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 877 | */ 878 | QUnit.jsDump = (function() { 879 | function quote( str ) { 880 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 881 | }; 882 | function literal( o ) { 883 | return o + ''; 884 | }; 885 | function join( pre, arr, post ) { 886 | var s = jsDump.separator(), 887 | base = jsDump.indent(), 888 | inner = jsDump.indent(1); 889 | if ( arr.join ) 890 | arr = arr.join( ',' + s + inner ); 891 | if ( !arr ) 892 | return pre + post; 893 | return [ pre, inner + arr, base + post ].join(s); 894 | }; 895 | function array( arr ) { 896 | var i = arr.length, ret = Array(i); 897 | this.up(); 898 | while ( i-- ) 899 | ret[i] = this.parse( arr[i] ); 900 | this.down(); 901 | return join( '[', ret, ']' ); 902 | }; 903 | 904 | var reName = /^function (\w+)/; 905 | 906 | var jsDump = { 907 | parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance 908 | var parser = this.parsers[ type || this.typeOf(obj) ]; 909 | type = typeof parser; 910 | 911 | return type == 'function' ? parser.call( this, obj ) : 912 | type == 'string' ? parser : 913 | this.parsers.error; 914 | }, 915 | typeOf:function( obj ) { 916 | var type; 917 | if ( obj === null ) { 918 | type = "null"; 919 | } else if (typeof obj === "undefined") { 920 | type = "undefined"; 921 | } else if (QUnit.is("RegExp", obj)) { 922 | type = "regexp"; 923 | } else if (QUnit.is("Date", obj)) { 924 | type = "date"; 925 | } else if (QUnit.is("Function", obj)) { 926 | type = "function"; 927 | } else if (QUnit.is("Array", obj)) { 928 | type = "array"; 929 | } else if (QUnit.is("Window", obj) || QUnit.is("global", obj)) { 930 | type = "window"; 931 | } else if (QUnit.is("HTMLDocument", obj)) { 932 | type = "document"; 933 | } else if (QUnit.is("HTMLCollection", obj) || QUnit.is("NodeList", obj)) { 934 | type = "nodelist"; 935 | } else if (/^\[object HTML/.test(Object.prototype.toString.call( obj ))) { 936 | type = "node"; 937 | } else { 938 | type = typeof obj; 939 | } 940 | return type; 941 | }, 942 | separator:function() { 943 | return this.multiline ? this.HTML ? '
      ' : '\n' : this.HTML ? ' ' : ' '; 944 | }, 945 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 946 | if ( !this.multiline ) 947 | return ''; 948 | var chr = this.indentChar; 949 | if ( this.HTML ) 950 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 951 | return Array( this._depth_ + (extra||0) ).join(chr); 952 | }, 953 | up:function( a ) { 954 | this._depth_ += a || 1; 955 | }, 956 | down:function( a ) { 957 | this._depth_ -= a || 1; 958 | }, 959 | setParser:function( name, parser ) { 960 | this.parsers[name] = parser; 961 | }, 962 | // The next 3 are exposed so you can use them 963 | quote:quote, 964 | literal:literal, 965 | join:join, 966 | // 967 | _depth_: 1, 968 | // This is the list of parsers, to modify them, use jsDump.setParser 969 | parsers:{ 970 | window: '[Window]', 971 | document: '[Document]', 972 | error:'[ERROR]', //when no parser is found, shouldn't happen 973 | unknown: '[Unknown]', 974 | 'null':'null', 975 | undefined:'undefined', 976 | 'function':function( fn ) { 977 | var ret = 'function', 978 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 979 | if ( name ) 980 | ret += ' ' + name; 981 | ret += '('; 982 | 983 | ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); 984 | return join( ret, this.parse(fn,'functionCode'), '}' ); 985 | }, 986 | array: array, 987 | nodelist: array, 988 | arguments: array, 989 | object:function( map ) { 990 | var ret = [ ]; 991 | this.up(); 992 | for ( var key in map ) 993 | ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); 994 | this.down(); 995 | return join( '{', ret, '}' ); 996 | }, 997 | node:function( node ) { 998 | var open = this.HTML ? '<' : '<', 999 | close = this.HTML ? '>' : '>'; 1000 | 1001 | var tag = node.nodeName.toLowerCase(), 1002 | ret = open + tag; 1003 | 1004 | for ( var a in this.DOMAttrs ) { 1005 | var val = node[this.DOMAttrs[a]]; 1006 | if ( val ) 1007 | ret += ' ' + a + '=' + this.parse( val, 'attribute' ); 1008 | } 1009 | return ret + close + open + '/' + tag + close; 1010 | }, 1011 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1012 | var l = fn.length; 1013 | if ( !l ) return ''; 1014 | 1015 | var args = Array(l); 1016 | while ( l-- ) 1017 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1018 | return ' ' + args.join(', ') + ' '; 1019 | }, 1020 | key:quote, //object calls it internally, the key part of an item in a map 1021 | functionCode:'[code]', //function calls it internally, it's the content of the function 1022 | attribute:quote, //node calls it internally, it's an html attribute value 1023 | string:quote, 1024 | date:quote, 1025 | regexp:literal, //regex 1026 | number:literal, 1027 | 'boolean':literal 1028 | }, 1029 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1030 | id:'id', 1031 | name:'name', 1032 | 'class':'className' 1033 | }, 1034 | HTML:true,//if true, entities are escaped ( <, >, \t, space and \n ) 1035 | indentChar:' ',//indentation unit 1036 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1037 | }; 1038 | 1039 | return jsDump; 1040 | })(); 1041 | 1042 | })(this); 1043 | -------------------------------------------------------------------------------- /dist/lib/stomp.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Client, Stomp; 3 | var __hasProp = Object.prototype.hasOwnProperty, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 4 | Stomp = { 5 | WebSocket: typeof WebSocket !== "undefined" && WebSocket !== null ? WebSocket : null, 6 | frame: function(command, headers, body) { 7 | if (headers == null) { 8 | headers = []; 9 | } 10 | if (body == null) { 11 | body = ''; 12 | } 13 | return { 14 | command: command, 15 | headers: headers, 16 | body: body, 17 | id: headers.id, 18 | receipt: headers.receipt, 19 | transaction: headers.transaction, 20 | destination: headers.destination, 21 | subscription: headers.subscription, 22 | error: null, 23 | toString: function() { 24 | var lines, name, value; 25 | lines = [command]; 26 | for (name in headers) { 27 | if (!__hasProp.call(headers, name)) continue; 28 | value = headers[name]; 29 | lines.push("" + name + ": " + value); 30 | } 31 | lines.push('\n' + body); 32 | return lines.join('\n'); 33 | } 34 | }; 35 | }, 36 | unmarshal: function(data) { 37 | var body, chr, command, divider, headerLines, headers, i, idx, line, trim, _ref, _ref2, _ref3; 38 | divider = data.search(/\n\n/); 39 | headerLines = data.substring(0, divider).split('\n'); 40 | command = headerLines.shift(); 41 | headers = {}; 42 | body = ''; 43 | trim = function(str) { 44 | return str.replace(/^\s+/g, '').replace(/\s+$/g, ''); 45 | }; 46 | line = idx = null; 47 | for (i = 0, _ref = headerLines.length; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) { 48 | line = headerLines[i]; 49 | idx = line.indexOf(':'); 50 | headers[trim(line.substring(0, idx))] = trim(line.substring(idx + 1)); 51 | } 52 | chr = null; 53 | for (i = _ref2 = divider + 2, _ref3 = data.length; _ref2 <= _ref3 ? i < _ref3 : i > _ref3; _ref2 <= _ref3 ? i++ : i--) { 54 | chr = data.charAt(i); 55 | if (chr === '\0') { 56 | break; 57 | } 58 | body += chr; 59 | } 60 | return Stomp.frame(command, headers, body); 61 | }, 62 | marshal: function(command, headers, body) { 63 | return Stomp.frame(command, headers, body).toString() + '\0'; 64 | }, 65 | client: function(url) { 66 | return new Client(url); 67 | } 68 | }; 69 | Client = (function() { 70 | function Client(url) { 71 | this.url = url; 72 | this.counter = 0; 73 | this.connected = false; 74 | this.subscriptions = {}; 75 | } 76 | Client.prototype._transmit = function(command, headers, body) { 77 | var out; 78 | out = Stomp.marshal(command, headers, body); 79 | if (typeof this.debug === "function") { 80 | this.debug(">>> " + out); 81 | } 82 | return this.ws.send(out); 83 | }; 84 | Client.prototype.connect = function(login_, passcode_, connectCallback, errorCallback) { 85 | if (typeof this.debug === "function") { 86 | this.debug("Opening Web Socket..."); 87 | } 88 | this.ws = new Stomp.WebSocket(this.url); 89 | this.ws.onmessage = __bind(function(evt) { 90 | var frame, onreceive; 91 | if (typeof this.debug === "function") { 92 | this.debug('<<< ' + evt.data); 93 | } 94 | frame = Stomp.unmarshal(evt.data); 95 | if (frame.command === "CONNECTED" && connectCallback) { 96 | this.connected = true; 97 | return connectCallback(frame); 98 | } else if (frame.command === "MESSAGE") { 99 | onreceive = this.subscriptions[frame.headers.subscription]; 100 | return typeof onreceive === "function" ? onreceive(frame) : void 0; 101 | } 102 | }, this); 103 | this.ws.onclose = __bind(function() { 104 | var msg; 105 | msg = "Whoops! Lost connection to " + this.url; 106 | if (typeof this.debug === "function") { 107 | this.debug(msg); 108 | } 109 | return typeof errorCallback === "function" ? errorCallback(msg) : void 0; 110 | }, this); 111 | this.ws.onopen = __bind(function() { 112 | if (typeof this.debug === "function") { 113 | this.debug('Web Socket Opened...'); 114 | } 115 | return this._transmit("CONNECT", { 116 | login: login_, 117 | passcode: passcode_ 118 | }); 119 | }, this); 120 | return this.connectCallback = connectCallback; 121 | }; 122 | Client.prototype.disconnect = function(disconnectCallback) { 123 | this._transmit("DISCONNECT"); 124 | this.ws.close(); 125 | this.connected = false; 126 | return typeof disconnectCallback === "function" ? disconnectCallback() : void 0; 127 | }; 128 | Client.prototype.send = function(destination, headers, body) { 129 | if (headers == null) { 130 | headers = {}; 131 | } 132 | if (body == null) { 133 | body = ''; 134 | } 135 | headers.destination = destination; 136 | return this._transmit("SEND", headers, body); 137 | }; 138 | Client.prototype.subscribe = function(destination, callback, headers) { 139 | var id; 140 | if (headers == null) { 141 | headers = {}; 142 | } 143 | id = "sub-" + this.counter++; 144 | headers.destination = destination; 145 | headers.id = id; 146 | this.subscriptions[id] = callback; 147 | this._transmit("SUBSCRIBE", headers); 148 | return id; 149 | }; 150 | Client.prototype.unsubscribe = function(id, headers) { 151 | if (headers == null) { 152 | headers = {}; 153 | } 154 | headers.id = id; 155 | delete this.subscriptions[id]; 156 | return this._transmit("UNSUBSCRIBE", headers); 157 | }; 158 | Client.prototype.begin = function(transaction, headers) { 159 | if (headers == null) { 160 | headers = {}; 161 | } 162 | headers.transaction = transaction; 163 | return this._transmit("BEGIN", headers); 164 | }; 165 | Client.prototype.commit = function(transaction, headers) { 166 | if (headers == null) { 167 | headers = {}; 168 | } 169 | headers.transaction = transaction; 170 | return this._transmit("COMMIT", headers); 171 | }; 172 | Client.prototype.abort = function(transaction, headers) { 173 | if (headers == null) { 174 | headers = {}; 175 | } 176 | headers.transaction = transaction; 177 | return this._transmit("ABORT", headers); 178 | }; 179 | Client.prototype.ack = function(message_id, headers) { 180 | if (headers == null) { 181 | headers = {}; 182 | } 183 | headers["message-id"] = message_id; 184 | return this._transmit("ACK", headers); 185 | }; 186 | return Client; 187 | })(); 188 | if (typeof window !== "undefined" && window !== null) { 189 | window.Stomp = Stomp; 190 | } else { 191 | exports.Stomp = Stomp; 192 | } 193 | }).call(this); 194 | -------------------------------------------------------------------------------- /dist/nullmq.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Queue, Socket, Stomp, assert, nullmq, 3 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 4 | __indexOf = Array.prototype.indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 5 | 6 | nullmq = { 7 | PUB: 'pub', 8 | SUB: 'sub', 9 | REQ: 'req', 10 | REP: 'rep', 11 | XREQ: 'dealer', 12 | XREP: 'router', 13 | PULL: 'pull', 14 | PUSH: 'push', 15 | DEALER: 'dealer', 16 | ROUTER: 'router', 17 | HWM: 100, 18 | IDENTITY: 101, 19 | SUBSCRIBE: 102, 20 | UNSUBSCRIBE: 103, 21 | _SENDERS: ['req', 'dealer', 'push', 'pub', 'router', 'rep'] 22 | }; 23 | 24 | assert = function(description, condition) { 25 | if (condition == null) condition = false; 26 | if (!condition) throw Error("Assertion: " + description); 27 | }; 28 | 29 | Queue = (function() { 30 | 31 | function Queue(maxsize) { 32 | this.maxsize = maxsize != null ? maxsize : null; 33 | this.queue = []; 34 | this.offset = 0; 35 | this.watches = []; 36 | } 37 | 38 | Queue.prototype.getLength = function() { 39 | return this.queue.length - this.offset; 40 | }; 41 | 42 | Queue.prototype.isEmpty = function() { 43 | return this.queue.length === 0; 44 | }; 45 | 46 | Queue.prototype.isFull = function() { 47 | if (this.maxsize === null) return false; 48 | return this.getLength() >= this.maxsize; 49 | }; 50 | 51 | Queue.prototype.put = function(item) { 52 | var _base; 53 | if (!this.isFull()) { 54 | this.queue.push(item); 55 | if (typeof (_base = this.watches.shift()) === "function") _base(); 56 | return item; 57 | } else { 58 | 59 | } 60 | }; 61 | 62 | Queue.prototype.get = function() { 63 | var item; 64 | if (this.queue.length === 0) return; 65 | item = this.queue[this.offset]; 66 | if (++this.offset * 2 >= this.queue.length) { 67 | this.queue = this.queue.slice(this.offset); 68 | this.offset = 0; 69 | } 70 | return item; 71 | }; 72 | 73 | Queue.prototype.peek = function() { 74 | if (this.queue.length > 0) { 75 | return this.queue[this.offset]; 76 | } else { 77 | return; 78 | } 79 | }; 80 | 81 | Queue.prototype.watch = function(fn) { 82 | if (this.queue.length === 0) { 83 | return this.watches.push(fn); 84 | } else { 85 | return fn(); 86 | } 87 | }; 88 | 89 | return Queue; 90 | 91 | })(); 92 | 93 | nullmq.Context = (function() { 94 | 95 | function Context(url, onconnect) { 96 | var _this = this; 97 | this.url = url; 98 | if (onconnect == null) onconnect = function() {}; 99 | this.active = false; 100 | this.client = Stomp.client(this.url); 101 | this.client.connect("guest", "guest", function() { 102 | var op, _results; 103 | _this.active = true; 104 | _results = []; 105 | while (op = _this.pending_operations.shift()) { 106 | _results.push(op()); 107 | } 108 | return _results; 109 | }); 110 | this.pending_operations = [onconnect]; 111 | this.sockets = []; 112 | } 113 | 114 | Context.prototype.socket = function(type) { 115 | return new Socket(this, type); 116 | }; 117 | 118 | Context.prototype.term = function() { 119 | var _this = this; 120 | return this._when_connected(function() { 121 | var socket, _i, _len, _ref; 122 | assert("context is already connected", _this.client.connected); 123 | _ref = _this.sockets; 124 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 125 | socket = _ref[_i]; 126 | socket.close(); 127 | } 128 | return _this.client.disconnect(); 129 | }); 130 | }; 131 | 132 | Context.prototype._send = function(socket, destination, message) { 133 | var _this = this; 134 | return this._when_connected(function() { 135 | var headers, part, _i, _len; 136 | assert("context is already connected", _this.client.connected); 137 | headers = { 138 | 'socket': socket.type 139 | }; 140 | if (socket.type === nullmq.REQ) { 141 | headers['reply-to'] = socket.connections[destination]; 142 | } 143 | if (socket.type === nullmq.REP) { 144 | headers['reply-to'] = socket.last_recv.reply_to; 145 | } 146 | if (message instanceof Array) { 147 | headers['transaction'] = Math.random() + ''; 148 | _this.client.begin(transaction); 149 | for (_i = 0, _len = message.length; _i < _len; _i++) { 150 | part = message[_i]; 151 | _this.client.send(destination, headers, part); 152 | } 153 | return _this.client.commit(transaction); 154 | } else { 155 | return _this.client.send(destination, headers, message.toString()); 156 | } 157 | }); 158 | }; 159 | 160 | Context.prototype._subscribe = function(type, socket, destination) { 161 | var _this = this; 162 | return this._when_connected(function() { 163 | var id; 164 | assert("context is already connected", _this.client.connected); 165 | id = _this.client.subscribe(destination, function(frame) { 166 | var envelope; 167 | envelope = { 168 | 'message': frame.body, 169 | 'destination': frame.destination 170 | }; 171 | if (frame.headers['reply-to'] != null) { 172 | envelope['reply_to'] = frame.headers['reply-to']; 173 | } 174 | return socket.recv_queue.put(envelope); 175 | }, { 176 | 'socket': socket.type, 177 | 'type': type 178 | }); 179 | return socket.connections[destination] = id; 180 | }); 181 | }; 182 | 183 | Context.prototype._connect = function(socket, destination) { 184 | return this._subscribe('connect', socket, destination); 185 | }; 186 | 187 | Context.prototype._bind = function(socket, destination) { 188 | return this._subscribe('bind', socket, destination); 189 | }; 190 | 191 | Context.prototype._when_connected = function(op) { 192 | if (this.client.connected) { 193 | return op(); 194 | } else { 195 | return this.pending_operations.push(op); 196 | } 197 | }; 198 | 199 | return Context; 200 | 201 | })(); 202 | 203 | Socket = (function() { 204 | 205 | function Socket(context, type) { 206 | var _ref; 207 | this.context = context; 208 | this.type = type; 209 | this._dispatch_outgoing = __bind(this._dispatch_outgoing, this); 210 | this.client = this.context.client; 211 | this.closed = false; 212 | this.recv_queue = new Queue(); 213 | this.send_queue = new Queue(); 214 | this.identity = null; 215 | this.linger = -1; 216 | this.filters = []; 217 | this.connections = {}; 218 | this.rr_index = 0; 219 | this.last_recv = void 0; 220 | this.context.sockets.push(this); 221 | if (_ref = this.type, __indexOf.call(nullmq._SENDERS, _ref) >= 0) { 222 | this.send_queue.watch(this._dispatch_outgoing); 223 | } 224 | } 225 | 226 | Socket.prototype.connect = function(destination) { 227 | if (__indexOf.call(Object.keys(this.connections), destination) >= 0) return; 228 | return this.context._connect(this, destination); 229 | }; 230 | 231 | Socket.prototype.bind = function(destination) { 232 | if (__indexOf.call(Object.keys(this.connections), destination) >= 0) return; 233 | return this.context._bind(this, destination); 234 | }; 235 | 236 | Socket.prototype.setsockopt = function(option, value) { 237 | var _ref; 238 | switch (option) { 239 | case nullmq.HWM: 240 | return this.hwm = value; 241 | case nullmq.IDENTITY: 242 | return this._identity(value); 243 | case nullmq.LINGER: 244 | return this.linger = value; 245 | case nullmq.SUBSCRIBE: 246 | if (this.type !== nullmq.SUB) return; 247 | if (_ref = !value, __indexOf.call(this.filters, _ref) >= 0) { 248 | this.filters.push(value); 249 | } 250 | return value; 251 | case nullmq.UNSUBSCRIBE: 252 | if (this.type !== nullmq.SUB) return; 253 | if (__indexOf.call(this.filters, value) >= 0) { 254 | this.filters.splice(this.filters.indexOf(value), 1); 255 | } 256 | return value; 257 | default: 258 | return; 259 | } 260 | }; 261 | 262 | Socket.prototype.getsockopt = function(option) { 263 | switch (option) { 264 | case nullmq.HWM: 265 | return this.hwm; 266 | case nullmq.IDENTITY: 267 | return this.identity; 268 | case nullmq.LINGER: 269 | return this.linger; 270 | default: 271 | return; 272 | } 273 | }; 274 | 275 | Socket.prototype.close = function() { 276 | var destination, id, _ref; 277 | _ref = this.connections; 278 | for (destination in _ref) { 279 | id = _ref[destination]; 280 | this.client.unsubscribe(id); 281 | } 282 | this.connections = {}; 283 | return this.closed = true; 284 | }; 285 | 286 | Socket.prototype.send = function(message) { 287 | var _ref; 288 | if ((_ref = this.type) === nullmq.PULL || _ref === nullmq.SUB) { 289 | throw Error("Sending is not implemented for this socket type"); 290 | } 291 | return this.send_queue.put(message); 292 | }; 293 | 294 | Socket.prototype.recv = function(callback) { 295 | var _this = this; 296 | return this.recv_queue.watch(function() { 297 | return callback(_this._recv()); 298 | }); 299 | }; 300 | 301 | Socket.prototype.recvall = function(callback) { 302 | var watcher, 303 | _this = this; 304 | watcher = function() { 305 | callback(_this._recv()); 306 | return _this.recv_queue.watch(watcher); 307 | }; 308 | return this.recv_queue.watch(watcher); 309 | }; 310 | 311 | Socket.prototype._recv = function() { 312 | var envelope; 313 | envelope = this.recv_queue.get(); 314 | this.last_recv = envelope; 315 | return envelope.message; 316 | }; 317 | 318 | Socket.prototype._identity = function(value) { 319 | return this.identity = value; 320 | }; 321 | 322 | Socket.prototype._deliver_round_robin = function(message) { 323 | var connection_count, destination; 324 | destination = Object.keys(this.connections)[this.rr_index]; 325 | this.context._send(this, destination, message); 326 | connection_count = Object.keys(this.connections).length; 327 | return this.rr_index = ++this.rr_index % connection_count; 328 | }; 329 | 330 | Socket.prototype._deliver_fanout = function(message) { 331 | var destination, id, _ref, _results; 332 | _ref = this.connections; 333 | _results = []; 334 | for (destination in _ref) { 335 | id = _ref[destination]; 336 | _results.push(this.context._send(this, destination, message)); 337 | } 338 | return _results; 339 | }; 340 | 341 | Socket.prototype._deliver_routed = function(message) { 342 | var destination; 343 | destination = message.shift(); 344 | return this.context._send(this, destination, message); 345 | }; 346 | 347 | Socket.prototype._deliver_back = function(message) { 348 | return this.context._send(this, this.last_recv.destination, message); 349 | }; 350 | 351 | Socket.prototype._dispatch_outgoing = function() { 352 | var message; 353 | if (this.context.active) { 354 | message = this.send_queue.get(); 355 | switch (this.type) { 356 | case nullmq.REQ: 357 | case nullmq.DEALER: 358 | case nullmq.PUSH: 359 | this._deliver_round_robin(message); 360 | break; 361 | case nullmq.PUB: 362 | this._deliver_fanout(message); 363 | break; 364 | case nullmq.ROUTER: 365 | this._deliver_routed(message); 366 | break; 367 | case nullmq.REP: 368 | this._deliver_back(message); 369 | break; 370 | default: 371 | assert("outgoing dispatching shouldn't happen for this socket type"); 372 | } 373 | return this.send_queue.watch(this._dispatch_outgoing); 374 | } else { 375 | return setTimeout(this._dispatch_outgoing, 20); 376 | } 377 | }; 378 | 379 | return Socket; 380 | 381 | })(); 382 | 383 | if (typeof window !== "undefined" && window !== null) { 384 | window.nullmq = nullmq; 385 | if (!(window.Stomp != null)) { 386 | console.log("Required Stomp library not loaded."); 387 | } else { 388 | Stomp = window.Stomp; 389 | } 390 | } else { 391 | exports.nullmq = nullmq; 392 | exports.Queue = Queue; 393 | Stomp = require('./lib/stomp.js').Stomp; 394 | } 395 | 396 | }).call(this); 397 | -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | requirements: 2 | jasmine-node 3 | growl 4 | coffee-script 5 | 6 | revelations: 7 | - server is not like a device. it's a shared endpoint-space 8 | - no reason to re-use subscriptions. each connection/binding should have its own subscription 9 | 10 | 11 | 12 | v0.1 out of scope 13 | - performance 14 | - multipart 15 | - reconnects 16 | - linger? 17 | 18 | NullMQ: ZeroMQ-like sockets using STOMP over WebSocket 19 | 20 | automatic reconnects 21 | dynamic message batching 22 | queue 23 | 24 | Reflector pub/sub example 25 | ================= 26 | // Browser A, Publisher 27 | context = nullmq.Context('http://endpoint') 28 | socklet = context.socklet(nullmq.PUB) 29 | socklet.connect('/foo/bar') 30 | socklet.send("GET THIS") 31 | 32 | // Browser B, Subscriber 33 | context = nullmq.Context('http://endpoint') 34 | socklet = context.socklet(nullmq.SUB) 35 | socklet.connect('/foo/bar') 36 | socklet.setsockopt(nullmq.SUBSCRIBE, '') 37 | socklet.recvall(function (msg) { 38 | // got it! 39 | }) 40 | 41 | Reflector req/rep example 42 | ========================= 43 | // Browser A, Requester (client) 44 | context = nullmq.Context('http://endpoint') 45 | socklet = context.socklet(nullmq.REQ) 46 | socklet.connect('/service') 47 | socklet.send("give me a thing") 48 | socklet.recv(function (reply) { 49 | // yay! 50 | }); 51 | 52 | // Browser A, Replyer (server) 53 | context = nullmq.Context('http://endpoint') 54 | socklet = context.socklet(nullmq.REP) 55 | socklet.bind('/service') // Bind semantics here mean "don't let others bind as REP" 56 | socklet.recvall(function (req) { 57 | socklet.send("here's your thing"); 58 | }); 59 | 60 | 61 | 62 | context = nullmq.Context('http://endpoint') 63 | socklet = context.socklet(nullmq.SUB) 64 | socklet.connect('/presence/updates') 65 | socklet.recv(function (msg) { 66 | 67 | }) 68 | socklet.recvall(function (msg) { 69 | 70 | }) 71 | socklet.send() 72 | socklet.sendmulti() 73 | 74 | socklet = conn.socklet(nullmq.REQ) 75 | socklet.connect('/presence/snapshot') 76 | 77 | socklet = conn.socklet(nullmq.PUSH) 78 | socklet.connect('/presence/update') 79 | 80 | 81 | REQ 82 | connect 83 | SUBSCRIBE 84 | destination:/named-connection 85 | identity:(socket identity if set) 86 | send (client to server) 87 | SEND 88 | destination:/named-connection 89 | request-id: 1 90 | recv (server to client) 91 | MESSAGE 92 | destination:/named-connection 93 | message-id: 123 94 | request-id: 1 95 | REP 96 | connect 97 | SUBSCRIBE 98 | destination:/named-connection 99 | identity:(socket identity if set) 100 | recv (server to client) 101 | MESSAGE 102 | destination:/named-connection 103 | message-id: 123 104 | request-id: 1 105 | send (client to server) 106 | SEND 107 | destination:/named-connection 108 | request-id: 1 109 | PUSH 110 | send (client to server) 111 | SEND 112 | destination:/named-connection 113 | PULL 114 | connect 115 | SUBSCRIBE 116 | destination:/named-connection 117 | identity:(socket identity if set) 118 | recv (server to client) 119 | MESSAGE 120 | destination:/named-connection 121 | message-id: 123 122 | PUB 123 | send (client to server) 124 | SEND 125 | destination:/named-connection 126 | SUB 127 | connect 128 | SUBSCRIBE 129 | destination:/named-connection 130 | identity:(socket identity if set) 131 | recv (server to client) 132 | MESSAGE 133 | destination:/named-connection 134 | message-id: 123 135 | PAIR 136 | connect 137 | SUBSCRIBE 138 | destination:/named-connection 139 | identity:(socket identity if set) 140 | recv (server to client) 141 | MESSAGE 142 | destination:/named-connection 143 | message-id: 123 144 | send (client to server) 145 | SEND 146 | destination:/named-connection 147 | DEALER (PUSH/PULL) 148 | connect 149 | SUBSCRIBE 150 | destination:/named-connection 151 | identity:(socket identity if set) 152 | recv (server to client) 153 | MESSAGE 154 | destination:/named-connection 155 | message-id: 123 156 | send (client to server) 157 | SEND 158 | destination:/named-connection 159 | ROUTER 160 | connect 161 | SUBSCRIBE 162 | destination:/named-connection 163 | identity:(socket identity if set) 164 | recv (server to client) 165 | MESSAGE 166 | destination:/named-connection 167 | message-id: 123 168 | send (client to server) 169 | SEND 170 | destination:/named-connection 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nullmq" 3 | , "version": "0.1.0" 4 | , "dependencies" : { 5 | "jasmine-node": "*" 6 | } 7 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | coffee-script 2 | jasmine-node -------------------------------------------------------------------------------- /src/nullmq.coffee: -------------------------------------------------------------------------------- 1 | nullmq = 2 | # socket types 3 | #PAIR: 'pair' # Not sold we should support PAIR 4 | PUB: 'pub' 5 | SUB: 'sub' 6 | REQ: 'req' 7 | REP: 'rep' 8 | XREQ: 'dealer' # Deprecated in favor of DEALER 9 | XREP: 'router' # Deprecated in favor of ROUTER 10 | PULL: 'pull' 11 | PUSH: 'push' 12 | DEALER: 'dealer' 13 | ROUTER: 'router' 14 | 15 | # socket options 16 | HWM: 100 17 | IDENTITY: 101 18 | SUBSCRIBE: 102 19 | UNSUBSCRIBE: 103 20 | #LINGER: 104 21 | #RECONNECT_IVL_MAX: 105 22 | #RECONNECT_IVL: 106 23 | #RCVMORE: 107 24 | #SNDMORE: 108 25 | 26 | _SENDERS: ['req', 'dealer', 'push', 'pub', 'router', 'rep'] 27 | 28 | assert = (description, condition=false) -> 29 | # We assert assumed state so we can more easily catch bugs. 30 | # Do not assert if we *know* the user can get to it. 31 | throw Error "Assertion: #{description}" if not condition 32 | 33 | class Queue 34 | # Originally based on the implementation here: 35 | # http://code.stephenmorley.org/javascript/queues/ 36 | 37 | constructor: (@maxsize=null) -> 38 | @queue = [] 39 | @offset = 0 40 | @watches = [] 41 | 42 | getLength: -> 43 | @queue.length - @offset 44 | 45 | isEmpty: -> 46 | @queue.length is 0 47 | 48 | isFull: -> 49 | if @maxsize is null then return false 50 | return @getLength() >= @maxsize 51 | 52 | put: (item) -> 53 | if not @isFull() 54 | @queue.push item 55 | @watches.shift()?() 56 | return item 57 | else 58 | return undefined 59 | 60 | get: -> 61 | if @queue.length is 0 then return undefined 62 | item = @queue[@offset] 63 | if ++@offset*2 >= @queue.length 64 | @queue = @queue.slice(@offset) 65 | @offset = 0 66 | item 67 | 68 | peek: -> 69 | if @queue.length > 0 then @queue[@offset] else undefined 70 | 71 | watch: (fn) -> 72 | if @queue.length is 0 73 | @watches.push(fn) 74 | else 75 | fn() 76 | 77 | class nullmq.Context 78 | constructor: (@url, onconnect=->) -> 79 | @active = false 80 | @client = Stomp.client(@url) 81 | @client.connect "guest", "guest", => 82 | @active = true 83 | op() while op = @pending_operations.shift() 84 | @pending_operations = [onconnect] 85 | @sockets = [] 86 | 87 | socket: (type) -> 88 | new Socket this, type 89 | 90 | term: -> 91 | @_when_connected => 92 | assert "context is already connected", @client.connected 93 | for socket in @sockets 94 | socket.close() 95 | @client.disconnect() 96 | 97 | _send: (socket, destination, message) -> 98 | @_when_connected => 99 | assert "context is already connected", @client.connected 100 | headers = {'socket': socket.type} 101 | if socket.type is nullmq.REQ 102 | headers['reply-to'] = socket.connections[destination] 103 | if socket.type is nullmq.REP 104 | headers['reply-to'] = socket.last_recv.reply_to 105 | if message instanceof Array 106 | headers['transaction'] = Math.random()+'' 107 | @client.begin transaction 108 | for part in message 109 | @client.send destination, headers, part 110 | @client.commit transaction 111 | else 112 | @client.send destination, headers, message.toString() 113 | 114 | _subscribe: (type, socket, destination) -> 115 | @_when_connected => 116 | assert "context is already connected", @client.connected 117 | id = @client.subscribe destination, (frame) => 118 | envelope = {'message': frame.body, 'destination': frame.destination} 119 | if frame.headers['reply-to']? 120 | envelope['reply_to'] = frame.headers['reply-to'] 121 | socket.recv_queue.put envelope 122 | , {'socket': socket.type, 'type': type} 123 | socket.connections[destination] = id 124 | 125 | _connect: (socket, destination) -> @_subscribe('connect', socket, destination) 126 | _bind: (socket, destination) -> @_subscribe('bind', socket, destination) 127 | 128 | _when_connected: (op) -> 129 | if @client.connected then op() else @pending_operations.push op 130 | 131 | 132 | class Socket 133 | constructor: (@context, @type) -> 134 | @client = @context.client 135 | @closed = false 136 | @recv_queue = new Queue() 137 | @send_queue = new Queue() 138 | @identity = null 139 | @linger = -1 140 | @filters = [] 141 | @connections = {} 142 | @rr_index = 0 # round-robin counter 143 | @last_recv = undefined 144 | @context.sockets.push this 145 | if @type in nullmq._SENDERS 146 | @send_queue.watch @_dispatch_outgoing 147 | 148 | connect: (destination) -> 149 | if destination in Object.keys(@connections) then return 150 | @context._connect this, destination 151 | 152 | bind: (destination) -> 153 | if destination in Object.keys(@connections) then return 154 | @context._bind this, destination 155 | 156 | setsockopt: (option, value) -> 157 | switch option 158 | when nullmq.HWM then @hwm = value 159 | when nullmq.IDENTITY then @_identity value 160 | when nullmq.LINGER then @linger = value 161 | when nullmq.SUBSCRIBE 162 | if @type isnt nullmq.SUB then return undefined 163 | if not value in @filters 164 | @filters.push value 165 | value 166 | when nullmq.UNSUBSCRIBE 167 | if @type isnt nullmq.SUB then return undefined 168 | if value in @filters 169 | @filters.splice @filters.indexOf(value), 1 170 | value 171 | else undefined 172 | 173 | getsockopt: (option) -> 174 | switch option 175 | when nullmq.HWM then @hwm 176 | when nullmq.IDENTITY then @identity 177 | when nullmq.LINGER then @linger 178 | else undefined 179 | 180 | close: -> 181 | for destination, id of @connections 182 | @client.unsubscribe id 183 | @connections = {} 184 | @closed = true 185 | 186 | send: (message) -> 187 | if @type in [nullmq.PULL, nullmq.SUB] 188 | throw Error("Sending is not implemented for this socket type") 189 | @send_queue.put(message) 190 | 191 | recv: (callback) -> 192 | @recv_queue.watch => 193 | callback @_recv() 194 | 195 | recvall: (callback) -> 196 | watcher = => 197 | callback @_recv() 198 | @recv_queue.watch watcher 199 | @recv_queue.watch watcher 200 | 201 | _recv: -> 202 | envelope = @recv_queue.get() 203 | @last_recv = envelope 204 | envelope.message 205 | 206 | _identity: (value) -> 207 | @identity = value 208 | 209 | _deliver_round_robin: (message) -> 210 | destination = Object.keys(@connections)[@rr_index] 211 | @context._send this, destination, message 212 | connection_count = Object.keys(@connections).length 213 | @rr_index = ++@rr_index % connection_count 214 | 215 | _deliver_fanout: (message) -> 216 | for destination, id of @connections 217 | @context._send this, destination, message 218 | 219 | _deliver_routed: (message) -> 220 | destination = message.shift() 221 | @context._send this, destination, message 222 | 223 | _deliver_back: (message) -> 224 | @context._send this, @last_recv.destination, message 225 | 226 | _dispatch_outgoing: => 227 | if @context.active 228 | message = @send_queue.get() 229 | switch @type 230 | when nullmq.REQ, nullmq.DEALER, nullmq.PUSH 231 | @_deliver_round_robin message 232 | when nullmq.PUB 233 | @_deliver_fanout message 234 | when nullmq.ROUTER 235 | @_deliver_routed message 236 | when nullmq.REP 237 | @_deliver_back message 238 | else 239 | assert "outgoing dispatching shouldn't happen for this socket type" 240 | @send_queue.watch @_dispatch_outgoing 241 | else 242 | setTimeout @_dispatch_outgoing, 20 243 | 244 | if window? 245 | # For use in the browser 246 | window.nullmq = nullmq 247 | if not window.Stomp? 248 | console.log "Required Stomp library not loaded." 249 | else 250 | Stomp = window.Stomp 251 | else 252 | # For testing 253 | exports.nullmq = nullmq 254 | exports.Queue = Queue 255 | {Stomp} = require('./lib/stomp.js') 256 | -------------------------------------------------------------------------------- /test/nullmq.spec.coffee: -------------------------------------------------------------------------------- 1 | {nullmq} = require('../nullmq.js') 2 | {Stomp} = require('../lib/stomp.js') 3 | Stomp.WebSocket = require('./server.mock.js').ReflectorServerMock 4 | 5 | describe "nullmq with reflector", -> 6 | it "round robins and fans-in for PUSH-PULL pipelining", -> 7 | url = "ws://endpoint" 8 | received = 0 9 | messages = [] 10 | sink = null 11 | message = "Foobar" 12 | ctx = new nullmq.Context url, -> 13 | worker1in = ctx.socket(nullmq.PULL) 14 | worker1out = ctx.socket(nullmq.PUSH) 15 | worker1in.connect('/source') 16 | worker1out.connect('/sink') 17 | worker1in.recvall (msg) -> 18 | worker1out.send "worker1: #{msg}" 19 | 20 | worker2in = ctx.socket(nullmq.PULL) 21 | worker2out = ctx.socket(nullmq.PUSH) 22 | worker2in.connect('/source') 23 | worker2out.connect('/sink') 24 | worker2in.recvall (msg) -> 25 | worker2out.send "worker2: #{msg}" 26 | 27 | sink = ctx.socket(nullmq.PULL) 28 | sink.bind('/sink') 29 | sink.recvall (msg) -> 30 | messages.push msg 31 | 32 | source = ctx.socket(nullmq.PUSH) 33 | source.bind('/source') 34 | source.send message 35 | source.send message 36 | source.send message 37 | 38 | waitsFor -> messages.length > 2 39 | runs -> 40 | expect(messages).toContain "worker1: #{message}" 41 | expect(messages).toContain "worker2: #{message}" 42 | expect(messages.length).toBe 3 43 | ctx.term() 44 | 45 | it "round robin to REP sockets over several destinations from REQ socket", -> 46 | url = "ws://endpoint" 47 | received = 0 48 | messages = [] 49 | req = null 50 | ctx = new nullmq.Context url, -> 51 | rep1 = ctx.socket(nullmq.REP) 52 | rep1.connect('/destination1') 53 | rep1.recvall (msg) -> 54 | rep1.send "rep1" 55 | 56 | rep2 = ctx.socket(nullmq.REP) 57 | rep2.bind('/destination2') 58 | rep2.recvall (msg) -> 59 | rep2.send "rep2" 60 | 61 | req = ctx.socket(nullmq.REQ) 62 | req.connect('/destination1') 63 | req.connect('/destination2') 64 | req.send("foo") 65 | req.recv (reply) -> 66 | messages.push reply 67 | received++ 68 | 69 | waitsFor -> received > 0 70 | runs -> 71 | expect(messages).toContain "rep1" 72 | req.send("bar") 73 | req.recv (reply) -> 74 | messages.push reply 75 | received++ 76 | 77 | waitsFor -> received > 1 78 | runs -> 79 | expect(messages).toContain "rep2" 80 | ctx.term() 81 | 82 | it "does fanout when using PUB and SUB over several destinations", -> 83 | url = "ws://endpoint" 84 | received = 0 85 | [messages1, messages2, messages3] = [[],[],[]] 86 | msg = "Hello world" 87 | ctx = new nullmq.Context url, -> 88 | 89 | sub1 = ctx.socket(nullmq.SUB) 90 | sub1.connect('/destination1') 91 | sub1.setsockopt(nullmq.SUBSCRIBE, '') 92 | sub1.recvall (msg) -> 93 | messages1.push(msg) 94 | received++ 95 | 96 | sub2 = ctx.socket(nullmq.SUB) 97 | sub2.connect('/destination2') 98 | sub2.setsockopt(nullmq.SUBSCRIBE, '') 99 | sub2.recvall (msg) -> 100 | messages2.push(msg) 101 | received++ 102 | 103 | sub3 = ctx.socket(nullmq.SUB) 104 | sub3.connect('/destination1') 105 | sub3.connect('/destination2') 106 | sub3.setsockopt(nullmq.SUBSCRIBE, '') 107 | sub3.recvall (msg) -> 108 | messages3.push(msg) 109 | received++ 110 | 111 | pub = ctx.socket(nullmq.PUB) 112 | pub.connect('/destination1') 113 | pub.connect('/destination2') 114 | pub.send(msg) 115 | 116 | waitsFor -> received >= 4 117 | runs -> 118 | expect(messages1).toContain msg 119 | expect(messages2).toContain msg 120 | expect(messages3).toContain msg 121 | expect(messages3.length).toEqual 2 122 | ctx.term() 123 | 124 | 125 | describe "nullmq namespace", -> 126 | it "can create a Context", -> 127 | url = "ws://endpoint" 128 | ctx = new nullmq.Context url, -> 129 | expect(ctx.url).toEqual url 130 | expect(ctx.sockets.length).toEqual 0 131 | ctx.term() 132 | 133 | describe "nullmq.Context", -> 134 | url = "ws://endpoint" 135 | 136 | it "can create a Socket", -> 137 | ctx = new nullmq.Context url, -> 138 | socket = ctx.socket(nullmq.REQ) 139 | expect(socket.type).toEqual nullmq.REQ 140 | ctx.term() 141 | 142 | it "closes sockets on termination", -> 143 | ctx = new nullmq.Context url, -> 144 | socket1 = ctx.socket(nullmq.REQ) 145 | socket2 = ctx.socket(nullmq.REQ) 146 | expect(socket1.closed).toEqual false 147 | expect(socket2.closed).toEqual false 148 | ctx.term() 149 | expect(socket1.closed).toEqual true 150 | expect(socket2.closed).toEqual true 151 | -------------------------------------------------------------------------------- /test/queue.spec.coffee: -------------------------------------------------------------------------------- 1 | {Queue} = require('../nullmq.js') 2 | 3 | describe "Queue", -> 4 | it "gives you FIFO queue behavior", -> 5 | q = new Queue() 6 | item1 = "foobar" 7 | item2 = ["foobar"] 8 | item3 = {foo: "bar"} 9 | item4 = 42 10 | q.put(item1) 11 | q.put(item2) 12 | q.put(item3) 13 | q.put(item4) 14 | expect(q.get()).toBe item1 15 | expect(q.get()).toBe item2 16 | expect(q.get()).toBe item3 17 | expect(q.get()).toBe item4 18 | 19 | it "can have a maximum size", -> 20 | q = new Queue(3) 21 | items = ["foo", "bar", "baz", "qux"] 22 | expect(q.isFull()).toBeFalsy() 23 | expect(q.put(items[0])).toBe items[0] 24 | q.put(items[1]) 25 | q.put(items[2]) 26 | expect(q.isFull()).toBeTruthy() 27 | expect(q.put(items[3])).toBeUndefined() 28 | expect(q.getLength()).toEqual 3 29 | 30 | it "lets you peek without dequeuing", -> 31 | q = new Queue() 32 | q.put("foo") 33 | q.put("bar") 34 | q.put("baz") 35 | q.get() # Drop "foo" 36 | expect(q.peek()).toEqual "bar" 37 | expect(q.get()).toEqual "bar" 38 | 39 | it "lets you register a one-time watch callback for items", -> 40 | q = new Queue() 41 | from_watch = [] 42 | watcher = -> 43 | from_watch.push q.get() 44 | q.watch watcher 45 | q.put("item1") 46 | q.put("item2") 47 | expect(q.getLength()).toEqual 1 48 | expect(from_watch.length).toEqual 1 49 | expect(from_watch).toContain "item1" 50 | q.watch watcher 51 | expect(from_watch.length).toEqual 2 52 | expect(from_watch).toContain "item2" -------------------------------------------------------------------------------- /test/server.mock.coffee: -------------------------------------------------------------------------------- 1 | {WebSocketMock} = require('./websocket.mock.js') 2 | {Stomp} = require('../lib/stomp.js') 3 | 4 | class exports.ReflectorServerMock extends WebSocketMock 5 | # WebSocketMock handlers 6 | 7 | handle_send: (msg) => 8 | @stomp_dispatch(Stomp.unmarshal(msg)) 9 | 10 | handle_close: => 11 | @_shutdown() 12 | 13 | handle_open: => 14 | @stomp_init() 15 | @_accept() 16 | 17 | # Stomp server implementation. 18 | # Keep in mind this Stomp server is 19 | # meant to simulate a NullMQ reflector 20 | 21 | stomp_init: -> 22 | @transactions = {} 23 | @subscriptions = {} 24 | @mailbox = [] 25 | @rr_index = 0 26 | @router = setInterval => 27 | if @readyState isnt 1 then clearInterval @router 28 | is_corresponding = (subscription, frame) -> 29 | # Returns true if destination and socket pairings match 30 | socket_routing = 31 | pub: 'sub' 32 | req: 'rep' 33 | rep: 'req' 34 | push: 'pull' 35 | subscription.destination is frame.destination and \ 36 | subscription.socket is socket_routing[frame.headers.socket] 37 | while frame = @mailbox.shift() 38 | # Core routing logic of the reflector 39 | switch frame.headers['socket'] 40 | when 'pub' 41 | # Fanout 42 | for id, sub of @subscriptions when is_corresponding sub, frame 43 | sub.callback(Math.random(), frame.body) 44 | when 'req' 45 | # Round robin w/ reply-to 46 | reps = (sub for id, sub of @subscriptions when is_corresponding sub, frame) 47 | reply_to = frame.headers['reply-to'] 48 | @rr_index = ++@rr_index % reps.length 49 | reps[@rr_index].callback(Math.random(), frame.body, {'reply-to': reply_to}) 50 | when 'rep' 51 | # Reply-to lookup 52 | reply_to = frame.headers['reply-to'] 53 | @subscriptions[reply_to].callback(Math.random(), frame.body) 54 | when 'push' 55 | # Round robin 56 | pulls = (sub for id, sub of @subscriptions when is_corresponding sub, frame) 57 | @rr_index = ++@rr_index % pulls.length 58 | pulls[@rr_index].callback(Math.random(), frame.body) 59 | , 20 60 | 61 | stomp_send: (command, headers, body=null) -> 62 | @_respond(Stomp.marshal(command, headers, body)) 63 | 64 | stomp_send_receipt: (frame) -> 65 | if frame.error? 66 | @stomp_send("ERROR", {'receipt-id': frame.receipt, 'message': frame.error}) 67 | else 68 | @stomp_send("RECEIPT", {'receipt-id': frame.receipt}) 69 | 70 | stomp_send_message: (destination, subscription, message_id, body, headers={}) -> 71 | headers['destination'] = destination 72 | headers['message-id'] = message_id 73 | headers['subscription'] = subscription 74 | @stomp_send("MESSAGE", headers, body) 75 | 76 | stomp_dispatch: (frame) -> 77 | handler = "stomp_handle_#{frame.command.toLowerCase()}" 78 | if this[handler]? 79 | this[handler](frame) 80 | if frame.receipt 81 | @stomp_send_receipt(frame) 82 | else 83 | console.log "StompServerMock: Unknown command: #{frame.command}" 84 | 85 | stomp_handle_connect: (frame) -> 86 | @session_id = Math.random() 87 | @stomp_send("CONNECTED", {'session': @session_id}) 88 | 89 | stomp_handle_begin: (frame) -> 90 | @transactions[frame.transaction] = [] 91 | 92 | stomp_handle_commit: (frame) -> 93 | transaction = @transactions[frame.transaction] 94 | for frame in transaction 95 | @mailbox.push(frame) 96 | delete @transactions[frame.transaction] 97 | 98 | stomp_handle_abort: (frame) -> 99 | delete @transactions[frame.transaction] 100 | 101 | stomp_handle_send: (frame) -> 102 | if frame.transaction 103 | @transactions[frame.transaction].push frame 104 | else 105 | @mailbox.push frame 106 | 107 | stomp_handle_subscribe: (frame) -> 108 | sub_id = frame.id or Math.random() 109 | cb = (id, body, headers={}) => 110 | @stomp_send_message(frame.destination, sub_id, id, body, headers) 111 | @subscriptions[sub_id] = 112 | destination: frame.destination 113 | callback: cb 114 | type: frame.headers.type 115 | socket: frame.headers.socket 116 | 117 | stomp_handle_unsubscribe: (frame) -> 118 | if frame.id in Object.keys(@subscriptions) 119 | delete @subscriptions[frame.id] 120 | else 121 | frame.error = "Subscription does not exist" 122 | 123 | stomp_handle_disconnect: (frame) -> 124 | @_shutdown() 125 | 126 | # Test helpers 127 | 128 | test_send: (sub_id, message) -> 129 | msgid = 'msg-' + Math.random() 130 | @subscriptions[sub_id][1](msgid, message) 131 | -------------------------------------------------------------------------------- /test/websocket.mock.coffee: -------------------------------------------------------------------------------- 1 | class exports.WebSocketMock 2 | constructor: (@url) -> 3 | @onclose = -> 4 | @onopen = -> 5 | @onerror = -> 6 | @onmessage = -> 7 | @readyState = 0 8 | @bufferedAmount = 0 9 | @extensions = '' 10 | @protocol = '' 11 | setTimeout(@handle_open, 0) 12 | 13 | # WebSocket API 14 | 15 | close: -> 16 | @handle_close() 17 | @readyState = 2 18 | 19 | send: (msg) -> 20 | if @readyState isnt 1 then return false 21 | @handle_send(msg) 22 | return true 23 | 24 | # Helpers 25 | 26 | _accept: -> 27 | @readyState = 1 28 | @onopen({'type': 'open'}) 29 | 30 | _shutdown: -> 31 | @readyState = 3 32 | @onclose({'type': 'close'}) 33 | 34 | _error: -> 35 | @readyState = 3 36 | @onerror({'type': 'error'}) 37 | 38 | _respond: (data) -> 39 | @onmessage({'type': 'message', 'data': data}) 40 | 41 | # Handlers 42 | 43 | handle_send: (msg) -> 44 | # implement me 45 | 46 | handle_close: -> 47 | # implement me 48 | 49 | handle_open: -> 50 | # implement me --------------------------------------------------------------------------------