@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
@@ -2,8 +2,9 @@ import { assignReactiveObject } from "../utils/assignReactiveObject.js";
2
2
  import { loadingCombine } from "../utils/loadingCombine.js";
3
3
  import { useCancellableIntent } from "./cancellableIntent.js";
4
4
  import { useLoadingError } from "./loadingError.js";
5
- import { useObjectInstance, objectInstanceStateKeys } from "./objectInstance.js";
6
- import { computed, effectScope, reactive, toRef } from "vue";
5
+ import { useObjectInstance } from "./objectInstance.js";
6
+ import { computed, effectScope, reactive, ref, toRef, toRefs } from "vue";
7
+ import { CancellablePromise } from "../utils/cancellablePromise.js";
7
8
 
8
9
  /**
9
10
  * A composition function for managing object subscriptions, including subscription status, errors, and reactivity.
@@ -49,38 +50,39 @@ export const objectSubscriptionFunctions = [
49
50
  * The raw state of the object subscription.
50
51
  *
51
52
  * @typedef {object} ObjectSubscriptionRawState
52
- * @property {Readonly<import('vue').Ref<boolean>>} subscriptionLoading - Whether the subscription is loading.
53
- * @property {Readonly<import('vue').Ref<boolean>>} subscriptionErrored - Whether the subscription has errored.
54
- * @property {Readonly<import('vue').Ref<Error>>} subscriptionError - The error that occurred.
55
- * @property {boolean} intendToRetrieve - Whether the object intends to retrieve.
56
- * @property {boolean} intendToSubscribe - Whether the object intends to subscribe.
57
- * @property {boolean} subscribed - Whether the object is subscribed.
53
+ * @property {import('./loadingError.js').LoadingReadonlyRef} subscriptionLoading - Whether the subscription is loading.
54
+ * @property {import('./loadingError.js').ErroredReadonlyRef} subscriptionErrored - Whether the subscription has errored.
55
+ * @property {import('./loadingError.js').ErrorReadonlyRef} subscriptionError - The error that occurred.
56
+ * @property {import('vue').Ref<boolean>} intendToRetrieve - Whether the object intends to retrieve.
57
+ * @property {import('vue').Ref<boolean>} intendToSubscribe - Whether the object intends to subscribe.
58
+ * @property {import('vue').Ref<boolean|undefined>} subscribed - Whether the object is subscribed.
58
59
  */
59
60
 
60
61
  /**
61
62
  * The state of the object subscription, including both subscription and object instance states.
62
63
  *
63
- * @typedef {import('vue').UnwrapNestedRefs<
64
+ * @typedef {import('vue').Reactive<
64
65
  * ObjectSubscriptionRawState &
65
66
  * import('./objectInstance.js').ObjectInstanceRawState
66
67
  * >} ObjectSubscriptionState
67
68
  */
68
69
 
69
- /* eslint-disable jsdoc/valid-types -- the subscribe function signature is valid typescript that irks eslint-plugin-jsdoc */
70
70
  /**
71
71
  * Functions available for object subscription management.
72
72
  *
73
73
  * @typedef {object} ObjectSubscriptionFunctions
74
- * @property {({ retrieve }?: { retrieve?: boolean }) => boolean} subscribe - Subscribes to updates from an object, managing subscription state and
75
- * handling errors internally. Ensures that only one active subscription can exist at a time to prevent duplicate
76
- * calls. Returns a promise that resolves to true if the subscription was successful, and false if it failed.
74
+ * @property {(
75
+ * (options?: { retrieve?: boolean }) => boolean
76
+ * )} subscribe - Subscribes to updates from an object, managing subscription state and handling errors internally.
77
+ * Ensures that only one active subscription can exist at a time to prevent duplicate calls. Returns a promise that
78
+ * resolves to true if the subscription was successful, and false if it failed.
77
79
  * @property {() => boolean} unsubscribe - Unsubscribes from the object, resetting related state flags. Returns
78
80
  * true if the object was unsubscribed, and false if it was not subscribed.
79
- * @property {(data: import('./objectInstance.js').CrudObject) => void} updateFromSubscription - Update the object from a subscription.
81
+ * @property {(data: import('./objectInstance.js').ExistingCrudObject) => void} updateFromSubscription - Update the
82
+ * object from a subscription.
80
83
  * @property {() => void} deleteFromSubscription - Delete the object from a subscription.
81
84
  * @property {() => void} clearError - Clears any errors related to the subscription, and resets the loading state.
82
85
  */
83
- /* eslint-enable jsdoc/valid-types */
84
86
 
85
87
  /**
86
88
  * Properties of the object subscription.
@@ -103,8 +105,8 @@ export const objectSubscriptionFunctions = [
103
105
  * Raw props for the object subscription.
104
106
  *
105
107
  * @typedef {object} ObjectSubscriptionRawProps
106
- * @property {boolean} [intendToRetrieve] - Whether the object intends to retrieve.
107
- * @property {boolean} [intendToSubscribe] - Whether the object intends to subscribe.
108
+ * @property {boolean|undefined} intendToRetrieve - Whether the object intends to retrieve.
109
+ * @property {boolean|undefined} intendToSubscribe - Whether the object intends to subscribe.
108
110
  */
109
111
 
110
112
  /**
@@ -204,33 +206,42 @@ export function useObjectSubscription({ objectInstance, props, functions }) {
204
206
  );
205
207
  }
206
208
  }
209
+
210
+ const es = effectScope();
207
211
  const parentState = objectInstance.state;
208
212
  const loadingError = useLoadingError();
213
+ const {
214
+ crud: parentCrud,
215
+ pk: parentPk,
216
+ pkKey: parentPkKey,
217
+ retrieveArgs: parentRetrieveArgs,
218
+ object: parentObject,
219
+ deleted: parentDeleted,
220
+ } = toRefs(parentState);
209
221
  /** @type {ObjectSubscriptionState} */
210
- // @ts-ignore - we're going to assign all the keys later, and in the effect scope
211
222
  const state = reactive({
223
+ crud: parentCrud,
224
+ pk: parentPk,
225
+ pkKey: parentPkKey,
226
+ retrieveArgs: parentRetrieveArgs,
227
+ object: parentObject,
228
+ deleted: parentDeleted,
212
229
  subscriptionLoading: loadingError.loading,
213
230
  subscriptionErrored: loadingError.errored,
214
231
  subscriptionError: loadingError.error,
215
- subscribed: undefined,
216
- intendToSubscribe: false,
217
- intendToRetrieve: false,
232
+ subscribed: ref(undefined),
233
+ intendToSubscribe: "intendToSubscribe" in props ? toRef(props, "intendToSubscribe") : ref(false),
234
+ intendToRetrieve: "intendToRetrieve" in props ? toRef(props, "intendToRetrieve") : ref(false),
235
+ loading: es.run(() => computed(() => loadingCombine(parentState.loading, state.subscriptionLoading))),
236
+ errored: es.run(() => computed(() => parentState.errored || state.subscriptionErrored)),
237
+ error: es.run(() => computed(() => parentState.error || state.subscriptionError)),
218
238
  });
219
- if ("intendToRetrieve" in props) {
220
- // @ts-ignore - passing Ref<boolean> to boolean in an UnwrapNestedRefs is fine
221
- state.intendToRetrieve = toRef(props, "intendToRetrieve");
222
- }
223
- if ("intendToSubscribe" in props) {
224
- // @ts-ignore - passing Ref<boolean> to boolean in an UnwrapNestedRefs is fine
225
- state.intendToSubscribe = toRef(props, "intendToSubscribe");
226
- }
227
-
228
239
  /** @type {import('./cancellableIntent.js').CancellableIntent} */
229
240
  let subscribeIntent;
230
241
  /** @type {import('./cancellableIntent.js').CancellableIntent} */
231
242
  let retrieveIntent;
232
243
 
233
- function updateFromSubscription(data) {
244
+ function updateFromSubscription(/** @type {import('./objectInstance.js').ExistingCrudObject} */ data) {
234
245
  assignReactiveObject(parentState.object, data);
235
246
  }
236
247
 
@@ -257,55 +268,56 @@ export function useObjectSubscription({ objectInstance, props, functions }) {
257
268
  function subscribe() {
258
269
  // this function cannot be async, or the resulting promise will lose its .cancel() method
259
270
  if (state.subscribed) {
260
- return Promise.reject(
261
- new ObjectSubscriptionError("already subscribed or subscribing.", "already-subscribed")
262
- );
271
+ // we throw because we want devs to see this error in the console
272
+ // state.error should be for user facing errors, or unknown errors
273
+ throw new ObjectSubscriptionError("already subscribed or subscribing.", "already-subscribed");
263
274
  }
264
275
  loadingError.clearError();
265
276
  loadingError.setLoading();
266
- let subscribePromise;
267
- subscribePromise = parentState.crud.subscribe({
277
+ const isCancelled = ref(false);
278
+ /** @type {import('../config/objectCrud.js').CrudSubscribeCallback} */
279
+ const subscribeCallback = (data, action) => {
280
+ if (action === "delete") {
281
+ objectInstance.deleteFromSubscription();
282
+ } else {
283
+ objectInstance.updateFromSubscription(data);
284
+ }
285
+ };
286
+ const subscribePromise = parentState.crud.subscribe({
268
287
  crudArgs: parentState.crud.args,
269
288
  pk: parentState.pk,
270
289
  pkKey: parentState.pkKey,
271
290
  retrieveArgs: state.retrieveArgs,
272
- callback: (data, action) => {
273
- if (action === "delete") {
274
- objectInstance.deleteFromSubscription();
275
- } else {
276
- objectInstance.updateFromSubscription(data);
277
- }
278
- },
291
+ callback: subscribeCallback,
292
+ isCancelled,
279
293
  });
280
- let cancelSubscription = async () => {
281
- let cancelPromise = subscribePromise.cancel();
294
+ let cancelSubscription = async (/** @type {any} */ reason) => {
295
+ let cancelPromise = subscribePromise.cancel(reason);
282
296
  cancelSubscription = null;
283
297
  state.subscribed = false;
284
298
  return cancelPromise;
285
299
  };
286
- // then/catch/finally makes a new promise, we need to make sure the cancel method lives on.
287
- const catchPromise = subscribePromise
288
- .then(() => {
289
- state.subscribed = true;
290
- return Promise.resolve(true);
291
- })
292
- .catch((error) => {
293
- loadingError.setError(error);
294
- if (cancelSubscription) {
295
- cancelSubscription();
296
- cancelSubscription = null;
297
- state.subscribed = false;
298
- }
299
- return Promise.resolve(false);
300
- })
301
- .finally(() => {
302
- loadingError.clearLoading();
303
- if (!state.subscribed) {
304
- subscribePromise = null;
305
- }
306
- });
307
- catchPromise.cancel = cancelSubscription;
308
- return catchPromise;
300
+ return CancellablePromise(
301
+ subscribePromise
302
+ .then(() => {
303
+ state.subscribed = true;
304
+ return true;
305
+ })
306
+ .catch((/** @type {Error} */ error) => {
307
+ loadingError.setError(error);
308
+ if (cancelSubscription) {
309
+ // noinspection JSIgnoredPromiseFromCall
310
+ cancelSubscription("Subscription error cancellation");
311
+ cancelSubscription = null;
312
+ state.subscribed = false;
313
+ }
314
+ return false;
315
+ })
316
+ .finally(() => {
317
+ loadingError.clearLoading();
318
+ }),
319
+ cancelSubscription
320
+ );
309
321
  }
310
322
 
311
323
  function publicUnsubscribe() {
@@ -326,20 +338,7 @@ export function useObjectSubscription({ objectInstance, props, functions }) {
326
338
  objectInstance.clearError();
327
339
  }
328
340
 
329
- const es = effectScope();
330
-
331
341
  es.run(() => {
332
- // @ts-ignore - loadingCombine returns a boolean|undefined, our state loading is a boolean|undefined... should be fine
333
- state.loading = computed(() => loadingCombine(parentState.loading, state.subscriptionLoading));
334
- // @ts-ignore - the computed value is a boolean, our state errored is a boolean... should be fine
335
- state.errored = computed(() => parentState.errored || state.subscriptionErrored);
336
- // @ts-ignore - the computed value is an Error|undefined, our state error is an Error|undefined... should be fine
337
- state.error = computed(() => parentState.error || state.subscriptionError);
338
-
339
- for (const key of objectInstanceStateKeys.filter((key) => !["loading", "errored", "error"].includes(key))) {
340
- state[key] = toRef(parentState, key);
341
- }
342
-
343
342
  subscribeIntent = useCancellableIntent({
344
343
  awaitableWithCancel: subscribe,
345
344
  watchArguments: reactive({
@@ -359,7 +358,7 @@ export function useObjectSubscription({ objectInstance, props, functions }) {
359
358
  pkKey: toRef(parentState, "pkKey"),
360
359
  retrieveArgs: toRef(parentState, "retrieveArgs"),
361
360
  }),
362
- // delay triggering a retrieve until the last retrieve has finished/cancelled
361
+ // delay triggering a retrieve until the last retrieve has finished/cancelled.
363
362
  // cancel can still be triggered
364
363
  guardArguments: reactive({
365
364
  loading: toRef(state, "loading"),
@@ -1,51 +1,38 @@
1
1
  import { computed, readonly, unref } from "vue";
2
2
  import { loadingCombine } from "../utils/loadingCombine.js";
3
-
4
- /**
5
- * @typedef {(
6
- * import('./loadingError.js').LoadingErrorStatus[]
7
- * )} WatchableLoadingErrorsRaw
8
- */
3
+ import identity from "lodash-es/identity.js";
9
4
 
10
5
  /**
11
6
  * A watchable collection of loading errors.
12
7
  *
13
8
  * @typedef {(
14
- * import('vue').UnwrapNestedRefs<WatchableLoadingErrorsRaw> |
15
- * import('vue').Ref<WatchableLoadingErrorsRaw> |
16
- * WatchableLoadingErrorsRaw
17
- * )} WatchableLoadingErrors
18
- */
19
-
20
- /**
21
- * The instance of useProxyLoadingError.
22
- *
23
- * @typedef {import('./loadingError.js').LoadingErrorStatus} ProxyLoadingError
9
+ * import('vue').UnwrapNestedRefs<import('./loadingError.js').LoadingErrorStatus> |
10
+ * import('vue').Ref<import('./loadingError.js').LoadingErrorStatus> |
11
+ * import('./loadingError.js').LoadingErrorStatus
12
+ * )} WatchableLoadingError
24
13
  */
25
14
 
26
15
  /**
27
16
  * A composable function for managing aggregated loading and error states across multiple sources.
28
17
  *
29
- * @param {WatchableLoadingErrors} loadingErrors - A collection of loading error statuses to monitor and aggregate.
30
- * @returns {ProxyLoadingError} An object containing aggregated reactive fields and actions for loading and error states.
18
+ * @param {WatchableLoadingError[]} loadingErrors - A collection of loading error statuses to monitor and aggregate.
19
+ * @returns {import('./loadingError.js').LoadingErrorStatus} An object containing aggregated reactive fields and actions for loading and error states.
31
20
  */
32
21
  export function useProxyLoadingError(loadingErrors) {
33
- /** @type {import("vue").ComputedRef<boolean|undefined>} */
34
22
  const loading = computed(() =>
35
- loadingCombine(...unref(loadingErrors).map((loadingError) => unref(loadingError.loading)))
23
+ loadingCombine(...unref(loadingErrors).map((loadingError) => unref(unref(loadingError).loading)))
36
24
  );
37
- /** @type {import("vue").ComputedRef<Error|null>} */
38
25
  const error = computed(
39
26
  () =>
27
+ /** @type {Error|null} */
40
28
  unref(loadingErrors)
41
- .map((loadingError) => unref(loadingError.error))
42
- .find((error) => error) || null
29
+ .map((loadingError) => unref(unref(loadingError).error))
30
+ .find(identity) || null
43
31
  );
44
- /** @type {import("vue").ComputedRef<boolean>} */
45
32
  const errored = computed(() =>
46
33
  unref(loadingErrors)
47
- .map((loadingError) => unref(loadingError.errored))
48
- .some((errored) => errored)
34
+ .map((loadingError) => unref(unref(loadingError).errored))
35
+ .some(identity)
49
36
  );
50
37
 
51
38
  return {
@@ -53,7 +40,7 @@ export function useProxyLoadingError(loadingErrors) {
53
40
  error: readonly(error),
54
41
  errored: readonly(errored),
55
42
  clearError: () => {
56
- unref(loadingErrors).forEach((loadingError) => loadingError.clearError());
43
+ unref(loadingErrors).forEach((loadingError) => unref(loadingError).clearError());
57
44
  },
58
45
  };
59
46
  }
package/use/search.js CHANGED
@@ -4,7 +4,7 @@ import FlexSearch from "flexsearch";
4
4
  import cloneDeep from "lodash-es/cloneDeep.js";
5
5
  import isEqual from "lodash-es/isEqual.js";
6
6
  import { effectScope, reactive, toRef, watch, computed } from "vue";
7
- import { deepUnref } from "vue-deepunref";
7
+ import { deepUnref } from "../utils/deepUnref.js";
8
8
 
9
9
  /* minimize new Set() allocations */
10
10
  const unionReduce = (accumulator, currentValue) => {
@@ -0,0 +1,22 @@
1
+ import { CancellablePromise } from "./cancellablePromise.js";
2
+
3
+ /**
4
+ * A wrapper around fetch that adds cancellation via AbortController and returns a CancellablePromise.
5
+ *
6
+ * @template T
7
+ * @param {RequestInfo} input - The URL or Request object to fetch.
8
+ * @param {RequestInit} init - The options for the fetch request.
9
+ * @param {(response: Response) => Promise<T>} transform - A function to transform the response.
10
+ * @returns {CancellablePromise<T>} A cancellable promise that resolves to the transformed response.
11
+ */
12
+ export function cancellableFetch(input, init, transform) {
13
+ const controller = new AbortController();
14
+ const signal = controller.signal;
15
+
16
+ const basePromise = fetch(input, { ...init, signal }).then(transform);
17
+
18
+ return CancellablePromise(basePromise, async (/** @type {any} */ reason) => {
19
+ controller.abort(reason);
20
+ await basePromise.catch(() => {});
21
+ });
22
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * A Promise that can be cancelled.
3
+ *
4
+ * @template T
5
+ * @typedef {Promise<T> & { cancel: (reason?: any) => Promise<void> | void }} CancellablePromise
6
+ */
7
+
8
+ /**
9
+ * A possibly cancellable promise.
10
+ *
11
+ * @template T
12
+ * @typedef {Promise<T> & { cancel?: (reason?: any) => Promise<void> | void }} MaybeCancellablePromise
13
+ */
14
+
15
+ /**
16
+ * Creates a cancellable promise, mostly for easy of type checking.
17
+ *
18
+ * @template T
19
+ * @param {Promise<T>} promise - The promise to be cancellable.
20
+ * @param {() => (Promise<void>|void)} cancel - The function to cancel the promise.
21
+ * @returns {CancellablePromise<T>} The cancellable promise.
22
+ */
23
+ export const CancellablePromise = (promise, cancel) => {
24
+ const cancellablePromise = /** @type {CancellablePromise<T>} */ (promise);
25
+ cancellablePromise.cancel = cancel;
26
+ return cancellablePromise;
27
+ };
28
+
29
+ /**
30
+ * Creates a rejected 'cancellable' promise.
31
+ *
32
+ * @param {any} reason - The reason for the rejection.
33
+ * @returns {MaybeCancellablePromise<never>} A rejected 'cancellable' promise.
34
+ */
35
+ CancellablePromise.reject = (reason) => {
36
+ return Promise.reject(reason);
37
+ };
38
+
39
+ /**
40
+ * Creates a resolved 'cancellable' promise.
41
+ *
42
+ * @template T
43
+ * @param {T} value - The value to resolve the promise with.
44
+ * @returns {MaybeCancellablePromise<T>} A resolved 'cancellable' promise.
45
+ */
46
+ CancellablePromise.resolve = (value) => {
47
+ return Promise.resolve(value);
48
+ };
49
+
50
+ /**
51
+ * Wraps a promise and optionally adds a cancel method if provided.
52
+ *
53
+ * @template T
54
+ * @param {Promise<T>} inner - The inner promise to wrap.
55
+ * @param {(() => Promise<void> | void)=} cancel - Optional cancel function.
56
+ * @returns {MaybeCancellablePromise<T>} The wrapped promise with an optional cancel method.
57
+ */
58
+ export function wrapMaybeCancellable(inner, cancel) {
59
+ let resolve, reject;
60
+ /** @type {MaybeCancellablePromise<T>} */
61
+ const wrapped = new Promise((res, rej) => {
62
+ resolve = res;
63
+ reject = rej;
64
+ });
65
+ if (cancel) {
66
+ wrapped.cancel = cancel;
67
+ }
68
+ inner.then(resolve).catch(reject);
69
+ return wrapped;
70
+ }
package/utils/classes.js CHANGED
@@ -122,12 +122,12 @@ export const objectifyClasses = (...classes) => {
122
122
  * @returns {CombinedClasses} - The normalized form of the CSS classes, either as a string or an object.
123
123
  */
124
124
  export const combineClasses = (...classes) => {
125
- const rawClasses = deepUnrefFlatten(classes);
126
- const flattenedClasses = rawClasses.flat(Infinity);
127
- const filteredClasses = flattenedClasses.filter(identity);
128
- const hasStrings = filteredClasses.some(isString);
125
+ const filteredClasses = deepUnrefFlatten(classes).flat(Infinity).filter(identity);
126
+ if (filteredClasses.every(isString)) {
127
+ return filteredClasses.join(" ");
128
+ }
129
129
  const hasObjects = filteredClasses.some(isObject);
130
- if (hasStrings && !hasObjects) {
130
+ if (!hasObjects) {
131
131
  return stringifyClasses(...filteredClasses);
132
132
  }
133
133
  const result = objectifyClasses(...filteredClasses);
@@ -0,0 +1,28 @@
1
+ import { deepUnref as _deepUnref } from "vue-deepunref";
2
+
3
+ /* eslint-disable jsdoc/valid-types */
4
+
5
+ /**
6
+ * Recursively unwraps refs from a nested object, array, or primitive.
7
+ *
8
+ * @template T
9
+ * @typedef {T extends import('vue').Ref<infer U>
10
+ * ? DeepUnwrap<U>
11
+ * : T extends Array<infer V>
12
+ * ? Array<DeepUnwrap<V>>
13
+ * : T extends object
14
+ * ? { [K in keyof T]: DeepUnwrap<T[K]> }
15
+ * : T
16
+ * } DeepUnwrap
17
+ */
18
+
19
+ /* eslint-enable jsdoc/valid-types */
20
+
21
+ /**
22
+ * Safe, recursively-typed deep unref.
23
+ *
24
+ * @template T
25
+ * @param {T} val - The value to deeply unwrap.
26
+ * @returns {DeepUnwrap<T>} - The deeply unwrapped value.
27
+ */
28
+ export const deepUnref = _deepUnref;