jason-rails 0.6.7 → 0.7.3

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/Gemfile.lock +6 -4
  4. data/README.md +16 -14
  5. data/app/controllers/jason/jason_controller.rb +26 -4
  6. data/app/workers/jason/outbound_message_queue_worker.rb +1 -1
  7. data/client/lib/JasonProvider.d.ts +2 -1
  8. data/client/lib/JasonProvider.js +2 -2
  9. data/client/lib/addRelations.d.ts +1 -0
  10. data/client/lib/addRelations.js +39 -0
  11. data/client/lib/createJasonReducers.js +4 -2
  12. data/client/lib/createOptDis.d.ts +1 -1
  13. data/client/lib/createOptDis.js +9 -8
  14. data/client/lib/createServerActionQueue.d.ts +3 -2
  15. data/client/lib/createServerActionQueue.js +32 -6
  16. data/client/lib/createServerActionQueue.test.js +61 -6
  17. data/client/lib/createThenable.d.ts +1 -0
  18. data/client/lib/createThenable.js +5 -0
  19. data/client/lib/index.d.ts +7 -1
  20. data/client/lib/index.js +3 -1
  21. data/client/lib/transportAdapters/actionCableAdapter.js +24 -4
  22. data/client/lib/transportAdapters/pusherAdapter.js +1 -1
  23. data/client/lib/useDraft.d.ts +1 -0
  24. data/client/lib/useDraft.js +13 -0
  25. data/client/lib/useEager.d.ts +1 -1
  26. data/client/lib/useEager.js +10 -5
  27. data/client/lib/useJason.d.ts +2 -1
  28. data/client/lib/useJason.js +4 -6
  29. data/client/package.json +1 -1
  30. data/client/src/JasonProvider.tsx +2 -2
  31. data/client/src/addRelations.ts +33 -0
  32. data/client/src/createJasonReducers.ts +4 -2
  33. data/client/src/createOptDis.ts +10 -8
  34. data/client/src/createServerActionQueue.test.ts +60 -6
  35. data/client/src/createServerActionQueue.ts +41 -6
  36. data/client/src/index.ts +2 -0
  37. data/client/src/transportAdapters/actionCableAdapter.ts +24 -5
  38. data/client/src/transportAdapters/pusherAdapter.ts +1 -2
  39. data/client/src/useDraft.ts +17 -0
  40. data/client/src/useEager.ts +9 -6
  41. data/client/src/useJason.ts +3 -6
  42. data/lib/jason.rb +4 -1
  43. data/lib/jason/api_model.rb +0 -4
  44. data/lib/jason/channel.rb +0 -7
  45. data/lib/jason/conditions_matcher.rb +88 -0
  46. data/lib/jason/consistency_checker.rb +65 -0
  47. data/lib/jason/graph_helper.rb +4 -0
  48. data/lib/jason/publisher.rb +39 -37
  49. data/lib/jason/subscription.rb +63 -18
  50. data/lib/jason/version.rb +1 -1
  51. metadata +12 -5
  52. data/client/src/makeEager.ts +0 -46
  53. data/lib/jason/publisher_old.rb +0 -112
  54. data/lib/jason/subscription_old.rb +0 -171
data/client/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import _JasonContext from './JasonContext'
1
2
  import _JasonProvider from './JasonProvider'
2
3
  import _useAct from './useAct'
3
4
  import _useSub from './useSub'
4
5
  import _useEager from './useEager'
5
6
 
7
+ export const JasonContext = _JasonContext
6
8
  export const JasonProvider = _JasonProvider
7
9
  export const useAct = _useAct
8
10
  export const useSub = _useSub
@@ -1,6 +1,11 @@
1
1
  import { createConsumer } from "@rails/actioncable"
2
+ import restClient from '../restClient'
3
+ import { v4 as uuidv4 } from 'uuid'
4
+ import _ from 'lodash'
2
5
 
3
6
  export default function actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnected) {
7
+ const consumerId = uuidv4()
8
+
4
9
  const consumer = createConsumer()
5
10
  const subscription = (consumer.subscriptions.create({
6
11
  channel: 'Jason::Channel'
@@ -22,16 +27,30 @@ export default function actionCableAdapter(jasonConfig, handlePayload, dispatch,
22
27
  }
23
28
  }));
24
29
 
25
- function getPayload(config, options) {
26
- subscription.send({ getPayload: config, ...options })
27
- }
28
-
29
30
  function createSubscription(config) {
30
31
  subscription.send({ createSubscription: config })
31
32
  }
32
33
 
33
34
  function removeSubscription(config) {
34
- subscription.send({ removeSubscription: config })
35
+ restClient.post('/jason/api/remove_subscription', { config, consumerId })
36
+ .catch(e => console.error(e))
37
+ }
38
+
39
+ function getPayload(config, options) {
40
+ restClient.post('/jason/api/get_payload', {
41
+ config,
42
+ options
43
+ })
44
+ .then(({ data }) => {
45
+ _.map(data, (payload, modelName) => {
46
+ handlePayload(payload)
47
+ })
48
+ })
49
+ .catch(e => console.error(e))
50
+ }
51
+
52
+ function fullChannelName(channelName) {
53
+ return channelName
35
54
  }
36
55
 
37
56
  return { getPayload, createSubscription, removeSubscription }
@@ -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,13 +9,12 @@ 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'
16
15
  import React, { useState, useEffect } from 'react'
17
16
 
18
- export default function useJason({ reducers, middleware = [], extraActions }: { reducers?: any, middleware?: any[], extraActions?: any }) {
17
+ export default function useJason({ reducers, middleware = [], enhancers = [], extraActions }: { reducers?: any, middleware?: any[], enhancers?: any[], extraActions?: any }) {
19
18
  const [store, setStore] = useState(null as any)
20
19
  const [value, setValue] = useState(null as any)
21
20
 
@@ -36,12 +35,11 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
36
35
 
37
36
  console.debug({ allReducers })
38
37
 
39
- const store = configureStore({ reducer: allReducers, middleware: [...middleware, pruneIdsMiddleware(schema)] })
38
+ const store = configureStore({ reducer: allReducers, middleware: [...middleware, pruneIdsMiddleware(schema)], enhancers })
40
39
  const dispatch = store.dispatch
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/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
@@ -23,7 +25,8 @@ module Jason
23
25
  self.mattr_accessor :pusher_key
24
26
  self.mattr_accessor :pusher_region
25
27
  self.mattr_accessor :pusher_channel_prefix
26
- self.mattr_accessor :authorization_service
28
+ self.mattr_accessor :subscription_authorization_service
29
+ self.mattr_accessor :update_authorization_service
27
30
  self.mattr_accessor :sidekiq_queue
28
31
 
29
32
  self.schema = {}
@@ -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
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,65 @@
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 wipe_all_subs
25
+
26
+ end
27
+
28
+ def initialize(subscription)
29
+ @subscription = subscription
30
+ @inconsistent = false
31
+ end
32
+
33
+ def inconsistent?
34
+ inconsistent
35
+ end
36
+
37
+ # Take a subscription, get the current cached payload, and compare it to the data retrieved from the database
38
+ def check
39
+ cached_payload = subscription.get
40
+ edge_set = subscription.load_ids_for_sub_models(subscription.model, nil)
41
+
42
+ result = cached_payload.map do |model_name, data|
43
+ cached_payload_instance_ids = data[:payload].map { |row| row['id'] }
44
+
45
+ model_idx = edge_set[:model_names].index(model_name)
46
+ if model_idx.present?
47
+ edge_set_instance_ids = edge_set[:instance_ids].map { |row| row[model_idx] }
48
+ else
49
+ next
50
+ end
51
+
52
+ missing = edge_set_instance_ids - cached_payload_instance_ids
53
+ intruding = cached_payload_instance_ids - edge_set_instance_ids
54
+
55
+ if missing.present? || intruding.present?
56
+ @inconsistent = true
57
+ end
58
+
59
+ [model_name, {
60
+ 'missing' => missing,
61
+ 'intruding' => intruding
62
+ }]
63
+ end.compact.to_h
64
+ end
65
+ 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)
@@ -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
@@ -15,7 +16,7 @@ module Jason::Publisher
15
16
 
16
17
  # Exists
17
18
  if self.persisted? && (scope.blank? || self.class.unscoped.send(scope).exists?(self.id))
18
- payload = self.reload.as_json(as_json_config)
19
+ payload = self.as_json(as_json_config)
19
20
  gidx = Jason::LuaGenerator.new.cache_json(self.class.name.underscore, self.id, payload)
20
21
  return [payload, gidx]
21
22
  # Has been destroyed
@@ -72,13 +73,38 @@ module Jason::Publisher
72
73
  )
73
74
  end
74
75
 
75
- # - An instance is created where it belongs_to an _all_ subscription
76
- if previous_changes['id'].present?
77
- Jason::Subscription.add_id(self.class.name.underscore, id)
78
- end
79
-
80
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
+
81
105
  jason_subscriptions.each do |sub_id|
106
+ next if applied_sub_ids.include?(sub_id)
107
+
82
108
  Jason::Subscription.new(id: sub_id).update(self.class.name.underscore, id, payload, gidx)
83
109
  end
84
110
  end
@@ -93,13 +119,17 @@ module Jason::Publisher
93
119
  Jason::Subscription.for_instance(self.class.name.underscore, id)
94
120
  end
95
121
 
122
+ def jason_conditions
123
+ Jason::Subscription.conditions_for_model(self.class.name.underscore)
124
+ end
125
+
96
126
  def jason_cached_value
97
127
  JSON.parse($redis_jason.hget("jason:cache:#{self.class.name.underscore}", id) || '{}')
98
128
  end
99
129
 
100
130
  class_methods do
101
131
  def cache_all
102
- all.each(&:cache_json)
132
+ all.find_each(&:cache_json)
103
133
  end
104
134
 
105
135
  def has_jason?
@@ -117,36 +147,8 @@ module Jason::Publisher
117
147
  self.after_initialize -> {
118
148
  @api_model = Jason::ApiModel.new(self.class.name.underscore)
119
149
  }
120
- self.after_commit :publish_json_if_changed
121
- end
122
-
123
- def find_or_create_by_id(params)
124
- object = find_by(id: params[:id])
125
-
126
- if object
127
- object.update(params)
128
- elsif params[:hidden]
129
- return false ## If an object is passed with hidden = true but didn't already exist, it's safe to never create it
130
- else
131
- object = create!(params)
132
- end
133
-
134
- object
135
- end
136
-
137
- def find_or_create_by_id!(params)
138
- object = find_by(id: params[:id])
139
-
140
- if object
141
- object.update!(params)
142
- elsif params[:hidden]
143
- ## 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.
144
- return false ## If an object is passed with hidden = true but didn't already exist, it's safe to never create it
145
- else
146
- object = create!(params)
147
- end
148
-
149
- object
150
+ self.after_commit :force_publish_json, on: [:create, :destroy]
151
+ self.after_commit :publish_json_if_changed, on: [:update]
150
152
  end
151
153
  end
152
154