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 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