@arrai-innovations/reactive-helpers 21.0.1 → 21.1.2

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 (46) hide show
  1. package/README.md +11 -0
  2. package/config/commonCrud.js +12 -0
  3. package/config/listCrud.js +21 -6
  4. package/config/objectCrud.js +53 -11
  5. package/index.js +3 -0
  6. package/package.json +7 -5
  7. package/types/config/commonCrud.d.ts +8 -0
  8. package/types/config/listCrud.d.ts +14 -6
  9. package/types/config/objectCrud.d.ts +48 -10
  10. package/types/index.d.ts +3 -0
  11. package/types/tests/unit/utils/isReactiveTyped.spec.d.ts +1 -0
  12. package/types/tests/unit/utils/refIfReactive.spec.d.ts +1 -0
  13. package/types/tests/unit/utils/toRefsIfReactive.spec.d.ts +1 -0
  14. package/types/use/cancellableIntent.d.ts +16 -12
  15. package/types/use/error.d.ts +35 -7
  16. package/types/use/listCalculated.d.ts +2 -2
  17. package/types/use/listInstance.d.ts +22 -19
  18. package/types/use/listRelated.d.ts +6 -6
  19. package/types/use/listSubscription.d.ts +12 -3
  20. package/types/use/loading.d.ts +16 -7
  21. package/types/use/loadingError.d.ts +6 -2
  22. package/types/use/objectInstance.d.ts +38 -20
  23. package/types/use/objectSubscription.d.ts +22 -5
  24. package/types/use/proxyError.d.ts +21 -18
  25. package/types/use/proxyLoading.d.ts +18 -8
  26. package/types/use/proxyLoadingError.d.ts +20 -5
  27. package/types/utils/isReactiveTyped.d.ts +6 -0
  28. package/types/utils/refIfReactive.d.ts +2 -1
  29. package/types/utils/toRefsIfReactive.d.ts +14 -0
  30. package/use/cancellableIntent.js +8 -7
  31. package/use/error.js +22 -3
  32. package/use/listCalculated.js +1 -1
  33. package/use/listInstance.js +28 -26
  34. package/use/listRelated.js +3 -3
  35. package/use/listSubscription.js +102 -84
  36. package/use/loading.js +11 -3
  37. package/use/loadingError.js +3 -1
  38. package/use/objectInstance.js +77 -23
  39. package/use/objectSubscription.js +25 -26
  40. package/use/proxyError.js +28 -15
  41. package/use/proxyLoading.js +21 -8
  42. package/use/proxyLoadingError.js +33 -7
  43. package/utils/cancellableFetch.js +21 -1
  44. package/utils/isReactiveTyped.js +10 -0
  45. package/utils/refIfReactive.js +22 -2
  46. package/utils/toRefsIfReactive.js +28 -0
package/use/loading.js CHANGED
@@ -6,14 +6,22 @@ import { readonly, ref } from "vue";
6
6
  */
7
7
 
8
8
  /**
9
- * The loading state API.
10
- *
11
- * @typedef {object} LoadingStatus
9
+ * @typedef {object} LoadingProperties
12
10
  * @property {LoadingReadonlyRef} loading - Whether the component is loading.
11
+ */
12
+
13
+ /**
14
+ * @typedef {object} LoadingFunctions
13
15
  * @property {() => void} setLoading - Set the loading state to true.
14
16
  * @property {() => void} clearLoading - Set the loading state to false.
15
17
  */
16
18
 
19
+ /**
20
+ * The loading state API.
21
+ *
22
+ * @typedef {LoadingProperties & LoadingFunctions} LoadingStatus
23
+ */
24
+
17
25
  /**
18
26
  * A composable function for managing loading state.
19
27
  *
@@ -2,7 +2,9 @@ import { useLoading } from "./loading.js";
2
2
  import { useError } from "./error.js";
3
3
 
4
4
  /**
5
- * @typedef {import('./loading.js').LoadingStatus & import('./error.js').ErrorStatus} LoadingErrorStatus
5
+ * @typedef {import('./loading.js').LoadingProperties & import('./error.js').ErrorProperties} LoadingErrorProperties
6
+ * @typedef {import('./loading.js').LoadingFunctions & import('./error.js').ErrorFunctions} LoadingErrorFunctions
7
+ * @typedef {LoadingErrorProperties & LoadingErrorFunctions} LoadingErrorStatus
6
8
  */
7
9
 
8
10
  /**
@@ -3,10 +3,10 @@ import { assignReactiveObject } from "../utils/assignReactiveObject.js";
3
3
  import { useLoadingError } from "./loadingError.js";
4
4
  import { reactive, readonly, ref } from "vue";
5
5
  import { CancellablePromise, wrapMaybeCancellable } from "../utils/cancellablePromise.js";
6
- import { refIfReactive } from "../utils/refIfReactive.js";
6
+ import { pkRefIfReactive, refIfReactive } from "../utils/refIfReactive.js";
7
7
 
8
8
  /**
9
- * A composition function to manage create, retrieve, update, delete, and patch operations.
9
+ * A composition function to manage create, retrieve, update, delete, patch, and executeAction operations.
10
10
  *
11
11
  * @module use/objectInstance.js
12
12
  */
@@ -40,7 +40,7 @@ import { refIfReactive } from "../utils/refIfReactive.js";
40
40
  * Reactive arguments to be passed to the object instance.
41
41
  *
42
42
  * @typedef {object} ObjectInstanceRawProps
43
- * @property {string} [pk] - The pk of the object, optional to support creating new objects.
43
+ * @property {import('../config/commonCrud.js').PkInput} [pk] - The pk of the object, optional to support creating new objects.
44
44
  * @property {string} pkKey - The pk key of the object.
45
45
  * @property {object} params - The arguments to be passed to the retrieve function.
46
46
  * @property {import('../config/objectCrud.js').ObjectTarget} target - The arguments to be passed to the crud handlers.
@@ -55,6 +55,7 @@ import { refIfReactive } from "../utils/refIfReactive.js";
55
55
  * @property {import('../config/objectCrud.js').CrudPatchFn} patch - The patch function.
56
56
  * @property {import('../config/objectCrud.js').CrudDeleteFn} delete - The delete function.
57
57
  * @property {import('../config/objectCrud.js').CrudObjectSubscribeFn} subscribe - The subscribe function.
58
+ * @property {import('../config/objectCrud.js').CrudObjectExecuteActionFn} executeAction - The executeAction function.
58
59
  */
59
60
 
60
61
  /**
@@ -62,7 +63,7 @@ import { refIfReactive } from "../utils/refIfReactive.js";
62
63
  *
63
64
  * @typedef {object} ObjectInstanceRawMyState
64
65
  * @property {import('vue').Reactive<ObjectInstanceRawStateCrud>} crud - The crud handlers.
65
- * @property {import('vue').Ref<string|undefined>} pk - The pk of the object.
66
+ * @property {import('vue').Ref<import('../config/commonCrud.js').Pk|undefined>} pk - The pk of the object.
66
67
  * @property {import('vue').Ref<string|undefined>} pkKey - The pk key of the object.
67
68
  * @property {import('vue').Ref<{[key:string]: any}>} params - The arguments to be passed to the retrieve function.
68
69
  * @property {import('vue').Reactive<CrudObject>} object - The object.
@@ -72,7 +73,7 @@ import { refIfReactive } from "../utils/refIfReactive.js";
72
73
  /**
73
74
  * The raw state of the object instance.
74
75
  *
75
- * @typedef {ObjectInstanceRawMyState & Pick<import('./loadingError.js').LoadingErrorStatus, "loading" | "error" | "errored">} ObjectInstanceRawState
76
+ * @typedef {ObjectInstanceRawMyState & import('./loadingError.js').LoadingErrorProperties} ObjectInstanceRawState
76
77
  */
77
78
 
78
79
  /**
@@ -86,15 +87,20 @@ import { refIfReactive } from "../utils/refIfReactive.js";
86
87
  /** @typedef {{ object: ExistingCrudObject }} ObjectInstanceUpdateArgs */
87
88
  /** @typedef {{ partialObject: ExistingCrudObject }} ObjectInstancePatchArgs */
88
89
 
90
+ /**
91
+ * @typedef {{[key:string]: any}} AdditionalArgs
92
+ */
93
+
89
94
  /**
90
95
  * The functions available on the object instance.
91
96
  *
92
97
  * @typedef {object} ObjectInstanceMyFunctions
93
- * @property {(args: ObjectInstanceCreateArgs) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} create - Called to turn the current object into a new object on the server.
94
- * @property {(args?: import('./cancellableIntent.js').CommonRunTracking) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} retrieve - Called to retrieve the current object by pk from the server.
95
- * @property {(args: ObjectInstanceUpdateArgs) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} update - Called to update the current object on the server.
96
- * @property {() => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} delete - Called to delete the current object on the server.
97
- * @property {(args: ObjectInstancePatchArgs) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} patch - Called to patch the current object on the server.
98
+ * @property {(args: ObjectInstanceCreateArgs & AdditionalArgs) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} create - Called to turn the current object into a new object on the server.
99
+ * @property {(args?: Partial<import('./cancellableIntent.js').CommonRunTracking> & AdditionalArgs) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} retrieve - Called to retrieve the current object by pk from the server.
100
+ * @property {(args: ObjectInstanceUpdateArgs & AdditionalArgs) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} update - Called to update the current object on the server.
101
+ * @property {(args?: AdditionalArgs) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} delete - Called to delete the current object on the server.
102
+ * @property {(args: ObjectInstancePatchArgs & AdditionalArgs) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} patch - Called to patch the current object on the server.
103
+ * @property {(args: {action: string} & AdditionalArgs) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<boolean|never>} executeAction - Called to execute certain action on the current object.
98
104
  * @property {() => void} clear - Called to clear the object state.
99
105
  */
100
106
 
@@ -102,7 +108,7 @@ import { refIfReactive } from "../utils/refIfReactive.js";
102
108
  * The functions available on the object instance, including the ability to clear LoadingError errors.
103
109
  *
104
110
  * @typedef {(
105
- * Pick<import('./loadingError.js').LoadingErrorStatus, "clearError"> &
111
+ * import('./error.js').ErrorReadOnlyFunctions &
106
112
  * ObjectInstanceMyFunctions
107
113
  * )} ObjectInstanceFunctions
108
114
  */
@@ -150,7 +156,16 @@ export const objectInstanceStateKeys = [
150
156
  "deleted",
151
157
  ];
152
158
 
153
- export const objectInstanceFunctions = ["create", "retrieve", "update", "delete", "patch", "clearError", "clear"];
159
+ export const objectInstanceFunctions = [
160
+ "create",
161
+ "retrieve",
162
+ "update",
163
+ "delete",
164
+ "patch",
165
+ "executeAction",
166
+ "clearError",
167
+ "clear",
168
+ ];
154
169
 
155
170
  /**
156
171
  * Initializes multiple useObjectInstance instances, returning an object of them based on the keys of the instanceArgs.
@@ -217,7 +232,7 @@ export function useObjectInstances(instanceArgs) {
217
232
  * ```
218
233
  *
219
234
  * @param {ObjectInstanceOptions} options - The options to be passed to useObjectInstance.
220
- * @returns {ObjectInstance} - An object used to manage create, retrieve, update, delete, and patch operations.
235
+ * @returns {ObjectInstance} - An object used to manage create, retrieve, update, delete, patch, and executeAction operations.
221
236
  */
222
237
  export function useObjectInstance({ props, handlers = {} }) {
223
238
  // missing pk is fine, to support creating new objects
@@ -240,9 +255,10 @@ export function useObjectInstance({ props, handlers = {} }) {
240
255
  delete: defaultObjectCrud.delete,
241
256
  patch: defaultObjectCrud.patch,
242
257
  subscribe: defaultObjectCrud.subscribe,
258
+ executeAction: defaultObjectCrud.executeAction,
243
259
  },
244
260
  object: {},
245
- pk: refIfReactive(props, "pk", null),
261
+ pk: pkRefIfReactive(props, "pk", null),
246
262
  pkKey: refIfReactive(props, "pkKey"),
247
263
  params: refIfReactive(props, "params", {}),
248
264
  loading: loadingError.loading,
@@ -263,7 +279,7 @@ export function useObjectInstance({ props, handlers = {} }) {
263
279
  /** @type {ObjectInstance} */
264
280
  const instance = {
265
281
  state,
266
- create: ({ object }) => {
282
+ create: ({ object, ...additionalArgs }) => {
267
283
  // this function cannot be async, or the resulting promise will lose its .cancel() method
268
284
  if (state.loading) {
269
285
  // we throw because we want devs to see this error in the console
@@ -274,6 +290,7 @@ export function useObjectInstance({ props, handlers = {} }) {
274
290
  loadingError.clearError();
275
291
  const isCancelled = ref(false);
276
292
  const createPromise = state.crud.create({
293
+ ...additionalArgs,
277
294
  target: state.crud.args,
278
295
  object,
279
296
  params: state.params,
@@ -303,8 +320,7 @@ export function useObjectInstance({ props, handlers = {} }) {
303
320
  : undefined
304
321
  );
305
322
  },
306
- retrieve: (args) => {
307
- const { runId, isCurrentRun } = args || {};
323
+ retrieve: (args = {}) => {
308
324
  // this function cannot be async, or the resulting promise will lose its .cancel() method
309
325
  if (promises.retrieve) {
310
326
  // if a retrieve is already in progress, return the existing promise
@@ -322,8 +338,7 @@ export function useObjectInstance({ props, handlers = {} }) {
322
338
  let retrievePromise = null;
323
339
  try {
324
340
  retrievePromise = state.crud.retrieve({
325
- runId,
326
- isCurrentRun,
341
+ ...args,
327
342
  target: state.crud.args,
328
343
  pk: state.pk,
329
344
  params: state.params,
@@ -361,7 +376,7 @@ export function useObjectInstance({ props, handlers = {} }) {
361
376
 
362
377
  return promises.retrieve;
363
378
  },
364
- update: ({ object }) => {
379
+ update: ({ object, ...additionalArgs }) => {
365
380
  // this function cannot be async, or the resulting promise will lose its .cancel() method
366
381
  if (state.loading) {
367
382
  // we throw because we want devs to see this error in the console
@@ -372,6 +387,7 @@ export function useObjectInstance({ props, handlers = {} }) {
372
387
  loadingError.clearError();
373
388
  const isCancelled = ref(false);
374
389
  const updatePromise = state.crud.update({
390
+ ...additionalArgs,
375
391
  target: state.crud.args,
376
392
  object,
377
393
  params: state.params,
@@ -400,7 +416,7 @@ export function useObjectInstance({ props, handlers = {} }) {
400
416
  : undefined
401
417
  );
402
418
  },
403
- delete: () => {
419
+ delete: (args = {}) => {
404
420
  // this function cannot be async, or the resulting promise will lose its .cancel() method
405
421
  if (state.loading) {
406
422
  // we throw because we want devs to see this error in the console
@@ -410,6 +426,7 @@ export function useObjectInstance({ props, handlers = {} }) {
410
426
  loadingError.setLoading();
411
427
  loadingError.clearError();
412
428
  const deletePromise = state.crud.delete({
429
+ ...args,
413
430
  target: state.crud.args,
414
431
  pk: state.pk,
415
432
  pkKey: state.pkKey,
@@ -436,7 +453,7 @@ export function useObjectInstance({ props, handlers = {} }) {
436
453
  : undefined
437
454
  );
438
455
  },
439
- patch: ({ partialObject }) => {
456
+ patch: ({ partialObject, ...additionalArgs }) => {
440
457
  // this function cannot be async, or the resulting promise will lose its .cancel() method
441
458
  if (state.loading) {
442
459
  // we throw because we want devs to see this error in the console
@@ -447,10 +464,11 @@ export function useObjectInstance({ props, handlers = {} }) {
447
464
  loadingError.clearError();
448
465
  const isCancelled = ref(false);
449
466
  const patchPromise = state.crud.patch({
467
+ ...additionalArgs,
450
468
  target: state.crud.args,
469
+ partialObject,
451
470
  pk: state.pk,
452
471
  pkKey: state.pkKey,
453
- partialObject,
454
472
  params: state.params,
455
473
  isCancelled: readonly(isCancelled),
456
474
  });
@@ -476,6 +494,42 @@ export function useObjectInstance({ props, handlers = {} }) {
476
494
  : undefined
477
495
  );
478
496
  },
497
+ executeAction: ({ action, ...additionalArgs }) => {
498
+ if (state.loading) {
499
+ throw new ObjectError("already loading.", "already-loading");
500
+ }
501
+ loadingError.setLoading();
502
+ loadingError.clearError();
503
+ const isCancelled = ref(false);
504
+ const executeActionPromise = state.crud.executeAction({
505
+ ...additionalArgs,
506
+ target: state.crud.args,
507
+ action,
508
+ pk: state.pk,
509
+ pkKey: state.pkKey,
510
+ isCancelled: readonly(isCancelled),
511
+ });
512
+ return wrapMaybeCancellable(
513
+ executeActionPromise
514
+ .then(() => {
515
+ return true;
516
+ })
517
+ .catch((/** @type {Error} */ error) => {
518
+ loadingError.setError(error);
519
+ return false;
520
+ })
521
+ .finally(() => {
522
+ loadingError.clearLoading();
523
+ }),
524
+ executeActionPromise.cancel
525
+ ? async (/** @type {any} */ reason) => {
526
+ isCancelled.value = true;
527
+ await executeActionPromise.cancel?.(reason);
528
+ loadingError.clearLoading();
529
+ }
530
+ : undefined
531
+ );
532
+ },
479
533
  clearError: loadingError.clearError,
480
534
  clear: () => {
481
535
  loadingError.clearError();
@@ -1,9 +1,9 @@
1
1
  import { assignReactiveObject } from "../utils/assignReactiveObject.js";
2
2
  import { useCancellableIntent } from "./cancellableIntent.js";
3
3
  import { useObjectInstance } from "./objectInstance.js";
4
- import { computed, reactive, ref, toRef, toRefs } from "vue";
4
+ import { computed, reactive, readonly, ref, toRefs } from "vue";
5
5
  import { refIfReactive } from "../utils/refIfReactive.js";
6
- import { useProxyError } from "./proxyError.js";
6
+ import { asWatchableError, useProxyError } from "./proxyError.js";
7
7
  import { loadingCombine } from "../utils/loadingCombine.js";
8
8
 
9
9
  /**
@@ -91,14 +91,18 @@ export const objectSubscriptionFunctions = ["clearError"];
91
91
  */
92
92
 
93
93
  /**
94
- * Options for initializing an object subscription, including reactive props and non-reactive handlers.
95
- *
96
- * @typedef {object & import('./objectInstance.js').ObjectInstanceOptions} ObjectSubscriptionOptions
94
+ * @typedef {object} ObjectSubscriptionOwnOptions
97
95
  * @property {import('./objectInstance.js').ObjectInstance} [objectInstance] - An object instance to use instead of creating a new one.
98
96
  * @property {import('vue').UnwrapNestedRefs<(
99
97
  * ObjectSubscriptionRawProps & import('./objectInstance.js').ObjectInstanceRawProps
100
98
  * )>} props - The reactive args to be passed to useObjectInstance.
101
- * @property {import('./objectInstance.js').ObjectInstanceHandlers} [handlers] - The handlers to be passed to useObjectInstance.
99
+ * @property {import('../config/objectCrud.js').ObjectCrudHandlers} [handlers] - The handlers to be passed to useObjectInstance.
100
+ */
101
+
102
+ /**
103
+ * Options for initializing an object subscription, including reactive props and non-reactive handlers.
104
+ *
105
+ * @typedef {ObjectSubscriptionOwnOptions & import('./objectInstance.js').ObjectInstanceOptions} ObjectSubscriptionOptions
102
106
  */
103
107
 
104
108
  /**
@@ -200,14 +204,8 @@ export function useObjectSubscription({ objectInstance, props, handlers }) {
200
204
  const intendToRetrieve = refIfReactive(props, "intendToRetrieve", false);
201
205
  const intendToSubscribe = refIfReactive(props, "intendToSubscribe", false);
202
206
  const parentState = objectInstance.state;
203
- /** @type {import('./proxyError.js').WatchableErrors} */
204
- const errorStates = ref([
205
- {
206
- error: toRef(parentState, "error"),
207
- errored: toRef(parentState, "errored"),
208
- clearError: objectInstance.clearError,
209
- },
210
- ]);
207
+ /** @type {import('vue').Ref<import('./proxyError.js').WatchableError[]>} */
208
+ const errorStates = ref([asWatchableError(objectInstance)]);
211
209
  const proxyError = useProxyError(errorStates);
212
210
  const {
213
211
  crud: parentCrud,
@@ -249,7 +247,7 @@ export function useObjectSubscription({ objectInstance, props, handlers }) {
249
247
  }
250
248
  };
251
249
  const parentState = objectInstance.state;
252
- return parentState.crud.subscribe({
250
+ const subscribePromise = parentState.crud.subscribe({
253
251
  runId,
254
252
  isCurrentRun,
255
253
  target: parentState.crud.args,
@@ -257,8 +255,17 @@ export function useObjectSubscription({ objectInstance, props, handlers }) {
257
255
  pkKey: parentState.pkKey,
258
256
  params: state.params,
259
257
  callback: subscribeCallback,
260
- isCancelled,
258
+ isCancelled: readonly(isCancelled),
261
259
  });
260
+ if (subscribePromise?.cancel) {
261
+ const originalCancel = subscribePromise.cancel.bind(subscribePromise);
262
+ const cancelWithFlag = async (/** @type {any} */ reason) => {
263
+ isCancelled.value = true;
264
+ return originalCancel(reason);
265
+ };
266
+ subscribePromise.cancel = cancelWithFlag;
267
+ }
268
+ return subscribePromise;
262
269
  },
263
270
  watchArguments: {
264
271
  intendToSubscribe,
@@ -272,16 +279,8 @@ export function useObjectSubscription({ objectInstance, props, handlers }) {
272
279
  // subscriptions persist until cancelled
273
280
  clearActiveOnResolved: false,
274
281
  });
275
- errorStates.value.push({
276
- error: toRef(retrieveIntent.state, "error"),
277
- errored: toRef(retrieveIntent.state, "errored"),
278
- clearError: retrieveIntent.clearError,
279
- });
280
- errorStates.value.push({
281
- error: toRef(subscribeIntent.state, "error"),
282
- errored: toRef(subscribeIntent.state, "errored"),
283
- clearError: subscribeIntent.clearError,
284
- });
282
+ errorStates.value.push(asWatchableError(retrieveIntent));
283
+ errorStates.value.push(asWatchableError(subscribeIntent));
285
284
  /** @type {ObjectSubscriptionState} */
286
285
  const state = reactive({
287
286
  crud: parentCrud,
package/use/proxyError.js CHANGED
@@ -1,27 +1,17 @@
1
1
  import { computed, readonly, unref } from "vue";
2
2
  import identity from "lodash-es/identity.js";
3
+ import { toRefsIfReactive } from "../utils/toRefsIfReactive.js";
3
4
 
4
5
  /**
5
- * @typedef {Pick<import('./error.js').ErrorStatus, "error" | "errored" | "clearError">} ReadonlyErrorStatus
6
- */
7
- /**
8
- * @typedef {import("vue").Ref<ReadonlyErrorStatus>} RefErrorStatus
9
- */
10
- /**
11
- * @typedef {ReadonlyErrorStatus | RefErrorStatus} WatchableError
12
- */
13
- /**
14
- * @typedef {import('vue').Ref<WatchableError[]>} WatchableErrorRef
15
- */
16
- /**
17
- * @typedef {WatchableErrorRef|WatchableError[]} WatchableErrors
6
+ * @typedef {import('./error.js').ReadonlyErrorStatus | import("vue").Reactive<import('./error.js').ReadonlyErrorStatus>} WatchableError
7
+ * @typedef {import('vue').MaybeRef<WatchableError>} MaybeRefWatchableError
18
8
  */
19
9
 
20
10
  /**
21
11
  * A composable function for aggregating error state across multiple sources.
22
12
  *
23
- * @param {WatchableErrors} errors - The error states to monitor.
24
- * @returns {ReadonlyErrorStatus} An object containing aggregated reactive fields and actions for error state.
13
+ * @param {import('vue').MaybeRef<MaybeRefWatchableError[]>} errors - The error states to monitor.
14
+ * @returns {import('./error.js').ReadonlyErrorStatus} An object containing aggregated reactive fields and actions for error state.
25
15
  */
26
16
  export function useProxyError(errors) {
27
17
  const error = computed(
@@ -46,3 +36,26 @@ export function useProxyError(errors) {
46
36
  },
47
37
  };
48
38
  }
39
+
40
+ /**
41
+ * @typedef {{ state: import('vue').Reactive<import('./error.js').ErrorProperties> } & import('./error.js').ErrorReadOnlyFunctions} SeparateStateError
42
+ */
43
+
44
+ /**
45
+ * Adapt an object with reactive error state into a WatchableError shape.
46
+ * Accepts either an object with a `state` property or an object that already exposes error/errored/clearError.
47
+ *
48
+ * @param {import('vue').MaybeRef<
49
+ * SeparateStateError |
50
+ * WatchableError
51
+ * >} source - The source object to adapt.
52
+ * @returns {WatchableError} - The adapted WatchableError object.
53
+ */
54
+ export function asWatchableError(source) {
55
+ const unwrapped = unref(source);
56
+ if ("state" in unwrapped) {
57
+ const stateRefs = toRefsIfReactive(unwrapped.state);
58
+ return { ...stateRefs, clearError: unwrapped.clearError };
59
+ }
60
+ return toRefsIfReactive(unwrapped);
61
+ }
@@ -1,22 +1,35 @@
1
- import { computed, readonly, unref } from "vue";
1
+ import { computed, unref } from "vue";
2
2
  import { loadingCombine } from "../utils/loadingCombine.js";
3
3
 
4
4
  /**
5
- * @typedef {Pick<import('./loading.js').LoadingStatus, "loading">} ReadonlyLoadingStatus
6
- * @typedef {import("vue").Ref<ReadonlyLoadingStatus>} RefLoadingStatus
7
- * @typedef {ReadonlyLoadingStatus | RefLoadingStatus} WatchableLoading
5
+ * @typedef {import('./loading.js').LoadingProperties | import("vue").Reactive<import('./loading.js').LoadingProperties>} WatchableLoading
6
+ * @typedef {import('vue').MaybeRef<WatchableLoading>} MaybeRefWatchableLoading
7
+ * @typedef {import('./loading.js').LoadingProperties} ReadonlyLoadingStatus
8
8
  */
9
9
 
10
10
  /**
11
11
  * A composable function for aggregating loading state across multiple sources.
12
12
  *
13
- * @param {WatchableLoading[]} loadings - The loading states to monitor.
13
+ * @param {import('vue').MaybeRef<MaybeRefWatchableLoading[]>} loadings - The loading states to monitor.
14
14
  * @returns {ReadonlyLoadingStatus} An object containing the aggregated loading field.
15
15
  */
16
16
  export function useProxyLoading(loadings) {
17
- const loading = computed(() => loadingCombine(...unref(loadings).map((l) => unref(unref(l).loading))));
18
-
19
17
  return {
20
- loading: readonly(loading),
18
+ loading: computed(() => loadingCombine(...unref(loadings).map((l) => unref(unref(l).loading)))),
21
19
  };
22
20
  }
21
+
22
+ /**
23
+ * Adapt an object with reactive loading state into a WatchableLoading shape.
24
+ * Accepts either an object with a `state` property or an object that already exposes `loading`.
25
+ *
26
+ * @param {import('vue').MaybeRef<{ state: WatchableLoading } | WatchableLoading>} source - The source object to adapt.
27
+ * @returns {WatchableLoading} - The adapted WatchableLoading object.
28
+ */
29
+ export function asWatchableLoading(source) {
30
+ const unwrappedSource = unref(source);
31
+ return (
32
+ /** @type {WatchableLoading} */
33
+ "state" in unwrappedSource ? unwrappedSource.state : unwrappedSource
34
+ );
35
+ }
@@ -1,20 +1,46 @@
1
- import { useProxyLoading } from "./proxyLoading.js";
2
- import { useProxyError } from "./proxyError.js";
1
+ import { asWatchableLoading, useProxyLoading } from "./proxyLoading.js";
2
+ import { asWatchableError, useProxyError } from "./proxyError.js";
3
+ import { toRefsIfReactive } from "../utils/toRefsIfReactive.js";
4
+ import { unref } from "vue";
3
5
 
4
6
  /**
5
7
  * @typedef {import('./proxyLoading.js').WatchableLoading & import('./proxyError.js').WatchableError} WatchableLoadingError
6
- * @typedef {import('./proxyLoading.js').ReadonlyLoadingStatus & import('./proxyError.js').ReadonlyErrorStatus} ProxyLoadingError
8
+ * @typedef {import('vue').MaybeRef<WatchableLoadingError>} MaybeRefWatchableLoadingError
9
+ * @typedef {import('./loading.js').LoadingProperties & import('./error.js').ReadonlyErrorStatus} ProxyLoadingError
7
10
  */
8
11
 
9
12
  /**
10
- * A composable function combining aggregated loading and error state.
13
+ * A composable function combining aggregated loading and error state. Use `asWatchableLoadingError` to convert <List|Object><Instance|Subscription> to WatchableLoadingError.
11
14
  *
12
- * @param {WatchableLoadingError[]} loadingErrors - The loading and error states to monitor.
15
+ * @param {import('vue').MaybeRef<MaybeRefWatchableLoadingError[]>} loadingErrors - The loading and error states to monitor.
13
16
  * @returns {ProxyLoadingError} - An object containing aggregated reactive fields and actions for both loading and error state.
14
17
  */
15
18
  export function useProxyLoadingError(loadingErrors) {
19
+ const unwrappedLoadingErrors = unref(loadingErrors);
16
20
  return {
17
- ...useProxyLoading(loadingErrors),
18
- ...useProxyError(loadingErrors),
21
+ ...useProxyLoading(unwrappedLoadingErrors),
22
+ ...useProxyError(unwrappedLoadingErrors),
23
+ };
24
+ }
25
+
26
+ /**
27
+ * @typedef {{ state: import('vue').Reactive<import('./error.js').ErrorProperties> } & import('./error.js').ErrorReadOnlyFunctions} SeparateStateLoadingError
28
+ */
29
+ /**
30
+ * Adapt an object that exposes loading/error state and clearError into a WatchableLoadingError shape.
31
+ *
32
+ * @param {import('vue').MaybeRef<SeparateStateLoadingError | WatchableLoadingError>} source - The source object to adapt.
33
+ * @returns {WatchableLoadingError} - The adapted WatchableLoadingError object.
34
+ */
35
+ export function asWatchableLoadingError(source) {
36
+ const unwrappedSource = unref(source);
37
+ const isSeparateState = "state" in unwrappedSource;
38
+ const normalizedRefs = /** @type {WatchableLoadingError} */ (
39
+ isSeparateState ? unwrappedSource.state : unwrappedSource
40
+ );
41
+ const unwrappedRefs = toRefsIfReactive(normalizedRefs);
42
+ return {
43
+ ...asWatchableLoading(unwrappedRefs),
44
+ ...asWatchableError(unwrappedRefs),
19
45
  };
20
46
  }
@@ -11,9 +11,29 @@ import { CancellablePromise } from "./cancellablePromise.js";
11
11
  */
12
12
  export function cancellableFetch(input, init, transform) {
13
13
  const controller = new AbortController();
14
+ const externalSignal = init?.signal;
14
15
  const signal = controller.signal;
16
+ let cleanupExternalSignal = () => {};
15
17
 
16
- const basePromise = fetch(input, { ...init, signal }).then(transform);
18
+ if (externalSignal) {
19
+ if (externalSignal.aborted) {
20
+ controller.abort(externalSignal.reason);
21
+ } else {
22
+ const handleAbort = () => {
23
+ controller.abort(externalSignal.reason);
24
+ };
25
+ externalSignal.addEventListener("abort", handleAbort, { once: true });
26
+ cleanupExternalSignal = () => {
27
+ externalSignal.removeEventListener("abort", handleAbort);
28
+ };
29
+ }
30
+ }
31
+
32
+ const basePromise = fetch(input, { ...init, signal })
33
+ .then(transform)
34
+ .finally(() => {
35
+ cleanupExternalSignal();
36
+ });
17
37
 
18
38
  return CancellablePromise(basePromise, async (/** @type {any} */ reason) => {
19
39
  controller.abort(reason);
@@ -0,0 +1,10 @@
1
+ import { isReactive } from "vue";
2
+
3
+ /**
4
+ * @template {object} T
5
+ * @param {T | import('vue').Reactive<T>} v - The value to check.
6
+ * @returns {v is import('vue').Reactive<T>} - True if the value is reactive, false otherwise.
7
+ */
8
+ export function isReactiveTyped(v) {
9
+ return !!v && isReactive(v);
10
+ }
@@ -7,8 +7,8 @@ import { computed, isReactive, toRef, unref } from "vue";
7
7
  * @template {keyof T} K
8
8
  * @param {T & object | undefined | null} source - The source object.
9
9
  * @param {K} property - The property to access.
10
- * @param {T[K]} [defaultValue] - The default value to use if source or property is missing.
11
- * @returns {import('vue').ComputedRef<T[K]> | import('vue').Ref<T[K]>} The ref to the property if the source is reactive, otherwise the unrefed value.
10
+ * @param {T[K] | undefined} [defaultValue] - The default value to use if source or property is missing.
11
+ * @returns {import('vue').ComputedRef<T[K] | undefined> | import('vue').Ref<T[K] | undefined>} The ref to the property if the source is reactive; otherwise a computed that can be undefined when missing.
12
12
  */
13
13
  export const refIfReactive = (source, property, defaultValue) => {
14
14
  if (source && isReactive(source)) {
@@ -16,3 +16,23 @@ export const refIfReactive = (source, property, defaultValue) => {
16
16
  }
17
17
  return computed(() => unref(source?.[property]) ?? defaultValue);
18
18
  };
19
+
20
+ /**
21
+ * Returns a ref to a pk property, coercing string|number input to string output.
22
+ * Returns undefined if the source pk is null/undefined.
23
+ *
24
+ * @param {object | undefined | null} source - The source object containing the pk.
25
+ * @param {string} [property="pk"] - The property name to access.
26
+ * @param {import('../config/commonCrud.js').Pk | null} [defaultValue=null] - The default value if missing.
27
+ * @returns {import('vue').ComputedRef<import('../config/commonCrud.js').Pk | undefined>} A computed ref that coerces to string.
28
+ */
29
+ export const pkRefIfReactive = (source, property = "pk", defaultValue = null) => {
30
+ const rawRef = refIfReactive(source, property, defaultValue);
31
+ return computed(() => {
32
+ const value = unref(rawRef);
33
+ if (value === null || value === undefined) {
34
+ return undefined;
35
+ }
36
+ return String(value);
37
+ });
38
+ };
@@ -0,0 +1,28 @@
1
+ import { toRefs } from "vue";
2
+ import { isReactiveTyped } from "./isReactiveTyped.js";
3
+
4
+ /**
5
+ * @template {object} T
6
+ * @overload
7
+ * @param {import('vue').Reactive<T>} source - The source reactive object.
8
+ * @returns {import('vue').ToRefs<T>} - The refs of the reactive object.
9
+ */
10
+ /**
11
+ * @template {object} T
12
+ * @overload
13
+ * @param {T} source - The source object.
14
+ * @returns {T} - The original object.
15
+ */
16
+ /**
17
+ * Converts a reactive object to refs, or returns the original object if not reactive.
18
+ *
19
+ * @template {object} T
20
+ * @param {T | import('vue').Reactive<T>} source - The source object.
21
+ * @returns {T | import('vue').ToRefs<T>} - The refs of the reactive object or the original object.
22
+ */
23
+ export function toRefsIfReactive(source) {
24
+ if (isReactiveTyped(source)) {
25
+ return toRefs(/** @type {import('vue').Reactive<object>} */ (source));
26
+ }
27
+ return source;
28
+ }