jason-rails 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e6d1d960c2bb555ccaa555a17de7730ce90c35aa57594256bcb73bc96dade09
4
- data.tar.gz: b096c733bd15195ac383097a8138507915caa5fba29d9099325755972a64fe2a
3
+ metadata.gz: 25daf144fbb017120604c8a458e6eaeeffc705752ae046f9df03aa7b9e0e1f77
4
+ data.tar.gz: 741129a4872a9d384018f88049ded591989bbfa23f87c6915be317008c278051
5
5
  SHA512:
6
- metadata.gz: 4ce914a0e658fe97051ff6a64a48a29bccde202ad3fa60bc3aadc287f4a50a4f169d647b66f456e3147da8551b5b321185caa05c9790c964202d5275b20a8732
7
- data.tar.gz: ebde6f3323cb516915e3ddea2f747595a864df2cac9f52e05c67b1320c46f20693c479d7b39b11f391638dc9e5027b92e86ad65089282c6c190b6ba4982b3de3
6
+ metadata.gz: f09a2f2f7003f4a259dcdbea1edc702f7e21a5797ed8e8bb73684997a8eccc227349d87b3db27d53a9815407408e9051fa7749002b4ab476000b4cded468dda2
7
+ data.tar.gz: 28a93aa98358bc098921442af5c583eff00b610b2be196365a47233468706dc5e42e4a96ec18aa87bfa339cd12d25c39942708b62ef05a821aa6b9a59898dd4d
data/.gitignore CHANGED
@@ -11,4 +11,7 @@
11
11
  .rspec_status
12
12
  client/node_modules/
13
13
 
14
- client/yarn-error.log
14
+ client/yarn-error.log
15
+ *.log
16
+ *.gem
17
+ .DS_Store
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
- - Automatic persistence to database
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
- ### Define your schema
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).
@@ -1,2 +1,2 @@
1
- declare const restClient: import("axios").AxiosInstance;
1
+ declare const restClient: any;
2
2
  export default restClient;
@@ -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.values(configs).forEach(config => createSubscription(config));
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: () => removeSubscription(hashableConfig),
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: config => createSubscription(config),
105
+ subscribe: createSubscription,
91
106
  eager,
92
107
  handlePayload
93
108
  });
@@ -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;
@@ -8,6 +8,6 @@ const restClient = applyCaseMiddleware(axios.create() as any, {
8
8
  preservedKeys: (key) => {
9
9
  return isUuid(key)
10
10
  }
11
- })
11
+ }) as any
12
12
 
13
13
  export default restClient
@@ -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
- _.values(configs).forEach(config => createSubscription(config))
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: () => removeSubscription(hashableConfig),
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: config => createSubscription(config),
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
@@ -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
 
@@ -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
- $redis_jason.srem("jason:subscriptions:#{id}:ids:#{model_name}", (old_ids - ids))
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
- (old_ids - ids).each do |instance_id|
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
- $redis_jason.sadd("jason:subscriptions:#{id}:ids:#{model_name}", (ids - old_ids))
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
- (ids - old_ids).each do |instance_id|
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
@@ -1,3 +1,3 @@
1
1
  module Jason
2
- VERSION = "0.5.0"
2
+ VERSION = "0.5.1"
3
3
  end
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.0
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-01-27 00:00:00.000000000 Z
11
+ date: 2021-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails