websocket-rails 0.6.2 → 0.7.0

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 (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
+