@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.
- package/.lintstagedrc +3 -0
- package/README.md +21 -16
- package/package.json +1 -1
- package/tests/unit/crudPromise.js +23 -0
- package/tests/unit/poll.js +50 -0
- package/tests/unit/use/listInstance.spec.js +47 -89
- package/tests/unit/use/listSort.spec.js +1 -1
- package/tests/unit/use/listSubscription.spec.js +184 -104
- package/tests/unit/use/objectInstance.spec.js +156 -147
- package/tests/unit/use/objectSubscription.spec.js +144 -120
- package/tests/unit/use/watches.spec.js +2 -2
- package/use/listInstance.js +39 -28
- package/use/listSubscription.js +80 -34
- package/use/objectInstance.js +36 -45
- package/use/objectSubscription.js +66 -71
- package/use/paginatedListInstance.js +2 -2
- package/utils/cancellableIntent.js +93 -0
- package/utils/index.js +1 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { computed, effectScope,
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
50
|
+
subscribed: undefined,
|
|
43
51
|
intendToSubscribe: false,
|
|
44
52
|
intendToRetrieve: false,
|
|
45
53
|
});
|
|
46
|
-
|
|
47
|
-
|
|
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(
|
|
63
|
+
assignReactiveObject(state.object, data);
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
function deleteFromSubscription() {
|
|
54
|
-
|
|
55
|
-
assignReactiveObject(
|
|
67
|
+
state.deleted = true;
|
|
68
|
+
assignReactiveObject(state.object, {});
|
|
56
69
|
}
|
|
57
70
|
|
|
58
|
-
|
|
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
|
|
83
|
+
return didSubscribe;
|
|
68
84
|
}
|
|
69
85
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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,
|
|
4
|
-
const listInstance = useListInstance({ crudArgs,
|
|
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
|
+
}
|