jason-rails 0.6.7 → 0.7.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/Gemfile.lock +6 -4
- data/README.md +16 -14
- data/app/controllers/jason/jason_controller.rb +26 -4
- data/app/workers/jason/outbound_message_queue_worker.rb +1 -1
- data/client/lib/JasonProvider.d.ts +2 -1
- data/client/lib/JasonProvider.js +2 -2
- data/client/lib/addRelations.d.ts +1 -0
- data/client/lib/addRelations.js +39 -0
- data/client/lib/createJasonReducers.js +4 -2
- data/client/lib/createOptDis.d.ts +1 -1
- data/client/lib/createOptDis.js +9 -8
- data/client/lib/createServerActionQueue.d.ts +3 -2
- data/client/lib/createServerActionQueue.js +32 -6
- data/client/lib/createServerActionQueue.test.js +61 -6
- data/client/lib/createThenable.d.ts +1 -0
- data/client/lib/createThenable.js +5 -0
- data/client/lib/index.d.ts +7 -1
- data/client/lib/index.js +3 -1
- data/client/lib/transportAdapters/actionCableAdapter.js +24 -4
- data/client/lib/transportAdapters/pusherAdapter.js +1 -1
- data/client/lib/useDraft.d.ts +1 -0
- data/client/lib/useDraft.js +13 -0
- data/client/lib/useEager.d.ts +1 -1
- data/client/lib/useEager.js +10 -5
- data/client/lib/useJason.d.ts +2 -1
- data/client/lib/useJason.js +4 -6
- data/client/package.json +1 -1
- data/client/src/JasonProvider.tsx +2 -2
- data/client/src/addRelations.ts +33 -0
- data/client/src/createJasonReducers.ts +4 -2
- data/client/src/createOptDis.ts +10 -8
- data/client/src/createServerActionQueue.test.ts +60 -6
- data/client/src/createServerActionQueue.ts +41 -6
- data/client/src/index.ts +2 -0
- data/client/src/transportAdapters/actionCableAdapter.ts +24 -5
- data/client/src/transportAdapters/pusherAdapter.ts +1 -2
- data/client/src/useDraft.ts +17 -0
- data/client/src/useEager.ts +9 -6
- data/client/src/useJason.ts +3 -6
- data/lib/jason.rb +4 -1
- data/lib/jason/api_model.rb +0 -4
- data/lib/jason/channel.rb +0 -7
- data/lib/jason/conditions_matcher.rb +88 -0
- data/lib/jason/consistency_checker.rb +65 -0
- data/lib/jason/graph_helper.rb +4 -0
- data/lib/jason/publisher.rb +39 -37
- data/lib/jason/subscription.rb +63 -18
- data/lib/jason/version.rb +1 -1
- metadata +12 -5
- data/client/src/makeEager.ts +0 -46
- data/lib/jason/publisher_old.rb +0 -112
- data/lib/jason/subscription_old.rb +0 -171
@@ -0,0 +1 @@
|
|
1
|
+
export default function createThenable(): void;
|
data/client/lib/index.d.ts
CHANGED
@@ -2,9 +2,15 @@
|
|
2
2
|
import _useAct from './useAct';
|
3
3
|
import _useSub from './useSub';
|
4
4
|
import _useEager from './useEager';
|
5
|
-
export declare const
|
5
|
+
export declare const JasonContext: import("react").Context<{
|
6
|
+
actions: any;
|
7
|
+
subscribe: null;
|
8
|
+
eager: (entity: any, id: any, relations: any) => void;
|
9
|
+
}>;
|
10
|
+
export declare const JasonProvider: ({ reducers, middleware, enhancers, extraActions, children }: {
|
6
11
|
reducers?: any;
|
7
12
|
middleware?: any;
|
13
|
+
enhancers?: any;
|
8
14
|
extraActions?: any;
|
9
15
|
children?: import("react").FC<{}> | undefined;
|
10
16
|
}) => JSX.Element;
|
data/client/lib/index.js
CHANGED
@@ -3,11 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
-
exports.useEager = exports.useSub = exports.useAct = exports.JasonProvider = void 0;
|
6
|
+
exports.useEager = exports.useSub = exports.useAct = exports.JasonProvider = exports.JasonContext = void 0;
|
7
|
+
const JasonContext_1 = __importDefault(require("./JasonContext"));
|
7
8
|
const JasonProvider_1 = __importDefault(require("./JasonProvider"));
|
8
9
|
const useAct_1 = __importDefault(require("./useAct"));
|
9
10
|
const useSub_1 = __importDefault(require("./useSub"));
|
10
11
|
const useEager_1 = __importDefault(require("./useEager"));
|
12
|
+
exports.JasonContext = JasonContext_1.default;
|
11
13
|
exports.JasonProvider = JasonProvider_1.default;
|
12
14
|
exports.useAct = useAct_1.default;
|
13
15
|
exports.useSub = useSub_1.default;
|
@@ -1,7 +1,14 @@
|
|
1
1
|
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
6
|
const actioncable_1 = require("@rails/actioncable");
|
7
|
+
const restClient_1 = __importDefault(require("../restClient"));
|
8
|
+
const uuid_1 = require("uuid");
|
9
|
+
const lodash_1 = __importDefault(require("lodash"));
|
4
10
|
function actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnected) {
|
11
|
+
const consumerId = uuid_1.v4();
|
5
12
|
const consumer = actioncable_1.createConsumer();
|
6
13
|
const subscription = (consumer.subscriptions.create({
|
7
14
|
channel: 'Jason::Channel'
|
@@ -21,14 +28,27 @@ function actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnected) {
|
|
21
28
|
console.warn('Disconnected from ActionCable');
|
22
29
|
}
|
23
30
|
}));
|
24
|
-
function getPayload(config, options) {
|
25
|
-
subscription.send(Object.assign({ getPayload: config }, options));
|
26
|
-
}
|
27
31
|
function createSubscription(config) {
|
28
32
|
subscription.send({ createSubscription: config });
|
29
33
|
}
|
30
34
|
function removeSubscription(config) {
|
31
|
-
|
35
|
+
restClient_1.default.post('/jason/api/remove_subscription', { config, consumerId })
|
36
|
+
.catch(e => console.error(e));
|
37
|
+
}
|
38
|
+
function getPayload(config, options) {
|
39
|
+
restClient_1.default.post('/jason/api/get_payload', {
|
40
|
+
config,
|
41
|
+
options
|
42
|
+
})
|
43
|
+
.then(({ data }) => {
|
44
|
+
lodash_1.default.map(data, (payload, modelName) => {
|
45
|
+
handlePayload(payload);
|
46
|
+
});
|
47
|
+
})
|
48
|
+
.catch(e => console.error(e));
|
49
|
+
}
|
50
|
+
function fullChannelName(channelName) {
|
51
|
+
return channelName;
|
32
52
|
}
|
33
53
|
return { getPayload, createSubscription, removeSubscription };
|
34
54
|
}
|
@@ -8,7 +8,7 @@ const restClient_1 = __importDefault(require("../restClient"));
|
|
8
8
|
const uuid_1 = require("uuid");
|
9
9
|
const lodash_1 = __importDefault(require("lodash"));
|
10
10
|
function pusherAdapter(jasonConfig, handlePayload, dispatch) {
|
11
|
-
|
11
|
+
const consumerId = uuid_1.v4();
|
12
12
|
const { pusherKey, pusherRegion, pusherChannelPrefix } = jasonConfig;
|
13
13
|
const pusher = new pusher_js_1.default(pusherKey, {
|
14
14
|
cluster: 'eu',
|
@@ -0,0 +1 @@
|
|
1
|
+
export default function useDraft(entity: any, id: any, relations?: never[]): void;
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
/* Can be called as
|
4
|
+
useDraft() => draft object for making updates
|
5
|
+
useDraft('entity', id) => returns [draft, object]
|
6
|
+
useDraft('entity', id, relations) => returns [draft, objectWithEmbeddedRelations]
|
7
|
+
*/
|
8
|
+
function useDraft(entity, id, relations = []) {
|
9
|
+
// const entityDraft =`${entity}Draft`
|
10
|
+
// const object = { ...s[entityDraft].entities[String(id)] }
|
11
|
+
// return useSelector(s => addRelations(s, object, entity, relations, 'Draft'), _.isEqual)
|
12
|
+
}
|
13
|
+
exports.default = useDraft;
|
data/client/lib/useEager.d.ts
CHANGED
@@ -1 +1 @@
|
|
1
|
-
export default function useEager(entity:
|
1
|
+
export default function useEager(entity: string, id?: string, relations?: any): any;
|
data/client/lib/useEager.js
CHANGED
@@ -3,10 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
-
const
|
7
|
-
const
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
const lodash_1 = __importDefault(require("lodash"));
|
7
|
+
const react_redux_1 = require("react-redux");
|
8
|
+
const addRelations_1 = __importDefault(require("./addRelations"));
|
9
|
+
function useEager(entity, id = '', relations = []) {
|
10
|
+
if (id) {
|
11
|
+
return react_redux_1.useSelector(s => addRelations_1.default(s, Object.assign({}, s[entity].entities[String(id)]), entity, relations), lodash_1.default.isEqual);
|
12
|
+
}
|
13
|
+
else {
|
14
|
+
return react_redux_1.useSelector(s => addRelations_1.default(s, lodash_1.default.values(s[entity].entities), entity, relations), lodash_1.default.isEqual);
|
15
|
+
}
|
11
16
|
}
|
12
17
|
exports.default = useEager;
|
data/client/lib/useJason.d.ts
CHANGED
data/client/lib/useJason.js
CHANGED
@@ -12,12 +12,11 @@ const restClient_1 = __importDefault(require("./restClient"));
|
|
12
12
|
const pruneIdsMiddleware_1 = __importDefault(require("./pruneIdsMiddleware"));
|
13
13
|
const createTransportAdapter_1 = __importDefault(require("./createTransportAdapter"));
|
14
14
|
const toolkit_1 = require("@reduxjs/toolkit");
|
15
|
-
const makeEager_1 = __importDefault(require("./makeEager"));
|
16
15
|
const humps_1 = require("humps");
|
17
16
|
const blueimp_md5_1 = __importDefault(require("blueimp-md5"));
|
18
17
|
const lodash_1 = __importDefault(require("lodash"));
|
19
18
|
const react_1 = require("react");
|
20
|
-
function useJason({ reducers, middleware = [], extraActions }) {
|
19
|
+
function useJason({ reducers, middleware = [], enhancers = [], extraActions }) {
|
21
20
|
const [store, setStore] = react_1.useState(null);
|
22
21
|
const [value, setValue] = react_1.useState(null);
|
23
22
|
react_1.useEffect(() => {
|
@@ -29,11 +28,10 @@ function useJason({ reducers, middleware = [], extraActions }) {
|
|
29
28
|
const serverActionQueue = createServerActionQueue_1.default();
|
30
29
|
const allReducers = Object.assign(Object.assign({}, reducers), createJasonReducers_1.default(schema));
|
31
30
|
console.debug({ allReducers });
|
32
|
-
const store = toolkit_1.configureStore({ reducer: allReducers, middleware: [...middleware, pruneIdsMiddleware_1.default(schema)] });
|
31
|
+
const store = toolkit_1.configureStore({ reducer: allReducers, middleware: [...middleware, pruneIdsMiddleware_1.default(schema)], enhancers });
|
33
32
|
const dispatch = store.dispatch;
|
34
33
|
const optDis = createOptDis_1.default(schema, dispatch, restClient_1.default, serverActionQueue);
|
35
34
|
const actions = createActions_1.default(schema, store, restClient_1.default, optDis, extraActions);
|
36
|
-
const eager = makeEager_1.default(schema);
|
37
35
|
let payloadHandlers = {};
|
38
36
|
let configs = {};
|
39
37
|
let subOptions = {};
|
@@ -73,9 +71,10 @@ function useJason({ reducers, middleware = [], extraActions }) {
|
|
73
71
|
};
|
74
72
|
}
|
75
73
|
function removeSubscription(config) {
|
74
|
+
var _a;
|
76
75
|
transportAdapter.removeSubscription(config);
|
77
76
|
const md5Hash = blueimp_md5_1.default(JSON.stringify(config));
|
78
|
-
payloadHandlers[md5Hash].tearDown();
|
77
|
+
(_a = payloadHandlers[md5Hash]) === null || _a === void 0 ? void 0 : _a.tearDown(); // Race condition where component mounts then unmounts quickly
|
79
78
|
delete payloadHandlers[md5Hash];
|
80
79
|
delete configs[md5Hash];
|
81
80
|
delete subOptions[md5Hash];
|
@@ -83,7 +82,6 @@ function useJason({ reducers, middleware = [], extraActions }) {
|
|
83
82
|
setValue({
|
84
83
|
actions: actions,
|
85
84
|
subscribe: createSubscription,
|
86
|
-
eager,
|
87
85
|
handlePayload
|
88
86
|
});
|
89
87
|
setStore(store);
|
data/client/package.json
CHANGED
@@ -3,8 +3,8 @@ import useJason from './useJason'
|
|
3
3
|
import { Provider } from 'react-redux'
|
4
4
|
import JasonContext from './JasonContext'
|
5
5
|
|
6
|
-
const JasonProvider = ({ reducers, middleware, extraActions, children }: { reducers?: any, middleware?: any, extraActions?: any, children?: React.FC }) => {
|
7
|
-
const [store, value] = useJason({ reducers, middleware, extraActions })
|
6
|
+
const JasonProvider = ({ reducers, middleware, enhancers, extraActions, children }: { reducers?: any, middleware?: any, enhancers?: any, extraActions?: any, children?: React.FC }) => {
|
7
|
+
const [store, value] = useJason({ reducers, middleware, enhancers, extraActions })
|
8
8
|
|
9
9
|
if(!(store && value)) return <div /> // Wait for async fetch of schema to complete
|
10
10
|
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import pluralize from 'pluralize'
|
2
|
+
import _ from 'lodash'
|
3
|
+
|
4
|
+
export default function addRelations(s, objects, objectType, relations, suffix = '') {
|
5
|
+
// first find out relation name
|
6
|
+
if (_.isArray(relations)) {
|
7
|
+
relations.forEach(relation => {
|
8
|
+
objects = addRelations(s, objects, objectType, relation)
|
9
|
+
})
|
10
|
+
} else if (typeof(relations) === 'object') {
|
11
|
+
const relation = Object.keys(relations)[0]
|
12
|
+
const subRelations = relations[relation]
|
13
|
+
|
14
|
+
objects = addRelations(s, objects, objectType, relation)
|
15
|
+
objects[relation] = addRelations(s, objects[relation], pluralize(relation), subRelations)
|
16
|
+
// #
|
17
|
+
} else if (typeof(relations) === 'string') {
|
18
|
+
const relation = relations
|
19
|
+
if (_.isArray(objects)) {
|
20
|
+
objects = objects.map(obj => addRelations(s, obj, objectType, relation))
|
21
|
+
} else if (_.isObject(objects)) {
|
22
|
+
const relatedObjects = _.values(s[pluralize(relation) + suffix].entities)
|
23
|
+
|
24
|
+
if(pluralize.isSingular(relation)) {
|
25
|
+
objects = { ...objects, [relation]: _.find(relatedObjects, { id: objects[relation + 'Id'] }) }
|
26
|
+
} else {
|
27
|
+
objects = { ...objects, [relation]: relatedObjects.filter(e => e[pluralize.singular(objectType) + 'Id'] === objects.id) }
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
return objects
|
33
|
+
}
|
@@ -3,7 +3,8 @@ import pluralize from 'pluralize'
|
|
3
3
|
import _ from 'lodash'
|
4
4
|
|
5
5
|
function generateSlices(models) {
|
6
|
-
|
6
|
+
// create two slices for each model. One to hold the persisted data, and one to hold draft data
|
7
|
+
const sliceNames = models.map(k => pluralize(k)).concat(models.map(k => `${pluralize(k)}Drafts`))
|
7
8
|
const adapter = createEntityAdapter()
|
8
9
|
|
9
10
|
return _.fromPairs(_.map(sliceNames, name => {
|
@@ -67,7 +68,8 @@ function generateJasonSlices(models) {
|
|
67
68
|
name: 'jason',
|
68
69
|
initialState: {
|
69
70
|
connected: false,
|
70
|
-
queueSize: 0
|
71
|
+
queueSize: 0,
|
72
|
+
error: null
|
71
73
|
},
|
72
74
|
reducers: {
|
73
75
|
upsert: (s,a) => ({ ...s, ...a.payload })
|
data/client/src/createOptDis.ts
CHANGED
@@ -15,18 +15,20 @@ export default function createOptDis(schema, dispatch, restClient, serverActionQ
|
|
15
15
|
const plurals = _.keys(schema).map(k => pluralize(k))
|
16
16
|
|
17
17
|
function enqueueServerAction (action) {
|
18
|
-
serverActionQueue.addItem(action)
|
18
|
+
return serverActionQueue.addItem(action)
|
19
19
|
}
|
20
20
|
|
21
21
|
function dispatchServerAction() {
|
22
|
-
const
|
23
|
-
if (!
|
22
|
+
const item = serverActionQueue.getItem()
|
23
|
+
if (!item) return
|
24
|
+
|
25
|
+
const { id, action } = item
|
24
26
|
|
25
27
|
restClient.post('/jason/api/action', action)
|
26
|
-
.then(serverActionQueue.itemProcessed)
|
27
|
-
.catch(
|
28
|
-
dispatch({ type: '
|
29
|
-
serverActionQueue.
|
28
|
+
.then(({ data }) => serverActionQueue.itemProcessed(id, data))
|
29
|
+
.catch(error => {
|
30
|
+
dispatch({ type: 'jason/upsert', payload: { error } })
|
31
|
+
serverActionQueue.itemFailed(id, error)
|
30
32
|
})
|
31
33
|
}
|
32
34
|
|
@@ -39,7 +41,7 @@ export default function createOptDis(schema, dispatch, restClient, serverActionQ
|
|
39
41
|
dispatch({ type, payload: data })
|
40
42
|
|
41
43
|
if (plurals.indexOf(type.split('/')[0]) > -1) {
|
42
|
-
enqueueServerAction({ type, payload: data })
|
44
|
+
return enqueueServerAction({ type, payload: data })
|
43
45
|
}
|
44
46
|
}
|
45
47
|
}
|
@@ -4,7 +4,7 @@ test('Adding items', () => {
|
|
4
4
|
const serverActionQueue = createServerActionQueue()
|
5
5
|
serverActionQueue.addItem({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } })
|
6
6
|
const item = serverActionQueue.getItem()
|
7
|
-
expect(item).toStrictEqual({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } })
|
7
|
+
expect(item.action).toStrictEqual({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } })
|
8
8
|
})
|
9
9
|
|
10
10
|
test('Deduping of items that will overwrite each other', () => {
|
@@ -15,7 +15,7 @@ test('Deduping of items that will overwrite each other', () => {
|
|
15
15
|
|
16
16
|
const item = serverActionQueue.getItem()
|
17
17
|
|
18
|
-
expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 3 } })
|
18
|
+
expect(item.action).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 3 } })
|
19
19
|
})
|
20
20
|
|
21
21
|
test('Deduping of items with a superset', () => {
|
@@ -25,7 +25,7 @@ test('Deduping of items with a superset', () => {
|
|
25
25
|
|
26
26
|
const item = serverActionQueue.getItem()
|
27
27
|
|
28
|
-
expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } })
|
28
|
+
expect(item.action).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } })
|
29
29
|
})
|
30
30
|
|
31
31
|
test("doesn't dedupe items with some attributes missing", () => {
|
@@ -34,9 +34,63 @@ test("doesn't dedupe items with some attributes missing", () => {
|
|
34
34
|
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute2: 'test' } })
|
35
35
|
|
36
36
|
const item = serverActionQueue.getItem()
|
37
|
-
serverActionQueue.itemProcessed()
|
37
|
+
serverActionQueue.itemProcessed(item.id)
|
38
38
|
const item2 = serverActionQueue.getItem()
|
39
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' } })
|
40
|
+
expect(item.action).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
41
|
+
expect(item2.action).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute2: 'test' } })
|
42
42
|
})
|
43
|
+
|
44
|
+
test("executes success callback", async function() {
|
45
|
+
const serverActionQueue = createServerActionQueue()
|
46
|
+
let cb = ''
|
47
|
+
let data = ''
|
48
|
+
|
49
|
+
// Check it can resolve chained promises
|
50
|
+
const promise = serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
51
|
+
.then(d => data = d)
|
52
|
+
.then(() => cb = 'resolved')
|
53
|
+
|
54
|
+
const item = serverActionQueue.getItem()
|
55
|
+
serverActionQueue.itemProcessed(item.id, 'testdata');
|
56
|
+
|
57
|
+
await promise
|
58
|
+
expect(data).toEqual('testdata')
|
59
|
+
expect(cb).toEqual('resolved')
|
60
|
+
})
|
61
|
+
|
62
|
+
test("executes error callback", async function() {
|
63
|
+
const serverActionQueue = createServerActionQueue()
|
64
|
+
let cb = ''
|
65
|
+
let error = ''
|
66
|
+
|
67
|
+
// Check it can resolve chained promises
|
68
|
+
const promise = serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
69
|
+
.then(() => cb = 'resolved')
|
70
|
+
.catch(e => error = e)
|
71
|
+
|
72
|
+
const item = serverActionQueue.getItem()
|
73
|
+
serverActionQueue.itemFailed(item.id, 'testerror');
|
74
|
+
|
75
|
+
await promise
|
76
|
+
expect(cb).toEqual('')
|
77
|
+
expect(error).toEqual('testerror')
|
78
|
+
})
|
79
|
+
|
80
|
+
|
81
|
+
test("merges success callbacks", async function() {
|
82
|
+
const results: any[] = []
|
83
|
+
|
84
|
+
const serverActionQueue = createServerActionQueue()
|
85
|
+
const p1 = serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
86
|
+
.then(data => results.push(data))
|
87
|
+
|
88
|
+
const p2 = serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } })
|
89
|
+
.then(data => results.push(data))
|
90
|
+
|
91
|
+
const item = serverActionQueue.getItem()
|
92
|
+
serverActionQueue.itemProcessed(item.id, 'complete')
|
93
|
+
|
94
|
+
await Promise.all([p1,p2])
|
95
|
+
expect(results).toEqual(['complete', 'complete'])
|
96
|
+
})
|
@@ -1,31 +1,65 @@
|
|
1
1
|
// A FIFO queue with deduping of actions whose effect will be cancelled by later actions
|
2
|
-
|
2
|
+
import { v4 as uuidv4 } from 'uuid'
|
3
3
|
import _ from 'lodash'
|
4
4
|
|
5
|
+
class Deferred {
|
6
|
+
promise: Promise<any>;
|
7
|
+
resolve: any;
|
8
|
+
reject: any;
|
9
|
+
|
10
|
+
constructor() {
|
11
|
+
this.promise = new Promise((resolve, reject)=> {
|
12
|
+
this.reject = reject
|
13
|
+
this.resolve = resolve
|
14
|
+
})
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
5
18
|
export default function createServerActionQueue() {
|
6
19
|
const queue: any[] = []
|
20
|
+
const deferreds = {}
|
21
|
+
|
7
22
|
let inFlight = false
|
8
23
|
|
9
|
-
function addItem(
|
24
|
+
function addItem(action) {
|
10
25
|
// Check if there are any items ahead in the queue that this item would effectively overwrite.
|
11
26
|
// In that case we can remove them
|
12
27
|
// 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 } =
|
28
|
+
const { type, payload } = action
|
29
|
+
const id = uuidv4()
|
30
|
+
const dfd = new Deferred()
|
31
|
+
deferreds[id] = [dfd]
|
32
|
+
|
33
|
+
const item = { id, action }
|
34
|
+
|
14
35
|
if (type.split('/')[1] !== 'upsert') {
|
15
36
|
queue.push(item)
|
16
|
-
return
|
37
|
+
return dfd.promise
|
17
38
|
}
|
18
39
|
|
19
40
|
_.remove(queue, item => {
|
20
|
-
const { type: itemType, payload: itemPayload } = item
|
41
|
+
const { type: itemType, payload: itemPayload } = item.action
|
21
42
|
if (type !== itemType) return false
|
22
43
|
if (itemPayload.id !== payload.id) return false
|
23
44
|
|
24
45
|
// Check that all keys of itemPayload are in payload.
|
46
|
+
deferreds[id].push(...deferreds[item.id])
|
25
47
|
return _.difference(_.keys(itemPayload),_.keys(payload)).length === 0
|
26
48
|
})
|
27
49
|
|
28
50
|
queue.push(item)
|
51
|
+
return dfd.promise
|
52
|
+
}
|
53
|
+
|
54
|
+
function itemProcessed(id, data?: any) {
|
55
|
+
inFlight = false
|
56
|
+
deferreds[id].forEach(dfd => dfd.resolve(data))
|
57
|
+
}
|
58
|
+
|
59
|
+
function itemFailed(id, error?: any) {
|
60
|
+
queue.length = 0
|
61
|
+
deferreds[id].forEach(dfd => dfd.reject(error))
|
62
|
+
inFlight = false
|
29
63
|
}
|
30
64
|
|
31
65
|
return {
|
@@ -40,7 +74,8 @@ export default function createServerActionQueue() {
|
|
40
74
|
}
|
41
75
|
return false
|
42
76
|
},
|
43
|
-
itemProcessed
|
77
|
+
itemProcessed,
|
78
|
+
itemFailed,
|
44
79
|
fullySynced: () => queue.length === 0 && !inFlight,
|
45
80
|
getData: () => ({ queue, inFlight })
|
46
81
|
}
|