jason-rails 0.3.0 → 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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.ruby-version +1 -0
  4. data/Gemfile.lock +184 -0
  5. data/README.md +118 -10
  6. data/app/controllers/jason/api/pusher_controller.rb +15 -0
  7. data/app/controllers/jason/api_controller.rb +78 -0
  8. data/client/babel.config.js +13 -0
  9. data/client/lib/JasonContext.d.ts +6 -1
  10. data/client/lib/JasonProvider.d.ts +6 -5
  11. data/client/lib/JasonProvider.js +5 -97
  12. data/client/lib/actionFactory.js +1 -1
  13. data/client/lib/createActions.d.ts +1 -1
  14. data/client/lib/createActions.js +2 -27
  15. data/client/lib/createJasonReducers.js +49 -3
  16. data/client/lib/createOptDis.d.ts +1 -0
  17. data/client/lib/createOptDis.js +43 -0
  18. data/client/lib/createPayloadHandler.d.ts +9 -1
  19. data/client/lib/createPayloadHandler.js +52 -43
  20. data/client/lib/createServerActionQueue.d.ts +10 -0
  21. data/client/lib/createServerActionQueue.js +48 -0
  22. data/client/lib/createServerActionQueue.test.d.ts +1 -0
  23. data/client/lib/createServerActionQueue.test.js +37 -0
  24. data/client/lib/createTransportAdapter.d.ts +5 -0
  25. data/client/lib/createTransportAdapter.js +20 -0
  26. data/client/lib/deepCamelizeKeys.d.ts +1 -0
  27. data/client/lib/deepCamelizeKeys.js +23 -0
  28. data/client/lib/deepCamelizeKeys.test.d.ts +1 -0
  29. data/client/lib/deepCamelizeKeys.test.js +106 -0
  30. data/client/lib/index.d.ts +6 -5
  31. data/client/lib/pruneIdsMiddleware.d.ts +2 -0
  32. data/client/lib/pruneIdsMiddleware.js +24 -0
  33. data/client/lib/restClient.d.ts +2 -0
  34. data/client/lib/restClient.js +17 -0
  35. data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
  36. data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
  37. data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
  38. data/client/lib/transportAdapters/pusherAdapter.js +68 -0
  39. data/client/lib/useJason.d.ts +5 -0
  40. data/client/lib/useJason.js +94 -0
  41. data/client/lib/useJason.test.d.ts +1 -0
  42. data/client/lib/useJason.test.js +85 -0
  43. data/client/lib/useSub.d.ts +1 -1
  44. data/client/lib/useSub.js +6 -3
  45. data/client/package.json +19 -4
  46. data/client/src/JasonProvider.tsx +6 -96
  47. data/client/src/actionFactory.ts +1 -1
  48. data/client/src/createActions.ts +2 -33
  49. data/client/src/createJasonReducers.ts +57 -3
  50. data/client/src/createOptDis.ts +45 -0
  51. data/client/src/createPayloadHandler.ts +58 -47
  52. data/client/src/createServerActionQueue.test.ts +42 -0
  53. data/client/src/createServerActionQueue.ts +47 -0
  54. data/client/src/createTransportAdapter.ts +13 -0
  55. data/client/src/deepCamelizeKeys.test.ts +113 -0
  56. data/client/src/deepCamelizeKeys.ts +17 -0
  57. data/client/src/pruneIdsMiddleware.ts +24 -0
  58. data/client/src/restClient.ts +14 -0
  59. data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
  60. data/client/src/transportAdapters/pusherAdapter.ts +72 -0
  61. data/client/src/useJason.test.ts +87 -0
  62. data/client/src/useJason.ts +110 -0
  63. data/client/src/useSub.ts +6 -3
  64. data/client/yarn.lock +4607 -81
  65. data/config/routes.rb +8 -0
  66. data/jason-rails.gemspec +9 -0
  67. data/lib/jason.rb +40 -1
  68. data/lib/jason/api_model.rb +15 -9
  69. data/lib/jason/broadcaster.rb +19 -0
  70. data/lib/jason/channel.rb +50 -21
  71. data/lib/jason/engine.rb +5 -0
  72. data/lib/jason/graph_helper.rb +165 -0
  73. data/lib/jason/includes_helper.rb +108 -0
  74. data/lib/jason/lua_generator.rb +71 -0
  75. data/lib/jason/publisher.rb +103 -30
  76. data/lib/jason/publisher_old.rb +112 -0
  77. data/lib/jason/subscription.rb +352 -101
  78. data/lib/jason/subscription_old.rb +171 -0
  79. data/lib/jason/version.rb +1 -1
  80. metadata +151 -4
@@ -0,0 +1,45 @@
1
+ import _ from 'lodash'
2
+ import pluralize from 'pluralize'
3
+ import { v4 as uuidv4 } from 'uuid'
4
+
5
+ function enrich(type, payload) {
6
+ if (type.split('/')[1] === 'upsert' && !(type.split('/')[0] === 'session')) {
7
+ if (!payload.id) {
8
+ return { ...payload, id: uuidv4() }
9
+ }
10
+ }
11
+ return payload
12
+ }
13
+
14
+ export default function createOptDis(schema, dispatch, restClient, serverActionQueue) {
15
+ const plurals = _.keys(schema).map(k => pluralize(k))
16
+
17
+ function enqueueServerAction (action) {
18
+ serverActionQueue.addItem(action)
19
+ }
20
+
21
+ function dispatchServerAction() {
22
+ const action = serverActionQueue.getItem()
23
+ if (!action) return
24
+
25
+ restClient.post('/jason/api/action', action)
26
+ .then(serverActionQueue.itemProcessed)
27
+ .catch(e => {
28
+ dispatch({ type: 'upsertLocalUi', data: { error: JSON.stringify(e) } })
29
+ serverActionQueue.itemProcessed()
30
+ })
31
+ }
32
+
33
+ setInterval(dispatchServerAction, 10)
34
+
35
+ return function (action) {
36
+ const { type, payload } = action
37
+ const data = enrich(type, payload)
38
+
39
+ dispatch({ type, payload: data })
40
+
41
+ if (plurals.indexOf(type.split('/')[0]) > -1) {
42
+ enqueueServerAction({ type, payload: data })
43
+ }
44
+ }
45
+ }
@@ -1,17 +1,18 @@
1
1
  import { apply_patch } from 'jsonpatch'
2
- import { camelizeKeys } from 'humps'
2
+ import deepCamelizeKeys from './deepCamelizeKeys'
3
3
  import pluralize from 'pluralize'
4
4
  import _ from 'lodash'
5
+ import { validate as isUuid, v4 as uuidv4 } from 'uuid'
5
6
 
6
7
  function diffSeconds(dt2, dt1) {
7
8
  var diff =(dt2.getTime() - dt1.getTime()) / 1000
8
9
  return Math.abs(Math.round(diff))
9
10
  }
10
11
 
11
- export default function createPayloadHandler(dispatch, subscription, model, config) {
12
- console.log({ model, config })
13
- let payload = {}
14
- let idx = 0
12
+ export default function createPayloadHandler({ dispatch, serverActionQueue, transportAdapter, config }) {
13
+ const subscriptionId = uuidv4()
14
+
15
+ let idx = {}
15
16
  let patchQueue = {}
16
17
 
17
18
  let lastCheckAt = new Date()
@@ -19,77 +20,87 @@ export default function createPayloadHandler(dispatch, subscription, model, conf
19
20
  let checkInterval
20
21
 
21
22
  function getPayload() {
22
- console.log({ getPayload: model, subscription })
23
- subscription.send({ getPayload: { model, config } })
23
+ setTimeout(() => transportAdapter.getPayload(config), 1000)
24
24
  }
25
25
 
26
- const tGetPayload = _.throttle(getPayload, 10000)
27
-
28
- function dispatchPayload() {
29
- const includeModels = (config.includeModels || []).map(m => _.camelCase(m))
30
-
31
- console.log("Dispatching", { payload, includeModels })
32
-
33
- includeModels.forEach(m => {
34
- const subPayload = _.flatten(_.compact(camelizeKeys(payload).map(instance => instance[m])))
35
- console.log({ type: `${pluralize(m)}/upsertMany`, payload: subPayload })
36
- dispatch({ type: `${pluralize(m)}/upsertMany`, payload: subPayload })
37
- })
38
-
39
- dispatch({ type: `${pluralize(model)}/upsertMany`, payload: camelizeKeys(payload) })
26
+ function camelizeKeys(item) {
27
+ return deepCamelizeKeys(item, key => isUuid(key))
40
28
  }
41
29
 
42
- function processQueue() {
43
- console.log({ idx, patchQueue })
30
+ const tGetPayload = _.throttle(getPayload, 10000)
31
+
32
+ function processQueue(model) {
33
+ console.debug("processQueue", model, idx[model], patchQueue[model])
44
34
  lastCheckAt = new Date()
45
- if (patchQueue[idx]) {
46
- payload = apply_patch(payload, patchQueue[idx])
47
- if (patchQueue[idx]) {
48
- dispatchPayload()
35
+ if (patchQueue[model][idx[model]]) {
36
+ if (!serverActionQueue.fullySynced()) {
37
+ console.debug(serverActionQueue.getData())
38
+ setTimeout(() => processQueue(model), 100)
39
+ return
49
40
  }
50
- delete patchQueue[idx]
51
- idx++
41
+
42
+ const { payload, destroy, id, type } = patchQueue[model][idx[model]]
43
+
44
+ if (type === 'payload') {
45
+ dispatch({ type: `${pluralize(model)}/upsertMany`, payload })
46
+ const ids = payload.map(instance => instance.id)
47
+ dispatch({ type: `jasonModels/setSubscriptionIds`, payload: { model, subscriptionId, ids }})
48
+ } else if (destroy) {
49
+ // Middleware will determine if this model should be removed if it isn't in any other subscriptions
50
+ dispatch({ type: `jasonModels/removeSubscriptionId`, payload: { model, subscriptionId, id }})
51
+ } else {
52
+ dispatch({ type: `${pluralize(model)}/upsert`, payload })
53
+ dispatch({ type: `jasonModels/addSubscriptionId`, payload: { model, subscriptionId, id }})
54
+ }
55
+
56
+ delete patchQueue[model][idx[model]]
57
+ idx[model]++
52
58
  updateDeadline = null
53
- processQueue()
59
+ processQueue(model)
54
60
  // If there are updates in the queue that are ahead of the index, some have arrived out of order
55
61
  // Set a deadline for new updates before it declares the update missing and refetches.
56
- } else if (_.keys(patchQueue).length > 0 && !updateDeadline) {
62
+ } else if (_.keys(patchQueue[model]).length > 0 && !updateDeadline) {
57
63
  var t = new Date()
58
64
  t.setSeconds(t.getSeconds() + 3)
59
65
  updateDeadline = t
60
- setTimeout(processQueue, 3100)
66
+ setTimeout(() => processQueue(model), 3100)
61
67
  // If more than 10 updates in queue, or deadline has passed, restart
62
- } else if (_.keys(patchQueue).length > 10 || (updateDeadline && diffSeconds(updateDeadline, new Date()) < 0)) {
68
+ } else if (_.keys(patchQueue[model]).length > 10 || (updateDeadline && diffSeconds(updateDeadline, new Date()) < 0)) {
63
69
  tGetPayload()
64
70
  updateDeadline = null
65
71
  }
66
72
  }
67
73
 
68
74
  function handlePayload(data) {
69
- const { value, idx: newIdx, diff, latency, type } = data
70
- console.log({ data })
75
+ const { idx: newIdx, model: snake_model, type } = data
76
+ const model = _.camelCase(snake_model)
71
77
 
72
- if (type === 'payload') {
73
- if (!value) return null;
78
+ idx[model] = idx[model] || 0
79
+ patchQueue[model] = patchQueue[model] || {}
74
80
 
75
- payload = value
76
- dispatchPayload()
77
- idx = newIdx + 1
81
+ if (type === 'payload') {
82
+ idx[model] = newIdx
78
83
  // Clear any old changes left in the queue
79
- patchQueue= _.pick(patchQueue, _.keys(patchQueue).filter(k => k > newIdx + 1))
80
- return
84
+ patchQueue[model] = _.pick(patchQueue[model], _.keys(patchQueue[model]).filter(k => k > newIdx + 1))
81
85
  }
82
86
 
83
- patchQueue[newIdx] = diff
84
-
85
- processQueue()
87
+ patchQueue[model][newIdx] = camelizeKeys({ ...data, model })
88
+ console.debug("Added to queue", model, idx[model], camelizeKeys({ ...data, model }), serverActionQueue.getData())
89
+ processQueue(model)
86
90
 
87
91
  if (diffSeconds((new Date()), lastCheckAt) >= 3) {
88
92
  lastCheckAt = new Date()
89
- console.log('Interval lost. Pulling from server')
93
+ console.debug('Interval lost. Pulling from server')
90
94
  tGetPayload()
91
95
  }
92
96
  }
93
97
 
94
- return handlePayload
98
+ tGetPayload()
99
+
100
+ // Clean up after ourselves
101
+ function tearDown() {
102
+ dispatch({ type: `jasonModels/removeSubscription`, payload: { subscriptionId }})
103
+ }
104
+
105
+ return { handlePayload, tearDown }
95
106
  }
@@ -0,0 +1,42 @@
1
+ import createServerActionQueue from './createServerActionQueue'
2
+
3
+ test('Adding items', () => {
4
+ const serverActionQueue = createServerActionQueue()
5
+ serverActionQueue.addItem({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } })
6
+ const item = serverActionQueue.getItem()
7
+ expect(item).toStrictEqual({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } })
8
+ })
9
+
10
+ test('Deduping of items that will overwrite each other', () => {
11
+ const serverActionQueue = createServerActionQueue()
12
+ serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
13
+ serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2 } })
14
+ serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 3 } })
15
+
16
+ const item = serverActionQueue.getItem()
17
+
18
+ expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 3 } })
19
+ })
20
+
21
+ test('Deduping of items with a superset', () => {
22
+ const serverActionQueue = createServerActionQueue()
23
+ serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
24
+ serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } })
25
+
26
+ const item = serverActionQueue.getItem()
27
+
28
+ expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } })
29
+ })
30
+
31
+ test("doesn't dedupe items with some attributes missing", () => {
32
+ const serverActionQueue = createServerActionQueue()
33
+ serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
34
+ serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute2: 'test' } })
35
+
36
+ const item = serverActionQueue.getItem()
37
+ serverActionQueue.itemProcessed()
38
+ const item2 = serverActionQueue.getItem()
39
+
40
+ expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
41
+ expect(item2).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute2: 'test' } })
42
+ })
@@ -0,0 +1,47 @@
1
+ // A FIFO queue with deduping of actions whose effect will be cancelled by later actions
2
+
3
+ import _ from 'lodash'
4
+
5
+ export default function createServerActionQueue() {
6
+ const queue: any[] = []
7
+ let inFlight = false
8
+
9
+ function addItem(item) {
10
+ // Check if there are any items ahead in the queue that this item would effectively overwrite.
11
+ // In that case we can remove them
12
+ // 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
14
+ if (type.split('/')[1] !== 'upsert') {
15
+ queue.push(item)
16
+ return
17
+ }
18
+
19
+ _.remove(queue, item => {
20
+ const { type: itemType, payload: itemPayload } = item
21
+ if (type !== itemType) return false
22
+ if (itemPayload.id !== payload.id) return false
23
+
24
+ // Check that all keys of itemPayload are in payload.
25
+ return _.difference(_.keys(itemPayload),_.keys(payload)).length === 0
26
+ })
27
+
28
+ queue.push(item)
29
+ }
30
+
31
+ return {
32
+ addItem,
33
+ getItem: () => {
34
+ if (inFlight) return false
35
+
36
+ const item = queue.shift()
37
+ if (item) {
38
+ inFlight = true
39
+ return item
40
+ }
41
+ return false
42
+ },
43
+ itemProcessed: () => inFlight = false,
44
+ fullySynced: () => queue.length === 0 && !inFlight,
45
+ getData: () => ({ queue, inFlight })
46
+ }
47
+ }
@@ -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
+ }
@@ -0,0 +1,113 @@
1
+ import deepCamelizeKeys from './deepCamelizeKeys'
2
+
3
+ test('scalar number', () => {
4
+ expect(deepCamelizeKeys(1)).toBe(1)
5
+ })
6
+
7
+ test('scalar number float', () => {
8
+ expect(deepCamelizeKeys(1.123)).toBe(1.123)
9
+ })
10
+
11
+ test('scalar string', () => {
12
+ expect(deepCamelizeKeys('test')).toBe('test')
13
+ })
14
+
15
+ test('scalar null', () => {
16
+ expect(deepCamelizeKeys(null)).toBe(null)
17
+ })
18
+
19
+ test('scalar boolean', () => {
20
+ expect(deepCamelizeKeys(true)).toBe(true)
21
+ })
22
+
23
+ test('object with existing camelized keys', () => {
24
+ expect(deepCamelizeKeys({ testMe: 'test' })).toStrictEqual({ testMe: 'test' })
25
+ })
26
+
27
+ test('array with existing camelized keys', () => {
28
+ expect(deepCamelizeKeys([{ testMe: 'test' }, { testMe2: 'test' }])).toStrictEqual([{ testMe: 'test' }, { testMe2: 'test' }])
29
+ })
30
+
31
+ test('object with mixed keys', () => {
32
+ expect(deepCamelizeKeys({ testMe: 'test', test_2: 'dog', test_me2: true })).toStrictEqual({ testMe: 'test', test2: 'dog', testMe2: true })
33
+ })
34
+
35
+ test('array with mixed keys', () => {
36
+ expect(deepCamelizeKeys([
37
+ { testMe: 'test', test_2: 'dog', test_me2: true },
38
+ { testMe3: 'test', test_3: 'dog', test_me4: true }
39
+ ])).toStrictEqual([
40
+ { testMe: 'test', test2: 'dog', testMe2: true },
41
+ { testMe3: 'test', test3: 'dog', testMe4: true }
42
+ ])
43
+ })
44
+
45
+ test('nested with object at top level', () => {
46
+ expect(deepCamelizeKeys({
47
+ test_me: {
48
+ test_me2: {
49
+ test_me3: [
50
+ { test_it_out: '49' },
51
+ { test_fun: 'what' }
52
+ ]
53
+ }
54
+ }
55
+ })).toStrictEqual({
56
+ testMe: {
57
+ testMe2: {
58
+ testMe3: [
59
+ { testItOut: '49' },
60
+ { testFun: 'what' }
61
+ ]
62
+ }
63
+ }
64
+ })
65
+ })
66
+
67
+ test('nested with object at top level', () => {
68
+ expect(deepCamelizeKeys([{
69
+ test_me: {
70
+ test_me2: {
71
+ test_me3: [
72
+ { test_it_out: '49' },
73
+ { test_fun: 'what' }
74
+ ]
75
+ }
76
+ }
77
+ }, {
78
+ test_it52: 'what?'
79
+ }])).toStrictEqual([{
80
+ testMe: {
81
+ testMe2: {
82
+ testMe3: [
83
+ { testItOut: '49' },
84
+ { testFun: 'what' }
85
+ ]
86
+ }
87
+ }
88
+ }, {
89
+ testIt52: 'what?'
90
+ }])
91
+ })
92
+
93
+ test('excludes keys by function', () => {
94
+ expect(deepCamelizeKeys({
95
+ test_me: {
96
+ test_me2: {
97
+ test_me3: [
98
+ { test_it_out: '49' },
99
+ { test_fun: 'what' }
100
+ ]
101
+ }
102
+ }
103
+ }, k => (k === 'test_me2'))).toStrictEqual({
104
+ testMe: {
105
+ test_me2: {
106
+ testMe3: [
107
+ { testItOut: '49' },
108
+ { testFun: 'what' }
109
+ ]
110
+ }
111
+ }
112
+ })
113
+ })
@@ -0,0 +1,17 @@
1
+ import _ from 'lodash'
2
+
3
+ export default function deepCamelizeKeys(item, excludeIf = k => false) {
4
+ function camelizeKey(key) {
5
+ if (excludeIf(key)) return key
6
+ return _.camelCase(key)
7
+ }
8
+
9
+ if (_.isArray(item)) {
10
+ return _.map(item, item => deepCamelizeKeys(item, excludeIf))
11
+ } else if (_.isObject(item)) {
12
+ return _.mapValues(_.mapKeys(item, (v, k) => camelizeKey(k)), (v, k) => deepCamelizeKeys(v, excludeIf))
13
+ } else {
14
+ return item
15
+ }
16
+ }
17
+
@@ -0,0 +1,24 @@
1
+ import _ from 'lodash'
2
+ import pluralize from 'pluralize'
3
+
4
+ const pruneIdsMiddleware = schema => store => next => action => {
5
+ const { type, payload } = action
6
+ const result = next(action)
7
+
8
+ const state = store.getState()
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)
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
+ }
20
+
21
+ return result
22
+ }
23
+
24
+ export default pruneIdsMiddleware