import { ExpiryStatus } from '@data-client/core'; import type { EndpointInterface, Denormalize, Schema, FetchFunction, DenormalizeNullable, ResolveType, } from '@data-client/core'; import { computed, unref, watch, readonly, type DeepReadonly, type ComputedRef, } from 'vue'; import { useController, injectState } from '../context.js'; import type { MaybeRefsOrGetters, MaybeRefsOrGettersNullable, } from '../types.js'; /** * Ensure an endpoint is available. * Suspends until it is. * * @see https://dataclient.io/docs/api/useSuspense * @throws {Promise} If data is not yet available. * @throws {NetworkError} If fetch fails. */ export default function useSuspense< E extends EndpointInterface< FetchFunction, Schema | undefined, undefined | false >, >( endpoint: E, ...args: MaybeRefsOrGetters> ): Promise< DeepReadonly< ComputedRef< E['schema'] extends undefined | null ? ResolveType : Denormalize > > >; export default function useSuspense< E extends EndpointInterface< FetchFunction, Schema | undefined, undefined | false >, >( endpoint: E, ...args: MaybeRefsOrGettersNullable> | readonly [null] ): Promise< DeepReadonly< ComputedRef< E['schema'] extends undefined | null ? ResolveType | undefined : DenormalizeNullable > > >; export default async function useSuspense( endpoint: any, ...args: any[] ): Promise { const stateRef = injectState(); const controller = useController(); // Track top-level reactive args (Refs are unwrapped). This allows props/refs to trigger updates. const resolvedArgs = computed(() => args.map(a => unref(a as any)) as any); // Compute a key that changes when args change (including reactive props) const argsKey = computed(() => resolvedArgs.value[0] !== null ? endpoint.key(...resolvedArgs.value) : '', ); // Compute response meta reactively so we can respond to store updates const responseMeta = computed(() => { return controller.getResponseMeta( endpoint, ...resolvedArgs.value, stateRef.value, ); }); const maybeFetch = async () => { const currentKey = argsKey.value; if (!currentKey) return; const meta = responseMeta.value; const forceFetch = meta.expiryStatus === ExpiryStatus.Invalid; if (Date.now() <= meta.expiresAt && !forceFetch) return; await controller.fetch(endpoint, ...resolvedArgs.value); }; // Watch for changes to key, expiry, or store state that require refetch watch( () => { const m = responseMeta.value; return [ m.expiresAt, m.expiryStatus, stateRef.value.lastReset, argsKey.value, ]; }, () => { return maybeFetch(); }, ); // Maintain GC refcounts on data mount/changes watch( () => responseMeta.value.data, (_newVal, _oldVal, onCleanup) => { const decrement = responseMeta.value.countRef(); onCleanup(() => decrement?.()); }, { immediate: true }, ); // Trigger on initial call await maybeFetch(); // Return readonly computed ref - Vue automatically unwraps in templates and reactive contexts return readonly(computed(() => responseMeta.value.data)); }