jason-rails 0.5.0 → 0.5.1
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.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/README.md +107 -5
- data/client/lib/restClient.d.ts +1 -1
- data/client/lib/useJason.js +19 -4
- data/client/lib/useSub.d.ts +1 -1
- data/client/lib/useSub.js +5 -3
- data/client/src/restClient.ts +1 -1
- data/client/src/useJason.ts +20 -4
- data/client/src/useSub.ts +5 -3
- data/lib/jason/channel.rb +5 -2
- data/lib/jason/publisher.rb +1 -1
- data/lib/jason/subscription.rb +10 -4
- data/lib/jason/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25daf144fbb017120604c8a458e6eaeeffc705752ae046f9df03aa7b9e0e1f77
|
4
|
+
data.tar.gz: 741129a4872a9d384018f88049ded591989bbfa23f87c6915be317008c278051
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f09a2f2f7003f4a259dcdbea1edc702f7e21a5797ed8e8bb73684997a8eccc227349d87b3db27d53a9815407408e9051fa7749002b4ab476000b4cded468dda2
|
7
|
+
data.tar.gz: 28a93aa98358bc098921442af5c583eff00b610b2be196365a47233468706dc5e42e4a96ec18aa87bfa339cd12d25c39942708b62ef05a821aa6b9a59898dd4d
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
# Jason
|
2
2
|
|
3
|
+
Jason is still in a highly experimental phase with a rapidly changing API. Production use not recommended - but please give it a try!
|
4
|
+
|
3
5
|
## The goal
|
4
6
|
|
5
7
|
I wanted:
|
6
8
|
- Automatic updates to client state based on database state
|
7
|
-
-
|
9
|
+
- Persistence to the database without many layers of passing parameters
|
8
10
|
- Redux for awesome state management
|
9
11
|
- Optimistic updates
|
10
12
|
|
@@ -24,22 +26,122 @@ gem 'jason-rails'
|
|
24
26
|
yarn add @jamesr2323/jason
|
25
27
|
```
|
26
28
|
|
29
|
+
You will also need have peer dependencies of `redux`, `react-redux` and `@reduxjs/toolkit`.
|
30
|
+
|
31
|
+
### In Rails
|
32
|
+
|
33
|
+
Include the module `Jason::Publisher` in all models you want to publish via Jason.
|
34
|
+
|
35
|
+
Create a new initializer e.g. `jason.rb` which defines your schema
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
Jason.setup do |config|
|
39
|
+
config.schema = {
|
40
|
+
post: {
|
41
|
+
subscribed_fields: [:id, :name]
|
42
|
+
},
|
43
|
+
comment: {
|
44
|
+
subscribed_fields: [:id]
|
45
|
+
},
|
46
|
+
user: {
|
47
|
+
subscribed_fields: [:id]
|
48
|
+
}
|
49
|
+
}
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
### In your frontend code
|
54
|
+
|
55
|
+
First you need to wrap your root component in a `JasonProvider`.
|
56
|
+
|
57
|
+
```
|
58
|
+
import { JasonProvider } from '@jamesr2323/jason'
|
59
|
+
|
60
|
+
return <JasonProvider>
|
61
|
+
<YourApp />
|
62
|
+
</JasonProvider>
|
63
|
+
```
|
64
|
+
|
65
|
+
This is a wrapper around `react-redux` Provider component. This accepts the following props (all optional):
|
66
|
+
|
67
|
+
- `reducers` - An object of reducers that will be included in `configureStore`. Make sure these do not conflict with the names of any of the models you are configuring for use with Jason
|
68
|
+
- `extraActions` - Extra actions you want to be available via the `useAct` hook. (See below)
|
69
|
+
This must be a function which returns an object which will be merged with the main Jason actions. The function will be passed a dispatch function, store, axios instance and the Jason actions. For example you can add actions for one of your custom slices:
|
70
|
+
```
|
71
|
+
function extraActions(dispatch, store, restClient, act) {
|
72
|
+
return {
|
73
|
+
local: {
|
74
|
+
upsert: payload => dis({ type: 'local/upsert', payload })
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
```
|
79
|
+
|
80
|
+
- `middleware` - Passed directly to `configureStore` with additional Jason middleware
|
81
|
+
|
27
82
|
## Usage
|
83
|
+
Jason provides two custom hooks to access functionality.
|
84
|
+
|
85
|
+
### useAct
|
86
|
+
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.
|
28
87
|
|
29
|
-
|
88
|
+
Example
|
89
|
+
```jsx
|
90
|
+
import React, { useState } from 'react'
|
91
|
+
import { useAct } from '@jamesr2323/jason'
|
92
|
+
|
93
|
+
export default function PostCreator() {
|
94
|
+
const act = useAct()
|
95
|
+
const [name, setName] = useState('')
|
96
|
+
|
97
|
+
function handleClick() {
|
98
|
+
act.posts.add({ name })
|
99
|
+
}
|
100
|
+
|
101
|
+
return <div>
|
102
|
+
<input value={name} onChange={e => setName(e.target.value)} />
|
103
|
+
<button onClick={handleClick}>Add</button>
|
104
|
+
</div>
|
105
|
+
}
|
106
|
+
```
|
30
107
|
|
108
|
+
### useSub
|
109
|
+
This subscribes your Redux store to a model or set of models. It will automatically unsubscribe when the component unmounts.
|
110
|
+
|
111
|
+
Example
|
112
|
+
```
|
113
|
+
import React from 'react'
|
114
|
+
import { useSelector } from 'react-redux'
|
115
|
+
import { useSub } from '@jamesr2323/jason'
|
116
|
+
import _ from 'lodash'
|
117
|
+
|
118
|
+
export default function PostsList() {
|
119
|
+
useSub({ model: 'post', includes: ['comments'] })
|
120
|
+
const posts = useSelector(s => _.values(s.posts.entities))
|
121
|
+
|
122
|
+
return <div>
|
123
|
+
{ posts.map(({ id, name }) => <div key={id}>{ name }</div>) }
|
124
|
+
</div>
|
125
|
+
}
|
126
|
+
```
|
127
|
+
|
128
|
+
|
129
|
+
## Roadmap
|
130
|
+
|
131
|
+
Development is primarily driven by the needs of projects we're using Jason in. In no particular order, being considered is:
|
132
|
+
- Failure handling - rolling back local state in case of an error on the server
|
133
|
+
- Authorization - integrating with a library like Pundit to determine who can subscribe to given state updates and perform updates on models
|
134
|
+
- 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)
|
135
|
+
- Integration with pub/sub-as-a-service tools, such as Pusher
|
31
136
|
|
32
137
|
## Development
|
33
138
|
|
34
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
35
139
|
|
36
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
37
140
|
|
38
141
|
## Contributing
|
39
142
|
|
40
143
|
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).
|
41
144
|
|
42
|
-
|
43
145
|
## License
|
44
146
|
|
45
147
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/client/lib/restClient.d.ts
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
declare const restClient:
|
1
|
+
declare const restClient: any;
|
2
2
|
export default restClient;
|
data/client/lib/useJason.js
CHANGED
@@ -37,6 +37,7 @@ function useJason({ reducers, middleware = [], extraActions }) {
|
|
37
37
|
const eager = makeEager_1.default(schema);
|
38
38
|
let payloadHandlers = {};
|
39
39
|
let configs = {};
|
40
|
+
let subOptions = {};
|
40
41
|
function handlePayload(payload) {
|
41
42
|
const { md5Hash } = payload;
|
42
43
|
const handler = payloadHandlers[md5Hash];
|
@@ -55,7 +56,7 @@ function useJason({ reducers, middleware = [], extraActions }) {
|
|
55
56
|
dispatch({ type: 'jason/upsert', payload: { connected: true } });
|
56
57
|
console.debug('Connected to ActionCable');
|
57
58
|
// When AC loses connection - all state is lost, so we need to re-initialize all subscriptions
|
58
|
-
lodash_1.default.
|
59
|
+
lodash_1.default.keys(configs).forEach(md5Hash => createSubscription(configs[md5Hash], subOptions[md5Hash]));
|
59
60
|
},
|
60
61
|
received: payload => {
|
61
62
|
handlePayload(payload);
|
@@ -67,15 +68,28 @@ function useJason({ reducers, middleware = [], extraActions }) {
|
|
67
68
|
console.warn('Disconnected from ActionCable');
|
68
69
|
}
|
69
70
|
}));
|
70
|
-
function createSubscription(config) {
|
71
|
+
function createSubscription(config, options = {}) {
|
71
72
|
// We need the hash to be consistent in Ruby / Javascript
|
72
73
|
const hashableConfig = lodash_1.default(Object.assign({ conditions: {}, includes: {} }, config)).toPairs().sortBy(0).fromPairs().value();
|
73
74
|
const md5Hash = blueimp_md5_1.default(JSON.stringify(hashableConfig));
|
74
75
|
payloadHandlers[md5Hash] = createPayloadHandler_1.default({ dispatch, serverActionQueue, subscription, config });
|
75
76
|
configs[md5Hash] = hashableConfig;
|
77
|
+
subOptions[md5Hash] = options;
|
76
78
|
setTimeout(() => subscription.send({ createSubscription: hashableConfig }), 500);
|
79
|
+
let pollInterval = null;
|
80
|
+
console.log("createSubscription", { config, options });
|
81
|
+
// This is only for debugging / dev - not prod!
|
82
|
+
// @ts-ignore
|
83
|
+
if (options.pollInterval) {
|
84
|
+
// @ts-ignore
|
85
|
+
pollInterval = setInterval(() => subscription.send({ getPayload: config, forceRefresh: true }), options.pollInterval);
|
86
|
+
}
|
77
87
|
return {
|
78
|
-
remove
|
88
|
+
remove() {
|
89
|
+
removeSubscription(hashableConfig);
|
90
|
+
if (pollInterval)
|
91
|
+
clearInterval(pollInterval);
|
92
|
+
},
|
79
93
|
md5Hash
|
80
94
|
};
|
81
95
|
}
|
@@ -84,10 +98,11 @@ function useJason({ reducers, middleware = [], extraActions }) {
|
|
84
98
|
const md5Hash = blueimp_md5_1.default(JSON.stringify(config));
|
85
99
|
delete payloadHandlers[md5Hash];
|
86
100
|
delete configs[md5Hash];
|
101
|
+
delete subOptions[md5Hash];
|
87
102
|
}
|
88
103
|
setValue({
|
89
104
|
actions: actions,
|
90
|
-
subscribe:
|
105
|
+
subscribe: createSubscription,
|
91
106
|
eager,
|
92
107
|
handlePayload
|
93
108
|
});
|
data/client/lib/useSub.d.ts
CHANGED
@@ -1 +1 @@
|
|
1
|
-
export default function useSub(config: any): void;
|
1
|
+
export default function useSub(config: any, options?: {}): void;
|
data/client/lib/useSub.js
CHANGED
@@ -5,11 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
6
|
const JasonContext_1 = __importDefault(require("./JasonContext"));
|
7
7
|
const react_1 = require("react");
|
8
|
-
function useSub(config) {
|
8
|
+
function useSub(config, options = {}) {
|
9
|
+
// useEffect uses strict equality
|
10
|
+
const configJson = JSON.stringify(config);
|
9
11
|
const subscribe = react_1.useContext(JasonContext_1.default).subscribe;
|
10
12
|
react_1.useEffect(() => {
|
11
13
|
// @ts-ignore
|
12
|
-
return subscribe(config);
|
13
|
-
}, []);
|
14
|
+
return subscribe(config, options).remove;
|
15
|
+
}, [configJson]);
|
14
16
|
}
|
15
17
|
exports.default = useSub;
|
data/client/src/restClient.ts
CHANGED
data/client/src/useJason.ts
CHANGED
@@ -45,6 +45,7 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
|
|
45
45
|
|
46
46
|
let payloadHandlers = {}
|
47
47
|
let configs = {}
|
48
|
+
let subOptions = {}
|
48
49
|
|
49
50
|
function handlePayload(payload) {
|
50
51
|
const { md5Hash } = payload
|
@@ -66,7 +67,7 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
|
|
66
67
|
console.debug('Connected to ActionCable')
|
67
68
|
|
68
69
|
// When AC loses connection - all state is lost, so we need to re-initialize all subscriptions
|
69
|
-
_.
|
70
|
+
_.keys(configs).forEach(md5Hash => createSubscription(configs[md5Hash], subOptions[md5Hash]))
|
70
71
|
},
|
71
72
|
received: payload => {
|
72
73
|
handlePayload(payload)
|
@@ -79,17 +80,31 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
|
|
79
80
|
}
|
80
81
|
}));
|
81
82
|
|
82
|
-
function createSubscription(config) {
|
83
|
+
function createSubscription(config, options = {}) {
|
83
84
|
// We need the hash to be consistent in Ruby / Javascript
|
84
85
|
const hashableConfig = _({ conditions: {}, includes: {}, ...config }).toPairs().sortBy(0).fromPairs().value()
|
85
86
|
const md5Hash = md5(JSON.stringify(hashableConfig))
|
86
87
|
payloadHandlers[md5Hash] = createPayloadHandler({ dispatch, serverActionQueue, subscription, config })
|
87
88
|
configs[md5Hash] = hashableConfig
|
89
|
+
subOptions[md5Hash] = options
|
88
90
|
|
89
91
|
setTimeout(() => subscription.send({ createSubscription: hashableConfig }), 500)
|
92
|
+
let pollInterval = null as any;
|
93
|
+
|
94
|
+
console.log("createSubscription", { config, options })
|
95
|
+
|
96
|
+
// This is only for debugging / dev - not prod!
|
97
|
+
// @ts-ignore
|
98
|
+
if (options.pollInterval) {
|
99
|
+
// @ts-ignore
|
100
|
+
pollInterval = setInterval(() => subscription.send({ getPayload: config, forceRefresh: true }), options.pollInterval)
|
101
|
+
}
|
90
102
|
|
91
103
|
return {
|
92
|
-
remove
|
104
|
+
remove() {
|
105
|
+
removeSubscription(hashableConfig)
|
106
|
+
if (pollInterval) clearInterval(pollInterval)
|
107
|
+
},
|
93
108
|
md5Hash
|
94
109
|
}
|
95
110
|
}
|
@@ -99,11 +114,12 @@ export default function useJason({ reducers, middleware = [], extraActions }: {
|
|
99
114
|
const md5Hash = md5(JSON.stringify(config))
|
100
115
|
delete payloadHandlers[md5Hash]
|
101
116
|
delete configs[md5Hash]
|
117
|
+
delete subOptions[md5Hash]
|
102
118
|
}
|
103
119
|
|
104
120
|
setValue({
|
105
121
|
actions: actions,
|
106
|
-
subscribe:
|
122
|
+
subscribe: createSubscription,
|
107
123
|
eager,
|
108
124
|
handlePayload
|
109
125
|
})
|
data/client/src/useSub.ts
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
import JasonContext from './JasonContext'
|
2
2
|
import { useContext, useEffect } from 'react'
|
3
3
|
|
4
|
-
export default function useSub(config) {
|
4
|
+
export default function useSub(config, options = {}) {
|
5
|
+
// useEffect uses strict equality
|
6
|
+
const configJson = JSON.stringify(config)
|
5
7
|
const subscribe = useContext(JasonContext).subscribe
|
6
8
|
|
7
9
|
useEffect(() => {
|
8
10
|
// @ts-ignore
|
9
|
-
return subscribe(config)
|
10
|
-
}, [])
|
11
|
+
return subscribe(config, options).remove
|
12
|
+
}, [configJson])
|
11
13
|
}
|
data/lib/jason/channel.rb
CHANGED
@@ -21,7 +21,7 @@ class Jason::Channel < ActionCable::Channel::Base
|
|
21
21
|
elsif (config = message['removeSubscription'])
|
22
22
|
remove_subscription(config)
|
23
23
|
elsif (config = message['getPayload'])
|
24
|
-
get_payload(config)
|
24
|
+
get_payload(config, message['forceRefresh'])
|
25
25
|
end
|
26
26
|
rescue => e
|
27
27
|
puts e.message
|
@@ -50,8 +50,11 @@ class Jason::Channel < ActionCable::Channel::Base
|
|
50
50
|
# TODO Stop streams
|
51
51
|
end
|
52
52
|
|
53
|
-
def get_payload(config)
|
53
|
+
def get_payload(config, force_refresh = false)
|
54
54
|
subscription = Jason::Subscription.upsert_by_config(config['model'], conditions: config['conditions'], includes: config['includes'])
|
55
|
+
if force_refresh
|
56
|
+
subscription.set_ids(enforce: true)
|
57
|
+
end
|
55
58
|
subscription.get.each do |payload|
|
56
59
|
transmit(payload) if payload.present?
|
57
60
|
end
|
data/lib/jason/publisher.rb
CHANGED
@@ -35,7 +35,7 @@ module Jason::Publisher
|
|
35
35
|
# - TODO: The value of an instance changes so that it enters/leaves a subscription
|
36
36
|
|
37
37
|
# TODO: Optimize this, by caching associations rather than checking each time instance is saved
|
38
|
-
jason_assocs = self.class.reflect_on_all_associations(:belongs_to).select { |assoc| assoc.klass.has_jason? }
|
38
|
+
jason_assocs = self.class.reflect_on_all_associations(:belongs_to).select { |assoc| assoc.klass.respond_to?(:has_jason?) }
|
39
39
|
jason_assocs.each do |assoc|
|
40
40
|
if self.previous_changes[assoc.foreign_key].present?
|
41
41
|
|
data/lib/jason/subscription.rb
CHANGED
@@ -149,16 +149,22 @@ class Jason::Subscription
|
|
149
149
|
old_ids = $redis_jason.smembers("jason:subscriptions:#{id}:ids:#{model_name}")
|
150
150
|
|
151
151
|
# Remove
|
152
|
-
|
152
|
+
ids_to_remove = old_ids - ids
|
153
|
+
if ids_to_remove.present?
|
154
|
+
$redis_jason.srem("jason:subscriptions:#{id}:ids:#{model_name}", ids_to_remove)
|
155
|
+
end
|
153
156
|
|
154
|
-
|
157
|
+
ids_to_remove.each do |instance_id|
|
155
158
|
$redis_jason.srem("jason:models:#{model_name}:#{instance_id}:subscriptions", id)
|
156
159
|
end
|
157
160
|
|
158
161
|
# Add
|
159
|
-
|
162
|
+
ids_to_add = ids - old_ids
|
163
|
+
if ids_to_add.present?
|
164
|
+
$redis_jason.sadd("jason:subscriptions:#{id}:ids:#{model_name}", ids_to_add)
|
165
|
+
end
|
160
166
|
|
161
|
-
|
167
|
+
ids_to_add.each do |instance_id|
|
162
168
|
$redis_jason.sadd("jason:models:#{model_name}:#{instance_id}:subscriptions", id)
|
163
169
|
end
|
164
170
|
end
|
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.5.
|
4
|
+
version: 0.5.1
|
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-
|
11
|
+
date: 2021-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|