jason-rails 0.6.8 → 0.7.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/Gemfile.lock +5 -3
- data/README.md +20 -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 +3 -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/createTransportAdapter.d.ts +1 -1
- data/client/lib/createTransportAdapter.js +2 -2
- data/client/lib/index.d.ts +8 -1
- data/client/lib/index.js +3 -1
- data/client/lib/transportAdapters/actionCableAdapter.d.ts +1 -1
- data/client/lib/transportAdapters/actionCableAdapter.js +27 -6
- 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 +3 -1
- data/client/lib/useJason.js +4 -7
- 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/createTransportAdapter.ts +2 -2
- data/client/src/index.ts +2 -0
- data/client/src/transportAdapters/actionCableAdapter.ts +28 -7
- 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 +10 -7
- data/lib/jason.rb +9 -2
- 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 +36 -36
- data/lib/jason/subscription.rb +51 -15
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b901c6b15b2b4f00d307c0e86811d660a68be05ac8c43f3d48c41412a020b4e8
|
4
|
+
data.tar.gz: 3fbed93cb66f5acffe86a8254f0be3ed67979bf93d72ab8faed77a0ae552fff3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6959b8b6e2b97547a9c596d4efe5332339deb240c31c3951eb10e017e54932e529c851303e38bdc347c8ed89b4a209974fe790f3377128b0fb4229d14c490f66
|
7
|
+
data.tar.gz: a9a17d52153d5580efcd2940437b1f5eac249f7611d38a152a4a043ea35803f1de6d2ce9e53ecd0e8dee923efd47df1be3c1b1d73e515095a4356875e25f0438
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,37 @@
|
|
1
|
+
## v0.7.3
|
2
|
+
- Added: Add JasonContext to exports, for use in scenarios where you need to forward the context into some other React reconciler (e.g. `react-three-fiber`)
|
3
|
+
- Fixed: Unneeded `reload` in Publisher resulting in extra database calls
|
4
|
+
|
5
|
+
## v0.7.1
|
6
|
+
- Added: Authorization for REST endpoints. Previously these just inherited logic from ApplicationController. Pass a `update_authorization_service` option to the Jason initializer to use this.
|
7
|
+
|
8
|
+
## v0.7.0
|
9
|
+
- Added: New forms of conditional subscription. You can now add conditions on fields other than the primary key.
|
10
|
+
E.g.
|
11
|
+
```
|
12
|
+
useSub({ model: 'post', conditions: { created_at: { type: 'between' value: ['2020-01-01', '2020-02-01'] } })
|
13
|
+
useSub({ model: 'post', conditions: { hidden: false } })
|
14
|
+
```
|
15
|
+
|
16
|
+
- 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.
|
17
|
+
|
18
|
+
- 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.
|
19
|
+
|
20
|
+
- Changed: ActionCable subscriptions get their initial payload via REST instead of ActionCable, as this seems to deliver snappier results
|
21
|
+
|
22
|
+
- Fixed: Small bug in useEager that could throw error if relation wasn't present.
|
23
|
+
|
24
|
+
## v0.6.9
|
25
|
+
- 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)
|
26
|
+
```
|
27
|
+
act.posts.add({ name: 'new post' })
|
28
|
+
.then(loadEditPostModal)
|
29
|
+
.catch(e => console.error("Oh no!", e))
|
30
|
+
```
|
31
|
+
|
32
|
+
## v0.6.8
|
33
|
+
- Fix: Objects in 'all' subscription not always being broadcast
|
34
|
+
|
1
35
|
## v0.6.7
|
2
36
|
- Fix: Change names of controllers to be less likely to conflict with host app inflections
|
3
37
|
- Added: Pusher now pushes asychronously via Sidekiq using the Pusher batch API
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
jason-rails (0.
|
4
|
+
jason-rails (0.7.1)
|
5
5
|
connection_pool (>= 2.2.3)
|
6
6
|
jsondiff
|
7
7
|
rails (>= 5)
|
@@ -89,8 +89,10 @@ GEM
|
|
89
89
|
marcel (0.3.3)
|
90
90
|
mimemagic (~> 0.3.2)
|
91
91
|
method_source (1.0.0)
|
92
|
-
mimemagic (0.3.
|
93
|
-
|
92
|
+
mimemagic (0.3.10)
|
93
|
+
nokogiri (~> 1)
|
94
|
+
rake
|
95
|
+
mini_mime (1.1.0)
|
94
96
|
mini_portile2 (2.5.0)
|
95
97
|
minitest (5.14.3)
|
96
98
|
nio4r (2.5.7)
|
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
|
-
|
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
|
-
|
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
|
|
@@ -52,6 +52,11 @@ Jason.setup do |config|
|
|
52
52
|
end
|
53
53
|
```
|
54
54
|
|
55
|
+
Mount the Jason engine in `routes.rb`
|
56
|
+
```ruby
|
57
|
+
mount Jason::Engine => "/jason"
|
58
|
+
```
|
59
|
+
|
55
60
|
### In your frontend code
|
56
61
|
|
57
62
|
First you need to wrap your root component in a `JasonProvider`.
|
@@ -152,13 +157,15 @@ export default function Comment({ id }) {
|
|
152
157
|
|
153
158
|
## Authorization
|
154
159
|
|
155
|
-
By default all models can be subscribed to and updated without authentication or authorization. Probably you want to lock down access.
|
160
|
+
By default all models can be subscribed to and updated without authentication or authorization. Probably you want to lock down access. At the moment Jason has no opinion on how to handle authorization, it simply forwards parameters to a service that you provide - so the implementation can be as simple or as complex as you need.
|
156
161
|
|
157
162
|
### Authorizing subscriptions
|
158
163
|
You can do this by providing an class to Jason in the initializer under the `subscription_authorization_service` key. This must be a class receiving a message `call` with the parameters `user`, `model`, `conditions`, `sub_models` and return true or false for whether the user is allowed to access a subscription with those parameters. You can decide the implementation details of this to be as simple or complex as your app requires.
|
159
164
|
|
160
165
|
### Authorizing updates
|
161
|
-
Similarly to authorizing subscriptions, you can do this by providing an class to Jason in the initializer under the `update_authorization_service` key. This must be a class receiving a message `call` with the parameters `user`, `model`, `
|
166
|
+
Similarly to authorizing subscriptions, you can do this by providing an class to Jason in the initializer under the `update_authorization_service` key. This must be a class receiving a message `call` with the parameters `user`, `model`, `action`, `instance`, `params` and return true or false for whether the user is allowed to make this update.
|
167
|
+
|
168
|
+
See the specs for some examples of this.
|
162
169
|
|
163
170
|
## Roadmap
|
164
171
|
|
@@ -169,19 +176,18 @@ Development is primarily driven by the needs of projects we're using Jason in. I
|
|
169
176
|
- 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)
|
170
177
|
- Benchmark and migrate if necessary ConnectionPool::Wrapper vs ConnectionPool
|
171
178
|
- Assess using RedisGraph for the graph diffing functionality, to see if this would provide a performance boost
|
179
|
+
- Improve the Typescript definitions (ie remove the abundant `any` typing currently used)
|
172
180
|
|
173
|
-
##
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
181
|
+
## Publishing a new version
|
182
|
+
- Update `version.rb`
|
183
|
+
- `gem build`
|
184
|
+
- `gem push`
|
185
|
+
- `npm version [major/minor/patch]`
|
186
|
+
- `npm publish`
|
187
|
+
- Push new version to Github
|
180
188
|
|
181
189
|
## License
|
182
190
|
|
183
|
-
The gem
|
191
|
+
The gem, npm package and source code in the git repository are available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
184
192
|
|
185
|
-
## Code of Conduct
|
186
193
|
|
187
|
-
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).
|
@@ -21,14 +21,17 @@ class Jason::JasonController < ::ApplicationController
|
|
21
21
|
def action
|
22
22
|
type = params[:type]
|
23
23
|
entity = type.split('/')[0].underscore
|
24
|
-
|
25
|
-
|
24
|
+
model_name = entity.singularize
|
25
|
+
api_model = Jason::ApiModel.new(model_name)
|
26
|
+
model = model_name.camelize.constantize
|
26
27
|
action = type.split('/')[1].underscore
|
27
28
|
|
28
29
|
if action == 'move_priority'
|
29
30
|
id, priority = params[:payload].values_at(:id, :priority)
|
30
31
|
|
31
32
|
instance = model.find(id)
|
33
|
+
|
34
|
+
return head :forbidden if !action_permitted?(model_name, action, instance, params)
|
32
35
|
priority_filter = instance.as_json.with_indifferent_access.slice(*api_model.priority_scope)
|
33
36
|
|
34
37
|
all_instance_ids = model.send(api_model.scope || :all).where(priority_filter).where.not(id: instance.id).order(:priority).pluck(:id)
|
@@ -40,10 +43,24 @@ class Jason::JasonController < ::ApplicationController
|
|
40
43
|
|
41
44
|
model.find(all_instance_ids).each(&:force_publish_json)
|
42
45
|
elsif action == 'upsert' || action == 'add'
|
46
|
+
id = params[:payload][:id]
|
43
47
|
payload = api_model.permit(params)
|
44
|
-
|
48
|
+
|
49
|
+
instance = model.find_by(id: id)
|
50
|
+
return head :forbidden if !action_permitted?(model_name, action, instance, params)
|
51
|
+
|
52
|
+
if instance.present?
|
53
|
+
instance.update!(payload)
|
54
|
+
else
|
55
|
+
instance = model.create!(payload)
|
56
|
+
end
|
57
|
+
|
58
|
+
return render json: instance.as_json(api_model.as_json_config)
|
45
59
|
elsif action == 'remove'
|
46
|
-
model.find(params[:payload])
|
60
|
+
instance = model.find(params[:payload])
|
61
|
+
return head :forbidden if !action_permitted?(model_name, action, instance, params)
|
62
|
+
|
63
|
+
instance.destroy!
|
47
64
|
end
|
48
65
|
|
49
66
|
return head :ok
|
@@ -75,4 +92,9 @@ class Jason::JasonController < ::ApplicationController
|
|
75
92
|
return head :forbidden
|
76
93
|
end
|
77
94
|
end
|
95
|
+
|
96
|
+
def action_permitted?(model_name, action, instance, params)
|
97
|
+
return true if Jason.update_authorization_service.blank?
|
98
|
+
Jason.update_authorization_service.call(current_user, model_name, action, instance, params)
|
99
|
+
end
|
78
100
|
end
|
@@ -1,8 +1,10 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
declare const JasonProvider: ({ reducers, middleware, extraActions, children }: {
|
2
|
+
declare const JasonProvider: ({ reducers, middleware, enhancers, extraActions, transportOptions, children }: {
|
3
3
|
reducers?: any;
|
4
4
|
middleware?: any;
|
5
|
+
enhancers?: any;
|
5
6
|
extraActions?: any;
|
7
|
+
transportOptions?: any;
|
6
8
|
children?: React.FC<{}> | undefined;
|
7
9
|
}) => JSX.Element;
|
8
10
|
export default JasonProvider;
|
data/client/lib/JasonProvider.js
CHANGED
@@ -7,8 +7,8 @@ const react_1 = __importDefault(require("react"));
|
|
7
7
|
const useJason_1 = __importDefault(require("./useJason"));
|
8
8
|
const react_redux_1 = require("react-redux");
|
9
9
|
const JasonContext_1 = __importDefault(require("./JasonContext"));
|
10
|
-
const JasonProvider = ({ reducers, middleware, extraActions, children }) => {
|
11
|
-
const [store, value] = useJason_1.default({ reducers, middleware, extraActions });
|
10
|
+
const JasonProvider = ({ reducers, middleware, enhancers, extraActions, transportOptions = {}, children }) => {
|
11
|
+
const [store, value] = useJason_1.default({ reducers, middleware, enhancers, extraActions, transportOptions });
|
12
12
|
if (!(store && value))
|
13
13
|
return react_1.default.createElement("div", null); // Wait for async fetch of schema to complete
|
14
14
|
return react_1.default.createElement(react_redux_1.Provider, { store: store },
|
@@ -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
|
-
|
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) =>
|
1
|
+
export default function createOptDis(schema: any, dispatch: any, restClient: any, serverActionQueue: any): (action: any) => any;
|
data/client/lib/createOptDis.js
CHANGED
@@ -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
|
24
|
-
if (!
|
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(
|
29
|
-
dispatch({ type: '
|
30
|
-
serverActionQueue.
|
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: (
|
2
|
+
addItem: (action: any) => Promise<any>;
|
3
3
|
getItem: () => any;
|
4
|
-
itemProcessed: () =>
|
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(
|
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 } =
|
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
|
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
|
});
|