face-faye 0.8.9

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