jason-rails 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +52 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/client/lib/JasonContext.d.ts +2 -0
  13. data/client/lib/JasonContext.js +5 -0
  14. data/client/lib/JasonProvider.d.ts +7 -0
  15. data/client/lib/JasonProvider.js +109 -0
  16. data/client/lib/actionFactory.d.ts +5 -0
  17. data/client/lib/actionFactory.js +33 -0
  18. data/client/lib/createActions.d.ts +2 -0
  19. data/client/lib/createActions.js +46 -0
  20. data/client/lib/createJasonReducers.d.ts +1 -0
  21. data/client/lib/createJasonReducers.js +36 -0
  22. data/client/lib/createPayloadHandler.d.ts +1 -0
  23. data/client/lib/createPayloadHandler.js +87 -0
  24. data/client/lib/index.d.ts +10 -0
  25. data/client/lib/index.js +12 -0
  26. data/client/lib/makeEager.d.ts +1 -0
  27. data/client/lib/makeEager.js +51 -0
  28. data/client/lib/useAct.d.ts +1 -0
  29. data/client/lib/useAct.js +12 -0
  30. data/client/lib/useSub.d.ts +1 -0
  31. data/client/lib/useSub.js +14 -0
  32. data/client/package.json +27 -0
  33. data/client/src/JasonContext.ts +5 -0
  34. data/client/src/JasonProvider.tsx +108 -0
  35. data/client/src/actionFactory.ts +34 -0
  36. data/client/src/createActions.ts +50 -0
  37. data/client/src/createJasonReducers.ts +34 -0
  38. data/client/src/createPayloadHandler.ts +95 -0
  39. data/client/src/index.ts +7 -0
  40. data/client/src/makeEager.ts +46 -0
  41. data/client/src/useAct.ts +9 -0
  42. data/client/src/useSub.ts +10 -0
  43. data/client/tsconfig.json +15 -0
  44. data/client/yarn.lock +140 -0
  45. data/jason-rails.gemspec +25 -0
  46. data/lib/jason.rb +10 -0
  47. data/lib/jason/api_model.rb +47 -0
  48. data/lib/jason/channel.rb +37 -0
  49. data/lib/jason/publisher.rb +79 -0
  50. data/lib/jason/subscription.rb +172 -0
  51. data/lib/jason/version.rb +3 -0
  52. metadata +96 -0
@@ -0,0 +1 @@
1
+ export default function createJasonReducers(schema: any): any;
@@ -0,0 +1,36 @@
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 toolkit_1 = require("@reduxjs/toolkit");
7
+ const pluralize_1 = __importDefault(require("pluralize"));
8
+ const lodash_1 = __importDefault(require("lodash"));
9
+ function generateSlices(schema) {
10
+ const sliceNames = schema.map(k => pluralize_1.default(k));
11
+ const adapter = toolkit_1.createEntityAdapter();
12
+ return lodash_1.default.fromPairs(lodash_1.default.map(sliceNames, name => {
13
+ return [name, toolkit_1.createSlice({
14
+ name,
15
+ initialState: adapter.getInitialState(),
16
+ reducers: {
17
+ upsert: adapter.upsertOne,
18
+ upsertMany: adapter.upsertMany,
19
+ add: adapter.addOne,
20
+ setAll: adapter.setAll,
21
+ remove: adapter.removeOne,
22
+ movePriority: (s, { payload: { id, priority, parentFilter } }) => {
23
+ // Get IDs and insert our item at the new index
24
+ var affectedIds = lodash_1.default.orderBy(lodash_1.default.filter(lodash_1.default.values(s.entities), parentFilter).filter(e => e.id !== id), 'priority').map(e => e.id);
25
+ affectedIds.splice(priority, 0, id);
26
+ // Apply update
27
+ affectedIds.forEach((id, i) => s.entities[id].priority = i);
28
+ }
29
+ }
30
+ }).reducer];
31
+ }));
32
+ }
33
+ function createJasonReducers(schema) {
34
+ return generateSlices(lodash_1.default.keys(schema));
35
+ }
36
+ exports.default = createJasonReducers;
@@ -0,0 +1 @@
1
+ export default function createPayloadHandler(dispatch: any, subscription: any, model: any, config: any): (data: any) => null | undefined;
@@ -0,0 +1,87 @@
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 jsonpatch_1 = require("jsonpatch");
7
+ const humps_1 = require("humps");
8
+ const pluralize_1 = __importDefault(require("pluralize"));
9
+ const lodash_1 = __importDefault(require("lodash"));
10
+ function diffSeconds(dt2, dt1) {
11
+ var diff = (dt2.getTime() - dt1.getTime()) / 1000;
12
+ return Math.abs(Math.round(diff));
13
+ }
14
+ function createPayloadHandler(dispatch, subscription, model, config) {
15
+ console.log({ model, config });
16
+ let payload = {};
17
+ let idx = 0;
18
+ let patchQueue = {};
19
+ let lastCheckAt = new Date();
20
+ let updateDeadline = null;
21
+ let checkInterval;
22
+ function getPayload() {
23
+ console.log({ getPayload: model, subscription });
24
+ subscription.send({ getPayload: { model, config } });
25
+ }
26
+ const tGetPayload = lodash_1.default.throttle(getPayload, 10000);
27
+ function dispatchPayload() {
28
+ const includeModels = (config.includeModels || []).map(m => lodash_1.default.camelCase(m));
29
+ console.log("Dispatching", { payload, includeModels });
30
+ includeModels.forEach(m => {
31
+ const subPayload = lodash_1.default.flatten(lodash_1.default.compact(humps_1.camelizeKeys(payload).map(instance => instance[m])));
32
+ console.log({ type: `${pluralize_1.default(m)}/upsertMany`, payload: subPayload });
33
+ dispatch({ type: `${pluralize_1.default(m)}/upsertMany`, payload: subPayload });
34
+ });
35
+ dispatch({ type: `${pluralize_1.default(model)}/upsertMany`, payload: humps_1.camelizeKeys(payload) });
36
+ }
37
+ function processQueue() {
38
+ console.log({ idx, patchQueue });
39
+ lastCheckAt = new Date();
40
+ if (patchQueue[idx]) {
41
+ payload = jsonpatch_1.apply_patch(payload, patchQueue[idx]);
42
+ if (patchQueue[idx]) {
43
+ dispatchPayload();
44
+ }
45
+ delete patchQueue[idx];
46
+ idx++;
47
+ updateDeadline = null;
48
+ processQueue();
49
+ // If there are updates in the queue that are ahead of the index, some have arrived out of order
50
+ // Set a deadline for new updates before it declares the update missing and refetches.
51
+ }
52
+ else if (lodash_1.default.keys(patchQueue).length > 0 && !updateDeadline) {
53
+ var t = new Date();
54
+ t.setSeconds(t.getSeconds() + 3);
55
+ updateDeadline = t;
56
+ setTimeout(processQueue, 3100);
57
+ // If more than 10 updates in queue, or deadline has passed, restart
58
+ }
59
+ else if (lodash_1.default.keys(patchQueue).length > 10 || (updateDeadline && diffSeconds(updateDeadline, new Date()) < 0)) {
60
+ tGetPayload();
61
+ updateDeadline = null;
62
+ }
63
+ }
64
+ function handlePayload(data) {
65
+ const { value, idx: newIdx, diff, latency, type } = data;
66
+ console.log({ data });
67
+ if (type === 'payload') {
68
+ if (!value)
69
+ return null;
70
+ payload = value;
71
+ dispatchPayload();
72
+ idx = newIdx + 1;
73
+ // Clear any old changes left in the queue
74
+ patchQueue = lodash_1.default.pick(patchQueue, lodash_1.default.keys(patchQueue).filter(k => k > newIdx + 1));
75
+ return;
76
+ }
77
+ patchQueue[newIdx] = diff;
78
+ processQueue();
79
+ if (diffSeconds((new Date()), lastCheckAt) >= 3) {
80
+ lastCheckAt = new Date();
81
+ console.log('Interval lost. Pulling from server');
82
+ tGetPayload();
83
+ }
84
+ }
85
+ return handlePayload;
86
+ }
87
+ exports.default = createPayloadHandler;
@@ -0,0 +1,10 @@
1
+ import _useAct from './useAct';
2
+ import _useSub from './useSub';
3
+ export declare const JasonProvider: ({ reducers, middleware, extraActions, children }: {
4
+ reducers: any;
5
+ middleware: any;
6
+ extraActions: any;
7
+ children: any;
8
+ }) => any;
9
+ export declare const useAct: typeof _useAct;
10
+ export declare const useSub: typeof _useSub;
@@ -0,0 +1,12 @@
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
+ exports.useSub = exports.useAct = exports.JasonProvider = void 0;
7
+ const JasonProvider_1 = __importDefault(require("./JasonProvider"));
8
+ const useAct_1 = __importDefault(require("./useAct"));
9
+ const useSub_1 = __importDefault(require("./useSub"));
10
+ exports.JasonProvider = JasonProvider_1.default;
11
+ exports.useAct = useAct_1.default;
12
+ exports.useSub = useSub_1.default;
@@ -0,0 +1 @@
1
+ export default function (schema: any): (entity: any, id?: null, relations?: never[]) => any;
@@ -0,0 +1,51 @@
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 pluralize_1 = __importDefault(require("pluralize"));
7
+ const lodash_1 = __importDefault(require("lodash"));
8
+ const react_redux_1 = require("react-redux");
9
+ function default_1(schema) {
10
+ function addRelations(s, objects, objectType, relations) {
11
+ // first find out relation name
12
+ if (lodash_1.default.isArray(relations)) {
13
+ relations.forEach(relation => {
14
+ objects = addRelations(s, objects, objectType, relation);
15
+ });
16
+ }
17
+ else if (typeof (relations) === 'object') {
18
+ const relation = Object.keys(relations)[0];
19
+ const subRelations = relations[relation];
20
+ objects = addRelations(s, objects, objectType, relation);
21
+ objects[relation] = addRelations(s, objects[relation], pluralize_1.default(relation), subRelations);
22
+ // #
23
+ }
24
+ else if (typeof (relations) === 'string') {
25
+ const relation = relations;
26
+ if (lodash_1.default.isArray(objects)) {
27
+ objects = objects.map(obj => addRelations(s, obj, objectType, relation));
28
+ }
29
+ else {
30
+ const relatedObjects = lodash_1.default.values(s[pluralize_1.default(relation)].entities);
31
+ if (pluralize_1.default.isSingular(relation)) {
32
+ objects = Object.assign(Object.assign({}, objects), { [relation]: lodash_1.default.find(relatedObjects, { id: objects[relation + 'Id'] }) });
33
+ }
34
+ else {
35
+ objects = Object.assign(Object.assign({}, objects), { [relation]: relatedObjects.filter(e => e[pluralize_1.default.singular(objectType) + 'Id'] === objects.id) });
36
+ }
37
+ }
38
+ }
39
+ return objects;
40
+ }
41
+ function useEager(entity, id = null, relations = []) {
42
+ if (id) {
43
+ return react_redux_1.useSelector(s => addRelations(s, Object.assign({}, s[entity].entities[String(id)]), entity, relations));
44
+ }
45
+ else {
46
+ return react_redux_1.useSelector(s => addRelations(s, lodash_1.default.values(s[entity].entities), entity, relations));
47
+ }
48
+ }
49
+ return useEager;
50
+ }
51
+ exports.default = default_1;
@@ -0,0 +1 @@
1
+ export default function useAct(): any;
@@ -0,0 +1,12 @@
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 JasonContext_1 = __importDefault(require("./JasonContext"));
7
+ const react_1 = require("react");
8
+ function useAct() {
9
+ const { actions } = react_1.useContext(JasonContext_1.default);
10
+ return actions;
11
+ }
12
+ exports.default = useAct;
@@ -0,0 +1 @@
1
+ export default function useSub(config: any): void;
@@ -0,0 +1,14 @@
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 JasonContext_1 = __importDefault(require("./JasonContext"));
7
+ const react_1 = require("react");
8
+ function useSub(config) {
9
+ const subscribe = react_1.useContext(JasonContext_1.default).subscribe;
10
+ react_1.useEffect(() => {
11
+ return subscribe(config);
12
+ }, []);
13
+ }
14
+ exports.default = useSub;
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@jamesr2323/jason",
3
+ "version": "0.3.0",
4
+ "module": "./lib/index.js",
5
+ "scripts": {
6
+ "build": "tsc"
7
+ },
8
+ "dependencies": {
9
+ "@rails/actioncable": "^6.0.3-4",
10
+ "axios": "^0.21.0",
11
+ "axios-case-converter": "^0.6.0",
12
+ "blueimp-md5": "^2.18.0",
13
+ "humps": "^2.0.1",
14
+ "jsonpatch": "^3.0.1",
15
+ "lodash": "^4.17.20",
16
+ "pluralize": "^8.0.0",
17
+ "uuid": "^8.3.1"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "^4.1.2"
21
+ },
22
+ "peerDependencies": {
23
+ "react": "^16.8.3",
24
+ "react-redux": "^7.2.2",
25
+ "@reduxjs/toolkit": "^1.5.0"
26
+ }
27
+ }
@@ -0,0 +1,5 @@
1
+ import { createContext } from 'react'
2
+
3
+ const context = createContext({ actions: {} as any, subscribe: null, eager: null })
4
+
5
+ export default context
@@ -0,0 +1,108 @@
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'
6
+ 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 makeEager from './makeEager'
11
+ import { camelizeKeys } from 'humps'
12
+ import md5 from 'blueimp-md5'
13
+ import _ from 'lodash'
14
+ import React, { useState, useEffect } from 'react'
15
+
16
+ const JasonProvider = ({ reducers, middleware, extraActions, children }) => {
17
+ const [store, setStore] = useState(null)
18
+ const [value, setValue] = useState(null)
19
+ const [connected, setConnected] = useState(false)
20
+
21
+ const csrfToken = (document.querySelector("meta[name=csrf-token]") as any).content
22
+ axios.defaults.headers.common['X-CSRF-Token'] = csrfToken
23
+ const restClient = applyCaseMiddleware(axios.create())
24
+
25
+ useEffect(() => {
26
+ restClient.get('/jason/api/schema')
27
+ .then(({ data: snakey_schema }) => {
28
+ const schema = camelizeKeys(snakey_schema)
29
+
30
+ const consumer = createConsumer()
31
+ const allReducers = {
32
+ ...reducers,
33
+ ...createJasonReducers(schema)
34
+ }
35
+
36
+ console.log({ schema, allReducers })
37
+ const store = configureStore({ reducer: allReducers, middleware })
38
+
39
+ let payloadHandlers = {}
40
+ function handlePayload(payload) {
41
+ const { model, md5Hash } = payload
42
+ console.log({ md5Hash, fn: `${model}:${md5Hash}`, payloadHandlers, model: _.camelCase(model), payload })
43
+ const handler = payloadHandlers[`${_.camelCase(model)}:${md5Hash}`]
44
+ if (handler) {
45
+ handler({ ...payload, model: _.camelCase(model) })
46
+ }
47
+ }
48
+
49
+ const subscription = (consumer.subscriptions.create({
50
+ channel: 'Jason::Channel'
51
+ }, {
52
+ connected: () => {
53
+ setConnected(true)
54
+ },
55
+ received: payload => {
56
+ console.log("Payload received", payload)
57
+ handlePayload(payload)
58
+ },
59
+ disconnected: () => console.warn('Disconnected from ActionCable')
60
+ }));
61
+
62
+ console.log('sending message')
63
+ subscription.send({ message: 'test' })
64
+
65
+ function createSubscription(config) {
66
+ const md5Hash = md5(JSON.stringify(config))
67
+ console.log('Subscribe with', config, md5Hash)
68
+
69
+ _.map(config, (v, model) => {
70
+ payloadHandlers[`${model}:${md5Hash}`] = createPayloadHandler(store.dispatch, subscription, model, schema[model])
71
+ })
72
+ subscription.send({ createSubscription: config })
73
+
74
+ return () => removeSubscription(config)
75
+ }
76
+
77
+ function removeSubscription(config) {
78
+ subscription.send({ removeSubscription: config })
79
+ const md5Hash = md5(JSON.stringify(config))
80
+ _.map(config, (v, model) => {
81
+ delete payloadHandlers[`${model}:${md5Hash}`]
82
+ })
83
+ }
84
+
85
+ const actions = createActions(schema, store, restClient, extraActions)
86
+ const eager = makeEager(schema)
87
+
88
+ console.log({ actions })
89
+
90
+ setValue({
91
+ actions: actions,
92
+ subscribe: (config) => createSubscription(config),
93
+ eager
94
+ })
95
+ setStore(store)
96
+ })
97
+ }, [])
98
+
99
+ if(!(store && value && connected)) return <div /> // Wait for async fetch of schema to complete
100
+
101
+ return <Provider store={store}>
102
+ <JasonContext.Provider value={value}>{ children }</JasonContext.Provider>
103
+ </Provider>
104
+ }
105
+
106
+ export default JasonProvider
107
+
108
+
@@ -0,0 +1,34 @@
1
+ import pluralize from 'pluralize'
2
+ import _ from 'lodash'
3
+ import { v4 as uuidv4 } from 'uuid'
4
+
5
+ export default (dis, store, entity, { extraActions = {}, hasPriority = false } = {}) => {
6
+ function add(data = {}) {
7
+ const id = uuidv4()
8
+ return dis({ type: `${pluralize(entity)}/add`, payload: { id, ...data } })
9
+ }
10
+
11
+ function upsert(id, data) {
12
+ return dis({ type: `${pluralize(entity)}/upsert`, payload: { id, ...data } })
13
+ }
14
+
15
+ function movePriority(id, priority, parentFilter = {}) {
16
+ return dis({ type: `${pluralize(entity)}/movePriority`, payload: { id, priority, parentFilter } })
17
+ }
18
+
19
+ function setAll(data) {
20
+ return dis({ type: `${pluralize(entity)}/setAll`, payload: data })
21
+ }
22
+
23
+ function remove(id) {
24
+ return dis({ type: `${pluralize(entity)}/remove`, payload: id })
25
+ }
26
+
27
+ const extraActionsResolved = _.mapValues(extraActions, v => v(dis, store, entity))
28
+
29
+ if (hasPriority) {
30
+ return { add, upsert, setAll, remove, movePriority, ...extraActionsResolved }
31
+ } else {
32
+ return { add, upsert, setAll, remove, ...extraActionsResolved }
33
+ }
34
+ }
@@ -0,0 +1,50 @@
1
+ import actionFactory from './actionFactory'
2
+ import pluralize from 'pluralize'
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
+
37
+ const actions = _.fromPairs(_.map(schema, (config, model: string) => {
38
+ if (config.priorityScope) {
39
+ return [pluralize(model), actionFactory(optDis, store, model, { hasPriority: true })]
40
+ } else {
41
+ return [pluralize(model), actionFactory(optDis, store, model)]
42
+ }
43
+ }))
44
+
45
+ const extraActionsResolved = extraActions(optDis, store, restClient)
46
+
47
+ return _.merge(actions, extraActionsResolved)
48
+ }
49
+
50
+ export default createActions