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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/Gemfile.lock +6 -4
  4. data/README.md +16 -14
  5. data/app/controllers/jason/jason_controller.rb +26 -4
  6. data/app/workers/jason/outbound_message_queue_worker.rb +1 -1
  7. data/client/lib/JasonProvider.d.ts +2 -1
  8. data/client/lib/JasonProvider.js +2 -2
  9. data/client/lib/addRelations.d.ts +1 -0
  10. data/client/lib/addRelations.js +39 -0
  11. data/client/lib/createJasonReducers.js +4 -2
  12. data/client/lib/createOptDis.d.ts +1 -1
  13. data/client/lib/createOptDis.js +9 -8
  14. data/client/lib/createServerActionQueue.d.ts +3 -2
  15. data/client/lib/createServerActionQueue.js +32 -6
  16. data/client/lib/createServerActionQueue.test.js +61 -6
  17. data/client/lib/createThenable.d.ts +1 -0
  18. data/client/lib/createThenable.js +5 -0
  19. data/client/lib/index.d.ts +7 -1
  20. data/client/lib/index.js +3 -1
  21. data/client/lib/transportAdapters/actionCableAdapter.js +24 -4
  22. data/client/lib/transportAdapters/pusherAdapter.js +1 -1
  23. data/client/lib/useDraft.d.ts +1 -0
  24. data/client/lib/useDraft.js +13 -0
  25. data/client/lib/useEager.d.ts +1 -1
  26. data/client/lib/useEager.js +10 -5
  27. data/client/lib/useJason.d.ts +2 -1
  28. data/client/lib/useJason.js +4 -6
  29. data/client/package.json +1 -1
  30. data/client/src/JasonProvider.tsx +2 -2
  31. data/client/src/addRelations.ts +33 -0
  32. data/client/src/createJasonReducers.ts +4 -2
  33. data/client/src/createOptDis.ts +10 -8
  34. data/client/src/createServerActionQueue.test.ts +60 -6
  35. data/client/src/createServerActionQueue.ts +41 -6
  36. data/client/src/index.ts +2 -0
  37. data/client/src/transportAdapters/actionCableAdapter.ts +24 -5
  38. data/client/src/transportAdapters/pusherAdapter.ts +1 -2
  39. data/client/src/useDraft.ts +17 -0
  40. data/client/src/useEager.ts +9 -6
  41. data/client/src/useJason.ts +3 -6
  42. data/lib/jason.rb +4 -1
  43. data/lib/jason/api_model.rb +0 -4
  44. data/lib/jason/channel.rb +0 -7
  45. data/lib/jason/conditions_matcher.rb +88 -0
  46. data/lib/jason/consistency_checker.rb +65 -0
  47. data/lib/jason/graph_helper.rb +4 -0
  48. data/lib/jason/publisher.rb +39 -37
  49. data/lib/jason/subscription.rb +63 -18
  50. data/lib/jason/version.rb +1 -1
  51. metadata +12 -5
  52. data/client/src/makeEager.ts +0 -46
  53. data/lib/jason/publisher_old.rb +0 -112
  54. data/lib/jason/subscription_old.rb +0 -171
@@ -0,0 +1 @@
1
+ export default function createThenable(): void;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ function createThenable() {
4
+ }
5
+ exports.default = createThenable;
@@ -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 JasonProvider: ({ reducers, middleware, extraActions, children }: {
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
- subscription.send({ removeSubscription: config });
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
- let consumerId = uuid_1.v4();
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;
@@ -1 +1 @@
1
- export default function useEager(entity: any, id?: null, relations?: never[]): void;
1
+ export default function useEager(entity: string, id?: string, relations?: any): any;
@@ -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 JasonContext_1 = __importDefault(require("./JasonContext"));
7
- const react_1 = require("react");
8
- function useEager(entity, id = null, relations = []) {
9
- const { eager } = react_1.useContext(JasonContext_1.default);
10
- return eager(entity, id, relations);
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;
@@ -1,5 +1,6 @@
1
- export default function useJason({ reducers, middleware, extraActions }: {
1
+ export default function useJason({ reducers, middleware, enhancers, extraActions }: {
2
2
  reducers?: any;
3
3
  middleware?: any[];
4
+ enhancers?: any[];
4
5
  extraActions?: any;
5
6
  }): any[];
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jamesr2323/jason",
3
- "version": "0.6.7",
3
+ "version": "0.7.3",
4
4
  "module": "./lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "scripts": {
@@ -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
- const sliceNames = models.map(k => pluralize(k))
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 })
@@ -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 action = serverActionQueue.getItem()
23
- if (!action) return
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(e => {
28
- dispatch({ type: 'upsertLocalUi', data: { error: JSON.stringify(e) } })
29
- serverActionQueue.itemProcessed()
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(item) {
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 } = item
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: () => inFlight = false,
77
+ itemProcessed,
78
+ itemFailed,
44
79
  fullySynced: () => queue.length === 0 && !inFlight,
45
80
  getData: () => ({ queue, inFlight })
46
81
  }