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.
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