@arrai-innovations/reactive-helpers 18.0.2 → 19.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.
Files changed (49) hide show
  1. package/README.md +7 -1
  2. package/config/commonCrud.js +75 -0
  3. package/config/listCrud.js +57 -56
  4. package/config/objectCrud.js +110 -77
  5. package/index.js +6 -1
  6. package/package.json +3 -2
  7. package/types/config/commonCrud.d.ts +19 -0
  8. package/types/config/commonCrud.d.ts.map +1 -0
  9. package/types/config/listCrud.d.ts +44 -39
  10. package/types/config/listCrud.d.ts.map +1 -1
  11. package/types/config/objectCrud.d.ts +102 -40
  12. package/types/config/objectCrud.d.ts.map +1 -1
  13. package/types/index.d.ts +6 -1
  14. package/types/use/cancellableIntent.d.ts +2 -13
  15. package/types/use/cancellableIntent.d.ts.map +1 -1
  16. package/types/use/listInstance.d.ts +10 -10
  17. package/types/use/listInstance.d.ts.map +1 -1
  18. package/types/use/loadingError.d.ts +30 -6
  19. package/types/use/loadingError.d.ts.map +1 -1
  20. package/types/use/objectInstance.d.ts +83 -31
  21. package/types/use/objectInstance.d.ts.map +1 -1
  22. package/types/use/objectSubscription.d.ts +33 -29
  23. package/types/use/objectSubscription.d.ts.map +1 -1
  24. package/types/use/proxyLoadingError.d.ts +8 -23
  25. package/types/use/proxyLoadingError.d.ts.map +1 -1
  26. package/types/utils/cancellableFetch.d.ts +12 -0
  27. package/types/utils/cancellableFetch.d.ts.map +1 -0
  28. package/types/utils/cancellablePromise.d.ts +40 -0
  29. package/types/utils/cancellablePromise.d.ts.map +1 -0
  30. package/types/utils/deepUnref.d.ts +26 -0
  31. package/types/utils/deepUnref.d.ts.map +1 -0
  32. package/types/utils/refIfReactive.d.ts +2 -0
  33. package/types/utils/refIfReactive.d.ts.map +1 -0
  34. package/types/utils/unwrapNested.d.ts +2 -0
  35. package/types/utils/unwrapNested.d.ts.map +1 -0
  36. package/use/cancellableIntent.js +8 -13
  37. package/use/listInstance.js +67 -68
  38. package/use/listSearch.js +1 -1
  39. package/use/loadingError.js +30 -6
  40. package/use/objectInstance.js +228 -128
  41. package/use/objectSubscription.js +81 -82
  42. package/use/proxyLoadingError.js +14 -27
  43. package/use/search.js +1 -1
  44. package/utils/cancellableFetch.js +22 -0
  45. package/utils/cancellablePromise.js +70 -0
  46. package/utils/classes.js +5 -5
  47. package/utils/deepUnref.js +28 -0
  48. package/utils/refIfReactive.js +18 -0
  49. package/utils/unwrapNested.js +25 -0
@@ -1,9 +1,11 @@
1
- import { getListCrud } from "../config/listCrud.js";
1
+ import { defaultListCrud, getListCrud } from "../config/listCrud.js";
2
2
  import { assignReactiveObject } from "../utils/assignReactiveObject.js";
3
3
  import { getFakePk } from "../utils/getFakePk.js";
4
4
  import { useLoadingError } from "./loadingError.js";
5
5
  import inspect from "browser-util-inspect";
6
- import { computed, effectScope, nextTick, reactive, readonly, ref, toRef, unref, watch } from "vue";
6
+ import { computed, effectScope, nextTick, reactive, readonly, ref, shallowReactive, toRef, unref, watch } from "vue";
7
+ import { CancellablePromise, wrapMaybeCancellable } from "../utils/cancellablePromise.js";
8
+ import { refIfReactive } from "../utils/refIfReactive.js";
7
9
 
8
10
  /**
9
11
  * A composable function for managing a list of objects.
@@ -52,13 +54,13 @@ export class ListInstanceError extends Error {
52
54
  * @typedef {object} ListInstanceOptions
53
55
  * @property {import('vue').UnwrapNestedRefs<ListInstanceProps>} props - The props for the list instance.
54
56
  * @property {object} [functions] - Default implementation are used as set by `setListCrud`.
55
- * @property {import('../config/listCrud.js').ListFn} [functions.list] - Provide the implementation for the list
57
+ * @property {import('../config/listCrud.js').CrudListFn} [functions.list] - Provide the implementation for the list
56
58
  * function.
57
- * @property {import('../config/listCrud.js').BulkDeleteFn} [functions.bulkDelete] - Provide the implementation for the bulkDelete
59
+ * @property {import('../config/listCrud.js').CrudBulkDeleteFn} [functions.bulkDelete] - Provide the implementation for the bulkDelete
58
60
  * function.
59
- * @property {import('../config/listCrud.js').ExecuteActionFn} [functions.executeAction] - Provide the implementation for the executeAction
61
+ * @property {import('../config/listCrud.js').CrudExecuteActionFn} [functions.executeAction] - Provide the implementation for the executeAction
60
62
  * function.
61
- * @property {import('../config/listCrud.js').SubscribeFn} [functions.subscribe] - Provide the implementation for the
63
+ * @property {import('../config/listCrud.js').CrudListSubscribeFn} [functions.subscribe] - Provide the implementation for the
62
64
  * subscribe function.
63
65
  * @property {boolean} keepOldPages - If true, pages will not be cleared when defaultPageCallback is called.
64
66
  */
@@ -117,7 +119,7 @@ export class ListInstanceError extends Error {
117
119
  * @property {(objectId: string) => void} deleteListObject - Deletes an object from the list by pk.
118
120
  * @property {() => void} clearList - Clears all objects and errors from the list.
119
121
  * @property {() => string} getFakePk - Generates a unique fake pk for use within the list.
120
- * @property {() => Promise<boolean>} list - Initiates a fetch to retrieve objects according to the CRUD configuration, returning a promise to a boolean indicating success.
122
+ * @property {() => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} list - Initiates a fetch to retrieve objects according to the CRUD configuration, returning a promise to a boolean indicating success.
121
123
  * @property {() => Promise<boolean>} bulkDelete - Initiates a bulk delete operation on all objects in the list, returning a promise to a boolean indicating success.
122
124
  * @property {() => Promise<object|string|false>} executeAction - Initiates an action on all objects in the list, returning the response, or false if the action failed.
123
125
  */
@@ -206,16 +208,22 @@ export function useListInstances(listInstanceArgs) {
206
208
  */
207
209
  export function useListInstance({ props, functions = {}, keepOldPages }) {
208
210
  if (!props) {
209
- throw new ListInstanceError(`useListInstance requires props`, "missing-props");
211
+ throw new ListInstanceError("useListInstance requires props", "missing-props");
210
212
  }
211
213
  if (keepOldPages === undefined) {
212
- throw new ListInstanceError(`useListInstance requires keepOldPages`, "missing-keepOldPages");
214
+ throw new ListInstanceError("useListInstance requires keepOldPages", "missing-keepOldPages");
213
215
  }
216
+ if (!props.pkKey) {
217
+ throw new ListInstanceError("useListInstance requires pkKey.", "missing-pkKey");
218
+ }
219
+
220
+ const es = effectScope();
214
221
 
215
222
  // ### touching the _objectsMap or _objectsProxy directly will not trigger reactivity ###
216
223
  const _objectsMap = new Map(); // maps are ordered, if you don't clear lists, you need to insert pages in order.
217
224
 
218
225
  // ### touching the _objectsMap or _objectsProxy directly will not trigger reactivity ###
226
+ // noinspection JSValidateTypes
219
227
  /** @type {{[key: string]: ListObject}} */
220
228
  // @ts-ignore - we are using a proxy to make this map behave like an object for reactivity
221
229
  const _objectsProxy = new Proxy(_objectsMap, {
@@ -250,7 +258,7 @@ export function useListInstance({ props, functions = {}, keepOldPages }) {
250
258
  }
251
259
  : Reflect.getOwnPropertyDescriptor(target, p); // we can't report target properties as non-existent re: proxy invariants
252
260
  },
253
- // things introspect us thing we are a map, we need to pretend to be a object
261
+ // things introspect us thing we are a map, we need to pretend to be an object
254
262
  getPrototypeOf() {
255
263
  return Object.prototype;
256
264
  },
@@ -262,31 +270,28 @@ export function useListInstance({ props, functions = {}, keepOldPages }) {
262
270
 
263
271
  // ### touching the _objectsMap or _objectsProxy directly will not trigger reactivity ###
264
272
  const state = reactive({
265
- crud: {
266
- args: {},
267
- list: undefined,
268
- bulkDelete: undefined,
269
- executeAction: undefined,
270
- },
271
- pkKey: toRef(props, "pkKey"),
272
- retrieveArgs: toRef(props, "retrieveArgs"),
273
- listArgs: toRef(props, "listArgs"),
273
+ crud: shallowReactive({
274
+ args: reactive({}),
275
+ list: defaultListCrud.list,
276
+ bulkDelete: defaultListCrud.bulkDelete,
277
+ executeAction: defaultListCrud.executeAction,
278
+ }),
279
+ pkKey: refIfReactive(props, "pkKey"),
280
+ retrieveArgs: refIfReactive(props, "retrieveArgs", {}),
281
+ listArgs: refIfReactive(props, "listArgs", {}),
274
282
  /** @type {{[key: string]: ListObject}} */
275
283
  objects: /** @type {{[key: string]: ListObject}} */ _objectsProxy,
276
284
  running: false,
277
285
  loading: loadingError.loading,
278
286
  errored: loadingError.errored,
279
287
  error: loadingError.error,
280
- /** @type {import('vue').ComputedRef<string[]>|undefined} */
281
- order: undefined,
282
- /** @type {import('vue').ComputedRef<ListObject[]>|undefined} */
283
- objectsInOrder: undefined,
288
+ order: es.run(() => computed(() => Object.keys(state.objects))),
289
+ objectsInOrder: es.run(() => computed(() => objectsInOrderRefs.value.map((ref) => unref(ref)))),
284
290
  });
285
- const es = effectScope();
286
291
 
287
292
  getListCrud(state.crud, { props, functions });
288
293
 
289
- const defaultPageCallback = (newObjects) => {
294
+ const defaultPageCallback = (/** @type {ListObject[]} */ newObjects) => {
290
295
  // with keepOldPages, you are responsible for clearing the list as needed
291
296
  if (!keepOldPages) {
292
297
  // display one page at a time, clear the list
@@ -301,7 +306,7 @@ export function useListInstance({ props, functions = {}, keepOldPages }) {
301
306
  });
302
307
  };
303
308
 
304
- /** @type {{[key: string]: import('./cancellableIntent.js').CancellablePromise|null}} */
309
+ /** @type {{[key: string]: import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean>|null}} */
305
310
  const promises = {
306
311
  list: null,
307
312
  };
@@ -323,7 +328,7 @@ export function useListInstance({ props, functions = {}, keepOldPages }) {
323
328
  loadingError.clearError();
324
329
  return Promise.resolve(true);
325
330
  })
326
- .catch((error) => {
331
+ .catch((/** @type {Error} */ error) => {
327
332
  loadingError.setError(error);
328
333
  return Promise.resolve(false);
329
334
  })
@@ -344,19 +349,25 @@ export function useListInstance({ props, functions = {}, keepOldPages }) {
344
349
  pks: Object.keys(state.objects).map(Number),
345
350
  pkKey: state.pkKey,
346
351
  })
347
- .then((responseData) => {
352
+ .then((/** @type {object|string} */ responseData) => {
348
353
  loadingError.clearError();
349
354
  return Promise.resolve(responseData);
350
355
  })
351
- .catch((error) => {
356
+ .catch((/** @type {Error} */ error) => {
352
357
  loadingError.setError(error);
353
- return Promise.resolve(false);
358
+ return Promise.resolve(null);
354
359
  })
355
360
  .finally(() => {
356
361
  loadingError.clearLoading();
357
362
  });
358
363
  }
359
364
 
365
+ /**
366
+ * Fetches a list of objects from the server, using the configured CRUD operations and arguments.
367
+ *
368
+ * @throws {ListInstanceError} If the list is already loading.
369
+ * @returns {import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} A promise that resolves to true if the list was successfully retrieved, or false if an error occurred.
370
+ */
360
371
  function list() {
361
372
  // this function cannot be async, or the resulting promise will lose its .cancel() method
362
373
  if (promises.list) {
@@ -364,13 +375,8 @@ export function useListInstance({ props, functions = {}, keepOldPages }) {
364
375
  return promises.list;
365
376
  }
366
377
  if (state.loading) {
367
- return Promise.reject(new ListInstanceError("already loading.", "already-loading"));
378
+ return CancellablePromise.reject(new ListInstanceError("already loading.", "already-loading"));
368
379
  }
369
- let returnPromiseResolve;
370
- // @ts-ignore - we are setting this in the promise
371
- promises.list = /** @type {import('./cancellableIntent.js').CancellablePromise} */ new Promise(
372
- (resolve) => (returnPromiseResolve = resolve)
373
- );
374
380
  loadingError.clearError();
375
381
  loadingError.setLoading();
376
382
  const isCancelled = ref(false);
@@ -382,35 +388,31 @@ export function useListInstance({ props, functions = {}, keepOldPages }) {
382
388
  pageCallback: returnedObject.pageCallback,
383
389
  isCancelled: readonly(isCancelled),
384
390
  });
385
-
386
- let resolveState = false;
387
- if (listPromise.cancel) {
388
- promises.list.cancel = async () => {
389
- let promise = listPromise.cancel();
390
- isCancelled.value = true;
391
- if (promise) {
392
- await promise;
393
- }
394
- loadingError.clearLoading();
395
- };
396
- }
397
- // the indirection of promises here is to allow us to do additional work on listPromise's cancel
398
- listPromise
399
- .then(() => {
400
- resolveState = true;
401
- })
402
- .catch((error) => {
403
- loadingError.setError(error);
404
- })
405
- .finally(() => {
406
- loadingError.clearLoading();
407
- returnPromiseResolve(resolveState);
408
- promises.list = null;
409
- });
391
+ promises.list = wrapMaybeCancellable(
392
+ listPromise
393
+ .then(() => {
394
+ return true;
395
+ })
396
+ .catch((/** @type {Error} */ error) => {
397
+ loadingError.setError(error);
398
+ return false;
399
+ })
400
+ .finally(() => {
401
+ loadingError.clearLoading();
402
+ promises.list = null;
403
+ }),
404
+ listPromise.cancel
405
+ ? async (/** @type {any} */ reason) => {
406
+ isCancelled.value = true;
407
+ await listPromise.cancel?.(reason);
408
+ loadingError.clearLoading();
409
+ }
410
+ : undefined
411
+ );
410
412
  return promises.list;
411
413
  }
412
414
 
413
- function addListObject(object) {
415
+ function addListObject(/** @type {ListObject} */ object) {
414
416
  if (!object[state.pkKey]) {
415
417
  throw new ListInstanceError(
416
418
  `addListObject: object missing pk(${state.pkKey}).\n${inspect(object)}`,
@@ -429,7 +431,7 @@ export function useListInstance({ props, functions = {}, keepOldPages }) {
429
431
  }
430
432
  }
431
433
 
432
- function updateListObject(object) {
434
+ function updateListObject(/** @type {ListObject} */ object) {
433
435
  if (!object[state.pkKey]) {
434
436
  throw new ListInstanceError(
435
437
  `updateListObject: object missing pk(${state.pkKey}).\n${inspect(object)}`,
@@ -448,7 +450,7 @@ export function useListInstance({ props, functions = {}, keepOldPages }) {
448
450
  }
449
451
  }
450
452
 
451
- function deleteListObject(pk) {
453
+ function deleteListObject(/** @type {string} */ pk) {
452
454
  if (!(pk in state.objects)) {
453
455
  throw new ListInstanceError(
454
456
  `deleteListObject: list missing object for removal by pk(${state.pkKey}): ${inspect(pk)}`,
@@ -479,6 +481,7 @@ export function useListInstance({ props, functions = {}, keepOldPages }) {
479
481
  toRef(state, "objects"),
480
482
  () => {
481
483
  objectsInOrderRefs.value = Object.values(state.objects).map((object) => ref(object));
484
+ // noinspection JSIgnoredPromiseFromCall
482
485
  nextTick(() => {
483
486
  if (state.running) {
484
487
  state.running = false;
@@ -490,10 +493,6 @@ export function useListInstance({ props, functions = {}, keepOldPages }) {
490
493
  deep: true,
491
494
  }
492
495
  );
493
- // @ts-ignore - we want the computed in the explicit effect scope, tsc is mad that we are 'changing' the type
494
- state.objectsInOrder = computed(() => objectsInOrderRefs.value.map((ref) => unref(ref)));
495
- // @ts-ignore - we want the computed in the explicit effect scope, tsc is mad that we are 'changing' the type
496
- state.order = computed(() => Object.keys(state.objects));
497
496
  });
498
497
 
499
498
  // This isn't a direct return because we want the live returnedObject.pageCallback in list()
package/use/listSearch.js CHANGED
@@ -15,7 +15,7 @@ import { useSearch } from "./search.js";
15
15
  import get from "lodash-es/get.js";
16
16
  import isEqual from "lodash-es/isEqual.js";
17
17
  import { computed, effectScope, onScopeDispose, reactive, readonly, ref, toRef, unref, watch } from "vue";
18
- import { deepUnref } from "vue-deepunref";
18
+ import { deepUnref } from "../utils/deepUnref.js";
19
19
  import { assignReactiveObject } from "../utils/assignReactiveObject.js";
20
20
  import { loadingCombine } from "../utils/loadingCombine.js";
21
21
 
@@ -8,13 +8,37 @@ import { readonly, ref } from "vue";
8
8
  * @typedef {() => void} ClearErrorFn - Clear the error state.
9
9
  */
10
10
 
11
+ /**
12
+ * @typedef {import("vue").Ref<boolean|undefined>} LoadingRef
13
+ */
14
+
15
+ /**
16
+ * @typedef {import("vue").Ref<Error|null>} ErrorRef
17
+ */
18
+
19
+ /**
20
+ * @typedef {import("vue").Ref<boolean>} ErroredRef
21
+ */
22
+
23
+ /**
24
+ * @typedef {Readonly<LoadingRef>} LoadingReadonlyRef
25
+ */
26
+
27
+ /**
28
+ * @typedef {Readonly<ErrorRef>} ErrorReadonlyRef
29
+ */
30
+
31
+ /**
32
+ * @typedef {Readonly<ErroredRef>} ErroredReadonlyRef
33
+ */
34
+
11
35
  /**
12
36
  * The common API for loading and error states.
13
37
  *
14
38
  * @typedef {object} LoadingErrorStatus
15
- * @property {Readonly<import("vue").Ref<boolean|undefined>>} loading - Whether the component is loading.
16
- * @property {Readonly<import("vue").Ref<Error|null>>} error - The error that occurred.
17
- * @property {Readonly<import("vue").Ref<boolean>>} errored - Whether an error has occurred.
39
+ * @property {LoadingReadonlyRef} loading - Whether the component is loading.
40
+ * @property {ErrorReadonlyRef} error - The error that occurred.
41
+ * @property {ErroredReadonlyRef} errored - Whether an error has occurred.
18
42
  * @property {ClearErrorFn} clearError - Clear the error state.
19
43
  */
20
44
 
@@ -39,11 +63,11 @@ import { readonly, ref } from "vue";
39
63
  * @returns {LoadingError} - An object containing reactive fields and actions for loading and error states.
40
64
  */
41
65
  export function useLoadingError() {
42
- /** @type {import("vue").Ref<boolean|undefined>} */
66
+ /** @type {LoadingRef} */
43
67
  const loading = ref(undefined);
44
- /** @type {import("vue").Ref<Error|null>} */
68
+ /** @type {ErrorRef} */
45
69
  const error = ref(null);
46
- /** @type {import("vue").Ref<boolean>} */
70
+ /** @type {ErroredRef} */
47
71
  const errored = ref(false);
48
72
 
49
73
  return {