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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 47e523fb330c5b6afaf2a591a5b182cb0f7bb6cd
4
- data.tar.gz: c2f08b77e3c57ba6f7ac7141b52f7bb4c2cfd286
3
+ metadata.gz: a57bb2d361ca6fc01d21e41081913491ec6d3f96
4
+ data.tar.gz: 8e76dffbad36c6d4f5ce3189852c054d919bbe36
5
5
  SHA512:
6
- metadata.gz: c31e87f3e09508d7c56361af3e828f84da4c0156b7ad9763fe3d888dfc2038c5ef3d1df2dbdbf473f6173707f44625d8da37dbaba59ea6e7d69d3fc2992ae85b
7
- data.tar.gz: 102f1d094bd646896aade1752327b3edbe0eaea578b1e904572e6ad206d7e701acd369b74950630fc38308a7fc0ab828c4351b4e3f66e1fcc160849d780982e2
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
@@ -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'
@@ -1 +1 @@
1
- angular.module("entangled",[]).factory("Entangled",function(){var e=function(e,r){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);this.webSocketUrl=r};e.prototype.$save=function(e){var r=this;if(this.id){var t=new WebSocket(r.webSocketUrl+"/"+r.id+"/update");t.onopen=function(){t.send(JSON.stringify(r))},t.onmessage=function(t){if(t.data){var o=JSON.parse(t.data);if(o.resource)for(key in o.resource)r[key]=o.resource[key]}e&&e(r)}}else{var t=new WebSocket(r.webSocketUrl+"/create");t.onopen=function(){t.send(JSON.stringify(r))},t.onmessage=function(t){if(t.data){var o=JSON.parse(t.data);if(o.resource)for(key in o.resource)r[key]=o.resource[key]}e&&e(r)}}},e.prototype.$update=function(e,r){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);this.$save(r)},e.prototype.$destroy=function(e){var r=new WebSocket(this.webSocketUrl+"/"+this.id+"/destroy");r.onopen=function(){r.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 r=function(r,t){this.all=[];for(var o=0;o<r.length;o++){var s=new e(r[o],t);this.all.push(s)}},t=function(e){this.webSocketUrl=e};return t.prototype["new"]=function(r){return new e(r,this.webSocketUrl)},t.prototype.all=function(t){var o=new WebSocket(this.webSocketUrl);o.onmessage=function(s){if(s.data.length){var n=JSON.parse(s.data);if(n.resources)this.resources=new r(n.resources,o.url);else if(n.action)if("create"===n.action)this.resources.all.push(new e(n.resource,o.url));else if("update"===n.action){for(var i,a=0;a<this.resources.all.length;a++)this.resources.all[a].id===n.resource.id&&(i=a);this.resources.all[i]=new e(n.resource,o.url)}else if("destroy"===n.action){for(var i,a=0;a<this.resources.all.length;a++)this.resources.all[a].id===n.resource.id&&(i=a);this.resources.all.splice(i,1)}else console.log("Something else other than CRUD happened..."),console.log(n)}t(this.resources.all)}},t.prototype.create=function(e,r){var t=this["new"](e);t.$save(r)},t.prototype.find=function(r,t){var o=this.webSocketUrl,s=new WebSocket(o+"/"+r);s.onmessage=function(r){if(r.data.length){var s=JSON.parse(r.data);s.resource&&!s.action?this.resource=new e(s.resource,o):s.action?"update"===s.action?this.resource=new e(s.resource,o):"destroy"===s.action&&(this.resource=void 0):(console.log("Something else other than CRUD happened..."),console.log(s))}t(this.resource)}},t});
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});
@@ -42,15 +42,9 @@ module Entangled
42
42
  instance_variable_get(:"@#{resource_name}")
43
43
  end
44
44
 
45
- # Channel name for single resource, used in show action
46
- def member_channel
47
- member.channel
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 collection_channel do |on|
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 member_channel do |on|
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
@@ -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 inferred channel name for a single record
83
- # containing the inferred channel name from the class
84
- # and the record's id. For example, if it's a
85
- # DeliciousTaco with the id 1, the inferred channel
86
- # name for the single record is "delicious_tacos/1"
87
- def channel
88
- "#{self.class.channel}/#{self.to_param}"
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
- self.class.channel,
120
+ collection_channel,
100
121
  json(action)
101
122
  )
102
123
 
103
124
  # Publish to record#s channel
104
125
  redis.publish(
105
- channel,
126
+ member_channel,
106
127
  json(action)
107
128
  )
108
129
  end
@@ -1,3 +1,3 @@
1
1
  module Entangled
2
- VERSION = "0.0.20"
2
+ VERSION = "0.0.21"
3
3
  end
@@ -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
@@ -0,0 +1,6 @@
1
+ class Item < ActiveRecord::Base
2
+ include Entangled::Model
3
+ entangle
4
+
5
+ belongs_to :list, required: true
6
+ end
@@ -0,0 +1,6 @@
1
+ class List < ActiveRecord::Base
2
+ include Entangled::Model
3
+ entangle
4
+
5
+ has_many :items, dependent: :destroy
6
+ end
@@ -0,0 +1,9 @@
1
+ class CreateLists < ActiveRecord::Migration
2
+ def change
3
+ create_table :lists do |t|
4
+ t.string :name
5
+
6
+ t.timestamps null: false
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ class CreateItems < ActiveRecord::Migration
2
+ def change
3
+ create_table :items do |t|
4
+ t.string :name
5
+ t.boolean :complete, default: false
6
+ t.integer :list_id
7
+
8
+ t.timestamps null: false
9
+ end
10
+ end
11
+ end
@@ -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: 20150223004852) do
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
- }).when('/messages/:id', {
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
+ });