jason-rails 0.5.0 → 0.6.3
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/.gitignore +4 -1
- data/Gemfile.lock +1 -1
- data/README.md +141 -5
- data/app/controllers/jason/api/pusher_controller.rb +15 -0
- data/app/controllers/jason/api_controller.rb +46 -4
- data/client/lib/JasonContext.d.ts +1 -1
- data/client/lib/JasonContext.js +4 -1
- data/client/lib/JasonProvider.js +1 -1
- data/client/lib/createJasonReducers.js +7 -0
- data/client/lib/createPayloadHandler.d.ts +6 -3
- data/client/lib/createPayloadHandler.js +8 -4
- data/client/lib/createTransportAdapter.d.ts +5 -0
- data/client/lib/createTransportAdapter.js +20 -0
- data/client/lib/index.d.ts +2 -0
- data/client/lib/index.js +3 -1
- data/client/lib/makeEager.js +2 -2
- data/client/lib/pruneIdsMiddleware.js +9 -11
- data/client/lib/restClient.d.ts +1 -1
- data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
- data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
- data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
- data/client/lib/transportAdapters/pusherAdapter.js +68 -0
- data/client/lib/useEager.d.ts +1 -0
- data/client/lib/useEager.js +12 -0
- data/client/lib/useJason.js +30 -35
- data/client/lib/useJason.test.js +8 -2
- data/client/lib/useSub.d.ts +1 -1
- data/client/lib/useSub.js +5 -3
- data/client/package.json +2 -1
- data/client/src/JasonContext.ts +4 -1
- data/client/src/JasonProvider.tsx +1 -1
- data/client/src/createJasonReducers.ts +7 -0
- data/client/src/createPayloadHandler.ts +9 -4
- data/client/src/createTransportAdapter.ts +13 -0
- data/client/src/index.ts +3 -1
- data/client/src/makeEager.ts +2 -2
- data/client/src/pruneIdsMiddleware.ts +11 -11
- data/client/src/restClient.ts +2 -1
- data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
- data/client/src/transportAdapters/pusherAdapter.ts +72 -0
- data/client/src/useEager.ts +9 -0
- data/client/src/useJason.test.ts +8 -2
- data/client/src/useJason.ts +31 -36
- data/client/src/useSub.ts +5 -3
- data/client/yarn.lock +12 -0
- data/config/routes.rb +5 -1
- data/lib/jason.rb +56 -8
- data/lib/jason/broadcaster.rb +19 -0
- data/lib/jason/channel.rb +10 -3
- data/lib/jason/graph_helper.rb +165 -0
- data/lib/jason/includes_helper.rb +108 -0
- data/lib/jason/lua_generator.rb +23 -1
- data/lib/jason/publisher.rb +21 -17
- data/lib/jason/subscription.rb +208 -179
- data/lib/jason/version.rb +1 -1
- metadata +18 -2
@@ -0,0 +1,19 @@
|
|
1
|
+
class Jason::Broadcaster
|
2
|
+
attr_reader :channel
|
3
|
+
|
4
|
+
def initialize(channel)
|
5
|
+
@channel = channel
|
6
|
+
end
|
7
|
+
|
8
|
+
def pusher_channel_name
|
9
|
+
"private-#{Jason.pusher_channel_prefix}-#{channel}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def broadcast(message)
|
13
|
+
if Jason.transport_service == :action_cable
|
14
|
+
ActionCable.server.broadcast(channel, message)
|
15
|
+
elsif Jason.transport_service == :pusher
|
16
|
+
Jason.pusher.trigger(pusher_channel_name, 'changed', message)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/jason/channel.rb
CHANGED
@@ -21,7 +21,7 @@ class Jason::Channel < ActionCable::Channel::Base
|
|
21
21
|
elsif (config = message['removeSubscription'])
|
22
22
|
remove_subscription(config)
|
23
23
|
elsif (config = message['getPayload'])
|
24
|
-
get_payload(config)
|
24
|
+
get_payload(config, message['forceRefresh'])
|
25
25
|
end
|
26
26
|
rescue => e
|
27
27
|
puts e.message
|
@@ -32,6 +32,8 @@ class Jason::Channel < ActionCable::Channel::Base
|
|
32
32
|
|
33
33
|
def create_subscription(model, conditions, includes)
|
34
34
|
subscription = Jason::Subscription.upsert_by_config(model, conditions: conditions || {}, includes: includes || nil)
|
35
|
+
|
36
|
+
return if !subscription.user_can_access?(current_user)
|
35
37
|
stream_from subscription.channel
|
36
38
|
|
37
39
|
subscriptions.push(subscription)
|
@@ -50,9 +52,14 @@ class Jason::Channel < ActionCable::Channel::Base
|
|
50
52
|
# TODO Stop streams
|
51
53
|
end
|
52
54
|
|
53
|
-
def get_payload(config)
|
55
|
+
def get_payload(config, force_refresh = false)
|
54
56
|
subscription = Jason::Subscription.upsert_by_config(config['model'], conditions: config['conditions'], includes: config['includes'])
|
55
|
-
|
57
|
+
|
58
|
+
return if !subscription.user_can_access?(current_user)
|
59
|
+
if force_refresh
|
60
|
+
subscription.set_ids_for_sub_models
|
61
|
+
end
|
62
|
+
subscription.get.each do |model_name, payload|
|
56
63
|
transmit(payload) if payload.present?
|
57
64
|
end
|
58
65
|
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
class Jason::GraphHelper
|
2
|
+
attr_reader :id, :includes_helper
|
3
|
+
|
4
|
+
def initialize(id, includes_helper)
|
5
|
+
@id = id
|
6
|
+
@includes_helper = includes_helper
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_edge(parent_model, parent_id, child_model, child_id)
|
10
|
+
edge = "#{parent_model}:#{parent_id}/#{child_model}:#{child_id}"
|
11
|
+
$redis_jason.sadd("jason:subscriptions:#{id}:graph", edge)
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove_edge(parent_model, parent_id, child_model, child_id)
|
15
|
+
edge = "#{parent_model}:#{parent_id}/#{child_model}:#{child_id}"
|
16
|
+
$redis_jason.srem("jason:subscriptions:#{id}:graph", edge)
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_edges(all_models, instance_ids)
|
20
|
+
edges = build_edges(all_models, instance_ids)
|
21
|
+
$redis_jason.sadd("jason:subscriptions:#{id}:graph", edges)
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove_edges(all_models, instance_ids)
|
25
|
+
edges = build_edges(all_models, instance_ids)
|
26
|
+
$redis_jason.srem("jason:subscriptions:#{id}:graph", edges)
|
27
|
+
end
|
28
|
+
|
29
|
+
def apply_remove_node(node)
|
30
|
+
edges = $redis_jason.smembers("jason:subscriptions:#{id}:graph")
|
31
|
+
edges = find_edges_with_node(edges, node)
|
32
|
+
diff_edges_from_graph(remove_edges: edges)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Add and remove edges, return graph before and after
|
36
|
+
def apply_update(add: nil, remove: nil)
|
37
|
+
add_edges = []
|
38
|
+
remove_edges = []
|
39
|
+
|
40
|
+
if add.present?
|
41
|
+
add.each do |edge_set|
|
42
|
+
add_edges += build_edges(edge_set[:model_names], edge_set[:instance_ids])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
if remove.present?
|
47
|
+
remove.each do |edge_set|
|
48
|
+
remove_edges += build_edges(edge_set[:model_names], edge_set[:instance_ids], include_root: false)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
diff_edges_from_graph(add_edges: add_edges, remove_edges: remove_edges)
|
52
|
+
end
|
53
|
+
|
54
|
+
def diff_edges_from_graph(add_edges: [], remove_edges: [])
|
55
|
+
old_edges, new_edges = Jason::LuaGenerator.new.update_set_with_diff("jason:subscriptions:#{id}:graph", add_edges.flatten, remove_edges.flatten)
|
56
|
+
|
57
|
+
old_graph = build_graph_from_edges(old_edges)
|
58
|
+
new_graph = build_graph_from_edges(new_edges)
|
59
|
+
|
60
|
+
old_nodes = (old_graph.values + old_graph.keys).flatten.uniq - ['root']
|
61
|
+
new_nodes = (new_graph.values + new_graph.keys).flatten.uniq - ['root']
|
62
|
+
orphan_nodes = find_orphans_in_graph(new_graph)
|
63
|
+
|
64
|
+
added_nodes = new_nodes - old_nodes - orphan_nodes
|
65
|
+
removed_nodes = old_nodes - new_nodes + orphan_nodes
|
66
|
+
|
67
|
+
orphaned_edges = orphan_nodes.map do |node|
|
68
|
+
find_edges_with_node(new_edges, node)
|
69
|
+
end.flatten
|
70
|
+
|
71
|
+
if orphaned_edges.present?
|
72
|
+
$redis_jason.srem("jason:subscriptions:#{id}:graph", orphaned_edges)
|
73
|
+
end
|
74
|
+
|
75
|
+
ids_to_add = {}
|
76
|
+
ids_to_remove = {}
|
77
|
+
|
78
|
+
added_nodes.each do |node|
|
79
|
+
model_name, instance_id = node.split(':')
|
80
|
+
ids_to_add[model_name] ||= []
|
81
|
+
ids_to_add[model_name].push(instance_id)
|
82
|
+
end
|
83
|
+
|
84
|
+
removed_nodes.each do |node|
|
85
|
+
model_name, instance_id = node.split(':')
|
86
|
+
ids_to_remove[model_name] ||= []
|
87
|
+
ids_to_remove[model_name].push(instance_id)
|
88
|
+
end
|
89
|
+
|
90
|
+
{ ids_to_remove: ids_to_remove, ids_to_add: ids_to_add }
|
91
|
+
end
|
92
|
+
|
93
|
+
def find_edges_with_node(edges, node)
|
94
|
+
edges.select do |edge|
|
95
|
+
parent, child = edge.split('/')
|
96
|
+
parent == node || child == node
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def find_orphans
|
101
|
+
edges = $redis_jason.smembers("jason:subscriptions:#{id}:graph")
|
102
|
+
graph = build_graph_from_edges(edges)
|
103
|
+
find_orphans_in_graph(graph)
|
104
|
+
end
|
105
|
+
|
106
|
+
def find_orphans_in_graph(graph)
|
107
|
+
reachable_nodes = get_reachable_nodes(graph)
|
108
|
+
all_nodes = (graph.values + graph.keys).flatten.uniq - ['root']
|
109
|
+
all_nodes - reachable_nodes
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_reachable_nodes(graph, parent = 'root')
|
113
|
+
reached_nodes = graph[parent] || []
|
114
|
+
reached_nodes.each do |child|
|
115
|
+
reached_nodes += get_reachable_nodes(graph, child)
|
116
|
+
end
|
117
|
+
reached_nodes
|
118
|
+
end
|
119
|
+
|
120
|
+
def build_graph_from_edges(edges)
|
121
|
+
graph = {}
|
122
|
+
edges.each do |edge|
|
123
|
+
parent, child = edge.split('/')
|
124
|
+
graph[parent] ||= []
|
125
|
+
graph[parent].push(child)
|
126
|
+
end
|
127
|
+
graph
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def build_edges(all_models, instance_ids, include_root: true)
|
133
|
+
# Build the tree
|
134
|
+
# Find parent -> child model relationships
|
135
|
+
edges = []
|
136
|
+
|
137
|
+
all_models.each_with_index do |parent_model, parent_idx|
|
138
|
+
all_models.each_with_index do |child_model, child_idx|
|
139
|
+
next if parent_model == child_model
|
140
|
+
next if !includes_helper.in_sub(parent_model, child_model)
|
141
|
+
|
142
|
+
pairs = instance_ids.map { |row| [row[parent_idx], row[child_idx]] }
|
143
|
+
.uniq
|
144
|
+
.reject{ |pair| pair[0].blank? || pair[1].blank? }
|
145
|
+
|
146
|
+
edges += pairs.map.each do |pair|
|
147
|
+
"#{parent_model}:#{pair[0]}/#{child_model}:#{pair[1]}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
root_model = includes_helper.root_model
|
153
|
+
|
154
|
+
if include_root && all_models.include?(root_model)
|
155
|
+
root_idx = all_models.find_index(root_model)
|
156
|
+
root_ids = instance_ids.map { |row| row[root_idx] }.uniq.compact
|
157
|
+
|
158
|
+
edges += root_ids.map do |id|
|
159
|
+
"root/#{root_model}:#{id}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
edges
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# Helper to provide other modules with information about the includes of a subscription
|
2
|
+
|
3
|
+
class Jason::IncludesHelper
|
4
|
+
attr_accessor :main_tree
|
5
|
+
|
6
|
+
def initialize(main_tree)
|
7
|
+
raise "Root must be hash" if !main_tree.is_a?(Hash)
|
8
|
+
raise "Only one root key allowed" if main_tree.keys.size != 1
|
9
|
+
@main_tree = main_tree
|
10
|
+
end
|
11
|
+
|
12
|
+
def all_models_recursive(tree)
|
13
|
+
sub_models = if tree.is_a?(Hash)
|
14
|
+
tree.map do |k,v|
|
15
|
+
[k, all_models_recursive(v)]
|
16
|
+
end
|
17
|
+
elsif tree.is_a?(Array)
|
18
|
+
tree.map do |v|
|
19
|
+
all_models_recursive(v)
|
20
|
+
end
|
21
|
+
else
|
22
|
+
tree
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def all_models(model_name = nil)
|
27
|
+
model_name = model_name.presence || root_model
|
28
|
+
assoc_name = get_assoc_name(model_name)
|
29
|
+
tree = get_tree_for(assoc_name)
|
30
|
+
[model_name, all_models_recursive(tree)].flatten.uniq.map(&:to_s).map(&:singularize)
|
31
|
+
end
|
32
|
+
|
33
|
+
def root_model
|
34
|
+
main_tree.keys[0]
|
35
|
+
end
|
36
|
+
|
37
|
+
# assoc could be plural or not, so need to scan both.
|
38
|
+
def get_assoc_name(model_name, haystack = main_tree)
|
39
|
+
if haystack.is_a?(Hash)
|
40
|
+
haystack.each do |assoc_name, includes_tree|
|
41
|
+
if model_name.pluralize == assoc_name.to_s.pluralize
|
42
|
+
return assoc_name
|
43
|
+
else
|
44
|
+
found_assoc = get_assoc_name(model_name, includes_tree)
|
45
|
+
return found_assoc if found_assoc
|
46
|
+
end
|
47
|
+
end
|
48
|
+
elsif haystack.is_a?(Array)
|
49
|
+
haystack.each do |element|
|
50
|
+
if element.is_a?(String)
|
51
|
+
if model_name.pluralize == element.pluralize
|
52
|
+
return element
|
53
|
+
end
|
54
|
+
else
|
55
|
+
found_assoc = get_assoc_name(model_name, element)
|
56
|
+
return found_assoc if found_assoc
|
57
|
+
end
|
58
|
+
end
|
59
|
+
else
|
60
|
+
if model_name.pluralize == haystack.to_s.pluralize
|
61
|
+
return haystack
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
return nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_tree_for(needle, assoc_name = nil, haystack = main_tree)
|
69
|
+
return haystack if needle.to_s.pluralize == assoc_name.to_s.pluralize
|
70
|
+
|
71
|
+
if haystack.is_a?(Hash)
|
72
|
+
haystack.each do |assoc_name, includes_tree|
|
73
|
+
found_haystack = get_tree_for(needle, assoc_name, includes_tree)
|
74
|
+
return found_haystack if found_haystack.present?
|
75
|
+
end
|
76
|
+
elsif haystack.is_a?(Array)
|
77
|
+
haystack.each do |includes_tree|
|
78
|
+
found_haystack = get_tree_for(needle, nil, includes_tree)
|
79
|
+
return found_haystack if found_haystack.present?
|
80
|
+
end
|
81
|
+
elsif haystack.is_a?(String)
|
82
|
+
found_haystack = get_tree_for(needle, haystack, nil)
|
83
|
+
return found_haystack if found_haystack.present?
|
84
|
+
end
|
85
|
+
|
86
|
+
return []
|
87
|
+
end
|
88
|
+
|
89
|
+
def in_sub(parent_model, child_model)
|
90
|
+
tree = get_tree_for(parent_model)
|
91
|
+
|
92
|
+
if tree.is_a?(Hash)
|
93
|
+
return tree.keys.map(&:singularize).include?(child_model)
|
94
|
+
elsif tree.is_a?(Array)
|
95
|
+
tree.each do |element|
|
96
|
+
if element.is_a?(String)
|
97
|
+
return true if element.singularize == child_model
|
98
|
+
elsif element.is_a?(Hash)
|
99
|
+
return true if element.keys.map(&:singularize).include?(child_model)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
elsif tree.is_a?(String)
|
103
|
+
return tree.singularize == child_model
|
104
|
+
end
|
105
|
+
|
106
|
+
return false
|
107
|
+
end
|
108
|
+
end
|
data/lib/jason/lua_generator.rb
CHANGED
@@ -44,6 +44,28 @@ class Jason::LuaGenerator
|
|
44
44
|
end
|
45
45
|
LUA
|
46
46
|
|
47
|
-
|
47
|
+
$redis_jason.eval cmd, [], [model_name, id, sub_id, gidx]
|
48
|
+
end
|
49
|
+
|
50
|
+
def update_set_with_diff(key, add_members, remove_members)
|
51
|
+
cmd = <<~LUA
|
52
|
+
local old_members = redis.call('smembers', KEYS[1])
|
53
|
+
local add_size = ARGV[1]
|
54
|
+
|
55
|
+
for k, m in pairs({unpack(ARGV, 2, add_size + 1)}) do
|
56
|
+
redis.call('sadd', KEYS[1], m)
|
57
|
+
end
|
58
|
+
|
59
|
+
for k, m in pairs({unpack(ARGV, add_size + 2, #ARGV)}) do
|
60
|
+
redis.call('srem', KEYS[1], m)
|
61
|
+
end
|
62
|
+
|
63
|
+
return old_members
|
64
|
+
LUA
|
65
|
+
|
66
|
+
args = [add_members.size, add_members, remove_members].flatten
|
67
|
+
|
68
|
+
old_members = $redis_jason.eval cmd, [key], args
|
69
|
+
return [old_members, (old_members + add_members - remove_members).uniq]
|
48
70
|
end
|
49
71
|
end
|
data/lib/jason/publisher.rb
CHANGED
@@ -25,8 +25,15 @@ module Jason::Publisher
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
def
|
28
|
+
def force_publish_json
|
29
|
+
# As-if newly created
|
30
|
+
publish_json(self.attributes.map { |k,v| [k, [nil, v]] }.to_h)
|
31
|
+
end
|
32
|
+
|
33
|
+
def publish_json(previous_changes = {})
|
29
34
|
payload, gidx = cache_json
|
35
|
+
|
36
|
+
return if skip_publish_json
|
30
37
|
subs = jason_subscriptions # Get this first, because could be changed
|
31
38
|
|
32
39
|
# Situations where IDs may need to change and this can't be immediately determined
|
@@ -35,22 +42,21 @@ module Jason::Publisher
|
|
35
42
|
# - TODO: The value of an instance changes so that it enters/leaves a subscription
|
36
43
|
|
37
44
|
# TODO: Optimize this, by caching associations rather than checking each time instance is saved
|
38
|
-
jason_assocs = self.class.reflect_on_all_associations(:belongs_to).select { |assoc| assoc.klass.has_jason? }
|
45
|
+
jason_assocs = self.class.reflect_on_all_associations(:belongs_to).select { |assoc| assoc.klass.respond_to?(:has_jason?) }
|
39
46
|
jason_assocs.each do |assoc|
|
40
|
-
if
|
41
|
-
|
47
|
+
if previous_changes[assoc.foreign_key].present?
|
42
48
|
Jason::Subscription.update_ids(
|
43
49
|
self.class.name.underscore,
|
44
50
|
id,
|
45
|
-
assoc.
|
46
|
-
|
47
|
-
|
51
|
+
assoc.name.to_s.singularize,
|
52
|
+
previous_changes[assoc.foreign_key][0],
|
53
|
+
previous_changes[assoc.foreign_key][1]
|
48
54
|
)
|
49
55
|
elsif (persisted? && @was_a_new_record && send(assoc.foreign_key).present?)
|
50
56
|
Jason::Subscription.update_ids(
|
51
57
|
self.class.name.underscore,
|
52
58
|
id,
|
53
|
-
assoc.
|
59
|
+
assoc.name.to_s.singularize,
|
54
60
|
nil,
|
55
61
|
send(assoc.foreign_key)
|
56
62
|
)
|
@@ -65,32 +71,30 @@ module Jason::Publisher
|
|
65
71
|
end
|
66
72
|
|
67
73
|
# - An instance is created where it belongs_to an _all_ subscription
|
68
|
-
if
|
74
|
+
if previous_changes['id'].present?
|
69
75
|
Jason::Subscription.add_id(self.class.name.underscore, id)
|
70
76
|
end
|
71
77
|
|
72
|
-
|
73
|
-
|
74
|
-
if self.persisted?
|
78
|
+
if persisted?
|
75
79
|
jason_subscriptions.each do |sub_id|
|
76
80
|
Jason::Subscription.new(id: sub_id).update(self.class.name.underscore, id, payload, gidx)
|
77
81
|
end
|
78
|
-
else
|
79
|
-
subs.each do |sub_id|
|
80
|
-
Jason::Subscription.new(id: sub_id).destroy(self.class.name.underscore, id)
|
81
|
-
end
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
85
85
|
def publish_json_if_changed
|
86
86
|
subscribed_fields = api_model.subscribed_fields
|
87
|
-
publish_json if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present? || !self.persisted?
|
87
|
+
publish_json(self.previous_changes) if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present? || !self.persisted?
|
88
88
|
end
|
89
89
|
|
90
90
|
def jason_subscriptions
|
91
91
|
Jason::Subscription.for_instance(self.class.name.underscore, id)
|
92
92
|
end
|
93
93
|
|
94
|
+
def jason_cached_value
|
95
|
+
JSON.parse($redis_jason.hget("jason:cache:#{self.class.name.underscore}", id) || '{}')
|
96
|
+
end
|
97
|
+
|
94
98
|
class_methods do
|
95
99
|
def cache_all
|
96
100
|
all.each(&:cache_json)
|
data/lib/jason/subscription.rb
CHANGED
@@ -1,17 +1,32 @@
|
|
1
1
|
class Jason::Subscription
|
2
2
|
attr_accessor :id, :config
|
3
|
+
attr_reader :includes_helper, :graph_helper
|
3
4
|
|
4
5
|
def initialize(id: nil, config: nil)
|
5
6
|
if id
|
6
7
|
@id = id
|
7
8
|
raw_config = $redis_jason.hgetall("jason:subscriptions:#{id}").map { |k,v| [k, JSON.parse(v)] }.to_h
|
9
|
+
raise "Subscription ID #{id} does not exist" if raw_config.blank?
|
8
10
|
set_config(raw_config)
|
9
11
|
else
|
10
12
|
@id = Digest::MD5.hexdigest(config.sort_by { |key| key }.to_h.to_json)
|
11
|
-
pp config.sort_by { |key| key }.to_h.to_json
|
12
13
|
configure(config)
|
13
14
|
end
|
14
|
-
|
15
|
+
@includes_helper = Jason::IncludesHelper.new({ model => self.config['includes'] })
|
16
|
+
@graph_helper = Jason::GraphHelper.new(self.id, @includes_helper)
|
17
|
+
|
18
|
+
check_for_missing_keys
|
19
|
+
end
|
20
|
+
|
21
|
+
def broadcaster
|
22
|
+
@broadcaster ||= Jason::Broadcaster.new(channel)
|
23
|
+
end
|
24
|
+
|
25
|
+
def check_for_missing_keys
|
26
|
+
missing_keys = includes_helper.all_models - Jason.schema.keys.map(&:to_s)
|
27
|
+
if missing_keys.present?
|
28
|
+
raise "#{missing_keys.inspect} are not in the schema. Only models in the Jason schema can be subscribed."
|
29
|
+
end
|
15
30
|
end
|
16
31
|
|
17
32
|
def self.upsert_by_config(model, conditions: {}, includes: {})
|
@@ -26,6 +41,13 @@ class Jason::Subscription
|
|
26
41
|
self.new(id: id)
|
27
42
|
end
|
28
43
|
|
44
|
+
def self.for_instance_with_child(model_name, id, child_model_name, include_all = true)
|
45
|
+
sub_ids = for_instance(model_name, id, include_all = true)
|
46
|
+
sub_ids.select do |sub_id|
|
47
|
+
find_by_id(sub_id).includes_helper.in_sub(model_name, child_model_name)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
29
51
|
def self.for_instance(model_name, id, include_all = true)
|
30
52
|
subs = $redis_jason.smembers("jason:models:#{model_name}:#{id}:subscriptions")
|
31
53
|
if include_all
|
@@ -41,37 +63,117 @@ class Jason::Subscription
|
|
41
63
|
|
42
64
|
# Find and update subscriptions affected by a model changing foreign key
|
43
65
|
# comment, comment_id, post, old_post_id, new_post_id
|
44
|
-
def self.update_ids(
|
45
|
-
#
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
66
|
+
def self.update_ids(changed_model_name, changed_model_id, foreign_model_name, old_foreign_id, new_foreign_id)
|
67
|
+
# There are 4 cases to consider.
|
68
|
+
# changed_instance ---/--- foreign_instance
|
69
|
+
# \--+--- new_foreign_instance
|
70
|
+
#
|
71
|
+
# foreign instance can either be parent or child for a given subscription
|
72
|
+
# 1. Child swap/add: foreign is child
|
73
|
+
# 2. Stay in the family: foreign is parent + both old and new foreign instances are part of the sub
|
74
|
+
# 3. Join the family: foreign is parent + only new foreign instance are part of the sub
|
75
|
+
# 4. Leave the family: foreign is parent + only the old foreign instance is part of the sub
|
76
|
+
|
77
|
+
#########
|
78
|
+
# Subs where changed is parent
|
79
|
+
sub_ids = for_instance_with_child(changed_model_name, changed_model_id, foreign_model_name, true)
|
80
|
+
sub_ids.each do |sub_id|
|
81
|
+
subscription = find_by_id(sub_id)
|
82
|
+
|
83
|
+
# If foreign key has been nulled, nothing to add
|
84
|
+
add = new_foreign_id.present? ? [
|
85
|
+
{
|
86
|
+
model_names: [changed_model_name, foreign_model_name],
|
87
|
+
instance_ids: [[changed_model_id, new_foreign_id]]
|
88
|
+
},
|
89
|
+
# Add IDs of child models
|
90
|
+
subscription.load_ids_for_sub_models(foreign_model_name, new_foreign_id)
|
91
|
+
] : nil
|
92
|
+
|
93
|
+
id_changeset = subscription.graph_helper.apply_update({
|
94
|
+
remove: [{
|
95
|
+
model_names: [changed_model_name, foreign_model_name],
|
96
|
+
instance_ids: [[changed_model_id, old_foreign_id]]
|
97
|
+
}],
|
98
|
+
add: add
|
99
|
+
})
|
100
|
+
|
101
|
+
subscription.apply_id_changeset(id_changeset)
|
102
|
+
subscription.broadcast_id_changeset(id_changeset)
|
55
103
|
end
|
56
104
|
|
57
|
-
|
58
|
-
(
|
59
|
-
|
60
|
-
|
105
|
+
old_sub_ids = for_instance_with_child(foreign_model_name, old_foreign_id, changed_model_name, true)
|
106
|
+
new_sub_ids = for_instance_with_child(foreign_model_name, new_foreign_id, changed_model_name, true)
|
107
|
+
|
108
|
+
#########
|
109
|
+
# Subs where changed is child
|
110
|
+
# + parent in both old + new
|
111
|
+
# this is simple, only the edges need to change - no IDs can be changed
|
112
|
+
(old_sub_ids & new_sub_ids).each do |sub_id|
|
113
|
+
subscription = find_by_id(sub_id)
|
114
|
+
subscription.graph_helper.apply_update({
|
115
|
+
remove: [{
|
116
|
+
model_names: [changed_model_name, foreign_model_name],
|
117
|
+
instance_ids: [[changed_model_id, old_foreign_id]]
|
118
|
+
}],
|
119
|
+
add: [{
|
120
|
+
model_names: [changed_model_name, foreign_model_name],
|
121
|
+
instance_ids: [[changed_model_id, new_foreign_id]]
|
122
|
+
}]
|
123
|
+
})
|
61
124
|
end
|
62
125
|
|
63
|
-
|
64
|
-
|
65
|
-
|
126
|
+
#########
|
127
|
+
# Subs where changed is child
|
128
|
+
# + old parent wasn't in the sub, but new parent is
|
129
|
+
# IE the changed instance is joining the sub
|
130
|
+
# No edges are removed, just added
|
131
|
+
(new_sub_ids - old_sub_ids).each do |sub_id|
|
132
|
+
subscription = find_by_id(sub_id)
|
133
|
+
id_changeset = subscription.graph_helper.apply_update({
|
134
|
+
add: [
|
135
|
+
{
|
136
|
+
model_names: [changed_model_name, foreign_model_name],
|
137
|
+
instance_ids: [[changed_model_id, new_foreign_id]]
|
138
|
+
},
|
139
|
+
# Add IDs of child models
|
140
|
+
subscription.load_ids_for_sub_models(changed_model_name, changed_model_id)
|
141
|
+
]
|
142
|
+
})
|
143
|
+
|
144
|
+
subscription.apply_id_changeset(id_changeset)
|
145
|
+
subscription.broadcast_id_changeset(id_changeset)
|
66
146
|
end
|
67
147
|
|
68
|
-
|
148
|
+
#########
|
149
|
+
# --> Leaving the family
|
150
|
+
# Subs where changed is child
|
151
|
+
# + old parent was in the sub, but new parent isn't
|
152
|
+
# Just need to remove the link, orphan detection will do the rest
|
153
|
+
(old_sub_ids - new_sub_ids).each do |sub_id|
|
154
|
+
subscription = find_by_id(sub_id)
|
155
|
+
id_changeset = subscription.graph_helper.apply_update({
|
156
|
+
remove: [
|
157
|
+
{
|
158
|
+
model_names: [changed_model_name, foreign_model_name],
|
159
|
+
instance_ids: [[changed_model_id, old_foreign_id]]
|
160
|
+
}
|
161
|
+
]
|
162
|
+
})
|
163
|
+
subscription.apply_id_changeset(id_changeset)
|
164
|
+
subscription.broadcast_id_changeset(id_changeset)
|
165
|
+
end
|
69
166
|
end
|
70
167
|
|
71
168
|
def self.remove_ids(model_name, ids)
|
169
|
+
# td: finish this
|
72
170
|
ids.each do |instance_id|
|
73
171
|
for_instance(model_name, instance_id, false).each do |sub_id|
|
74
|
-
find_by_id(sub_id)
|
172
|
+
subscription = find_by_id(sub_id)
|
173
|
+
|
174
|
+
id_changeset = subscription.graph_helper.apply_remove_node("#{model_name}:#{instance_id}")
|
175
|
+
subscription.apply_id_changeset(id_changeset)
|
176
|
+
subscription.broadcast_id_changeset(id_changeset)
|
75
177
|
end
|
76
178
|
end
|
77
179
|
end
|
@@ -82,87 +184,25 @@ class Jason::Subscription
|
|
82
184
|
end
|
83
185
|
|
84
186
|
def self.all
|
85
|
-
$redis_jason.
|
187
|
+
$redis_jason.smembers('jason:subscriptions').map { |id| Jason::Subscription.find_by_id(id) }
|
86
188
|
end
|
87
189
|
|
88
190
|
def set_config(raw_config)
|
89
|
-
@config = raw_config.
|
90
|
-
end
|
91
|
-
|
92
|
-
# E.g. add comment#123, and then sub models
|
93
|
-
def set_id(model_name, id)
|
94
|
-
commit_ids(model_name, [id])
|
95
|
-
assoc_name = get_assoc_name(model_name)
|
96
|
-
set_ids_for_sub_models(assoc_name, [id])
|
191
|
+
@config = raw_config.deep_stringify_keys.deep_transform_values { |v| v.is_a?(Symbol) ? v.to_s : v }
|
97
192
|
end
|
98
193
|
|
99
194
|
def clear_id(model_name, id, parent_model_name)
|
100
195
|
remove_ids(model_name, [id])
|
101
196
|
end
|
102
197
|
|
103
|
-
# Set the instance IDs for the subscription
|
104
|
-
# Add an entry to the subscription list for each instance
|
105
|
-
def set_ids(assoc_name = model, referrer_model_name = nil, referrer_ids = nil, enforce: false)
|
106
|
-
model_name = assoc_name.to_s.singularize
|
107
|
-
|
108
|
-
if referrer_model_name.blank? && conditions.blank?
|
109
|
-
$redis_jason.sadd("jason:models:#{model_name}:all:subscriptions", id)
|
110
|
-
ids = model_klass(model_name).all.pluck(:id)
|
111
|
-
set_ids_for_sub_models(assoc_name, ids, enforce: enforce)
|
112
|
-
return
|
113
|
-
end
|
114
|
-
|
115
|
-
if referrer_model_name.blank?
|
116
|
-
ids = model_klass(model_name).where(conditions).pluck(:id)
|
117
|
-
else
|
118
|
-
assoc = model_klass(referrer_model_name).reflect_on_association(assoc_name.to_sym)
|
119
|
-
|
120
|
-
if assoc.is_a?(ActiveRecord::Reflection::HasManyReflection)
|
121
|
-
ids = model_klass(model_name).where(assoc.foreign_key => referrer_ids).pluck(:id)
|
122
|
-
elsif assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection)
|
123
|
-
ids = model_klass(referrer_model_name).where(id: referrer_ids).pluck(assoc.foreign_key)
|
124
|
-
end
|
125
|
-
end
|
126
|
-
return if ids.blank?
|
127
|
-
|
128
|
-
enforce ? enforce_ids(model_name, ids) : commit_ids(model_name, ids)
|
129
|
-
set_ids_for_sub_models(assoc_name, ids, enforce: enforce)
|
130
|
-
end
|
131
|
-
|
132
|
-
def refresh_ids(assoc_name = model, referrer_model_name = nil, referrer_ids)
|
133
|
-
|
134
|
-
end
|
135
|
-
|
136
198
|
# Add IDs that aren't present
|
137
199
|
def commit_ids(model_name, ids)
|
138
|
-
pp 'COMMIT'
|
139
|
-
pp model_name
|
140
|
-
pp ids
|
141
200
|
$redis_jason.sadd("jason:subscriptions:#{id}:ids:#{model_name}", ids)
|
142
201
|
ids.each do |instance_id|
|
143
202
|
$redis_jason.sadd("jason:models:#{model_name}:#{instance_id}:subscriptions", id)
|
144
203
|
end
|
145
204
|
end
|
146
205
|
|
147
|
-
# Ensure IDs are _only_ the ones passed
|
148
|
-
def enforce_ids(model_name, ids)
|
149
|
-
old_ids = $redis_jason.smembers("jason:subscriptions:#{id}:ids:#{model_name}")
|
150
|
-
|
151
|
-
# Remove
|
152
|
-
$redis_jason.srem("jason:subscriptions:#{id}:ids:#{model_name}", (old_ids - ids))
|
153
|
-
|
154
|
-
(old_ids - ids).each do |instance_id|
|
155
|
-
$redis_jason.srem("jason:models:#{model_name}:#{instance_id}:subscriptions", id)
|
156
|
-
end
|
157
|
-
|
158
|
-
# Add
|
159
|
-
$redis_jason.sadd("jason:subscriptions:#{id}:ids:#{model_name}", (ids - old_ids))
|
160
|
-
|
161
|
-
(ids - old_ids).each do |instance_id|
|
162
|
-
$redis_jason.sadd("jason:models:#{model_name}:#{instance_id}:subscriptions", id)
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
206
|
def remove_ids(model_name, ids)
|
167
207
|
$redis_jason.srem("jason:subscriptions:#{id}:ids:#{model_name}", ids)
|
168
208
|
ids.each do |instance_id|
|
@@ -170,108 +210,81 @@ class Jason::Subscription
|
|
170
210
|
end
|
171
211
|
end
|
172
212
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
includes_tree = get_tree_for(assoc_name)
|
213
|
+
def apply_id_changeset(changeset)
|
214
|
+
changeset[:ids_to_add].each do |model_name, ids|
|
215
|
+
commit_ids(model_name, ids)
|
216
|
+
end
|
178
217
|
|
179
|
-
|
180
|
-
|
181
|
-
set_ids(assoc_name, model_name, ids, enforce: enforce)
|
182
|
-
end
|
183
|
-
# [:likes, :user]
|
184
|
-
elsif includes_tree.is_a?(Array)
|
185
|
-
includes_tree.each do |assoc_name|
|
186
|
-
set_ids(assoc_name, model_name, ids, enforce: enforce)
|
187
|
-
end
|
188
|
-
elsif includes_tree.is_a?(String)
|
189
|
-
set_ids(includes_tree, model_name, ids, enforce: enforce)
|
218
|
+
changeset[:ids_to_remove].each do |model_name, ids|
|
219
|
+
remove_ids(model_name, ids)
|
190
220
|
end
|
191
221
|
end
|
192
222
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
if haystack.is_a?(Hash)
|
198
|
-
haystack.each do |assoc_name, includes_tree|
|
199
|
-
if model_name.pluralize == assoc_name.to_s.pluralize
|
200
|
-
return assoc_name
|
201
|
-
else
|
202
|
-
found_assoc = get_assoc_name(model_name, includes_tree)
|
203
|
-
return found_assoc if found_assoc
|
204
|
-
end
|
205
|
-
end
|
206
|
-
elsif haystack.is_a?(Array)
|
207
|
-
haystack.each do |assoc_name|
|
208
|
-
if model_name.pluralize == assoc_name.to_s.pluralize
|
209
|
-
return assoc_name
|
210
|
-
end
|
211
|
-
end
|
212
|
-
else
|
213
|
-
if model_name.pluralize == haystack.to_s.pluralize
|
214
|
-
return haystack
|
215
|
-
end
|
223
|
+
def broadcast_id_changeset(changeset)
|
224
|
+
changeset[:ids_to_add].each do |model_name, ids|
|
225
|
+
ids.each { |id| add(model_name, id) }
|
216
226
|
end
|
217
227
|
|
218
|
-
|
228
|
+
changeset[:ids_to_remove].each do |model_name, ids|
|
229
|
+
ids.each { |id| destroy(model_name, id) }
|
230
|
+
end
|
219
231
|
end
|
220
232
|
|
221
|
-
|
222
|
-
|
223
|
-
|
233
|
+
# Take a model name and IDs and return an edge set of all the models that appear and
|
234
|
+
# their instance IDs
|
235
|
+
def load_ids_for_sub_models(model_name, ids)
|
236
|
+
# Limitation: Same association can't appear twice
|
237
|
+
includes_tree = includes_helper.get_tree_for(model_name)
|
238
|
+
all_models = includes_helper.all_models(model_name)
|
239
|
+
|
240
|
+
relation = model_name.classify.constantize.all.eager_load(includes_tree)
|
224
241
|
|
225
|
-
if
|
226
|
-
|
227
|
-
|
228
|
-
|
242
|
+
if model_name == model
|
243
|
+
if conditions.blank?
|
244
|
+
$redis_jason.sadd("jason:models:#{model_name}:all:subscriptions", id)
|
245
|
+
all_models -= [model_name]
|
246
|
+
else
|
247
|
+
relation = relation.where(conditions)
|
229
248
|
end
|
249
|
+
else
|
250
|
+
raise "Must supply IDs for sub models" if ids.nil?
|
251
|
+
return if ids.blank?
|
252
|
+
relation = relation.where(id: ids)
|
230
253
|
end
|
231
254
|
|
232
|
-
|
233
|
-
|
255
|
+
pluck_args = all_models.map { |m| "#{m.pluralize}.id" }
|
256
|
+
instance_ids = relation.pluck(*pluck_args)
|
234
257
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
[k, all_models(v)]
|
239
|
-
end
|
240
|
-
else
|
241
|
-
tree
|
258
|
+
# pluck returns only a 1D array if only 1 arg passed
|
259
|
+
if all_models.size == 1
|
260
|
+
instance_ids = [instance_ids]
|
242
261
|
end
|
243
262
|
|
244
|
-
|
245
|
-
([model] + [sub_models]).flatten.uniq.map(&:to_s).map(&:singularize)
|
263
|
+
return { model_names: all_models, instance_ids: instance_ids }
|
246
264
|
end
|
247
265
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
if model_name == model && conditions.blank?
|
253
|
-
$redis_jason.srem("jason:models:#{model_name}:all:subscriptions", id)
|
254
|
-
end
|
266
|
+
# 'posts', [post#1, post#2,...]
|
267
|
+
def set_ids_for_sub_models(model_name = model, ids = nil, enforce: false)
|
268
|
+
edge_set = load_ids_for_sub_models(model_name, ids)
|
255
269
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
270
|
+
# Build the tree
|
271
|
+
id_changeset = graph_helper.apply_update({
|
272
|
+
add: [edge_set]
|
273
|
+
})
|
274
|
+
apply_id_changeset(id_changeset)
|
275
|
+
end
|
261
276
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
clear_all_ids(assoc_name)
|
277
|
+
def clear_all_ids
|
278
|
+
includes_helper.all_models.each do |model_name|
|
279
|
+
if model_name == model && conditions.blank?
|
280
|
+
$redis_jason.srem("jason:models:#{model_name}:all:subscriptions", id)
|
267
281
|
end
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
282
|
+
|
283
|
+
ids = $redis_jason.smembers("jason:subscriptions:#{id}:ids:#{model_name}")
|
284
|
+
ids.each do |instance_id|
|
285
|
+
$redis_jason.srem("jason:models:#{model_name}:#{instance_id}:subscriptions", id)
|
272
286
|
end
|
273
|
-
|
274
|
-
clear_all_ids(includes_tree)
|
287
|
+
$redis_jason.del("jason:subscriptions:#{id}:ids:#{model_name}")
|
275
288
|
end
|
276
289
|
end
|
277
290
|
|
@@ -291,12 +304,9 @@ class Jason::Subscription
|
|
291
304
|
@config['conditions']
|
292
305
|
end
|
293
306
|
|
294
|
-
def includes
|
295
|
-
@config['includes']
|
296
|
-
end
|
297
|
-
|
298
307
|
def configure(raw_config)
|
299
308
|
set_config(raw_config)
|
309
|
+
$redis_jason.sadd("jason:subscriptions", id)
|
300
310
|
$redis_jason.hmset("jason:subscriptions:#{id}", *config.map { |k,v| [k, v.to_json] }.flatten)
|
301
311
|
end
|
302
312
|
|
@@ -310,7 +320,7 @@ class Jason::Subscription
|
|
310
320
|
$redis_jason.hset("jason:consumers", consumer_id, Time.now.utc)
|
311
321
|
|
312
322
|
if before_consumer_count == 0
|
313
|
-
|
323
|
+
set_ids_for_sub_models
|
314
324
|
end
|
315
325
|
end
|
316
326
|
|
@@ -328,11 +338,17 @@ class Jason::Subscription
|
|
328
338
|
end
|
329
339
|
|
330
340
|
def channel
|
331
|
-
"jason
|
341
|
+
"jason-#{id}"
|
342
|
+
end
|
343
|
+
|
344
|
+
def user_can_access?(user)
|
345
|
+
# td: implement the authorization logic here
|
346
|
+
return true if Jason.authorization_service.blank?
|
347
|
+
Jason.authorization_service.call(user, model, conditions, includes_helper.all_models - [model])
|
332
348
|
end
|
333
349
|
|
334
350
|
def get
|
335
|
-
all_models.map { |model_name| get_for_model(model_name) }
|
351
|
+
includes_helper.all_models.map { |model_name| [model_name, get_for_model(model_name)] }.to_h
|
336
352
|
end
|
337
353
|
|
338
354
|
def get_for_model(model_name)
|
@@ -346,8 +362,6 @@ class Jason::Subscription
|
|
346
362
|
instance_jsons, idx = Jason::LuaGenerator.new.get_payload(model_name, id)
|
347
363
|
end
|
348
364
|
|
349
|
-
return if instance_jsons.blank?
|
350
|
-
|
351
365
|
payload = instance_jsons.map do |instance_json|
|
352
366
|
instance_json ? JSON.parse(instance_json) : {}
|
353
367
|
end
|
@@ -361,6 +375,21 @@ class Jason::Subscription
|
|
361
375
|
}
|
362
376
|
end
|
363
377
|
|
378
|
+
def add(model_name, instance_id)
|
379
|
+
idx = $redis_jason.incr("jason:subscription:#{id}:#{model_name}:idx")
|
380
|
+
payload = JSON.parse($redis_jason.hget("jason:cache:#{model_name}", instance_id) || '{}')
|
381
|
+
|
382
|
+
payload = {
|
383
|
+
id: instance_id,
|
384
|
+
model: model_name,
|
385
|
+
payload: payload,
|
386
|
+
md5Hash: id,
|
387
|
+
idx: idx.to_i
|
388
|
+
}
|
389
|
+
|
390
|
+
broadcaster.broadcast(payload)
|
391
|
+
end
|
392
|
+
|
364
393
|
def update(model_name, instance_id, payload, gidx)
|
365
394
|
idx = Jason::LuaGenerator.new.get_subscription(model_name, instance_id, id, gidx)
|
366
395
|
return if idx.blank?
|
@@ -373,11 +402,11 @@ class Jason::Subscription
|
|
373
402
|
idx: idx.to_i
|
374
403
|
}
|
375
404
|
|
376
|
-
|
405
|
+
broadcaster.broadcast(payload)
|
377
406
|
end
|
378
407
|
|
379
408
|
def destroy(model_name, instance_id)
|
380
|
-
idx = $redis_jason.incr("jason:subscription:#{id}:#{model_name}idx")
|
409
|
+
idx = $redis_jason.incr("jason:subscription:#{id}:#{model_name}:idx")
|
381
410
|
|
382
411
|
payload = {
|
383
412
|
id: instance_id,
|
@@ -387,6 +416,6 @@ class Jason::Subscription
|
|
387
416
|
idx: idx.to_i
|
388
417
|
}
|
389
418
|
|
390
|
-
|
419
|
+
broadcaster.broadcast(payload)
|
391
420
|
end
|
392
421
|
end
|