jason-rails 0.6.4 → 0.7.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.
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)