websocket-rails 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +32 -0
  3. data/Gemfile +2 -1
  4. data/README.md +29 -34
  5. data/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee +45 -0
  6. data/lib/assets/javascripts/websocket_rails/channel.js.coffee +34 -17
  7. data/lib/assets/javascripts/websocket_rails/event.js.coffee +13 -11
  8. data/lib/assets/javascripts/websocket_rails/http_connection.js.coffee +44 -45
  9. data/lib/assets/javascripts/websocket_rails/main.js +1 -0
  10. data/lib/assets/javascripts/websocket_rails/websocket_connection.js.coffee +20 -34
  11. data/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee +60 -15
  12. data/lib/generators/websocket_rails/install/templates/websocket_rails.rb +15 -0
  13. data/lib/rails/config/routes.rb +1 -1
  14. data/lib/rails/tasks/websocket_rails.tasks +6 -2
  15. data/lib/websocket_rails/channel.rb +28 -2
  16. data/lib/websocket_rails/channel_manager.rb +16 -0
  17. data/lib/websocket_rails/configuration.rb +26 -1
  18. data/lib/websocket_rails/connection_adapters/http.rb +7 -0
  19. data/lib/websocket_rails/connection_adapters/web_socket.rb +3 -1
  20. data/lib/websocket_rails/connection_manager.rb +1 -1
  21. data/lib/websocket_rails/controller_factory.rb +1 -1
  22. data/lib/websocket_rails/event.rb +9 -2
  23. data/lib/websocket_rails/logging.rb +0 -1
  24. data/lib/websocket_rails/synchronization.rb +11 -7
  25. data/lib/websocket_rails/version.rb +1 -1
  26. data/spec/javascripts/generated/assets/abstract_connection.js +71 -0
  27. data/spec/javascripts/generated/assets/channel.js +58 -34
  28. data/spec/javascripts/generated/assets/event.js +12 -16
  29. data/spec/javascripts/generated/assets/http_connection.js +67 -65
  30. data/spec/javascripts/generated/assets/websocket_connection.js +36 -51
  31. data/spec/javascripts/generated/assets/websocket_rails.js +68 -21
  32. data/spec/javascripts/generated/specs/channel_spec.js +102 -19
  33. data/spec/javascripts/generated/specs/helpers.js +17 -0
  34. data/spec/javascripts/generated/specs/websocket_connection_spec.js +72 -19
  35. data/spec/javascripts/generated/specs/websocket_rails_spec.js +146 -47
  36. data/spec/javascripts/support/jasmine.yml +10 -2
  37. data/spec/javascripts/support/jasmine_helper.rb +38 -0
  38. data/spec/javascripts/websocket_rails/channel_spec.coffee +66 -12
  39. data/spec/javascripts/websocket_rails/event_spec.coffee +7 -7
  40. data/spec/javascripts/websocket_rails/helpers.coffee +6 -0
  41. data/spec/javascripts/websocket_rails/websocket_connection_spec.coffee +53 -15
  42. data/spec/javascripts/websocket_rails/websocket_rails_spec.coffee +108 -25
  43. data/spec/unit/base_controller_spec.rb +41 -0
  44. data/spec/unit/channel_manager_spec.rb +21 -0
  45. data/spec/unit/channel_spec.rb +43 -3
  46. data/spec/unit/connection_adapters/http_spec.rb +24 -3
  47. data/spec/unit/connection_adapters_spec.rb +2 -2
  48. data/spec/unit/connection_manager_spec.rb +1 -1
  49. data/spec/unit/event_spec.rb +25 -1
  50. data/spec/unit/logging_spec.rb +1 -1
  51. metadata +57 -67
  52. data/spec/javascripts/support/jasmine_config.rb +0 -63
@@ -1,8 +1,13 @@
1
1
  (function() {
2
2
  describe('WebsocketRails.WebSocketConnection:', function() {
3
+ var SAMPLE_EVENT, SAMPLE_EVENT_DATA;
4
+ SAMPLE_EVENT_DATA = ['event', 'message'];
5
+ SAMPLE_EVENT = {
6
+ data: JSON.stringify(SAMPLE_EVENT_DATA)
7
+ };
3
8
  beforeEach(function() {
4
- var dispatcher;
5
- dispatcher = {
9
+ var WebSocketStub;
10
+ this.dispatcher = {
6
11
  new_message: function() {
7
12
  return true;
8
13
  },
@@ -11,21 +16,40 @@
11
16
  },
12
17
  state: 'connected'
13
18
  };
14
- window.WebSocket = function(url) {
15
- this.url = url;
16
- return this.send = function() {
19
+ window.WebSocket = WebSocketStub = (function() {
20
+ function WebSocketStub(url, dispatcher) {
21
+ this.url = url;
22
+ this.dispatcher = dispatcher;
23
+ }
24
+
25
+ WebSocketStub.prototype.send = function() {
17
26
  return true;
18
27
  };
19
- };
20
- this.dispatcher = dispatcher;
21
- return this.connection = new WebSocketRails.WebSocketConnection('localhost:3000/websocket', dispatcher);
28
+
29
+ WebSocketStub.prototype.close = function() {
30
+ return this.onclose(null);
31
+ };
32
+
33
+ return WebSocketStub;
34
+
35
+ })();
36
+ this.connection = new WebSocketRails.WebSocketConnection('localhost:3000/websocket', this.dispatcher);
37
+ return this.dispatcher._conn = this.connection;
22
38
  });
23
39
  describe('constructor', function() {
24
- it('should set the onmessage event on the WebSocket object to this.on_message', function() {
25
- return expect(this.connection._conn.onmessage).toEqual(this.connection.on_message);
40
+ it('should redirect onmessage events\' data from the WebSocket object to this.on_message', function() {
41
+ var mock_connection;
42
+ mock_connection = sinon.mock(this.connection);
43
+ mock_connection.expects('on_message').once().withArgs(SAMPLE_EVENT_DATA);
44
+ this.connection._conn.onmessage(SAMPLE_EVENT);
45
+ return mock_connection.verify();
26
46
  });
27
- it('should set the onclose event on the WebSocket object to this.on_close', function() {
28
- return expect(this.connection._conn.onclose).toEqual(this.connection.on_close);
47
+ it('should redirect onclose events from the WebSocket object to this.on_close', function() {
48
+ var mock_connection;
49
+ mock_connection = sinon.mock(this.connection);
50
+ mock_connection.expects('on_close').once().withArgs(SAMPLE_EVENT);
51
+ this.connection._conn.onclose(SAMPLE_EVENT);
52
+ return mock_connection.verify();
29
53
  });
30
54
  describe('with ssl', function() {
31
55
  return it('should not add the ws:// prefix to the URL', function() {
@@ -40,6 +64,12 @@
40
64
  });
41
65
  });
42
66
  });
67
+ describe('.close', function() {
68
+ return it('should close the connection', function() {
69
+ this.connection.close();
70
+ return expect(this.dispatcher.state).toEqual('disconnected');
71
+ });
72
+ });
43
73
  describe('.trigger', function() {
44
74
  describe('before the connection has been fully established', function() {
45
75
  return it('should queue up the events', function() {
@@ -69,14 +99,10 @@
69
99
  });
70
100
  describe('.on_message', function() {
71
101
  return it('should decode the message and pass it to the dispatcher', function() {
72
- var encoded_data, event, mock_dispatcher;
73
- encoded_data = JSON.stringify(['event', 'message']);
74
- event = {
75
- data: encoded_data
76
- };
102
+ var mock_dispatcher;
77
103
  mock_dispatcher = sinon.mock(this.connection.dispatcher);
78
- mock_dispatcher.expects('new_message').once().withArgs(JSON.parse(encoded_data));
79
- this.connection.on_message(event);
104
+ mock_dispatcher.expects('new_message').once().withArgs(SAMPLE_EVENT_DATA);
105
+ this.connection.on_message(SAMPLE_EVENT_DATA);
80
106
  return mock_dispatcher.verify();
81
107
  });
82
108
  });
@@ -120,6 +146,33 @@
120
146
  return expect(this.dispatcher.state).toEqual('disconnected');
121
147
  });
122
148
  });
149
+ describe("it's no longer active connection", function() {
150
+ beforeEach(function() {
151
+ this.new_connection = new WebSocketRails.WebSocketConnection('localhost:3000/websocket', this.dispatcher);
152
+ return this.dispatcher._conn = this.new_connection;
153
+ });
154
+ it(".on_error should not react to the event response", function() {
155
+ var mock_dispatcher;
156
+ mock_dispatcher = sinon.mock(this.connection.dispatcher);
157
+ mock_dispatcher.expects('dispatch').never();
158
+ this.connection.on_error(SAMPLE_EVENT_DATA);
159
+ return mock_dispatcher.verify();
160
+ });
161
+ it(".on_close should not react to the event response", function() {
162
+ var mock_dispatcher;
163
+ mock_dispatcher = sinon.mock(this.connection.dispatcher);
164
+ mock_dispatcher.expects('dispatch').never();
165
+ this.connection.on_close(SAMPLE_EVENT_DATA);
166
+ return mock_dispatcher.verify();
167
+ });
168
+ return it(".on_message should not react to the event response", function() {
169
+ var mock_dispatcher;
170
+ mock_dispatcher = sinon.mock(this.connection.dispatcher);
171
+ mock_dispatcher.expects('new_message').never();
172
+ this.connection.on_message(SAMPLE_EVENT_DATA);
173
+ return mock_dispatcher.verify();
174
+ });
175
+ });
123
176
  return describe('.flush_queue', function() {
124
177
  beforeEach(function() {
125
178
  this.event = new WebSocketRails.Event(['event', 'message']);
@@ -1,26 +1,43 @@
1
1
  (function() {
2
+ var __hasProp = {}.hasOwnProperty,
3
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
4
+
2
5
  describe('WebSocketRails:', function() {
3
6
  beforeEach(function() {
7
+ var HttpConnectionStub, WebSocketConnectionStub;
4
8
  this.url = 'localhost:3000/websocket';
5
- WebSocketRails.WebSocketConnection = function() {
6
- return {
7
- connection_type: 'websocket',
8
- flush_queue: function() {
9
- return true;
10
- }
11
- };
12
- };
13
- WebSocketRails.HttpConnection = function() {
14
- return {
15
- connection_type: 'http',
16
- flush_queue: function() {
17
- return true;
18
- }
19
- };
20
- };
9
+ WebSocketRails.WebSocketConnection = WebSocketConnectionStub = (function(_super) {
10
+ __extends(WebSocketConnectionStub, _super);
11
+
12
+ function WebSocketConnectionStub() {
13
+ return WebSocketConnectionStub.__super__.constructor.apply(this, arguments);
14
+ }
15
+
16
+ WebSocketConnectionStub.prototype.connection_type = 'websocket';
17
+
18
+ return WebSocketConnectionStub;
19
+
20
+ })(WebSocketRails.AbstractConnection);
21
+ WebSocketRails.HttpConnection = HttpConnectionStub = (function(_super) {
22
+ __extends(HttpConnectionStub, _super);
23
+
24
+ function HttpConnectionStub() {
25
+ return HttpConnectionStub.__super__.constructor.apply(this, arguments);
26
+ }
27
+
28
+ HttpConnectionStub.prototype.connection_type = 'http';
29
+
30
+ return HttpConnectionStub;
31
+
32
+ })(WebSocketRails.AbstractConnection);
21
33
  return this.dispatcher = new WebSocketRails(this.url);
22
34
  });
23
35
  describe('constructor', function() {
36
+ return it('should start connection automatically', function() {
37
+ return expect(this.dispatcher.state).toEqual('connecting');
38
+ });
39
+ });
40
+ describe('.connect', function() {
24
41
  it('should set the new_message method on connection to this.new_message', function() {
25
42
  return expect(this.dispatcher._conn.new_message).toEqual(this.dispatcher.new_message);
26
43
  });
@@ -34,7 +51,7 @@
34
51
  return expect(dispatcher._conn.connection_type).toEqual('websocket');
35
52
  });
36
53
  });
37
- describe('when use_webosckets is false', function() {
54
+ describe('when use_websockets is false', function() {
38
55
  return it('should use the Http Connection', function() {
39
56
  var dispatcher;
40
57
  dispatcher = new WebSocketRails(this.url, false);
@@ -50,43 +67,119 @@
50
67
  });
51
68
  });
52
69
  });
70
+ describe('.disconnect', function() {
71
+ beforeEach(function() {
72
+ return this.dispatcher.disconnect();
73
+ });
74
+ it('should close the connection', function() {
75
+ return expect(this.dispatcher.state).toEqual('disconnected');
76
+ });
77
+ return it('existing connection should be destroyed', function() {
78
+ return expect(this.dispatcher._conn).toBeUndefined();
79
+ });
80
+ });
81
+ describe('.reconnect', function() {
82
+ var NEW_CONNECTION_ID, OLD_CONNECTION_ID;
83
+ OLD_CONNECTION_ID = 1;
84
+ NEW_CONNECTION_ID = 2;
85
+ it('should connect, when disconnected', function() {
86
+ var mock_dispatcher;
87
+ mock_dispatcher = sinon.mock(this.dispatcher);
88
+ mock_dispatcher.expects('connect').once();
89
+ this.dispatcher.disconnect();
90
+ this.dispatcher.reconnect();
91
+ return mock_dispatcher.verify();
92
+ });
93
+ it('should recreate the connection', function() {
94
+ helpers.startConnection(this.dispatcher, OLD_CONNECTION_ID);
95
+ this.dispatcher.reconnect();
96
+ helpers.startConnection(this.dispatcher, NEW_CONNECTION_ID);
97
+ return expect(this.dispatcher._conn.connection_id).toEqual(NEW_CONNECTION_ID);
98
+ });
99
+ it('should resend all uncompleted events', function() {
100
+ var event;
101
+ event = this.dispatcher.trigger('create_post');
102
+ helpers.startConnection(this.dispatcher, OLD_CONNECTION_ID);
103
+ this.dispatcher.reconnect();
104
+ helpers.startConnection(this.dispatcher, NEW_CONNECTION_ID);
105
+ return expect(this.dispatcher.queue[event.id].connection_id).toEqual(NEW_CONNECTION_ID);
106
+ });
107
+ it('should not resend completed events', function() {
108
+ var event;
109
+ event = this.dispatcher.trigger('create_post');
110
+ event.run_callbacks(true, {});
111
+ helpers.startConnection(this.dispatcher, OLD_CONNECTION_ID);
112
+ this.dispatcher.reconnect();
113
+ helpers.startConnection(this.dispatcher, NEW_CONNECTION_ID);
114
+ return expect(this.dispatcher.queue[event.id].connection_id).toEqual(OLD_CONNECTION_ID);
115
+ });
116
+ return it('should reconnect to all channels', function() {
117
+ var mock_dispatcher;
118
+ mock_dispatcher = sinon.mock(this.dispatcher);
119
+ mock_dispatcher.expects('reconnect_channels').once();
120
+ this.dispatcher.reconnect();
121
+ return mock_dispatcher.verify();
122
+ });
123
+ });
124
+ describe('.reconnect_channels', function() {
125
+ beforeEach(function() {
126
+ this.channel_callback = function() {
127
+ return true;
128
+ };
129
+ helpers.startConnection(this.dispatcher, 1);
130
+ this.dispatcher.subscribe('public 4chan');
131
+ this.dispatcher.subscribe_private('private 4chan');
132
+ return this.dispatcher.channels['public 4chan'].bind('new_post', this.channel_callback);
133
+ });
134
+ it('should recreate existing channels, keeping their private/public type', function() {
135
+ this.dispatcher.reconnect_channels();
136
+ expect(this.dispatcher.channels['public 4chan'].is_private).toEqual(false);
137
+ return expect(this.dispatcher.channels['private 4chan'].is_private).toEqual(true);
138
+ });
139
+ return it('should move all existing callbacks from old channel objects to new ones', function() {
140
+ var old_public_channel;
141
+ old_public_channel = this.dispatcher.channels['public 4chan'];
142
+ this.dispatcher.reconnect_channels();
143
+ expect(old_public_channel._callbacks).toEqual({});
144
+ return expect(this.dispatcher.channels['public 4chan']._callbacks).toEqual({
145
+ new_post: [this.channel_callback]
146
+ });
147
+ });
148
+ });
53
149
  describe('.new_message', function() {
54
150
  describe('when this.state is "connecting"', function() {
55
151
  beforeEach(function() {
56
- this.message = {
57
- data: {
58
- connection_id: 123
59
- }
60
- };
61
- return this.data = [['client_connected', this.message]];
152
+ return this.connection_id = 123;
62
153
  });
63
154
  it('should call this.connection_established on the "client_connected" event', function() {
64
155
  var mock_dispatcher;
65
156
  mock_dispatcher = sinon.mock(this.dispatcher);
66
- mock_dispatcher.expects('connection_established').once().withArgs(this.message.data);
67
- this.dispatcher.new_message(this.data);
157
+ mock_dispatcher.expects('connection_established').once().withArgs({
158
+ connection_id: this.connection_id
159
+ });
160
+ helpers.startConnection(this.dispatcher, this.connection_id);
68
161
  return mock_dispatcher.verify();
69
162
  });
70
163
  it('should set the state to connected', function() {
71
- this.dispatcher.new_message(this.data);
164
+ helpers.startConnection(this.dispatcher, this.connection_id);
72
165
  return expect(this.dispatcher.state).toEqual('connected');
73
166
  });
74
167
  it('should flush any messages queued before the connection was established', function() {
75
168
  var mock_con;
76
169
  mock_con = sinon.mock(this.dispatcher._conn);
77
170
  mock_con.expects('flush_queue').once();
78
- this.dispatcher.new_message(this.data);
171
+ helpers.startConnection(this.dispatcher, this.connection_id);
79
172
  return mock_con.verify();
80
173
  });
81
174
  it('should set the correct connection_id', function() {
82
- this.dispatcher.new_message(this.data);
83
- return expect(this.dispatcher.connection_id).toEqual(123);
175
+ helpers.startConnection(this.dispatcher, this.connection_id);
176
+ return expect(this.dispatcher._conn.connection_id).toEqual(123);
84
177
  });
85
178
  return it('should call the user defined on_open callback', function() {
86
179
  var spy;
87
180
  spy = sinon.spy();
88
181
  this.dispatcher.on_open = spy;
89
- this.dispatcher.new_message(this.data);
182
+ helpers.startConnection(this.dispatcher, this.connection_id);
90
183
  return expect(spy.calledOnce).toEqual(true);
91
184
  });
92
185
  });
@@ -122,15 +215,18 @@
122
215
  run_callbacks: function(data) {}
123
216
  };
124
217
  this.event_mock = sinon.mock(this.event);
125
- return this.dispatcher.queue[1] = this.event;
218
+ this.dispatcher.queue[1] = this.event;
219
+ return this.event_data = [['event', this.attributes]];
126
220
  });
127
- return it('should run callbacks for result events', function() {
128
- var data;
129
- data = [['event', this.attributes]];
221
+ it('should run callbacks for result events', function() {
130
222
  this.event_mock.expects('run_callbacks').once();
131
- this.dispatcher.new_message(data);
223
+ this.dispatcher.new_message(this.event_data);
132
224
  return this.event_mock.verify();
133
225
  });
226
+ return it('should remove the event from the queue', function() {
227
+ this.dispatcher.new_message(this.event_data);
228
+ return expect(this.dispatcher.queue[1]).toBeUndefined();
229
+ });
134
230
  });
135
231
  });
136
232
  });
@@ -158,23 +254,26 @@
158
254
  });
159
255
  describe('triggering events with', function() {
160
256
  beforeEach(function() {
161
- this.dispatcher.connection_id = 123;
162
257
  return this.dispatcher._conn = {
163
- trigger: function() {},
164
- trigger_channel: function() {}
258
+ connection_id: 123,
259
+ trigger: function() {}
165
260
  };
166
261
  });
167
262
  return describe('.trigger', function() {
168
- return it('should delegate to the connection object', function() {
169
- var con_trigger, event;
170
- con_trigger = sinon.spy(this.dispatcher._conn, 'trigger');
263
+ it('should add the event to the queue', function() {
264
+ var event;
265
+ event = this.dispatcher.trigger('event', 'message');
266
+ return expect(this.dispatcher.queue[event.id]).toEqual(event);
267
+ });
268
+ it('should delegate to the connection object', function() {
269
+ var conn_trigger;
270
+ conn_trigger = sinon.spy(this.dispatcher._conn, 'trigger');
171
271
  this.dispatcher.trigger('event', 'message');
172
- event = new WebSocketRails.Event([
173
- 'websocket_rails.subscribe', {
174
- channel: 'awesome'
175
- }, 123
176
- ]);
177
- return expect(con_trigger.called).toEqual(true);
272
+ return expect(conn_trigger.called).toEqual(true);
273
+ });
274
+ return it("should not delegate to the connection object, if it's not available", function() {
275
+ this.dispatcher._conn = null;
276
+ return this.dispatcher.trigger('event', 'message');
178
277
  });
179
278
  });
180
279
  });
@@ -12,11 +12,19 @@ src_dir: spec/javascripts
12
12
  src_files:
13
13
  - support/vendor/sinon-1.7.1.js
14
14
  - generated/assets/websocket_rails.js
15
- - generated/assets/*.js
15
+ - generated/assets/event.js
16
+ - generated/assets/abstract_connection.js
17
+ - generated/assets/http_connection.js
18
+ - generated/assets/websocket_connection.js
19
+ - generated/assets/channel.js
20
+ - generated/specs/helpers.js
16
21
 
17
22
  spec_dir: spec/javascripts/generated
18
23
  spec_files:
19
- - specs/*_spec.js
24
+ - specs/event_spec.js
25
+ - specs/websocket_connection_spec.js
26
+ - specs/channel_spec.js
27
+ - specs/websocket_rails_spec.js
20
28
 
21
29
  # stylesheets
22
30
  #
@@ -0,0 +1,38 @@
1
+ #Use this file to set/override Jasmine configuration options
2
+ #You can remove it if you don't need it.
3
+ #This file is loaded *after* jasmine.yml is interpreted.
4
+ #
5
+ #Example: using a different boot file.
6
+ #Jasmine.configure do |config|
7
+ # config.boot_dir = '/absolute/path/to/boot_dir'
8
+ # config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] }
9
+ #end
10
+ #
11
+ require 'coffee-script'
12
+
13
+ puts "Precompiling assets..."
14
+
15
+ root = File.expand_path("../../../../lib/assets/javascripts/websocket_rails", __FILE__)
16
+ destination_dir = File.expand_path("../../../../spec/javascripts/generated/assets", __FILE__)
17
+
18
+ glob = File.expand_path("**/*.js.coffee", root)
19
+
20
+ Dir.glob(glob).each do |srcfile|
21
+ srcfile = Pathname.new(srcfile)
22
+ destfile = srcfile.sub(root, destination_dir).sub(".coffee", "")
23
+ FileUtils.mkdir_p(destfile.dirname)
24
+ File.open(destfile, "w") {|f| f.write(CoffeeScript.compile(File.new(srcfile)))}
25
+ end
26
+ puts "Compiling jasmine coffee scripts into javascript..."
27
+ root = File.expand_path("../../../../spec/javascripts/websocket_rails", __FILE__)
28
+ destination_dir = File.expand_path("../../generated/specs", __FILE__)
29
+
30
+ glob = File.expand_path("**/*.coffee", root)
31
+
32
+ Dir.glob(glob).each do |srcfile|
33
+ srcfile = Pathname.new(srcfile)
34
+ destfile = srcfile.sub(root, destination_dir).sub(".coffee", ".js")
35
+ FileUtils.mkdir_p(destfile.dirname)
36
+ File.open(destfile, "w") {|f| f.write(CoffeeScript.compile(File.new(srcfile)))}
37
+ end
38
+