@arrai-innovations/reactive-helpers 1.2.2 → 2.0.0

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,7 +1,8 @@
1
- import { identity } from "lodash";
2
- import { computed, effectScope, onScopeDispose, reactive, watch } from "vue";
1
+ import { cloneDeep } from "lodash";
2
+ import { computed, effectScope, reactive, toRef } from "vue";
3
3
  import { assignReactiveObject } from "../utils/assignReactiveObject";
4
4
  import { useObjectInstance } from "./objectInstance";
5
+ import { useCancellableIntent } from "../utils/cancellableIntent";
5
6
 
6
7
  export class ObjectSubscriptionError extends Error {
7
8
  constructor(message) {
@@ -10,12 +11,14 @@ export class ObjectSubscriptionError extends Error {
10
11
  }
11
12
  }
12
13
 
13
- const defaultCrud = reactive({
14
+ const defaultCrud = {
15
+ args: {},
14
16
  subscribe: undefined,
15
- });
17
+ };
16
18
 
17
- export function setObjectSubscriptionCrud({ subscribe }) {
19
+ export function setObjectSubscriptionCrud({ subscribe, args = {} }) {
18
20
  defaultCrud.subscribe = subscribe;
21
+ Object.assign(defaultCrud.args, args);
19
22
  }
20
23
 
21
24
  export function useObjectSubscriptions(subscriptionArgs) {
@@ -27,61 +30,70 @@ export function useObjectSubscriptions(subscriptionArgs) {
27
30
  }
28
31
 
29
32
  export function useObjectSubscription({ objectInstance, crudArgs, id, retrieveArgs = {} }) {
33
+ if (retrieveArgs && objectInstance) {
34
+ throw new ObjectSubscriptionError(
35
+ "Cannot use retrieveArgs and objectInstance together, set retrieveArgs on objectInstance instead"
36
+ );
37
+ }
30
38
  if (!objectInstance) {
31
- objectInstance = useObjectInstance({ crudArgs, retrieveArgs });
39
+ objectInstance = useObjectInstance({ crudArgs, id, retrieveArgs });
32
40
  }
33
41
  const state = reactive({
34
- objectSubscriptionCrud: {
42
+ crud: {
43
+ args: {},
35
44
  subscribe: undefined,
36
45
  },
37
46
  id,
38
- retrieveArgs,
39
47
  subscriptionLoading: undefined,
40
48
  subscriptionErrored: false,
41
49
  subscriptionError: null,
42
- subscribed: false,
50
+ subscribed: undefined,
43
51
  intendToSubscribe: false,
44
52
  intendToRetrieve: false,
45
53
  });
46
- assignReactiveObject(state.objectSubscriptionCrud, defaultCrud);
47
- let cancelSubscription;
54
+ // prevent linking of all instances to the same default .args object
55
+ Object.assign(state.crud, cloneDeep(defaultCrud));
56
+ if (crudArgs) {
57
+ assignReactiveObject(state.crud.args, crudArgs);
58
+ }
59
+
60
+ let subscribeIntent, retrieveIntent;
48
61
 
49
62
  function updateFromSubscription(data) {
50
- assignReactiveObject(objectInstance.state.object, data);
63
+ assignReactiveObject(state.object, data);
51
64
  }
52
65
 
53
66
  function deleteFromSubscription() {
54
- objectInstance.state.deleted = true;
55
- assignReactiveObject(objectInstance.state.object, {});
67
+ state.deleted = true;
68
+ assignReactiveObject(state.object, {});
56
69
  }
57
70
 
58
- async function publicSubscribe({ retrieve = true } = {}) {
71
+ function publicSubscribe({ retrieve = true } = {}) {
72
+ let didSubscribe = false;
59
73
  if (!state.intendToSubscribe) {
60
74
  state.intendToSubscribe = true;
75
+ didSubscribe = true;
61
76
  }
62
77
  if (retrieve) {
63
78
  if (!state.intendToRetrieve) {
64
79
  state.intendToRetrieve = true;
80
+ didSubscribe = true;
65
81
  }
66
82
  }
67
- return subscribe();
83
+ return didSubscribe;
68
84
  }
69
85
 
70
- async function subscribe() {
71
- if (cancelSubscription || state.subscribed) {
72
- throw new ObjectSubscriptionError("already subscribed.");
73
- }
74
- if (![state.id, state.retrieveArgs].every(identity)) {
75
- // delayed until stuff is true;
76
- return false;
86
+ function subscribe() {
87
+ // this function cannot be async, or the resulting promise will lose its .cancel() method
88
+ if (subscribeIntent.state.active || state.subscribed) {
89
+ return Promise.reject(new ObjectSubscriptionError("already subscribed or subscribing."));
77
90
  }
78
91
  state.subscriptionLoading = true;
79
92
  state.subscriptionErrored = false;
80
93
  state.subscriptionError = null;
81
94
  let subscribePromise;
82
- cancelSubscription = () => subscribePromise.cancel();
83
- subscribePromise = state.objectSubscriptionCrud.subscribe({
84
- crudArgs: objectInstance.state.objectInstanceCrud.args,
95
+ subscribePromise = state.crud.subscribe({
96
+ crudArgs: state.crud.args,
85
97
  id,
86
98
  retrieveArgs: state.retrieveArgs,
87
99
  callback: (data, action) => {
@@ -92,7 +104,14 @@ export function useObjectSubscription({ objectInstance, crudArgs, id, retrieveAr
92
104
  }
93
105
  },
94
106
  });
95
- return subscribePromise
107
+ let cancelSubscription = async () => {
108
+ let cancelPromise = subscribePromise.cancel();
109
+ cancelSubscription = null;
110
+ state.subscribed = false;
111
+ return cancelPromise;
112
+ };
113
+ // then/catch/finally makes a new promise, we need to make sure the cancel method lives on.
114
+ const catchPromise = subscribePromise
96
115
  .then(() => {
97
116
  state.subscribed = true;
98
117
  return Promise.resolve(true);
@@ -110,26 +129,21 @@ export function useObjectSubscription({ objectInstance, crudArgs, id, retrieveAr
110
129
  .finally(() => {
111
130
  state.subscriptionLoading = false;
112
131
  });
132
+ catchPromise.cancel = cancelSubscription;
133
+ return catchPromise;
113
134
  }
114
135
 
115
- async function publicUnsubscribe() {
136
+ function publicUnsubscribe() {
137
+ let didUnsubscribe = false;
116
138
  if (state.intendToSubscribe) {
117
139
  state.intendToSubscribe = false;
140
+ didUnsubscribe = true;
118
141
  }
119
142
  if (state.intendToRetrieve) {
120
143
  state.intendToRetrieve = false;
144
+ didUnsubscribe = true;
121
145
  }
122
- return unsubscribe();
123
- }
124
-
125
- async function unsubscribe() {
126
- if (cancelSubscription) {
127
- state.subscribed = false;
128
- let returnPromise = cancelSubscription();
129
- cancelSubscription = null;
130
- return returnPromise;
131
- }
132
- return false;
146
+ return didUnsubscribe;
133
147
  }
134
148
 
135
149
  const es = effectScope();
@@ -139,46 +153,27 @@ export function useObjectSubscription({ objectInstance, crudArgs, id, retrieveAr
139
153
  state.errored = computed(() => objectInstance.state.errored || state.subscriptionErrored);
140
154
  state.error = computed(() => objectInstance.state.error || state.subscriptionError);
141
155
 
142
- watch(
143
- [() => state.intendToSubscribe, () => state.id, () => state.retrieveArgs],
144
- async (newArgs, oldArgs) => {
145
- const everyNew = newArgs.every((e) => e);
146
- const everyOld = oldArgs.every((e) => e);
147
- if (everyOld) {
148
- await unsubscribe();
149
- }
150
- if (everyNew) {
151
- if (!cancelSubscription && !state.subscribed) {
152
- await subscribe().catch(console.error);
153
- }
154
- }
155
- },
156
- {
157
- deep: true,
158
- }
159
- );
156
+ state.retrieveArgs = computed(() => objectInstance.state.retrieveArgs);
157
+ state.object = toRef(objectInstance.state, "object");
158
+ state.deleted = toRef(objectInstance.state, "deleted");
160
159
 
161
- watch(
162
- [() => state.intendToRetrieve, () => state.id, () => state.retrieveArgs],
163
- async (newArgs) => {
164
- const everyNew = newArgs.every((e) => e);
165
- if (everyNew) {
166
- if (!objectInstance.state.loading) {
167
- await objectInstance.retrieve({ id: state.id, ...state.retrieveArgs }).catch(console.error);
168
- }
169
- }
170
- },
171
- { deep: true }
172
- );
160
+ subscribeIntent = useCancellableIntent({
161
+ awaitableWithCancel: subscribe,
162
+ watchArguments: [toRef(state, "intendToSubscribe"), toRef(state, "id"), toRef(state, "retrieveArgs")],
163
+ clearActiveOnResolved: false,
164
+ });
173
165
 
174
- onScopeDispose(async () => {
175
- await unsubscribe();
166
+ retrieveIntent = useCancellableIntent({
167
+ awaitableWithCancel: objectInstance.retrieve,
168
+ watchArguments: [toRef(state, "intendToRetrieve"), toRef(state, "id"), toRef(state, "retrieveArgs")],
176
169
  });
177
170
  });
178
171
 
179
172
  return {
180
173
  state,
181
174
  objectInstance,
175
+ subscribeIntent,
176
+ retrieveIntent,
182
177
  subscribe: publicSubscribe,
183
178
  unsubscribe: publicUnsubscribe,
184
179
  updateFromSubscription,
@@ -1,7 +1,7 @@
1
1
  import { useListInstance } from "./listInstance";
2
2
 
3
- export function usePagedListInstance({ crudArgs, defaultListArgs = {}, defaultRetrieveArgs = {} }) {
4
- const listInstance = useListInstance({ crudArgs, defaultListArgs, defaultRetrieveArgs });
3
+ export function usePagedListInstance({ crudArgs, listArgs = {}, retrieveArgs = {} }) {
4
+ const listInstance = useListInstance({ crudArgs, listArgs, retrieveArgs });
5
5
 
6
6
  listInstance.state.totalRecords = 0;
7
7
  listInstance.state.totalPages = 0;
@@ -0,0 +1,93 @@
1
+ import { effectScope, isReactive, isRef, onScopeDispose, reactive, unref, watch } from "vue";
2
+ import { cloneDeep, isEqual } from "lodash";
3
+
4
+ /*
5
+ * Calls your awaitable function with the arguments you pass in, when the watch arguments change and are all truthy.
6
+ * Watch arguments can be an array or an object.
7
+ * If the promise is not resolved before the watch arguments change again, the previous promise is cancelled.
8
+ */
9
+ export function useCancellableIntent({ awaitableWithCancel, watchArguments = {}, clearActiveOnResolved = true }) {
10
+ if (!awaitableWithCancel) {
11
+ throw new Error("awaitableWithCancel is required");
12
+ }
13
+ if (typeof awaitableWithCancel !== "function") {
14
+ throw new Error("awaitableWithCancel must be a function");
15
+ }
16
+ const state = reactive({
17
+ active: undefined,
18
+ errored: false,
19
+ error: null,
20
+ clearActiveOnResolved,
21
+ });
22
+ let previousWatchArguments = null,
23
+ cancelFunction = null;
24
+
25
+ const es = effectScope();
26
+
27
+ function stop() {
28
+ // effect scopes are stopped automatically in onUnmounted / a parent onScopeDispose; this is for other use cases
29
+ es.stop();
30
+ }
31
+
32
+ async function cancel() {
33
+ if (cancelFunction) {
34
+ state.active = false;
35
+ const cancelPromise = cancelFunction().catch(console.error);
36
+ cancelFunction = null;
37
+ return cancelPromise;
38
+ }
39
+ return false;
40
+ }
41
+
42
+ es.run(() => {
43
+ watch(
44
+ isReactive(watchArguments) || isRef(watchArguments) ? watchArguments : Object.values(watchArguments),
45
+ () => {
46
+ let newArguments = cloneDeep(watchArguments.map((arg) => unref(arg)));
47
+ if (isEqual(unref(watchArguments), previousWatchArguments)) {
48
+ return;
49
+ }
50
+ previousWatchArguments = newArguments;
51
+ cancel().catch(console.error);
52
+ if (Object.values(previousWatchArguments).every((v) => unref(v))) {
53
+ state.errored = false;
54
+ state.error = null;
55
+ let awaitablePromise = awaitableWithCancel();
56
+ state.active = true;
57
+ if (awaitablePromise.cancel) {
58
+ cancelFunction = async () => {
59
+ state.active = false;
60
+ cancelFunction = null;
61
+ return awaitablePromise.cancel();
62
+ };
63
+ }
64
+ awaitablePromise
65
+ .then(() => {
66
+ if (state.clearActiveOnResolved) {
67
+ cancelFunction = null;
68
+ state.active = false;
69
+ }
70
+ })
71
+ .catch(async (err) => {
72
+ await cancel();
73
+ console.error(err);
74
+ state.errored = true;
75
+ state.error = err;
76
+ throw err;
77
+ });
78
+ }
79
+ },
80
+ {
81
+ deep: true,
82
+ immediate: true,
83
+ }
84
+ );
85
+
86
+ onScopeDispose(cancel);
87
+ });
88
+ return {
89
+ state,
90
+ stop,
91
+ cancel,
92
+ };
93
+ }
package/utils/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./assignReactiveObject";
2
+ export * from "./cancellableIntent";
2
3
  export * from "./getFakeId";
3
4
  export * from "./keyDiff";
4
5
  export * from "./set";