jason-rails 0.3.0 → 0.6.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 +4 -4
- data/.gitignore +4 -1
- data/.ruby-version +1 -0
- data/Gemfile.lock +184 -0
- data/README.md +118 -10
- data/app/controllers/jason/api/pusher_controller.rb +15 -0
- data/app/controllers/jason/api_controller.rb +78 -0
- data/client/babel.config.js +13 -0
- data/client/lib/JasonContext.d.ts +6 -1
- data/client/lib/JasonProvider.d.ts +6 -5
- data/client/lib/JasonProvider.js +5 -97
- data/client/lib/actionFactory.js +1 -1
- data/client/lib/createActions.d.ts +1 -1
- data/client/lib/createActions.js +2 -27
- data/client/lib/createJasonReducers.js +49 -3
- data/client/lib/createOptDis.d.ts +1 -0
- data/client/lib/createOptDis.js +43 -0
- data/client/lib/createPayloadHandler.d.ts +9 -1
- data/client/lib/createPayloadHandler.js +52 -43
- data/client/lib/createServerActionQueue.d.ts +10 -0
- data/client/lib/createServerActionQueue.js +48 -0
- data/client/lib/createServerActionQueue.test.d.ts +1 -0
- data/client/lib/createServerActionQueue.test.js +37 -0
- data/client/lib/createTransportAdapter.d.ts +5 -0
- data/client/lib/createTransportAdapter.js +20 -0
- data/client/lib/deepCamelizeKeys.d.ts +1 -0
- data/client/lib/deepCamelizeKeys.js +23 -0
- data/client/lib/deepCamelizeKeys.test.d.ts +1 -0
- data/client/lib/deepCamelizeKeys.test.js +106 -0
- data/client/lib/index.d.ts +6 -5
- data/client/lib/pruneIdsMiddleware.d.ts +2 -0
- data/client/lib/pruneIdsMiddleware.js +24 -0
- data/client/lib/restClient.d.ts +2 -0
- data/client/lib/restClient.js +17 -0
- data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
- data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
- data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
- data/client/lib/transportAdapters/pusherAdapter.js +68 -0
- data/client/lib/useJason.d.ts +5 -0
- data/client/lib/useJason.js +94 -0
- data/client/lib/useJason.test.d.ts +1 -0
- data/client/lib/useJason.test.js +85 -0
- data/client/lib/useSub.d.ts +1 -1
- data/client/lib/useSub.js +6 -3
- data/client/package.json +19 -4
- data/client/src/JasonProvider.tsx +6 -96
- data/client/src/actionFactory.ts +1 -1
- data/client/src/createActions.ts +2 -33
- data/client/src/createJasonReducers.ts +57 -3
- data/client/src/createOptDis.ts +45 -0
- data/client/src/createPayloadHandler.ts +58 -47
- data/client/src/createServerActionQueue.test.ts +42 -0
- data/client/src/createServerActionQueue.ts +47 -0
- data/client/src/createTransportAdapter.ts +13 -0
- data/client/src/deepCamelizeKeys.test.ts +113 -0
- data/client/src/deepCamelizeKeys.ts +17 -0
- data/client/src/pruneIdsMiddleware.ts +24 -0
- data/client/src/restClient.ts +14 -0
- data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
- data/client/src/transportAdapters/pusherAdapter.ts +72 -0
- data/client/src/useJason.test.ts +87 -0
- data/client/src/useJason.ts +110 -0
- data/client/src/useSub.ts +6 -3
- data/client/yarn.lock +4607 -81
- data/config/routes.rb +8 -0
- data/jason-rails.gemspec +9 -0
- data/lib/jason.rb +40 -1
- data/lib/jason/api_model.rb +15 -9
- data/lib/jason/broadcaster.rb +19 -0
- data/lib/jason/channel.rb +50 -21
- data/lib/jason/engine.rb +5 -0
- data/lib/jason/graph_helper.rb +165 -0
- data/lib/jason/includes_helper.rb +108 -0
- data/lib/jason/lua_generator.rb +71 -0
- data/lib/jason/publisher.rb +103 -30
- data/lib/jason/publisher_old.rb +112 -0
- data/lib/jason/subscription.rb +352 -101
- data/lib/jason/subscription_old.rb +171 -0
- data/lib/jason/version.rb +1 -1
- metadata +151 -4
@@ -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
|
data/lib/jason/publisher.rb
CHANGED
@@ -1,73 +1,146 @@
|
|
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
|
-
|
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
|
-
$
|
23
|
+
$redis_jason.hdel("jason:cache:#{self.class.name.underscore}", self.id)
|
24
|
+
return []
|
13
25
|
end
|
14
26
|
end
|
15
27
|
|
16
|
-
def
|
17
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
22
43
|
|
23
|
-
|
24
|
-
|
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
|
72
|
+
|
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)
|
25
81
|
end
|
26
82
|
end
|
27
83
|
end
|
28
84
|
|
29
85
|
def publish_json_if_changed
|
30
86
|
subscribed_fields = api_model.subscribed_fields
|
31
|
-
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
|
+
end
|
89
|
+
|
90
|
+
def jason_subscriptions
|
91
|
+
Jason::Subscription.for_instance(self.class.name.underscore, id)
|
32
92
|
end
|
33
93
|
|
34
94
|
class_methods do
|
35
|
-
def
|
36
|
-
|
95
|
+
def cache_all
|
96
|
+
all.each(&:cache_json)
|
37
97
|
end
|
38
98
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
subscriptions.each do |id, config_json|
|
43
|
-
Jason::Subscription.new(id: id).update(self.name.underscore)
|
44
|
-
end
|
99
|
+
def has_jason?
|
100
|
+
true
|
45
101
|
end
|
46
102
|
|
47
103
|
def flush_cache
|
48
|
-
$
|
104
|
+
$redis_jason.del("jason:cache:#{self.name.underscore}")
|
49
105
|
end
|
50
106
|
|
51
107
|
def setup_json
|
108
|
+
self.before_save -> {
|
109
|
+
@was_a_new_record = new_record?
|
110
|
+
}
|
52
111
|
self.after_initialize -> {
|
53
112
|
@api_model = Jason::ApiModel.new(self.class.name.underscore)
|
54
113
|
}
|
55
114
|
self.after_commit :publish_json_if_changed
|
115
|
+
end
|
116
|
+
|
117
|
+
def find_or_create_by_id(params)
|
118
|
+
object = find_by(id: params[:id])
|
119
|
+
|
120
|
+
if object
|
121
|
+
object.update(params)
|
122
|
+
elsif params[:hidden]
|
123
|
+
return false ## If an object is passed with hidden = true but didn't already exist, it's safe to never create it
|
124
|
+
else
|
125
|
+
object = create!(params)
|
126
|
+
end
|
56
127
|
|
57
|
-
|
128
|
+
object
|
129
|
+
end
|
58
130
|
|
59
|
-
|
60
|
-
|
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
|
131
|
+
def find_or_create_by_id!(params)
|
132
|
+
object = find_by(id: params[:id])
|
65
133
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
134
|
+
if object
|
135
|
+
object.update!(params)
|
136
|
+
elsif params[:hidden]
|
137
|
+
## 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.
|
138
|
+
return false ## If an object is passed with hidden = true but didn't already exist, it's safe to never create it
|
139
|
+
else
|
140
|
+
object = create!(params)
|
70
141
|
end
|
142
|
+
|
143
|
+
object
|
71
144
|
end
|
72
145
|
end
|
73
146
|
|
@@ -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
|
data/lib/jason/subscription.rb
CHANGED
@@ -1,172 +1,423 @@
|
|
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 = $
|
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
|
13
19
|
end
|
14
20
|
|
15
|
-
def
|
16
|
-
@
|
21
|
+
def broadcaster
|
22
|
+
@broadcaster ||= Jason::Broadcaster.new(channel)
|
17
23
|
end
|
18
24
|
|
19
|
-
def
|
20
|
-
|
21
|
-
|
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
|
22
30
|
end
|
23
31
|
|
24
|
-
def
|
25
|
-
config
|
26
|
-
|
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)
|
27
48
|
end
|
28
|
-
$redis.del("jason:subscriptions:#{id}")
|
29
49
|
end
|
30
50
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
35
56
|
|
36
|
-
|
37
|
-
|
38
|
-
|
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)
|
39
165
|
end
|
40
166
|
end
|
41
167
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
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)
|
45
173
|
|
46
|
-
|
47
|
-
|
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
|
48
178
|
end
|
49
179
|
end
|
50
180
|
|
51
|
-
|
52
|
-
|
181
|
+
# Add ID to any _all_ subscriptions
|
182
|
+
def self.add_id(model_name, id)
|
183
|
+
|
53
184
|
end
|
54
185
|
|
55
|
-
def
|
56
|
-
|
186
|
+
def self.all
|
187
|
+
$redis_jason.smembers('jason:subscriptions').map { |id| Jason::Subscription.find_by_id(id) }
|
57
188
|
end
|
58
189
|
|
59
|
-
def
|
60
|
-
config.
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
190
|
+
def set_config(raw_config)
|
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)
|
65
203
|
end
|
66
204
|
end
|
67
205
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
|
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)
|
72
210
|
end
|
73
211
|
end
|
74
212
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
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)
|
78
220
|
end
|
79
221
|
end
|
80
222
|
|
81
|
-
def
|
82
|
-
|
83
|
-
|
84
|
-
|
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) }
|
85
230
|
end
|
86
231
|
end
|
87
232
|
|
88
|
-
|
89
|
-
|
90
|
-
|
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)
|
91
239
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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 }
|
99
264
|
end
|
100
265
|
|
101
|
-
|
102
|
-
|
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)
|
103
275
|
end
|
104
276
|
|
105
|
-
def
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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}")
|
110
288
|
end
|
111
289
|
end
|
112
290
|
|
113
|
-
def
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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']
|
305
|
+
end
|
306
|
+
|
307
|
+
def configure(raw_config)
|
308
|
+
set_config(raw_config)
|
309
|
+
$redis_jason.sadd("jason:subscriptions", id)
|
310
|
+
$redis_jason.hmset("jason:subscriptions:#{id}", *config.map { |k,v| [k, v.to_json] }.flatten)
|
311
|
+
end
|
312
|
+
|
313
|
+
def destroy
|
314
|
+
raise
|
315
|
+
end
|
316
|
+
|
317
|
+
def add_consumer(consumer_id)
|
318
|
+
before_consumer_count = consumer_count
|
319
|
+
$redis_jason.sadd("jason:subscriptions:#{id}:consumers", consumer_id)
|
320
|
+
$redis_jason.hset("jason:consumers", consumer_id, Time.now.utc)
|
321
|
+
|
322
|
+
if before_consumer_count == 0
|
323
|
+
set_ids_for_sub_models
|
119
324
|
end
|
120
325
|
end
|
121
326
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
conditions = config[model]['conditions']
|
327
|
+
def remove_consumer(consumer_id)
|
328
|
+
$redis_jason.srem("jason:subscriptions:#{id}:consumers", consumer_id)
|
329
|
+
$redis_jason.hdel("jason:consumers", consumer_id)
|
126
330
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
331
|
+
if consumer_count == 0
|
332
|
+
clear_all_ids
|
333
|
+
end
|
334
|
+
end
|
131
335
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
336
|
+
def consumer_count
|
337
|
+
$redis_jason.scard("jason:subscriptions:#{id}:consumers")
|
338
|
+
end
|
339
|
+
|
340
|
+
def channel
|
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])
|
348
|
+
end
|
349
|
+
|
350
|
+
def get
|
351
|
+
includes_helper.all_models.map { |model_name| [model_name, get_for_model(model_name)] }.to_h
|
352
|
+
end
|
353
|
+
|
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
|
136
361
|
else
|
137
|
-
|
362
|
+
instance_jsons, idx = Jason::LuaGenerator.new.get_payload(model_name, id)
|
138
363
|
end
|
139
364
|
|
140
|
-
|
365
|
+
return if instance_jsons.blank?
|
141
366
|
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
367
|
+
payload = instance_jsons.map do |instance_json|
|
368
|
+
instance_json ? JSON.parse(instance_json) : {}
|
369
|
+
end
|
151
370
|
|
152
|
-
|
153
|
-
|
371
|
+
{
|
372
|
+
type: 'payload',
|
373
|
+
model: model_name,
|
374
|
+
payload: payload,
|
375
|
+
md5Hash: id,
|
376
|
+
idx: idx.to_i
|
377
|
+
}
|
378
|
+
end
|
154
379
|
|
155
|
-
|
156
|
-
|
380
|
+
def add(model_name, instance_id)
|
381
|
+
idx = $redis_jason.incr("jason:subscription:#{id}:#{model_name}:idx")
|
382
|
+
payload = JSON.parse($redis_jason.hget("jason:cache:#{model_name}", instance_id) || '{}')
|
157
383
|
|
158
|
-
|
384
|
+
payload = {
|
385
|
+
id: instance_id,
|
386
|
+
model: model_name,
|
387
|
+
payload: payload,
|
388
|
+
md5Hash: id,
|
389
|
+
idx: idx.to_i
|
390
|
+
}
|
159
391
|
|
160
|
-
|
392
|
+
broadcaster.broadcast(payload)
|
393
|
+
end
|
394
|
+
|
395
|
+
def update(model_name, instance_id, payload, gidx)
|
396
|
+
idx = Jason::LuaGenerator.new.get_subscription(model_name, instance_id, id, gidx)
|
397
|
+
return if idx.blank?
|
161
398
|
|
162
399
|
payload = {
|
163
|
-
|
400
|
+
id: instance_id,
|
401
|
+
model: model_name,
|
402
|
+
payload: payload,
|
403
|
+
md5Hash: id,
|
404
|
+
idx: idx.to_i
|
405
|
+
}
|
406
|
+
|
407
|
+
broadcaster.broadcast(payload)
|
408
|
+
end
|
409
|
+
|
410
|
+
def destroy(model_name, instance_id)
|
411
|
+
idx = $redis_jason.incr("jason:subscription:#{id}:#{model_name}:idx")
|
412
|
+
|
413
|
+
payload = {
|
414
|
+
id: instance_id,
|
415
|
+
model: model_name,
|
416
|
+
destroy: true,
|
164
417
|
md5Hash: id,
|
165
|
-
|
166
|
-
idx: idx.to_i,
|
167
|
-
latency: ((end_time - start_time)*1000).round
|
418
|
+
idx: idx.to_i
|
168
419
|
}
|
169
420
|
|
170
|
-
|
421
|
+
broadcaster.broadcast(payload)
|
171
422
|
end
|
172
423
|
end
|