entangled 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +2 -0
- data/entangled.gemspec +25 -0
- data/lib/entangled.rb +269 -0
- data/lib/entangled/version.rb +3 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b7dd7732a7af0fdbc6a792eb242f507817adcfde
|
4
|
+
data.tar.gz: 0091ccff6cdad24ac350dace118ae33a5331bc5a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f64204d586309f97651845b5a642a333a4f88d943608326d84063560b4f2ed180304433a7caa5810cb5ddb2eed9c296ab5e1f4c535ea6d05219215ed02385e93
|
7
|
+
data.tar.gz: 8573a37652353e3b32807bb653040036b2981927a74fa4565a9b51897f24d37ae3996b4720b689b4a791ad59e6933f624d0fe4d23d17b92e4bd9fee98f23832b
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Dennis Charles Hackethal
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Entangled
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'entangled'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install entangled
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Contributing
|
26
|
+
|
27
|
+
1. Fork it ( https://github.com/[my-github-username]/entangled/fork )
|
28
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
29
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
30
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
31
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/entangled.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'entangled/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "entangled"
|
8
|
+
spec.version = Entangled::VERSION
|
9
|
+
spec.authors = ["Dennis Charles Hackethal"]
|
10
|
+
spec.email = ["dennis.hackethal@gmail.com"]
|
11
|
+
spec.summary = %q{Makes Rails real time through websockets.}
|
12
|
+
spec.description = %q{Makes Rails real time through websockets. Check out the JavaScript counterpart for the front end.}
|
13
|
+
spec.homepage = "https://github.com/so-entangled/rails"
|
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_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_dependency 'tubesock', '~> 0.2'
|
24
|
+
spec.add_dependency 'rails', '~> 4.2'
|
25
|
+
end
|
data/lib/entangled.rb
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
require "entangled/version"
|
2
|
+
require 'tubesock'
|
3
|
+
|
4
|
+
module Entangled
|
5
|
+
module Model
|
6
|
+
module ClassMethods
|
7
|
+
# Create after_ callbacks for options
|
8
|
+
def entangle(options = {})
|
9
|
+
|
10
|
+
# If :only is specified, the options can either
|
11
|
+
# be an array or a symbol
|
12
|
+
if options[:only].present?
|
13
|
+
|
14
|
+
# If it is a symbol, something like only: :create
|
15
|
+
# was passed in, and we need to create a hook
|
16
|
+
# only for that one option
|
17
|
+
if options[:only].is_a?(Symbol)
|
18
|
+
create_hook options[:only]
|
19
|
+
|
20
|
+
# If it is an array, something like only: [:create, :update]
|
21
|
+
# was passed in, and we need to create hook for each
|
22
|
+
# of these options
|
23
|
+
elsif options[:only].is_a?(Array)
|
24
|
+
options[:only].each { |option| create_hook option }
|
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
|
268
|
+
end
|
269
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: entangled
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dennis Charles Hackethal
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: tubesock
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.2'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.2'
|
69
|
+
description: Makes Rails real time through websockets. Check out the JavaScript counterpart
|
70
|
+
for the front end.
|
71
|
+
email:
|
72
|
+
- dennis.hackethal@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- Gemfile
|
79
|
+
- LICENSE.txt
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- entangled.gemspec
|
83
|
+
- lib/entangled.rb
|
84
|
+
- lib/entangled/version.rb
|
85
|
+
homepage: https://github.com/so-entangled/rails
|
86
|
+
licenses:
|
87
|
+
- MIT
|
88
|
+
metadata: {}
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
requirements: []
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 2.4.5
|
106
|
+
signing_key:
|
107
|
+
specification_version: 4
|
108
|
+
summary: Makes Rails real time through websockets.
|
109
|
+
test_files: []
|