jason-rails 0.6.2 → 0.6.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +7 -2
- data/README.md +27 -2
- data/app/controllers/jason/{api_controller.rb → jason_controller.rb} +3 -3
- data/app/controllers/jason/{api/pusher_controller.rb → pusher_controller.rb} +1 -1
- data/app/workers/jason/outbound_message_queue_worker.rb +21 -0
- data/client/lib/createJasonReducers.js +3 -4
- data/client/lib/createPayloadHandler.js +2 -2
- data/client/lib/pruneIdsMiddleware.js +1 -1
- data/client/lib/useEager.d.ts +1 -0
- data/client/lib/useEager.js +12 -0
- data/client/lib/useJason.test.js +33 -1
- data/client/package.json +1 -1
- data/client/src/createJasonReducers.ts +3 -4
- data/client/src/createPayloadHandler.ts +2 -2
- data/client/src/pruneIdsMiddleware.ts +2 -1
- data/client/src/useEager.ts +9 -0
- data/client/src/useJason.test.ts +41 -1
- data/config/routes.rb +6 -6
- data/jason-rails.gemspec +1 -0
- data/lib/jason.rb +5 -3
- data/lib/jason/broadcaster.rb +2 -1
- data/lib/jason/graph_helper.rb +15 -4
- data/lib/jason/publisher.rb +3 -1
- data/lib/jason/subscription.rb +17 -2
- data/lib/jason/version.rb +1 -1
- metadata +23 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48abc57aac9f4529a93812d46422ac21d48e58432fc21e570f1801b84a3ac436
|
4
|
+
data.tar.gz: ac21dab5464e5dd254a1a223a80581c1410c928bab4c0fbc9fe32cdfedb7c474
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '09c42d2b035af32efdc006224e3e81fa398d2327199845640ff7c710c7cecbd0c5edd9d18e441b5b3fefb2a356f45159c77194ed72f866880171071fb302c422'
|
7
|
+
data.tar.gz: 1b5f4f16a5032ca96f8106dc4fff8c8a1a1d5792392b4ed28ad9f1c8974848dec5359e4bb0bc675463cc923e5f77552b6bcfda6c733bf72bb66f156648ce521c
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
## v0.6.6
|
2
|
+
- Fix: don't run the schema change detection and cache rebuild inside rake tasks or migrations
|
3
|
+
|
4
|
+
## v0.6.5
|
5
|
+
- 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.
|
6
|
+
- Added `enforce: boolean` option to GraphHelper
|
7
|
+
- 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.
|
4
|
+
jason-rails (0.6.6)
|
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.
|
96
|
+
nio4r (2.5.5)
|
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
@@ -1,6 +1,6 @@
|
|
1
1
|
# Jason
|
2
2
|
|
3
|
-
Jason is still in
|
3
|
+
Jason is still in an experimental phase with a rapidly changing API. It is being used in some production applications, however it is still in 0.x.x series versions, which means that any 0.x version bump could introduce breaking changes.
|
4
4
|
|
5
5
|
## The goal
|
6
6
|
|
@@ -14,6 +14,8 @@ I also wanted to avoid writing essentially the same code multiple times in diffe
|
|
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
|
|
17
|
+
An alternative way of thinking about Jason is "what if we applied the Flux/Redux state update pattern to make the _database_ the store?".
|
18
|
+
|
17
19
|
## Installation
|
18
20
|
|
19
21
|
Add the gem and the NPM package
|
@@ -81,7 +83,7 @@ function extraActions(dispatch, store, restClient, act) {
|
|
81
83
|
- `middleware` - Passed directly to `configureStore` with additional Jason middleware
|
82
84
|
|
83
85
|
## Usage
|
84
|
-
Jason provides
|
86
|
+
Jason provides three custom hooks to access functionality.
|
85
87
|
|
86
88
|
### useAct
|
87
89
|
This returns an object which allows you to access actions which both update models on the server, and perform an optimistic update to the Redux store.
|
@@ -126,6 +128,28 @@ export default function PostsList() {
|
|
126
128
|
}
|
127
129
|
```
|
128
130
|
|
131
|
+
### useEager
|
132
|
+
Jason stores all the data in a normalized form - one redux slice per model. Often you might want to get nested data from several slices for use in components. The `useEager` hook provides an API for doing that. Under the hood it's just a wrapper around useSelector, which aims to mimic the behaviour of Rails eager loading.
|
133
|
+
|
134
|
+
Example
|
135
|
+
This will fetch the comment as well as the post and user linked to it.
|
136
|
+
|
137
|
+
```jsx
|
138
|
+
import React from 'react'
|
139
|
+
import { useSelector } from 'react-redux'
|
140
|
+
import { useEager } from '@jamesr2323/jason'
|
141
|
+
import _ from 'lodash'
|
142
|
+
|
143
|
+
export default function Comment({ id }) {
|
144
|
+
const comment = useEager('comments', id, ['post', 'user'])
|
145
|
+
|
146
|
+
return <div>
|
147
|
+
<p>{ comment.body }</p>
|
148
|
+
<p>Made on post { comment.post.name } by { comment.user.name }</p>
|
149
|
+
</div>
|
150
|
+
}
|
151
|
+
```
|
152
|
+
|
129
153
|
## Authorization
|
130
154
|
|
131
155
|
By default all models can be subscribed to and updated without authentication or authorization. Probably you want to lock down access.
|
@@ -143,6 +167,7 @@ Development is primarily driven by the needs of projects we're using Jason in. I
|
|
143
167
|
- 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.
|
144
168
|
- 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)
|
145
169
|
- Benchmark and migrate if necessary ConnectionPool::Wrapper vs ConnectionPool
|
170
|
+
- Assess using RedisGraph for the graph diffing functionality, to see if this would provide a performance boost
|
146
171
|
|
147
172
|
## Development
|
148
173
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class Jason::
|
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
|
@@ -35,10 +35,10 @@ class Jason::ApiController < ::ApplicationController
|
|
35
35
|
all_instance_ids.insert(priority.to_i, instance.id)
|
36
36
|
|
37
37
|
all_instance_ids.each_with_index do |id, i|
|
38
|
-
model.find(id).update!(priority: i
|
38
|
+
model.find(id).update!(priority: i)
|
39
39
|
end
|
40
40
|
|
41
|
-
model.
|
41
|
+
model.find(all_instance_ids).each(&:force_publish_json)
|
42
42
|
elsif action == 'upsert' || action == 'add'
|
43
43
|
payload = api_model.permit(params)
|
44
44
|
return render json: model.find_or_create_by_id!(payload).as_json(api_model.as_json_config)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Jason::OutboundMessageQueueWorker
|
2
|
+
include Sidekiq::Worker
|
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
|
@@ -42,18 +42,17 @@ function generateJasonSlices(models) {
|
|
42
42
|
setSubscriptionIds(s, a) {
|
43
43
|
const { payload } = a;
|
44
44
|
const { subscriptionId, model, ids } = payload;
|
45
|
-
|
46
|
-
s[model][subscriptionId] = ids;
|
45
|
+
s[model][subscriptionId] = ids.map(id => String(id));
|
47
46
|
},
|
48
47
|
addSubscriptionId(s, a) {
|
49
48
|
const { payload } = a;
|
50
49
|
const { subscriptionId, model, id } = payload;
|
51
|
-
s[model][subscriptionId] = lodash_1.default.union(s[model][subscriptionId] || [], [id]);
|
50
|
+
s[model][subscriptionId] = lodash_1.default.union(s[model][subscriptionId] || [], [String(id)]);
|
52
51
|
},
|
53
52
|
removeSubscriptionId(s, a) {
|
54
53
|
const { payload } = a;
|
55
54
|
const { subscriptionId, model, id } = payload;
|
56
|
-
s[model][subscriptionId] = lodash_1.default.
|
55
|
+
s[model][subscriptionId] = lodash_1.default.difference(s[model][subscriptionId] || [], [String(id)]);
|
57
56
|
},
|
58
57
|
removeSubscription(s, a) {
|
59
58
|
const { payload: { subscriptionId } } = a;
|
@@ -36,7 +36,7 @@ function createPayloadHandler({ dispatch, serverActionQueue, transportAdapter, c
|
|
36
36
|
}
|
37
37
|
const { payload, destroy, id, type } = patchQueue[model][idx[model]];
|
38
38
|
if (type === 'payload') {
|
39
|
-
dispatch({ type: `${pluralize_1.default(model)}/upsertMany`, payload });
|
39
|
+
dispatch({ type: `${pluralize_1.default(model)}/upsertMany`, payload: payload.map(m => (Object.assign(Object.assign({}, m), { id: String(m.id) }))) });
|
40
40
|
const ids = payload.map(instance => instance.id);
|
41
41
|
dispatch({ type: `jasonModels/setSubscriptionIds`, payload: { model, subscriptionId, ids } });
|
42
42
|
}
|
@@ -45,7 +45,7 @@ function createPayloadHandler({ dispatch, serverActionQueue, transportAdapter, c
|
|
45
45
|
dispatch({ type: `jasonModels/removeSubscriptionId`, payload: { model, subscriptionId, id } });
|
46
46
|
}
|
47
47
|
else {
|
48
|
-
dispatch({ type: `${pluralize_1.default(model)}/upsert`, payload });
|
48
|
+
dispatch({ type: `${pluralize_1.default(model)}/upsert`, payload: Object.assign(Object.assign({}, payload), { id: String(payload.id) }) });
|
49
49
|
dispatch({ type: `jasonModels/addSubscriptionId`, payload: { model, subscriptionId, id } });
|
50
50
|
}
|
51
51
|
delete patchQueue[model][idx[model]];
|
@@ -9,7 +9,7 @@ const pruneIdsMiddleware = schema => store => next => action => {
|
|
9
9
|
const { type, payload } = action;
|
10
10
|
const result = next(action);
|
11
11
|
const state = store.getState();
|
12
|
-
if (type === 'jasonModels/setSubscriptionIds' || type === 'jasonModels/
|
12
|
+
if (type === 'jasonModels/setSubscriptionIds' || type === 'jasonModels/removeSubscriptionId') {
|
13
13
|
const { model, ids } = payload;
|
14
14
|
let idsInSubs = [];
|
15
15
|
lodash_1.default.map(state.jasonModels[model], (subscribedIds, k) => {
|
@@ -0,0 +1 @@
|
|
1
|
+
export default function useEager(entity: any, id?: null, relations?: never[]): void;
|
@@ -0,0 +1,12 @@
|
|
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 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);
|
11
|
+
}
|
12
|
+
exports.default = useEager;
|
data/client/lib/useJason.test.js
CHANGED
@@ -81,5 +81,37 @@ test('pruning IDs', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
81
81
|
idx: 2
|
82
82
|
});
|
83
83
|
// The ID 4 should have been pruned
|
84
|
-
expect(store.getState().posts.ids).toStrictEqual([5]);
|
84
|
+
expect(store.getState().posts.ids).toStrictEqual(['5']);
|
85
|
+
}));
|
86
|
+
test('pruning IDs by destroy', () => __awaiter(void 0, void 0, void 0, function* () {
|
87
|
+
const resp = { data: {
|
88
|
+
schema: { post: {} },
|
89
|
+
transportService: 'action_cable'
|
90
|
+
} };
|
91
|
+
// @ts-ignore
|
92
|
+
restClient_1.default.get.mockResolvedValue(resp);
|
93
|
+
const { result, waitForNextUpdate } = react_hooks_1.renderHook(() => useJason_1.default({ reducers: {
|
94
|
+
test: (s, a) => s || {}
|
95
|
+
} }));
|
96
|
+
yield waitForNextUpdate();
|
97
|
+
const [store, value, connected] = result.current;
|
98
|
+
const { handlePayload, subscribe } = value;
|
99
|
+
const subscription = subscribe({ post: {} });
|
100
|
+
handlePayload({
|
101
|
+
type: 'payload',
|
102
|
+
model: 'post',
|
103
|
+
payload: [{ id: 4, name: 'test' }, { id: 5, name: 'test it out' }],
|
104
|
+
md5Hash: subscription.md5Hash,
|
105
|
+
idx: 1
|
106
|
+
});
|
107
|
+
expect(store.getState().posts.ids).toStrictEqual(['4', '5']);
|
108
|
+
handlePayload({
|
109
|
+
destroy: true,
|
110
|
+
model: 'post',
|
111
|
+
id: 5,
|
112
|
+
md5Hash: subscription.md5Hash,
|
113
|
+
idx: 2
|
114
|
+
});
|
115
|
+
// The ID 4 should have been pruned
|
116
|
+
expect(store.getState().posts.ids).toStrictEqual(['4']);
|
85
117
|
}));
|
data/client/package.json
CHANGED
@@ -42,18 +42,17 @@ function generateJasonSlices(models) {
|
|
42
42
|
setSubscriptionIds(s,a) {
|
43
43
|
const { payload } = a
|
44
44
|
const { subscriptionId, model, ids } = payload
|
45
|
-
|
46
|
-
s[model][subscriptionId] = ids
|
45
|
+
s[model][subscriptionId] = ids.map(id => String(id))
|
47
46
|
},
|
48
47
|
addSubscriptionId(s,a) {
|
49
48
|
const { payload } = a
|
50
49
|
const { subscriptionId, model, id } = payload
|
51
|
-
s[model][subscriptionId] = _.union(s[model][subscriptionId] || [], [id])
|
50
|
+
s[model][subscriptionId] = _.union(s[model][subscriptionId] || [], [String(id)])
|
52
51
|
},
|
53
52
|
removeSubscriptionId(s,a) {
|
54
53
|
const { payload } = a
|
55
54
|
const { subscriptionId, model, id } = payload
|
56
|
-
s[model][subscriptionId] = _.
|
55
|
+
s[model][subscriptionId] = _.difference(s[model][subscriptionId] || [], [String(id)])
|
57
56
|
},
|
58
57
|
removeSubscription(s, a) {
|
59
58
|
const { payload: { subscriptionId } } = a
|
@@ -42,14 +42,14 @@ export default function createPayloadHandler({ dispatch, serverActionQueue, tran
|
|
42
42
|
const { payload, destroy, id, type } = patchQueue[model][idx[model]]
|
43
43
|
|
44
44
|
if (type === 'payload') {
|
45
|
-
dispatch({ type: `${pluralize(model)}/upsertMany`, payload })
|
45
|
+
dispatch({ type: `${pluralize(model)}/upsertMany`, payload: payload.map(m => ({ ...m, id: String(m.id) })) })
|
46
46
|
const ids = payload.map(instance => instance.id)
|
47
47
|
dispatch({ type: `jasonModels/setSubscriptionIds`, payload: { model, subscriptionId, ids }})
|
48
48
|
} else if (destroy) {
|
49
49
|
// Middleware will determine if this model should be removed if it isn't in any other subscriptions
|
50
50
|
dispatch({ type: `jasonModels/removeSubscriptionId`, payload: { model, subscriptionId, id }})
|
51
51
|
} else {
|
52
|
-
dispatch({ type: `${pluralize(model)}/upsert`, payload })
|
52
|
+
dispatch({ type: `${pluralize(model)}/upsert`, payload: { ...payload, id: String(payload.id) } })
|
53
53
|
dispatch({ type: `jasonModels/addSubscriptionId`, payload: { model, subscriptionId, id }})
|
54
54
|
}
|
55
55
|
|
@@ -6,13 +6,14 @@ const pruneIdsMiddleware = schema => store => next => action => {
|
|
6
6
|
const result = next(action)
|
7
7
|
|
8
8
|
const state = store.getState()
|
9
|
-
if (type === 'jasonModels/setSubscriptionIds' || type === 'jasonModels/
|
9
|
+
if (type === 'jasonModels/setSubscriptionIds' || type === 'jasonModels/removeSubscriptionId') {
|
10
10
|
const { model, ids } = payload
|
11
11
|
|
12
12
|
let idsInSubs = []
|
13
13
|
_.map(state.jasonModels[model], (subscribedIds, k) => {
|
14
14
|
idsInSubs = _.union(idsInSubs, subscribedIds)
|
15
15
|
})
|
16
|
+
|
16
17
|
// Find IDs currently in Redux that aren't in any subscription
|
17
18
|
const idsToRemove = _.difference(state[pluralize(model)].ids, idsInSubs)
|
18
19
|
store.dispatch({ type: `${pluralize(model)}/removeMany`, payload: idsToRemove })
|
data/client/src/useJason.test.ts
CHANGED
@@ -83,5 +83,45 @@ test('pruning IDs', async () => {
|
|
83
83
|
})
|
84
84
|
|
85
85
|
// The ID 4 should have been pruned
|
86
|
-
expect(store.getState().posts.ids).toStrictEqual([5])
|
86
|
+
expect(store.getState().posts.ids).toStrictEqual(['5'])
|
87
|
+
})
|
88
|
+
|
89
|
+
test('pruning IDs by destroy', async () => {
|
90
|
+
const resp = { data: {
|
91
|
+
schema: { post: {} },
|
92
|
+
transportService: 'action_cable'
|
93
|
+
} };
|
94
|
+
|
95
|
+
// @ts-ignore
|
96
|
+
restClient.get.mockResolvedValue(resp);
|
97
|
+
|
98
|
+
const { result, waitForNextUpdate } = renderHook(() => useJason({ reducers: {
|
99
|
+
test: (s,a) => s || {}
|
100
|
+
}}));
|
101
|
+
|
102
|
+
await waitForNextUpdate()
|
103
|
+
const [store, value, connected] = result.current
|
104
|
+
const { handlePayload, subscribe } = value
|
105
|
+
|
106
|
+
const subscription = subscribe({ post: {} })
|
107
|
+
|
108
|
+
handlePayload({
|
109
|
+
type: 'payload',
|
110
|
+
model: 'post',
|
111
|
+
payload: [{ id: 4, name: 'test' }, { id: 5, name: 'test it out' }],
|
112
|
+
md5Hash: subscription.md5Hash,
|
113
|
+
idx: 1
|
114
|
+
})
|
115
|
+
expect(store.getState().posts.ids).toStrictEqual(['4', '5'])
|
116
|
+
|
117
|
+
handlePayload({
|
118
|
+
destroy: true,
|
119
|
+
model: 'post',
|
120
|
+
id: 5,
|
121
|
+
md5Hash: subscription.md5Hash,
|
122
|
+
idx: 2
|
123
|
+
})
|
124
|
+
|
125
|
+
// The ID 4 should have been pruned
|
126
|
+
expect(store.getState().posts.ids).toStrictEqual(['4'])
|
87
127
|
})
|
data/config/routes.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
Jason::Engine.routes.draw do
|
2
|
-
get '/api/config', to: '
|
3
|
-
post '/api/action', to: '
|
4
|
-
post '/api/create_subscription', to: '
|
5
|
-
post '/api/remove_subscription', to: '
|
6
|
-
post '/api/get_payload', to: '
|
7
|
-
post '/api/pusher/auth', to: '
|
2
|
+
get '/api/config', to: 'jason#configuration'
|
3
|
+
post '/api/action', to: 'jason#action'
|
4
|
+
post '/api/create_subscription', to: 'jason#create_subscription'
|
5
|
+
post '/api/remove_subscription', to: 'jason#remove_subscription'
|
6
|
+
post '/api/get_payload', to: 'jason#get_payload'
|
7
|
+
post '/api/pusher/auth', to: 'pusher#auth'
|
8
8
|
end
|
data/jason-rails.gemspec
CHANGED
data/lib/jason.rb
CHANGED
@@ -24,20 +24,24 @@ module Jason
|
|
24
24
|
self.mattr_accessor :pusher_region
|
25
25
|
self.mattr_accessor :pusher_channel_prefix
|
26
26
|
self.mattr_accessor :authorization_service
|
27
|
+
self.mattr_accessor :sidekiq_queue
|
27
28
|
|
28
29
|
self.schema = {}
|
29
30
|
self.transport_service = :action_cable
|
30
31
|
self.pusher_region = 'eu'
|
31
32
|
self.pusher_channel_prefix = 'jason'
|
33
|
+
self.sidekiq_queue = 'default'
|
32
34
|
|
33
35
|
def self.init
|
36
|
+
# Don't run in AR migration / generator etc.
|
37
|
+
return if $PROGRAM_NAME == '-e' || ActiveRecord::Base.connection.migration_context.needs_migration?
|
38
|
+
|
34
39
|
# Check if the schema has changed since last time app was started. If so, do some work to ensure cache contains the correct data
|
35
40
|
got_lock = $redis_jason.set('jason:schema:lock', nx: true, ex: 3600) # Basic lock mechanism for multi-process environments
|
36
41
|
return if !got_lock
|
37
42
|
|
38
43
|
previous_schema = JSON.parse($redis_jason.get('jason:last_schema') || '{}')
|
39
44
|
current_schema = Jason.schema.deep_stringify_keys.deep_transform_values { |v| v.is_a?(Symbol) ? v.to_s : v }
|
40
|
-
pp current_schema
|
41
45
|
current_schema.each do |model, config|
|
42
46
|
if config != previous_schema[model]
|
43
47
|
puts "Config changed for #{model}"
|
@@ -52,8 +56,6 @@ module Jason
|
|
52
56
|
$redis_jason.set('jason:last_schema', current_schema.to_json)
|
53
57
|
ensure
|
54
58
|
$redis_jason.del('jason:schema:lock')
|
55
|
-
|
56
|
-
previous_config = 'test'
|
57
59
|
end
|
58
60
|
|
59
61
|
|
data/lib/jason/broadcaster.rb
CHANGED
@@ -13,7 +13,8 @@ class Jason::Broadcaster
|
|
13
13
|
if Jason.transport_service == :action_cable
|
14
14
|
ActionCable.server.broadcast(channel, message)
|
15
15
|
elsif Jason.transport_service == :pusher
|
16
|
-
|
16
|
+
$redis_jason.rpush("jason:outbound_message_queue", { channel: pusher_channel_name, name: 'changed', data: message }.to_json)
|
17
|
+
Jason::OutboundMessageQueueWorker.perform_async
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|
data/lib/jason/graph_helper.rb
CHANGED
@@ -33,7 +33,8 @@ class Jason::GraphHelper
|
|
33
33
|
end
|
34
34
|
|
35
35
|
# Add and remove edges, return graph before and after
|
36
|
-
|
36
|
+
# Enforce means make the graph contain only the add_edges
|
37
|
+
def apply_update(add: nil, remove: nil, enforce: false)
|
37
38
|
add_edges = []
|
38
39
|
remove_edges = []
|
39
40
|
|
@@ -48,11 +49,21 @@ class Jason::GraphHelper
|
|
48
49
|
remove_edges += build_edges(edge_set[:model_names], edge_set[:instance_ids], include_root: false)
|
49
50
|
end
|
50
51
|
end
|
51
|
-
|
52
|
+
|
53
|
+
diff_edges_from_graph(add_edges: add_edges, remove_edges: remove_edges, enforce: enforce)
|
52
54
|
end
|
53
55
|
|
54
|
-
def diff_edges_from_graph(add_edges: [], remove_edges: [])
|
55
|
-
|
56
|
+
def diff_edges_from_graph(add_edges: [], remove_edges: [], enforce: false)
|
57
|
+
if enforce
|
58
|
+
old_edges = $redis_jason.multi do |r|
|
59
|
+
r.smembers("jason:subscriptions:#{id}:graph")
|
60
|
+
r.del("jason:subscriptions:#{id}:graph")
|
61
|
+
r.sadd("jason:subscriptions:#{id}:graph", add_edges) if add_edges.present?
|
62
|
+
end[0]
|
63
|
+
new_edges = add_edges
|
64
|
+
else
|
65
|
+
old_edges, new_edges = Jason::LuaGenerator.new.update_set_with_diff("jason:subscriptions:#{id}:graph", add_edges.flatten, remove_edges.flatten)
|
66
|
+
end
|
56
67
|
|
57
68
|
old_graph = build_graph_from_edges(old_edges)
|
58
69
|
new_graph = build_graph_from_edges(new_edges)
|
data/lib/jason/publisher.rb
CHANGED
@@ -42,7 +42,9 @@ module Jason::Publisher
|
|
42
42
|
# - TODO: The value of an instance changes so that it enters/leaves a subscription
|
43
43
|
|
44
44
|
# TODO: Optimize this, by caching associations rather than checking each time instance is saved
|
45
|
-
jason_assocs = self.class.reflect_on_all_associations(:belongs_to)
|
45
|
+
jason_assocs = self.class.reflect_on_all_associations(:belongs_to)
|
46
|
+
.reject { |assoc| assoc.polymorphic? } # Can't get the class name of a polymorphic association, by
|
47
|
+
.select { |assoc| assoc.klass.respond_to?(:has_jason?) }
|
46
48
|
jason_assocs.each do |assoc|
|
47
49
|
if previous_changes[assoc.foreign_key].present?
|
48
50
|
Jason::Subscription.update_ids(
|
data/lib/jason/subscription.rb
CHANGED
@@ -269,7 +269,8 @@ class Jason::Subscription
|
|
269
269
|
|
270
270
|
# Build the tree
|
271
271
|
id_changeset = graph_helper.apply_update({
|
272
|
-
add: [edge_set]
|
272
|
+
add: [edge_set],
|
273
|
+
enforce: enforce
|
273
274
|
})
|
274
275
|
apply_id_changeset(id_changeset)
|
275
276
|
end
|
@@ -320,7 +321,7 @@ class Jason::Subscription
|
|
320
321
|
$redis_jason.hset("jason:consumers", consumer_id, Time.now.utc)
|
321
322
|
|
322
323
|
if before_consumer_count == 0
|
323
|
-
set_ids_for_sub_models
|
324
|
+
set_ids_for_sub_models(enforce: true)
|
324
325
|
end
|
325
326
|
end
|
326
327
|
|
@@ -375,6 +376,20 @@ class Jason::Subscription
|
|
375
376
|
}
|
376
377
|
end
|
377
378
|
|
379
|
+
# To be used as a fallback when some corruption of the subscription has taken place
|
380
|
+
def reset!(hard: false)
|
381
|
+
# Remove subscription state
|
382
|
+
if hard
|
383
|
+
clear_all_ids
|
384
|
+
$redis_jason.del("jason:subscriptions:#{id}:graph")
|
385
|
+
end
|
386
|
+
|
387
|
+
set_ids_for_sub_models(enforce: true)
|
388
|
+
includes_helper.all_models.each do |model_name|
|
389
|
+
broadcaster.broadcast(get_for_model(model_name))
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
378
393
|
def add(model_name, instance_id)
|
379
394
|
idx = $redis_jason.incr("jason:subscription:#{id}:#{model_name}:idx")
|
380
395
|
payload = JSON.parse($redis_jason.hget("jason:cache:#{model_name}", instance_id) || '{}')
|
data/lib/jason/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jason-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Rees
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-02-
|
11
|
+
date: 2021-02-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -108,6 +108,20 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sidekiq
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
111
125
|
description:
|
112
126
|
email:
|
113
127
|
- jarees@gmail.com
|
@@ -119,14 +133,16 @@ files:
|
|
119
133
|
- ".rspec"
|
120
134
|
- ".ruby-version"
|
121
135
|
- ".travis.yml"
|
136
|
+
- CHANGELOG.md
|
122
137
|
- CODE_OF_CONDUCT.md
|
123
138
|
- Gemfile
|
124
139
|
- Gemfile.lock
|
125
140
|
- LICENSE.txt
|
126
141
|
- README.md
|
127
142
|
- Rakefile
|
128
|
-
- app/controllers/jason/
|
129
|
-
- app/controllers/jason/
|
143
|
+
- app/controllers/jason/jason_controller.rb
|
144
|
+
- app/controllers/jason/pusher_controller.rb
|
145
|
+
- app/workers/jason/outbound_message_queue_worker.rb
|
130
146
|
- bin/console
|
131
147
|
- bin/setup
|
132
148
|
- client/babel.config.js
|
@@ -168,6 +184,8 @@ files:
|
|
168
184
|
- client/lib/transportAdapters/pusherAdapter.js
|
169
185
|
- client/lib/useAct.d.ts
|
170
186
|
- client/lib/useAct.js
|
187
|
+
- client/lib/useEager.d.ts
|
188
|
+
- client/lib/useEager.js
|
171
189
|
- client/lib/useJason.d.ts
|
172
190
|
- client/lib/useJason.js
|
173
191
|
- client/lib/useJason.test.d.ts
|
@@ -194,6 +212,7 @@ files:
|
|
194
212
|
- client/src/transportAdapters/actionCableAdapter.ts
|
195
213
|
- client/src/transportAdapters/pusherAdapter.ts
|
196
214
|
- client/src/useAct.ts
|
215
|
+
- client/src/useEager.ts
|
197
216
|
- client/src/useJason.test.ts
|
198
217
|
- client/src/useJason.ts
|
199
218
|
- client/src/useSub.ts
|