jason-rails 0.5.1 → 0.6.4

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +41 -7
  4. data/app/controllers/jason/api/pusher_controller.rb +15 -0
  5. data/app/controllers/jason/api_controller.rb +46 -4
  6. data/client/lib/JasonContext.d.ts +1 -1
  7. data/client/lib/JasonContext.js +4 -1
  8. data/client/lib/JasonProvider.js +1 -1
  9. data/client/lib/createJasonReducers.js +9 -3
  10. data/client/lib/createPayloadHandler.d.ts +6 -3
  11. data/client/lib/createPayloadHandler.js +10 -6
  12. data/client/lib/createTransportAdapter.d.ts +5 -0
  13. data/client/lib/createTransportAdapter.js +20 -0
  14. data/client/lib/index.d.ts +2 -0
  15. data/client/lib/index.js +3 -1
  16. data/client/lib/makeEager.js +2 -2
  17. data/client/lib/pruneIdsMiddleware.js +9 -11
  18. data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
  19. data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
  20. data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
  21. data/client/lib/transportAdapters/pusherAdapter.js +68 -0
  22. data/client/lib/useEager.d.ts +1 -0
  23. data/client/lib/useEager.js +12 -0
  24. data/client/lib/useJason.js +14 -34
  25. data/client/lib/useJason.test.js +41 -3
  26. data/client/package.json +2 -1
  27. data/client/src/JasonContext.ts +4 -1
  28. data/client/src/JasonProvider.tsx +1 -1
  29. data/client/src/createJasonReducers.ts +9 -3
  30. data/client/src/createPayloadHandler.ts +11 -6
  31. data/client/src/createTransportAdapter.ts +13 -0
  32. data/client/src/index.ts +3 -1
  33. data/client/src/makeEager.ts +2 -2
  34. data/client/src/pruneIdsMiddleware.ts +12 -11
  35. data/client/src/restClient.ts +1 -0
  36. data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
  37. data/client/src/transportAdapters/pusherAdapter.ts +72 -0
  38. data/client/src/useEager.ts +9 -0
  39. data/client/src/useJason.test.ts +49 -3
  40. data/client/src/useJason.ts +15 -36
  41. data/client/yarn.lock +12 -0
  42. data/config/routes.rb +5 -1
  43. data/lib/jason.rb +56 -8
  44. data/lib/jason/broadcaster.rb +19 -0
  45. data/lib/jason/channel.rb +6 -2
  46. data/lib/jason/graph_helper.rb +165 -0
  47. data/lib/jason/includes_helper.rb +108 -0
  48. data/lib/jason/lua_generator.rb +23 -1
  49. data/lib/jason/publisher.rb +20 -16
  50. data/lib/jason/subscription.rb +208 -185
  51. data/lib/jason/version.rb +1 -1
  52. metadata +18 -2
@@ -0,0 +1,5 @@
1
+ export default function pusherAdapter(jasonConfig: any, handlePayload: any, dispatch: any): {
2
+ getPayload: (config: any, options: any) => void;
3
+ createSubscription: (config: any) => void;
4
+ removeSubscription: (config: any) => void;
5
+ };
@@ -0,0 +1,68 @@
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 pusher_js_1 = __importDefault(require("pusher-js"));
7
+ const restClient_1 = __importDefault(require("../restClient"));
8
+ const uuid_1 = require("uuid");
9
+ const lodash_1 = __importDefault(require("lodash"));
10
+ function pusherAdapter(jasonConfig, handlePayload, dispatch) {
11
+ let consumerId = uuid_1.v4();
12
+ const { pusherKey, pusherRegion, pusherChannelPrefix } = jasonConfig;
13
+ const pusher = new pusher_js_1.default(pusherKey, {
14
+ cluster: 'eu',
15
+ forceTLS: true,
16
+ authEndpoint: '/jason/api/pusher/auth'
17
+ });
18
+ pusher.connection.bind('state_change', ({ current }) => {
19
+ if (current === 'connected') {
20
+ dispatch({ type: 'jason/upsert', payload: { connected: true } });
21
+ }
22
+ else {
23
+ dispatch({ type: 'jason/upsert', payload: { connected: false } });
24
+ }
25
+ });
26
+ pusher.connection.bind('error', error => {
27
+ dispatch({ type: 'jason/upsert', payload: { connected: false } });
28
+ });
29
+ const configToChannel = {};
30
+ function createSubscription(config) {
31
+ restClient_1.default.post('/jason/api/create_subscription', { config, consumerId })
32
+ .then(({ data: { channelName } }) => {
33
+ configToChannel[JSON.stringify(config)] = channelName;
34
+ subscribeToChannel(channelName);
35
+ })
36
+ .catch(e => console.error(e));
37
+ }
38
+ function removeSubscription(config) {
39
+ const channelName = configToChannel[JSON.stringify(config)];
40
+ unsubscribeFromChannel(fullChannelName(channelName));
41
+ restClient_1.default.post('/jason/api/remove_subscription', { config, consumerId })
42
+ .catch(e => console.error(e));
43
+ }
44
+ function getPayload(config, options) {
45
+ restClient_1.default.post('/jason/api/get_payload', {
46
+ config,
47
+ options
48
+ })
49
+ .then(({ data }) => {
50
+ lodash_1.default.map(data, (payload, modelName) => {
51
+ handlePayload(payload);
52
+ });
53
+ })
54
+ .catch(e => console.error(e));
55
+ }
56
+ function subscribeToChannel(channelName) {
57
+ const channel = pusher.subscribe(fullChannelName(channelName));
58
+ channel.bind('changed', message => handlePayload(message));
59
+ }
60
+ function unsubscribeFromChannel(channelName) {
61
+ const channel = pusher.unsubscribe(fullChannelName(channelName));
62
+ }
63
+ function fullChannelName(channelName) {
64
+ return `private-${pusherChannelPrefix}-${channelName}`;
65
+ }
66
+ return { getPayload, createSubscription, removeSubscription };
67
+ }
68
+ exports.default = pusherAdapter;
@@ -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;
@@ -10,7 +10,7 @@ const createOptDis_1 = __importDefault(require("./createOptDis"));
10
10
  const createServerActionQueue_1 = __importDefault(require("./createServerActionQueue"));
11
11
  const restClient_1 = __importDefault(require("./restClient"));
12
12
  const pruneIdsMiddleware_1 = __importDefault(require("./pruneIdsMiddleware"));
13
- const actioncable_1 = require("@rails/actioncable");
13
+ const createTransportAdapter_1 = __importDefault(require("./createTransportAdapter"));
14
14
  const toolkit_1 = require("@reduxjs/toolkit");
15
15
  const makeEager_1 = __importDefault(require("./makeEager"));
16
16
  const humps_1 = require("humps");
@@ -20,14 +20,13 @@ const react_1 = require("react");
20
20
  function useJason({ reducers, middleware = [], extraActions }) {
21
21
  const [store, setStore] = react_1.useState(null);
22
22
  const [value, setValue] = react_1.useState(null);
23
- const [connected, setConnected] = react_1.useState(false);
24
23
  react_1.useEffect(() => {
25
- restClient_1.default.get('/jason/api/schema')
26
- .then(({ data: snakey_schema }) => {
24
+ restClient_1.default.get('/jason/api/config')
25
+ .then(({ data: jasonConfig }) => {
26
+ const { schema: snakey_schema } = jasonConfig;
27
27
  const schema = humps_1.camelizeKeys(snakey_schema);
28
28
  console.debug({ schema });
29
29
  const serverActionQueue = createServerActionQueue_1.default();
30
- const consumer = actioncable_1.createConsumer();
31
30
  const allReducers = Object.assign(Object.assign({}, reducers), createJasonReducers_1.default(schema));
32
31
  console.debug({ allReducers });
33
32
  const store = toolkit_1.configureStore({ reducer: allReducers, middleware: [...middleware, pruneIdsMiddleware_1.default(schema)] });
@@ -40,49 +39,29 @@ function useJason({ reducers, middleware = [], extraActions }) {
40
39
  let subOptions = {};
41
40
  function handlePayload(payload) {
42
41
  const { md5Hash } = payload;
43
- const handler = payloadHandlers[md5Hash];
44
- if (handler) {
45
- handler(payload);
42
+ const { handlePayload } = payloadHandlers[md5Hash];
43
+ if (handlePayload) {
44
+ handlePayload(payload);
46
45
  }
47
46
  else {
48
47
  console.warn("Payload arrived with no handler", payload, payloadHandlers);
49
48
  }
50
49
  }
51
- const subscription = (consumer.subscriptions.create({
52
- channel: 'Jason::Channel'
53
- }, {
54
- connected: () => {
55
- setConnected(true);
56
- dispatch({ type: 'jason/upsert', payload: { connected: true } });
57
- console.debug('Connected to ActionCable');
58
- // When AC loses connection - all state is lost, so we need to re-initialize all subscriptions
59
- lodash_1.default.keys(configs).forEach(md5Hash => createSubscription(configs[md5Hash], subOptions[md5Hash]));
60
- },
61
- received: payload => {
62
- handlePayload(payload);
63
- console.debug("ActionCable Payload received: ", payload);
64
- },
65
- disconnected: () => {
66
- setConnected(false);
67
- dispatch({ type: 'jason/upsert', payload: { connected: false } });
68
- console.warn('Disconnected from ActionCable');
69
- }
70
- }));
50
+ const transportAdapter = createTransportAdapter_1.default(jasonConfig, handlePayload, dispatch, () => lodash_1.default.keys(configs).forEach(md5Hash => createSubscription(configs[md5Hash], subOptions[md5Hash])));
71
51
  function createSubscription(config, options = {}) {
72
52
  // We need the hash to be consistent in Ruby / Javascript
73
53
  const hashableConfig = lodash_1.default(Object.assign({ conditions: {}, includes: {} }, config)).toPairs().sortBy(0).fromPairs().value();
74
54
  const md5Hash = blueimp_md5_1.default(JSON.stringify(hashableConfig));
75
- payloadHandlers[md5Hash] = createPayloadHandler_1.default({ dispatch, serverActionQueue, subscription, config });
55
+ payloadHandlers[md5Hash] = createPayloadHandler_1.default({ dispatch, serverActionQueue, transportAdapter, config });
76
56
  configs[md5Hash] = hashableConfig;
77
57
  subOptions[md5Hash] = options;
78
- setTimeout(() => subscription.send({ createSubscription: hashableConfig }), 500);
58
+ setTimeout(() => transportAdapter.createSubscription(hashableConfig), 500);
79
59
  let pollInterval = null;
80
- console.log("createSubscription", { config, options });
81
60
  // This is only for debugging / dev - not prod!
82
61
  // @ts-ignore
83
62
  if (options.pollInterval) {
84
63
  // @ts-ignore
85
- pollInterval = setInterval(() => subscription.send({ getPayload: config, forceRefresh: true }), options.pollInterval);
64
+ pollInterval = setInterval(() => transportAdapter.getPayload(hashableConfig, { forceRefresh: true }), options.pollInterval);
86
65
  }
87
66
  return {
88
67
  remove() {
@@ -94,8 +73,9 @@ function useJason({ reducers, middleware = [], extraActions }) {
94
73
  };
95
74
  }
96
75
  function removeSubscription(config) {
97
- subscription.send({ removeSubscription: config });
76
+ transportAdapter.removeSubscription(config);
98
77
  const md5Hash = blueimp_md5_1.default(JSON.stringify(config));
78
+ payloadHandlers[md5Hash].tearDown();
99
79
  delete payloadHandlers[md5Hash];
100
80
  delete configs[md5Hash];
101
81
  delete subOptions[md5Hash];
@@ -109,6 +89,6 @@ function useJason({ reducers, middleware = [], extraActions }) {
109
89
  setStore(store);
110
90
  });
111
91
  }, []);
112
- return [store, value, connected];
92
+ return [store, value];
113
93
  }
114
94
  exports.default = useJason;
@@ -17,7 +17,10 @@ const useJason_1 = __importDefault(require("./useJason"));
17
17
  const restClient_1 = __importDefault(require("./restClient"));
18
18
  jest.mock('./restClient');
19
19
  test('it works', () => __awaiter(void 0, void 0, void 0, function* () {
20
- const resp = { data: { post: {} } };
20
+ const resp = { data: {
21
+ schema: { post: {} },
22
+ transportService: 'action_cable'
23
+ } };
21
24
  // @ts-ignore
22
25
  restClient_1.default.get.mockResolvedValue(resp);
23
26
  const { result, waitForNextUpdate } = react_hooks_1.renderHook(() => useJason_1.default({ reducers: {
@@ -50,7 +53,10 @@ test('it works', () => __awaiter(void 0, void 0, void 0, function* () {
50
53
  });
51
54
  }));
52
55
  test('pruning IDs', () => __awaiter(void 0, void 0, void 0, function* () {
53
- const resp = { data: { post: {} } };
56
+ const resp = { data: {
57
+ schema: { post: {} },
58
+ transportService: 'action_cable'
59
+ } };
54
60
  // @ts-ignore
55
61
  restClient_1.default.get.mockResolvedValue(resp);
56
62
  const { result, waitForNextUpdate } = react_hooks_1.renderHook(() => useJason_1.default({ reducers: {
@@ -75,5 +81,37 @@ test('pruning IDs', () => __awaiter(void 0, void 0, void 0, function* () {
75
81
  idx: 2
76
82
  });
77
83
  // The ID 4 should have been pruned
78
- 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']);
79
117
  }));
data/client/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jamesr2323/jason",
3
- "version": "0.5.0",
3
+ "version": "0.6.4",
4
4
  "module": "./lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "scripts": {
@@ -16,6 +16,7 @@
16
16
  "jsonpatch": "^3.0.1",
17
17
  "lodash": "^4.17.20",
18
18
  "pluralize": "^8.0.0",
19
+ "pusher-js": "^7.0.3",
19
20
  "uuid": "^8.3.1"
20
21
  },
21
22
  "devDependencies": {
@@ -1,5 +1,8 @@
1
1
  import { createContext } from 'react'
2
+ const eager = function(entity, id, relations) {
3
+ console.error("Eager called but is not implemented")
4
+ }
2
5
 
3
- const context = createContext({ actions: {} as any, subscribe: null, eager: null })
6
+ const context = createContext({ actions: {} as any, subscribe: null, eager })
4
7
 
5
8
  export default context
@@ -4,7 +4,7 @@ import { Provider } from 'react-redux'
4
4
  import JasonContext from './JasonContext'
5
5
 
6
6
  const JasonProvider = ({ reducers, middleware, extraActions, children }: { reducers?: any, middleware?: any, extraActions?: any, children?: React.FC }) => {
7
- const [store, value, connected] = useJason({ reducers, middleware, extraActions })
7
+ const [store, value] = useJason({ reducers, middleware, extraActions })
8
8
 
9
9
  if(!(store && value)) return <div /> // Wait for async fetch of schema to complete
10
10
 
@@ -42,17 +42,23 @@ function generateJasonSlices(models) {
42
42
  setSubscriptionIds(s,a) {
43
43
  const { payload } = a
44
44
  const { subscriptionId, model, ids } = payload
45
- s[model][subscriptionId] = ids
45
+ s[model][subscriptionId] = ids.map(id => String(id))
46
46
  },
47
47
  addSubscriptionId(s,a) {
48
48
  const { payload } = a
49
49
  const { subscriptionId, model, id } = payload
50
- s[model][subscriptionId] = _.union(s[model][subscriptionId] || [], [id])
50
+ s[model][subscriptionId] = _.union(s[model][subscriptionId] || [], [String(id)])
51
51
  },
52
52
  removeSubscriptionId(s,a) {
53
53
  const { payload } = a
54
54
  const { subscriptionId, model, id } = payload
55
- s[model][subscriptionId] = _.remove(s[model][subscriptionId] || [], id)
55
+ s[model][subscriptionId] = _.difference(s[model][subscriptionId] || [], [String(id)])
56
+ },
57
+ removeSubscription(s, a) {
58
+ const { payload: { subscriptionId } } = a
59
+ _.map(models, model => {
60
+ delete s[model][subscriptionId]
61
+ })
56
62
  }
57
63
  }
58
64
  }).reducer
@@ -9,7 +9,7 @@ function diffSeconds(dt2, dt1) {
9
9
  return Math.abs(Math.round(diff))
10
10
  }
11
11
 
12
- export default function createPayloadHandler({ dispatch, serverActionQueue, subscription, config }) {
12
+ export default function createPayloadHandler({ dispatch, serverActionQueue, transportAdapter, config }) {
13
13
  const subscriptionId = uuidv4()
14
14
 
15
15
  let idx = {}
@@ -20,7 +20,7 @@ export default function createPayloadHandler({ dispatch, serverActionQueue, subs
20
20
  let checkInterval
21
21
 
22
22
  function getPayload() {
23
- setTimeout(() => subscription.send({ getPayload: config }), 1000)
23
+ setTimeout(() => transportAdapter.getPayload(config), 1000)
24
24
  }
25
25
 
26
26
  function camelizeKeys(item) {
@@ -42,14 +42,14 @@ export default function createPayloadHandler({ dispatch, serverActionQueue, subs
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
- dispatch({ type: `${pluralize(model)}/remove`, payload: id })
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
 
@@ -97,5 +97,10 @@ export default function createPayloadHandler({ dispatch, serverActionQueue, subs
97
97
 
98
98
  tGetPayload()
99
99
 
100
- return handlePayload
100
+ // Clean up after ourselves
101
+ function tearDown() {
102
+ dispatch({ type: `jasonModels/removeSubscription`, payload: { subscriptionId }})
103
+ }
104
+
105
+ return { handlePayload, tearDown }
101
106
  }
@@ -0,0 +1,13 @@
1
+ import actionCableAdapter from './transportAdapters/actionCableAdapter'
2
+ import pusherAdapter from './transportAdapters/pusherAdapter'
3
+
4
+ export default function createTransportAdapter(jasonConfig, handlePayload, dispatch, onConnect) {
5
+ const { transportService } = jasonConfig
6
+ if (transportService === 'action_cable') {
7
+ return actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnect)
8
+ } else if (transportService === 'pusher') {
9
+ return pusherAdapter(jasonConfig, handlePayload, dispatch)
10
+ } else {
11
+ throw(`Transport adapter does not exist for ${transportService}`)
12
+ }
13
+ }
data/client/src/index.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import _JasonProvider from './JasonProvider'
2
2
  import _useAct from './useAct'
3
3
  import _useSub from './useSub'
4
+ import _useEager from './useEager'
4
5
 
5
6
  export const JasonProvider = _JasonProvider
6
7
  export const useAct = _useAct
7
- export const useSub = _useSub
8
+ export const useSub = _useSub
9
+ export const useEager = _useEager
@@ -36,9 +36,9 @@ export default function (schema) {
36
36
 
37
37
  function useEager(entity, id = null, relations = []) {
38
38
  if (id) {
39
- return useSelector(s => addRelations(s, { ...s[entity].entities[String(id)] }, entity, relations))
39
+ return useSelector(s => addRelations(s, { ...s[entity].entities[String(id)] }, entity, relations), _.isEqual)
40
40
  } else {
41
- return useSelector(s => addRelations(s, _.values(s[entity].entities), entity, relations))
41
+ return useSelector(s => addRelations(s, _.values(s[entity].entities), entity, relations), _.isEqual)
42
42
  }
43
43
  }
44
44
 
@@ -2,20 +2,21 @@ import _ from 'lodash'
2
2
  import pluralize from 'pluralize'
3
3
 
4
4
  const pruneIdsMiddleware = schema => store => next => action => {
5
- const { type } = action
5
+ const { type, payload } = action
6
6
  const result = next(action)
7
+
7
8
  const state = store.getState()
8
- if (type === 'jasonModels/setSubscriptionIds') {
9
- // Check every model
10
- _.map(_.keys(schema), model => {
11
- let ids = []
12
- _.map(state.jasonModels[model], (subscribedIds, k) => {
13
- ids = _.union(ids, subscribedIds)
14
- })
15
- // Find IDs currently in Redux that aren't in any subscription
16
- const idsToRemove = _.difference(state[pluralize(model)].ids, ids)
17
- store.dispatch({ type: `${pluralize(model)}/removeMany`, payload: idsToRemove })
9
+ if (type === 'jasonModels/setSubscriptionIds' || type === 'jasonModels/removeSubscriptionId') {
10
+ const { model, ids } = payload
11
+
12
+ let idsInSubs = []
13
+ _.map(state.jasonModels[model], (subscribedIds, k) => {
14
+ idsInSubs = _.union(idsInSubs, subscribedIds)
18
15
  })
16
+
17
+ // Find IDs currently in Redux that aren't in any subscription
18
+ const idsToRemove = _.difference(state[pluralize(model)].ids, idsInSubs)
19
+ store.dispatch({ type: `${pluralize(model)}/removeMany`, payload: idsToRemove })
19
20
  }
20
21
 
21
22
  return result
@@ -4,6 +4,7 @@ import { validate as isUuid } from 'uuid'
4
4
 
5
5
  const csrfToken = (document?.querySelector("meta[name=csrf-token]") as any)?.content
6
6
  axios.defaults.headers.common['X-CSRF-Token'] = csrfToken
7
+
7
8
  const restClient = applyCaseMiddleware(axios.create() as any, {
8
9
  preservedKeys: (key) => {
9
10
  return isUuid(key)