entangled 0.0.1 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +122 -5
- data/entangled-0.0.1.gem +0 -0
- data/entangled-0.0.2.gem +0 -0
- data/entangled-0.0.3.gem +0 -0
- data/entangled-0.0.4.gem +0 -0
- data/entangled-0.0.5.gem +0 -0
- data/entangled.gemspec +1 -0
- data/lib/entangled.rb +24 -267
- data/lib/entangled/controller.rb +154 -0
- data/lib/entangled/model.rb +115 -0
- data/lib/entangled/version.rb +1 -1
- data/spec/spec_helper.rb +2 -0
- metadata +27 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5061458f7fad47437868c5e5c445d05c1e25060a
|
4
|
+
data.tar.gz: 4a5ea5580591c4d01aba2686a1392d1257bd818f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb26c2be5a2e95619dfae839f8929672fdc6e403cef4025212a1a992eac53fab21d81074b04f7ec1fc40a7caee96e83f244d70675da242ee3835d602a20e899a
|
7
|
+
data.tar.gz: 83a79396d556515b2a4124e15160fe513f2d12f87ef0ac0236f383a16633cdb01744b228817b4fa1d42a59b72db72d3ef3398f2eeddf1d22d81715ad03bac3c5
|
data/README.md
CHANGED
@@ -1,15 +1,24 @@
|
|
1
1
|
# Entangled
|
2
|
+
Services like Firebase are great because they provide real time data binding between client and server. But they come at a price: You give up control over your backend. Wouldn't it be great to have real time functionality but still keep your beloved Rails backend? That's where Entangled comes in.
|
2
3
|
|
3
|
-
|
4
|
+
Entangled is a layer behind your controllers and models that pushes updates to clients subscribed to certain channels in real time. For example, if you display a list of five messages on a page, if anyone adds a sixth message, everyone who is currently looking at that page will instantly see that sixth message being added to the list.
|
4
5
|
|
5
|
-
|
6
|
+
The idea is that real time data binding should be the default, not an add-on. Entangled aims at making real time features as easy to implement as possible, while at the same time making your restful controllers thinner.
|
6
7
|
|
8
|
+
## Installation
|
7
9
|
Add this line to your application's Gemfile:
|
8
10
|
|
9
11
|
```ruby
|
10
12
|
gem 'entangled'
|
11
13
|
```
|
12
14
|
|
15
|
+
Note that Redis and Puma are required as well. Redis is needed to build the channels clients subscribe to, Puma is needed to handle websockets concurrently.
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'redis'
|
19
|
+
gem 'puma'
|
20
|
+
```
|
21
|
+
|
13
22
|
And then execute:
|
14
23
|
|
15
24
|
$ bundle
|
@@ -19,13 +28,121 @@ Or install it yourself as:
|
|
19
28
|
$ gem install entangled
|
20
29
|
|
21
30
|
## Usage
|
31
|
+
Entangled is needed in three parts of your app. Given the example of a `MessagesController` and a `Message` model for a chat app, you will need:
|
22
32
|
|
23
|
-
|
33
|
+
### Routes
|
34
|
+
Add the following to your routes file:
|
24
35
|
|
25
|
-
|
36
|
+
```ruby
|
37
|
+
sockets_for :messages
|
38
|
+
```
|
39
|
+
|
40
|
+
Replace `messages` with your resource name.
|
41
|
+
|
42
|
+
Under the hood, this creates the following routes:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
get '/messages', to: 'messages#index', as: :messages
|
46
|
+
get '/messages/create', to: 'messages#create', as: :create_message
|
47
|
+
get '/messages/:id', to: 'messages#show', as: :message
|
48
|
+
get '/messages/:id/destroy', to: 'messages#destroy', as: :destroy_message
|
49
|
+
get '/messages/:id/update', to: 'messages#update', as: :update_message
|
50
|
+
```
|
51
|
+
|
52
|
+
Note that Websockets don't speak HTTP, so only GET requests are available. That's why these routes deviate slightly from restful routes. Also note that there are no `edit` and `new` actions, since an Entangled controller is only concerned with rendering data, not views.
|
53
|
+
|
54
|
+
### Model
|
55
|
+
Add the following to the top inside your model (e.g., a `Message` model):
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class Message < ActiveRecord::Base
|
59
|
+
include Entangled::Model
|
60
|
+
entangle
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
This will create the callbacks needed to push changes to data to all clients who are subscribed. This is essentially where the data binding is set up.
|
65
|
+
|
66
|
+
### Controller
|
67
|
+
Your controllers will be a little more lightweight than in a standard restful Rails app. A restful-style controller is expected and should look like this:
|
26
68
|
|
27
|
-
|
69
|
+
```ruby
|
70
|
+
class MessagesController < ApplicationController
|
71
|
+
include Entangled::Controller
|
72
|
+
|
73
|
+
def index
|
74
|
+
broadcast do
|
75
|
+
@messages = Message.all
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def show
|
80
|
+
broadcast do
|
81
|
+
@message = Message.find(params[:id])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def create
|
86
|
+
broadcast do
|
87
|
+
Message.create(message_params)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def update
|
92
|
+
broadcast do
|
93
|
+
Message.find(params[:id]).update(message_params)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def destroy
|
98
|
+
broadcast do
|
99
|
+
Message.find(params[:id]).destroy
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
def message_params
|
105
|
+
# params logic here
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
Note the following:
|
111
|
+
|
112
|
+
- All methods are wrapped in a new `broadcast` block needed to send messages to connected clients
|
113
|
+
- The `index` method will expect an instance variable with the same name as your controller in the plural form (e.g. `@messages` in a `MessagesController`)
|
114
|
+
- The `show` method will expect an instance variable with the singular name of your controller (e.g. `@message` in a `MessagesController`)
|
115
|
+
- Instance variables only need to be assigned in `index` and `show` since these are the only methods that should be concerned with sending data to clients. All other methods only publish updates to the data clients are subscribed to through the callbacks added to the model, so no instance variables are needed
|
116
|
+
- Data sent to clients arrives as stringified JSON
|
117
|
+
- Strong parameters are expected
|
118
|
+
|
119
|
+
## Server
|
120
|
+
Remember to run Redis whenever you run your server:
|
121
|
+
|
122
|
+
```shell
|
123
|
+
$ redis-server
|
124
|
+
```
|
125
|
+
|
126
|
+
Otherwise the channels won't work.
|
127
|
+
|
128
|
+
### Database
|
129
|
+
Depending on your app's settings, you might have to increase the pool size in your database.yml configuration file, since every new socket will open a new connection to your database.
|
130
|
+
|
131
|
+
### The Client
|
132
|
+
You will need to configure your client to create Websockets and understand incoming requests on those sockets. If you use Angular for your frontend, you can use [this library](https://github.com/so-entangled/angular). The use of Angular as counterpart of this gem is highly recommended, since its inherent two way data binding complements the real time functionality of this gem nicely.
|
133
|
+
|
134
|
+
## Planning Your Infrastructure
|
135
|
+
This gem is best used for Rails apps that serve as APIs only and are not concerned with rendering views. A frontend separate from your Rails app, such as Angular with Grunt, is recommended.
|
136
|
+
|
137
|
+
## Limitations
|
138
|
+
The gem rely's heavily on convention over configuration and currently only works with restful style controllers as shown above. More customization will be available soon.
|
139
|
+
|
140
|
+
## Contributing
|
141
|
+
1. Fork it ( https://github.com/so-entangled/rails/fork )
|
28
142
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
29
143
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
30
144
|
4. Push to the branch (`git push origin my-new-feature`)
|
31
145
|
5. Create a new Pull Request
|
146
|
+
|
147
|
+
## Credits
|
148
|
+
Thanks to [Ilias Tsangaris](https://github.com/iliastsangaris) for inspiring the name "Entanglement" based on [Quantum Entanglement](http://en.wikipedia.org/wiki/Quantum_entanglement) where pairs or groups of particles always react to changes as a whole, i.e. changes to one particle will result in immediate change of all particles in the group.
|
data/entangled-0.0.1.gem
ADDED
Binary file
|
data/entangled-0.0.2.gem
ADDED
Binary file
|
data/entangled-0.0.3.gem
ADDED
Binary file
|
data/entangled-0.0.4.gem
ADDED
Binary file
|
data/entangled-0.0.5.gem
ADDED
Binary file
|
data/entangled.gemspec
CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.7"
|
22
22
|
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency 'rspec', '~> 3.2'
|
23
24
|
spec.add_dependency 'tubesock', '~> 0.2'
|
24
25
|
spec.add_dependency 'rails', '~> 4.2'
|
25
26
|
end
|
data/lib/entangled.rb
CHANGED
@@ -1,269 +1,26 @@
|
|
1
|
-
require
|
2
|
-
require '
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
|
-
# Instead of :only, :except can be specified; similarly,
|
28
|
-
# the options can either be an array or a symbol
|
29
|
-
elsif options[:except].present?
|
30
|
-
|
31
|
-
# If it is a symbol, it has to be taken out of the default
|
32
|
-
# options. A callback has to be defined for each of the
|
33
|
-
# remaining options
|
34
|
-
if options[:except].is_a?(Symbol)
|
35
|
-
(default_options - [options[:except]]).each do |option|
|
36
|
-
create_hook option
|
37
|
-
end
|
38
|
-
|
39
|
-
# If it is an array, it also has to be taen out of the
|
40
|
-
# default options. A callback then also has to be defined
|
41
|
-
# for each of the remaining options
|
42
|
-
elsif options[:except].is_a?(Array)
|
43
|
-
(default_options - options[:except]).each do |option|
|
44
|
-
create_hook option
|
45
|
-
end
|
46
|
-
end
|
47
|
-
else
|
48
|
-
|
49
|
-
# If neither :only nor :except is specified, simply create
|
50
|
-
# a callback for each default option
|
51
|
-
default_options.each { |option| create_hook option }
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
# By default, model updates will be published after_create,
|
56
|
-
# after_update, and after_destroy. This behavior can be
|
57
|
-
# modified by passing :only or :except options to the
|
58
|
-
# entangle class method
|
59
|
-
def default_options
|
60
|
-
[:create, :update, :destroy]
|
61
|
-
end
|
62
|
-
|
63
|
-
# The inferred channel name. For example, if the class name
|
64
|
-
# is DeliciousTaco, the inferred channel name is "delicious_tacos"
|
65
|
-
def inferred_channel_name
|
66
|
-
name.underscore.pluralize
|
67
|
-
end
|
68
|
-
|
69
|
-
# Creates callbacks in the extented model
|
70
|
-
def create_hook(name)
|
71
|
-
send :"after_#{name}", proc { publish(name) }
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
module InstanceMethods
|
76
|
-
private
|
77
|
-
|
78
|
-
# Publishes to client. Whoever is subscribed
|
79
|
-
# to the model's channel or the record's channel
|
80
|
-
# gets the message
|
81
|
-
def publish(action)
|
82
|
-
Redis.new.publish(
|
83
|
-
self.class.inferred_channel_name,
|
84
|
-
json(action)
|
85
|
-
)
|
86
|
-
|
87
|
-
Redis.new.publish(
|
88
|
-
inferred_channel_name_for_single_record,
|
89
|
-
json(action)
|
90
|
-
)
|
91
|
-
end
|
92
|
-
|
93
|
-
# The inferred channel name for a single record
|
94
|
-
# containing the inferred channel name from the class
|
95
|
-
# and the record's id. For example, if it's a
|
96
|
-
# DeliciousTaco with the id 1, the inferred channel
|
97
|
-
# name for the single record is "delicious_tacos/1"
|
98
|
-
def inferred_channel_name_for_single_record
|
99
|
-
"#{self.class.inferred_channel_name}/#{id}"
|
100
|
-
end
|
101
|
-
|
102
|
-
# JSON containing the type of action (:create, :update
|
103
|
-
# or :destroy) and the record itself. This is eventually
|
104
|
-
# broadcast to the client
|
105
|
-
def json(action)
|
106
|
-
{
|
107
|
-
action: action,
|
108
|
-
resource: self
|
109
|
-
}.to_json
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def self.included(receiver)
|
114
|
-
receiver.extend ClassMethods
|
115
|
-
receiver.send :include, InstanceMethods
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
module Controller
|
120
|
-
include Tubesock::Hijack
|
121
|
-
|
122
|
-
module ClassMethods
|
123
|
-
|
124
|
-
end
|
125
|
-
|
126
|
-
module InstanceMethods
|
127
|
-
private
|
128
|
-
|
129
|
-
# The plural name of the resource, inferred from the
|
130
|
-
# controller's name. For example, if it's the TacosController,
|
131
|
-
# the resources_name will be "tacos". This is used to
|
132
|
-
# infer the instance variable name for collections assigned
|
133
|
-
# in the controller action
|
134
|
-
def resources_name
|
135
|
-
controller_name
|
136
|
-
end
|
137
|
-
|
138
|
-
# The singular name of the resource, inferred from the
|
139
|
-
# resources_name. This is used to infer the instance
|
140
|
-
# variable name for a single record assigned in the controller
|
141
|
-
# action
|
142
|
-
def resource_name
|
143
|
-
resources_name.singularize
|
144
|
-
end
|
145
|
-
|
146
|
-
# Broadcast events to every connected client
|
147
|
-
def broadcast(&block)
|
148
|
-
# Use hijack to handle sockets
|
149
|
-
hijack do |tubesock|
|
150
|
-
# Assuming restful controllers, the behavior of
|
151
|
-
# this method has to change depending on the action
|
152
|
-
# it's being used in
|
153
|
-
case action_name
|
154
|
-
|
155
|
-
# If the controller action is 'index', a collection
|
156
|
-
# of records should be broadcast
|
157
|
-
when 'index'
|
158
|
-
yield
|
159
|
-
|
160
|
-
# The following code will run if an instance
|
161
|
-
# variable with the plural resource name has been
|
162
|
-
# assigned in yield. For example, if a
|
163
|
-
# TacosController's index action looked something
|
164
|
-
# like this:
|
165
|
-
|
166
|
-
# def index
|
167
|
-
# broadcast do
|
168
|
-
# @tacos = Taco.all
|
169
|
-
# end
|
170
|
-
# end
|
171
|
-
|
172
|
-
# ...then @tacos will be broadcast to all connected
|
173
|
-
# clients. The variable name, in this example,
|
174
|
-
# has to be "@tacos"
|
175
|
-
if instance_variable_get(:"@#{resources_name}")
|
176
|
-
redis_thread = Thread.new do
|
177
|
-
Redis.new.subscribe resources_name do |on|
|
178
|
-
on.message do |channel, message|
|
179
|
-
tubesock.send_data message
|
180
|
-
end
|
181
|
-
|
182
|
-
# Broadcast collection to all connected clients
|
183
|
-
tubesock.send_data({
|
184
|
-
resources: instance_variable_get(:"@#{resources_name}")
|
185
|
-
}.to_json)
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
# When client disconnects, kill the thread
|
190
|
-
tubesock.onclose do
|
191
|
-
redis_thread.kill
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
# If the controller's action name is 'show', a single record
|
196
|
-
# should be broadcast
|
197
|
-
when 'show'
|
198
|
-
yield
|
199
|
-
|
200
|
-
# The following code will run if an instance variable
|
201
|
-
# with the singular resource name has been assigned in
|
202
|
-
# yield. For example, if a TacosController's show action
|
203
|
-
# looked something like this:
|
204
|
-
|
205
|
-
# def show
|
206
|
-
# broadcast do
|
207
|
-
# @taco = Taco.find(params[:id])
|
208
|
-
# end
|
209
|
-
# end
|
210
|
-
|
211
|
-
# ...then @taco will be broadcast to all connected clients.
|
212
|
-
# The variable name, in this example, has to be "@taco"
|
213
|
-
if instance_variable_get(:"@#{resource_name}")
|
214
|
-
redis_thread = Thread.new do
|
215
|
-
Redis.new.subscribe "#{resources_name}/#{instance_variable_get(:"@#{resource_name}").id}" do |on|
|
216
|
-
on.message do |channel, message|
|
217
|
-
tubesock.send_data message
|
218
|
-
end
|
219
|
-
|
220
|
-
# Broadcast single resource to all connected clients
|
221
|
-
tubesock.send_data({ resource: instance_variable_get(:"@#{resource_name}") }.to_json)
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
|
-
# When client disconnects, kill the thread
|
226
|
-
tubesock.onclose do
|
227
|
-
redis_thread.kill
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
# If the controller's action name is 'create', a record should be
|
232
|
-
# created. Before yielding, the params hash has to be prepared
|
233
|
-
# with attributes sent to the socket. The actual publishing
|
234
|
-
# happens in the model's callback
|
235
|
-
when 'create'
|
236
|
-
tubesock.onmessage do |m|
|
237
|
-
params[resource_name.to_sym] = JSON.parse(m).symbolize_keys
|
238
|
-
yield
|
239
|
-
end
|
240
|
-
|
241
|
-
# If the controller's action name is 'update', a record should be
|
242
|
-
# updated. Before yielding, the params hash has to be prepared
|
243
|
-
# with attributes sent to the socket. The default attributes
|
244
|
-
# id, created_at, and updated_at should not be included in params.
|
245
|
-
when 'update'
|
246
|
-
tubesock.onmessage do |m|
|
247
|
-
params[resource_name.to_sym] = JSON.parse(m).except('id', 'created_at', 'updated_at', 'webSocketUrl').symbolize_keys
|
248
|
-
yield
|
249
|
-
end
|
250
|
-
|
251
|
-
# For every other controller action, simply wrap whatever is
|
252
|
-
# yielded in the tubesock block to execute it in the context
|
253
|
-
# of the socket. The delete action is automatically covered
|
254
|
-
# by this, and other custom action can be added through this.
|
255
|
-
else
|
256
|
-
tubesock.onmessage do |m|
|
257
|
-
yield
|
258
|
-
end
|
259
|
-
end
|
260
|
-
end
|
261
|
-
end
|
262
|
-
end
|
263
|
-
|
264
|
-
def self.included(receiver)
|
265
|
-
receiver.extend ClassMethods
|
266
|
-
receiver.send :include, InstanceMethods
|
267
|
-
end
|
1
|
+
require 'entangled/version'
|
2
|
+
require 'entangled/model'
|
3
|
+
require 'entangled/controller'
|
4
|
+
require 'action_dispatch/routing'
|
5
|
+
require 'active_support/concern'
|
6
|
+
|
7
|
+
module ActionDispatch::Routing
|
8
|
+
class Mapper
|
9
|
+
def sockets_for(resource, &block)
|
10
|
+
resources = resource.to_s.underscore.pluralize.to_sym
|
11
|
+
resource = resource.to_s.underscore.singularize.to_sym
|
12
|
+
|
13
|
+
get :"/#{resources}", to: "#{resources}#index", as: resources
|
14
|
+
get :"/#{resources}/create", to: "#{resources}#create", as: :"create_#{resource}"
|
15
|
+
get :"/#{resources}/:id", to: "#{resources}#show", as: resource
|
16
|
+
get :"/#{resources}/:id/destroy", to: "#{resources}#destroy", as: :"destroy_#{resource}"
|
17
|
+
get :"/#{resources}/:id/update", to: "#{resources}#update", as: :"update_#{resource}"
|
18
|
+
|
19
|
+
if block_given?
|
20
|
+
namespace resources do
|
21
|
+
yield
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
268
25
|
end
|
269
26
|
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'tubesock'
|
2
|
+
|
3
|
+
module Entangled
|
4
|
+
module Controller
|
5
|
+
include Tubesock::Hijack
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
module InstanceMethods
|
12
|
+
private
|
13
|
+
|
14
|
+
# The plural name of the resource, inferred from the
|
15
|
+
# controller's name. For example, if it's the TacosController,
|
16
|
+
# the resources_name will be "tacos". This is used to
|
17
|
+
# infer the instance variable name for collections assigned
|
18
|
+
# in the controller action
|
19
|
+
def resources_name
|
20
|
+
controller_name
|
21
|
+
end
|
22
|
+
|
23
|
+
# The singular name of the resource, inferred from the
|
24
|
+
# resources_name. This is used to infer the instance
|
25
|
+
# variable name for a single record assigned in the controller
|
26
|
+
# action
|
27
|
+
def resource_name
|
28
|
+
resources_name.singularize
|
29
|
+
end
|
30
|
+
|
31
|
+
# Broadcast events to every connected client
|
32
|
+
def broadcast(&block)
|
33
|
+
# Use hijack to handle sockets
|
34
|
+
hijack do |tubesock|
|
35
|
+
# Assuming restful controllers, the behavior of
|
36
|
+
# this method has to change depending on the action
|
37
|
+
# it's being used in
|
38
|
+
case action_name
|
39
|
+
|
40
|
+
# If the controller action is 'index', a collection
|
41
|
+
# of records should be broadcast
|
42
|
+
when 'index'
|
43
|
+
yield
|
44
|
+
|
45
|
+
# The following code will run if an instance
|
46
|
+
# variable with the plural resource name has been
|
47
|
+
# assigned in yield. For example, if a
|
48
|
+
# TacosController's index action looked something
|
49
|
+
# like this:
|
50
|
+
|
51
|
+
# def index
|
52
|
+
# broadcast do
|
53
|
+
# @tacos = Taco.all
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
|
57
|
+
# ...then @tacos will be broadcast to all connected
|
58
|
+
# clients. The variable name, in this example,
|
59
|
+
# has to be "@tacos"
|
60
|
+
if instance_variable_get(:"@#{resources_name}")
|
61
|
+
redis_thread = Thread.new do
|
62
|
+
Redis.new.subscribe resources_name do |on|
|
63
|
+
on.message do |channel, message|
|
64
|
+
tubesock.send_data message
|
65
|
+
end
|
66
|
+
|
67
|
+
# Broadcast collection to all connected clients
|
68
|
+
tubesock.send_data({
|
69
|
+
resources: instance_variable_get(:"@#{resources_name}")
|
70
|
+
}.to_json)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# When client disconnects, kill the thread
|
75
|
+
tubesock.onclose do
|
76
|
+
redis_thread.kill
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# If the controller's action name is 'show', a single record
|
81
|
+
# should be broadcast
|
82
|
+
when 'show'
|
83
|
+
yield
|
84
|
+
|
85
|
+
# The following code will run if an instance variable
|
86
|
+
# with the singular resource name has been assigned in
|
87
|
+
# yield. For example, if a TacosController's show action
|
88
|
+
# looked something like this:
|
89
|
+
|
90
|
+
# def show
|
91
|
+
# broadcast do
|
92
|
+
# @taco = Taco.find(params[:id])
|
93
|
+
# end
|
94
|
+
# end
|
95
|
+
|
96
|
+
# ...then @taco will be broadcast to all connected clients.
|
97
|
+
# The variable name, in this example, has to be "@taco"
|
98
|
+
if instance_variable_get(:"@#{resource_name}")
|
99
|
+
redis_thread = Thread.new do
|
100
|
+
Redis.new.subscribe "#{resources_name}/#{instance_variable_get(:"@#{resource_name}").id}" do |on|
|
101
|
+
on.message do |channel, message|
|
102
|
+
tubesock.send_data message
|
103
|
+
end
|
104
|
+
|
105
|
+
# Broadcast single resource to all connected clients
|
106
|
+
tubesock.send_data({ resource: instance_variable_get(:"@#{resource_name}") }.to_json)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# When client disconnects, kill the thread
|
111
|
+
tubesock.onclose do
|
112
|
+
redis_thread.kill
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# If the controller's action name is 'create', a record should be
|
117
|
+
# created. Before yielding, the params hash has to be prepared
|
118
|
+
# with attributes sent to the socket. The actual publishing
|
119
|
+
# happens in the model's callback
|
120
|
+
when 'create'
|
121
|
+
tubesock.onmessage do |m|
|
122
|
+
params[resource_name.to_sym] = JSON.parse(m).symbolize_keys
|
123
|
+
yield
|
124
|
+
end
|
125
|
+
|
126
|
+
# If the controller's action name is 'update', a record should be
|
127
|
+
# updated. Before yielding, the params hash has to be prepared
|
128
|
+
# with attributes sent to the socket. The default attributes
|
129
|
+
# id, created_at, and updated_at should not be included in params.
|
130
|
+
when 'update'
|
131
|
+
tubesock.onmessage do |m|
|
132
|
+
params[resource_name.to_sym] = JSON.parse(m).except('id', 'created_at', 'updated_at', 'webSocketUrl').symbolize_keys
|
133
|
+
yield
|
134
|
+
end
|
135
|
+
|
136
|
+
# For every other controller action, simply wrap whatever is
|
137
|
+
# yielded in the tubesock block to execute it in the context
|
138
|
+
# of the socket. The delete action is automatically covered
|
139
|
+
# by this, and other custom action can be added through this.
|
140
|
+
else
|
141
|
+
tubesock.onmessage do |m|
|
142
|
+
yield
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.included(receiver)
|
150
|
+
receiver.extend ClassMethods
|
151
|
+
receiver.send :include, InstanceMethods
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Entangled
|
2
|
+
module Model
|
3
|
+
module ClassMethods
|
4
|
+
# Create after_ callbacks for options
|
5
|
+
def entangle(options = {})
|
6
|
+
|
7
|
+
# If :only is specified, the options can either
|
8
|
+
# be an array or a symbol
|
9
|
+
if options[:only].present?
|
10
|
+
|
11
|
+
# If it is a symbol, something like only: :create
|
12
|
+
# was passed in, and we need to create a hook
|
13
|
+
# only for that one option
|
14
|
+
if options[:only].is_a?(Symbol)
|
15
|
+
create_hook options[:only]
|
16
|
+
|
17
|
+
# If it is an array, something like only: [:create, :update]
|
18
|
+
# was passed in, and we need to create hook for each
|
19
|
+
# of these options
|
20
|
+
elsif options[:only].is_a?(Array)
|
21
|
+
options[:only].each { |option| create_hook option }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Instead of :only, :except can be specified; similarly,
|
25
|
+
# the options can either be an array or a symbol
|
26
|
+
elsif options[:except].present?
|
27
|
+
|
28
|
+
# If it is a symbol, it has to be taken out of the default
|
29
|
+
# options. A callback has to be defined for each of the
|
30
|
+
# remaining options
|
31
|
+
if options[:except].is_a?(Symbol)
|
32
|
+
(default_options - [options[:except]]).each do |option|
|
33
|
+
create_hook option
|
34
|
+
end
|
35
|
+
|
36
|
+
# If it is an array, it also has to be taen out of the
|
37
|
+
# default options. A callback then also has to be defined
|
38
|
+
# for each of the remaining options
|
39
|
+
elsif options[:except].is_a?(Array)
|
40
|
+
(default_options - options[:except]).each do |option|
|
41
|
+
create_hook option
|
42
|
+
end
|
43
|
+
end
|
44
|
+
else
|
45
|
+
|
46
|
+
# If neither :only nor :except is specified, simply create
|
47
|
+
# a callback for each default option
|
48
|
+
default_options.each { |option| create_hook option }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# By default, model updates will be published after_create,
|
53
|
+
# after_update, and after_destroy. This behavior can be
|
54
|
+
# modified by passing :only or :except options to the
|
55
|
+
# entangle class method
|
56
|
+
def default_options
|
57
|
+
[:create, :update, :destroy]
|
58
|
+
end
|
59
|
+
|
60
|
+
# The inferred channel name. For example, if the class name
|
61
|
+
# is DeliciousTaco, the inferred channel name is "delicious_tacos"
|
62
|
+
def inferred_channel_name
|
63
|
+
name.underscore.pluralize
|
64
|
+
end
|
65
|
+
|
66
|
+
# Creates callbacks in the extented model
|
67
|
+
def create_hook(name)
|
68
|
+
send :"after_#{name}", proc { publish(name) }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module InstanceMethods
|
73
|
+
private
|
74
|
+
|
75
|
+
# Publishes to client. Whoever is subscribed
|
76
|
+
# to the model's channel or the record's channel
|
77
|
+
# gets the message
|
78
|
+
def publish(action)
|
79
|
+
Redis.new.publish(
|
80
|
+
self.class.inferred_channel_name,
|
81
|
+
json(action)
|
82
|
+
)
|
83
|
+
|
84
|
+
Redis.new.publish(
|
85
|
+
inferred_channel_name_for_single_record,
|
86
|
+
json(action)
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
# The inferred channel name for a single record
|
91
|
+
# containing the inferred channel name from the class
|
92
|
+
# and the record's id. For example, if it's a
|
93
|
+
# DeliciousTaco with the id 1, the inferred channel
|
94
|
+
# name for the single record is "delicious_tacos/1"
|
95
|
+
def inferred_channel_name_for_single_record
|
96
|
+
"#{self.class.inferred_channel_name}/#{id}"
|
97
|
+
end
|
98
|
+
|
99
|
+
# JSON containing the type of action (:create, :update
|
100
|
+
# or :destroy) and the record itself. This is eventually
|
101
|
+
# broadcast to the client
|
102
|
+
def json(action)
|
103
|
+
{
|
104
|
+
action: action,
|
105
|
+
resource: self
|
106
|
+
}.to_json
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.included(receiver)
|
111
|
+
receiver.extend ClassMethods
|
112
|
+
receiver.send :include, InstanceMethods
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/entangled/version.rb
CHANGED
data/spec/spec_helper.rb
ADDED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: entangled
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dennis Charles Hackethal
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-02-
|
11
|
+
date: 2015-02-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.2'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.2'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: tubesock
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -79,9 +93,17 @@ files:
|
|
79
93
|
- LICENSE.txt
|
80
94
|
- README.md
|
81
95
|
- Rakefile
|
96
|
+
- entangled-0.0.1.gem
|
97
|
+
- entangled-0.0.2.gem
|
98
|
+
- entangled-0.0.3.gem
|
99
|
+
- entangled-0.0.4.gem
|
100
|
+
- entangled-0.0.5.gem
|
82
101
|
- entangled.gemspec
|
83
102
|
- lib/entangled.rb
|
103
|
+
- lib/entangled/controller.rb
|
104
|
+
- lib/entangled/model.rb
|
84
105
|
- lib/entangled/version.rb
|
106
|
+
- spec/spec_helper.rb
|
85
107
|
homepage: https://github.com/so-entangled/rails
|
86
108
|
licenses:
|
87
109
|
- MIT
|
@@ -102,8 +124,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
124
|
version: '0'
|
103
125
|
requirements: []
|
104
126
|
rubyforge_project:
|
105
|
-
rubygems_version: 2.
|
127
|
+
rubygems_version: 2.2.2
|
106
128
|
signing_key:
|
107
129
|
specification_version: 4
|
108
130
|
summary: Makes Rails real time through websockets.
|
109
|
-
test_files:
|
131
|
+
test_files:
|
132
|
+
- spec/spec_helper.rb
|