jason-rails 0.4.0 → 0.6.1

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.ruby-version +1 -0
  4. data/Gemfile.lock +152 -2
  5. data/README.md +117 -5
  6. data/app/controllers/jason/api/pusher_controller.rb +15 -0
  7. data/app/controllers/jason/api_controller.rb +44 -2
  8. data/client/lib/JasonContext.d.ts +6 -1
  9. data/client/lib/JasonProvider.d.ts +2 -2
  10. data/client/lib/JasonProvider.js +5 -124
  11. data/client/lib/createJasonReducers.js +48 -3
  12. data/client/lib/createOptDis.js +0 -2
  13. data/client/lib/createPayloadHandler.d.ts +9 -1
  14. data/client/lib/createPayloadHandler.js +47 -55
  15. data/client/lib/createServerActionQueue.d.ts +10 -0
  16. data/client/lib/createServerActionQueue.js +48 -0
  17. data/client/lib/createServerActionQueue.test.d.ts +1 -0
  18. data/client/lib/createServerActionQueue.test.js +37 -0
  19. data/client/lib/createTransportAdapter.d.ts +5 -0
  20. data/client/lib/createTransportAdapter.js +20 -0
  21. data/client/lib/index.d.ts +3 -2
  22. data/client/lib/pruneIdsMiddleware.d.ts +2 -0
  23. data/client/lib/pruneIdsMiddleware.js +24 -0
  24. data/client/lib/restClient.d.ts +2 -0
  25. data/client/lib/restClient.js +17 -0
  26. data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
  27. data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
  28. data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
  29. data/client/lib/transportAdapters/pusherAdapter.js +68 -0
  30. data/client/lib/useJason.d.ts +5 -0
  31. data/client/lib/useJason.js +94 -0
  32. data/client/lib/useJason.test.d.ts +1 -0
  33. data/client/lib/useJason.test.js +85 -0
  34. data/client/lib/useSub.d.ts +1 -1
  35. data/client/lib/useSub.js +6 -3
  36. data/client/package.json +5 -3
  37. data/client/src/JasonProvider.tsx +5 -123
  38. data/client/src/createJasonReducers.ts +56 -3
  39. data/client/src/createOptDis.ts +0 -2
  40. data/client/src/createPayloadHandler.ts +53 -64
  41. data/client/src/createServerActionQueue.test.ts +42 -0
  42. data/client/src/createServerActionQueue.ts +47 -0
  43. data/client/src/createTransportAdapter.ts +13 -0
  44. data/client/src/pruneIdsMiddleware.ts +24 -0
  45. data/client/src/restClient.ts +14 -0
  46. data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
  47. data/client/src/transportAdapters/pusherAdapter.ts +72 -0
  48. data/client/src/useJason.test.ts +87 -0
  49. data/client/src/useJason.ts +110 -0
  50. data/client/src/useSub.ts +6 -3
  51. data/client/yarn.lock +71 -3
  52. data/config/routes.rb +5 -1
  53. data/jason-rails.gemspec +4 -0
  54. data/lib/jason.rb +61 -1
  55. data/lib/jason/api_model.rb +2 -12
  56. data/lib/jason/broadcaster.rb +19 -0
  57. data/lib/jason/channel.rb +50 -21
  58. data/lib/jason/graph_helper.rb +165 -0
  59. data/lib/jason/includes_helper.rb +108 -0
  60. data/lib/jason/lua_generator.rb +71 -0
  61. data/lib/jason/publisher.rb +82 -37
  62. data/lib/jason/publisher_old.rb +112 -0
  63. data/lib/jason/subscription.rb +349 -97
  64. data/lib/jason/subscription_old.rb +171 -0
  65. data/lib/jason/version.rb +1 -1
  66. metadata +80 -11
  67. data/app/assets/config/jason_engine_manifest.js +0 -1
  68. data/app/assets/images/jason/engine/.keep +0 -0
  69. data/app/assets/stylesheets/jason/engine/application.css +0 -15
  70. data/app/helpers/jason/engine/application_helper.rb +0 -6
  71. data/app/jobs/jason/engine/application_job.rb +0 -6
  72. data/app/mailers/jason/engine/application_mailer.rb +0 -8
  73. data/app/models/jason/engine/application_record.rb +0 -7
  74. data/app/views/layouts/jason/engine/application.html.erb +0 -15
@@ -0,0 +1,71 @@
1
+ class Jason::LuaGenerator
2
+ ## TODO load these scripts and evalsha
3
+ def cache_json(model_name, id, payload)
4
+ cmd = <<~LUA
5
+ local gidx = redis.call('INCR', 'jason:gidx')
6
+ redis.call( 'set', 'jason:cache:' .. ARGV[1] .. ':' .. ARGV[2] .. ':gidx', gidx )
7
+ redis.call( 'hset', 'jason:cache:' .. ARGV[1], ARGV[2], ARGV[3] )
8
+ return gidx
9
+ LUA
10
+
11
+ result = $redis_jason.eval cmd, [], [model_name, id, payload.to_json]
12
+ end
13
+
14
+ def get_payload(model_name, sub_id)
15
+ # If value has changed, return old value and new idx. Otherwise do nothing.
16
+ cmd = <<~LUA
17
+ local t = {}
18
+ local models = {}
19
+ local ids = redis.call('smembers', 'jason:subscriptions:' .. ARGV[2] .. ':ids:' .. ARGV[1])
20
+
21
+ for k,id in pairs(ids) do
22
+ models[#models+1] = redis.call( 'hget', 'jason:cache:' .. ARGV[1], id)
23
+ end
24
+
25
+ t[#t+1] = models
26
+ t[#t+1] = redis.call( 'get', 'jason:subscription:' .. ARGV[2] .. ':' .. ARGV[1] .. ':idx' )
27
+
28
+ return t
29
+ LUA
30
+
31
+ $redis_jason.eval cmd, [], [model_name, sub_id]
32
+ end
33
+
34
+ def get_subscription(model_name, id, sub_id, gidx)
35
+ # If value has changed, return old value and new idx. Otherwise do nothing.
36
+ cmd = <<~LUA
37
+ local last_gidx = redis.call('get', 'jason:cache:' .. ARGV[1] .. ':' .. ARGV[2] .. ':gidx') or 0
38
+
39
+ if (ARGV[4] >= last_gidx) then
40
+ local sub_idx = redis.call( 'incr', 'jason:subscription:' .. ARGV[3] .. ':' .. ARGV[1] .. ':idx' )
41
+ return sub_idx
42
+ else
43
+ return false
44
+ end
45
+ LUA
46
+
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]
70
+ end
71
+ end
@@ -1,76 +1,121 @@
1
1
  module Jason::Publisher
2
2
  extend ActiveSupport::Concern
3
3
 
4
+ # Warning: Could be expensive. Mainly useful for rebuilding cache after changing Jason config or on deploy
5
+ def self.cache_all
6
+ Rails.application.eager_load!
7
+ ActiveRecord::Base.descendants.each do |klass|
8
+ klass.cache_all if klass.respond_to?(:cache_all)
9
+ end
10
+ end
11
+
4
12
  def cache_json
5
13
  as_json_config = api_model.as_json_config
6
14
  scope = api_model.scope
7
15
 
16
+ # Exists
8
17
  if self.persisted? && (scope.blank? || self.class.unscoped.send(scope).exists?(self.id))
9
18
  payload = self.reload.as_json(as_json_config)
10
- $redis_jason.hset("jason:#{self.class.name.underscore}:cache", self.id, payload.to_json)
19
+ gidx = Jason::LuaGenerator.new.cache_json(self.class.name.underscore, self.id, payload)
20
+ return [payload, gidx]
21
+ # Has been destroyed
11
22
  else
12
- $redis_jason.hdel("jason:#{self.class.name.underscore}:cache", self.id)
23
+ $redis_jason.hdel("jason:cache:#{self.class.name.underscore}", self.id)
24
+ return []
13
25
  end
14
26
  end
15
27
 
16
- def publish_json
17
- cache_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 = {})
34
+ payload, gidx = cache_json
35
+
18
36
  return if skip_publish_json
19
- self.class.jason_subscriptions.each do |id, config_json|
20
- config = JSON.parse(config_json)
37
+ subs = jason_subscriptions # Get this first, because could be changed
38
+
39
+ # Situations where IDs may need to change and this can't be immediately determined
40
+ # - An instance is created where it belongs_to an instance under a subscription
41
+ # - An instance belongs_to association changes - e.g. comment.post_id changes to/from one with a subscription
42
+ # - TODO: The value of an instance changes so that it enters/leaves a subscription
43
+
44
+ # TODO: Optimize this, by caching associations rather than checking each time instance is saved
45
+ jason_assocs = self.class.reflect_on_all_associations(:belongs_to).select { |assoc| assoc.klass.respond_to?(:has_jason?) }
46
+ jason_assocs.each do |assoc|
47
+ if previous_changes[assoc.foreign_key].present?
48
+ Jason::Subscription.update_ids(
49
+ self.class.name.underscore,
50
+ id,
51
+ assoc.name.to_s.singularize,
52
+ previous_changes[assoc.foreign_key][0],
53
+ previous_changes[assoc.foreign_key][1]
54
+ )
55
+ elsif (persisted? && @was_a_new_record && send(assoc.foreign_key).present?)
56
+ Jason::Subscription.update_ids(
57
+ self.class.name.underscore,
58
+ id,
59
+ assoc.name.to_s.singularize,
60
+ nil,
61
+ send(assoc.foreign_key)
62
+ )
63
+ end
64
+ end
65
+
66
+ if !persisted? # Deleted
67
+ Jason::Subscription.remove_ids(
68
+ self.class.name.underscore,
69
+ [id]
70
+ )
71
+ end
21
72
 
22
- if (config['conditions'] || {}).all? { |field, value| self.send(field) == value }
23
- Jason::Subscription.new(id: id).update(self.class.name.underscore)
73
+ # - An instance is created where it belongs_to an _all_ subscription
74
+ if previous_changes['id'].present?
75
+ Jason::Subscription.add_id(self.class.name.underscore, id)
76
+ end
77
+
78
+ if persisted?
79
+ jason_subscriptions.each do |sub_id|
80
+ Jason::Subscription.new(id: sub_id).update(self.class.name.underscore, id, payload, gidx)
24
81
  end
25
82
  end
26
83
  end
27
84
 
28
85
  def publish_json_if_changed
29
86
  subscribed_fields = api_model.subscribed_fields
30
- 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?
31
88
  end
32
89
 
33
- class_methods do
34
- def subscriptions
35
- $redis_jason.hgetall("jason:#{self.name.underscore}:subscriptions")
36
- end
90
+ def jason_subscriptions
91
+ Jason::Subscription.for_instance(self.class.name.underscore, id)
92
+ end
37
93
 
38
- def jason_subscriptions
39
- $redis_jason.hgetall("jason:#{self.name.underscore}:subscriptions")
40
- end
94
+ def jason_cached_value
95
+ JSON.parse($redis_jason.hget("jason:cache:#{self.class.name.underscore}", id) || '{}')
96
+ end
41
97
 
42
- def publish_all(instances)
43
- instances.each(&:cache_json)
98
+ class_methods do
99
+ def cache_all
100
+ all.each(&:cache_json)
101
+ end
44
102
 
45
- subscriptions.each do |id, config_json|
46
- Jason::Subscription.new(id: id).update(self.name.underscore)
47
- end
103
+ def has_jason?
104
+ true
48
105
  end
49
106
 
50
107
  def flush_cache
51
- $redis_jason.del("jason:#{self.name.underscore}:cache")
108
+ $redis_jason.del("jason:cache:#{self.name.underscore}")
52
109
  end
53
110
 
54
111
  def setup_json
112
+ self.before_save -> {
113
+ @was_a_new_record = new_record?
114
+ }
55
115
  self.after_initialize -> {
56
116
  @api_model = Jason::ApiModel.new(self.class.name.underscore)
57
117
  }
58
118
  self.after_commit :publish_json_if_changed
59
-
60
- include_models = Jason::ApiModel.new(self.name.underscore).include_models
61
-
62
- include_models.map do |assoc|
63
- puts assoc
64
- reflection = self.reflect_on_association(assoc.to_sym)
65
- reflection.klass.after_commit -> {
66
- subscribed_fields = Jason::ApiModel.new(self.class.name.underscore).subscribed_fields
67
- puts subscribed_fields.inspect
68
-
69
- if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present?
70
- self.send(reflection.inverse_of.name)&.publish_json
71
- end
72
- }
73
- end
74
119
  end
75
120
 
76
121
  def find_or_create_by_id(params)
@@ -0,0 +1,112 @@
1
+ module Jason::PublisherOld
2
+ extend ActiveSupport::Concern
3
+
4
+ def cache_json
5
+ as_json_config = api_model.as_json_config
6
+ scope = api_model.scope
7
+
8
+ if self.persisted? && (scope.blank? || self.class.unscoped.send(scope).exists?(self.id))
9
+ payload = self.reload.as_json(as_json_config)
10
+ $redis_jason.hset("jason:#{self.class.name.underscore}:cache", self.id, payload.to_json)
11
+ else
12
+ $redis_jason.hdel("jason:#{self.class.name.underscore}:cache", self.id)
13
+ end
14
+ end
15
+
16
+ def publish_json
17
+ cache_json
18
+ return if skip_publish_json
19
+
20
+ self.class.jason_subscriptions.each do |id, config_json|
21
+ config = JSON.parse(config_json)
22
+
23
+ if (config['conditions'] || {}).all? { |field, value| self.send(field) == value }
24
+ Jason::Subscription.new(id: id).update(self.class.name.underscore)
25
+ end
26
+ end
27
+ end
28
+
29
+ def publish_json_if_changed
30
+ subscribed_fields = api_model.subscribed_fields
31
+ publish_json if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present? || !self.persisted?
32
+ end
33
+
34
+ class_methods do
35
+ def subscriptions
36
+ $redis_jason.hgetall("jason:#{self.name.underscore}:subscriptions")
37
+ end
38
+
39
+ def jason_subscriptions
40
+ $redis_jason.hgetall("jason:#{self.name.underscore}:subscriptions")
41
+ end
42
+
43
+ def publish_all(instances)
44
+ instances.each(&:cache_json)
45
+
46
+ subscriptions.each do |id, config_json|
47
+ Jason::Subscription.new(id: id).update(self.name.underscore)
48
+ end
49
+ end
50
+
51
+ def flush_cache
52
+ $redis_jason.del("jason:#{self.name.underscore}:cache")
53
+ end
54
+
55
+ def setup_json
56
+ self.after_initialize -> {
57
+ @api_model = Jason::ApiModel.new(self.class.name.underscore)
58
+ }
59
+ self.after_commit :publish_json_if_changed
60
+
61
+ include_models = Jason::ApiModel.new(self.name.underscore).include_models
62
+
63
+ include_models.map do |assoc|
64
+ puts assoc
65
+ reflection = self.reflect_on_association(assoc.to_sym)
66
+ reflection.klass.after_commit -> {
67
+ subscribed_fields = Jason::ApiModel.new(self.class.name.underscore).subscribed_fields
68
+ puts subscribed_fields.inspect
69
+
70
+ if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present?
71
+ self.send(reflection.inverse_of.name)&.publish_json
72
+ end
73
+ }
74
+ end
75
+ end
76
+
77
+ def find_or_create_by_id(params)
78
+ object = find_by(id: params[:id])
79
+
80
+ if object
81
+ object.update(params)
82
+ elsif params[:hidden]
83
+ return false ## If an object is passed with hidden = true but didn't already exist, it's safe to never create it
84
+ else
85
+ object = create!(params)
86
+ end
87
+
88
+ object
89
+ end
90
+
91
+ def find_or_create_by_id!(params)
92
+ object = find_by(id: params[:id])
93
+
94
+ if object
95
+ object.update!(params)
96
+ elsif params[:hidden]
97
+ ## TODO: We're diverging from semantics of the Rails bang! methods here, which would normally either raise or return an object. Find a way to make this better.
98
+ return false ## If an object is passed with hidden = true but didn't already exist, it's safe to never create it
99
+ else
100
+ object = create!(params)
101
+ end
102
+
103
+ object
104
+ end
105
+ end
106
+
107
+ included do
108
+ attr_accessor :skip_publish_json, :api_model
109
+
110
+ setup_json
111
+ end
112
+ end
@@ -1,31 +1,317 @@
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
- raw_config = $redis_jason.hgetall("jason:subscriptions:#{id}").map { |k,v| [k, JSON.parse(v)] }.to_h.with_indifferent_access
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
- @id = Digest::MD5.hexdigest(config.to_json)
12
+ @id = Digest::MD5.hexdigest(config.sort_by { |key| key }.to_h.to_json)
11
13
  configure(config)
12
14
  end
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
30
+ end
31
+
32
+ def self.upsert_by_config(model, conditions: {}, includes: {})
33
+ self.new(config: {
34
+ model: model,
35
+ conditions: conditions || {},
36
+ includes: includes || {}
37
+ })
38
+ end
39
+
40
+ def self.find_by_id(id)
41
+ self.new(id: id)
42
+ end
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
+
51
+ def self.for_instance(model_name, id, include_all = true)
52
+ subs = $redis_jason.smembers("jason:models:#{model_name}:#{id}:subscriptions")
53
+ if include_all
54
+ subs += $redis_jason.smembers("jason:models:#{model_name}:all:subscriptions")
55
+ end
56
+
57
+ subs
58
+ end
59
+
60
+ def self.for_model(model_name)
61
+
62
+ end
63
+
64
+ # Find and update subscriptions affected by a model changing foreign key
65
+ # comment, comment_id, post, old_post_id, new_post_id
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)
103
+ end
104
+
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
+ })
124
+ end
125
+
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)
146
+ end
147
+
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
166
+ end
167
+
168
+ def self.remove_ids(model_name, ids)
169
+ # td: finish this
170
+ ids.each do |instance_id|
171
+ for_instance(model_name, instance_id, false).each do |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)
177
+ end
178
+ end
179
+ end
180
+
181
+ # Add ID to any _all_ subscriptions
182
+ def self.add_id(model_name, id)
183
+
184
+ end
185
+
186
+ def self.all
187
+ $redis_jason.smembers('jason:subscriptions').map { |id| Jason::Subscription.find_by_id(id) }
13
188
  end
14
189
 
15
190
  def set_config(raw_config)
16
- @config = raw_config.with_indifferent_access.map { |k,v| [k.underscore.to_s, v] }.to_h
191
+ @config = raw_config.deep_stringify_keys.deep_transform_values { |v| v.is_a?(Symbol) ? v.to_s : v }
192
+ end
193
+
194
+ def clear_id(model_name, id, parent_model_name)
195
+ remove_ids(model_name, [id])
196
+ end
197
+
198
+ # Add IDs that aren't present
199
+ def commit_ids(model_name, ids)
200
+ $redis_jason.sadd("jason:subscriptions:#{id}:ids:#{model_name}", ids)
201
+ ids.each do |instance_id|
202
+ $redis_jason.sadd("jason:models:#{model_name}:#{instance_id}:subscriptions", id)
203
+ end
204
+ end
205
+
206
+ def remove_ids(model_name, ids)
207
+ $redis_jason.srem("jason:subscriptions:#{id}:ids:#{model_name}", ids)
208
+ ids.each do |instance_id|
209
+ $redis_jason.srem("jason:models:#{model_name}:#{instance_id}:subscriptions", id)
210
+ end
211
+ end
212
+
213
+ def apply_id_changeset(changeset)
214
+ changeset[:ids_to_add].each do |model_name, ids|
215
+ commit_ids(model_name, ids)
216
+ end
217
+
218
+ changeset[:ids_to_remove].each do |model_name, ids|
219
+ remove_ids(model_name, ids)
220
+ end
221
+ end
222
+
223
+ def broadcast_id_changeset(changeset)
224
+ changeset[:ids_to_add].each do |model_name, ids|
225
+ ids.each { |id| add(model_name, id) }
226
+ end
227
+
228
+ changeset[:ids_to_remove].each do |model_name, ids|
229
+ ids.each { |id| destroy(model_name, id) }
230
+ end
231
+ end
232
+
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)
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)
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)
253
+ end
254
+
255
+ pluck_args = all_models.map { |m| "#{m.pluralize}.id" }
256
+ instance_ids = relation.pluck(*pluck_args)
257
+
258
+ # pluck returns only a 1D array if only 1 arg passed
259
+ if all_models.size == 1
260
+ instance_ids = [instance_ids]
261
+ end
262
+
263
+ return { model_names: all_models, instance_ids: instance_ids }
264
+ end
265
+
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)
269
+
270
+ # Build the tree
271
+ id_changeset = graph_helper.apply_update({
272
+ add: [edge_set]
273
+ })
274
+ apply_id_changeset(id_changeset)
275
+ end
276
+
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)
281
+ end
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)
286
+ end
287
+ $redis_jason.del("jason:subscriptions:#{id}:ids:#{model_name}")
288
+ end
289
+ end
290
+
291
+ def ids(model_name = model)
292
+ $redis_jason.smembers("jason:subscriptions:#{id}:ids:#{model_name}")
293
+ end
294
+
295
+ def model
296
+ @config['model']
297
+ end
298
+
299
+ def model_klass(model_name)
300
+ model_name.to_s.classify.constantize
301
+ end
302
+
303
+ def conditions
304
+ @config['conditions']
17
305
  end
18
306
 
19
307
  def configure(raw_config)
20
308
  set_config(raw_config)
21
- $redis_jason.hmset("jason:subscriptions:#{id}", *config.map { |k,v| [k, v.to_json]}.flatten)
309
+ $redis_jason.sadd("jason:subscriptions", id)
310
+ $redis_jason.hmset("jason:subscriptions:#{id}", *config.map { |k,v| [k, v.to_json] }.flatten)
22
311
  end
23
312
 
24
313
  def destroy
25
- config.each do |model, value|
26
- $redis_jason.srem("jason:#{model.to_s.underscore}:subscriptions", id)
27
- end
28
- $redis_jason.del("jason:subscriptions:#{id}")
314
+ raise
29
315
  end
30
316
 
31
317
  def add_consumer(consumer_id)
@@ -33,8 +319,9 @@ class Jason::Subscription
33
319
  $redis_jason.sadd("jason:subscriptions:#{id}:consumers", consumer_id)
34
320
  $redis_jason.hset("jason:consumers", consumer_id, Time.now.utc)
35
321
 
36
- add_subscriptions
37
- publish_all
322
+ if before_consumer_count == 0
323
+ set_ids_for_sub_models
324
+ end
38
325
  end
39
326
 
40
327
  def remove_consumer(consumer_id)
@@ -42,7 +329,7 @@ class Jason::Subscription
42
329
  $redis_jason.hdel("jason:consumers", consumer_id)
43
330
 
44
331
  if consumer_count == 0
45
- remove_subscriptions
332
+ clear_all_ids
46
333
  end
47
334
  end
48
335
 
@@ -51,119 +338,84 @@ class Jason::Subscription
51
338
  end
52
339
 
53
340
  def channel
54
- "jason:#{id}"
341
+ "jason-#{id}"
55
342
  end
56
343
 
57
- def publish_all
58
- config.each do |model, model_config|
59
- klass = model.to_s.classify.constantize
60
- conditions = model_config['conditions'] || {}
61
- klass.where(conditions).find_each(&:cache_json)
62
- update(model)
63
- end
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])
64
348
  end
65
349
 
66
- def add_subscriptions
67
- config.each do |model, value|
68
- $redis_jason.hset("jason:#{model.to_s.underscore}:subscriptions", id, value.to_json)
69
- update(model)
70
- end
350
+ def get
351
+ includes_helper.all_models.map { |model_name| [model_name, get_for_model(model_name)] }.to_h
71
352
  end
72
353
 
73
- def remove_subscriptions
74
- config.each do |model, _|
75
- $redis_jason.hdel("jason:#{model.to_s.underscore}:subscriptions", id)
354
+ def get_for_model(model_name)
355
+ if $redis_jason.sismember("jason:models:#{model_name}:all:subscriptions", id)
356
+ instance_jsons_hash, idx = $redis_jason.multi do |r|
357
+ r.hgetall("jason:cache:#{model_name}")
358
+ r.get("jason:subscription:#{id}:#{model_name}:idx")
359
+ end
360
+ instance_jsons = instance_jsons_hash.values
361
+ else
362
+ instance_jsons, idx = Jason::LuaGenerator.new.get_payload(model_name, id)
76
363
  end
77
- end
78
364
 
79
- def self.publish_all
80
- JASON_API_MODEL.each do |model, _v|
81
- klass = model.to_s.classify.constantize
82
- klass.publish_all(klass.all) if klass.respond_to?(:publish_all)
365
+ payload = instance_jsons.map do |instance_json|
366
+ instance_json ? JSON.parse(instance_json) : {}
83
367
  end
84
- end
85
-
86
- def get(model)
87
- value = JSON.parse($redis_jason.get("#{channel}:#{model}:value") || '[]')
88
- idx = $redis_jason.get("#{channel}:#{model}:idx").to_i
89
368
 
90
369
  {
91
370
  type: 'payload',
371
+ model: model_name,
372
+ payload: payload,
92
373
  md5Hash: id,
93
- model: model,
94
- value: value,
95
- idx: idx
374
+ idx: idx.to_i
96
375
  }
97
376
  end
98
377
 
99
- def get_diff(old_value, value)
100
- JsonDiff.generate(old_value, value)
101
- end
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) || '{}')
102
381
 
103
- def deep_stringify(value)
104
- if value.is_a?(Hash)
105
- value.deep_stringify_keys
106
- elsif value.is_a?(Array)
107
- value.map { |x| x.deep_stringify_keys }
108
- end
109
- end
382
+ payload = {
383
+ id: instance_id,
384
+ model: model_name,
385
+ payload: payload,
386
+ md5Hash: id,
387
+ idx: idx.to_i
388
+ }
110
389
 
111
- def get_throttle
112
- if !$throttle_rate || !$throttle_timeout || Time.now.utc > $throttle_timeout
113
- $throttle_timeout = Time.now.utc + 5.seconds
114
- $throttle_rate = ($redis_jason.get('global_throttle_rate') || 0).to_i
115
- else
116
- $throttle_rate
117
- end
390
+ broadcaster.broadcast(payload)
118
391
  end
119
392
 
120
- # Atomically update and return patch
121
- def update(model)
122
- start_time = Time.now.utc
123
- conditions = config[model]['conditions']
124
-
125
- value = $redis_jason.hgetall("jason:#{model}:cache")
126
- .values.map { |v| JSON.parse(v) }
127
- .select { |v| (conditions || {}).all? { |field, value| v[field] == value } }
128
- .sort_by { |v| v['id'] }
129
-
130
- # lfsa = last finished, started at
131
- # If another job that started after this one, finished before this one, skip sending this state update
132
- if Time.parse($redis_jason.get("jason:#{channel}:lfsa") || '1970-01-01 00:00:00 UTC') < start_time
133
- $redis_jason.set("jason:#{channel}:lfsa", start_time)
134
- else
135
- return
136
- end
137
-
138
- value = deep_stringify(value)
393
+ def update(model_name, instance_id, payload, gidx)
394
+ idx = Jason::LuaGenerator.new.get_subscription(model_name, instance_id, id, gidx)
395
+ return if idx.blank?
139
396
 
140
- # If value has changed, return old value and new idx. Otherwise do nothing.
141
- cmd = <<~LUA
142
- local old_val=redis.call('get', ARGV[1] .. ':value')
143
- if old_val ~= ARGV[2] then
144
- redis.call('set', ARGV[1] .. ':value', ARGV[2])
145
- local new_idx = redis.call('incr', ARGV[1] .. ':idx')
146
- return { new_idx, old_val }
147
- end
148
- LUA
149
-
150
- result = $redis_jason.eval cmd, [], ["#{channel}:#{model}", value.to_json]
151
- return if result.blank?
397
+ payload = {
398
+ id: instance_id,
399
+ model: model_name,
400
+ payload: payload,
401
+ md5Hash: id,
402
+ idx: idx.to_i
403
+ }
152
404
 
153
- idx = result[0]
154
- old_value = JSON.parse(result[1] || '[]')
155
- diff = get_diff(old_value, value)
405
+ broadcaster.broadcast(payload)
406
+ end
156
407
 
157
- end_time = Time.now.utc
408
+ def destroy(model_name, instance_id)
409
+ idx = $redis_jason.incr("jason:subscription:#{id}:#{model_name}:idx")
158
410
 
159
411
  payload = {
160
- model: model,
412
+ id: instance_id,
413
+ model: model_name,
414
+ destroy: true,
161
415
  md5Hash: id,
162
- diff: diff,
163
- idx: idx.to_i,
164
- latency: ((end_time - start_time)*1000).round
416
+ idx: idx.to_i
165
417
  }
166
418
 
167
- ActionCable.server.broadcast("jason:#{id}", payload)
419
+ broadcaster.broadcast(payload)
168
420
  end
169
421
  end