entangled 0.0.20 → 0.0.21
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 +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
|
+
});
|