jason-rails 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +34 -0
  3. data/README.md +6 -9
  4. data/app/assets/config/jason_engine_manifest.js +1 -0
  5. data/app/assets/images/jason/engine/.keep +0 -0
  6. data/app/assets/stylesheets/jason/engine/application.css +15 -0
  7. data/app/controllers/jason/api_controller.rb +36 -0
  8. data/app/helpers/jason/engine/application_helper.rb +6 -0
  9. data/app/jobs/jason/engine/application_job.rb +6 -0
  10. data/app/mailers/jason/engine/application_mailer.rb +8 -0
  11. data/app/models/jason/engine/application_record.rb +7 -0
  12. data/app/views/layouts/jason/engine/application.html.erb +15 -0
  13. data/client/babel.config.js +13 -0
  14. data/client/lib/JasonProvider.d.ts +5 -4
  15. data/client/lib/JasonProvider.js +30 -3
  16. data/client/lib/actionFactory.js +1 -1
  17. data/client/lib/createActions.d.ts +1 -1
  18. data/client/lib/createActions.js +2 -27
  19. data/client/lib/createJasonReducers.js +1 -0
  20. data/client/lib/createOptDis.d.ts +1 -0
  21. data/client/lib/createOptDis.js +45 -0
  22. data/client/lib/createPayloadHandler.d.ts +1 -1
  23. data/client/lib/createPayloadHandler.js +23 -6
  24. data/client/lib/deepCamelizeKeys.d.ts +1 -0
  25. data/client/lib/deepCamelizeKeys.js +23 -0
  26. data/client/lib/deepCamelizeKeys.test.d.ts +1 -0
  27. data/client/lib/deepCamelizeKeys.test.js +106 -0
  28. data/client/lib/index.d.ts +4 -4
  29. data/client/package.json +17 -4
  30. data/client/src/JasonProvider.tsx +33 -5
  31. data/client/src/actionFactory.ts +1 -1
  32. data/client/src/createActions.ts +2 -33
  33. data/client/src/createJasonReducers.ts +1 -0
  34. data/client/src/createOptDis.ts +47 -0
  35. data/client/src/createPayloadHandler.ts +26 -4
  36. data/client/src/deepCamelizeKeys.test.ts +113 -0
  37. data/client/src/deepCamelizeKeys.ts +17 -0
  38. data/client/yarn.lock +4539 -81
  39. data/config/routes.rb +4 -0
  40. data/jason-rails.gemspec +5 -0
  41. data/lib/jason.rb +7 -1
  42. data/lib/jason/api_model.rb +16 -0
  43. data/lib/jason/channel.rb +2 -2
  44. data/lib/jason/engine.rb +5 -0
  45. data/lib/jason/publisher.rb +38 -6
  46. data/lib/jason/subscription.rb +21 -24
  47. data/lib/jason/version.rb +1 -1
  48. metadata +81 -3
@@ -0,0 +1 @@
1
+ export default function deepCamelizeKeys(item: any, excludeIf?: (k: any) => boolean): any;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const lodash_1 = __importDefault(require("lodash"));
7
+ function deepCamelizeKeys(item, excludeIf = k => false) {
8
+ function camelizeKey(key) {
9
+ if (excludeIf(key))
10
+ return key;
11
+ return lodash_1.default.camelCase(key);
12
+ }
13
+ if (lodash_1.default.isArray(item)) {
14
+ return lodash_1.default.map(item, item => deepCamelizeKeys(item, excludeIf));
15
+ }
16
+ else if (lodash_1.default.isObject(item)) {
17
+ return lodash_1.default.mapValues(lodash_1.default.mapKeys(item, (v, k) => camelizeKey(k)), (v, k) => deepCamelizeKeys(v, excludeIf));
18
+ }
19
+ else {
20
+ return item;
21
+ }
22
+ }
23
+ exports.default = deepCamelizeKeys;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const deepCamelizeKeys_1 = __importDefault(require("./deepCamelizeKeys"));
7
+ test('scalar number', () => {
8
+ expect(deepCamelizeKeys_1.default(1)).toBe(1);
9
+ });
10
+ test('scalar number float', () => {
11
+ expect(deepCamelizeKeys_1.default(1.123)).toBe(1.123);
12
+ });
13
+ test('scalar string', () => {
14
+ expect(deepCamelizeKeys_1.default('test')).toBe('test');
15
+ });
16
+ test('scalar null', () => {
17
+ expect(deepCamelizeKeys_1.default(null)).toBe(null);
18
+ });
19
+ test('scalar boolean', () => {
20
+ expect(deepCamelizeKeys_1.default(true)).toBe(true);
21
+ });
22
+ test('object with existing camelized keys', () => {
23
+ expect(deepCamelizeKeys_1.default({ testMe: 'test' })).toStrictEqual({ testMe: 'test' });
24
+ });
25
+ test('array with existing camelized keys', () => {
26
+ expect(deepCamelizeKeys_1.default([{ testMe: 'test' }, { testMe2: 'test' }])).toStrictEqual([{ testMe: 'test' }, { testMe2: 'test' }]);
27
+ });
28
+ test('object with mixed keys', () => {
29
+ expect(deepCamelizeKeys_1.default({ testMe: 'test', test_2: 'dog', test_me2: true })).toStrictEqual({ testMe: 'test', test2: 'dog', testMe2: true });
30
+ });
31
+ test('array with mixed keys', () => {
32
+ expect(deepCamelizeKeys_1.default([
33
+ { testMe: 'test', test_2: 'dog', test_me2: true },
34
+ { testMe3: 'test', test_3: 'dog', test_me4: true }
35
+ ])).toStrictEqual([
36
+ { testMe: 'test', test2: 'dog', testMe2: true },
37
+ { testMe3: 'test', test3: 'dog', testMe4: true }
38
+ ]);
39
+ });
40
+ test('nested with object at top level', () => {
41
+ expect(deepCamelizeKeys_1.default({
42
+ test_me: {
43
+ test_me2: {
44
+ test_me3: [
45
+ { test_it_out: '49' },
46
+ { test_fun: 'what' }
47
+ ]
48
+ }
49
+ }
50
+ })).toStrictEqual({
51
+ testMe: {
52
+ testMe2: {
53
+ testMe3: [
54
+ { testItOut: '49' },
55
+ { testFun: 'what' }
56
+ ]
57
+ }
58
+ }
59
+ });
60
+ });
61
+ test('nested with object at top level', () => {
62
+ expect(deepCamelizeKeys_1.default([{
63
+ test_me: {
64
+ test_me2: {
65
+ test_me3: [
66
+ { test_it_out: '49' },
67
+ { test_fun: 'what' }
68
+ ]
69
+ }
70
+ }
71
+ }, {
72
+ test_it52: 'what?'
73
+ }])).toStrictEqual([{
74
+ testMe: {
75
+ testMe2: {
76
+ testMe3: [
77
+ { testItOut: '49' },
78
+ { testFun: 'what' }
79
+ ]
80
+ }
81
+ }
82
+ }, {
83
+ testIt52: 'what?'
84
+ }]);
85
+ });
86
+ test('excludes keys by function', () => {
87
+ expect(deepCamelizeKeys_1.default({
88
+ test_me: {
89
+ test_me2: {
90
+ test_me3: [
91
+ { test_it_out: '49' },
92
+ { test_fun: 'what' }
93
+ ]
94
+ }
95
+ }
96
+ }, k => (k === 'test_me2'))).toStrictEqual({
97
+ testMe: {
98
+ test_me2: {
99
+ testMe3: [
100
+ { testItOut: '49' },
101
+ { testFun: 'what' }
102
+ ]
103
+ }
104
+ }
105
+ });
106
+ });
@@ -1,10 +1,10 @@
1
1
  import _useAct from './useAct';
2
2
  import _useSub from './useSub';
3
3
  export declare const JasonProvider: ({ reducers, middleware, extraActions, children }: {
4
- reducers: any;
5
- middleware: any;
6
- extraActions: any;
7
- children: any;
4
+ reducers?: any;
5
+ middleware?: any;
6
+ extraActions?: any;
7
+ children?: any;
8
8
  }) => any;
9
9
  export declare const useAct: typeof _useAct;
10
10
  export declare const useSub: typeof _useSub;
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@jamesr2323/jason",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "module": "./lib/index.js",
5
+ "types": "./lib/index.d.ts",
5
6
  "scripts": {
6
- "build": "tsc"
7
+ "build": "tsc",
8
+ "test": "jest"
7
9
  },
8
10
  "dependencies": {
9
11
  "@rails/actioncable": "^6.0.3-4",
@@ -17,11 +19,22 @@
17
19
  "uuid": "^8.3.1"
18
20
  },
19
21
  "devDependencies": {
22
+ "@babel/core": "^7.12.10",
23
+ "@babel/preset-env": "^7.12.11",
24
+ "@babel/preset-typescript": "^7.12.7",
25
+ "@reduxjs/toolkit": "^1.5.0",
26
+ "@types/jest": "^26.0.19",
27
+ "babel-jest": "^26.6.3",
28
+ "jest": "^26.6.3",
29
+ "react": "^16.8.3",
30
+ "react-dom": "^16.8.3",
31
+ "react-redux": "^7.2.2",
20
32
  "typescript": "^4.1.2"
21
33
  },
22
34
  "peerDependencies": {
35
+ "@reduxjs/toolkit": "^1.5.0",
23
36
  "react": "^16.8.3",
24
- "react-redux": "^7.2.2",
25
- "@reduxjs/toolkit": "^1.5.0"
37
+ "react-dom": "^16.8.3",
38
+ "react-redux": "^7.2.2"
26
39
  }
27
40
  }
@@ -7,26 +7,54 @@ import { Provider } from 'react-redux'
7
7
  import { createEntityAdapter, createSlice, createReducer, configureStore } from '@reduxjs/toolkit'
8
8
  import createJasonReducers from './createJasonReducers'
9
9
  import createPayloadHandler from './createPayloadHandler'
10
+ import createOptDis from './createOptDis'
10
11
  import makeEager from './makeEager'
11
12
  import { camelizeKeys } from 'humps'
12
13
  import md5 from 'blueimp-md5'
13
14
  import _ from 'lodash'
14
15
  import React, { useState, useEffect } from 'react'
16
+ import { validate as isUuid } from 'uuid'
15
17
 
16
- const JasonProvider = ({ reducers, middleware, extraActions, children }) => {
18
+ const JasonProvider = ({ reducers, middleware, extraActions, children }: { reducers?: any, middleware?: any, extraActions?: any, children?: React.FC }) => {
17
19
  const [store, setStore] = useState(null)
18
20
  const [value, setValue] = useState(null)
19
21
  const [connected, setConnected] = useState(false)
20
22
 
21
23
  const csrfToken = (document.querySelector("meta[name=csrf-token]") as any).content
22
24
  axios.defaults.headers.common['X-CSRF-Token'] = csrfToken
23
- const restClient = applyCaseMiddleware(axios.create())
25
+ const restClient = applyCaseMiddleware(axios.create(), {
26
+ preservedKeys: (key) => {
27
+ return isUuid(key)
28
+ }
29
+ })
24
30
 
25
31
  useEffect(() => {
26
32
  restClient.get('/jason/api/schema')
27
33
  .then(({ data: snakey_schema }) => {
28
34
  const schema = camelizeKeys(snakey_schema)
29
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
+
30
58
  const consumer = createConsumer()
31
59
  const allReducers = {
32
60
  ...reducers,
@@ -67,7 +95,7 @@ const JasonProvider = ({ reducers, middleware, extraActions, children }) => {
67
95
  console.log('Subscribe with', config, md5Hash)
68
96
 
69
97
  _.map(config, (v, model) => {
70
- payloadHandlers[`${model}:${md5Hash}`] = createPayloadHandler(store.dispatch, subscription, model, schema[model])
98
+ payloadHandlers[`${model}:${md5Hash}`] = createPayloadHandler(store.dispatch, serverActionQueue, subscription, model, schema[model])
71
99
  })
72
100
  subscription.send({ createSubscription: config })
73
101
 
@@ -81,8 +109,8 @@ const JasonProvider = ({ reducers, middleware, extraActions, children }) => {
81
109
  delete payloadHandlers[`${model}:${md5Hash}`]
82
110
  })
83
111
  }
84
-
85
- const actions = createActions(schema, store, restClient, extraActions)
112
+ const optDis = createOptDis(schema, store.dispatch, restClient, serverActionQueue)
113
+ const actions = createActions(schema, store, restClient, optDis, extraActions)
86
114
  const eager = makeEager(schema)
87
115
 
88
116
  console.log({ actions })
@@ -24,7 +24,7 @@ export default (dis, store, entity, { extraActions = {}, hasPriority = false } =
24
24
  return dis({ type: `${pluralize(entity)}/remove`, payload: id })
25
25
  }
26
26
 
27
- const extraActionsResolved = _.mapValues(extraActions, v => v(dis, store, entity))
27
+ const extraActionsResolved = extraActions ? _.mapValues(extraActions, v => v(dis, store, entity)) : {}
28
28
 
29
29
  if (hasPriority) {
30
30
  return { add, upsert, setAll, remove, movePriority, ...extraActionsResolved }
@@ -1,39 +1,8 @@
1
1
  import actionFactory from './actionFactory'
2
2
  import pluralize from 'pluralize'
3
3
  import _ from 'lodash'
4
- import { v4 as uuidv4 } from 'uuid'
5
-
6
- function enrich(type, payload) {
7
- if (type.split('/')[1] === 'upsert' && !(type.split('/')[0] === 'session')) {
8
- if (!payload.id) {
9
- return { ...payload, id: uuidv4() }
10
- }
11
- }
12
- return payload
13
- }
14
-
15
- function makeOptDis(schema, dispatch, restClient) {
16
- const plurals = _.keys(schema).map(k => pluralize(k))
17
-
18
- return function (action) {
19
- const { type, payload } = action
20
- const data = enrich(type, payload)
21
-
22
- dispatch(action)
23
-
24
- if (plurals.indexOf(type.split('/')[0]) > -1) {
25
- return restClient.post('/jason/api/action', { type, payload: data } )
26
- .catch(e => {
27
- dispatch({ type: 'upsertLocalUi', data: { error: JSON.stringify(e) } })
28
- })
29
- }
30
- }
31
- }
32
-
33
- function createActions(schema, store, restClient, extraActions) {
34
- const dis = store.dispatch;
35
- const optDis = makeOptDis(schema, dis, restClient)
36
4
 
5
+ function createActions(schema, store, restClient, optDis, extraActions) {
37
6
  const actions = _.fromPairs(_.map(schema, (config, model: string) => {
38
7
  if (config.priorityScope) {
39
8
  return [pluralize(model), actionFactory(optDis, store, model, { hasPriority: true })]
@@ -42,7 +11,7 @@ function createActions(schema, store, restClient, extraActions) {
42
11
  }
43
12
  }))
44
13
 
45
- const extraActionsResolved = extraActions(optDis, store, restClient)
14
+ const extraActionsResolved = extraActions ? extraActions(optDis, store, restClient, actions) : {}
46
15
 
47
16
  return _.merge(actions, extraActionsResolved)
48
17
  }
@@ -16,6 +16,7 @@ function generateSlices(schema) {
16
16
  add: adapter.addOne,
17
17
  setAll: adapter.setAll,
18
18
  remove: adapter.removeOne,
19
+ removeMany: adapter.removeMany,
19
20
  movePriority: (s, { payload: { id, priority, parentFilter } }) => {
20
21
  // Get IDs and insert our item at the new index
21
22
  var affectedIds = _.orderBy(_.filter(_.values(s.entities), parentFilter).filter(e => e.id !== id), 'priority').map(e => e.id)
@@ -0,0 +1,47 @@
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
+ let inFlight = false
17
+
18
+ function enqueueServerAction (action) {
19
+ serverActionQueue.addItem(action)
20
+ }
21
+
22
+ function dispatchServerAction() {
23
+ const action = serverActionQueue.getItem()
24
+ if (!action) return
25
+
26
+ inFlight = true
27
+ restClient.post('/jason/api/action', action)
28
+ .then(serverActionQueue.itemProcessed)
29
+ .catch(e => {
30
+ dispatch({ type: 'upsertLocalUi', data: { error: JSON.stringify(e) } })
31
+ serverActionQueue.itemProcessed()
32
+ })
33
+ }
34
+
35
+ setInterval(dispatchServerAction, 10)
36
+
37
+ return function (action) {
38
+ const { type, payload } = action
39
+ const data = enrich(type, payload)
40
+
41
+ dispatch({ type, payload: data })
42
+
43
+ if (plurals.indexOf(type.split('/')[0]) > -1) {
44
+ enqueueServerAction({ type, payload: data })
45
+ }
46
+ }
47
+ }
@@ -1,16 +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 } 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
+ export default function createPayloadHandler(dispatch, serverActionQueue, subscription, model, config) {
12
13
  console.log({ model, config })
13
- let payload = {}
14
+ let payload = [] as any[]
15
+ let previousPayload = [] as any[]
14
16
  let idx = 0
15
17
  let patchQueue = {}
16
18
 
@@ -23,20 +25,40 @@ export default function createPayloadHandler(dispatch, subscription, model, conf
23
25
  subscription.send({ getPayload: { model, config } })
24
26
  }
25
27
 
28
+ function camelizeKeys(item) {
29
+ return deepCamelizeKeys(item, key => isUuid(key))
30
+ }
31
+
26
32
  const tGetPayload = _.throttle(getPayload, 10000)
27
33
 
28
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
+
29
42
  const includeModels = (config.includeModels || []).map(m => _.camelCase(m))
30
43
 
31
44
  console.log("Dispatching", { payload, includeModels })
32
45
 
33
46
  includeModels.forEach(m => {
34
47
  const subPayload = _.flatten(_.compact(camelizeKeys(payload).map(instance => instance[m])))
35
- console.log({ type: `${pluralize(m)}/upsertMany`, payload: subPayload })
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
+
36
53
  dispatch({ type: `${pluralize(m)}/upsertMany`, payload: subPayload })
54
+ dispatch({ type: `${pluralize(m)}/removeMany`, payload: idsToRemove })
37
55
  })
38
56
 
57
+ const idsToRemove = _.difference(previousPayload.map(i => i.id), payload.map(i => i.id))
58
+
39
59
  dispatch({ type: `${pluralize(model)}/upsertMany`, payload: camelizeKeys(payload) })
60
+ dispatch({ type: `${pluralize(model)}/removeMany`, payload: idsToRemove })
61
+ previousPayload = payload
40
62
  }
41
63
 
42
64
  function processQueue() {