jason-rails 0.5.0 → 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/Gemfile.lock +1 -1
  4. data/README.md +141 -5
  5. data/app/controllers/jason/api/pusher_controller.rb +15 -0
  6. data/app/controllers/jason/api_controller.rb +46 -4
  7. data/client/lib/JasonContext.d.ts +1 -1
  8. data/client/lib/JasonContext.js +4 -1
  9. data/client/lib/JasonProvider.js +1 -1
  10. data/client/lib/createJasonReducers.js +7 -0
  11. data/client/lib/createPayloadHandler.d.ts +6 -3
  12. data/client/lib/createPayloadHandler.js +8 -4
  13. data/client/lib/createTransportAdapter.d.ts +5 -0
  14. data/client/lib/createTransportAdapter.js +20 -0
  15. data/client/lib/index.d.ts +2 -0
  16. data/client/lib/index.js +3 -1
  17. data/client/lib/makeEager.js +2 -2
  18. data/client/lib/pruneIdsMiddleware.js +9 -11
  19. data/client/lib/restClient.d.ts +1 -1
  20. data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
  21. data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
  22. data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
  23. data/client/lib/transportAdapters/pusherAdapter.js +68 -0
  24. data/client/lib/useEager.d.ts +1 -0
  25. data/client/lib/useEager.js +12 -0
  26. data/client/lib/useJason.js +30 -35
  27. data/client/lib/useJason.test.js +8 -2
  28. data/client/lib/useSub.d.ts +1 -1
  29. data/client/lib/useSub.js +5 -3
  30. data/client/package.json +2 -1
  31. data/client/src/JasonContext.ts +4 -1
  32. data/client/src/JasonProvider.tsx +1 -1
  33. data/client/src/createJasonReducers.ts +7 -0
  34. data/client/src/createPayloadHandler.ts +9 -4
  35. data/client/src/createTransportAdapter.ts +13 -0
  36. data/client/src/index.ts +3 -1
  37. data/client/src/makeEager.ts +2 -2
  38. data/client/src/pruneIdsMiddleware.ts +11 -11
  39. data/client/src/restClient.ts +2 -1
  40. data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
  41. data/client/src/transportAdapters/pusherAdapter.ts +72 -0
  42. data/client/src/useEager.ts +9 -0
  43. data/client/src/useJason.test.ts +8 -2
  44. data/client/src/useJason.ts +31 -36
  45. data/client/src/useSub.ts +5 -3
  46. data/client/yarn.lock +12 -0
  47. data/config/routes.rb +5 -1
  48. data/lib/jason.rb +56 -8
  49. data/lib/jason/broadcaster.rb +19 -0
  50. data/lib/jason/channel.rb +10 -3
  51. data/lib/jason/graph_helper.rb +165 -0
  52. data/lib/jason/includes_helper.rb +108 -0
  53. data/lib/jason/lua_generator.rb +23 -1
  54. data/lib/jason/publisher.rb +21 -17
  55. data/lib/jason/subscription.rb +208 -179
  56. data/lib/jason/version.rb +1 -1
  57. 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
- subscription.get.each do |payload|
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
@@ -44,6 +44,28 @@ class Jason::LuaGenerator
44
44
  end
45
45
  LUA
46
46
 
47
- result = $redis_jason.eval cmd, [], [model_name, id, sub_id, gidx]
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
@@ -25,8 +25,15 @@ module Jason::Publisher
25
25
  end
26
26
  end
27
27
 
28
- def publish_json
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 self.previous_changes[assoc.foreign_key].present?
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.klass.name.underscore,
46
- self.previous_changes[assoc.foreign_key][0],
47
- self.previous_changes[assoc.foreign_key][1]
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.klass.name.underscore,
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 self.previous_changes['id'].present?
74
+ if previous_changes['id'].present?
69
75
  Jason::Subscription.add_id(self.class.name.underscore, id)
70
76
  end
71
77
 
72
- return if skip_publish_json
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)
@@ -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
- pp @id
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(model_name, id, parent_model_name, old_foreign_id, new_foreign_id)
45
- # Check if this change means it needs to be removed
46
- # First find subscriptions that reference this model
47
-
48
- if old_foreign_id
49
- old_model_subscriptions = for_instance(parent_model_name, old_foreign_id, false)
50
- new_model_subscriptions = for_instance(parent_model_name, new_foreign_id, false)
51
- else
52
- # If this is a new instance, we need to include _all_ subscriptions
53
- old_model_subscriptions = []
54
- new_model_subscriptions = for_instance(parent_model_name, new_foreign_id, true)
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
- # To add
58
- (new_model_subscriptions - old_model_subscriptions).each do |sub_id|
59
- # add the current ID to the subscription, then add the tree below it
60
- find_by_id(sub_id).set_id(model_name, id)
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
- # To remove
64
- (old_model_subscriptions - new_model_subscriptions).each do |sub_id|
65
- find_by_id(sub_id).remove_ids(model_name, [id])
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
- # TODO changes to sub models - e.g. post -> comment -> user
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).remove_ids(model_name, [instance_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.keys('jason:subscriptions:*')
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.with_indifferent_access
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
- # 'posts', [post#1, post#2,...]
174
- def set_ids_for_sub_models(assoc_name, ids, enforce: false)
175
- model_name = assoc_name.to_s.singularize
176
- # Limitation: Same association can't appear twice
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
- if includes_tree.is_a?(Hash)
180
- includes_tree.each do |assoc_name, includes_tree|
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
- # assoc could be plural or not, so need to scan both.
194
- def get_assoc_name(model_name, haystack = includes)
195
- return model_name if model_name == model
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
- return nil
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
- def get_tree_for(needle, assoc_name = nil, haystack = includes)
222
- return includes if needle == model
223
- return haystack if needle.to_s == assoc_name.to_s
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 haystack.is_a?(Hash)
226
- haystack.each do |assoc_name, includes_tree|
227
- found_haystack = get_tree_for(needle, assoc_name, includes_tree)
228
- return found_haystack if found_haystack
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
- return nil
233
- end
255
+ pluck_args = all_models.map { |m| "#{m.pluralize}.id" }
256
+ instance_ids = relation.pluck(*pluck_args)
234
257
 
235
- def all_models(tree = includes)
236
- sub_models = if tree.is_a?(Hash)
237
- tree.map do |k,v|
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
- pp ([model] + [sub_models]).flatten.uniq.map(&:to_s).map(&:singularize)
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
- def clear_all_ids(assoc_name = model)
249
- model_name = assoc_name.to_s.singularize
250
- includes_tree = model_name == model ? includes : get_tree_for(assoc_name)
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
- ids = $redis_jason.smembers("jason:subscriptions:#{id}:ids:#{model_name}")
257
- ids.each do |instance_id|
258
- $redis_jason.srem("jason:models:#{model_name}:#{instance_id}:subscriptions", id)
259
- end
260
- $redis_jason.del("jason:subscriptions:#{id}:ids:#{model_name}")
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
- # Recursively clear IDs
263
- # { comments: [:like] }
264
- if includes_tree.is_a?(Hash)
265
- includes_tree.each do |assoc_name, includes_tree|
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
- # [:likes, :user]
269
- elsif includes_tree.is_a?(Array)
270
- includes_tree.each do |assoc_name|
271
- clear_all_ids(assoc_name)
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
- elsif includes_tree.is_a?(String)
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
- set_ids
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:#{id}"
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
- ActionCable.server.broadcast(channel, payload)
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
- ActionCable.server.broadcast(channel, payload)
419
+ broadcaster.broadcast(payload)
391
420
  end
392
421
  end