@arrai-innovations/reactive-helpers 1.2.3 → 2.0.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.
@@ -1,5 +1,5 @@
1
- import { cloneDeep, isEmpty } from "lodash";
2
- import { computed, effectScope, reactive, unref } from "vue";
1
+ import { cloneDeep } from "lodash";
2
+ import { computed, effectScope, reactive } from "vue";
3
3
  import { assignReactiveObject } from "../utils/assignReactiveObject";
4
4
  import { getFakeId } from "../utils/getFakeId";
5
5
  import inspect from "browser-util-inspect";
@@ -30,23 +30,24 @@ export function useListInstances(listInstanceArgs) {
30
30
  return instances;
31
31
  }
32
32
 
33
- export function useListInstance({ crudArgs, defaultListArgs = {}, defaultRetrieveArgs = {} }) {
33
+ export function useListInstance({ crudArgs, listArgs = {}, retrieveArgs = {} }) {
34
34
  const state = reactive({
35
- listInstanceCrud: {
35
+ crud: {
36
36
  args: {},
37
37
  list: undefined,
38
38
  },
39
- defaultRetrieveArgs,
40
- defaultListArgs,
39
+ retrieveArgs,
40
+ listArgs,
41
41
  objects: {},
42
42
  loading: undefined,
43
43
  errored: false,
44
44
  error: null,
45
45
  order: [],
46
46
  });
47
- Object.assign(state.listInstanceCrud, cloneDeep(defaultCrud));
47
+ // prevent linking of all instances to the same default .args object
48
+ Object.assign(state.crud, cloneDeep(defaultCrud));
48
49
  if (crudArgs) {
49
- assignReactiveObject(state.listInstanceCrud.args, crudArgs);
50
+ assignReactiveObject(state.crud.args, crudArgs);
50
51
  }
51
52
 
52
53
  const defaultPageCallback = (newObjects) => {
@@ -59,37 +60,47 @@ export function useListInstance({ crudArgs, defaultListArgs = {}, defaultRetriev
59
60
  });
60
61
  };
61
62
 
62
- async function list({ listArgs, retrieveArgs } = {}) {
63
+ function list() {
64
+ // this function cannot be async, or the resulting promise will lose its .cancel() method
63
65
  if (state.loading) {
64
- throw new ListError("already loading.");
65
- }
66
- if (isEmpty(retrieveArgs) && !isEmpty(unref(state.defaultRetrieveArgs))) {
67
- retrieveArgs = unref(state.defaultRetrieveArgs);
68
- }
69
- if (isEmpty(listArgs) && !isEmpty(unref(state.defaultListArgs))) {
70
- listArgs = unref(state.defaultListArgs);
66
+ return Promise.reject(new ListError("already loading."));
71
67
  }
68
+ let returnPromiseResolve;
69
+ const returnPromise = new Promise((resolve) => {
70
+ returnPromiseResolve = resolve;
71
+ });
72
72
  state.loading = true;
73
73
  state.errored = false;
74
74
  state.error = null;
75
- return state.listInstanceCrud
76
- .list({
77
- crudArgs: state.listInstanceCrud.args,
78
- retrieveArgs,
79
- listArgs,
80
- pageCallback: returnedObject.pageCallback,
81
- })
75
+ const listPromise = state.crud.list({
76
+ crudArgs: state.crud.args,
77
+ retrieveArgs: state.retrieveArgs,
78
+ listArgs: state.listArgs,
79
+ pageCallback: returnedObject.pageCallback,
80
+ });
81
+ if (listPromise.cancel) {
82
+ returnPromise.cancel = async () => {
83
+ let promise = listPromise.cancel();
84
+ state.loading = false;
85
+ returnPromiseResolve(false);
86
+ if (promise) {
87
+ await promise;
88
+ }
89
+ };
90
+ }
91
+ // the indirection of promises here is to allow us to do additional work on listPromise's cancel
92
+ listPromise
82
93
  .then(() => {
83
- return Promise.resolve(true);
94
+ state.loading = false;
95
+ returnPromiseResolve(true);
84
96
  })
85
97
  .catch((error) => {
98
+ state.loading = false;
86
99
  state.errored = true;
87
100
  state.error = error;
88
- return Promise.resolve(false);
89
- })
90
- .finally(() => {
91
- state.loading = false;
101
+ returnPromiseResolve(false);
92
102
  });
103
+ return returnPromise;
93
104
  }
94
105
 
95
106
  function addListObject(object) {
@@ -1,8 +1,9 @@
1
- import { effectScope, onScopeDispose, reactive, toRef } from "vue";
1
+ import { computed, effectScope, reactive, toRef } from "vue";
2
2
  import { useListInstance } from "./listInstance";
3
3
  import { cloneDeep, isEmpty, isObject } from "lodash";
4
4
  import { assignReactiveObject } from "../utils/assignReactiveObject";
5
5
  import inspect from "browser-util-inspect";
6
+ import { useCancellableIntent } from "../utils/cancellableIntent";
6
7
 
7
8
  export class ListSubscriptionError extends Error {
8
9
  constructor(message) {
@@ -12,11 +13,13 @@ export class ListSubscriptionError extends Error {
12
13
  }
13
14
 
14
15
  const defaultCrud = {
16
+ args: {},
15
17
  subscribe: undefined,
16
18
  };
17
19
 
18
- export function setListSubscriptionCrud({ subscribe }) {
20
+ export function setListSubscriptionCrud({ subscribe, args = {} }) {
19
21
  defaultCrud.subscribe = subscribe;
22
+ Object.assign(defaultCrud.args, args);
20
23
  }
21
24
 
22
25
  export function useListSubscriptions(args, listInstances = {}) {
@@ -27,35 +30,41 @@ export function useListSubscriptions(args, listInstances = {}) {
27
30
  return subscriptions;
28
31
  }
29
32
 
30
- export function useListSubscription({ listInstance, crudArgs, defaultListArgs, defaultRetrieveArgs }) {
33
+ export function useListSubscription({ listInstance, crudArgs, listArgs, retrieveArgs }) {
31
34
  if (!listInstance) {
32
- listInstance = useListInstance({ crudArgs, defaultListArgs, defaultRetrieveArgs });
35
+ listInstance = useListInstance({ crudArgs, listArgs, retrieveArgs });
33
36
  }
34
- let cancelSubscription = null;
37
+ let subscribeIntent, listIntent;
35
38
  const state = reactive({
36
- listSubscriptionCrud: {},
39
+ crud: {
40
+ args: {},
41
+ subscribe: undefined,
42
+ },
43
+ subscriptionLoading: undefined,
44
+ subscriptionErrored: false,
45
+ subscriptionError: null,
46
+ intendToList: false,
37
47
  intendToSubscribe: false,
38
48
  });
39
- assignReactiveObject(state.listSubscriptionCrud, cloneDeep(defaultCrud));
49
+ // prevent linking of all instances to the same default .args object
50
+ Object.assign(state.crud, cloneDeep(defaultCrud));
51
+ if (crudArgs) {
52
+ assignReactiveObject(state.crud.args, crudArgs);
53
+ }
40
54
 
41
- async function subscribe({ listArgs, retrieveArgs } = {}) {
42
- if (!listArgs) {
43
- listArgs = cloneDeep(listInstance.state.defaultListArgs);
44
- }
45
- if (!retrieveArgs) {
46
- retrieveArgs = cloneDeep(listInstance.state.defaultRetrieveArgs);
55
+ function publicSubscribe({ list = true } = {}) {
56
+ let didSubscribe = false;
57
+ if (!state.intendToSubscribe) {
58
+ state.intendToSubscribe = true;
59
+ didSubscribe = true;
47
60
  }
48
- if (!cancelSubscription) {
49
- cancelSubscription = await state.listSubscriptionCrud.subscribe({
50
- crudArgs: listInstance.state.listInstanceCrud.args,
51
- listArgs,
52
- retrieveArgs,
53
- subscriptionEventCallback,
54
- });
55
- state.subscribed = true;
56
- return true;
61
+ if (list) {
62
+ if (!state.intendToList) {
63
+ state.intendToList = true;
64
+ didSubscribe = true;
65
+ }
57
66
  }
58
- return false;
67
+ return didSubscribe;
59
68
  }
60
69
 
61
70
  function subscriptionEventCallback(data, action) {
@@ -72,14 +81,17 @@ export function useListSubscription({ listInstance, crudArgs, defaultListArgs, d
72
81
  }
73
82
  }
74
83
 
75
- async function unsubscribe() {
76
- if (cancelSubscription) {
77
- const returnValue = await cancelSubscription();
78
- state.subscribed = false;
79
- cancelSubscription = null;
80
- return returnValue;
84
+ function publicUnsubscribe() {
85
+ let didUnsubscribe = false;
86
+ if (state.intendToSubscribe) {
87
+ state.intendToSubscribe = false;
88
+ didUnsubscribe = true;
81
89
  }
82
- return false;
90
+ if (state.intendToList) {
91
+ state.intendToList = false;
92
+ didUnsubscribe = true;
93
+ }
94
+ return didUnsubscribe;
83
95
  }
84
96
 
85
97
  function addFromSubscription(data) {
@@ -128,19 +140,56 @@ export function useListSubscription({ listInstance, crudArgs, defaultListArgs, d
128
140
  const es = effectScope();
129
141
 
130
142
  es.run(() => {
143
+ state.loading = computed(() => listInstance.state.loading || state.subscriptionLoading);
144
+ state.errored = computed(() => listInstance.state.errored || state.subscriptionErrored);
145
+ state.error = computed(() => listInstance.state.error || state.subscriptionError);
146
+
147
+ state.retrieveArgs = computed(() => listInstance.state.retrieveArgs);
148
+ state.listArgs = computed(() => listInstance.state.listArgs);
131
149
  state.objects = toRef(listInstance.state, "objects");
132
150
  state.order = toRef(listInstance.state, "order");
133
151
  state.objectsInOrder = toRef(listInstance.state, "objectsInOrder");
134
- onScopeDispose(async () => {
135
- await unsubscribe();
152
+
153
+ subscribeIntent = useCancellableIntent({
154
+ awaitableWithCancel: () => {
155
+ // this function cannot be async, or the resulting promise will lose its .cancel() method
156
+ const subscribePromise = state.crud.subscribe({
157
+ crudArgs: cloneDeep(state.crud.args),
158
+ listArgs: cloneDeep(listInstance.state.listArgs),
159
+ retrieveArgs: cloneDeep(listInstance.state.retrieveArgs),
160
+ subscriptionEventCallback,
161
+ });
162
+ // catching makes a new promise, we need to make sure the cancel method lives on.
163
+ const catchPromise = subscribePromise.catch((err) => {
164
+ console.error(err);
165
+ state.subscriptionErrored = true;
166
+ state.subscriptionError = err;
167
+ });
168
+ catchPromise.cancel = subscribePromise.cancel;
169
+ return catchPromise;
170
+ },
171
+ watchArguments: [toRef(state, "intendToSubscribe"), toRef(state, "listArgs"), toRef(state, "retrieveArgs")],
172
+ clearActiveOnResolved: false,
173
+ });
174
+
175
+ state.subscribed = toRef(subscribeIntent.state, "active");
176
+
177
+ listIntent = useCancellableIntent({
178
+ awaitableWithCancel: () => {
179
+ listInstance.clearList();
180
+ return listInstance.list();
181
+ },
182
+ watchArguments: [toRef(state, "intendToList"), toRef(state, "listArgs"), toRef(state, "retrieveArgs")],
136
183
  });
137
184
  });
138
185
 
139
186
  return {
140
187
  state,
141
188
  listInstance,
142
- subscribe,
143
- unsubscribe,
189
+ listIntent,
190
+ subscribeIntent,
191
+ subscribe: publicSubscribe,
192
+ unsubscribe: publicUnsubscribe,
144
193
  effectScope: es,
145
194
  };
146
195
  }
@@ -1,5 +1,5 @@
1
- import { cloneDeep, isEmpty } from "lodash";
2
- import { effectScope, reactive, unref } from "vue";
1
+ import { cloneDeep } from "lodash";
2
+ import { effectScope, reactive } from "vue";
3
3
  import { assignReactiveObject } from "../utils/assignReactiveObject";
4
4
 
5
5
  export class ObjectError extends Error {
@@ -24,7 +24,7 @@ export function setObjectInstanceCrud({ retrieve, create, update, patch, delete:
24
24
  defaultCrud.update = update;
25
25
  defaultCrud.patch = patch;
26
26
  defaultCrud.delete = deleteFn;
27
- assignReactiveObject(defaultCrud.args, args);
27
+ Object.assign(defaultCrud.args, args);
28
28
  }
29
29
 
30
30
  export function useObjectInstances(instanceArgs) {
@@ -35,9 +35,9 @@ export function useObjectInstances(instanceArgs) {
35
35
  return instances;
36
36
  }
37
37
 
38
- export function useObjectInstance({ crudArgs, retrieveArgs }) {
38
+ export function useObjectInstance({ crudArgs, id, retrieveArgs }) {
39
39
  const state = reactive({
40
- objectInstanceCrud: {
40
+ crud: {
41
41
  args: {},
42
42
  retrieve: undefined,
43
43
  create: undefined,
@@ -46,32 +46,32 @@ export function useObjectInstance({ crudArgs, retrieveArgs }) {
46
46
  delete: undefined,
47
47
  },
48
48
  object: {},
49
- defaultRetrieveArgs: retrieveArgs,
49
+ id,
50
+ retrieveArgs,
50
51
  loading: undefined,
51
52
  errored: false,
52
53
  error: null,
53
54
  deleted: false,
54
55
  });
55
- assignReactiveObject(state.objectInstanceCrud, cloneDeep(defaultCrud));
56
+ // prevent linking of all instances to the same default .args object
57
+ Object.assign(state.crud, cloneDeep(defaultCrud));
56
58
  if (crudArgs) {
57
- assignReactiveObject(state.objectInstanceCrud.args, crudArgs);
59
+ assignReactiveObject(state.crud.args, crudArgs);
58
60
  }
59
61
 
60
- async function retrieve({ id, ...retrieveArgs }) {
62
+ function retrieve() {
63
+ // this function cannot be async, or the resulting promise will lose its .cancel() method
61
64
  if (state.loading) {
62
- throw new ObjectError("already loading.");
63
- }
64
- if (isEmpty(retrieveArgs) && !isEmpty(unref(state.defaultRetrieveArgs))) {
65
- retrieveArgs = unref(state.defaultRetrieveArgs);
65
+ return Promise.reject(new ObjectError("already loading."));
66
66
  }
67
67
  state.loading = true;
68
68
  state.errored = false;
69
69
  state.error = null;
70
- return state.objectInstanceCrud
70
+ return state.crud
71
71
  .retrieve({
72
- crudArgs: state.objectInstanceCrud.args,
73
- id,
74
- retrieveArgs,
72
+ crudArgs: state.crud.args,
73
+ id: state.id,
74
+ retrieveArgs: state.retrieveArgs,
75
75
  })
76
76
  .then((object) => {
77
77
  assignReactiveObject(state.object, object);
@@ -87,21 +87,18 @@ export function useObjectInstance({ crudArgs, retrieveArgs }) {
87
87
  });
88
88
  }
89
89
 
90
- async function create({ object, ...retrieveArgs }) {
90
+ async function create({ object }) {
91
91
  if (state.loading) {
92
92
  throw new ObjectError("already loading.");
93
93
  }
94
- if (isEmpty(retrieveArgs) && !isEmpty(unref(state.defaultRetrieveArgs))) {
95
- retrieveArgs = unref(state.defaultRetrieveArgs);
96
- }
97
94
  state.loading = true;
98
95
  state.errored = false;
99
96
  state.error = null;
100
- return state.objectInstanceCrud
97
+ return state.crud
101
98
  .create({
102
- crudArgs: state.objectInstanceCrud.args,
99
+ crudArgs: state.crud.args,
103
100
  object,
104
- retrieveArgs,
101
+ retrieveArgs: state.retrieveArgs,
105
102
  })
106
103
  .then((object) => {
107
104
  assignReactiveObject(state.object, object);
@@ -117,21 +114,18 @@ export function useObjectInstance({ crudArgs, retrieveArgs }) {
117
114
  });
118
115
  }
119
116
 
120
- async function update({ object, ...retrieveArgs }) {
117
+ async function update({ object }) {
121
118
  if (state.loading) {
122
119
  throw new ObjectError("already loading.");
123
120
  }
124
- if (isEmpty(retrieveArgs) && !isEmpty(unref(state.defaultRetrieveArgs))) {
125
- retrieveArgs = unref(state.defaultRetrieveArgs);
126
- }
127
121
  state.loading = true;
128
122
  state.errored = false;
129
123
  state.error = null;
130
- return state.objectInstanceCrud
124
+ return state.crud
131
125
  .update({
132
- crudArgs: state.objectInstanceCrud.args,
126
+ crudArgs: state.crud.args,
133
127
  object,
134
- retrieveArgs,
128
+ retrieveArgs: state.retrieveArgs,
135
129
  })
136
130
  .then((object) => {
137
131
  assignReactiveObject(state.object, object);
@@ -147,22 +141,19 @@ export function useObjectInstance({ crudArgs, retrieveArgs }) {
147
141
  });
148
142
  }
149
143
 
150
- async function patch({ id, partialObject, ...retrieveArgs }) {
144
+ async function patch({ partialObject }) {
151
145
  if (state.loading) {
152
146
  throw new ObjectError("already loading.");
153
147
  }
154
- if (isEmpty(retrieveArgs) && !isEmpty(unref(state.defaultRetrieveArgs))) {
155
- retrieveArgs = unref(state.defaultRetrieveArgs);
156
- }
157
148
  state.loading = true;
158
149
  state.errored = false;
159
150
  state.error = null;
160
- return state.objectInstanceCrud
151
+ return state.crud
161
152
  .patch({
162
- crudArgs: state.objectInstanceCrud.args,
163
- id,
153
+ crudArgs: state.crud.args,
154
+ id: state.id,
164
155
  partialObject,
165
- retrieveArgs,
156
+ retrieveArgs: state.retrieveArgs,
166
157
  })
167
158
  .then((object) => {
168
159
  assignReactiveObject(state.object, object);
@@ -178,17 +169,17 @@ export function useObjectInstance({ crudArgs, retrieveArgs }) {
178
169
  });
179
170
  }
180
171
 
181
- async function deleteFn(id) {
172
+ async function deleteFn() {
182
173
  if (state.loading) {
183
174
  throw new ObjectError("already loading.");
184
175
  }
185
176
  state.loading = true;
186
177
  state.errored = false;
187
178
  state.error = null;
188
- return state.objectInstanceCrud
179
+ return state.crud
189
180
  .delete({
190
- crudArgs: state.objectInstanceCrud.args,
191
- id,
181
+ crudArgs: state.crud.args,
182
+ id: state.id,
192
183
  })
193
184
  .then(() => {
194
185
  state.deleted = true;