jason-rails 0.4.1 → 0.6.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.ruby-version +1 -0
  4. data/Gemfile.lock +152 -2
  5. data/README.md +117 -5
  6. data/app/controllers/jason/api/pusher_controller.rb +15 -0
  7. data/app/controllers/jason/api_controller.rb +44 -2
  8. data/client/lib/JasonContext.d.ts +6 -1
  9. data/client/lib/JasonContext.js +4 -1
  10. data/client/lib/JasonProvider.d.ts +2 -2
  11. data/client/lib/JasonProvider.js +5 -124
  12. data/client/lib/createJasonReducers.js +48 -3
  13. data/client/lib/createOptDis.js +0 -2
  14. data/client/lib/createPayloadHandler.d.ts +9 -1
  15. data/client/lib/createPayloadHandler.js +47 -55
  16. data/client/lib/createServerActionQueue.d.ts +10 -0
  17. data/client/lib/createServerActionQueue.js +48 -0
  18. data/client/lib/createServerActionQueue.test.d.ts +1 -0
  19. data/client/lib/createServerActionQueue.test.js +37 -0
  20. data/client/lib/createTransportAdapter.d.ts +5 -0
  21. data/client/lib/createTransportAdapter.js +20 -0
  22. data/client/lib/index.d.ts +5 -2
  23. data/client/lib/index.js +3 -1
  24. data/client/lib/makeEager.js +2 -2
  25. data/client/lib/pruneIdsMiddleware.d.ts +2 -0
  26. data/client/lib/pruneIdsMiddleware.js +24 -0
  27. data/client/lib/restClient.d.ts +2 -0
  28. data/client/lib/restClient.js +17 -0
  29. data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
  30. data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
  31. data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
  32. data/client/lib/transportAdapters/pusherAdapter.js +68 -0
  33. data/client/lib/useJason.d.ts +5 -0
  34. data/client/lib/useJason.js +94 -0
  35. data/client/lib/useJason.test.d.ts +1 -0
  36. data/client/lib/useJason.test.js +85 -0
  37. data/client/lib/useSub.d.ts +1 -1
  38. data/client/lib/useSub.js +6 -3
  39. data/client/package.json +5 -3
  40. data/client/src/JasonContext.ts +4 -1
  41. data/client/src/JasonProvider.tsx +5 -123
  42. data/client/src/createJasonReducers.ts +56 -3
  43. data/client/src/createOptDis.ts +0 -2
  44. data/client/src/createPayloadHandler.ts +53 -64
  45. data/client/src/createServerActionQueue.test.ts +42 -0
  46. data/client/src/createServerActionQueue.ts +47 -0
  47. data/client/src/createTransportAdapter.ts +13 -0
  48. data/client/src/index.ts +3 -1
  49. data/client/src/makeEager.ts +2 -2
  50. data/client/src/pruneIdsMiddleware.ts +24 -0
  51. data/client/src/restClient.ts +14 -0
  52. data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
  53. data/client/src/transportAdapters/pusherAdapter.ts +72 -0
  54. data/client/src/useJason.test.ts +87 -0
  55. data/client/src/useJason.ts +110 -0
  56. data/client/src/useSub.ts +6 -3
  57. data/client/yarn.lock +71 -3
  58. data/config/routes.rb +5 -1
  59. data/jason-rails.gemspec +4 -0
  60. data/lib/jason.rb +61 -1
  61. data/lib/jason/api_model.rb +2 -12
  62. data/lib/jason/broadcaster.rb +19 -0
  63. data/lib/jason/channel.rb +50 -21
  64. data/lib/jason/graph_helper.rb +165 -0
  65. data/lib/jason/includes_helper.rb +108 -0
  66. data/lib/jason/lua_generator.rb +71 -0
  67. data/lib/jason/publisher.rb +82 -37
  68. data/lib/jason/publisher_old.rb +112 -0
  69. data/lib/jason/subscription.rb +349 -97
  70. data/lib/jason/subscription_old.rb +171 -0
  71. data/lib/jason/version.rb +1 -1
  72. metadata +80 -3
@@ -1 +1 @@
1
- export default function useSub(config: any): void;
1
+ export default function useSub(config: any, options?: {}): void;
data/client/lib/useSub.js CHANGED
@@ -5,10 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const JasonContext_1 = __importDefault(require("./JasonContext"));
7
7
  const react_1 = require("react");
8
- function useSub(config) {
8
+ function useSub(config, options = {}) {
9
+ // useEffect uses strict equality
10
+ const configJson = JSON.stringify(config);
9
11
  const subscribe = react_1.useContext(JasonContext_1.default).subscribe;
10
12
  react_1.useEffect(() => {
11
- return subscribe(config);
12
- }, []);
13
+ // @ts-ignore
14
+ return subscribe(config, options).remove;
15
+ }, [configJson]);
13
16
  }
14
17
  exports.default = useSub;
data/client/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jamesr2323/jason",
3
- "version": "0.4.0",
3
+ "version": "0.6.1",
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": {
@@ -23,11 +24,12 @@
23
24
  "@babel/preset-env": "^7.12.11",
24
25
  "@babel/preset-typescript": "^7.12.7",
25
26
  "@reduxjs/toolkit": "^1.5.0",
27
+ "@testing-library/react-hooks": "^5.0.3",
26
28
  "@types/jest": "^26.0.19",
27
29
  "babel-jest": "^26.6.3",
28
30
  "jest": "^26.6.3",
29
- "react": "^16.8.3",
30
- "react-dom": "^16.8.3",
31
+ "react": "^16.9.x",
32
+ "react-dom": "^16.9.x",
31
33
  "react-redux": "^7.2.2",
32
34
  "typescript": "^4.1.2"
33
35
  },
@@ -1,5 +1,8 @@
1
1
  import { createContext } from 'react'
2
+ const eager = function(entity, id, relations) {
3
+ console.error("Eager called but is not implemented")
4
+ }
2
5
 
3
- const context = createContext({ actions: {} as any, subscribe: null, eager: null })
6
+ const context = createContext({ actions: {} as any, subscribe: null, eager })
4
7
 
5
8
  export default context
@@ -1,130 +1,12 @@
1
- import createActions from './createActions'
2
- import { createConsumer } from "@rails/actioncable"
3
- import JasonContext from './JasonContext'
4
- import axios from 'axios'
5
- import applyCaseMiddleware from 'axios-case-converter'
1
+ import React from 'react'
2
+ import useJason from './useJason'
6
3
  import { Provider } from 'react-redux'
7
- import { createEntityAdapter, createSlice, createReducer, configureStore } from '@reduxjs/toolkit'
8
- import createJasonReducers from './createJasonReducers'
9
- import createPayloadHandler from './createPayloadHandler'
10
- import createOptDis from './createOptDis'
11
- import makeEager from './makeEager'
12
- import { camelizeKeys } from 'humps'
13
- import md5 from 'blueimp-md5'
14
- import _ from 'lodash'
15
- import React, { useState, useEffect } from 'react'
16
- import { validate as isUuid } from 'uuid'
4
+ import JasonContext from './JasonContext'
17
5
 
18
6
  const JasonProvider = ({ reducers, middleware, extraActions, children }: { reducers?: any, middleware?: any, extraActions?: any, children?: React.FC }) => {
19
- const [store, setStore] = useState(null)
20
- const [value, setValue] = useState(null)
21
- const [connected, setConnected] = useState(false)
22
-
23
- const csrfToken = (document.querySelector("meta[name=csrf-token]") as any).content
24
- axios.defaults.headers.common['X-CSRF-Token'] = csrfToken
25
- const restClient = applyCaseMiddleware(axios.create(), {
26
- preservedKeys: (key) => {
27
- return isUuid(key)
28
- }
29
- })
30
-
31
- useEffect(() => {
32
- restClient.get('/jason/api/schema')
33
- .then(({ data: snakey_schema }) => {
34
- const schema = camelizeKeys(snakey_schema)
35
-
36
- const serverActionQueue = function() {
37
- const queue: any[] = []
38
- let inFlight = false
39
-
40
- return {
41
- addItem: (item) => queue.push(item),
42
- getItem: () => {
43
- if (inFlight) return false
44
-
45
- const item = queue.shift()
46
- if (item) {
47
- inFlight = true
48
- return item
49
- }
50
- return false
51
- },
52
- itemProcessed: () => inFlight = false,
53
- fullySynced: () => queue.length === 0 && !inFlight,
54
- getData: () => ({ queue, inFlight })
55
- }
56
- }()
57
-
58
- const consumer = createConsumer()
59
- const allReducers = {
60
- ...reducers,
61
- ...createJasonReducers(schema)
62
- }
63
-
64
- console.log({ schema, allReducers })
65
- const store = configureStore({ reducer: allReducers, middleware })
66
-
67
- let payloadHandlers = {}
68
- function handlePayload(payload) {
69
- const { model, md5Hash } = payload
70
- console.log({ md5Hash, fn: `${model}:${md5Hash}`, payloadHandlers, model: _.camelCase(model), payload })
71
- const handler = payloadHandlers[`${_.camelCase(model)}:${md5Hash}`]
72
- if (handler) {
73
- handler({ ...payload, model: _.camelCase(model) })
74
- }
75
- }
76
-
77
- const subscription = (consumer.subscriptions.create({
78
- channel: 'Jason::Channel'
79
- }, {
80
- connected: () => {
81
- setConnected(true)
82
- },
83
- received: payload => {
84
- console.log("Payload received", payload)
85
- handlePayload(payload)
86
- },
87
- disconnected: () => console.warn('Disconnected from ActionCable')
88
- }));
89
-
90
- console.log('sending message')
91
- subscription.send({ message: 'test' })
92
-
93
- function createSubscription(config) {
94
- const md5Hash = md5(JSON.stringify(config))
95
- console.log('Subscribe with', config, md5Hash)
96
-
97
- _.map(config, (v, model) => {
98
- payloadHandlers[`${model}:${md5Hash}`] = createPayloadHandler(store.dispatch, serverActionQueue, subscription, model, schema[model])
99
- })
100
- subscription.send({ createSubscription: config })
101
-
102
- return () => removeSubscription(config)
103
- }
104
-
105
- function removeSubscription(config) {
106
- subscription.send({ removeSubscription: config })
107
- const md5Hash = md5(JSON.stringify(config))
108
- _.map(config, (v, model) => {
109
- delete payloadHandlers[`${model}:${md5Hash}`]
110
- })
111
- }
112
- const optDis = createOptDis(schema, store.dispatch, restClient, serverActionQueue)
113
- const actions = createActions(schema, store, restClient, optDis, extraActions)
114
- const eager = makeEager(schema)
115
-
116
- console.log({ actions })
117
-
118
- setValue({
119
- actions: actions,
120
- subscribe: (config) => createSubscription(config),
121
- eager
122
- })
123
- setStore(store)
124
- })
125
- }, [])
7
+ const [store, value] = useJason({ reducers, middleware, extraActions })
126
8
 
127
- if(!(store && value && connected)) return <div /> // Wait for async fetch of schema to complete
9
+ if(!(store && value)) return <div /> // Wait for async fetch of schema to complete
128
10
 
129
11
  return <Provider store={store}>
130
12
  <JasonContext.Provider value={value}>{ children }</JasonContext.Provider>
@@ -2,8 +2,8 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'
2
2
  import pluralize from 'pluralize'
3
3
  import _ from 'lodash'
4
4
 
5
- function generateSlices(schema) {
6
- const sliceNames = schema.map(k => pluralize(k))
5
+ function generateSlices(models) {
6
+ const sliceNames = models.map(k => pluralize(k))
7
7
  const adapter = createEntityAdapter()
8
8
 
9
9
  return _.fromPairs(_.map(sliceNames, name => {
@@ -30,6 +30,59 @@ function generateSlices(schema) {
30
30
  }))
31
31
  }
32
32
 
33
+ function generateJasonSlices(models) {
34
+ const initialState = _.fromPairs(_.map(models, (model_name) => {
35
+ return [model_name, {}]
36
+ }))
37
+
38
+ const modelSliceReducer = createSlice({
39
+ name: 'jasonModels',
40
+ initialState,
41
+ reducers: {
42
+ setSubscriptionIds(s,a) {
43
+ const { payload } = a
44
+ const { subscriptionId, model, ids } = payload
45
+ console.log({ initialState })
46
+ s[model][subscriptionId] = ids
47
+ },
48
+ addSubscriptionId(s,a) {
49
+ const { payload } = a
50
+ const { subscriptionId, model, id } = payload
51
+ s[model][subscriptionId] = _.union(s[model][subscriptionId] || [], [id])
52
+ },
53
+ removeSubscriptionId(s,a) {
54
+ const { payload } = a
55
+ const { subscriptionId, model, id } = payload
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
+ })
63
+ }
64
+ }
65
+ }).reducer
66
+
67
+ const jasonSliceReducer = createSlice({
68
+ name: 'jason',
69
+ initialState: {
70
+ connected: false,
71
+ queueSize: 0
72
+ },
73
+ reducers: {
74
+ upsert: (s,a) => ({ ...s, ...a.payload })
75
+ }
76
+ }).reducer
77
+
78
+ return { jason: jasonSliceReducer, jasonModels: modelSliceReducer }
79
+ }
80
+
33
81
  export default function createJasonReducers(schema) {
34
- return generateSlices(_.keys(schema))
82
+ const models = _.keys(schema)
83
+
84
+ return {
85
+ ...generateSlices(models),
86
+ ...generateJasonSlices(models)
87
+ }
35
88
  }
@@ -13,7 +13,6 @@ function enrich(type, payload) {
13
13
 
14
14
  export default function createOptDis(schema, dispatch, restClient, serverActionQueue) {
15
15
  const plurals = _.keys(schema).map(k => pluralize(k))
16
- let inFlight = false
17
16
 
18
17
  function enqueueServerAction (action) {
19
18
  serverActionQueue.addItem(action)
@@ -23,7 +22,6 @@ export default function createOptDis(schema, dispatch, restClient, serverActionQ
23
22
  const action = serverActionQueue.getItem()
24
23
  if (!action) return
25
24
 
26
- inFlight = true
27
25
  restClient.post('/jason/api/action', action)
28
26
  .then(serverActionQueue.itemProcessed)
29
27
  .catch(e => {
@@ -2,18 +2,17 @@ import { apply_patch } from 'jsonpatch'
2
2
  import deepCamelizeKeys from './deepCamelizeKeys'
3
3
  import pluralize from 'pluralize'
4
4
  import _ from 'lodash'
5
- import { validate as isUuid } from 'uuid'
5
+ import { validate as isUuid, v4 as uuidv4 } from 'uuid'
6
6
 
7
7
  function diffSeconds(dt2, dt1) {
8
8
  var diff =(dt2.getTime() - dt1.getTime()) / 1000
9
9
  return Math.abs(Math.round(diff))
10
10
  }
11
11
 
12
- export default function createPayloadHandler(dispatch, serverActionQueue, subscription, model, config) {
13
- console.log({ model, config })
14
- let payload = [] as any[]
15
- let previousPayload = [] as any[]
16
- let idx = 0
12
+ export default function createPayloadHandler({ dispatch, serverActionQueue, transportAdapter, config }) {
13
+ const subscriptionId = uuidv4()
14
+
15
+ let idx = {}
17
16
  let patchQueue = {}
18
17
 
19
18
  let lastCheckAt = new Date()
@@ -21,8 +20,7 @@ export default function createPayloadHandler(dispatch, serverActionQueue, subscr
21
20
  let checkInterval
22
21
 
23
22
  function getPayload() {
24
- console.log({ getPayload: model, subscription })
25
- subscription.send({ getPayload: { model, config } })
23
+ setTimeout(() => transportAdapter.getPayload(config), 1000)
26
24
  }
27
25
 
28
26
  function camelizeKeys(item) {
@@ -31,87 +29,78 @@ export default function createPayloadHandler(dispatch, serverActionQueue, subscr
31
29
 
32
30
  const tGetPayload = _.throttle(getPayload, 10000)
33
31
 
34
- function dispatchPayload() {
35
- // We want to avoid updates from server overwriting changes to local state, so if there is a queue then wait.
36
- if (!serverActionQueue.fullySynced()) {
37
- console.log(serverActionQueue.getData())
38
- setTimeout(dispatchPayload, 100)
39
- return
40
- }
41
-
42
- const includeModels = (config.includeModels || []).map(m => _.camelCase(m))
43
-
44
- console.log("Dispatching", { payload, includeModels })
45
-
46
- includeModels.forEach(m => {
47
- const subPayload = _.flatten(_.compact(camelizeKeys(payload).map(instance => instance[m])))
48
- const previousSubPayload = _.flatten(_.compact(camelizeKeys(previousPayload).map(instance => instance[m])))
49
-
50
- // Find IDs that were in the payload but are no longer
51
- const idsToRemove = _.difference(previousSubPayload.map(i => i.id), subPayload.map(i => i.id))
52
-
53
- dispatch({ type: `${pluralize(m)}/upsertMany`, payload: subPayload })
54
- dispatch({ type: `${pluralize(m)}/removeMany`, payload: idsToRemove })
55
- })
56
-
57
- const idsToRemove = _.difference(previousPayload.map(i => i.id), payload.map(i => i.id))
58
-
59
- dispatch({ type: `${pluralize(model)}/upsertMany`, payload: camelizeKeys(payload) })
60
- dispatch({ type: `${pluralize(model)}/removeMany`, payload: idsToRemove })
61
- previousPayload = payload
62
- }
63
-
64
- function processQueue() {
65
- console.log({ idx, patchQueue })
32
+ function processQueue(model) {
33
+ console.debug("processQueue", model, idx[model], patchQueue[model])
66
34
  lastCheckAt = new Date()
67
- if (patchQueue[idx]) {
68
- payload = apply_patch(payload, patchQueue[idx])
69
- if (patchQueue[idx]) {
70
- dispatchPayload()
35
+ if (patchQueue[model][idx[model]]) {
36
+ if (!serverActionQueue.fullySynced()) {
37
+ console.debug(serverActionQueue.getData())
38
+ setTimeout(() => processQueue(model), 100)
39
+ return
40
+ }
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 }})
71
54
  }
72
- delete patchQueue[idx]
73
- idx++
55
+
56
+ delete patchQueue[model][idx[model]]
57
+ idx[model]++
74
58
  updateDeadline = null
75
- processQueue()
59
+ processQueue(model)
76
60
  // If there are updates in the queue that are ahead of the index, some have arrived out of order
77
61
  // Set a deadline for new updates before it declares the update missing and refetches.
78
- } else if (_.keys(patchQueue).length > 0 && !updateDeadline) {
62
+ } else if (_.keys(patchQueue[model]).length > 0 && !updateDeadline) {
79
63
  var t = new Date()
80
64
  t.setSeconds(t.getSeconds() + 3)
81
65
  updateDeadline = t
82
- setTimeout(processQueue, 3100)
66
+ setTimeout(() => processQueue(model), 3100)
83
67
  // If more than 10 updates in queue, or deadline has passed, restart
84
- } 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)) {
85
69
  tGetPayload()
86
70
  updateDeadline = null
87
71
  }
88
72
  }
89
73
 
90
74
  function handlePayload(data) {
91
- const { value, idx: newIdx, diff, latency, type } = data
92
- console.log({ data })
75
+ const { idx: newIdx, model: snake_model, type } = data
76
+ const model = _.camelCase(snake_model)
93
77
 
94
- if (type === 'payload') {
95
- if (!value) return null;
78
+ idx[model] = idx[model] || 0
79
+ patchQueue[model] = patchQueue[model] || {}
96
80
 
97
- payload = value
98
- dispatchPayload()
99
- idx = newIdx + 1
81
+ if (type === 'payload') {
82
+ idx[model] = newIdx
100
83
  // Clear any old changes left in the queue
101
- patchQueue= _.pick(patchQueue, _.keys(patchQueue).filter(k => k > newIdx + 1))
102
- return
84
+ patchQueue[model] = _.pick(patchQueue[model], _.keys(patchQueue[model]).filter(k => k > newIdx + 1))
103
85
  }
104
86
 
105
- patchQueue[newIdx] = diff
106
-
107
- 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)
108
90
 
109
91
  if (diffSeconds((new Date()), lastCheckAt) >= 3) {
110
92
  lastCheckAt = new Date()
111
- console.log('Interval lost. Pulling from server')
93
+ console.debug('Interval lost. Pulling from server')
112
94
  tGetPayload()
113
95
  }
114
96
  }
115
97
 
116
- 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 }
117
106
  }