jason-rails 0.6.8 → 0.7.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/Gemfile.lock +5 -3
  4. data/README.md +20 -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 +3 -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/createTransportAdapter.d.ts +1 -1
  20. data/client/lib/createTransportAdapter.js +2 -2
  21. data/client/lib/index.d.ts +8 -1
  22. data/client/lib/index.js +3 -1
  23. data/client/lib/transportAdapters/actionCableAdapter.d.ts +1 -1
  24. data/client/lib/transportAdapters/actionCableAdapter.js +27 -6
  25. data/client/lib/transportAdapters/pusherAdapter.js +1 -1
  26. data/client/lib/useDraft.d.ts +1 -0
  27. data/client/lib/useDraft.js +13 -0
  28. data/client/lib/useEager.d.ts +1 -1
  29. data/client/lib/useEager.js +10 -5
  30. data/client/lib/useJason.d.ts +3 -1
  31. data/client/lib/useJason.js +4 -7
  32. data/client/package.json +1 -1
  33. data/client/src/JasonProvider.tsx +2 -2
  34. data/client/src/addRelations.ts +33 -0
  35. data/client/src/createJasonReducers.ts +4 -2
  36. data/client/src/createOptDis.ts +10 -8
  37. data/client/src/createServerActionQueue.test.ts +60 -6
  38. data/client/src/createServerActionQueue.ts +41 -6
  39. data/client/src/createTransportAdapter.ts +2 -2
  40. data/client/src/index.ts +2 -0
  41. data/client/src/transportAdapters/actionCableAdapter.ts +28 -7
  42. data/client/src/transportAdapters/pusherAdapter.ts +1 -2
  43. data/client/src/useDraft.ts +17 -0
  44. data/client/src/useEager.ts +9 -6
  45. data/client/src/useJason.ts +10 -7
  46. data/lib/jason.rb +9 -2
  47. data/lib/jason/api_model.rb +0 -4
  48. data/lib/jason/channel.rb +0 -7
  49. data/lib/jason/conditions_matcher.rb +88 -0
  50. data/lib/jason/consistency_checker.rb +65 -0
  51. data/lib/jason/graph_helper.rb +4 -0
  52. data/lib/jason/publisher.rb +36 -36
  53. data/lib/jason/subscription.rb +51 -15
  54. data/lib/jason/version.rb +1 -1
  55. metadata +12 -5
  56. data/client/src/makeEager.ts +0 -46
  57. data/lib/jason/publisher_old.rb +0 -112
  58. data/lib/jason/subscription_old.rb +0 -171
@@ -1,31 +1,65 @@
1
1
  // A FIFO queue with deduping of actions whose effect will be cancelled by later actions
2
-
2
+ import { v4 as uuidv4 } from 'uuid'
3
3
  import _ from 'lodash'
4
4
 
5
+ class Deferred {
6
+ promise: Promise<any>;
7
+ resolve: any;
8
+ reject: any;
9
+
10
+ constructor() {
11
+ this.promise = new Promise((resolve, reject)=> {
12
+ this.reject = reject
13
+ this.resolve = resolve
14
+ })
15
+ }
16
+ }
17
+
5
18
  export default function createServerActionQueue() {
6
19
  const queue: any[] = []
20
+ const deferreds = {}
21
+
7
22
  let inFlight = false
8
23
 
9
- function addItem(item) {
24
+ function addItem(action) {
10
25
  // Check if there are any items ahead in the queue that this item would effectively overwrite.
11
26
  // In that case we can remove them
12
27
  // If this is an upsert && item ID is the same && current item attributes are a superset of the earlier item attributes
13
- const { type, payload } = item
28
+ const { type, payload } = action
29
+ const id = uuidv4()
30
+ const dfd = new Deferred()
31
+ deferreds[id] = [dfd]
32
+
33
+ const item = { id, action }
34
+
14
35
  if (type.split('/')[1] !== 'upsert') {
15
36
  queue.push(item)
16
- return
37
+ return dfd.promise
17
38
  }
18
39
 
19
40
  _.remove(queue, item => {
20
- const { type: itemType, payload: itemPayload } = item
41
+ const { type: itemType, payload: itemPayload } = item.action
21
42
  if (type !== itemType) return false
22
43
  if (itemPayload.id !== payload.id) return false
23
44
 
24
45
  // Check that all keys of itemPayload are in payload.
46
+ deferreds[id].push(...deferreds[item.id])
25
47
  return _.difference(_.keys(itemPayload),_.keys(payload)).length === 0
26
48
  })
27
49
 
28
50
  queue.push(item)
51
+ return dfd.promise
52
+ }
53
+
54
+ function itemProcessed(id, data?: any) {
55
+ inFlight = false
56
+ deferreds[id].forEach(dfd => dfd.resolve(data))
57
+ }
58
+
59
+ function itemFailed(id, error?: any) {
60
+ queue.length = 0
61
+ deferreds[id].forEach(dfd => dfd.reject(error))
62
+ inFlight = false
29
63
  }
30
64
 
31
65
  return {
@@ -40,7 +74,8 @@ export default function createServerActionQueue() {
40
74
  }
41
75
  return false
42
76
  },
43
- itemProcessed: () => inFlight = false,
77
+ itemProcessed,
78
+ itemFailed,
44
79
  fullySynced: () => queue.length === 0 && !inFlight,
45
80
  getData: () => ({ queue, inFlight })
46
81
  }
@@ -1,10 +1,10 @@
1
1
  import actionCableAdapter from './transportAdapters/actionCableAdapter'
2
2
  import pusherAdapter from './transportAdapters/pusherAdapter'
3
3
 
4
- export default function createTransportAdapter(jasonConfig, handlePayload, dispatch, onConnect) {
4
+ export default function createTransportAdapter(jasonConfig, handlePayload, dispatch, onConnect, transportOptions) {
5
5
  const { transportService } = jasonConfig
6
6
  if (transportService === 'action_cable') {
7
- return actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnect)
7
+ return actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnect, transportOptions)
8
8
  } else if (transportService === 'pusher') {
9
9
  return pusherAdapter(jasonConfig, handlePayload, dispatch)
10
10
  } else {
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,7 +1,14 @@
1
1
  import { createConsumer } from "@rails/actioncable"
2
+ import restClient from '../restClient'
3
+ import { v4 as uuidv4 } from 'uuid'
4
+ import _ from 'lodash'
5
+
6
+ export default function actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnected, transportOptions) {
7
+ const consumerId = uuidv4()
8
+
9
+ const { cableUrl } = transportOptions
10
+ const consumer = cableUrl ? createConsumer(cableUrl) : createConsumer()
2
11
 
3
- export default function actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnected) {
4
- const consumer = createConsumer()
5
12
  const subscription = (consumer.subscriptions.create({
6
13
  channel: 'Jason::Channel'
7
14
  }, {
@@ -22,16 +29,30 @@ export default function actionCableAdapter(jasonConfig, handlePayload, dispatch,
22
29
  }
23
30
  }));
24
31
 
25
- function getPayload(config, options) {
26
- subscription.send({ getPayload: config, ...options })
27
- }
28
-
29
32
  function createSubscription(config) {
30
33
  subscription.send({ createSubscription: config })
31
34
  }
32
35
 
33
36
  function removeSubscription(config) {
34
- subscription.send({ removeSubscription: config })
37
+ restClient.post('/jason/api/remove_subscription', { config, consumerId })
38
+ .catch(e => console.error(e))
39
+ }
40
+
41
+ function getPayload(config, options) {
42
+ restClient.post('/jason/api/get_payload', {
43
+ config,
44
+ options
45
+ })
46
+ .then(({ data }) => {
47
+ _.map(data, (payload, modelName) => {
48
+ handlePayload(payload)
49
+ })
50
+ })
51
+ .catch(e => console.error(e))
52
+ }
53
+
54
+ function fullChannelName(channelName) {
55
+ return channelName
35
56
  }
36
57
 
37
58
  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 = [], transportOptions = {}, extraActions }: { reducers?: any, middleware?: any[], enhancers?: any[], extraActions?: any, transportOptions?: 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 = {}
@@ -50,7 +48,7 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
50
48
  function handlePayload(payload) {
51
49
  const { md5Hash } = payload
52
50
 
53
- const { handlePayload } = payloadHandlers[md5Hash]
51
+ const { handlePayload } = payloadHandlers[md5Hash] || {}
54
52
  if (handlePayload) {
55
53
  handlePayload(payload)
56
54
  } else {
@@ -58,7 +56,13 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
58
56
  }
59
57
  }
60
58
 
61
- const transportAdapter = createTransportAdapater(jasonConfig, handlePayload, dispatch, () => _.keys(configs).forEach(md5Hash => createSubscription(configs[md5Hash], subOptions[md5Hash])))
59
+ const transportAdapter = createTransportAdapater(
60
+ jasonConfig,
61
+ handlePayload,
62
+ dispatch,
63
+ () => _.keys(configs).forEach(md5Hash => createSubscription(configs[md5Hash], subOptions[md5Hash])),
64
+ transportOptions
65
+ )
62
66
 
63
67
  function createSubscription(config, options = {}) {
64
68
  // We need the hash to be consistent in Ruby / Javascript
@@ -99,7 +103,6 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
99
103
  setValue({
100
104
  actions: actions,
101
105
  subscribe: createSubscription,
102
- eager,
103
106
  handlePayload
104
107
  })
105
108
  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 = {}
@@ -48,7 +51,11 @@ module Jason
48
51
  puts "Old config was #{previous_schema[model]}"
49
52
  puts "New config is #{config}"
50
53
  puts "Rebuilding cache for #{model}"
51
- model.classify.constantize.cache_all
54
+
55
+ # This is necessary to ensure all Rails methods have been added to model before we attempt to cache.
56
+ Rails.configuration.after_initialize do
57
+ model.classify.constantize.cache_all
58
+ end
52
59
  puts "Done"
53
60
  end
54
61
  end
@@ -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