face-faye 0.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }})