jason-rails 0.6.8 → 0.7.5

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 (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