action_cable_notifications 0.1.14 → 0.1.18
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/app/assets/javascripts/action_cable_notifications/application.js +1 -0
- data/app/assets/javascripts/action_cable_notifications/cable_notifications.coffee +2 -2
- data/app/assets/javascripts/action_cable_notifications/collection.coffee +90 -21
- data/app/assets/javascripts/action_cable_notifications/default_callbacks.coffee +12 -45
- data/app/assets/javascripts/action_cable_notifications/store.coffee +90 -18
- data/app/assets/javascripts/action_cable_notifications/tracker.js +633 -0
- data/lib/action_cable_notifications/channel.rb +192 -0
- data/lib/action_cable_notifications/channel_actions.rb +144 -0
- data/lib/action_cable_notifications/engine.rb +2 -2
- data/lib/action_cable_notifications/{callbacks.rb → model.rb} +22 -19
- data/lib/action_cable_notifications/version.rb +1 -1
- metadata +34 -4
- data/lib/action_cable_notifications/streams.rb +0 -59
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'action_cable_notifications/channel_actions.rb'
|
2
|
+
|
3
|
+
module ActionCableNotifications
|
4
|
+
module Channel
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
# class variables
|
9
|
+
class_attribute :ActionCableNotifications
|
10
|
+
|
11
|
+
self.ActionCableNotifications = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Public methods
|
16
|
+
################################################################
|
17
|
+
|
18
|
+
#
|
19
|
+
# Process actions sent from the client
|
20
|
+
#
|
21
|
+
# @param [Hash] data Contains command to be executed and its parameters
|
22
|
+
# {
|
23
|
+
# "collection": "model.model_name.collection"
|
24
|
+
# "command": "fetch"
|
25
|
+
# "params": {}
|
26
|
+
# }
|
27
|
+
def action(data)
|
28
|
+
data.deep_symbolize_keys!
|
29
|
+
|
30
|
+
channel_options = self.ActionCableNotifications[data[:collection]]
|
31
|
+
model = channel_options[:model]
|
32
|
+
broadcasting = channel_options[:broadcasting]
|
33
|
+
model_options = model.ActionCableNotificationsOptions[broadcasting]
|
34
|
+
|
35
|
+
params = {
|
36
|
+
model: model,
|
37
|
+
model_options: model_options,
|
38
|
+
params: data[:params]
|
39
|
+
}
|
40
|
+
|
41
|
+
case data[:command]
|
42
|
+
when "fetch"
|
43
|
+
fetch(params)
|
44
|
+
when "create"
|
45
|
+
create(params)
|
46
|
+
when "update"
|
47
|
+
update(params)
|
48
|
+
when "destroy"
|
49
|
+
destroy(params)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize(*args)
|
54
|
+
super
|
55
|
+
@collections = {}
|
56
|
+
end
|
57
|
+
|
58
|
+
def subscribed
|
59
|
+
# XXX Check if this is new connection or a reconection of a
|
60
|
+
# previously active client
|
61
|
+
end
|
62
|
+
|
63
|
+
def unsubscribed
|
64
|
+
stop_all_streams
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Private methods
|
69
|
+
################################################################
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
include ActionCableNotifications::Channel::Actions
|
74
|
+
|
75
|
+
#
|
76
|
+
# Streams notification for ActiveRecord model changes
|
77
|
+
#
|
78
|
+
# @param [ActiveRecord::Base] model Model to watch for changes
|
79
|
+
# @param [Hash] options Streaming options
|
80
|
+
#
|
81
|
+
def stream_notifications_for(model, options = {})
|
82
|
+
# Default options
|
83
|
+
options = {
|
84
|
+
}.merge(options)
|
85
|
+
|
86
|
+
# These options cannot be overridden
|
87
|
+
options[:model] = model
|
88
|
+
options[:channel] = self
|
89
|
+
options[:broadcasting] = model.model_name.collection
|
90
|
+
|
91
|
+
# Sets channel options
|
92
|
+
self.ActionCableNotifications[options[:broadcasting]] = options
|
93
|
+
|
94
|
+
# Checks if model already includes notification callbacks
|
95
|
+
if !model.respond_to? :ActionCableNotificationsOptions
|
96
|
+
model.send('include', ActionCableNotifications::Model)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Sets broadcast options if they are not already present in the model
|
100
|
+
if not model.ActionCableNotificationsOptions.key? options[:broadcasting]
|
101
|
+
model.broadcast_notifications_from options[:broadcasting], options
|
102
|
+
end
|
103
|
+
|
104
|
+
# Start streaming
|
105
|
+
stream_from options[:broadcasting], coder: ActiveSupport::JSON do |packet|
|
106
|
+
transmit_packet(packet)
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Transmits packets to connected client
|
113
|
+
#
|
114
|
+
# @param [Hash] packet Packet with changes notifications
|
115
|
+
#
|
116
|
+
def transmit_packet(packet)
|
117
|
+
packet = packet.as_json.deep_symbolize_keys!
|
118
|
+
if update_cache(packet)
|
119
|
+
transmit packet
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Updates server side cache of client side collections
|
125
|
+
# XXX compute cache diff before sending to clients
|
126
|
+
#
|
127
|
+
def update_cache(packet)
|
128
|
+
updated = false
|
129
|
+
|
130
|
+
# Check if collection already exists
|
131
|
+
new_collection = false
|
132
|
+
if @collections[packet[:collection]].nil?
|
133
|
+
@collections[packet[:collection]] = []
|
134
|
+
new_collection = true
|
135
|
+
end
|
136
|
+
|
137
|
+
collection = @collections[packet[:collection]]
|
138
|
+
|
139
|
+
case packet[:msg]
|
140
|
+
when 'upsert_many'
|
141
|
+
if new_collection
|
142
|
+
packet[:data].each do |record|
|
143
|
+
collection.push record
|
144
|
+
end
|
145
|
+
updated = true
|
146
|
+
else
|
147
|
+
packet[:data].each do |record|
|
148
|
+
current_record = collection.find{|c| c[:id]==record[:id]}
|
149
|
+
if current_record
|
150
|
+
new_record = current_record.merge(record)
|
151
|
+
if new_record != current_record
|
152
|
+
current_record.merge!(record)
|
153
|
+
updated = true
|
154
|
+
end
|
155
|
+
else
|
156
|
+
collection.push record
|
157
|
+
updated = true
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
when 'create'
|
163
|
+
record = collection.find{|c| c[:id]==packet[:id]}
|
164
|
+
if !record
|
165
|
+
@collections[packet[:collection]].push packet[:data]
|
166
|
+
updated = true
|
167
|
+
end
|
168
|
+
|
169
|
+
when 'update'
|
170
|
+
record = @collections[packet[:collection]].find{|c| c[:id]==packet[:id]}
|
171
|
+
if record
|
172
|
+
record.merge!(packet[:data])
|
173
|
+
updated = true
|
174
|
+
end
|
175
|
+
|
176
|
+
when 'destroy'
|
177
|
+
index = @collections[packet[:collection]].find_index{|c| c.id==packet[:id]}
|
178
|
+
if index
|
179
|
+
@collections[packet[:collection]].delete_at(index)
|
180
|
+
updated = true
|
181
|
+
end
|
182
|
+
|
183
|
+
else
|
184
|
+
updated = true
|
185
|
+
end
|
186
|
+
|
187
|
+
updated
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module ActionCableNotifications
|
2
|
+
module Channel
|
3
|
+
module Actions
|
4
|
+
#
|
5
|
+
# Fetch records from the DB and send them to the client
|
6
|
+
#
|
7
|
+
# @param [Hash] selector Specifies conditions that the registers should match
|
8
|
+
#
|
9
|
+
def fetch(data)
|
10
|
+
# XXX: Check if the client is allowed to call the method
|
11
|
+
|
12
|
+
params = data[:params] || {}
|
13
|
+
|
14
|
+
# Get results using provided parameters and model configured scope
|
15
|
+
results = data[:model].
|
16
|
+
select(params[:select]).
|
17
|
+
limit(params[:limit]).
|
18
|
+
where(params[:where] || {}).
|
19
|
+
scoped_collection(data[:model_options][:scope]).
|
20
|
+
to_a() rescue []
|
21
|
+
|
22
|
+
response = { collection: data[:model].model_name.collection,
|
23
|
+
msg: 'upsert_many',
|
24
|
+
data: results
|
25
|
+
}
|
26
|
+
|
27
|
+
# Send data to the client
|
28
|
+
transmit_packet response
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Creates one record in the DB
|
33
|
+
#
|
34
|
+
def create(data)
|
35
|
+
# XXX: Check if the client is allowed to call the method
|
36
|
+
|
37
|
+
params = data[:params] || {}
|
38
|
+
fields = params[:fields].except(:id)
|
39
|
+
|
40
|
+
error = nil
|
41
|
+
|
42
|
+
if fields.present?
|
43
|
+
begin
|
44
|
+
record = data[:model].create(fields)
|
45
|
+
|
46
|
+
if !record.persisted?
|
47
|
+
error = true
|
48
|
+
end
|
49
|
+
rescue Exception => e
|
50
|
+
error = e.message
|
51
|
+
end
|
52
|
+
else
|
53
|
+
error = "No fields were provided"
|
54
|
+
end
|
55
|
+
|
56
|
+
if error
|
57
|
+
response = {
|
58
|
+
collection: data[:model].model_name.collection,
|
59
|
+
msg: 'error',
|
60
|
+
cmd: 'create',
|
61
|
+
error: error || record.errors.full_messages
|
62
|
+
}
|
63
|
+
|
64
|
+
# Send error notification to the client
|
65
|
+
transmit response
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
#
|
72
|
+
# Update one record from the DB
|
73
|
+
#
|
74
|
+
def update(data)
|
75
|
+
# XXX: Check if the client is allowed to call the method
|
76
|
+
|
77
|
+
params = data[:params] || {}
|
78
|
+
|
79
|
+
record = data[:model].find(params[:id]) rescue nil
|
80
|
+
|
81
|
+
error = nil
|
82
|
+
|
83
|
+
if record.present?
|
84
|
+
begin
|
85
|
+
record.update_attributes(params[:fields])
|
86
|
+
rescue Exception => e
|
87
|
+
error = e.message
|
88
|
+
end
|
89
|
+
else
|
90
|
+
error = "There is no record with id: #{params[:id]}"
|
91
|
+
end
|
92
|
+
|
93
|
+
if error
|
94
|
+
response = {
|
95
|
+
collection: data[:model].model_name.collection,
|
96
|
+
msg: 'error',
|
97
|
+
cmd: 'update',
|
98
|
+
error: error || record.errors.full_messages
|
99
|
+
}
|
100
|
+
|
101
|
+
# Send error notification to the client
|
102
|
+
transmit response
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Remove records from the DB
|
109
|
+
#
|
110
|
+
def destroy(data)
|
111
|
+
# XXX: Check if the client is allowed to call the method
|
112
|
+
|
113
|
+
params = data[:params] || {}
|
114
|
+
|
115
|
+
record = data[:model].find(params[:id]) rescue nil
|
116
|
+
|
117
|
+
error = nil
|
118
|
+
|
119
|
+
if record.present?
|
120
|
+
begin
|
121
|
+
record.destroy
|
122
|
+
rescue Exception => e
|
123
|
+
error = e.message
|
124
|
+
end
|
125
|
+
else
|
126
|
+
error = "There is no record with id: #{params[:id]}"
|
127
|
+
end
|
128
|
+
|
129
|
+
if error
|
130
|
+
response = { collection: data[:model].model_name.collection,
|
131
|
+
msg: 'error',
|
132
|
+
cmd: 'destroy',
|
133
|
+
error: error || record.errors.full_messages
|
134
|
+
}
|
135
|
+
|
136
|
+
# Send error notification to the client
|
137
|
+
transmit response
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'lodash-rails'
|
2
|
-
require 'action_cable_notifications/
|
3
|
-
require 'action_cable_notifications/
|
2
|
+
require 'action_cable_notifications/model.rb'
|
3
|
+
require 'action_cable_notifications/channel.rb'
|
4
4
|
|
5
5
|
module ActionCableNotifications
|
6
6
|
class Engine < ::Rails::Engine
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module ActionCableNotifications
|
2
|
-
module
|
2
|
+
module Model
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
@@ -10,7 +10,7 @@ module ActionCableNotifications
|
|
10
10
|
# Register Callbacks
|
11
11
|
after_update :notify_update
|
12
12
|
after_create :notify_create
|
13
|
-
|
13
|
+
after_destroy :notify_destroy
|
14
14
|
end
|
15
15
|
|
16
16
|
module ClassMethods
|
@@ -40,7 +40,8 @@ module ActionCableNotifications
|
|
40
40
|
# @return [ActiveRecordRelation] Results fetched from the database
|
41
41
|
#
|
42
42
|
def scoped_collection ( scope = :all )
|
43
|
-
|
43
|
+
scope = scope.to_a if scope.is_a? Hash
|
44
|
+
Array(scope).inject(self) {|o, a| o.try(*a)} rescue nil
|
44
45
|
end
|
45
46
|
|
46
47
|
#
|
@@ -59,7 +60,7 @@ module ActionCableNotifications
|
|
59
60
|
if options.present?
|
60
61
|
{
|
61
62
|
collection: self.model_name.collection,
|
62
|
-
msg: '
|
63
|
+
msg: 'upsert_many',
|
63
64
|
data: self.scoped_collection(options[:scope])
|
64
65
|
}
|
65
66
|
end
|
@@ -94,21 +95,23 @@ module ActionCableNotifications
|
|
94
95
|
changes[k] = v[1]
|
95
96
|
end
|
96
97
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
98
|
+
if !changes.empty?
|
99
|
+
self.ActionCableNotificationsOptions.each do |broadcasting, options|
|
100
|
+
if options[:actions].include? :update
|
101
|
+
# Checks if record is within scope before broadcasting
|
102
|
+
if self.class.scoped_collection(options[:scope]).where(id: self.id)
|
103
|
+
# XXX: Performance required. For small data sets this should be
|
104
|
+
# fast enough, but for large data sets this could be slow. As
|
105
|
+
# clients should have a limited subset of the dataset loaded at a
|
106
|
+
# time, caching the results already sent to clients in server memory
|
107
|
+
# should not have a big impact in memory usage but can improve
|
108
|
+
# performance for large data sets where only a sub
|
109
|
+
ActionCable.server.broadcast broadcasting,
|
110
|
+
collection: self.model_name.collection,
|
111
|
+
msg: 'update',
|
112
|
+
id: self.id,
|
113
|
+
data: changes
|
114
|
+
end
|
112
115
|
end
|
113
116
|
end
|
114
117
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_cable_notifications
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.18
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ByS Sistemas de Control
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-10-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -58,6 +58,34 @@ dependencies:
|
|
58
58
|
- - ">="
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: pry
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: pry-byebug
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
61
89
|
description: Rails engine that provides Automatic realtime notification broadcast
|
62
90
|
for ActiveRecord models changes using Action Cable and websockets
|
63
91
|
email:
|
@@ -76,6 +104,7 @@ files:
|
|
76
104
|
- app/assets/javascripts/action_cable_notifications/collection.coffee
|
77
105
|
- app/assets/javascripts/action_cable_notifications/default_callbacks.coffee
|
78
106
|
- app/assets/javascripts/action_cable_notifications/store.coffee
|
107
|
+
- app/assets/javascripts/action_cable_notifications/tracker.js
|
79
108
|
- app/assets/stylesheets/action_cable_notifications/application.css
|
80
109
|
- app/controllers/action_cable_notifications/application_controller.rb
|
81
110
|
- app/helpers/action_cable_notifications/application_helper.rb
|
@@ -85,9 +114,10 @@ files:
|
|
85
114
|
- app/views/layouts/action_cable_notifications/application.html.erb
|
86
115
|
- config/routes.rb
|
87
116
|
- lib/action_cable_notifications.rb
|
88
|
-
- lib/action_cable_notifications/
|
117
|
+
- lib/action_cable_notifications/channel.rb
|
118
|
+
- lib/action_cable_notifications/channel_actions.rb
|
89
119
|
- lib/action_cable_notifications/engine.rb
|
90
|
-
- lib/action_cable_notifications/
|
120
|
+
- lib/action_cable_notifications/model.rb
|
91
121
|
- lib/action_cable_notifications/version.rb
|
92
122
|
- lib/tasks/action_cable_notifications_tasks.rake
|
93
123
|
homepage: https://github.com/bys-control/action_cable_notifications
|