jason-rails 0.5.0 → 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/Gemfile.lock +1 -1
  4. data/README.md +141 -5
  5. data/app/controllers/jason/api/pusher_controller.rb +15 -0
  6. data/app/controllers/jason/api_controller.rb +46 -4
  7. data/client/lib/JasonContext.d.ts +1 -1
  8. data/client/lib/JasonContext.js +4 -1
  9. data/client/lib/JasonProvider.js +1 -1
  10. data/client/lib/createJasonReducers.js +7 -0
  11. data/client/lib/createPayloadHandler.d.ts +6 -3
  12. data/client/lib/createPayloadHandler.js +8 -4
  13. data/client/lib/createTransportAdapter.d.ts +5 -0
  14. data/client/lib/createTransportAdapter.js +20 -0
  15. data/client/lib/index.d.ts +2 -0
  16. data/client/lib/index.js +3 -1
  17. data/client/lib/makeEager.js +2 -2
  18. data/client/lib/pruneIdsMiddleware.js +9 -11
  19. data/client/lib/restClient.d.ts +1 -1
  20. data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
  21. data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
  22. data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
  23. data/client/lib/transportAdapters/pusherAdapter.js +68 -0
  24. data/client/lib/useEager.d.ts +1 -0
  25. data/client/lib/useEager.js +12 -0
  26. data/client/lib/useJason.js +30 -35
  27. data/client/lib/useJason.test.js +8 -2
  28. data/client/lib/useSub.d.ts +1 -1
  29. data/client/lib/useSub.js +5 -3
  30. data/client/package.json +2 -1
  31. data/client/src/JasonContext.ts +4 -1
  32. data/client/src/JasonProvider.tsx +1 -1
  33. data/client/src/createJasonReducers.ts +7 -0
  34. data/client/src/createPayloadHandler.ts +9 -4
  35. data/client/src/createTransportAdapter.ts +13 -0
  36. data/client/src/index.ts +3 -1
  37. data/client/src/makeEager.ts +2 -2
  38. data/client/src/pruneIdsMiddleware.ts +11 -11
  39. data/client/src/restClient.ts +2 -1
  40. data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
  41. data/client/src/transportAdapters/pusherAdapter.ts +72 -0
  42. data/client/src/useEager.ts +9 -0
  43. data/client/src/useJason.test.ts +8 -2
  44. data/client/src/useJason.ts +31 -36
  45. data/client/src/useSub.ts +5 -3
  46. data/client/yarn.lock +12 -0
  47. data/config/routes.rb +5 -1
  48. data/lib/jason.rb +56 -8
  49. data/lib/jason/broadcaster.rb +19 -0
  50. data/lib/jason/channel.rb +10 -3
  51. data/lib/jason/graph_helper.rb +165 -0
  52. data/lib/jason/includes_helper.rb +108 -0
  53. data/lib/jason/lua_generator.rb +23 -1
  54. data/lib/jason/publisher.rb +21 -17
  55. data/lib/jason/subscription.rb +208 -179
  56. data/lib/jason/version.rb +1 -1
  57. metadata +18 -2
data/client/lib/index.js CHANGED
@@ -3,10 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.useSub = exports.useAct = exports.JasonProvider = void 0;
6
+ exports.useEager = exports.useSub = exports.useAct = exports.JasonProvider = void 0;
7
7
  const JasonProvider_1 = __importDefault(require("./JasonProvider"));
8
8
  const useAct_1 = __importDefault(require("./useAct"));
9
9
  const useSub_1 = __importDefault(require("./useSub"));
10
+ const useEager_1 = __importDefault(require("./useEager"));
10
11
  exports.JasonProvider = JasonProvider_1.default;
11
12
  exports.useAct = useAct_1.default;
12
13
  exports.useSub = useSub_1.default;
14
+ exports.useEager = useEager_1.default;
@@ -40,10 +40,10 @@ function default_1(schema) {
40
40
  }
41
41
  function useEager(entity, id = null, relations = []) {
42
42
  if (id) {
43
- return react_redux_1.useSelector(s => addRelations(s, Object.assign({}, s[entity].entities[String(id)]), entity, relations));
43
+ return react_redux_1.useSelector(s => addRelations(s, Object.assign({}, s[entity].entities[String(id)]), entity, relations), lodash_1.default.isEqual);
44
44
  }
45
45
  else {
46
- return react_redux_1.useSelector(s => addRelations(s, lodash_1.default.values(s[entity].entities), entity, relations));
46
+ return react_redux_1.useSelector(s => addRelations(s, lodash_1.default.values(s[entity].entities), entity, relations), lodash_1.default.isEqual);
47
47
  }
48
48
  }
49
49
  return useEager;
@@ -6,20 +6,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const lodash_1 = __importDefault(require("lodash"));
7
7
  const pluralize_1 = __importDefault(require("pluralize"));
8
8
  const pruneIdsMiddleware = schema => store => next => action => {
9
- const { type } = action;
9
+ const { type, payload } = action;
10
10
  const result = next(action);
11
11
  const state = store.getState();
12
- if (type === 'jasonModels/setSubscriptionIds') {
13
- // Check every model
14
- lodash_1.default.map(lodash_1.default.keys(schema), model => {
15
- let ids = [];
16
- lodash_1.default.map(state.jasonModels[model], (subscribedIds, k) => {
17
- ids = lodash_1.default.union(ids, subscribedIds);
18
- });
19
- // Find IDs currently in Redux that aren't in any subscription
20
- const idsToRemove = lodash_1.default.difference(state[pluralize_1.default(model)].ids, ids);
21
- store.dispatch({ type: `${pluralize_1.default(model)}/removeMany`, payload: idsToRemove });
12
+ if (type === 'jasonModels/setSubscriptionIds' || type === 'jasonModels/removeSubscriptionIds') {
13
+ const { model, ids } = payload;
14
+ let idsInSubs = [];
15
+ lodash_1.default.map(state.jasonModels[model], (subscribedIds, k) => {
16
+ idsInSubs = lodash_1.default.union(idsInSubs, subscribedIds);
22
17
  });
18
+ // Find IDs currently in Redux that aren't in any subscription
19
+ const idsToRemove = lodash_1.default.difference(state[pluralize_1.default(model)].ids, idsInSubs);
20
+ store.dispatch({ type: `${pluralize_1.default(model)}/removeMany`, payload: idsToRemove });
23
21
  }
24
22
  return result;
25
23
  };
@@ -1,2 +1,2 @@
1
- declare const restClient: import("axios").AxiosInstance;
1
+ declare const restClient: any;
2
2
  export default restClient;
@@ -0,0 +1,5 @@
1
+ export default function actionCableAdapter(jasonConfig: any, handlePayload: any, dispatch: any, onConnected: any): {
2
+ getPayload: (config: any, options: any) => void;
3
+ createSubscription: (config: any) => void;
4
+ removeSubscription: (config: any) => void;
5
+ };
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const actioncable_1 = require("@rails/actioncable");
4
+ function actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnected) {
5
+ const consumer = actioncable_1.createConsumer();
6
+ const subscription = (consumer.subscriptions.create({
7
+ channel: 'Jason::Channel'
8
+ }, {
9
+ connected: () => {
10
+ dispatch({ type: 'jason/upsert', payload: { connected: true } });
11
+ console.debug('Connected to ActionCable');
12
+ // When AC loses connection - all state is lost, so we need to re-initialize all subscriptions
13
+ onConnected();
14
+ },
15
+ received: payload => {
16
+ handlePayload(payload);
17
+ console.debug("ActionCable Payload received: ", payload);
18
+ },
19
+ disconnected: () => {
20
+ dispatch({ type: 'jason/upsert', payload: { connected: false } });
21
+ console.warn('Disconnected from ActionCable');
22
+ }
23
+ }));
24
+ function getPayload(config, options) {
25
+ subscription.send(Object.assign({ getPayload: config }, options));
26
+ }
27
+ function createSubscription(config) {
28
+ subscription.send({ createSubscription: config });
29
+ }
30
+ function removeSubscription(config) {
31
+ subscription.send({ removeSubscription: config });
32
+ }
33
+ return { getPayload, createSubscription, removeSubscription };
34
+ }
35
+ exports.default = actionCableAdapter;
@@ -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)] });
@@ -37,63 +36,59 @@ function useJason({ reducers, middleware = [], extraActions }) {
37
36
  const eager = makeEager_1.default(schema);
38
37
  let payloadHandlers = {};
39
38
  let configs = {};
39
+ let subOptions = {};
40
40
  function handlePayload(payload) {
41
41
  const { md5Hash } = payload;
42
- const handler = payloadHandlers[md5Hash];
43
- if (handler) {
44
- handler(payload);
42
+ const { handlePayload } = payloadHandlers[md5Hash];
43
+ if (handlePayload) {
44
+ handlePayload(payload);
45
45
  }
46
46
  else {
47
47
  console.warn("Payload arrived with no handler", payload, payloadHandlers);
48
48
  }
49
49
  }
50
- const subscription = (consumer.subscriptions.create({
51
- channel: 'Jason::Channel'
52
- }, {
53
- connected: () => {
54
- setConnected(true);
55
- dispatch({ type: 'jason/upsert', payload: { connected: true } });
56
- console.debug('Connected to ActionCable');
57
- // 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
- },
60
- received: payload => {
61
- handlePayload(payload);
62
- console.debug("ActionCable Payload received: ", payload);
63
- },
64
- disconnected: () => {
65
- setConnected(false);
66
- dispatch({ type: 'jason/upsert', payload: { connected: false } });
67
- console.warn('Disconnected from ActionCable');
68
- }
69
- }));
70
- function createSubscription(config) {
50
+ const transportAdapter = createTransportAdapter_1.default(jasonConfig, handlePayload, dispatch, () => lodash_1.default.keys(configs).forEach(md5Hash => createSubscription(configs[md5Hash], subOptions[md5Hash])));
51
+ function createSubscription(config, options = {}) {
71
52
  // We need the hash to be consistent in Ruby / Javascript
72
53
  const hashableConfig = lodash_1.default(Object.assign({ conditions: {}, includes: {} }, config)).toPairs().sortBy(0).fromPairs().value();
73
54
  const md5Hash = blueimp_md5_1.default(JSON.stringify(hashableConfig));
74
- payloadHandlers[md5Hash] = createPayloadHandler_1.default({ dispatch, serverActionQueue, subscription, config });
55
+ payloadHandlers[md5Hash] = createPayloadHandler_1.default({ dispatch, serverActionQueue, transportAdapter, config });
75
56
  configs[md5Hash] = hashableConfig;
76
- setTimeout(() => subscription.send({ createSubscription: hashableConfig }), 500);
57
+ subOptions[md5Hash] = options;
58
+ setTimeout(() => transportAdapter.createSubscription(hashableConfig), 500);
59
+ let pollInterval = null;
60
+ // This is only for debugging / dev - not prod!
61
+ // @ts-ignore
62
+ if (options.pollInterval) {
63
+ // @ts-ignore
64
+ pollInterval = setInterval(() => transportAdapter.getPayload(hashableConfig, { forceRefresh: true }), options.pollInterval);
65
+ }
77
66
  return {
78
- remove: () => removeSubscription(hashableConfig),
67
+ remove() {
68
+ removeSubscription(hashableConfig);
69
+ if (pollInterval)
70
+ clearInterval(pollInterval);
71
+ },
79
72
  md5Hash
80
73
  };
81
74
  }
82
75
  function removeSubscription(config) {
83
- subscription.send({ removeSubscription: config });
76
+ transportAdapter.removeSubscription(config);
84
77
  const md5Hash = blueimp_md5_1.default(JSON.stringify(config));
78
+ payloadHandlers[md5Hash].tearDown();
85
79
  delete payloadHandlers[md5Hash];
86
80
  delete configs[md5Hash];
81
+ delete subOptions[md5Hash];
87
82
  }
88
83
  setValue({
89
84
  actions: actions,
90
- subscribe: config => createSubscription(config),
85
+ subscribe: createSubscription,
91
86
  eager,
92
87
  handlePayload
93
88
  });
94
89
  setStore(store);
95
90
  });
96
91
  }, []);
97
- return [store, value, connected];
92
+ return [store, value];
98
93
  }
99
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: {
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jamesr2323/jason",
3
- "version": "0.5.0",
3
+ "version": "0.6.2",
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,6 +42,7 @@ function generateJasonSlices(models) {
42
42
  setSubscriptionIds(s,a) {
43
43
  const { payload } = a
44
44
  const { subscriptionId, model, ids } = payload
45
+ console.log({ initialState })
45
46
  s[model][subscriptionId] = ids
46
47
  },
47
48
  addSubscriptionId(s,a) {
@@ -53,6 +54,12 @@ function generateJasonSlices(models) {
53
54
  const { payload } = a
54
55
  const { subscriptionId, model, id } = payload
55
56
  s[model][subscriptionId] = _.remove(s[model][subscriptionId] || [], id)
57
+ },
58
+ removeSubscription(s, a) {
59
+ const { payload: { subscriptionId } } = a
60
+ _.map(models, model => {
61
+ delete s[model][subscriptionId]
62
+ })
56
63
  }
57
64
  }
58
65
  }).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) {
@@ -46,7 +46,7 @@ export default function createPayloadHandler({ dispatch, serverActionQueue, subs
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
52
  dispatch({ type: `${pluralize(model)}/upsert`, payload })
@@ -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
  }