atr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9067fa1049b19201c58fea01f1d223530981a3bf
4
+ data.tar.gz: 74926a242a7203a02df577c59e69a59e930fbc80
5
+ SHA512:
6
+ metadata.gz: 4e78aaa088307f96fdf2991934f08820aaac8255cfaa7595aa415deab5ed32bce4c032368247fefe8c9d12a50b26180251ca2f23b0a6684d056c9d8dabeff47c
7
+ data.tar.gz: add7f386011c77ae85486850d5d4656644f610d52de0afb099581ca96c3720aebb774e9832e20e10e74772515178e7fb83a48ec4ba4e1ab4522ebf36d447a699
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in atr.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jason Ayre
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,347 @@
1
+ ## What a stupid name
2
+ You likely think that this library has something to do with attributes, but it does not. So yes, it probably is a stupid name. Its named after
3
+
4
+ http://en.wikipedia.org/wiki/Ampex_ATR-100
5
+
6
+ in the spirit of following the celluloid/reel analog tape metaphor naming convention. But mostly because its short to type and Im lazy.
7
+
8
+ ## What it does
9
+
10
+ Websockets/publishing events to connected clients, using the following as its backbone:
11
+
12
+ Reel, Celluloid&Celluloid::IO, and Redis
13
+
14
+ ## Why use this over other websocket libraries for ruby?
15
+
16
+ Well dont use this yet because its incomplete. But the goal is mainly:
17
+
18
+ 1. Make it easy to publish changes to the connected client
19
+ 2. Event machine is a pile of *your preferred synonym for garbage*, so, didnt want to use any library that uses event machine. Use CelluloidIO and Reel as a foundation, because Reel is pretty cool.
20
+ 3. Be able to publish events from anywhere in your application eventually, but for now just focus on making it easy to publish when resources themselves are created/updated/destroyed.
21
+ 4. Single thread per websocket request that comes in. That lets you scope the websockets to the lowest level in your application that you choose to. I.E.
22
+
23
+ Lets say you have a subscriber which has many users. You can scope the publishing of events to the subscriber, and each connected user has its own websocket thread open to prevent chaotic behavior Ive seen happen in many other websocket implementations.
24
+
25
+ **So its strongly focused on listening for events, not triggering, maybe that will change in future maybe not IDK.**
26
+
27
+ ### Usage
28
+
29
+ To generate events upon actions taking place within your application, include
30
+ ``` ruby
31
+ include ::Atr::Publishable
32
+ ```
33
+
34
+ Into your model. Example
35
+
36
+ ### How it works
37
+
38
+ ``` ruby
39
+ class Post < ::ActiveRecord::Base
40
+ include ::Atr::Publishable
41
+ end
42
+ ```
43
+
44
+ This will set up 3 publishing queues, and 3 after_action callbacks for the respective actions.
45
+
46
+ ``` ruby
47
+ post.created
48
+ post.destroyed
49
+ post.updated
50
+ ```
51
+
52
+ which will basically do
53
+
54
+ ``` ruby
55
+
56
+ def publish_created_event
57
+ routing_key = self.class.build_routing_key_for_record_action(self, "created")
58
+ // post.created
59
+ event_name = self.class.resource_action_routing_key("created")
60
+ // post.created
61
+
62
+ record_created_event = ::Atr::Event.new(routing_key, event_name, self)
63
+
64
+ ::Atr.publish_event(record_created_event)
65
+ end
66
+ ```
67
+
68
+ Etc Etc for updated/destroyed.
69
+
70
+ **So to walkthrough publish_created_event above**
71
+
72
+ 1. First, we create routing key based on the name of the class, + the action. Additionally if you scope the event, this will be reflected in the routing key (i.e. you can scope it to a particular subscriber or user or whatever, so you can share state and or sync events between multiple users belonging to the same organization). (more on that later)
73
+ 2. we generate an event name based on name of the class + the action (scope doesent matter we just want to describe what happened)
74
+ 3. wrap the record in an ::Atr::Event object
75
+ 4. Publish the event, this will Marshal.dump the record through redis, and if there is a subscriber listening on the routing key of the event, the websocket connection (Atr::Reactor) instance, will receive that message, unmarshall it back into the original event object, and write it to the websocket.
76
+
77
+ This allows us to publish events with pretty fine grained precision, and broadcast them to specific subscribers. If you're unfamiliar with redis pub/sub, rundown is, if you are listening to the channel at the moment the message is published, the subscriber will get it, otherwise it removes it and pays no regard to the msssage being published. No durability, but thats what we want for websocket events.
78
+
79
+ ### How the websocket server works
80
+
81
+ The websocket server works differently than many other implementations, in that its by design a standalone process, which acts mainly as a router for websocket connections that come in. When a valid websocket request comes in, it will launch a brand new thread, close the original request and detatch the websocket, and pass it into the object which controls the websocket (::Atr::Reactor, for lack of a better name ATM). This once again is mainly about scope, and has arisen out of the past frustrations of using websocket libraries which were built on event machine which I spent countless hours debugging, issues like duplicate events.
82
+
83
+ Its also IMO the ideal way to model a socket server, 1 thread belonging to each client which connects to it. Close the thread when they disconnect. Only use resources for whats currently relevant.
84
+
85
+ ### Starting the socket server
86
+ ``` ruby
87
+ bx atr_server start --server_host="127.0.0.1" --server_port="7777"
88
+ ```
89
+ (the defaults, the above is the same as)
90
+
91
+ ``` ruby
92
+ bx atr_server start
93
+ ```
94
+
95
+ ### Connecting to socket server via JS
96
+
97
+ ``` javascript
98
+ var ws = new WebSocket("ws://127.0.0.1:7777");
99
+ ```
100
+
101
+ ### Listen for events
102
+ ``` javascript
103
+ ws.onmessage = function(e) {
104
+ var event, parsed_event;
105
+
106
+ event = e.data;
107
+ parsed_event = JSON.parse(event);
108
+ console.log(parsed_event);
109
+ }
110
+ ```
111
+
112
+ ### Full Example (including client side)
113
+
114
+ Here is a snippet of code from an inprogress sideproject, using a base angular controller (using angular-ui-router). This is enough to listen to any event in the application, and display growl notifications for all connected members of the "organization", notifying them of what action took place.
115
+
116
+ ``` javascript
117
+ $stateProvider.state('base', {
118
+ abstract: false,
119
+ url: "",
120
+ templateUrl: '/templates/base.html',
121
+ resolve: {
122
+ current_organization: function(CurrentOrganization) {
123
+ return CurrentOrganization.get();
124
+ },
125
+ current_user: function(CurrentUser) {
126
+ return CurrentUser.get();
127
+ }
128
+ },
129
+ controller: function($scope, current_organization, current_user, $state, growl) {
130
+ $scope.current_organization = current_organization;
131
+ $scope.current_user = current_user;
132
+
133
+ $scope.websocket_params = {
134
+ organization: current_organization.id
135
+ };
136
+
137
+ $scope.websocket_base_url = "ws://127.0.0.1:7777";
138
+
139
+ $scope.websocketUrl = function() {
140
+ return [ $scope.websocket_base_url, _.flatten(_.pairs($scope.websocket_params)).join("/") ].join("/");
141
+ };
142
+
143
+ $scope.ws = new WebSocket($scope.websocketUrl());
144
+
145
+ $scope.ws.onopen = function() {
146
+ console.log('opening ws con');
147
+ $scope.ws.send(JSON.stringify({action: "do.something." + current_user.id}));
148
+ };
149
+
150
+ $scope.ws.onmessage = function(e) {
151
+ $scope.dispatchMessage(e.data);
152
+ };
153
+
154
+ $scope.ws.onclose = function() {
155
+ alert("websocket connection closed");
156
+ };
157
+
158
+ $scope.do_something = function() {
159
+ $scope.ws.send('do_something');
160
+ };
161
+
162
+ $scope.dispatchMessage = function (message) {
163
+ var event = JSON.parse(message);
164
+ $scope.$root.$broadcast(event.name, event);
165
+ };
166
+
167
+ _.each(current_organization.websocket_channels, function(channel){
168
+ $scope.$on(channel, function(e, websocket_event){
169
+ console.log(websocket_event);
170
+ growl.addInfoMessage(websocket_event.name);
171
+ $scope.$root.$digest();
172
+ });
173
+ });
174
+ }
175
+ });
176
+ ```
177
+
178
+ **Initializer**
179
+ ``` ruby
180
+ ::Atr.configure do |config|
181
+ config.authenticate_with = ::WebsocketAuthenticator
182
+ config.scope_with = ::WebsocketScope
183
+ config.event_serializer = ::WebsocketEventSerializer
184
+ end
185
+ ```
186
+
187
+ **NOTE:** the following are bad examples. I.e. Im not really authenticating anything im just checking that the websocket request has a valid organization id in the path, really youd want to use auth token or some way to validate the request. But it's so low level that it should be easy to do whatever you need to w/this middlewarish pattern for scoping/validating.
188
+
189
+ **Websocket Authenticator**
190
+
191
+ ``` ruby
192
+ class WebsocketAuthenticator < ::Atr::RequestAuthenticator
193
+ def matches?
194
+ current_organization.present?
195
+ end
196
+
197
+ def current_organization
198
+ @current_organization ||= ::Client::Organization.find(segs[1])
199
+ end
200
+
201
+ def segs
202
+ @segs ||= request.url.split("/").reject(&:empty?)
203
+ end
204
+ end
205
+ ```
206
+ **Websocket Scope**
207
+ ``` ruby
208
+ class WebsocketScope < ::Atr::RequestScope
209
+ VALID_SCOPE_KEYS = ["organization"]
210
+
211
+ def segs
212
+ @segs ||= request.url.split("/").reject(&:empty?)
213
+ end
214
+
215
+ def routing_key
216
+ [segs[0], segs[1]].join(".")
217
+ end
218
+
219
+ def valid?
220
+ VALID_SCOPE_KEYS.include?(segs[0]) && segs.size == 2
221
+ end
222
+ end
223
+ ```
224
+
225
+ You also have access to query string params as a hash with params method in either class.
226
+
227
+
228
+ **Event Serializer**
229
+
230
+ Im using ActiveModel Serializers, but any serializer that is instantiated as new, passes in the record, and is serialized via the .to_json method should work (so if you want to use decorators or a custom class or something).
231
+
232
+ ``` ruby
233
+ class WebsocketEventSerializer < BaseSerializer
234
+ self.root = false
235
+
236
+ attributes :id, :name, :record, :routing_key, :record, :occured_at, :time_ago
237
+
238
+ def time_ago
239
+ distance_of_time_in_words(object.occured_at, ::DateTime.now)
240
+ end
241
+
242
+ def action
243
+ object.name.split(".").pop
244
+ end
245
+ end
246
+ ```
247
+
248
+ **Model, and scoping the publication of the event**
249
+
250
+ ``` ruby
251
+ class Post < ::ActiveRecord::Base
252
+ include ::Atr::Publishable
253
+ publication_scope :organization_id
254
+ end
255
+ ```
256
+
257
+ Kind of ghetto, but works for now, basically, this will using the organization_id attribute, and prepend the key (without the _id), i.e.
258
+
259
+ organization.#{organization_id}.post.created
260
+
261
+ Whenever creating routing keys when publishing events. (only for that specific resource though, so you probably want to add that same publication scope, and define a method that gets to that scope for each of your models requiring publication).
262
+
263
+ Last but not least, for a programatic way of knowing which channels to listen to I.E., in the javascript above
264
+
265
+ ``` javascript
266
+ _.each(current_organization.websocket_channels, function(channel){
267
+ $scope.$on(channel, function(e, websocket_event){
268
+ console.log(websocket_event);
269
+ });
270
+ });
271
+ ```
272
+
273
+ You can get the channels via the registry
274
+
275
+ ``` ruby
276
+ def websocket_channels
277
+ ::Atr::Registry.channels
278
+ end
279
+ ```
280
+
281
+ ### If any of how it works is confusing, pay attention to the following as it may clear things up:
282
+
283
+ **NOTE:** the redis pubsub mechanism is only concerned about the publication scope, i.e.
284
+
285
+ organization.1234.post.created
286
+
287
+ But since each connection launches a new thread, listenting to that specific channel, we can then broadcast the event itself to the websocket as
288
+
289
+ post.created
290
+
291
+ and it will be scoped appropriately to correct client, as its only actually written out to the websocket threads belonging to that organization.
292
+
293
+ (we actually just write the entire event object to the socket and the client side JS is responsible for figuring out how to route it and what not)
294
+
295
+ ### Scoping and authentication
296
+
297
+ Quick explanation is, it works much like Rack middleware. Configure a class and it will be passed the request object on initialize.
298
+
299
+ Class must respond to matches? which will determine whether the request is valid, and in the case of scope_with, will scope the publishing of the record, i.e.
300
+
301
+ organization.1234.post.created
302
+ rather than post.created
303
+
304
+ ### Advanced Configuration
305
+ Todo: explain scoping and authenticating the websocket requests and provide better example.
306
+
307
+ ### Important To Do
308
+ Allow target redis instance to actually be configurable. Right now just running locally so connects to redis defaults.
309
+
310
+ ### Configuration / Initializer
311
+
312
+ ``` ruby
313
+ ::Atr.configure do |config|
314
+ config.authenticate_with = ::WebsocketAuthenticator
315
+ config.scope_with = ::WebsocketScope
316
+ config.event_serializer = ::WebsocketEventSerializer
317
+ end
318
+ ```
319
+
320
+ ### FYIs / Gotchas / Notes to self
321
+
322
+ ActiveRecord opens new connection each time request comes in, Im manually closing it as we do need to have AR loaded for the purposes of reading the schema, but after that we dont need a connection at all since all the marshaling/unmarshaling the event does not require it. (As far as I can see at least). The main application w/ the publisher, does the serialization, so the server doesen't need the connections. So no used up connections per the websocket threads that are created. Winning.
323
+
324
+ If I decide to go the route of websocket rails to allow cruding beyond just listening as it stands right now, the websocket connections will need to use connection pool.
325
+
326
+ ## Installation
327
+
328
+ Add this line to your application's Gemfile:
329
+
330
+ gem 'atr'
331
+
332
+ And then execute:
333
+
334
+ $ bundle
335
+
336
+ Or install it yourself as:
337
+
338
+ $ gem install atr
339
+
340
+
341
+ ## Contributing
342
+
343
+ 1. Fork it ( https://github.com/[my-github-username]/atr/fork )
344
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
345
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
346
+ 4. Push to the branch (`git push origin my-new-feature`)
347
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'atr/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "atr"
8
+ spec.version = Atr::VERSION
9
+ spec.authors = ["Jason Ayre"]
10
+ spec.email = ["jasonayre@gmail.com"]
11
+ spec.summary = %q{Pub, sub and websocket server}
12
+ spec.description = %q{Pub sub and websockets}
13
+ spec.homepage = "http://github.com/jasonayre/atr"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "active_attr"
22
+ spec.add_dependency "reel", "> 0.4.0"
23
+ spec.add_dependency "redis"
24
+ spec.add_dependency "celluloid-io"
25
+ spec.add_dependency "celluloid-redis"
26
+ spec.add_dependency "json"
27
+
28
+ spec.add_development_dependency 'activerecord'
29
+ spec.add_development_dependency 'sqlite3'
30
+ spec.add_development_dependency "bundler"
31
+ spec.add_development_dependency "rake"
32
+ spec.add_development_dependency "rspec"
33
+ spec.add_development_dependency "rspec-pride"
34
+ spec.add_development_dependency "pry-nav"
35
+ spec.add_development_dependency "simplecov"
36
+ end