jason-rails 0.3.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 (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