jason-rails 0.3.0
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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/client/lib/JasonContext.d.ts +2 -0
- data/client/lib/JasonContext.js +5 -0
- data/client/lib/JasonProvider.d.ts +7 -0
- data/client/lib/JasonProvider.js +109 -0
- data/client/lib/actionFactory.d.ts +5 -0
- data/client/lib/actionFactory.js +33 -0
- data/client/lib/createActions.d.ts +2 -0
- data/client/lib/createActions.js +46 -0
- data/client/lib/createJasonReducers.d.ts +1 -0
- data/client/lib/createJasonReducers.js +36 -0
- data/client/lib/createPayloadHandler.d.ts +1 -0
- data/client/lib/createPayloadHandler.js +87 -0
- data/client/lib/index.d.ts +10 -0
- data/client/lib/index.js +12 -0
- data/client/lib/makeEager.d.ts +1 -0
- data/client/lib/makeEager.js +51 -0
- data/client/lib/useAct.d.ts +1 -0
- data/client/lib/useAct.js +12 -0
- data/client/lib/useSub.d.ts +1 -0
- data/client/lib/useSub.js +14 -0
- data/client/package.json +27 -0
- data/client/src/JasonContext.ts +5 -0
- data/client/src/JasonProvider.tsx +108 -0
- data/client/src/actionFactory.ts +34 -0
- data/client/src/createActions.ts +50 -0
- data/client/src/createJasonReducers.ts +34 -0
- data/client/src/createPayloadHandler.ts +95 -0
- data/client/src/index.ts +7 -0
- data/client/src/makeEager.ts +46 -0
- data/client/src/useAct.ts +9 -0
- data/client/src/useSub.ts +10 -0
- data/client/tsconfig.json +15 -0
- data/client/yarn.lock +140 -0
- data/jason-rails.gemspec +25 -0
- data/lib/jason.rb +10 -0
- data/lib/jason/api_model.rb +47 -0
- data/lib/jason/channel.rb +37 -0
- data/lib/jason/publisher.rb +79 -0
- data/lib/jason/subscription.rb +172 -0
- data/lib/jason/version.rb +3 -0
- 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
|
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: []
|