jason-rails 0.6.6 → 0.7.2

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 +41 -0
  3. data/Gemfile.lock +11 -4
  4. data/README.md +9 -15
  5. data/app/controllers/jason/{api_controller.rb → jason_controller.rb} +27 -5
  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/JasonProvider.d.ts +2 -1
  9. data/client/lib/JasonProvider.js +2 -2
  10. data/client/lib/addRelations.d.ts +1 -0
  11. data/client/lib/addRelations.js +39 -0
  12. data/client/lib/createJasonReducers.js +4 -2
  13. data/client/lib/createOptDis.d.ts +1 -1
  14. data/client/lib/createOptDis.js +9 -8
  15. data/client/lib/createServerActionQueue.d.ts +3 -2
  16. data/client/lib/createServerActionQueue.js +32 -6
  17. data/client/lib/createServerActionQueue.test.js +61 -6
  18. data/client/lib/createThenable.d.ts +1 -0
  19. data/client/lib/createThenable.js +5 -0
  20. data/client/lib/index.d.ts +7 -1
  21. data/client/lib/index.js +3 -1
  22. data/client/lib/transportAdapters/actionCableAdapter.js +24 -4
  23. data/client/lib/transportAdapters/pusherAdapter.js +1 -1
  24. data/client/lib/useDraft.d.ts +1 -0
  25. data/client/lib/useDraft.js +13 -0
  26. data/client/lib/useEager.d.ts +1 -1
  27. data/client/lib/useEager.js +10 -5
  28. data/client/lib/useJason.d.ts +2 -1
  29. data/client/lib/useJason.js +4 -6
  30. data/client/package.json +1 -1
  31. data/client/src/JasonProvider.tsx +2 -2
  32. data/client/src/addRelations.ts +33 -0
  33. data/client/src/createJasonReducers.ts +4 -2
  34. data/client/src/createOptDis.ts +10 -8
  35. data/client/src/createServerActionQueue.test.ts +60 -6
  36. data/client/src/createServerActionQueue.ts +41 -6
  37. data/client/src/index.ts +2 -0
  38. data/client/src/transportAdapters/actionCableAdapter.ts +24 -5
  39. data/client/src/transportAdapters/pusherAdapter.ts +1 -2
  40. data/client/src/useDraft.ts +17 -0
  41. data/client/src/useEager.ts +9 -6
  42. data/client/src/useJason.ts +3 -6
  43. data/config/routes.rb +6 -6
  44. data/jason-rails.gemspec +1 -0
  45. data/lib/jason.rb +6 -1
  46. data/lib/jason/api_model.rb +0 -4
  47. data/lib/jason/broadcaster.rb +2 -1
  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 +42 -38
  53. data/lib/jason/subscription.rb +63 -18
  54. data/lib/jason/version.rb +1 -1
  55. metadata +29 -7
  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
  }
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/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
@@ -23,12 +25,15 @@ 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
30
+ self.mattr_accessor :sidekiq_queue
27
31
 
28
32
  self.schema = {}
29
33
  self.transport_service = :action_cable
30
34
  self.pusher_region = 'eu'
31
35
  self.pusher_channel_prefix = 'jason'
36
+ self.sidekiq_queue = 'default'
32
37
 
33
38
  def self.init
34
39
  # Don't run in AR migration / generator etc.
@@ -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,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