jason-rails 0.5.1 → 0.6.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +14 -5
  4. data/app/controllers/jason/api/pusher_controller.rb +15 -0
  5. data/app/controllers/jason/api_controller.rb +44 -2
  6. data/client/lib/JasonProvider.js +1 -1
  7. data/client/lib/createJasonReducers.js +7 -0
  8. data/client/lib/createPayloadHandler.d.ts +6 -3
  9. data/client/lib/createPayloadHandler.js +8 -4
  10. data/client/lib/createTransportAdapter.d.ts +5 -0
  11. data/client/lib/createTransportAdapter.js +20 -0
  12. data/client/lib/pruneIdsMiddleware.js +9 -11
  13. data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
  14. data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
  15. data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
  16. data/client/lib/transportAdapters/pusherAdapter.js +68 -0
  17. data/client/lib/useJason.js +14 -34
  18. data/client/lib/useJason.test.js +8 -2
  19. data/client/package.json +2 -1
  20. data/client/src/JasonProvider.tsx +1 -1
  21. data/client/src/createJasonReducers.ts +7 -0
  22. data/client/src/createPayloadHandler.ts +9 -4
  23. data/client/src/createTransportAdapter.ts +13 -0
  24. data/client/src/pruneIdsMiddleware.ts +11 -11
  25. data/client/src/restClient.ts +1 -0
  26. data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
  27. data/client/src/transportAdapters/pusherAdapter.ts +72 -0
  28. data/client/src/useJason.test.ts +8 -2
  29. data/client/src/useJason.ts +15 -36
  30. data/client/yarn.lock +12 -0
  31. data/config/routes.rb +5 -1
  32. data/lib/jason.rb +29 -8
  33. data/lib/jason/broadcaster.rb +19 -0
  34. data/lib/jason/channel.rb +6 -2
  35. data/lib/jason/graph_helper.rb +165 -0
  36. data/lib/jason/includes_helper.rb +108 -0
  37. data/lib/jason/lua_generator.rb +23 -1
  38. data/lib/jason/publisher.rb +16 -16
  39. data/lib/jason/subscription.rb +208 -183
  40. data/lib/jason/version.rb +1 -1
  41. metadata +15 -2
@@ -17,7 +17,10 @@ const useJason_1 = __importDefault(require("./useJason"));
17
17
  const restClient_1 = __importDefault(require("./restClient"));
18
18
  jest.mock('./restClient');
19
19
  test('it works', () => __awaiter(void 0, void 0, void 0, function* () {
20
- const resp = { data: { post: {} } };
20
+ const resp = { data: {
21
+ schema: { post: {} },
22
+ transportService: 'action_cable'
23
+ } };
21
24
  // @ts-ignore
22
25
  restClient_1.default.get.mockResolvedValue(resp);
23
26
  const { result, waitForNextUpdate } = react_hooks_1.renderHook(() => useJason_1.default({ reducers: {
@@ -50,7 +53,10 @@ test('it works', () => __awaiter(void 0, void 0, void 0, function* () {
50
53
  });
51
54
  }));
52
55
  test('pruning IDs', () => __awaiter(void 0, void 0, void 0, function* () {
53
- const resp = { data: { post: {} } };
56
+ const resp = { data: {
57
+ schema: { post: {} },
58
+ transportService: 'action_cable'
59
+ } };
54
60
  // @ts-ignore
55
61
  restClient_1.default.get.mockResolvedValue(resp);
56
62
  const { result, waitForNextUpdate } = react_hooks_1.renderHook(() => useJason_1.default({ reducers: {
data/client/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jamesr2323/jason",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "module": "./lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "scripts": {
@@ -16,6 +16,7 @@
16
16
  "jsonpatch": "^3.0.1",
17
17
  "lodash": "^4.17.20",
18
18
  "pluralize": "^8.0.0",
19
+ "pusher-js": "^7.0.3",
19
20
  "uuid": "^8.3.1"
20
21
  },
21
22
  "devDependencies": {
@@ -4,7 +4,7 @@ import { Provider } from 'react-redux'
4
4
  import JasonContext from './JasonContext'
5
5
 
6
6
  const JasonProvider = ({ reducers, middleware, extraActions, children }: { reducers?: any, middleware?: any, extraActions?: any, children?: React.FC }) => {
7
- const [store, value, connected] = useJason({ reducers, middleware, extraActions })
7
+ const [store, value] = useJason({ reducers, middleware, extraActions })
8
8
 
9
9
  if(!(store && value)) return <div /> // Wait for async fetch of schema to complete
10
10
 
@@ -42,6 +42,7 @@ function generateJasonSlices(models) {
42
42
  setSubscriptionIds(s,a) {
43
43
  const { payload } = a
44
44
  const { subscriptionId, model, ids } = payload
45
+ console.log({ initialState })
45
46
  s[model][subscriptionId] = ids
46
47
  },
47
48
  addSubscriptionId(s,a) {
@@ -53,6 +54,12 @@ function generateJasonSlices(models) {
53
54
  const { payload } = a
54
55
  const { subscriptionId, model, id } = payload
55
56
  s[model][subscriptionId] = _.remove(s[model][subscriptionId] || [], id)
57
+ },
58
+ removeSubscription(s, a) {
59
+ const { payload: { subscriptionId } } = a
60
+ _.map(models, model => {
61
+ delete s[model][subscriptionId]
62
+ })
56
63
  }
57
64
  }
58
65
  }).reducer
@@ -9,7 +9,7 @@ function diffSeconds(dt2, dt1) {
9
9
  return Math.abs(Math.round(diff))
10
10
  }
11
11
 
12
- export default function createPayloadHandler({ dispatch, serverActionQueue, subscription, config }) {
12
+ export default function createPayloadHandler({ dispatch, serverActionQueue, transportAdapter, config }) {
13
13
  const subscriptionId = uuidv4()
14
14
 
15
15
  let idx = {}
@@ -20,7 +20,7 @@ export default function createPayloadHandler({ dispatch, serverActionQueue, subs
20
20
  let checkInterval
21
21
 
22
22
  function getPayload() {
23
- setTimeout(() => subscription.send({ getPayload: config }), 1000)
23
+ setTimeout(() => transportAdapter.getPayload(config), 1000)
24
24
  }
25
25
 
26
26
  function camelizeKeys(item) {
@@ -46,7 +46,7 @@ export default function createPayloadHandler({ dispatch, serverActionQueue, subs
46
46
  const ids = payload.map(instance => instance.id)
47
47
  dispatch({ type: `jasonModels/setSubscriptionIds`, payload: { model, subscriptionId, ids }})
48
48
  } else if (destroy) {
49
- dispatch({ type: `${pluralize(model)}/remove`, payload: id })
49
+ // Middleware will determine if this model should be removed if it isn't in any other subscriptions
50
50
  dispatch({ type: `jasonModels/removeSubscriptionId`, payload: { model, subscriptionId, id }})
51
51
  } else {
52
52
  dispatch({ type: `${pluralize(model)}/upsert`, payload })
@@ -97,5 +97,10 @@ export default function createPayloadHandler({ dispatch, serverActionQueue, subs
97
97
 
98
98
  tGetPayload()
99
99
 
100
- return handlePayload
100
+ // Clean up after ourselves
101
+ function tearDown() {
102
+ dispatch({ type: `jasonModels/removeSubscription`, payload: { subscriptionId }})
103
+ }
104
+
105
+ return { handlePayload, tearDown }
101
106
  }
@@ -0,0 +1,13 @@
1
+ import actionCableAdapter from './transportAdapters/actionCableAdapter'
2
+ import pusherAdapter from './transportAdapters/pusherAdapter'
3
+
4
+ export default function createTransportAdapter(jasonConfig, handlePayload, dispatch, onConnect) {
5
+ const { transportService } = jasonConfig
6
+ if (transportService === 'action_cable') {
7
+ return actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnect)
8
+ } else if (transportService === 'pusher') {
9
+ return pusherAdapter(jasonConfig, handlePayload, dispatch)
10
+ } else {
11
+ throw(`Transport adapter does not exist for ${transportService}`)
12
+ }
13
+ }
@@ -2,20 +2,20 @@ import _ from 'lodash'
2
2
  import pluralize from 'pluralize'
3
3
 
4
4
  const pruneIdsMiddleware = schema => store => next => action => {
5
- const { type } = action
5
+ const { type, payload } = action
6
6
  const result = next(action)
7
+
7
8
  const state = store.getState()
8
- if (type === 'jasonModels/setSubscriptionIds') {
9
- // Check every model
10
- _.map(_.keys(schema), model => {
11
- let ids = []
12
- _.map(state.jasonModels[model], (subscribedIds, k) => {
13
- ids = _.union(ids, subscribedIds)
14
- })
15
- // Find IDs currently in Redux that aren't in any subscription
16
- const idsToRemove = _.difference(state[pluralize(model)].ids, ids)
17
- store.dispatch({ type: `${pluralize(model)}/removeMany`, payload: idsToRemove })
9
+ if (type === 'jasonModels/setSubscriptionIds' || type === 'jasonModels/removeSubscriptionIds') {
10
+ const { model, ids } = payload
11
+
12
+ let idsInSubs = []
13
+ _.map(state.jasonModels[model], (subscribedIds, k) => {
14
+ idsInSubs = _.union(idsInSubs, subscribedIds)
18
15
  })
16
+ // Find IDs currently in Redux that aren't in any subscription
17
+ const idsToRemove = _.difference(state[pluralize(model)].ids, idsInSubs)
18
+ store.dispatch({ type: `${pluralize(model)}/removeMany`, payload: idsToRemove })
19
19
  }
20
20
 
21
21
  return result
@@ -4,6 +4,7 @@ import { validate as isUuid } from 'uuid'
4
4
 
5
5
  const csrfToken = (document?.querySelector("meta[name=csrf-token]") as any)?.content
6
6
  axios.defaults.headers.common['X-CSRF-Token'] = csrfToken
7
+
7
8
  const restClient = applyCaseMiddleware(axios.create() as any, {
8
9
  preservedKeys: (key) => {
9
10
  return isUuid(key)
@@ -0,0 +1,38 @@
1
+ import { createConsumer } from "@rails/actioncable"
2
+
3
+ export default function actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnected) {
4
+ const consumer = createConsumer()
5
+ const subscription = (consumer.subscriptions.create({
6
+ channel: 'Jason::Channel'
7
+ }, {
8
+ connected: () => {
9
+ dispatch({ type: 'jason/upsert', payload: { connected: true } })
10
+ console.debug('Connected to ActionCable')
11
+
12
+ // When AC loses connection - all state is lost, so we need to re-initialize all subscriptions
13
+ onConnected()
14
+ },
15
+ received: payload => {
16
+ handlePayload(payload)
17
+ console.debug("ActionCable Payload received: ", payload)
18
+ },
19
+ disconnected: () => {
20
+ dispatch({ type: 'jason/upsert', payload: { connected: false } })
21
+ console.warn('Disconnected from ActionCable')
22
+ }
23
+ }));
24
+
25
+ function getPayload(config, options) {
26
+ subscription.send({ getPayload: config, ...options })
27
+ }
28
+
29
+ function createSubscription(config) {
30
+ subscription.send({ createSubscription: config })
31
+ }
32
+
33
+ function removeSubscription(config) {
34
+ subscription.send({ removeSubscription: config })
35
+ }
36
+
37
+ return { getPayload, createSubscription, removeSubscription }
38
+ }
@@ -0,0 +1,72 @@
1
+ import Pusher from 'pusher-js'
2
+ import { createConsumer } from "@rails/actioncable"
3
+ import restClient from '../restClient'
4
+ import { v4 as uuidv4 } from 'uuid'
5
+ import _ from 'lodash'
6
+
7
+ export default function pusherAdapter(jasonConfig, handlePayload, dispatch) {
8
+ let consumerId = uuidv4()
9
+
10
+ const { pusherKey, pusherRegion, pusherChannelPrefix } = jasonConfig
11
+ const pusher = new Pusher(pusherKey, {
12
+ cluster: 'eu',
13
+ forceTLS: true,
14
+ authEndpoint: '/jason/api/pusher/auth'
15
+ })
16
+ pusher.connection.bind('state_change', ({ current }) => {
17
+ if (current === 'connected') {
18
+ dispatch({ type: 'jason/upsert', payload: { connected: true } })
19
+ } else {
20
+ dispatch({ type: 'jason/upsert', payload: { connected: false } })
21
+ }
22
+ })
23
+ pusher.connection.bind( 'error', error => {
24
+ dispatch({ type: 'jason/upsert', payload: { connected: false } })
25
+ });
26
+
27
+ const configToChannel = {}
28
+
29
+ function createSubscription(config) {
30
+ restClient.post('/jason/api/create_subscription', { config, consumerId })
31
+ .then(({ data: { channelName } }) => {
32
+ configToChannel[JSON.stringify(config)] = channelName
33
+ subscribeToChannel(channelName)
34
+ })
35
+ .catch(e => console.error(e))
36
+ }
37
+
38
+ function removeSubscription(config) {
39
+ const channelName = configToChannel[JSON.stringify(config)]
40
+ unsubscribeFromChannel(fullChannelName(channelName))
41
+ restClient.post('/jason/api/remove_subscription', { config, consumerId })
42
+ .catch(e => console.error(e))
43
+ }
44
+
45
+ function getPayload(config, options) {
46
+ restClient.post('/jason/api/get_payload', {
47
+ config,
48
+ options
49
+ })
50
+ .then(({ data }) => {
51
+ _.map(data, (payload, modelName) => {
52
+ handlePayload(payload)
53
+ })
54
+ })
55
+ .catch(e => console.error(e))
56
+ }
57
+
58
+ function subscribeToChannel(channelName) {
59
+ const channel = pusher.subscribe(fullChannelName(channelName))
60
+ channel.bind('changed', message => handlePayload(message))
61
+ }
62
+
63
+ function unsubscribeFromChannel(channelName) {
64
+ const channel = pusher.unsubscribe(fullChannelName(channelName))
65
+ }
66
+
67
+ function fullChannelName(channelName) {
68
+ return `private-${pusherChannelPrefix}-${channelName}`
69
+ }
70
+
71
+ return { getPayload, createSubscription, removeSubscription }
72
+ }
@@ -5,7 +5,10 @@ import restClient from './restClient'
5
5
  jest.mock('./restClient')
6
6
 
7
7
  test('it works', async () => {
8
- const resp = { data: { post: {} } };
8
+ const resp = { data: {
9
+ schema: { post: {} },
10
+ transportService: 'action_cable'
11
+ } };
9
12
  // @ts-ignore
10
13
  restClient.get.mockResolvedValue(resp);
11
14
 
@@ -45,7 +48,10 @@ test('it works', async () => {
45
48
  })
46
49
 
47
50
  test('pruning IDs', async () => {
48
- const resp = { data: { post: {} } };
51
+ const resp = { data: {
52
+ schema: { post: {} },
53
+ transportService: 'action_cable'
54
+ } };
49
55
 
50
56
  // @ts-ignore
51
57
  restClient.get.mockResolvedValue(resp);
@@ -5,8 +5,8 @@ import createOptDis from './createOptDis'
5
5
  import createServerActionQueue from './createServerActionQueue'
6
6
  import restClient from './restClient'
7
7
  import pruneIdsMiddleware from './pruneIdsMiddleware'
8
+ import createTransportAdapater from './createTransportAdapter'
8
9
 
9
- import { createConsumer } from "@rails/actioncable"
10
10
  import { createEntityAdapter, createSlice, createReducer, configureStore } from '@reduxjs/toolkit'
11
11
 
12
12
  import makeEager from './makeEager'
@@ -18,17 +18,17 @@ import React, { useState, useEffect } from 'react'
18
18
  export default function useJason({ reducers, middleware = [], extraActions }: { reducers?: any, middleware?: any[], extraActions?: any }) {
19
19
  const [store, setStore] = useState(null as any)
20
20
  const [value, setValue] = useState(null as any)
21
- const [connected, setConnected] = useState(false)
22
21
 
23
22
  useEffect(() => {
24
- restClient.get('/jason/api/schema')
25
- .then(({ data: snakey_schema }) => {
23
+ restClient.get('/jason/api/config')
24
+ .then(({ data: jasonConfig }) => {
25
+ const { schema: snakey_schema } = jasonConfig
26
26
  const schema = camelizeKeys(snakey_schema)
27
27
  console.debug({ schema })
28
28
 
29
29
  const serverActionQueue = createServerActionQueue()
30
30
 
31
- const consumer = createConsumer()
31
+
32
32
  const allReducers = {
33
33
  ...reducers,
34
34
  ...createJasonReducers(schema)
@@ -50,54 +50,32 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
50
50
  function handlePayload(payload) {
51
51
  const { md5Hash } = payload
52
52
 
53
- const handler = payloadHandlers[md5Hash]
54
- if (handler) {
55
- handler(payload)
53
+ const { handlePayload } = payloadHandlers[md5Hash]
54
+ if (handlePayload) {
55
+ handlePayload(payload)
56
56
  } else {
57
57
  console.warn("Payload arrived with no handler", payload, payloadHandlers)
58
58
  }
59
59
  }
60
60
 
61
- const subscription = (consumer.subscriptions.create({
62
- channel: 'Jason::Channel'
63
- }, {
64
- connected: () => {
65
- setConnected(true)
66
- dispatch({ type: 'jason/upsert', payload: { connected: true } })
67
- console.debug('Connected to ActionCable')
68
-
69
- // When AC loses connection - all state is lost, so we need to re-initialize all subscriptions
70
- _.keys(configs).forEach(md5Hash => createSubscription(configs[md5Hash], subOptions[md5Hash]))
71
- },
72
- received: payload => {
73
- handlePayload(payload)
74
- console.debug("ActionCable Payload received: ", payload)
75
- },
76
- disconnected: () => {
77
- setConnected(false)
78
- dispatch({ type: 'jason/upsert', payload: { connected: false } })
79
- console.warn('Disconnected from ActionCable')
80
- }
81
- }));
61
+ const transportAdapter = createTransportAdapater(jasonConfig, handlePayload, dispatch, () => _.keys(configs).forEach(md5Hash => createSubscription(configs[md5Hash], subOptions[md5Hash])))
82
62
 
83
63
  function createSubscription(config, options = {}) {
84
64
  // We need the hash to be consistent in Ruby / Javascript
85
65
  const hashableConfig = _({ conditions: {}, includes: {}, ...config }).toPairs().sortBy(0).fromPairs().value()
86
66
  const md5Hash = md5(JSON.stringify(hashableConfig))
87
- payloadHandlers[md5Hash] = createPayloadHandler({ dispatch, serverActionQueue, subscription, config })
67
+ payloadHandlers[md5Hash] = createPayloadHandler({ dispatch, serverActionQueue, transportAdapter, config })
88
68
  configs[md5Hash] = hashableConfig
89
69
  subOptions[md5Hash] = options
90
70
 
91
- setTimeout(() => subscription.send({ createSubscription: hashableConfig }), 500)
71
+ setTimeout(() => transportAdapter.createSubscription(hashableConfig), 500)
92
72
  let pollInterval = null as any;
93
73
 
94
- console.log("createSubscription", { config, options })
95
-
96
74
  // This is only for debugging / dev - not prod!
97
75
  // @ts-ignore
98
76
  if (options.pollInterval) {
99
77
  // @ts-ignore
100
- pollInterval = setInterval(() => subscription.send({ getPayload: config, forceRefresh: true }), options.pollInterval)
78
+ pollInterval = setInterval(() => transportAdapter.getPayload(hashableConfig, { forceRefresh: true }), options.pollInterval)
101
79
  }
102
80
 
103
81
  return {
@@ -110,8 +88,9 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
110
88
  }
111
89
 
112
90
  function removeSubscription(config) {
113
- subscription.send({ removeSubscription: config })
91
+ transportAdapter.removeSubscription(config)
114
92
  const md5Hash = md5(JSON.stringify(config))
93
+ payloadHandlers[md5Hash].tearDown()
115
94
  delete payloadHandlers[md5Hash]
116
95
  delete configs[md5Hash]
117
96
  delete subOptions[md5Hash]
@@ -127,5 +106,5 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
127
106
  })
128
107
  }, [])
129
108
 
130
- return [store, value, connected]
109
+ return [store, value]
131
110
  }
data/client/yarn.lock CHANGED
@@ -3645,6 +3645,13 @@ punycode@^2.1.0, punycode@^2.1.1:
3645
3645
  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
3646
3646
  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
3647
3647
 
3648
+ pusher-js@^7.0.3:
3649
+ version "7.0.3"
3650
+ resolved "https://registry.yarnpkg.com/pusher-js/-/pusher-js-7.0.3.tgz#f81c78cdf2ad32f546caa7532ec7f9081ef00b8d"
3651
+ integrity sha512-HIfCvt00CAqgO4W0BrdpPsDcAwy51rB6DN0VMC+JeVRRbo8mn3XTeUeIFjmmlRLZLX8rPhUtLRo7vPag6b8GCw==
3652
+ dependencies:
3653
+ tweetnacl "^1.0.3"
3654
+
3648
3655
  qs@~6.5.2:
3649
3656
  version "6.5.2"
3650
3657
  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@@ -4358,6 +4365,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
4358
4365
  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
4359
4366
  integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
4360
4367
 
4368
+ tweetnacl@^1.0.3:
4369
+ version "1.0.3"
4370
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
4371
+ integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
4372
+
4361
4373
  type-check@~0.3.2:
4362
4374
  version "0.3.2"
4363
4375
  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
data/config/routes.rb CHANGED
@@ -1,4 +1,8 @@
1
1
  Jason::Engine.routes.draw do
2
- get '/api/schema', to: 'api#schema'
2
+ get '/api/config', to: 'api#configuration'
3
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'
4
8
  end