_bushido-faye 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/History.txt +247 -0
  2. data/README.rdoc +92 -0
  3. data/lib/faye-browser-min.js +1 -0
  4. data/lib/faye.rb +121 -0
  5. data/lib/faye/adapters/rack_adapter.rb +210 -0
  6. data/lib/faye/engines/connection.rb +60 -0
  7. data/lib/faye/engines/memory.rb +112 -0
  8. data/lib/faye/engines/proxy.rb +111 -0
  9. data/lib/faye/error.rb +49 -0
  10. data/lib/faye/mixins/logging.rb +47 -0
  11. data/lib/faye/mixins/publisher.rb +30 -0
  12. data/lib/faye/mixins/timeouts.rb +22 -0
  13. data/lib/faye/protocol/channel.rb +124 -0
  14. data/lib/faye/protocol/client.rb +378 -0
  15. data/lib/faye/protocol/extensible.rb +43 -0
  16. data/lib/faye/protocol/grammar.rb +58 -0
  17. data/lib/faye/protocol/publication.rb +5 -0
  18. data/lib/faye/protocol/server.rb +282 -0
  19. data/lib/faye/protocol/subscription.rb +24 -0
  20. data/lib/faye/transport/http.rb +76 -0
  21. data/lib/faye/transport/local.rb +22 -0
  22. data/lib/faye/transport/transport.rb +115 -0
  23. data/lib/faye/transport/web_socket.rb +99 -0
  24. data/lib/faye/util/namespace.rb +20 -0
  25. data/spec/browser.html +45 -0
  26. data/spec/encoding_helper.rb +7 -0
  27. data/spec/install.sh +78 -0
  28. data/spec/javascript/channel_spec.js +15 -0
  29. data/spec/javascript/client_spec.js +714 -0
  30. data/spec/javascript/engine/memory_spec.js +7 -0
  31. data/spec/javascript/engine_spec.js +417 -0
  32. data/spec/javascript/faye_spec.js +15 -0
  33. data/spec/javascript/grammar_spec.js +66 -0
  34. data/spec/javascript/node_adapter_spec.js +307 -0
  35. data/spec/javascript/publisher_spec.js +27 -0
  36. data/spec/javascript/server/connect_spec.js +168 -0
  37. data/spec/javascript/server/disconnect_spec.js +121 -0
  38. data/spec/javascript/server/extensions_spec.js +60 -0
  39. data/spec/javascript/server/handshake_spec.js +145 -0
  40. data/spec/javascript/server/integration_spec.js +124 -0
  41. data/spec/javascript/server/publish_spec.js +85 -0
  42. data/spec/javascript/server/subscribe_spec.js +247 -0
  43. data/spec/javascript/server/unsubscribe_spec.js +245 -0
  44. data/spec/javascript/server_spec.js +110 -0
  45. data/spec/javascript/transport_spec.js +130 -0
  46. data/spec/node.js +55 -0
  47. data/spec/phantom.js +17 -0
  48. data/spec/ruby/channel_spec.rb +17 -0
  49. data/spec/ruby/client_spec.rb +724 -0
  50. data/spec/ruby/engine/memory_spec.rb +7 -0
  51. data/spec/ruby/engine_examples.rb +427 -0
  52. data/spec/ruby/faye_spec.rb +14 -0
  53. data/spec/ruby/grammar_spec.rb +68 -0
  54. data/spec/ruby/publisher_spec.rb +27 -0
  55. data/spec/ruby/rack_adapter_spec.rb +236 -0
  56. data/spec/ruby/server/connect_spec.rb +170 -0
  57. data/spec/ruby/server/disconnect_spec.rb +120 -0
  58. data/spec/ruby/server/extensions_spec.rb +68 -0
  59. data/spec/ruby/server/handshake_spec.rb +143 -0
  60. data/spec/ruby/server/integration_spec.rb +126 -0
  61. data/spec/ruby/server/publish_spec.rb +81 -0
  62. data/spec/ruby/server/subscribe_spec.rb +247 -0
  63. data/spec/ruby/server/unsubscribe_spec.rb +247 -0
  64. data/spec/ruby/server_spec.rb +110 -0
  65. data/spec/ruby/transport_spec.rb +134 -0
  66. data/spec/spec_helper.rb +11 -0
  67. data/spec/testswarm +29 -0
  68. data/spec/thin_proxy.rb +37 -0
  69. metadata +302 -0
@@ -0,0 +1,22 @@
1
+ module Faye
2
+
3
+ class Transport::Local < Transport
4
+ def self.usable?(endpoint, &callback)
5
+ callback.call(endpoint.is_a?(Server))
6
+ end
7
+
8
+ def batching?
9
+ false
10
+ end
11
+
12
+ def request(message, timeout)
13
+ message = Faye.copy_object(message)
14
+ @endpoint.process(message, true) do |responses|
15
+ receive(Faye.copy_object(responses))
16
+ end
17
+ end
18
+ end
19
+
20
+ Transport.register 'in-process', Transport::Local
21
+
22
+ end
@@ -0,0 +1,115 @@
1
+ module Faye
2
+ class Transport
3
+
4
+ include Logging
5
+ include Publisher
6
+ include Timeouts
7
+
8
+ attr_accessor :cookies, :headers
9
+
10
+ def initialize(client, endpoint)
11
+ debug('Created new ? transport for ?', connection_type, endpoint)
12
+ @client = client
13
+ @endpoint = endpoint
14
+ @outbox = []
15
+ end
16
+
17
+ def batching?
18
+ true
19
+ end
20
+
21
+ def close
22
+ end
23
+
24
+ def connection_type
25
+ self.class.connection_type
26
+ end
27
+
28
+ def send(message, timeout)
29
+ debug('Client ? sending message to ?: ?', @client.client_id, @endpoint, message)
30
+
31
+ return request([message], timeout) unless batching?
32
+
33
+ @outbox << message
34
+ @timeout = timeout
35
+
36
+ return flush if message['channel'] == Channel::HANDSHAKE
37
+
38
+ if message['channel'] == Channel::CONNECT
39
+ @connection_message = message
40
+ end
41
+
42
+ add_timeout(:publish, Engine::MAX_DELAY) { flush }
43
+ end
44
+
45
+ def flush
46
+ remove_timeout(:publish)
47
+
48
+ if @outbox.size > 1 and @connection_message
49
+ @connection_message['advice'] = {'timeout' => 0}
50
+ end
51
+
52
+ request(@outbox, @timeout)
53
+
54
+ @connection_message = nil
55
+ @outbox = []
56
+ end
57
+
58
+ def receive(responses)
59
+ debug('Client ? received from ?: ?', @client.client_id, @endpoint, responses)
60
+ responses.each { |response| @client.receive_message(response) }
61
+ end
62
+
63
+ def retry_block(message, timeout)
64
+ lambda do
65
+ EventMachine.add_timer(@client.retry) { request(message, timeout) }
66
+ end
67
+ end
68
+
69
+ @transports = []
70
+
71
+ class << self
72
+ attr_accessor :connection_type
73
+
74
+ def get(client, connection_types = nil, &callback)
75
+ endpoint = client.endpoint
76
+ connection_types ||= supported_connection_types
77
+
78
+ select = lambda do |(conn_type, klass), resume|
79
+ if connection_types.include?(conn_type)
80
+ klass.usable?(endpoint) do |is_usable|
81
+ if is_usable
82
+ callback.call(klass.new(client, endpoint))
83
+ else
84
+ resume.call
85
+ end
86
+ end
87
+ else
88
+ resume.call
89
+ end
90
+ end
91
+
92
+ error = lambda do
93
+ raise "Could not find a usable connection type for #{ endpoint }"
94
+ end
95
+
96
+ Faye.async_each(@transports, select, error)
97
+ end
98
+
99
+ def register(type, klass)
100
+ @transports << [type, klass]
101
+ klass.connection_type = type
102
+ end
103
+
104
+ def supported_connection_types
105
+ @transports.map { |t| t.first }
106
+ end
107
+ end
108
+
109
+ %w[local web_socket http].each do |type|
110
+ require File.join(ROOT, 'faye', 'transport', type)
111
+ end
112
+
113
+ end
114
+ end
115
+
@@ -0,0 +1,99 @@
1
+ module Faye
2
+
3
+ class Transport::WebSocket < Transport
4
+ WEBSOCKET_TIMEOUT = 1
5
+
6
+ UNCONNECTED = 1
7
+ CONNECTING = 2
8
+ CONNECTED = 3
9
+
10
+ include EventMachine::Deferrable
11
+
12
+ def self.usable?(endpoint, &callback)
13
+ connected = false
14
+ called = false
15
+ socket_url = endpoint.gsub(/^http(s?):/, 'ws\1:')
16
+ socket = Faye::WebSocket::Client.new(socket_url)
17
+
18
+ socket.onopen = lambda do |event|
19
+ connected = true
20
+ socket.close
21
+ callback.call(true)
22
+ called = true
23
+ socket = nil
24
+ end
25
+
26
+ notconnected = lambda do |*args|
27
+ callback.call(false) unless called or connected
28
+ called = true
29
+ end
30
+
31
+ socket.onclose = socket.onerror = notconnected
32
+ EventMachine.add_timer(WEBSOCKET_TIMEOUT, &notconnected)
33
+ end
34
+
35
+ def batching?
36
+ false
37
+ end
38
+
39
+ def request(messages, timeout = nil)
40
+ return if messages.empty?
41
+ @messages ||= {}
42
+ messages.each { |message| @messages[message['id']] = message }
43
+ with_socket { |socket| socket.send(Faye.to_json(messages)) }
44
+ end
45
+
46
+ def with_socket(&resume)
47
+ callback(&resume)
48
+ connect
49
+ end
50
+
51
+ def close
52
+ return if @closed
53
+ @closed = true
54
+ @socket.close if @socket
55
+ end
56
+
57
+ def connect
58
+ return if @closed
59
+
60
+ @state ||= UNCONNECTED
61
+ return unless @state == UNCONNECTED
62
+
63
+ @state = CONNECTING
64
+
65
+ @socket = Faye::WebSocket::Client.new(@endpoint.gsub(/^http(s?):/, 'ws\1:'))
66
+
67
+ @socket.onopen = lambda do |*args|
68
+ @state = CONNECTED
69
+ set_deferred_status(:succeeded, @socket)
70
+ trigger(:up)
71
+ end
72
+
73
+ @socket.onmessage = lambda do |event|
74
+ messages = [Yajl::Parser.parse(event.data)].flatten
75
+ messages.each { |message| @messages.delete(message['id']) }
76
+ receive(messages)
77
+ end
78
+
79
+ @socket.onclose = lambda do |*args|
80
+ was_connected = (@state == CONNECTED)
81
+ set_deferred_status(:deferred)
82
+ @state = UNCONNECTED
83
+ @socket = nil
84
+
85
+ next resend if was_connected
86
+
87
+ EventMachine.add_timer(@client.retry) { connect }
88
+ trigger(:down)
89
+ end
90
+ end
91
+
92
+ def resend
93
+ request(@messages.values)
94
+ end
95
+ end
96
+
97
+ Transport.register 'websocket', Transport::WebSocket
98
+
99
+ end
@@ -0,0 +1,20 @@
1
+ module Faye
2
+ class Namespace
3
+
4
+ extend Forwardable
5
+ def_delegator :@used, :delete, :release
6
+ def_delegator :@used, :has_key?, :exists?
7
+
8
+ def initialize
9
+ @used = {}
10
+ end
11
+
12
+ def generate
13
+ name = Engine.random
14
+ name = Engine.random while @used.has_key?(name)
15
+ @used[name] = name
16
+ end
17
+
18
+ end
19
+ end
20
+
data/spec/browser.html ADDED
@@ -0,0 +1,45 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
5
+ <title>Faye test suite</title>
6
+ <script type="text/javascript" src="../node_modules/jsclass/min/loader.js"></script>
7
+ </head>
8
+ <body>
9
+ <script type="text/javascript">
10
+
11
+ if (typeof TestSwarm === 'undefined')
12
+ TestSwarm = {
13
+ submit: function(result) {
14
+ if (window.console) console.log(Faye.toJSON(result));
15
+ },
16
+ heartbeat: function() {}
17
+ }
18
+
19
+ JS.cacheBust = true
20
+
21
+ JS.Packages(function() { with(this) {
22
+ file('../build/faye-browser-min.js').provides('Faye')
23
+ autoload(/.*Spec/, {from: './javascript'})
24
+ }})
25
+
26
+ JS.require('Faye', 'JS.Test', 'JS.Range', function() {
27
+ JS.Test.Unit.Assertions.include({
28
+ assertYield: function(expected) {
29
+ var testcase = this
30
+ return function(actual) { testcase.assertEqual(expected, actual) }
31
+ }
32
+ })
33
+
34
+ JS.require( 'FayeSpec',
35
+ 'GrammarSpec',
36
+ 'ChannelSpec',
37
+ 'ClientSpec',
38
+ 'TransportSpec',
39
+ JS.Test.method('autorun'))
40
+ })
41
+
42
+ </script>
43
+ </body>
44
+ </html>
45
+
@@ -0,0 +1,7 @@
1
+ module EncodingHelper
2
+ def encode(string)
3
+ return string unless string.respond_to?(:force_encoding)
4
+ string.force_encoding("UTF-8")
5
+ end
6
+ end
7
+
data/spec/install.sh ADDED
@@ -0,0 +1,78 @@
1
+ # This script installs all the necessary software to run the Ruby and
2
+ # Node versions of Faye, as well as the load testing tools AB and Tsung.
3
+
4
+ # Tested on Ubuntu 10.04 LTS 64-bit EC2 image:
5
+ # http://uec-images.ubuntu.com/releases/10.04/release/
6
+
7
+ FAYE_BRANCH=master
8
+ NODE_VERSION=0.4.10
9
+ PHANTOM_VERSION=1.2
10
+ REDIS_VERSION=2.2.12
11
+ RUBY_VERSION=1.9.2
12
+ TSUNG_VERSION=1.3.3
13
+
14
+ sudo apt-get update
15
+ sudo apt-get install build-essential g++ git-core curl wget \
16
+ openssl libcurl4-openssl-dev libreadline-dev \
17
+ apache2-utils erlang gnuplot \
18
+ libqt4-dev qt4-qmake xvfb
19
+
20
+ # Install RVM and Ruby
21
+ bash < <(curl -s https://rvm.beginrescueend.com/install/rvm)
22
+ echo "source \"\$HOME/.rvm/scripts/rvm\"" | tee -a ~/.bashrc
23
+ source ~/.rvm/scripts/rvm
24
+ rvm install $RUBY_VERSION
25
+ rvm --default use $RUBY_VERSION
26
+ echo "install: --no-rdoc --no-ri
27
+ update: --no-rdoc --no-ri" | tee ~/.gemrc
28
+ gem install rake bundler
29
+
30
+ # Install nvm and Node
31
+ cd ~
32
+ git clone git://github.com/creationix/nvm.git ~/.nvm
33
+ . ~/.nvm/nvm.sh
34
+ echo ". ~/.nvm/nvm.sh" | tee -a ~/.bashrc
35
+ nvm install v$NODE_VERSION
36
+ nvm use v$NODE_VERSION
37
+
38
+ # Install Redis from source
39
+ cd /usr/src
40
+ sudo wget http://redis.googlecode.com/files/redis-$REDIS_VERSION.tar.gz
41
+ sudo tar zxvf redis-$REDIS_VERSION.tar.gz
42
+ cd redis-$REDIS_VERSION
43
+ sudo make
44
+ sudo ln -s /usr/src/redis-$REDIS_VERSION/src/redis-server /usr/bin/redis-server
45
+ sudo ln -s /usr/src/redis-$REDIS_VERSION/src/redis-cli /usr/bin/redis-cli
46
+
47
+ # Install PhantomJS
48
+ cd /usr/src
49
+ sudo git clone git://github.com/ariya/phantomjs.git
50
+ cd phantomjs
51
+ sudo git checkout $PHANTOM_VERSION
52
+ sudo qmake-qt4
53
+ sudo make
54
+ sudo ln -s /usr/src/phantomjs/bin/phantomjs /usr/bin/phantomjs
55
+ echo "To use phantomjs, run DISPLAY=:1 Xvfb :1 -screen 0 1024x768x16"
56
+
57
+ # Install Tsung and required Perl modules
58
+ cd /usr/src
59
+ sudo wget http://tsung.erlang-projects.org/dist/tsung-$TSUNG_VERSION.tar.gz
60
+ sudo tar zxvf tsung-$TSUNG_VERSION.tar.gz
61
+ cd tsung-$TSUNG_VERSION
62
+ sudo ./configure
63
+ sudo make
64
+ sudo make install
65
+ sudo ln -s /usr/lib/tsung/bin/tsung_stats.pl /usr/bin/tsung-stats
66
+ echo "To use tsung-stats you need to 'install Template' from CPAN"
67
+ sudo perl -MCPAN -eshell
68
+
69
+ # Check out and build Faye project
70
+ cd ~
71
+ git clone git://github.com/faye/faye.git
72
+ cd faye
73
+ git checkout $FAYE_BRANCH
74
+ git submodule update --init --recursive
75
+ bundle install
76
+ npm install redis
77
+ cd vendor/js.class && jake
78
+ cd ../.. && jake
@@ -0,0 +1,15 @@
1
+ JS.ENV.ChannelSpec = JS.Test.describe("Channel", function() { with(this) {
2
+ describe("expand", function() { with(this) {
3
+ it("returns all patterns that match a channel", function() { with(this) {
4
+
5
+ assertEqual( ["/**", "/foo", "/*"],
6
+ Faye.Channel.expand("/foo") )
7
+
8
+ assertEqual( ["/**", "/foo/bar", "/foo/*", "/foo/**"],
9
+ Faye.Channel.expand("/foo/bar") )
10
+
11
+ assertEqual( ["/**", "/foo/bar/qux", "/foo/bar/*", "/foo/**", "/foo/bar/**"],
12
+ Faye.Channel.expand("/foo/bar/qux") )
13
+ }})
14
+ }})
15
+ }})
@@ -0,0 +1,714 @@
1
+ JS.ENV.ClientSpec = JS.Test.describe("Client", function() { with(this) {
2
+ before(function() { with(this) {
3
+ this.transport = {connectionType: "fake", send: function() {}}
4
+ Faye.extend(transport, Faye.Publisher)
5
+ stub(Faye.Transport, "get").yields([transport])
6
+ }})
7
+
8
+ before(function() { with(this) {
9
+ stub("setTimeout")
10
+ }})
11
+
12
+ define("stubResponse", function(response) { with(this) {
13
+ stub(transport, "send", function(message) {
14
+ response.id = message.id
15
+ client.receiveMessage(response)
16
+ })
17
+ }})
18
+
19
+ define("createClient", function() { with(this) {
20
+ this.client = new Faye.Client("http://localhost/")
21
+ }})
22
+
23
+ define("createConnectedClient", function() { with(this) {
24
+ createClient()
25
+ stubResponse({channel: "/meta/handshake",
26
+ successful: true,
27
+ version: "1.0",
28
+ supportedConnectionTypes: ["websocket"],
29
+ clientId: "fakeid" })
30
+
31
+ client.handshake()
32
+ }})
33
+
34
+ define("subscribe", function(client, channel, callback) { with(this) {
35
+ stubResponse({channel: "/meta/subscribe",
36
+ successful: true,
37
+ clientId: "fakeid",
38
+ subscription: channel })
39
+
40
+ this.subsCalled = 0
41
+ callback = callback || function() { subsCalled += 1 }
42
+ client.subscribe(channel, callback)
43
+ }})
44
+
45
+ describe("initialize", function() { with(this) {
46
+ it("creates a transport the server must support", function() { with(this) {
47
+ expect(Faye.Transport, "get").given(instanceOf(Faye.Client),
48
+ ["long-polling", "callback-polling", "in-process"])
49
+ .yielding([transport])
50
+ new Faye.Client("http://localhost/")
51
+ }})
52
+
53
+ it("puts the client in the UNCONNECTED state", function() { with(this) {
54
+ stub(Faye.Transport, "get")
55
+ var client = new Faye.Client("http://localhost/")
56
+ assertEqual( "UNCONNECTED", client.getState() )
57
+ }})
58
+ }})
59
+
60
+ describe("handshake", function() { with(this) {
61
+ before(function() { this.createClient() })
62
+
63
+ it("sends a handshake message to the server", function() { with(this) {
64
+ expect(transport, "send").given({
65
+ channel: "/meta/handshake",
66
+ version: "1.0",
67
+ supportedConnectionTypes: ["fake"],
68
+ id: instanceOf("string")
69
+ }, 60)
70
+ client.handshake()
71
+ }})
72
+
73
+ it("puts the client in the CONNECTING state", function() { with(this) {
74
+ stub(transport, "send")
75
+ client.handshake()
76
+ assertEqual( "CONNECTING", client.getState() )
77
+ }})
78
+
79
+ describe("with an outgoing extension installed", function() { with(this) {
80
+ before(function() { with(this) {
81
+ var extension = {
82
+ outgoing: function(message, callback) {
83
+ message.ext = {auth: "password"}
84
+ callback(message)
85
+ }
86
+ }
87
+ client.addExtension(extension)
88
+ }})
89
+
90
+ it("passes the handshake message through the extension", function() { with(this) {
91
+ expect(transport, "send").given({
92
+ channel: "/meta/handshake",
93
+ version: "1.0",
94
+ supportedConnectionTypes: ["fake"],
95
+ id: instanceOf("string"),
96
+ ext: {auth: "password"}
97
+ }, 60)
98
+ client.handshake()
99
+ }})
100
+ }})
101
+
102
+ describe("on successful response", function() { with(this) {
103
+ before(function() { with(this) {
104
+ stubResponse({channel: "/meta/handshake",
105
+ successful: true,
106
+ version: "1.0",
107
+ supportedConnectionTypes: ["long-polling", "websocket"],
108
+ clientId: "fakeid" })
109
+ }})
110
+
111
+ it("stores the clientId", function() { with(this) {
112
+ client.handshake()
113
+ assertEqual( "fakeid", client.getClientId() )
114
+ }})
115
+
116
+ it("puts the client in the CONNECTED state", function() { with(this) {
117
+ client.handshake()
118
+ assertEqual( "CONNECTED", client.getState() )
119
+ }})
120
+
121
+ it("registers any pre-existing subscriptions", function() { with(this) {
122
+ expect(client, "subscribe").given([], true)
123
+ client.handshake()
124
+ }})
125
+
126
+ it("selects a new transport based on what the server supports", function() { with(this) {
127
+ expect(Faye.Transport, "get").given(instanceOf(Faye.Client), ["long-polling", "websocket"])
128
+ .yielding([transport])
129
+ client.handshake()
130
+ }})
131
+
132
+ describe("with websocket disabled", function() { with(this) {
133
+ before(function() { this.client.disable('websocket') })
134
+
135
+ it("selects a new transport, excluding websocket", function() { with(this) {
136
+ expect(Faye.Transport, "get").given(instanceOf(Faye.Client), ["long-polling"])
137
+ .yielding([transport])
138
+ client.handshake()
139
+ }})
140
+ }})
141
+ }})
142
+
143
+ describe("on unsuccessful response", function() { with(this) {
144
+ before(function() { with(this) {
145
+ stubResponse({channel: "/meta/handshake",
146
+ successful: false,
147
+ version: "1.0",
148
+ supportedConnectionTypes: ["websocket"] })
149
+ }})
150
+
151
+ it("schedules a retry", function() { with(this) {
152
+ expect("setTimeout")
153
+ client.handshake()
154
+ }})
155
+
156
+ it("puts the client in the UNCONNECTED state", function() { with(this) {
157
+ stub("setTimeout")
158
+ client.handshake()
159
+ assertEqual( "UNCONNECTED", client.getState() )
160
+ }})
161
+ }})
162
+
163
+ describe("with existing subscriptions after a server restart", function() { with(this) {
164
+ before(function() { with(this) {
165
+ createConnectedClient()
166
+
167
+ this.message = null
168
+ subscribe(client, "/messages/foo", function(m) { message = m })
169
+
170
+ client.receiveMessage({advice: {reconnect: "handshake"}})
171
+
172
+ stubResponse({channel: "/meta/handshake",
173
+ successful: true,
174
+ version: "1.0",
175
+ supportedConnectionTypes: ["websocket"],
176
+ clientId: "reconnectid",
177
+ subscription: "/messages/foo" }) // tacked on to trigger subscribe() callback
178
+ }})
179
+
180
+ it("resends the subscriptions to the server", function() { with(this) {
181
+ expect(transport, "send").given({
182
+ channel: "/meta/subscribe",
183
+ clientId: "reconnectid",
184
+ subscription: "/messages/foo",
185
+ id: instanceOf("string")
186
+ }, 60)
187
+ client.handshake()
188
+ }})
189
+
190
+ it("retains the listeners for the subscriptions", function() { with(this) {
191
+ client.handshake()
192
+ client.receiveMessage({channel: "/messages/foo", "data": "ok"})
193
+ assertEqual( "ok", message )
194
+ }})
195
+ }})
196
+
197
+ describe("with a connected client", function() { with(this) {
198
+ before(function() { this.createConnectedClient() })
199
+
200
+ it("does not send a handshake message to the server", function() { with(this) {
201
+ expect(transport, "send").given({
202
+ channel: "/meta/handshake",
203
+ version: "1.0",
204
+ supportedConnectionTypes: ["fake"],
205
+ id: instanceOf("string")
206
+ }, 60)
207
+ .exactly(0)
208
+
209
+ client.handshake()
210
+ }})
211
+ }})
212
+ }})
213
+
214
+ describe("connect", function() { with(this) {
215
+ describe("with an unconnected client", function() { with(this) {
216
+ before(function() { with(this) {
217
+ stubResponse({channel: "/meta/handshake",
218
+ successful: true,
219
+ version: "1.0",
220
+ supportedConnectionTypes: ["websocket"],
221
+ clientId: "handshakeid" })
222
+
223
+ createClient()
224
+ }})
225
+
226
+ it("handshakes before connecting", function() { with(this) {
227
+ expect(transport, "send").given({
228
+ channel: "/meta/connect",
229
+ clientId: "handshakeid",
230
+ connectionType: "fake",
231
+ id: instanceOf("string")
232
+ }, 60)
233
+ client.connect()
234
+ }})
235
+ }})
236
+
237
+ describe("with a connected client", function() { with(this) {
238
+ before(function() { this.createConnectedClient() })
239
+
240
+ it("sends a connect message to the server", function() { with(this) {
241
+ expect(transport, "send").given({
242
+ channel: "/meta/connect",
243
+ clientId: "fakeid",
244
+ connectionType: "fake",
245
+ id: instanceOf("string")
246
+ }, 60)
247
+ client.connect()
248
+ }})
249
+
250
+ it("only opens one connect request at a time", function() { with(this) {
251
+ expect(transport, "send").given({
252
+ channel: "/meta/connect",
253
+ clientId: "fakeid",
254
+ connectionType: "fake",
255
+ id: instanceOf("string")
256
+ }, 60)
257
+ .exactly(1)
258
+
259
+ client.connect()
260
+ client.connect()
261
+ }})
262
+ }})
263
+ }})
264
+
265
+ describe("disconnect", function() { with(this) {
266
+ before(function() { this.createConnectedClient() })
267
+
268
+ it("sends a disconnect message to the server", function() { with(this) {
269
+ expect(transport, "send").given({
270
+ channel: "/meta/disconnect",
271
+ clientId: "fakeid",
272
+ id: instanceOf("string")
273
+ }, 60)
274
+ client.disconnect()
275
+ }})
276
+
277
+ it("puts the client in the DISCONNECTED state", function() { with(this) {
278
+ stub(transport, "close")
279
+ client.disconnect()
280
+ assertEqual( "DISCONNECTED", client.getState() )
281
+ }})
282
+
283
+ describe("on successful response", function() { with(this) {
284
+ before(function() { with(this) {
285
+ stubResponse({channel: "/meta/disconnect",
286
+ successful: true,
287
+ clientId: "fakeid" })
288
+ }})
289
+
290
+ it("closes the transport", function() { with(this) {
291
+ expect(transport, "close")
292
+ client.disconnect()
293
+ }})
294
+ }})
295
+ }})
296
+
297
+ describe("subscribe", function() { with(this) {
298
+ before(function() { with(this) {
299
+ createConnectedClient()
300
+ this.subscribeMessage = {
301
+ channel: "/meta/subscribe",
302
+ clientId: "fakeid",
303
+ subscription: "/foo",
304
+ id: instanceOf("string")
305
+ }
306
+ }})
307
+
308
+ describe("with no prior subscriptions", function() { with(this) {
309
+ it("sends a subscribe message to the server", function() { with(this) {
310
+ expect(transport, "send").given(subscribeMessage, 60)
311
+ client.subscribe("/foo")
312
+ }})
313
+
314
+ // The Bayeux spec says the server should accept a list of subscriptions
315
+ // in one message but the cometD server doesn't actually support this
316
+ it("sends multiple subscribe messages if given an array", function() { with(this) {
317
+ expect(transport, "send").given({
318
+ channel: "/meta/subscribe",
319
+ clientId: "fakeid",
320
+ subscription: "/foo",
321
+ id: instanceOf("string")
322
+ }, 60)
323
+ expect(transport, "send").given({
324
+ channel: "/meta/subscribe",
325
+ clientId: "fakeid",
326
+ subscription: "/bar",
327
+ id: instanceOf("string")
328
+ }, 60)
329
+ client.subscribe(["/foo", "/bar"])
330
+ }})
331
+
332
+ describe("on successful response", function() { with(this) {
333
+ before(function() { with(this) {
334
+ stubResponse({channel: "/meta/subscribe",
335
+ successful: true,
336
+ clientId: "fakeid",
337
+ subscription: "/foo/*" })
338
+ }})
339
+
340
+ it("sets up a listener for the subscribed channel", function() { with(this) {
341
+ var message
342
+ client.subscribe("/foo/*", function(m) { message = m })
343
+ client.receiveMessage({channel: "/foo/bar", data: "hi"})
344
+ assertEqual( "hi", message )
345
+ }})
346
+
347
+ it("does not call the listener for non-matching channels", function() { with(this) {
348
+ var message
349
+ client.subscribe("/foo/*", function(m) { message = m })
350
+ client.receiveMessage({channel: "/bar", data: "hi"})
351
+ assertEqual( undefined, message )
352
+ }})
353
+
354
+ it("activates the subscription", function() { with(this) {
355
+ var active = false
356
+ client.subscribe("/foo/*").callback(function() { active = true })
357
+ assert( active )
358
+ }})
359
+
360
+ describe("with an incoming extension installed", function() { with(this) {
361
+ before(function() { with(this) {
362
+ var extension = {
363
+ incoming: function(message, callback) {
364
+ if (message.data) message.data.changed = true
365
+ callback(message)
366
+ }
367
+ }
368
+ client.addExtension(extension)
369
+ this.message = null
370
+ client.subscribe("/foo/*", function(m) { message = m })
371
+ }})
372
+
373
+ it("passes delivered messages through the extension", function() { with(this) {
374
+ client.receiveMessage({channel: "/foo/bar", data: {hello: "there"}})
375
+ assertEqual( {hello: "there", changed: true}, message )
376
+ }})
377
+ }})
378
+
379
+ describe("with an outgoing extension installed", function() { with(this) {
380
+ before(function() { with(this) {
381
+ var extension = {
382
+ outgoing: function(message, callback) {
383
+ if (message.data) message.data.changed = true
384
+ callback(message)
385
+ }
386
+ }
387
+ client.addExtension(extension)
388
+ this.message = null
389
+ client.subscribe("/foo/*", function(m) { message = m })
390
+ }})
391
+
392
+ it("leaves messages unchanged", function() { with(this) {
393
+ client.receiveMessage({channel: "/foo/bar", data: {hello: "there"}})
394
+ assertEqual( {hello: "there"}, message )
395
+ }})
396
+ }})
397
+
398
+ describe("with an incoming extension that invalidates the response", function() { with(this) {
399
+ before(function() { with(this) {
400
+ var extension = {
401
+ incoming: function(message, callback) {
402
+ if (message.channel === "/meta/subscribe") message.successful = false
403
+ callback(message)
404
+ }
405
+ }
406
+ client.addExtension(extension)
407
+ }})
408
+
409
+ it("does not set up a listener for the subscribed channel", function() { with(this) {
410
+ var message
411
+ client.subscribe("/foo/*", function(m) { message = m })
412
+ client.receiveMessage({channel: "/foo/bar", data: "hi"})
413
+ assertEqual( undefined, message )
414
+ }})
415
+
416
+ it("does not activate the subscription", function() { with(this) {
417
+ var active = false
418
+ client.subscribe("/foo/*").callback(function() { active = true })
419
+ assert( !active )
420
+ }})
421
+ }})
422
+ }})
423
+
424
+ describe("on unsuccessful response", function() { with(this) {
425
+ before(function() { with(this) {
426
+ stubResponse({channel: "/meta/subscribe",
427
+ successful: false,
428
+ error: "403:/meta/foo:Forbidden channel",
429
+ clientId: "fakeid",
430
+ subscription: "/meta/foo" })
431
+ }})
432
+
433
+ it("does not set up a listener for the subscribed channel", function() { with(this) {
434
+ var message
435
+ client.subscribe("/meta/foo", function(m) { message = m })
436
+ client.receiveMessage({channel: "/meta/foo", data: "hi"})
437
+ assertEqual( undefined, message )
438
+ }})
439
+
440
+ it("does not activate the subscription", function() { with(this) {
441
+ var active = false
442
+ client.subscribe("/meta/foo").callback(function() { active = true })
443
+ assert( !active )
444
+ }})
445
+
446
+ it("reports the error through an errback", function() { with(this) {
447
+ var error = null
448
+ client.subscribe("/meta/foo").errback(function(e) { error = e })
449
+ assertEqual( objectIncluding({code: 403, params: ["/meta/foo"], message: "Forbidden channel"}), error )
450
+ }})
451
+ }})
452
+ }})
453
+
454
+ describe("with an existing subscription", function() { with(this) {
455
+ before(function() { with(this) {
456
+ subscribe(client, "/foo/*")
457
+ }})
458
+
459
+ it("does not send another subscribe message to the server", function() { with(this) {
460
+ expect(transport, "send").given(subscribeMessage, 60).exactly(0)
461
+ client.subscribe("/foo/*")
462
+ }})
463
+
464
+ it("sets up another listener on the channel", function() { with(this) {
465
+ client.subscribe("/foo/*", function() { subsCalled += 1 })
466
+ client.receiveMessage({channel: "/foo/bar", data: "hi"})
467
+ assertEqual( 2, subsCalled )
468
+ }})
469
+
470
+ it("activates the subscription", function() { with(this) {
471
+ var active = false
472
+ client.subscribe("/foo/*").callback(function() { active = true })
473
+ assert( active )
474
+ }})
475
+ }})
476
+ }})
477
+
478
+ describe("unsubscribe", function() { with(this) {
479
+ before(function() { with(this) {
480
+ createConnectedClient()
481
+ this.unsubscribeMessage = {
482
+ channel: "/meta/unsubscribe",
483
+ clientId: "fakeid",
484
+ subscription: "/foo/*",
485
+ id: instanceOf("string")
486
+ }
487
+ }})
488
+
489
+ describe("with no subscriptions", function() { with(this) {
490
+ it("does not send an unsubscribe message to the server", function() { with(this) {
491
+ expect(transport, "send").given(unsubscribeMessage, 60).exactly(0)
492
+ client.unsubscribe("/foo/*")
493
+ }})
494
+ }})
495
+
496
+ describe("with a single subscription", function() { with(this) {
497
+ before(function() { with(this) {
498
+ this.message = null
499
+ this.listener = function(m) { message = m }
500
+ subscribe(client, "/foo/*", listener)
501
+ }})
502
+
503
+ it("sends an unsubscribe message to the server", function() { with(this) {
504
+ expect(transport, "send").given(unsubscribeMessage, 60)
505
+ client.unsubscribe("/foo/*")
506
+ }})
507
+
508
+ it("removes the listener from the channel", function() { with(this) {
509
+ client.receiveMessage({channel: "/foo/bar", data: "first"})
510
+ client.unsubscribe("/foo/*", listener)
511
+ client.receiveMessage({channel: "/foo/bar", data: "second"})
512
+ assertEqual( "first", message )
513
+ }})
514
+ }})
515
+
516
+ describe("with multiple subscriptions to the same channel", function() { with(this) {
517
+ before(function() { with(this) {
518
+ this.messages = []
519
+ this.hey = function(m) { messages.push("hey " + m.text) }
520
+ this.bye = function(m) { messages.push("bye " + m.text) }
521
+ subscribe(client, "/foo/*", hey)
522
+ subscribe(client, "/foo/*", bye)
523
+ }})
524
+
525
+ it("removes one of the listeners from the channel", function() { with(this) {
526
+ client.receiveMessage({channel: "/foo/bar", data: {text: "you"}})
527
+ client.unsubscribe("/foo/*", hey)
528
+ client.receiveMessage({channel: "/foo/bar", data: {text: "you"}})
529
+ assertEqual( ["hey you", "bye you", "bye you"], messages)
530
+ }})
531
+
532
+ it("does not send an unsubscribe message if one listener is removed", function() { with(this) {
533
+ expect(transport, "send").given(unsubscribeMessage, 60).exactly(0)
534
+ client.unsubscribe("/foo/*", bye)
535
+ }})
536
+
537
+ it("sends an unsubscribe message if each listener is removed", function() { with(this) {
538
+ expect(transport, "send").given(unsubscribeMessage, 60)
539
+ client.unsubscribe("/foo/*", bye)
540
+ client.unsubscribe("/foo/*", hey)
541
+ }})
542
+
543
+ it("sends an unsubscribe message if all listeners are removed", function() { with(this) {
544
+ expect(transport, "send").given(unsubscribeMessage, 60)
545
+ client.unsubscribe("/foo/*")
546
+ }})
547
+ }})
548
+
549
+ describe("with multiple subscriptions to different channels", function() { with(this) {
550
+ before(function() { with(this) {
551
+ subscribe(client, "/foo")
552
+ subscribe(client, "/bar")
553
+ }})
554
+
555
+ it("sends multiple unsubscribe messages if given an array", function() { with(this) {
556
+ expect(transport, "send").given({
557
+ channel: "/meta/unsubscribe",
558
+ clientId: "fakeid",
559
+ subscription: "/foo",
560
+ id: instanceOf("string")
561
+ }, 60)
562
+ expect(transport, "send").given({
563
+ channel: "/meta/unsubscribe",
564
+ clientId: "fakeid",
565
+ subscription: "/bar",
566
+ id: instanceOf("string")
567
+ }, 60)
568
+ client.unsubscribe(["/foo", "/bar"])
569
+ }})
570
+ }})
571
+ }})
572
+
573
+ describe("publish", function() { with(this) {
574
+ before(function() { this.createConnectedClient() })
575
+
576
+ it("sends the message to the server with an ID", function() { with(this) {
577
+ expect(transport, "send").given({
578
+ channel: "/messages/foo",
579
+ clientId: "fakeid",
580
+ data: {hello: "world"},
581
+ id: instanceOf("string")
582
+ }, 60)
583
+ client.publish("/messages/foo", {hello: "world"})
584
+ }})
585
+
586
+ describe("on publish failure", function() { with(this) {
587
+ before(function() { with(this) {
588
+ stubResponse({channel: "/messages/foo",
589
+ error: "407:/messages/foo:Failed to publish",
590
+ successful: false,
591
+ clientId: "fakeid" })
592
+ }})
593
+
594
+ it("should not be published", function() { with(this) {
595
+ var published = false
596
+ client.publish("/messages/foo", {text: "hi"}).callback(function() { published = true })
597
+ assert( !published )
598
+ }})
599
+
600
+ it("reports the error through an errback", function() { with(this) {
601
+ var error = null
602
+ client.publish("/messages/foo", {text: "hi"}).errback(function(e) { error = e })
603
+ assertEqual( 407, error.code )
604
+ assertEqual( ["/messages/foo"], error.params )
605
+ assertEqual( "Failed to publish", error.message )
606
+ }})
607
+ }})
608
+
609
+ describe("on receipt of the published message", function() { with(this) {
610
+ before(function() { with(this) {
611
+ stubResponse({channel: "/messages/foo",
612
+ data: {text: "hi"},
613
+ clientId: "fakeid" })
614
+ }})
615
+
616
+ it("does not trigger the callbacks", function() { with(this) {
617
+ var published = false
618
+ var publication = client.publish("/messages/foo", {text: "hi"})
619
+ publication.callback(function() { published = true })
620
+ publication.errback(function() { published = true })
621
+ assert( !published )
622
+ }})
623
+ }})
624
+
625
+ describe("with an outgoing extension installed", function() { with(this) {
626
+ before(function() { with(this) {
627
+ var extension = {
628
+ outgoing: function(message, callback) {
629
+ message.ext = {auth: "password"}
630
+ callback(message)
631
+ }
632
+ }
633
+ client.addExtension(extension)
634
+ }})
635
+
636
+ it("passes messages through the extension", function() { with(this) {
637
+ expect(transport, "send").given({
638
+ channel: "/messages/foo",
639
+ clientId: "fakeid",
640
+ data: {hello: "world"},
641
+ id: instanceOf("string"),
642
+ ext: {auth: "password"}
643
+ }, 60)
644
+ client.publish("/messages/foo", {hello: "world"})
645
+ }})
646
+ }})
647
+
648
+ describe("with an incoming extension installed", function() { with(this) {
649
+ before(function() { with(this) {
650
+ var extension = {
651
+ incoming: function(message, callback) {
652
+ message.ext = {auth: "password"}
653
+ callback(message)
654
+ }
655
+ }
656
+ client.addExtension(extension)
657
+ }})
658
+
659
+ it("leaves the message unchanged", function() { with(this) {
660
+ expect(transport, "send").given({
661
+ channel: "/messages/foo",
662
+ clientId: "fakeid",
663
+ data: {hello: "world"},
664
+ id: instanceOf("string")
665
+ }, 60)
666
+ client.publish("/messages/foo", {hello: "world"})
667
+ }})
668
+ }})
669
+ }})
670
+
671
+ describe("network notifications", function() { with(this) {
672
+ before(function() { this.createClient() })
673
+
674
+ describe("in the default state", function() { with(this) {
675
+ it("broadcasts a down notification", function() { with(this) {
676
+ expect(client, "trigger").given("transport:down")
677
+ transport.trigger("down")
678
+ }})
679
+
680
+ it("broadcasts an up notification", function() { with(this) {
681
+ expect(client, "trigger").given("transport:up")
682
+ transport.trigger("up")
683
+ }})
684
+ }})
685
+
686
+ describe("when the transport is up", function() { with(this) {
687
+ before(function() { this.transport.trigger("up") })
688
+
689
+ it("broadcasts a down notification", function() { with(this) {
690
+ expect(client, "trigger").given("transport:down")
691
+ transport.trigger("down")
692
+ }})
693
+
694
+ it("does not broadcast an up notification", function() { with(this) {
695
+ expect(client, "trigger").exactly(0)
696
+ transport.trigger("up")
697
+ }})
698
+ }})
699
+
700
+ describe("when the transport is down", function() { with(this) {
701
+ before(function() { this.transport.trigger("down") })
702
+
703
+ it("does not broadcast a down notification", function() { with(this) {
704
+ expect(client, "trigger").exactly(0)
705
+ transport.trigger("down")
706
+ }})
707
+
708
+ it("broadcasts an up notification", function() { with(this) {
709
+ expect(client, "trigger").given("transport:up")
710
+ transport.trigger("up")
711
+ }})
712
+ }})
713
+ }})
714
+ }})