entangled 0.0.20 → 0.0.21
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +96 -2
- data/entangled.gemspec +1 -0
- data/entangled.js +1 -1
- data/lib/entangled/controller.rb +5 -11
- data/lib/entangled/model.rb +36 -15
- data/lib/entangled/version.rb +1 -1
- data/spec/dummy/app/controllers/items_controller.rb +39 -0
- data/spec/dummy/app/controllers/lists_controller.rb +32 -0
- data/spec/dummy/app/models/item.rb +6 -0
- data/spec/dummy/app/models/list.rb +6 -0
- data/spec/dummy/db/migrate/20150314054548_create_lists.rb +9 -0
- data/spec/dummy/db/migrate/20150314055847_create_items.rb +11 -0
- data/spec/dummy/db/schema.rb +15 -1
- data/spec/dummy/public/app/app.js +10 -1
- data/spec/dummy/public/app/controllers/list.js +16 -0
- data/spec/dummy/public/app/controllers/lists.js +23 -0
- data/spec/dummy/public/app/controllers/message.js +0 -1
- data/spec/dummy/public/app/entangled/entangled.js +46 -29
- data/spec/dummy/public/app/services/list.js +9 -0
- data/spec/dummy/public/index.html +3 -0
- data/spec/dummy/public/test/controllers/lists_test.js +87 -0
- data/spec/dummy/public/views/lists/index.html +14 -0
- data/spec/dummy/public/views/lists/show.html +9 -0
- data/spec/models/inclusion_exclusion_spec.rb +22 -22
- data/spec/models/item_spec.rb +40 -0
- data/spec/models/list_spec.rb +31 -0
- data/spec/models/message_spec.rb +19 -16
- data/spec/routing/nested_routing_spec.rb +0 -7
- data/spec/spec_helper.rb +1 -0
- metadata +44 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a57bb2d361ca6fc01d21e41081913491ec6d3f96
|
4
|
+
data.tar.gz: 8e76dffbad36c6d4f5ce3189852c054d919bbe36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 313a25571895c0076d87989f4efae4cda3d8642255fa930844b02306b12f2dca7d6d5d5236ca8778d64588a5f55e8f555fc2c44a9d9155e1276e0f7b57339a99
|
7
|
+
data.tar.gz: 0fd5d35de6ee95bdb9f77b1a21df7959b17e19048dcd19e6bea3794ebb9e1980cf4c22ff589a51320fca923c08bf01d55a471531083b51a0f60a59a8f7a2d0c1
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@ Real time is important. Users have come to expect real time behavior from every
|
|
6
6
|
|
7
7
|
Entangled stores and syncs data from ActiveRecord instantly across every device. It is a layer behind your models and controllers that pushes updates to all connected clients in real time. It is cross-browser compatible and offers real time validations.
|
8
8
|
|
9
|
-
Currently, Entangled runs on Rails 4.2 in the back end and Angular in the front end.
|
9
|
+
Currently, Entangled runs on Rails 4.2 and Ruby 2.0 in the back end, and Angular 1.3 in the front end.
|
10
10
|
|
11
11
|
## Installation
|
12
12
|
Add this line to your application's Gemfile:
|
@@ -299,6 +299,101 @@ $scope.message.$save(function() {
|
|
299
299
|
|
300
300
|
Note that `$valid()` and `$invalid()` should only be used after $saving a resource, i.e. in the callback of `$save`, since they don't actually invoke server side validations. They only check if a resource contains errors.
|
301
301
|
|
302
|
+
#### Associations
|
303
|
+
What if you want to only fetch and subscribe to children that belong to a specific parent? Or maybe you want to create a child in your front end and assign it to a specific parent?
|
304
|
+
|
305
|
+
Entangled currently supports one `belongs_to` association per model.
|
306
|
+
|
307
|
+
For example, imagine the following Parent > Children relationship in your models:
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
class Parent < ActiveRecord::Base
|
311
|
+
include Entangled::Model
|
312
|
+
entangle
|
313
|
+
|
314
|
+
has_many :children
|
315
|
+
end
|
316
|
+
|
317
|
+
class Child < ActiveRecord::Base
|
318
|
+
include Entangled::Model
|
319
|
+
entangle
|
320
|
+
|
321
|
+
belongs_to :parent
|
322
|
+
end
|
323
|
+
```
|
324
|
+
|
325
|
+
To reflect this in your front end, you just need to add three things to your app:
|
326
|
+
|
327
|
+
- Nest your routes so that they resemble the parent/child relationship:
|
328
|
+
|
329
|
+
```ruby
|
330
|
+
sockets_for :parents do
|
331
|
+
sockets_for :children
|
332
|
+
end
|
333
|
+
```
|
334
|
+
|
335
|
+
- Adjust the `index` and `create` actions in your `ChildrenController` so that they look like this:
|
336
|
+
|
337
|
+
```ruby
|
338
|
+
class ChildrenController < ApplicationController
|
339
|
+
include Entangled::Controller
|
340
|
+
|
341
|
+
# Fetch children of specific parent
|
342
|
+
def index
|
343
|
+
broadcast do
|
344
|
+
@children = Parent.find(params[:parent_id]).children
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Create child of specific parent
|
349
|
+
def create
|
350
|
+
broadcast do
|
351
|
+
@child = Parent.find(params[:parent_id]).children.create(child_params)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# show, update and destroy don't need to be nested
|
356
|
+
end
|
357
|
+
```
|
358
|
+
|
359
|
+
- Lastly, inform your Angular parent service about the association:
|
360
|
+
|
361
|
+
```javascript
|
362
|
+
app.factory('Parent', function(Entangled) {
|
363
|
+
// Instantiate Entangled service
|
364
|
+
var entangled = new Entangled('ws://localhost:3000/parents');
|
365
|
+
|
366
|
+
// Set up association
|
367
|
+
entangled.hasMany('children');
|
368
|
+
|
369
|
+
return entangled;
|
370
|
+
});
|
371
|
+
```
|
372
|
+
|
373
|
+
This makes a `children()` function available on your parent records on which you can chain all other functions to fetch/manipulate data:
|
374
|
+
|
375
|
+
```javascript
|
376
|
+
Parent.find(1, function(parent) {
|
377
|
+
parent.children().all(function(children) {
|
378
|
+
// children here all belong to parent with id 1
|
379
|
+
});
|
380
|
+
|
381
|
+
parent.children().find(1, function(child) {
|
382
|
+
// child has id 1 and belongs to parent with id 1
|
383
|
+
});
|
384
|
+
|
385
|
+
parent.children().create({ foo: 'bar' }, function(child) {
|
386
|
+
// child has been persisted and associated with parent
|
387
|
+
});
|
388
|
+
|
389
|
+
// etc
|
390
|
+
});
|
391
|
+
```
|
392
|
+
|
393
|
+
This is the way to go if you want to fetch records that only belong to a certain record, or create records that should belong to a parent record. In short, it is ideal to scope records to parent records.
|
394
|
+
|
395
|
+
Naturally, all nested records are also synced in real time across all connected clients.
|
396
|
+
|
302
397
|
#### Persistence
|
303
398
|
Just as with ActiveRecord's `persisted?` method, you can use `$persisted()` on an object to check if it was successfully stored in the database.
|
304
399
|
|
@@ -319,7 +414,6 @@ The following features are to be implemented next:
|
|
319
414
|
- Offline capabilities - when client is disconnected, put websocket interactions in a queue and dequeue all once connected again
|
320
415
|
- Support for authentication
|
321
416
|
- Support for associations
|
322
|
-
- Remove angular dependencies from bower package (they're currently all being downloaded as well when doing bower install)
|
323
417
|
- On Heroku (maybe in production in general), objects are always in different order depending on their attributes
|
324
418
|
- Add $onChange listener to objects
|
325
419
|
- Add diagram on how it works to Readme
|
data/entangled.gemspec
CHANGED
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.add_development_dependency 'bundler', '~> 1.7'
|
22
22
|
s.add_development_dependency 'rake', '~> 10.0'
|
23
23
|
s.add_development_dependency 'rspec-rails', '~> 3.2'
|
24
|
+
s.add_development_dependency 'shoulda-matchers', '~> 2.6'
|
24
25
|
s.add_development_dependency 'sqlite3', '~> 1.3'
|
25
26
|
s.add_development_dependency 'byebug', '~> 3.5'
|
26
27
|
s.add_development_dependency 'bourne', '~> 1.6'
|
data/entangled.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
angular.module("entangled",[]).factory("Entangled",function(){var e=function(e,r){for(var
|
1
|
+
angular.module("entangled",[]).factory("Entangled",function(){var e=function(e,t,r){for(var o in e)e.hasOwnProperty(o)&&(this[o]=e[o]);this.webSocketUrl=t,r&&(this[r]=function(){return new s(this.webSocketUrl+"/"+this.id+"/"+r)})};e.prototype.$save=function(e){if(this.id){var t=new WebSocket(this.webSocketUrl+"/"+this.id+"/update");t.onopen=function(){t.send(JSON.stringify(this))}.bind(this),t.onmessage=function(t){if(t.data){var r=JSON.parse(t.data);if(r.resource)for(key in r.resource)this[key]=r.resource[key]}this[this.hasMany]=new s(this.webSocketUrl+"/"+this.id+"/"+this.hasMany),e&&e(this)}.bind(this)}else{var t=new WebSocket(this.webSocketUrl+"/create");t.onopen=function(){t.send(JSON.stringify(this))}.bind(this),t.onmessage=function(t){if(t.data){var s=JSON.parse(t.data);if(s.resource)for(key in s.resource)this[key]=s.resource[key]}e&&e(this)}.bind(this)}},e.prototype.$update=function(e,t){for(var s in e)e.hasOwnProperty(s)&&(this[s]=e[s]);this.$save(t)},e.prototype.$destroy=function(e){var t=new WebSocket(this.webSocketUrl+"/"+this.id+"/destroy");t.onopen=function(){t.send(null),e&&e()}},e.prototype.$valid=function(){return!(this.errors&&Object.keys(this.errors).length)},e.prototype.$invalid=function(){return!this.$valid()},e.prototype.$persisted=function(){return!!this.id};var t=function(t,s,r){this.all=[];for(var o=0;o<t.length;o++){var i=new e(t[o],s,r);this.all.push(i)}},s=function(e){this.webSocketUrl=e};return s.prototype.hasMany=function(e){this.hasMany=e},s.prototype["new"]=function(t){return new e(t,this.webSocketUrl,this.hasMany)},s.prototype.all=function(s){var r=new WebSocket(this.webSocketUrl);r.onmessage=function(o){if(o.data.length){var i=JSON.parse(o.data);if(i.resources)this.resources=new t(i.resources,r.url,this.hasMany);else if(i.action)if("create"===i.action)this.resources.all.push(new e(i.resource,r.url,this.hasMany));else if("update"===i.action){for(var n,a=0;a<this.resources.all.length;a++)this.resources.all[a].id===i.resource.id&&(n=a);this.resources.all[n]=new e(i.resource,r.url,this.hasMany)}else if("destroy"===i.action){for(var n,a=0;a<this.resources.all.length;a++)this.resources.all[a].id===i.resource.id&&(n=a);this.resources.all.splice(n,1)}else console.log("Something else other than CRUD happened..."),console.log(i)}s(this.resources.all)}.bind(this)},s.prototype.create=function(e,t){var s=this["new"](e);s.$save(t)},s.prototype.find=function(t,s){var r=this.webSocketUrl,o=new WebSocket(r+"/"+t);o.onmessage=function(t){if(t.data.length){var o=JSON.parse(t.data);o.resource&&!o.action?this.resource=new e(o.resource,r,this.hasMany):o.action?"update"===o.action?this.resource=new e(o.resource,r,this.hasMany):"destroy"===o.action&&(this.resource=void 0):(console.log("Something else other than CRUD happened..."),console.log(o))}s(this.resource)}.bind(this)},s});
|
data/lib/entangled/controller.rb
CHANGED
@@ -42,15 +42,9 @@ module Entangled
|
|
42
42
|
instance_variable_get(:"@#{resource_name}")
|
43
43
|
end
|
44
44
|
|
45
|
-
#
|
46
|
-
def
|
47
|
-
|
48
|
-
end
|
49
|
-
|
50
|
-
# Channel name for collection of resources, used in index
|
51
|
-
# action
|
52
|
-
def collection_channel
|
53
|
-
model.channel
|
45
|
+
# Infer channel from current path
|
46
|
+
def channel
|
47
|
+
request.path
|
54
48
|
end
|
55
49
|
|
56
50
|
# Close the connection to the DB so as to
|
@@ -94,7 +88,7 @@ module Entangled
|
|
94
88
|
# has to be "@tacos"
|
95
89
|
if collection
|
96
90
|
redis_thread = Thread.new do
|
97
|
-
redis.subscribe
|
91
|
+
redis.subscribe channel do |on|
|
98
92
|
# Broadcast messages to all connected clients
|
99
93
|
on.message do |channel, message|
|
100
94
|
tubesock.send_data message
|
@@ -135,7 +129,7 @@ module Entangled
|
|
135
129
|
# The variable name, in this example, has to be "@taco"
|
136
130
|
if member
|
137
131
|
redis_thread = Thread.new do
|
138
|
-
redis.subscribe
|
132
|
+
redis.subscribe channel do |on|
|
139
133
|
# Broadcast messages to all connected clients
|
140
134
|
on.message do |channel, message|
|
141
135
|
tubesock.send_data message
|
data/lib/entangled/model.rb
CHANGED
@@ -55,12 +55,6 @@ module Entangled
|
|
55
55
|
[:create, :update, :destroy]
|
56
56
|
end
|
57
57
|
|
58
|
-
# The inferred channel name. For example, if the class name
|
59
|
-
# is DeliciousTaco, the inferred channel name is "delicious_tacos"
|
60
|
-
def channel
|
61
|
-
name.underscore.pluralize
|
62
|
-
end
|
63
|
-
|
64
58
|
# Creates callbacks in the extented model
|
65
59
|
def create_hook(name)
|
66
60
|
send :"after_#{name}", -> { publish(name) }
|
@@ -79,13 +73,40 @@ module Entangled
|
|
79
73
|
attributes.merge(errors: errors).as_json
|
80
74
|
end
|
81
75
|
|
82
|
-
# The
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
#
|
87
|
-
|
88
|
-
|
76
|
+
# The channel name for a single record containing the
|
77
|
+
# inferred channel name from the class and the record's
|
78
|
+
# id. For example, if it's a DeliciousTaco with the id 1,
|
79
|
+
# the member channel for the single record is "delicious_tacos/1".
|
80
|
+
# Nesting is automatically applied through the use of
|
81
|
+
# the collection channel.
|
82
|
+
#
|
83
|
+
# The member channel has to be the same as the path to
|
84
|
+
# the resource's show action, including a leading
|
85
|
+
# forward slash
|
86
|
+
def member_channel
|
87
|
+
"#{collection_channel}/#{self.to_param}"
|
88
|
+
end
|
89
|
+
|
90
|
+
# The inferred channel name for the collection. For example,
|
91
|
+
# if the class name is DeliciousTaco, the collection channel
|
92
|
+
# is "delicious_tacos".
|
93
|
+
#
|
94
|
+
# If the model belongs to another model, the channel is nested
|
95
|
+
# accordingly. For example, if a child belongs to a parent,
|
96
|
+
# the child's collection channel is "parents/1/children".
|
97
|
+
#
|
98
|
+
# The collection channel has to be the same as the path to
|
99
|
+
# the resource's index action, including a leading forward slash
|
100
|
+
def collection_channel
|
101
|
+
belongs_to_assocations = self.class.reflect_on_all_associations(:belongs_to)
|
102
|
+
own_channel = self.class.name.underscore.pluralize
|
103
|
+
|
104
|
+
if belongs_to_assocations.any?
|
105
|
+
parent = send(belongs_to_assocations.first.name)
|
106
|
+
"#{parent.member_channel}/#{own_channel}"
|
107
|
+
else
|
108
|
+
"/#{own_channel}"
|
109
|
+
end
|
89
110
|
end
|
90
111
|
|
91
112
|
private
|
@@ -96,13 +117,13 @@ module Entangled
|
|
96
117
|
def publish(action)
|
97
118
|
# Publish to model's channel
|
98
119
|
redis.publish(
|
99
|
-
|
120
|
+
collection_channel,
|
100
121
|
json(action)
|
101
122
|
)
|
102
123
|
|
103
124
|
# Publish to record#s channel
|
104
125
|
redis.publish(
|
105
|
-
|
126
|
+
member_channel,
|
106
127
|
json(action)
|
107
128
|
)
|
108
129
|
end
|
data/lib/entangled/version.rb
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
class ItemsController < ApplicationController
|
2
|
+
include Entangled::Controller
|
3
|
+
|
4
|
+
def index
|
5
|
+
broadcast do
|
6
|
+
@items = List.find(params[:list_id]).items
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def create
|
11
|
+
broadcast do
|
12
|
+
@item = List.find(params[:list_id]).items.create(item_params)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def show
|
17
|
+
broadcast do
|
18
|
+
@item = Item.find(params[:id])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def update
|
23
|
+
broadcast do
|
24
|
+
@item = Item.find(params[:id])
|
25
|
+
@item.update(item_params)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def destroy
|
30
|
+
broadcast do
|
31
|
+
Item.find(params[:id]).destroy
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def item_params
|
37
|
+
params.require(:item).permit(:name, :complete)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class ListsController < ApplicationController
|
2
|
+
include Entangled::Controller
|
3
|
+
|
4
|
+
def index
|
5
|
+
broadcast do
|
6
|
+
@lists = List.all
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def create
|
11
|
+
broadcast do
|
12
|
+
@list = List.create(list_params)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def show
|
17
|
+
broadcast do
|
18
|
+
@list = List.find(params[:id])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def destroy
|
23
|
+
broadcast do
|
24
|
+
List.find(params[:id]).destroy
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def list_params
|
30
|
+
params.require(:list).permit(:name)
|
31
|
+
end
|
32
|
+
end
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -11,7 +11,7 @@
|
|
11
11
|
#
|
12
12
|
# It's strongly recommended that you check this file into your version control system.
|
13
13
|
|
14
|
-
ActiveRecord::Schema.define(version:
|
14
|
+
ActiveRecord::Schema.define(version: 20150314055847) do
|
15
15
|
|
16
16
|
create_table "barfoos", force: :cascade do |t|
|
17
17
|
t.text "body"
|
@@ -37,6 +37,20 @@ ActiveRecord::Schema.define(version: 20150223004852) do
|
|
37
37
|
t.datetime "updated_at", null: false
|
38
38
|
end
|
39
39
|
|
40
|
+
create_table "items", force: :cascade do |t|
|
41
|
+
t.string "name"
|
42
|
+
t.boolean "complete", default: false
|
43
|
+
t.integer "list_id"
|
44
|
+
t.datetime "created_at", null: false
|
45
|
+
t.datetime "updated_at", null: false
|
46
|
+
end
|
47
|
+
|
48
|
+
create_table "lists", force: :cascade do |t|
|
49
|
+
t.string "name"
|
50
|
+
t.datetime "created_at", null: false
|
51
|
+
t.datetime "updated_at", null: false
|
52
|
+
end
|
53
|
+
|
40
54
|
create_table "messages", force: :cascade do |t|
|
41
55
|
t.text "body"
|
42
56
|
t.datetime "created_at", null: false
|
@@ -10,10 +10,19 @@ angular.module('entangledTest', [
|
|
10
10
|
.when('/', {
|
11
11
|
templateUrl: 'views/messages/index.html',
|
12
12
|
controller: 'MessagesCtrl'
|
13
|
-
})
|
13
|
+
})
|
14
|
+
.when('/messages/:id', {
|
14
15
|
templateUrl: 'views/messages/show.html',
|
15
16
|
controller: 'MessageCtrl'
|
16
17
|
})
|
18
|
+
.when('/lists', {
|
19
|
+
templateUrl: 'views/lists/index.html',
|
20
|
+
controller: 'ListsCtrl'
|
21
|
+
})
|
22
|
+
.when('/lists/:id', {
|
23
|
+
templateUrl: 'views/lists/show.html',
|
24
|
+
controller: 'ListCtrl'
|
25
|
+
})
|
17
26
|
.otherwise({
|
18
27
|
redirectTo: '/'
|
19
28
|
});
|
@@ -0,0 +1,16 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
angular.module('entangledTest')
|
4
|
+
|
5
|
+
.controller('ListCtrl', function($scope, $routeParams, List) {
|
6
|
+
List.find($routeParams.id, function(list) {
|
7
|
+
$scope.$apply(function() {
|
8
|
+
$scope.list = list;
|
9
|
+
$scope.list.items().all(function(items) {
|
10
|
+
$scope.$apply(function() {
|
11
|
+
$scope.items = items;
|
12
|
+
});
|
13
|
+
});
|
14
|
+
});
|
15
|
+
});
|
16
|
+
});
|
@@ -0,0 +1,23 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
angular.module('entangledTest')
|
4
|
+
|
5
|
+
.controller('ListsCtrl', function($scope, List) {
|
6
|
+
$scope.list = List.new();
|
7
|
+
|
8
|
+
$scope.create = function() {
|
9
|
+
$scope.list.$save(function() {
|
10
|
+
$scope.list = List.new();
|
11
|
+
});
|
12
|
+
};
|
13
|
+
|
14
|
+
$scope.destroy = function(list) {
|
15
|
+
list.$destroy();
|
16
|
+
};
|
17
|
+
|
18
|
+
List.all(function(lists) {
|
19
|
+
$scope.$apply(function() {
|
20
|
+
$scope.lists = lists;
|
21
|
+
});
|
22
|
+
});
|
23
|
+
});
|