@arrai-innovations/reactive-helpers 18.0.2 → 18.1.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.
Files changed (38) hide show
  1. package/config/listCrud.js +1 -1
  2. package/config/objectCrud.js +129 -47
  3. package/index.js +3 -0
  4. package/package.json +2 -2
  5. package/types/config/listCrud.d.ts +1 -1
  6. package/types/config/objectCrud.d.ts +99 -33
  7. package/types/config/objectCrud.d.ts.map +1 -1
  8. package/types/index.d.ts +3 -0
  9. package/types/use/cancellableIntent.d.ts +2 -13
  10. package/types/use/cancellableIntent.d.ts.map +1 -1
  11. package/types/use/listInstance.d.ts +2 -2
  12. package/types/use/listInstance.d.ts.map +1 -1
  13. package/types/use/loadingError.d.ts +30 -6
  14. package/types/use/loadingError.d.ts.map +1 -1
  15. package/types/use/objectInstance.d.ts +83 -31
  16. package/types/use/objectInstance.d.ts.map +1 -1
  17. package/types/use/objectSubscription.d.ts +33 -29
  18. package/types/use/objectSubscription.d.ts.map +1 -1
  19. package/types/use/proxyLoadingError.d.ts +8 -23
  20. package/types/use/proxyLoadingError.d.ts.map +1 -1
  21. package/types/utils/cancellableFetch.d.ts +12 -0
  22. package/types/utils/cancellableFetch.d.ts.map +1 -0
  23. package/types/utils/cancellablePromise.d.ts +40 -0
  24. package/types/utils/cancellablePromise.d.ts.map +1 -0
  25. package/types/utils/deepUnref.d.ts +26 -0
  26. package/types/utils/deepUnref.d.ts.map +1 -0
  27. package/use/cancellableIntent.js +8 -13
  28. package/use/listInstance.js +39 -40
  29. package/use/listSearch.js +1 -1
  30. package/use/loadingError.js +30 -6
  31. package/use/objectInstance.js +225 -125
  32. package/use/objectSubscription.js +81 -82
  33. package/use/proxyLoadingError.js +14 -27
  34. package/use/search.js +1 -1
  35. package/utils/cancellableFetch.js +22 -0
  36. package/utils/cancellablePromise.js +70 -0
  37. package/utils/classes.js +5 -5
  38. package/utils/deepUnref.js +28 -0
@@ -1,7 +1,8 @@
1
- import { getObjectCrud } from "../config/objectCrud.js";
1
+ import { defaultCrud, getObjectCrud } from "../config/objectCrud.js";
2
2
  import { assignReactiveObject } from "../utils/assignReactiveObject.js";
3
3
  import { useLoadingError } from "./loadingError.js";
4
- import { reactive, toRef } from "vue";
4
+ import { reactive, readonly, ref, shallowReactive, toRef } from "vue";
5
+ import { wrapMaybeCancellable } from "../utils/cancellablePromise.js";
5
6
 
6
7
  /**
7
8
  * A composition function to manage create, retrieve, update, delete, and patch operations.
@@ -12,7 +13,15 @@ import { reactive, toRef } from "vue";
12
13
  /**
13
14
  * The object being managed by the instance. Empty object is the default.
14
15
  *
15
- * @typedef {{pkKey: string, [key: string]: any}|{}} CrudObject
16
+ * @typedef {{pkKey: string, [key: string]: any}} ExistingCrudObject
17
+ */
18
+
19
+ /**
20
+ * @typedef {{[key: string]: any}} NewCrudObject
21
+ */
22
+
23
+ /**
24
+ * @typedef {ExistingCrudObject|NewCrudObject} CrudObject
16
25
  */
17
26
 
18
27
  /**
@@ -20,7 +29,7 @@ import { reactive, toRef } from "vue";
20
29
  *
21
30
  * @typedef {object} ObjectInstanceOptions
22
31
  * @property {import('vue').UnwrapNestedRefs<ObjectInstanceRawProps>} props - The reactive configuration object.
23
- * @property {import('../config/objectCrud.js').ObjectCrudFunctions} functions - An object of custom crud functions to use instead of the defaults.
32
+ * @property {import('../config/objectCrud.js').ObjectCrudFunctions} [functions] - An object of custom crud functions to use instead of the defaults.
24
33
  */
25
34
 
26
35
  /**
@@ -33,37 +42,48 @@ import { reactive, toRef } from "vue";
33
42
  * @property {import('../config/objectCrud.js').ObjectCrudArgs} crudArgs - The arguments to be passed to the crud functions.
34
43
  */
35
44
 
45
+ /**
46
+ * @typedef {object} ObjectInstanceRawStateCrud
47
+ * @property {import('vue').Reactive<import('../config/objectCrud.js').ObjectCrudArgsArgs|{}>} args - The arguments to be passed to the crud functions.
48
+ * @property {import('../config/objectCrud.js').CrudCreateFn} create - The create function.
49
+ * @property {import('../config/objectCrud.js').CrudRetrieveFn} retrieve - The retrieve function.
50
+ * @property {import('../config/objectCrud.js').CrudUpdateFn} update - The update function.
51
+ * @property {import('../config/objectCrud.js').CrudPatchFn} patch - The patch function.
52
+ * @property {import('../config/objectCrud.js').CrudDeleteFn} delete - The delete function.
53
+ * @property {import('../config/objectCrud.js').CrudSubscribeFn} subscribe - The subscribe function.
54
+ */
55
+
36
56
  /**
37
57
  * The raw state of the object instance.
38
58
  *
39
59
  * @typedef {object} ObjectInstanceRawState
40
- * @property {import('../config/objectCrud.js').ObjectCrudArgs} crud - The crud functions.
41
- * @property {string} pk - The pk of the object.
42
- * @property {string} pkKey - The pk key of the object.
43
- * @property {object} retrieveArgs - The arguments to be passed to the retrieve function.
44
- * @property {CrudObject} object - The object.
60
+ * @property {import('vue').ShallowReactive<ObjectInstanceRawStateCrud>} crud - The crud functions.
61
+ * @property {import('vue').Ref<string|undefined>} pk - The pk of the object.
62
+ * @property {import('vue').Ref<string|undefined>} pkKey - The pk key of the object.
63
+ * @property {import('vue').Ref<{[key:string]: any}>} retrieveArgs - The arguments to be passed to the retrieve function.
64
+ * @property {import('vue').Reactive<CrudObject>} object - The object.
45
65
  * @property {Readonly<import('vue').Ref<boolean>>} loading - Whether the object is loading.
46
66
  * @property {Readonly<import('vue').Ref<boolean>>} errored - Whether the object errored.
47
67
  * @property {Readonly<import('vue').Ref<Error|null>>} error - The error.
48
- * @property {Readonly<import('vue').Ref<boolean>>} deleted - Whether the object is deleted.
68
+ * @property {boolean} deleted - Whether the object is deleted.
49
69
  */
50
70
 
51
71
  /**
52
72
  * Manages a reactive state of an object including its CRUD status, loading states, and any operational errors.
53
73
  * Reactivity ensures that any changes in state immediately reflect in the UI components that depend on this state.
54
74
  *
55
- * @typedef {import('vue').UnwrapNestedRefs<ObjectInstanceRawState>} ObjectInstanceState
75
+ * @typedef {import('vue').Reactive<ObjectInstanceRawState>} ObjectInstanceState
56
76
  */
57
77
 
58
78
  /**
59
79
  * The functions available on the object instance.
60
80
  *
61
81
  * @typedef {object} ObjectInstanceFunctions
62
- * @property {(args: { object: object }) => Promise<boolean>} create - Called to turn the current object into a new object on the server.
63
- * @property {() => Promise<boolean>} retrieve - Called to retrieve the current object by pk from the server.
64
- * @property {(args: { object: CrudObject }) => Promise<boolean>} update - Called to update the current object on the server.
65
- * @property {() => Promise<boolean>} delete - Called to delete the current object on the server.
66
- * @property {(args: { partialObject: CrudObject }) => Promise<boolean>} patch - Called to patch the current object on the server.
82
+ * @property {(args: { object: object }) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean>} create - Called to turn the current object into a new object on the server.
83
+ * @property {() => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean>} retrieve - Called to retrieve the current object by pk from the server.
84
+ * @property {(args: { object: ExistingCrudObject }) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean>} update - Called to update the current object on the server.
85
+ * @property {() => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean>} delete - Called to delete the current object on the server.
86
+ * @property {(args: { partialObject: ExistingCrudObject }) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean>} patch - Called to patch the current object on the server.
67
87
  * @property {import('./loadingError.js').ClearErrorFn} clearError - Called to clear the error state.
68
88
  * @property {() => void} clear - Called to clear the object state.
69
89
  */
@@ -189,17 +209,22 @@ export function useObjectInstance({ props, functions = {} }) {
189
209
  const loadingError = useLoadingError();
190
210
  /** @type {ObjectInstanceState} */
191
211
  const state = reactive(
192
- /** @type {ObjectInstanceRawState} */ {
193
- crud: {
194
- args: {},
195
- create: undefined,
196
- retrieve: undefined,
197
- update: undefined,
198
- delete: undefined,
199
- patch: undefined,
200
- subscribe: undefined,
201
- },
202
- object: {},
212
+ /** @type {ObjectInstanceRawState} */
213
+ {
214
+ // function typing support is a lot nicer with shallow reactive
215
+ crud: shallowReactive(
216
+ /** @type {ObjectInstanceRawStateCrud} */
217
+ {
218
+ args: reactive({}),
219
+ create: defaultCrud.create,
220
+ retrieve: defaultCrud.retrieve,
221
+ update: defaultCrud.update,
222
+ delete: defaultCrud.delete,
223
+ patch: defaultCrud.patch,
224
+ subscribe: defaultCrud.subscribe,
225
+ }
226
+ ),
227
+ object: reactive({}),
203
228
  pk: toRef(props, "pk"),
204
229
  pkKey: toRef(props, "pkKey"),
205
230
  retrieveArgs: toRef(props, "retrieveArgs"),
@@ -218,6 +243,9 @@ export function useObjectInstance({ props, functions = {} }) {
218
243
  retrieve: null,
219
244
  };
220
245
 
246
+ /**
247
+ * @returns {import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} - A promise that resolves to true if the object was retrieved successfully, or false if there was an error.
248
+ */
221
249
  function retrieve() {
222
250
  // this function cannot be async, or the resulting promise will lose its .cancel() method
223
251
  if (promises.retrieve) {
@@ -225,136 +253,208 @@ export function useObjectInstance({ props, functions = {} }) {
225
253
  return promises.retrieve;
226
254
  }
227
255
  if (state.loading) {
228
- // if another operation is already in progress, return a rejected promise
229
- return Promise.reject(new ObjectError("already loading.", "already-loading"));
256
+ // if another operation is already in progress, throw an error
257
+ // we throw because we want devs to see this error in the console
258
+ // state.error should be for user facing errors, or unknown errors
259
+ throw new ObjectError("already loading.", "already-loading");
230
260
  }
231
261
  loadingError.setLoading();
232
262
  loadingError.clearError();
233
- promises.retrieve = state.crud
234
- .retrieve({
235
- crudArgs: state.crud.args,
236
- pk: state.pk,
237
- retrieveArgs: state.retrieveArgs,
238
- pkKey: state.pkKey,
239
- })
240
- .then((object) => {
241
- assignReactiveObject(state.object, object);
242
- return Promise.resolve(true);
243
- })
244
- .catch((error) => {
245
- loadingError.setError(error);
246
- return Promise.resolve(false);
247
- })
248
- .finally(() => {
249
- loadingError.clearLoading();
250
- promises.retrieve = null;
251
- });
263
+ const isCancelled = ref(false);
264
+ const retrievePromise = state.crud.retrieve({
265
+ crudArgs: state.crud.args,
266
+ pk: state.pk,
267
+ retrieveArgs: state.retrieveArgs,
268
+ pkKey: state.pkKey,
269
+ isCancelled: readonly(isCancelled),
270
+ });
271
+
272
+ promises.retrieve = wrapMaybeCancellable(
273
+ retrievePromise
274
+ .then((/** @type {ExistingCrudObject} */ object) => {
275
+ assignReactiveObject(state.object, object);
276
+ return true;
277
+ })
278
+ .catch((/** @type {Error} */ error) => {
279
+ loadingError.setError(error);
280
+ return false;
281
+ })
282
+ .finally(() => {
283
+ loadingError.clearLoading();
284
+ promises.retrieve = null;
285
+ }),
286
+ retrievePromise.cancel
287
+ ? async (/** @type {any} */ reason) => {
288
+ isCancelled.value = true;
289
+ await retrievePromise.cancel?.(reason);
290
+ loadingError.clearLoading();
291
+ }
292
+ : undefined
293
+ );
294
+
252
295
  return promises.retrieve;
253
296
  }
254
297
 
255
- async function create({ object }) {
298
+ function create({ object }) {
299
+ // this function cannot be async, or the resulting promise will lose its .cancel() method
256
300
  if (state.loading) {
301
+ // we throw because we want devs to see this error in the console
302
+ // state.error should be for user facing errors, or unknown errors
257
303
  throw new ObjectError("already loading.", "already-loading");
258
304
  }
259
305
  loadingError.setLoading();
260
306
  loadingError.clearError();
261
- return state.crud
262
- .create({
263
- crudArgs: state.crud.args,
264
- object,
265
- retrieveArgs: state.retrieveArgs,
266
- pkKey: state.pkKey,
267
- })
268
- .then((object) => {
269
- assignReactiveObject(state.object, object);
270
- return Promise.resolve(true);
271
- })
272
- .catch((error) => {
273
- loadingError.setError(error);
274
- return Promise.resolve(false);
275
- })
276
- .finally(() => {
277
- loadingError.clearLoading();
278
- });
307
+ const isCancelled = ref(false);
308
+ const createPromise = state.crud.create({
309
+ crudArgs: state.crud.args,
310
+ object,
311
+ retrieveArgs: state.retrieveArgs,
312
+ pkKey: state.pkKey,
313
+ isCancelled: readonly(isCancelled),
314
+ });
315
+
316
+ return wrapMaybeCancellable(
317
+ createPromise
318
+ .then((/** @type {ExistingCrudObject} */ object) => {
319
+ assignReactiveObject(state.object, object);
320
+ return true;
321
+ })
322
+ .catch((/** @type {Error} */ error) => {
323
+ loadingError.setError(error);
324
+ return false;
325
+ })
326
+ .finally(() => {
327
+ loadingError.clearLoading();
328
+ }),
329
+ createPromise.cancel
330
+ ? async (/** @type {any} */ reason) => {
331
+ isCancelled.value = true;
332
+ await createPromise.cancel?.(reason);
333
+ loadingError.clearLoading();
334
+ }
335
+ : undefined
336
+ );
279
337
  }
280
338
 
281
- async function update({ object }) {
339
+ function update({ object }) {
340
+ // this function cannot be async, or the resulting promise will lose its .cancel() method
282
341
  if (state.loading) {
342
+ // we throw because we want devs to see this error in the console
343
+ // state.error should be for user facing errors, or unknown errors
283
344
  throw new ObjectError("already loading.", "already-loading");
284
345
  }
285
346
  loadingError.setLoading();
286
347
  loadingError.clearError();
287
- return state.crud
288
- .update({
289
- crudArgs: state.crud.args,
290
- object,
291
- retrieveArgs: state.retrieveArgs,
292
- pkKey: state.pkKey,
293
- })
294
- .then((object) => {
295
- assignReactiveObject(state.object, object);
296
- return Promise.resolve(true);
297
- })
298
- .catch((error) => {
299
- loadingError.setError(error);
300
- return Promise.resolve(false);
301
- })
302
- .finally(() => {
303
- loadingError.clearLoading();
304
- });
348
+ const isCancelled = ref(false);
349
+ const updatePromise = state.crud.update({
350
+ crudArgs: state.crud.args,
351
+ object,
352
+ retrieveArgs: state.retrieveArgs,
353
+ pkKey: state.pkKey,
354
+ isCancelled: readonly(isCancelled),
355
+ });
356
+ return wrapMaybeCancellable(
357
+ updatePromise
358
+ .then((/** @type {ExistingCrudObject} */ object) => {
359
+ assignReactiveObject(state.object, object);
360
+ return true;
361
+ })
362
+ .catch((/** @type {Error} */ error) => {
363
+ loadingError.setError(error);
364
+ return false;
365
+ })
366
+ .finally(() => {
367
+ loadingError.clearLoading();
368
+ }),
369
+ updatePromise.cancel
370
+ ? async (/** @type {any} */ reason) => {
371
+ isCancelled.value = true;
372
+ await updatePromise.cancel?.(reason);
373
+ loadingError.clearLoading();
374
+ }
375
+ : undefined
376
+ );
305
377
  }
306
378
 
307
- async function patch({ partialObject }) {
379
+ function patch({ partialObject }) {
380
+ // this function cannot be async, or the resulting promise will lose its .cancel() method
308
381
  if (state.loading) {
382
+ // we throw because we want devs to see this error in the console
383
+ // state.error should be for user facing errors, or unknown errors
309
384
  throw new ObjectError("already loading.", "already-loading");
310
385
  }
311
386
  loadingError.setLoading();
312
387
  loadingError.clearError();
313
- return state.crud
314
- .patch({
315
- crudArgs: state.crud.args,
316
- pk: state.pk,
317
- pkKey: state.pkKey,
318
- partialObject,
319
- retrieveArgs: state.retrieveArgs,
320
- })
321
- .then((object) => {
322
- assignReactiveObject(state.object, object);
323
- return Promise.resolve(true);
324
- })
325
- .catch((error) => {
326
- loadingError.setError(error);
327
- return Promise.resolve(false);
328
- })
329
- .finally(() => {
330
- loadingError.clearLoading();
331
- });
388
+ const isCancelled = ref(false);
389
+ const patchPromise = state.crud.patch({
390
+ crudArgs: state.crud.args,
391
+ pk: state.pk,
392
+ pkKey: state.pkKey,
393
+ partialObject,
394
+ retrieveArgs: state.retrieveArgs,
395
+ isCancelled: readonly(isCancelled),
396
+ });
397
+ return wrapMaybeCancellable(
398
+ patchPromise
399
+ .then((/** @type {ExistingCrudObject} */ object) => {
400
+ assignReactiveObject(state.object, object);
401
+ return true;
402
+ })
403
+ .catch((/** @type {Error} */ error) => {
404
+ loadingError.setError(error);
405
+ return false;
406
+ })
407
+ .finally(() => {
408
+ loadingError.clearLoading();
409
+ }),
410
+ patchPromise.cancel
411
+ ? async (/** @type {any} */ reason) => {
412
+ isCancelled.value = true;
413
+ await patchPromise.cancel?.(reason);
414
+ loadingError.clearLoading();
415
+ }
416
+ : undefined
417
+ );
332
418
  }
333
419
 
334
- async function deleteFn() {
420
+ function deleteFn() {
421
+ // this function cannot be async, or the resulting promise will lose its .cancel() method
335
422
  if (state.loading) {
423
+ // we throw because we want devs to see this error in the console
424
+ // state.error should be for user facing errors, or unknown errors
336
425
  throw new ObjectError("already loading.", "already-loading");
337
426
  }
338
427
  loadingError.setLoading();
339
428
  loadingError.clearError();
340
- return state.crud
341
- .delete({
342
- crudArgs: state.crud.args,
343
- pk: state.pk,
344
- pkKey: state.pkKey,
345
- })
346
- .then(() => {
347
- state.deleted = true;
348
- assignReactiveObject(state.object, {});
349
- return Promise.resolve(true);
350
- })
351
- .catch((error) => {
352
- loadingError.setError(error);
353
- return Promise.resolve(false);
354
- })
355
- .finally(() => {
356
- loadingError.clearLoading();
357
- });
429
+ const isCancelled = ref(false);
430
+ const deletePromise = state.crud.delete({
431
+ crudArgs: state.crud.args,
432
+ pk: state.pk,
433
+ pkKey: state.pkKey,
434
+ isCancelled: readonly(isCancelled),
435
+ });
436
+ return wrapMaybeCancellable(
437
+ deletePromise
438
+ .then(() => {
439
+ state.deleted = true;
440
+ assignReactiveObject(state.object, {});
441
+ return true;
442
+ })
443
+ .catch((/** @type {Error} */ error) => {
444
+ loadingError.setError(error);
445
+ return false;
446
+ })
447
+ .finally(() => {
448
+ loadingError.clearLoading();
449
+ }),
450
+ deletePromise.cancel
451
+ ? async (/** @type {any} */ reason) => {
452
+ isCancelled.value = true;
453
+ await deletePromise.cancel?.(reason);
454
+ loadingError.clearLoading();
455
+ }
456
+ : undefined
457
+ );
358
458
  }
359
459
 
360
460
  function clear() {