jason-rails 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/Gemfile.lock +7 -2
  4. data/README.md +4 -12
  5. data/app/controllers/jason/{api_controller.rb → jason_controller.rb} +1 -1
  6. data/app/controllers/jason/{api/pusher_controller.rb → pusher_controller.rb} +1 -1
  7. data/app/workers/jason/outbound_message_queue_worker.rb +21 -0
  8. data/client/lib/addRelations.d.ts +1 -0
  9. data/client/lib/addRelations.js +39 -0
  10. data/client/lib/createJasonReducers.js +4 -2
  11. data/client/lib/createOptDis.d.ts +1 -1
  12. data/client/lib/createOptDis.js +9 -8
  13. data/client/lib/createServerActionQueue.d.ts +3 -2
  14. data/client/lib/createServerActionQueue.js +32 -6
  15. data/client/lib/createServerActionQueue.test.js +61 -6
  16. data/client/lib/createThenable.d.ts +1 -0
  17. data/client/lib/createThenable.js +5 -0
  18. data/client/lib/transportAdapters/actionCableAdapter.js +24 -4
  19. data/client/lib/transportAdapters/pusherAdapter.js +1 -1
  20. data/client/lib/useDraft.d.ts +1 -0
  21. data/client/lib/useDraft.js +13 -0
  22. data/client/lib/useEager.d.ts +1 -1
  23. data/client/lib/useEager.js +10 -5
  24. data/client/lib/useJason.js +2 -4
  25. data/client/package.json +1 -1
  26. data/client/src/addRelations.ts +33 -0
  27. data/client/src/createJasonReducers.ts +4 -2
  28. data/client/src/createOptDis.ts +10 -8
  29. data/client/src/createServerActionQueue.test.ts +60 -6
  30. data/client/src/createServerActionQueue.ts +41 -6
  31. data/client/src/transportAdapters/actionCableAdapter.ts +24 -5
  32. data/client/src/transportAdapters/pusherAdapter.ts +1 -2
  33. data/client/src/useDraft.ts +17 -0
  34. data/client/src/useEager.ts +9 -6
  35. data/client/src/useJason.ts +1 -4
  36. data/config/routes.rb +6 -6
  37. data/jason-rails.gemspec +1 -0
  38. data/lib/jason.rb +7 -3
  39. data/lib/jason/api_model.rb +0 -4
  40. data/lib/jason/broadcaster.rb +2 -1
  41. data/lib/jason/channel.rb +0 -7
  42. data/lib/jason/conditions_matcher.rb +88 -0
  43. data/lib/jason/consistency_checker.rb +61 -0
  44. data/lib/jason/graph_helper.rb +19 -4
  45. data/lib/jason/publisher.rb +40 -7
  46. data/lib/jason/subscription.rb +77 -17
  47. data/lib/jason/version.rb +1 -1
  48. metadata +30 -5
  49. data/client/src/makeEager.ts +0 -46
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2788ecafe67bc475346d83681912063b1081610e06b685405d4f3b332a665695
4
- data.tar.gz: 6647cc3a7815a914b98a9477330c32a642fec64c809aa4195fa5c192a4b4b003
3
+ metadata.gz: a0bfff0c4de046d090ba0c1708fcd4692da2cccc2caf0911d6ae752310a7a8ee
4
+ data.tar.gz: 8851a0bfdbd521426f3650cbfa44eeea25a4e4bc0bceb8fac3bad7a667563a9c
5
5
  SHA512:
6
- metadata.gz: cf1b1ed464f9ef46cd5b3aa2dd7e8ebb982a3d6d3ebe68a3f92f2ad8b5ab31ea8476817fd16666346d358e9d5a635b4af792bba16e41105eefb7a789082567f1
7
- data.tar.gz: a06afb2c5db05447d7eda5a6f17020a14fd1d486b50f17e14c155738e7166534a52c418f5463742c0956102d744429198ee0101569cef156366586eee3e7c058
6
+ metadata.gz: 47cc3c7df9c3a4c9fedef6899a3948ba78d44a6a507f830c1f7a50dd920794654777d5a5a3c98f6431d2ef2edef3c1d104222bc06abccc39937f5d55fcd3d33e
7
+ data.tar.gz: b6305008dd05be63e8fa1dbfa785179129ecf4554877a801dbe44b979906997ce44761d2978ffd7a1129ab1ed4ce44e83e4464ab8566c3fccb9a137516dd48e7
data/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ ## v0.7.0
2
+ - Added: New forms of conditional subscription. You can now add conditions on fields other than the primary key.
3
+ E.g.
4
+ ```
5
+ useSub({ model: 'post', conditions: { created_at: { type: 'between' value: ['2020-01-01', '2020-02-01'] } })
6
+ useSub({ model: 'post', conditions: { hidden: false } })
7
+ ```
8
+
9
+ - Added: Consistency checker. You can run `Jason::ConsistencyChecker.check_all` to validate all current subscriptions against the contents of the database. If you call `check_all(fix: true)` it will called `reset!(hard: true)` on any subscription whose contents do not match the database.
10
+
11
+ - Changed: Subscriptions no longer get cleared when consumer count drops to 0. This will be replaced in a future release with a reaping process to clean up inactive subscriptions.
12
+
13
+ - Changed: ActionCable subscriptions get their initial payload via REST instead of ActionCable, as this seems to deliver snappier results
14
+
15
+ - Fixed: Small bug in useEager that could throw error if relation wasn't present.
16
+
17
+ ## v0.6.9
18
+ - Added: Optimistic updates now return a promise which can chained to perform actions _after_ an update is persisted to server. (For example, if your component depends on fetching additional data that only exists once your instance is persisted)
19
+ ```
20
+ act.posts.add({ name: 'new post' })
21
+ .then(loadEditPostModal)
22
+ .catch(e => console.error("Oh no!", e))
23
+ ```
24
+
25
+ ## v0.6.8
26
+ - Fix: Objects in 'all' subscription not always being broadcast
27
+
28
+ ## v0.6.7
29
+ - Fix: Change names of controllers to be less likely to conflict with host app inflections
30
+ - Added: Pusher now pushes asychronously via Sidekiq using the Pusher batch API
31
+
32
+ ## v0.6.6
33
+ - Fix: don't run the schema change detection and cache rebuild inside rake tasks or migrations
34
+
35
+ ## v0.6.5
36
+ - Added `reset!` and `reset!(hard: true)` methods to `Subscription`. Reset will load the IDs that should be part of the subscription from the database, and ensure that the graph matches those. It then re-broadcasts the payloads to all connected clients. Hard reset will do the same, but also clear all cached IDs and subscription hooks on instances - this is equivalent from starting from scratch.
37
+ - Added `enforce: boolean` option to GraphHelper
38
+ - When subscriptions are re-activated they now set the IDs with `enforce: true`, as there could be conditions where updates that were made while a subscription was not active would not be properly registered.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jason-rails (0.6.0)
4
+ jason-rails (0.6.8)
5
5
  connection_pool (>= 2.2.3)
6
6
  jsondiff
7
7
  rails (>= 5)
@@ -93,7 +93,7 @@ GEM
93
93
  mini_mime (1.0.2)
94
94
  mini_portile2 (2.5.0)
95
95
  minitest (5.14.3)
96
- nio4r (2.5.4)
96
+ nio4r (2.5.7)
97
97
  nokogiri (1.11.1)
98
98
  mini_portile2 (~> 2.5.0)
99
99
  racc (~> 1.4)
@@ -153,6 +153,10 @@ GEM
153
153
  rspec-mocks (~> 3.10)
154
154
  rspec-support (~> 3.10)
155
155
  rspec-support (3.10.0)
156
+ sidekiq (6.1.3)
157
+ connection_pool (>= 2.2.2)
158
+ rack (~> 2.0)
159
+ redis (>= 4.2.0)
156
160
  sprockets (4.0.2)
157
161
  concurrent-ruby (~> 1.0)
158
162
  rack (> 1, < 3)
@@ -178,6 +182,7 @@ DEPENDENCIES
178
182
  rake (~> 12.0)
179
183
  rspec (~> 3.0)
180
184
  rspec-rails
185
+ sidekiq
181
186
  sqlite3
182
187
 
183
188
  BUNDLED WITH
data/README.md CHANGED
@@ -4,13 +4,13 @@ Jason is still in an experimental phase with a rapidly changing API. It is being
4
4
 
5
5
  ## The goal
6
6
 
7
- I wanted:
7
+ We wanted:
8
8
  - Automatic updates to client state based on database state
9
9
  - Persistence to the database without many layers of passing parameters
10
10
  - Redux for awesome state management
11
11
  - Optimistic updates
12
12
 
13
- I also wanted to avoid writing essentially the same code multiple times in different places to handle common CRUD-like operations. Combine Rails schema definition files, REST endpoints, Redux actions, stores, reducers, handlers for websocket payloads and the translations between them, and it adds up to tons of repetitive boilerplate. Every change to the data schema requires updates in five or six files. This inhibits refactoring and makes mistakes more likely.
13
+ We also wanted to avoid writing essentially the same code multiple times in different places to handle common CRUD-like operations. Combine Rails schema definition files, REST endpoints, Redux actions, stores, reducers, handlers for websocket payloads and the translations between them, and it adds up to tons of repetitive boilerplate. Every change to the data schema requires updates in five or six files. This inhibits refactoring and makes mistakes more likely.
14
14
 
15
15
  Jason attempts to minimize this repitition by auto-generating API endpoints, redux stores and actions from a single schema definition. Further it adds listeners to ActiveRecord models allowing the redux store to be subscribed to updates from a model or set of models.
16
16
 
@@ -163,23 +163,15 @@ Similarly to authorizing subscriptions, you can do this by providing an class to
163
163
  ## Roadmap
164
164
 
165
165
  Development is primarily driven by the needs of projects we're using Jason in. In no particular order, being considered is:
166
+ - Better detection of when subscriptions drop, delete subscription
166
167
  - Failure handling - rolling back local state in case of an error on the server
167
168
  - Authorization - more thorough authorization integration, with utility functions for common authorizations. Allowing authorization of access to particular fields such as restricting the fields of a user that are publicly broadcast.
168
169
  - Utilities for "Draft editing" - both storing client-side copies of model trees which can be committed or discarded, as well as persisting a shadow copy to the database (to allow resumable editing, or possibly collaborative editing features)
169
170
  - Benchmark and migrate if necessary ConnectionPool::Wrapper vs ConnectionPool
170
-
171
- ## Development
172
-
173
-
174
-
175
- ## Contributing
176
-
177
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jason. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/jason/blob/master/CODE_OF_CONDUCT.md).
171
+ - Assess using RedisGraph for the graph diffing functionality, to see if this would provide a performance boost
178
172
 
179
173
  ## License
180
174
 
181
175
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
182
176
 
183
- ## Code of Conduct
184
177
 
185
- Everyone interacting in the Jason project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/jason/blob/master/CODE_OF_CONDUCT.md).
@@ -1,4 +1,4 @@
1
- class Jason::ApiController < ::ApplicationController
1
+ class Jason::JasonController < ::ApplicationController
2
2
  before_action :load_and_authorize_subscription, only: [:create_subscription, :remove_subscription, :get_payload]
3
3
  # config seems to be a reserved name, resulting in infinite loop
4
4
  def configuration
@@ -1,4 +1,4 @@
1
- class Jason::Api::PusherController < ApplicationController
1
+ class Jason::PusherController < ApplicationController
2
2
  skip_before_action :verify_authenticity_token
3
3
 
4
4
  def auth
@@ -0,0 +1,21 @@
1
+ class Jason::OutboundMessageQueueWorker
2
+ include Sidekiq::Worker if defined?(Sidekiq) # Even if not using Pusher, this gets autoloaded
3
+
4
+ def perform
5
+ batch = get_batch
6
+ return if batch.size == 0
7
+
8
+ Jason.pusher.trigger_batch(batch)
9
+ end
10
+
11
+ private
12
+
13
+ def get_batch
14
+ batch_json = $redis_jason.multi do |r|
15
+ r.lrange("jason:outbound_message_queue", 0, 9) # get first 10 elements
16
+ r.ltrim("jason:outbound_message_queue", 10, -1) # delete first 10 elements
17
+ end[0]
18
+
19
+ batch_json.map { |event| JSON.parse(event).with_indifferent_access } # Pusher wants symbol keys
20
+ end
21
+ end
@@ -0,0 +1 @@
1
+ export default function addRelations(s: any, objects: any, objectType: any, relations: any, suffix?: string): any;
@@ -0,0 +1,39 @@
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
+ function addRelations(s, objects, objectType, relations, suffix = '') {
9
+ // first find out relation name
10
+ if (lodash_1.default.isArray(relations)) {
11
+ relations.forEach(relation => {
12
+ objects = addRelations(s, objects, objectType, relation);
13
+ });
14
+ }
15
+ else if (typeof (relations) === 'object') {
16
+ const relation = Object.keys(relations)[0];
17
+ const subRelations = relations[relation];
18
+ objects = addRelations(s, objects, objectType, relation);
19
+ objects[relation] = addRelations(s, objects[relation], pluralize_1.default(relation), subRelations);
20
+ // #
21
+ }
22
+ else if (typeof (relations) === 'string') {
23
+ const relation = relations;
24
+ if (lodash_1.default.isArray(objects)) {
25
+ objects = objects.map(obj => addRelations(s, obj, objectType, relation));
26
+ }
27
+ else if (lodash_1.default.isObject(objects)) {
28
+ const relatedObjects = lodash_1.default.values(s[pluralize_1.default(relation) + suffix].entities);
29
+ if (pluralize_1.default.isSingular(relation)) {
30
+ objects = Object.assign(Object.assign({}, objects), { [relation]: lodash_1.default.find(relatedObjects, { id: objects[relation + 'Id'] }) });
31
+ }
32
+ else {
33
+ objects = Object.assign(Object.assign({}, objects), { [relation]: relatedObjects.filter(e => e[pluralize_1.default.singular(objectType) + 'Id'] === objects.id) });
34
+ }
35
+ }
36
+ }
37
+ return objects;
38
+ }
39
+ exports.default = addRelations;
@@ -7,7 +7,8 @@ const toolkit_1 = require("@reduxjs/toolkit");
7
7
  const pluralize_1 = __importDefault(require("pluralize"));
8
8
  const lodash_1 = __importDefault(require("lodash"));
9
9
  function generateSlices(models) {
10
- const sliceNames = models.map(k => pluralize_1.default(k));
10
+ // create two slices for each model. One to hold the persisted data, and one to hold draft data
11
+ const sliceNames = models.map(k => pluralize_1.default(k)).concat(models.map(k => `${pluralize_1.default(k)}Drafts`));
11
12
  const adapter = toolkit_1.createEntityAdapter();
12
13
  return lodash_1.default.fromPairs(lodash_1.default.map(sliceNames, name => {
13
14
  return [name, toolkit_1.createSlice({
@@ -66,7 +67,8 @@ function generateJasonSlices(models) {
66
67
  name: 'jason',
67
68
  initialState: {
68
69
  connected: false,
69
- queueSize: 0
70
+ queueSize: 0,
71
+ error: null
70
72
  },
71
73
  reducers: {
72
74
  upsert: (s, a) => (Object.assign(Object.assign({}, s), a.payload))
@@ -1 +1 @@
1
- export default function createOptDis(schema: any, dispatch: any, restClient: any, serverActionQueue: any): (action: any) => void;
1
+ export default function createOptDis(schema: any, dispatch: any, restClient: any, serverActionQueue: any): (action: any) => any;
@@ -17,17 +17,18 @@ function enrich(type, payload) {
17
17
  function createOptDis(schema, dispatch, restClient, serverActionQueue) {
18
18
  const plurals = lodash_1.default.keys(schema).map(k => pluralize_1.default(k));
19
19
  function enqueueServerAction(action) {
20
- serverActionQueue.addItem(action);
20
+ return serverActionQueue.addItem(action);
21
21
  }
22
22
  function dispatchServerAction() {
23
- const action = serverActionQueue.getItem();
24
- if (!action)
23
+ const item = serverActionQueue.getItem();
24
+ if (!item)
25
25
  return;
26
+ const { id, action } = item;
26
27
  restClient.post('/jason/api/action', action)
27
- .then(serverActionQueue.itemProcessed)
28
- .catch(e => {
29
- dispatch({ type: 'upsertLocalUi', data: { error: JSON.stringify(e) } });
30
- serverActionQueue.itemProcessed();
28
+ .then(({ data }) => serverActionQueue.itemProcessed(id, data))
29
+ .catch(error => {
30
+ dispatch({ type: 'jason/upsert', payload: { error } });
31
+ serverActionQueue.itemFailed(id, error);
31
32
  });
32
33
  }
33
34
  setInterval(dispatchServerAction, 10);
@@ -36,7 +37,7 @@ function createOptDis(schema, dispatch, restClient, serverActionQueue) {
36
37
  const data = enrich(type, payload);
37
38
  dispatch({ type, payload: data });
38
39
  if (plurals.indexOf(type.split('/')[0]) > -1) {
39
- enqueueServerAction({ type, payload: data });
40
+ return enqueueServerAction({ type, payload: data });
40
41
  }
41
42
  };
42
43
  }
@@ -1,7 +1,8 @@
1
1
  export default function createServerActionQueue(): {
2
- addItem: (item: any) => void;
2
+ addItem: (action: any) => Promise<any>;
3
3
  getItem: () => any;
4
- itemProcessed: () => boolean;
4
+ itemProcessed: (id: any, data?: any) => void;
5
+ itemFailed: (id: any, error?: any) => void;
5
6
  fullySynced: () => boolean;
6
7
  getData: () => {
7
8
  queue: any[];
@@ -1,32 +1,57 @@
1
1
  "use strict";
2
- // A FIFO queue with deduping of actions whose effect will be cancelled by later actions
3
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
4
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
4
  };
6
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ // A FIFO queue with deduping of actions whose effect will be cancelled by later actions
7
+ const uuid_1 = require("uuid");
7
8
  const lodash_1 = __importDefault(require("lodash"));
9
+ class Deferred {
10
+ constructor() {
11
+ this.promise = new Promise((resolve, reject) => {
12
+ this.reject = reject;
13
+ this.resolve = resolve;
14
+ });
15
+ }
16
+ }
8
17
  function createServerActionQueue() {
9
18
  const queue = [];
19
+ const deferreds = {};
10
20
  let inFlight = false;
11
- function addItem(item) {
21
+ function addItem(action) {
12
22
  // Check if there are any items ahead in the queue that this item would effectively overwrite.
13
23
  // In that case we can remove them
14
24
  // If this is an upsert && item ID is the same && current item attributes are a superset of the earlier item attributes
15
- const { type, payload } = item;
25
+ const { type, payload } = action;
26
+ const id = uuid_1.v4();
27
+ const dfd = new Deferred();
28
+ deferreds[id] = [dfd];
29
+ const item = { id, action };
16
30
  if (type.split('/')[1] !== 'upsert') {
17
31
  queue.push(item);
18
- return;
32
+ return dfd.promise;
19
33
  }
20
34
  lodash_1.default.remove(queue, item => {
21
- const { type: itemType, payload: itemPayload } = item;
35
+ const { type: itemType, payload: itemPayload } = item.action;
22
36
  if (type !== itemType)
23
37
  return false;
24
38
  if (itemPayload.id !== payload.id)
25
39
  return false;
26
40
  // Check that all keys of itemPayload are in payload.
41
+ deferreds[id].push(...deferreds[item.id]);
27
42
  return lodash_1.default.difference(lodash_1.default.keys(itemPayload), lodash_1.default.keys(payload)).length === 0;
28
43
  });
29
44
  queue.push(item);
45
+ return dfd.promise;
46
+ }
47
+ function itemProcessed(id, data) {
48
+ inFlight = false;
49
+ deferreds[id].forEach(dfd => dfd.resolve(data));
50
+ }
51
+ function itemFailed(id, error) {
52
+ queue.length = 0;
53
+ deferreds[id].forEach(dfd => dfd.reject(error));
54
+ inFlight = false;
30
55
  }
31
56
  return {
32
57
  addItem,
@@ -40,7 +65,8 @@ function createServerActionQueue() {
40
65
  }
41
66
  return false;
42
67
  },
43
- itemProcessed: () => inFlight = false,
68
+ itemProcessed,
69
+ itemFailed,
44
70
  fullySynced: () => queue.length === 0 && !inFlight,
45
71
  getData: () => ({ queue, inFlight })
46
72
  };
@@ -1,4 +1,13 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
13
  };
@@ -8,7 +17,7 @@ test('Adding items', () => {
8
17
  const serverActionQueue = createServerActionQueue_1.default();
9
18
  serverActionQueue.addItem({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } });
10
19
  const item = serverActionQueue.getItem();
11
- expect(item).toStrictEqual({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } });
20
+ expect(item.action).toStrictEqual({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } });
12
21
  });
13
22
  test('Deduping of items that will overwrite each other', () => {
14
23
  const serverActionQueue = createServerActionQueue_1.default();
@@ -16,22 +25,68 @@ test('Deduping of items that will overwrite each other', () => {
16
25
  serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2 } });
17
26
  serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 3 } });
18
27
  const item = serverActionQueue.getItem();
19
- expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 3 } });
28
+ expect(item.action).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 3 } });
20
29
  });
21
30
  test('Deduping of items with a superset', () => {
22
31
  const serverActionQueue = createServerActionQueue_1.default();
23
32
  serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } });
24
33
  serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } });
25
34
  const item = serverActionQueue.getItem();
26
- expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } });
35
+ expect(item.action).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } });
27
36
  });
28
37
  test("doesn't dedupe items with some attributes missing", () => {
29
38
  const serverActionQueue = createServerActionQueue_1.default();
30
39
  serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } });
31
40
  serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute2: 'test' } });
32
41
  const item = serverActionQueue.getItem();
33
- serverActionQueue.itemProcessed();
42
+ serverActionQueue.itemProcessed(item.id);
34
43
  const item2 = serverActionQueue.getItem();
35
- expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } });
36
- expect(item2).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute2: 'test' } });
44
+ expect(item.action).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } });
45
+ expect(item2.action).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute2: 'test' } });
46
+ });
47
+ test("executes success callback", function () {
48
+ return __awaiter(this, void 0, void 0, function* () {
49
+ const serverActionQueue = createServerActionQueue_1.default();
50
+ let cb = '';
51
+ let data = '';
52
+ // Check it can resolve chained promises
53
+ const promise = serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
54
+ .then(d => data = d)
55
+ .then(() => cb = 'resolved');
56
+ const item = serverActionQueue.getItem();
57
+ serverActionQueue.itemProcessed(item.id, 'testdata');
58
+ yield promise;
59
+ expect(data).toEqual('testdata');
60
+ expect(cb).toEqual('resolved');
61
+ });
62
+ });
63
+ test("executes error callback", function () {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ const serverActionQueue = createServerActionQueue_1.default();
66
+ let cb = '';
67
+ let error = '';
68
+ // Check it can resolve chained promises
69
+ const promise = serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
70
+ .then(() => cb = 'resolved')
71
+ .catch(e => error = e);
72
+ const item = serverActionQueue.getItem();
73
+ serverActionQueue.itemFailed(item.id, 'testerror');
74
+ yield promise;
75
+ expect(cb).toEqual('');
76
+ expect(error).toEqual('testerror');
77
+ });
78
+ });
79
+ test("merges success callbacks", function () {
80
+ return __awaiter(this, void 0, void 0, function* () {
81
+ const results = [];
82
+ const serverActionQueue = createServerActionQueue_1.default();
83
+ const p1 = serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
84
+ .then(data => results.push(data));
85
+ const p2 = serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } })
86
+ .then(data => results.push(data));
87
+ const item = serverActionQueue.getItem();
88
+ serverActionQueue.itemProcessed(item.id, 'complete');
89
+ yield Promise.all([p1, p2]);
90
+ expect(results).toEqual(['complete', 'complete']);
91
+ });
37
92
  });