jason-rails 0.5.1 → 0.6.4
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/Gemfile.lock +1 -1
- data/README.md +41 -7
- 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 +9 -3
- data/client/lib/createPayloadHandler.d.ts +6 -3
- data/client/lib/createPayloadHandler.js +10 -6
- 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/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 +14 -34
- data/client/lib/useJason.test.js +41 -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 +9 -3
- data/client/src/createPayloadHandler.ts +11 -6
- 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 +12 -11
- data/client/src/restClient.ts +1 -0
- 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 +49 -3
- data/client/src/useJason.ts +15 -36
- 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 +6 -2
- 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 +20 -16
- data/lib/jason/subscription.rb +208 -185
- data/lib/jason/version.rb +1 -1
- metadata +18 -2
@@ -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
|
@@ -37,20 +44,19 @@ module Jason::Publisher
|
|
37
44
|
# TODO: Optimize this, by caching associations rather than checking each time instance is saved
|
38
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,93 +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
|
-
ids_to_remove = old_ids - ids
|
153
|
-
if ids_to_remove.present?
|
154
|
-
$redis_jason.srem("jason:subscriptions:#{id}:ids:#{model_name}", ids_to_remove)
|
155
|
-
end
|
156
|
-
|
157
|
-
ids_to_remove.each do |instance_id|
|
158
|
-
$redis_jason.srem("jason:models:#{model_name}:#{instance_id}:subscriptions", id)
|
159
|
-
end
|
160
|
-
|
161
|
-
# Add
|
162
|
-
ids_to_add = ids - old_ids
|
163
|
-
if ids_to_add.present?
|
164
|
-
$redis_jason.sadd("jason:subscriptions:#{id}:ids:#{model_name}", ids_to_add)
|
165
|
-
end
|
166
|
-
|
167
|
-
ids_to_add.each do |instance_id|
|
168
|
-
$redis_jason.sadd("jason:models:#{model_name}:#{instance_id}:subscriptions", id)
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
206
|
def remove_ids(model_name, ids)
|
173
207
|
$redis_jason.srem("jason:subscriptions:#{id}:ids:#{model_name}", ids)
|
174
208
|
ids.each do |instance_id|
|
@@ -176,108 +210,81 @@ class Jason::Subscription
|
|
176
210
|
end
|
177
211
|
end
|
178
212
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
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
|
184
217
|
|
185
|
-
|
186
|
-
|
187
|
-
set_ids(assoc_name, model_name, ids, enforce: enforce)
|
188
|
-
end
|
189
|
-
# [:likes, :user]
|
190
|
-
elsif includes_tree.is_a?(Array)
|
191
|
-
includes_tree.each do |assoc_name|
|
192
|
-
set_ids(assoc_name, model_name, ids, enforce: enforce)
|
193
|
-
end
|
194
|
-
elsif includes_tree.is_a?(String)
|
195
|
-
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)
|
196
220
|
end
|
197
221
|
end
|
198
222
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
if haystack.is_a?(Hash)
|
204
|
-
haystack.each do |assoc_name, includes_tree|
|
205
|
-
if model_name.pluralize == assoc_name.to_s.pluralize
|
206
|
-
return assoc_name
|
207
|
-
else
|
208
|
-
found_assoc = get_assoc_name(model_name, includes_tree)
|
209
|
-
return found_assoc if found_assoc
|
210
|
-
end
|
211
|
-
end
|
212
|
-
elsif haystack.is_a?(Array)
|
213
|
-
haystack.each do |assoc_name|
|
214
|
-
if model_name.pluralize == assoc_name.to_s.pluralize
|
215
|
-
return assoc_name
|
216
|
-
end
|
217
|
-
end
|
218
|
-
else
|
219
|
-
if model_name.pluralize == haystack.to_s.pluralize
|
220
|
-
return haystack
|
221
|
-
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) }
|
222
226
|
end
|
223
227
|
|
224
|
-
|
228
|
+
changeset[:ids_to_remove].each do |model_name, ids|
|
229
|
+
ids.each { |id| destroy(model_name, id) }
|
230
|
+
end
|
225
231
|
end
|
226
232
|
|
227
|
-
|
228
|
-
|
229
|
-
|
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)
|
230
239
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
240
|
+
relation = model_name.classify.constantize.all.eager_load(includes_tree)
|
241
|
+
|
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)
|
235
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)
|
236
253
|
end
|
237
254
|
|
238
|
-
|
239
|
-
|
255
|
+
pluck_args = all_models.map { |m| "#{m.pluralize}.id" }
|
256
|
+
instance_ids = relation.pluck(*pluck_args)
|
240
257
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
[k, all_models(v)]
|
245
|
-
end
|
246
|
-
else
|
247
|
-
tree
|
258
|
+
# pluck returns only a 1D array if only 1 arg passed
|
259
|
+
if all_models.size == 1
|
260
|
+
instance_ids = [instance_ids]
|
248
261
|
end
|
249
262
|
|
250
|
-
|
251
|
-
([model] + [sub_models]).flatten.uniq.map(&:to_s).map(&:singularize)
|
263
|
+
return { model_names: all_models, instance_ids: instance_ids }
|
252
264
|
end
|
253
265
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
if model_name == model && conditions.blank?
|
259
|
-
$redis_jason.srem("jason:models:#{model_name}:all:subscriptions", id)
|
260
|
-
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)
|
261
269
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
270
|
+
# Build the tree
|
271
|
+
id_changeset = graph_helper.apply_update({
|
272
|
+
add: [edge_set]
|
273
|
+
})
|
274
|
+
apply_id_changeset(id_changeset)
|
275
|
+
end
|
267
276
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
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)
|
273
281
|
end
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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)
|
278
286
|
end
|
279
|
-
|
280
|
-
clear_all_ids(includes_tree)
|
287
|
+
$redis_jason.del("jason:subscriptions:#{id}:ids:#{model_name}")
|
281
288
|
end
|
282
289
|
end
|
283
290
|
|
@@ -297,12 +304,9 @@ class Jason::Subscription
|
|
297
304
|
@config['conditions']
|
298
305
|
end
|
299
306
|
|
300
|
-
def includes
|
301
|
-
@config['includes']
|
302
|
-
end
|
303
|
-
|
304
307
|
def configure(raw_config)
|
305
308
|
set_config(raw_config)
|
309
|
+
$redis_jason.sadd("jason:subscriptions", id)
|
306
310
|
$redis_jason.hmset("jason:subscriptions:#{id}", *config.map { |k,v| [k, v.to_json] }.flatten)
|
307
311
|
end
|
308
312
|
|
@@ -316,7 +320,7 @@ class Jason::Subscription
|
|
316
320
|
$redis_jason.hset("jason:consumers", consumer_id, Time.now.utc)
|
317
321
|
|
318
322
|
if before_consumer_count == 0
|
319
|
-
|
323
|
+
set_ids_for_sub_models
|
320
324
|
end
|
321
325
|
end
|
322
326
|
|
@@ -334,11 +338,17 @@ class Jason::Subscription
|
|
334
338
|
end
|
335
339
|
|
336
340
|
def channel
|
337
|
-
"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])
|
338
348
|
end
|
339
349
|
|
340
350
|
def get
|
341
|
-
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
|
342
352
|
end
|
343
353
|
|
344
354
|
def get_for_model(model_name)
|
@@ -352,8 +362,6 @@ class Jason::Subscription
|
|
352
362
|
instance_jsons, idx = Jason::LuaGenerator.new.get_payload(model_name, id)
|
353
363
|
end
|
354
364
|
|
355
|
-
return if instance_jsons.blank?
|
356
|
-
|
357
365
|
payload = instance_jsons.map do |instance_json|
|
358
366
|
instance_json ? JSON.parse(instance_json) : {}
|
359
367
|
end
|
@@ -367,6 +375,21 @@ class Jason::Subscription
|
|
367
375
|
}
|
368
376
|
end
|
369
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
|
+
|
370
393
|
def update(model_name, instance_id, payload, gidx)
|
371
394
|
idx = Jason::LuaGenerator.new.get_subscription(model_name, instance_id, id, gidx)
|
372
395
|
return if idx.blank?
|
@@ -379,11 +402,11 @@ class Jason::Subscription
|
|
379
402
|
idx: idx.to_i
|
380
403
|
}
|
381
404
|
|
382
|
-
|
405
|
+
broadcaster.broadcast(payload)
|
383
406
|
end
|
384
407
|
|
385
408
|
def destroy(model_name, instance_id)
|
386
|
-
idx = $redis_jason.incr("jason:subscription:#{id}:#{model_name}idx")
|
409
|
+
idx = $redis_jason.incr("jason:subscription:#{id}:#{model_name}:idx")
|
387
410
|
|
388
411
|
payload = {
|
389
412
|
id: instance_id,
|
@@ -393,6 +416,6 @@ class Jason::Subscription
|
|
393
416
|
idx: idx.to_i
|
394
417
|
}
|
395
418
|
|
396
|
-
|
419
|
+
broadcaster.broadcast(payload)
|
397
420
|
end
|
398
421
|
end
|