jason-rails 0.4.1 → 0.6.2

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 (72) 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/JasonContext.js +4 -1
  10. data/client/lib/JasonProvider.d.ts +2 -2
  11. data/client/lib/JasonProvider.js +5 -124
  12. data/client/lib/createJasonReducers.js +48 -3
  13. data/client/lib/createOptDis.js +0 -2
  14. data/client/lib/createPayloadHandler.d.ts +9 -1
  15. data/client/lib/createPayloadHandler.js +47 -55
  16. data/client/lib/createServerActionQueue.d.ts +10 -0
  17. data/client/lib/createServerActionQueue.js +48 -0
  18. data/client/lib/createServerActionQueue.test.d.ts +1 -0
  19. data/client/lib/createServerActionQueue.test.js +37 -0
  20. data/client/lib/createTransportAdapter.d.ts +5 -0
  21. data/client/lib/createTransportAdapter.js +20 -0
  22. data/client/lib/index.d.ts +5 -2
  23. data/client/lib/index.js +3 -1
  24. data/client/lib/makeEager.js +2 -2
  25. data/client/lib/pruneIdsMiddleware.d.ts +2 -0
  26. data/client/lib/pruneIdsMiddleware.js +24 -0
  27. data/client/lib/restClient.d.ts +2 -0
  28. data/client/lib/restClient.js +17 -0
  29. data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
  30. data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
  31. data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
  32. data/client/lib/transportAdapters/pusherAdapter.js +68 -0
  33. data/client/lib/useJason.d.ts +5 -0
  34. data/client/lib/useJason.js +94 -0
  35. data/client/lib/useJason.test.d.ts +1 -0
  36. data/client/lib/useJason.test.js +85 -0
  37. data/client/lib/useSub.d.ts +1 -1
  38. data/client/lib/useSub.js +6 -3
  39. data/client/package.json +5 -3
  40. data/client/src/JasonContext.ts +4 -1
  41. data/client/src/JasonProvider.tsx +5 -123
  42. data/client/src/createJasonReducers.ts +56 -3
  43. data/client/src/createOptDis.ts +0 -2
  44. data/client/src/createPayloadHandler.ts +53 -64
  45. data/client/src/createServerActionQueue.test.ts +42 -0
  46. data/client/src/createServerActionQueue.ts +47 -0
  47. data/client/src/createTransportAdapter.ts +13 -0
  48. data/client/src/index.ts +3 -1
  49. data/client/src/makeEager.ts +2 -2
  50. data/client/src/pruneIdsMiddleware.ts +24 -0
  51. data/client/src/restClient.ts +14 -0
  52. data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
  53. data/client/src/transportAdapters/pusherAdapter.ts +72 -0
  54. data/client/src/useJason.test.ts +87 -0
  55. data/client/src/useJason.ts +110 -0
  56. data/client/src/useSub.ts +6 -3
  57. data/client/yarn.lock +71 -3
  58. data/config/routes.rb +5 -1
  59. data/jason-rails.gemspec +4 -0
  60. data/lib/jason.rb +61 -1
  61. data/lib/jason/api_model.rb +2 -12
  62. data/lib/jason/broadcaster.rb +19 -0
  63. data/lib/jason/channel.rb +50 -21
  64. data/lib/jason/graph_helper.rb +165 -0
  65. data/lib/jason/includes_helper.rb +108 -0
  66. data/lib/jason/lua_generator.rb +71 -0
  67. data/lib/jason/publisher.rb +82 -37
  68. data/lib/jason/publisher_old.rb +112 -0
  69. data/lib/jason/subscription.rb +349 -97
  70. data/lib/jason/subscription_old.rb +171 -0
  71. data/lib/jason/version.rb +1 -1
  72. metadata +80 -3
@@ -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
@@ -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