jason-rails 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +52 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/client/lib/JasonContext.d.ts +2 -0
  13. data/client/lib/JasonContext.js +5 -0
  14. data/client/lib/JasonProvider.d.ts +7 -0
  15. data/client/lib/JasonProvider.js +109 -0
  16. data/client/lib/actionFactory.d.ts +5 -0
  17. data/client/lib/actionFactory.js +33 -0
  18. data/client/lib/createActions.d.ts +2 -0
  19. data/client/lib/createActions.js +46 -0
  20. data/client/lib/createJasonReducers.d.ts +1 -0
  21. data/client/lib/createJasonReducers.js +36 -0
  22. data/client/lib/createPayloadHandler.d.ts +1 -0
  23. data/client/lib/createPayloadHandler.js +87 -0
  24. data/client/lib/index.d.ts +10 -0
  25. data/client/lib/index.js +12 -0
  26. data/client/lib/makeEager.d.ts +1 -0
  27. data/client/lib/makeEager.js +51 -0
  28. data/client/lib/useAct.d.ts +1 -0
  29. data/client/lib/useAct.js +12 -0
  30. data/client/lib/useSub.d.ts +1 -0
  31. data/client/lib/useSub.js +14 -0
  32. data/client/package.json +27 -0
  33. data/client/src/JasonContext.ts +5 -0
  34. data/client/src/JasonProvider.tsx +108 -0
  35. data/client/src/actionFactory.ts +34 -0
  36. data/client/src/createActions.ts +50 -0
  37. data/client/src/createJasonReducers.ts +34 -0
  38. data/client/src/createPayloadHandler.ts +95 -0
  39. data/client/src/index.ts +7 -0
  40. data/client/src/makeEager.ts +46 -0
  41. data/client/src/useAct.ts +9 -0
  42. data/client/src/useSub.ts +10 -0
  43. data/client/tsconfig.json +15 -0
  44. data/client/yarn.lock +140 -0
  45. data/jason-rails.gemspec +25 -0
  46. data/lib/jason.rb +10 -0
  47. data/lib/jason/api_model.rb +47 -0
  48. data/lib/jason/channel.rb +37 -0
  49. data/lib/jason/publisher.rb +79 -0
  50. data/lib/jason/subscription.rb +172 -0
  51. data/lib/jason/version.rb +3 -0
  52. metadata +96 -0
@@ -0,0 +1,37 @@
1
+ class Jason::Channel < ActionCable::Channel::Base
2
+ attr_accessor :subscriptions
3
+
4
+ def receive(message)
5
+ subscriptions ||= []
6
+
7
+ begin # ActionCable swallows errors in this message - ensure they're output to logs.
8
+ if (config = message['createSubscription'])
9
+ subscription = Jason::Subscription.new(config: config)
10
+ subscriptions.push(subscription)
11
+ subscription.add_consumer(identifier)
12
+ config.keys.each do |model|
13
+ transmit(subscription.get(model.to_s.underscore))
14
+ end
15
+ stream_from subscription.channel
16
+ elsif (config = message['removeSubscription'])
17
+ subscription = Jason::Subscription.new(config: config)
18
+ subscriptions.reject! { |s| s.id == subscription.id }
19
+ subscription.remove_consumer(identifier)
20
+
21
+ # Rails for some reason removed stop_stream_from, so we need to stop all and then restart the other streams
22
+ # stop_all_streams
23
+ # subscriptions.each do |s|
24
+ # stream_from s.channel
25
+ # end
26
+ elsif (data = message['getPayload'])
27
+ config = data.config
28
+ model = data.model
29
+ Jason::Subscription.new(config: config).get(model.to_s.underscore)
30
+ end
31
+ rescue => e
32
+ puts e.message
33
+ puts e.backtrace
34
+ raise e
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,79 @@
1
+ module Jason::Publisher
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.hset("jason:#{self.class.name.underscore}:cache", self.id, payload.to_json)
11
+ else
12
+ $redis.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
+ subscriptions = $redis.hgetall("jason:#{self.class.name.underscore}:subscriptions")
20
+ 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.hgetall("jason:#{self.name.underscore}:subscriptions")
37
+ end
38
+
39
+ def publish_all(instances)
40
+ instances.each(&:cache_json)
41
+
42
+ subscriptions.each do |id, config_json|
43
+ Jason::Subscription.new(id: id).update(self.name.underscore)
44
+ end
45
+ end
46
+
47
+ def flush_cache
48
+ $redis.del("jason:#{self.name.underscore}:cache")
49
+ end
50
+
51
+ def setup_json
52
+ self.after_initialize -> {
53
+ @api_model = Jason::ApiModel.new(self.class.name.underscore)
54
+ }
55
+ self.after_commit :publish_json_if_changed
56
+
57
+ include_models = Jason::ApiModel.new(self.name.underscore).include_models
58
+
59
+ include_models.map do |assoc|
60
+ puts assoc
61
+ reflection = self.reflect_on_association(assoc.to_sym)
62
+ reflection.klass.after_commit -> {
63
+ subscribed_fields = Jason::ApiModel.new(self.class.name.underscore).subscribed_fields
64
+ puts subscribed_fields.inspect
65
+
66
+ if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present?
67
+ self.send(reflection.inverse_of.name)&.publish_json
68
+ end
69
+ }
70
+ end
71
+ end
72
+ end
73
+
74
+ included do
75
+ attr_accessor :skip_publish_json, :api_model
76
+
77
+ setup_json
78
+ end
79
+ end
@@ -0,0 +1,172 @@
1
+ class Jason::Subscription
2
+ attr_accessor :id, :config
3
+
4
+ def initialize(id: nil, config: nil)
5
+ if id
6
+ @id = id
7
+ raw_config = $redis.hgetall("jason:subscriptions:#{id}").map { |k,v| [k, JSON.parse(v)] }.to_h.with_indifferent_access
8
+ set_config(raw_config)
9
+ else
10
+ @id = Digest::MD5.hexdigest(config.to_json)
11
+ configure(config)
12
+ end
13
+ end
14
+
15
+ def set_config(raw_config)
16
+ @config = raw_config.with_indifferent_access.map { |k,v| [k.underscore.to_s, v] }.to_h
17
+ end
18
+
19
+ def configure(raw_config)
20
+ set_config(raw_config)
21
+ $redis.hmset("jason:subscriptions:#{id}", *config.map { |k,v| [k, v.to_json]}.flatten)
22
+ end
23
+
24
+ def destroy
25
+ config.each do |model, value|
26
+ $redis.srem("jason:#{model.to_s.underscore}:subscriptions", id)
27
+ end
28
+ $redis.del("jason:subscriptions:#{id}")
29
+ end
30
+
31
+ def add_consumer(consumer_id)
32
+ before_consumer_count = consumer_count
33
+ $redis.sadd("jason:subscriptions:#{id}:consumers", consumer_id)
34
+ $redis.hset("jason:consumers", consumer_id, Time.now.utc)
35
+
36
+ if before_consumer_count == 0
37
+ add_subscriptions
38
+ publish_all
39
+ end
40
+ end
41
+
42
+ def remove_consumer(consumer_id)
43
+ $redis.srem("jason:subscriptions:#{id}:consumers", consumer_id)
44
+ $redis.hdel("jason:consumers", consumer_id)
45
+
46
+ if consumer_count == 0
47
+ remove_subscriptions
48
+ end
49
+ end
50
+
51
+ def consumer_count
52
+ $redis.scard("jason:subscriptions:#{id}:consumers")
53
+ end
54
+
55
+ def channel
56
+ "jason:#{id}"
57
+ end
58
+
59
+ def publish_all
60
+ config.each do |model, model_config|
61
+ klass = model.to_s.classify.constantize
62
+ conditions = model_config['conditions'] || {}
63
+ klass.where(conditions).find_each(&:cache_json)
64
+ update(model)
65
+ end
66
+ end
67
+
68
+ def add_subscriptions
69
+ config.each do |model, value|
70
+ $redis.hset("jason:#{model.to_s.underscore}:subscriptions", id, value.to_json)
71
+ update(model)
72
+ end
73
+ end
74
+
75
+ def remove_subscriptions
76
+ config.each do |model, _|
77
+ $redis.hdel("jason:#{model.to_s.underscore}:subscriptions", id)
78
+ end
79
+ end
80
+
81
+ def self.publish_all
82
+ JASON_API_MODEL.each do |model, _v|
83
+ klass = model.to_s.classify.constantize
84
+ klass.publish_all(klass.all) if klass.respond_to?(:publish_all)
85
+ end
86
+ end
87
+
88
+ def get(model)
89
+ value = JSON.parse($redis.get("#{channel}:#{model}:value") || '{}')
90
+ idx = $redis.get("#{channel}:#{model}:idx").to_i
91
+
92
+ {
93
+ type: 'payload',
94
+ md5Hash: id,
95
+ model: model,
96
+ value: value,
97
+ idx: idx
98
+ }
99
+ end
100
+
101
+ def get_diff(old_value, value)
102
+ JsonDiff.generate(old_value, value)
103
+ end
104
+
105
+ def deep_stringify(value)
106
+ if value.is_a?(Hash)
107
+ value.deep_stringify_keys
108
+ elsif value.is_a?(Array)
109
+ value.map { |x| x.deep_stringify_keys }
110
+ end
111
+ end
112
+
113
+ def get_throttle
114
+ if !$throttle_rate || !$throttle_timeout || Time.now.utc > $throttle_timeout
115
+ $throttle_timeout = Time.now.utc + 5.seconds
116
+ $throttle_rate = (Sidekiq.redis { |r| r.get 'global_throttle_rate' } || 0).to_i
117
+ else
118
+ $throttle_rate
119
+ end
120
+ end
121
+
122
+ # Atomically update and return patch
123
+ def update(model)
124
+ start_time = Time.now.utc
125
+ conditions = config[model]['conditions']
126
+
127
+ value = $redis.hgetall("jason:#{model}:cache")
128
+ .values.map { |v| JSON.parse(v) }
129
+ .select { |v| (conditions || {}).all? { |field, value| v[field] == value } }
130
+ .sort_by { |v| v['id'] }
131
+
132
+ # lfsa = last finished, started at
133
+ # If another job that started after this one, finished before this one, skip sending this state update
134
+ if Time.parse(Sidekiq.redis { |r| r.get("jason:#{channel}:lfsa") || '1970-01-01 00:00:00 UTC' } ) < start_time
135
+ Sidekiq.redis { |r| r.set("jason:#{channel}:lfsa", start_time) }
136
+ else
137
+ return
138
+ end
139
+
140
+ value = deep_stringify(value)
141
+
142
+ # If value has changed, return old value and new idx. Otherwise do nothing.
143
+ cmd = <<~LUA
144
+ local old_val=redis.call('get', ARGV[1] .. ':value')
145
+ if old_val ~= ARGV[2] then
146
+ redis.call('set', ARGV[1] .. ':value', ARGV[2])
147
+ local new_idx = redis.call('incr', ARGV[1] .. ':idx')
148
+ return { new_idx, old_val }
149
+ end
150
+ LUA
151
+
152
+ result = $redis.eval cmd, [], ["#{channel}:#{model}", value.to_json]
153
+ return if result.blank?
154
+
155
+ idx = result[0]
156
+ old_value = JSON.parse(result[1] || '{}')
157
+
158
+ diff = get_diff(old_value, value)
159
+
160
+ end_time = Time.now.utc
161
+
162
+ payload = {
163
+ model: model,
164
+ md5Hash: id,
165
+ diff: diff,
166
+ idx: idx.to_i,
167
+ latency: ((end_time - start_time)*1000).round
168
+ }
169
+
170
+ ActionCable.server.broadcast("jason:#{id}", payload)
171
+ end
172
+ end
@@ -0,0 +1,3 @@
1
+ module Jason
2
+ VERSION = "0.3.0"
3
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jason-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - James Rees
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-12-04 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - jarees@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - ".rspec"
22
+ - ".travis.yml"
23
+ - CODE_OF_CONDUCT.md
24
+ - Gemfile
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - bin/console
29
+ - bin/setup
30
+ - client/lib/JasonContext.d.ts
31
+ - client/lib/JasonContext.js
32
+ - client/lib/JasonProvider.d.ts
33
+ - client/lib/JasonProvider.js
34
+ - client/lib/actionFactory.d.ts
35
+ - client/lib/actionFactory.js
36
+ - client/lib/createActions.d.ts
37
+ - client/lib/createActions.js
38
+ - client/lib/createJasonReducers.d.ts
39
+ - client/lib/createJasonReducers.js
40
+ - client/lib/createPayloadHandler.d.ts
41
+ - client/lib/createPayloadHandler.js
42
+ - client/lib/index.d.ts
43
+ - client/lib/index.js
44
+ - client/lib/makeEager.d.ts
45
+ - client/lib/makeEager.js
46
+ - client/lib/useAct.d.ts
47
+ - client/lib/useAct.js
48
+ - client/lib/useSub.d.ts
49
+ - client/lib/useSub.js
50
+ - client/package.json
51
+ - client/src/JasonContext.ts
52
+ - client/src/JasonProvider.tsx
53
+ - client/src/actionFactory.ts
54
+ - client/src/createActions.ts
55
+ - client/src/createJasonReducers.ts
56
+ - client/src/createPayloadHandler.ts
57
+ - client/src/index.ts
58
+ - client/src/makeEager.ts
59
+ - client/src/useAct.ts
60
+ - client/src/useSub.ts
61
+ - client/tsconfig.json
62
+ - client/yarn.lock
63
+ - jason-rails.gemspec
64
+ - lib/jason.rb
65
+ - lib/jason/api_model.rb
66
+ - lib/jason/channel.rb
67
+ - lib/jason/publisher.rb
68
+ - lib/jason/subscription.rb
69
+ - lib/jason/version.rb
70
+ homepage: https://github.com/jamesr2323/jason
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/jamesr2323/jason
75
+ source_code_uri: https://github.com/jamesr2323/jason
76
+ changelog_uri: https://github.com/jamesr2323/jason
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 2.3.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.0.8
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Reactive user interfaces with minimal boilerplate, using Rails + Redux
96
+ test_files: []