jason-rails 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/Gemfile.lock +7 -2
  4. data/README.md +4 -12
  5. data/app/controllers/jason/{api_controller.rb → jason_controller.rb} +1 -1
  6. data/app/controllers/jason/{api/pusher_controller.rb → pusher_controller.rb} +1 -1
  7. data/app/workers/jason/outbound_message_queue_worker.rb +21 -0
  8. data/client/lib/addRelations.d.ts +1 -0
  9. data/client/lib/addRelations.js +39 -0
  10. data/client/lib/createJasonReducers.js +4 -2
  11. data/client/lib/createOptDis.d.ts +1 -1
  12. data/client/lib/createOptDis.js +9 -8
  13. data/client/lib/createServerActionQueue.d.ts +3 -2
  14. data/client/lib/createServerActionQueue.js +32 -6
  15. data/client/lib/createServerActionQueue.test.js +61 -6
  16. data/client/lib/createThenable.d.ts +1 -0
  17. data/client/lib/createThenable.js +5 -0
  18. data/client/lib/transportAdapters/actionCableAdapter.js +24 -4
  19. data/client/lib/transportAdapters/pusherAdapter.js +1 -1
  20. data/client/lib/useDraft.d.ts +1 -0
  21. data/client/lib/useDraft.js +13 -0
  22. data/client/lib/useEager.d.ts +1 -1
  23. data/client/lib/useEager.js +10 -5
  24. data/client/lib/useJason.js +2 -4
  25. data/client/package.json +1 -1
  26. data/client/src/addRelations.ts +33 -0
  27. data/client/src/createJasonReducers.ts +4 -2
  28. data/client/src/createOptDis.ts +10 -8
  29. data/client/src/createServerActionQueue.test.ts +60 -6
  30. data/client/src/createServerActionQueue.ts +41 -6
  31. data/client/src/transportAdapters/actionCableAdapter.ts +24 -5
  32. data/client/src/transportAdapters/pusherAdapter.ts +1 -2
  33. data/client/src/useDraft.ts +17 -0
  34. data/client/src/useEager.ts +9 -6
  35. data/client/src/useJason.ts +1 -4
  36. data/config/routes.rb +6 -6
  37. data/jason-rails.gemspec +1 -0
  38. data/lib/jason.rb +7 -3
  39. data/lib/jason/api_model.rb +0 -4
  40. data/lib/jason/broadcaster.rb +2 -1
  41. data/lib/jason/channel.rb +0 -7
  42. data/lib/jason/conditions_matcher.rb +88 -0
  43. data/lib/jason/consistency_checker.rb +61 -0
  44. data/lib/jason/graph_helper.rb +19 -4
  45. data/lib/jason/publisher.rb +40 -7
  46. data/lib/jason/subscription.rb +77 -17
  47. data/lib/jason/version.rb +1 -1
  48. metadata +30 -5
  49. data/client/src/makeEager.ts +0 -46
@@ -1,11 +1,10 @@
1
1
  import Pusher from 'pusher-js'
2
- import { createConsumer } from "@rails/actioncable"
3
2
  import restClient from '../restClient'
4
3
  import { v4 as uuidv4 } from 'uuid'
5
4
  import _ from 'lodash'
6
5
 
7
6
  export default function pusherAdapter(jasonConfig, handlePayload, dispatch) {
8
- let consumerId = uuidv4()
7
+ const consumerId = uuidv4()
9
8
 
10
9
  const { pusherKey, pusherRegion, pusherChannelPrefix } = jasonConfig
11
10
  const pusher = new Pusher(pusherKey, {
@@ -0,0 +1,17 @@
1
+ import _ from 'lodash'
2
+ import { useSelector } from 'react-redux'
3
+ import addRelations from './addRelations'
4
+
5
+ /* Can be called as
6
+ useDraft() => draft object for making updates
7
+ useDraft('entity', id) => returns [draft, object]
8
+ useDraft('entity', id, relations) => returns [draft, objectWithEmbeddedRelations]
9
+ */
10
+
11
+ export default function useDraft(entity, id, relations = []) {
12
+ // const entityDraft =`${entity}Draft`
13
+ // const object = { ...s[entityDraft].entities[String(id)] }
14
+
15
+ // return useSelector(s => addRelations(s, object, entity, relations, 'Draft'), _.isEqual)
16
+ }
17
+
@@ -1,9 +1,12 @@
1
- import JasonContext from './JasonContext'
2
- import { useContext } from 'react'
1
+ import _ from 'lodash'
2
+ import { useSelector } from 'react-redux'
3
+ import addRelations from './addRelations'
3
4
 
4
- export default function useEager(entity, id = null, relations = []) {
5
- const { eager } = useContext(JasonContext)
6
-
7
- return eager(entity, id, relations)
5
+ export default function useEager(entity: string, id = '', relations = [] as any) {
6
+ if (id) {
7
+ return useSelector(s => addRelations(s, { ...s[entity].entities[String(id)] }, entity, relations), _.isEqual)
8
+ } else {
9
+ return useSelector(s => addRelations(s, _.values(s[entity].entities), entity, relations), _.isEqual)
10
+ }
8
11
  }
9
12
 
@@ -9,7 +9,6 @@ import createTransportAdapater from './createTransportAdapter'
9
9
 
10
10
  import { createEntityAdapter, createSlice, createReducer, configureStore } from '@reduxjs/toolkit'
11
11
 
12
- import makeEager from './makeEager'
13
12
  import { camelizeKeys } from 'humps'
14
13
  import md5 from 'blueimp-md5'
15
14
  import _ from 'lodash'
@@ -41,7 +40,6 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
41
40
 
42
41
  const optDis = createOptDis(schema, dispatch, restClient, serverActionQueue)
43
42
  const actions = createActions(schema, store, restClient, optDis, extraActions)
44
- const eager = makeEager(schema)
45
43
 
46
44
  let payloadHandlers = {}
47
45
  let configs = {}
@@ -90,7 +88,7 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
90
88
  function removeSubscription(config) {
91
89
  transportAdapter.removeSubscription(config)
92
90
  const md5Hash = md5(JSON.stringify(config))
93
- payloadHandlers[md5Hash].tearDown()
91
+ payloadHandlers[md5Hash]?.tearDown() // Race condition where component mounts then unmounts quickly
94
92
  delete payloadHandlers[md5Hash]
95
93
  delete configs[md5Hash]
96
94
  delete subOptions[md5Hash]
@@ -99,7 +97,6 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
99
97
  setValue({
100
98
  actions: actions,
101
99
  subscribe: createSubscription,
102
- eager,
103
100
  handlePayload
104
101
  })
105
102
  setStore(store)
data/config/routes.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  Jason::Engine.routes.draw do
2
- get '/api/config', to: 'api#configuration'
3
- post '/api/action', to: 'api#action'
4
- post '/api/create_subscription', to: 'api#create_subscription'
5
- post '/api/remove_subscription', to: 'api#remove_subscription'
6
- post '/api/get_payload', to: 'api#get_payload'
7
- post '/api/pusher/auth', to: 'api/pusher#auth'
2
+ get '/api/config', to: 'jason#configuration'
3
+ post '/api/action', to: 'jason#action'
4
+ post '/api/create_subscription', to: 'jason#create_subscription'
5
+ post '/api/remove_subscription', to: 'jason#remove_subscription'
6
+ post '/api/get_payload', to: 'jason#get_payload'
7
+ post '/api/pusher/auth', to: 'pusher#auth'
8
8
  end
data/jason-rails.gemspec CHANGED
@@ -31,4 +31,5 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "rspec-rails"
32
32
  spec.add_development_dependency "sqlite3"
33
33
  spec.add_development_dependency "pry"
34
+ spec.add_development_dependency "sidekiq"
34
35
  end
data/lib/jason.rb CHANGED
@@ -12,6 +12,8 @@ require 'jason/engine'
12
12
  require 'jason/lua_generator'
13
13
  require 'jason/includes_helper'
14
14
  require 'jason/graph_helper'
15
+ require 'jason/conditions_matcher'
16
+ require 'jason/consistency_checker'
15
17
 
16
18
  module Jason
17
19
  class Error < StandardError; end
@@ -24,20 +26,24 @@ module Jason
24
26
  self.mattr_accessor :pusher_region
25
27
  self.mattr_accessor :pusher_channel_prefix
26
28
  self.mattr_accessor :authorization_service
29
+ self.mattr_accessor :sidekiq_queue
27
30
 
28
31
  self.schema = {}
29
32
  self.transport_service = :action_cable
30
33
  self.pusher_region = 'eu'
31
34
  self.pusher_channel_prefix = 'jason'
35
+ self.sidekiq_queue = 'default'
32
36
 
33
37
  def self.init
38
+ # Don't run in AR migration / generator etc.
39
+ return if $PROGRAM_NAME == '-e' || ActiveRecord::Base.connection.migration_context.needs_migration?
40
+
34
41
  # Check if the schema has changed since last time app was started. If so, do some work to ensure cache contains the correct data
35
42
  got_lock = $redis_jason.set('jason:schema:lock', nx: true, ex: 3600) # Basic lock mechanism for multi-process environments
36
43
  return if !got_lock
37
44
 
38
45
  previous_schema = JSON.parse($redis_jason.get('jason:last_schema') || '{}')
39
46
  current_schema = Jason.schema.deep_stringify_keys.deep_transform_values { |v| v.is_a?(Symbol) ? v.to_s : v }
40
- pp current_schema
41
47
  current_schema.each do |model, config|
42
48
  if config != previous_schema[model]
43
49
  puts "Config changed for #{model}"
@@ -52,8 +58,6 @@ module Jason
52
58
  $redis_jason.set('jason:last_schema', current_schema.to_json)
53
59
  ensure
54
60
  $redis_jason.del('jason:schema:lock')
55
-
56
- previous_config = 'test'
57
61
  end
58
62
 
59
63
 
@@ -36,11 +36,7 @@ class Jason::ApiModel
36
36
  end
37
37
 
38
38
  def permit(params)
39
- pp self
40
- pp params
41
39
  params = params.require(:payload).permit(allowed_params).tap do |allowed|
42
- pp "ALLOWED"
43
- pp allowed
44
40
  allowed_object_params.each do |key|
45
41
  allowed[key] = params[:payload][key].to_unsafe_h if params[:payload][key]
46
42
  end
@@ -13,7 +13,8 @@ class Jason::Broadcaster
13
13
  if Jason.transport_service == :action_cable
14
14
  ActionCable.server.broadcast(channel, message)
15
15
  elsif Jason.transport_service == :pusher
16
- Jason.pusher.trigger(pusher_channel_name, 'changed', message)
16
+ $redis_jason.rpush("jason:outbound_message_queue", { channel: pusher_channel_name, name: 'changed', data: message }.to_json)
17
+ Jason::OutboundMessageQueueWorker.perform_async
17
18
  end
18
19
  end
19
20
  end
data/lib/jason/channel.rb CHANGED
@@ -12,7 +12,6 @@ class Jason::Channel < ActionCable::Channel::Base
12
12
  private
13
13
 
14
14
  def handle_message(message)
15
- pp message['createSubscription']
16
15
  @subscriptions ||= []
17
16
 
18
17
  begin # ActionCable swallows errors in this message - ensure they're output to logs.
@@ -38,18 +37,12 @@ class Jason::Channel < ActionCable::Channel::Base
38
37
 
39
38
  subscriptions.push(subscription)
40
39
  subscription.add_consumer(identifier)
41
- subscription.get.each do |payload|
42
- pp payload
43
- transmit(payload) if payload.present?
44
- end
45
40
  end
46
41
 
47
42
  def remove_subscription(config)
48
43
  subscription = Jason::Subscription.upsert_by_config(config['model'], conditions: config['conditions'], includes: config['includes'])
49
44
  subscriptions.reject! { |s| s.id == subscription.id }
50
45
  subscription.remove_consumer(identifier)
51
-
52
- # TODO Stop streams
53
46
  end
54
47
 
55
48
  def get_payload(config, force_refresh = false)
@@ -0,0 +1,88 @@
1
+ class Jason::ConditionsMatcher
2
+ attr_reader :klass
3
+
4
+ def initialize(klass)
5
+ @klass = klass
6
+ end
7
+
8
+ # key, rules = 'post_id', 123
9
+ # key, rules = 'post_id', { 'value': [123,C456], 'type': 'between' }
10
+ # key, rules = 'post_id', { 'value': [123,456], 'type': 'between', 'not': true }
11
+ # key, rules = 'post_id', { 'value': 123, 'type': 'equals', 'not': true }
12
+ def test_match(key, rules, previous_changes)
13
+ return nil if !previous_changes.keys.include?(key)
14
+
15
+ if rules.is_a?(Hash)
16
+ matches = false
17
+ value = convert_to_datatype(key, rules['value'])
18
+
19
+ if rules['type'] == 'equals'
20
+ matches = previous_changes[key][1] == value
21
+ elsif rules['type'] == 'between'
22
+ matches = (value[0]..value[1]).cover?(previous_changes[key][1])
23
+ else
24
+ raise "Unrecognized rule type #{rules['type']}"
25
+ end
26
+
27
+ if rules['not']
28
+ return !matches
29
+ else
30
+ return matches
31
+ end
32
+
33
+ elsif rules.is_a?(Array)
34
+ value = convert_to_datatype(key, rules)
35
+ return previous_changes[key][1].includes?(value)
36
+ else
37
+ value = convert_to_datatype(key, rules)
38
+ return previous_changes[key][1] == value
39
+ end
40
+ end
41
+
42
+ # conditions = { 'post_id' => 123, 'created_at' => { 'type' => 'between', 'value' => ['2020-01-01', '2020-01-02'] } }
43
+ def apply_conditions(relation, conditions)
44
+ conditions.each do |key, rules|
45
+ relation = apply_condition(relation, key, rules)
46
+ end
47
+
48
+ relation
49
+ end
50
+
51
+ private
52
+
53
+ def apply_condition(relation, key, rules)
54
+ if rules.is_a?(Hash)
55
+ value = convert_to_datatype(key, rules['value'])
56
+
57
+ if rules['type'] == 'equals'
58
+ arg = { key => value }
59
+ elsif rules['type'] == 'between'
60
+ arg = { key => value[0]..value[1] }
61
+ else
62
+ raise "Unrecognized rule type #{rules['type']}"
63
+ end
64
+
65
+ if rules['not']
66
+ return relation.where.not(arg)
67
+ else
68
+ return relation.where(arg)
69
+ end
70
+ else
71
+ value = convert_to_datatype(key, rules)
72
+ return relation.where({ key => value })
73
+ end
74
+ end
75
+
76
+ def convert_to_datatype(key, value)
77
+ datatype = klass.type_for_attribute(key).type
78
+ if datatype == :datetime || datatype == :date
79
+ if value.is_a?(Array)
80
+ value.map { |v| v&.to_datetime }
81
+ else
82
+ value&.to_datetime
83
+ end
84
+ else
85
+ value
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,61 @@
1
+ class Jason::ConsistencyChecker
2
+ attr_reader :subscription
3
+ attr_reader :inconsistent
4
+
5
+ def self.check_all(fix: false)
6
+ Jason::Subscription.all.each do |sub|
7
+ next if sub.consumer_count == 0
8
+ checker = Jason::ConsistencyChecker.new(sub)
9
+ result = checker.check
10
+ if checker.inconsistent?
11
+ pp sub.config
12
+ pp result
13
+ if fix
14
+ sub.reset!(hard: true)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ def self.fix_all
21
+ check_all(fix: true)
22
+ end
23
+
24
+ def initialize(subscription)
25
+ @subscription = subscription
26
+ @inconsistent = false
27
+ end
28
+
29
+ def inconsistent?
30
+ inconsistent
31
+ end
32
+
33
+ # Take a subscription, get the current cached payload, and compare it to the data retrieved from the database
34
+ def check
35
+ cached_payload = subscription.get
36
+ edge_set = subscription.load_ids_for_sub_models(subscription.model, nil)
37
+
38
+ result = cached_payload.map do |model_name, data|
39
+ cached_payload_instance_ids = data[:payload].map { |row| row['id'] }
40
+
41
+ model_idx = edge_set[:model_names].index(model_name)
42
+ if model_idx.present?
43
+ edge_set_instance_ids = edge_set[:instance_ids].map { |row| row[model_idx] }
44
+ else
45
+ next
46
+ end
47
+
48
+ missing = edge_set_instance_ids - cached_payload_instance_ids
49
+ intruding = cached_payload_instance_ids - edge_set_instance_ids
50
+
51
+ if missing.present? || intruding.present?
52
+ @inconsistent = true
53
+ end
54
+
55
+ [model_name, {
56
+ 'missing' => missing,
57
+ 'intruding' => intruding
58
+ }]
59
+ end.compact.to_h
60
+ end
61
+ end
@@ -26,6 +26,10 @@ class Jason::GraphHelper
26
26
  $redis_jason.srem("jason:subscriptions:#{id}:graph", edges)
27
27
  end
28
28
 
29
+ def apply_add_node_at_root(node)
30
+ diff_edges_from_graph(add_edges: ["root/#{node}"])
31
+ end
32
+
29
33
  def apply_remove_node(node)
30
34
  edges = $redis_jason.smembers("jason:subscriptions:#{id}:graph")
31
35
  edges = find_edges_with_node(edges, node)
@@ -33,7 +37,8 @@ class Jason::GraphHelper
33
37
  end
34
38
 
35
39
  # Add and remove edges, return graph before and after
36
- def apply_update(add: nil, remove: nil)
40
+ # Enforce means make the graph contain only the add_edges
41
+ def apply_update(add: nil, remove: nil, enforce: false)
37
42
  add_edges = []
38
43
  remove_edges = []
39
44
 
@@ -48,11 +53,21 @@ class Jason::GraphHelper
48
53
  remove_edges += build_edges(edge_set[:model_names], edge_set[:instance_ids], include_root: false)
49
54
  end
50
55
  end
51
- diff_edges_from_graph(add_edges: add_edges, remove_edges: remove_edges)
56
+
57
+ diff_edges_from_graph(add_edges: add_edges, remove_edges: remove_edges, enforce: enforce)
52
58
  end
53
59
 
54
- def diff_edges_from_graph(add_edges: [], remove_edges: [])
55
- old_edges, new_edges = Jason::LuaGenerator.new.update_set_with_diff("jason:subscriptions:#{id}:graph", add_edges.flatten, remove_edges.flatten)
60
+ def diff_edges_from_graph(add_edges: [], remove_edges: [], enforce: false)
61
+ if enforce
62
+ old_edges = $redis_jason.multi do |r|
63
+ r.smembers("jason:subscriptions:#{id}:graph")
64
+ r.del("jason:subscriptions:#{id}:graph")
65
+ r.sadd("jason:subscriptions:#{id}:graph", add_edges) if add_edges.present?
66
+ end[0]
67
+ new_edges = add_edges
68
+ else
69
+ old_edges, new_edges = Jason::LuaGenerator.new.update_set_with_diff("jason:subscriptions:#{id}:graph", add_edges.flatten, remove_edges.flatten)
70
+ end
56
71
 
57
72
  old_graph = build_graph_from_edges(old_edges)
58
73
  new_graph = build_graph_from_edges(new_edges)
@@ -5,6 +5,7 @@ module Jason::Publisher
5
5
  def self.cache_all
6
6
  Rails.application.eager_load!
7
7
  ActiveRecord::Base.descendants.each do |klass|
8
+ $redis_jason.del("jason:cache:#{klass.name.underscore}")
8
9
  klass.cache_all if klass.respond_to?(:cache_all)
9
10
  end
10
11
  end
@@ -42,7 +43,9 @@ module Jason::Publisher
42
43
  # - TODO: The value of an instance changes so that it enters/leaves a subscription
43
44
 
44
45
  # 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 = self.class.reflect_on_all_associations(:belongs_to)
47
+ .reject { |assoc| assoc.polymorphic? } # Can't get the class name of a polymorphic association, by
48
+ .select { |assoc| assoc.klass.respond_to?(:has_jason?) }
46
49
  jason_assocs.each do |assoc|
47
50
  if previous_changes[assoc.foreign_key].present?
48
51
  Jason::Subscription.update_ids(
@@ -70,13 +73,38 @@ module Jason::Publisher
70
73
  )
71
74
  end
72
75
 
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
76
  if persisted?
77
+ applied_sub_ids = []
78
+
79
+ jason_conditions.each do |row|
80
+ matches = row['conditions'].map do |key, rules|
81
+ Jason::ConditionsMatcher.new(self.class).test_match(key, rules, previous_changes)
82
+ end
83
+ next if matches.all? { |m| m.nil? } # None of the keys were in previous changes - therefore this condition does not apply
84
+ in_sub = matches.all? { |m| m }
85
+
86
+ if in_sub
87
+ row['subscription_ids'].each do |sub_id|
88
+ Jason::Subscription.find_by_id(sub_id).add_id(self.class.name.underscore, self.id)
89
+ applied_sub_ids.push(sub_id)
90
+ end
91
+ else
92
+ row['subscription_ids'].each do |sub_id|
93
+ jason_subscriptions.each do |already_sub_id|
94
+ # If this sub ID already has this instance, remove it
95
+ if already_sub_id == sub_id
96
+ sub = Jason::Subscription.find_by_id(already_sub_id)
97
+ sub.remove_id(self.class.name.underscore, self.id)
98
+ applied_sub_ids.push(already_sub_id)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+
79
105
  jason_subscriptions.each do |sub_id|
106
+ next if applied_sub_ids.include?(sub_id)
107
+
80
108
  Jason::Subscription.new(id: sub_id).update(self.class.name.underscore, id, payload, gidx)
81
109
  end
82
110
  end
@@ -91,6 +119,10 @@ module Jason::Publisher
91
119
  Jason::Subscription.for_instance(self.class.name.underscore, id)
92
120
  end
93
121
 
122
+ def jason_conditions
123
+ Jason::Subscription.conditions_for_model(self.class.name.underscore)
124
+ end
125
+
94
126
  def jason_cached_value
95
127
  JSON.parse($redis_jason.hget("jason:cache:#{self.class.name.underscore}", id) || '{}')
96
128
  end
@@ -115,7 +147,8 @@ module Jason::Publisher
115
147
  self.after_initialize -> {
116
148
  @api_model = Jason::ApiModel.new(self.class.name.underscore)
117
149
  }
118
- self.after_commit :publish_json_if_changed
150
+ self.after_commit :force_publish_json, on: [:create, :destroy]
151
+ self.after_commit :publish_json_if_changed, on: [:update]
119
152
  end
120
153
 
121
154
  def find_or_create_by_id(params)