jason-rails 0.6.7 → 0.7.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
}
|