feat: add useApi composable

Signed-off-by: Fernando Fernández <ferferga@hotmail.com>
This commit is contained in:
Fernando Fernández 2024-01-03 23:09:28 +01:00
parent f80a1084c1
commit 7d2656454b

View File

@ -4,16 +4,23 @@ import { items } from '@/store/items';
import type { Api } from '@jellyfin/sdk';
import { ItemFields, type BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import type { AxiosResponse } from 'axios';
import { computed, getCurrentScope, ref, toValue, watch, type ComputedRef, type Ref } from 'vue';
import { computed, getCurrentScope, ref, toValue, unref, watch, type ComputedRef, type MaybeRef, type Ref } from 'vue';
const allFields = Object.freeze(Object.values(ItemFields));
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment */
type ExtractResponseType<T> = Awaited<T> extends AxiosResponse<infer U, any> ?
(U extends BaseItemDto ? U : (U extends BaseItemDto[] ? U : undefined))
: undefined;
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unsafe-return */
type ParametersAsGetters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? { [K in keyof P]: () => P[K] } : never;
type ExtractResponseDataType<T> = Awaited<T> extends AxiosResponse<infer U, any> ? U : undefined;
/**
* If response.data is BaseItemDto or BaseItemDto[], returns it. Otherwise, returns undefined.
*/
type ExtractBaseItemDtoResponse<T> = (ExtractResponseDataType<T> extends BaseItemDto ? ExtractResponseDataType<T> : (ExtractResponseDataType<T> extends BaseItemDto[] ? ExtractResponseDataType<T> : undefined));
/**
* If response.data is BaseItemDto or BaseItemDto[], returns undefined. Otherwise, returns the data type.
*/
type ExtractResponseType<T> = (ExtractResponseDataType<T> extends BaseItemDto ? undefined : (ExtractResponseDataType<T> extends BaseItemDto[] ? undefined : ExtractResponseDataType<T>));
/**
* Ensures the function is used in the given
* @param func - The function to check.
@ -25,6 +32,50 @@ function ensureCorrectUsage(func: any): void {
}
}
/**
* Perfoms the given request
* @param api - Relevant API
* @param methodName - Method to execute
* @param isBaseItem - Whether the request is BaseItemDto based or not
* @param requestData - Ref to hold the request data
* @param loading - Ref to hold the loading state
* @param args - Func args
*/
async function resolveAndAdd<T extends Record<K, (...args: any[]) => any>, K extends keyof T>(
api: (api: Api) => T,
methodName: K,
isBaseItem: boolean,
requestData: Ref<Awaited<ReturnType<T[K]>['data']> | undefined>,
loading: Ref<boolean>,
...args: ParametersAsGetters<T[K]>): Promise<void> {
/**
* We add all BaseItemDto's fields for consistency in what we can expect from the store.
* toValue normalizes the getters.
*/
const extendedParams = [
{ ...toValue(args[0]), fields: allFields },
...args.slice(1).map((a) => toValue(a))
] as Parameters<T[K]>;
try {
loading.value = true;
const response = await remote.sdk.newUserApi(api)[methodName](...extendedParams) as Awaited<ReturnType<T[K]>>;
if (response.data) {
requestData.value = response.data;
if (isBaseItem) {
items.rawAdd(response.data as BaseItemDto | BaseItemDto[]);
}
} else {
requestData.value = undefined;
}
} catch {} finally {
loading.value = false;
}
}
/**
* Reactively performs item requests to the API:
*
@ -59,13 +110,81 @@ function ensureCorrectUsage(func: any): void {
export function useBaseItem<T extends Record<K, (...args: any[]) => any>, K extends keyof T>(
api: (api: Api) => T,
methodName: K
): (this: any, ...args: ParametersAsGetters<T[K]>) => Promise<{ loading: Ref<boolean>, data: ComputedRef<ExtractResponseType<ReturnType<T[K]>>> }> {
ensureCorrectUsage(remote.sdk.newUserApi(api)[methodName]);
): (this: any, ...args: ParametersAsGetters<T[K]>) => Promise<{ loading: Ref<boolean>, data: ComputedRef<ExtractBaseItemDtoResponse<ReturnType<T[K]>>> }> {
ensureCorrectUsage(remote.sdk.newUserApi(unref(api))[unref(methodName)]);
/**
* For some reason, the watcher also fires on startup, so we need to keep track of that to avoid double requests.
*/
let initialFetchDone = false;
const loading = ref(true);
const requestData = ref<Awaited<ReturnType<T[K]>['data']>>();
const calledFunctions = computed(() => `${api.name}.${methodName.toString()}`);
const calledFunctions = computed(() => `${unref(api).name}.${unref(methodName).toString()}`);
/**
* Returns a proxy ref from the store
*/
// @ts-expect-error - Typings get too complex at this point
const data = computed<ExtractBaseItemDtoResponse<ReturnType<T[K]>>>(() => {
if (typeof requestData.value === 'object') {
// @ts-expect-error - We check both capitalizations just in case
const itemArray: BaseItemDto[] | undefined = requestData.value.items ?? requestData.value.Items;
if (Array.isArray(itemArray)) {
const ids = itemArray.map((i) => i.Id).filter((id): id is string => typeof id === 'string');
return items.getItemsById(ids).filter((item): item is BaseItemDto => typeof item === 'object');
} else {
return items.getItemById((requestData.value as BaseItemDto).Id);
}
}
});
return async function (this: any, ...args: ParametersAsGetters<T[K]>) {
const run = async (args: ParametersAsGetters<T[K]>): Promise<void> => {
try {
await resolveAndAdd(unref(api), unref(methodName), true, requestData, loading, ...args);
initialFetchDone = true;
} catch {}
};
if (getCurrentScope() !== undefined) {
const cbk = async (): Promise<void> => {
if (initialFetchDone) {
await run(args);
}
};
watch(args, cbk);
watch(calledFunctions, cbk);
}
await run(args);
return { loading, data };
};
}
/**
* Initial JSDoc
*
* @param api
* @param methodName
* @returns
*/
export function useApi<T extends Record<K, (...args: any[]) => any>, K extends keyof T>(
api: MaybeRef<(api: Api) => T>,
methodName: MaybeRef<K>
): (this: any, ...args: ParametersAsGetters<T[K]>) => Promise<{ loading: Ref<boolean>, data: ComputedRef<ExtractResponseType<ReturnType<T[K]>>> }> {
ensureCorrectUsage(remote.sdk.newUserApi(unref(api))[unref(methodName)]);
/**
* For some reason, the watcher also fires on startup, so we need to keep track of that to avoid double requests.
*/
let initialFetchDone = false;
const loading = ref(true);
const requestData = ref<Awaited<ReturnType<T[K]>['data']>>();
const calledFunctions = computed(() => `${unref(api).name}.${unref(methodName).toString()}`);
/**
* Returns a proxy ref from the store
@ -86,46 +205,30 @@ export function useBaseItem<T extends Record<K, (...args: any[]) => any>, K exte
}
});
const resolveAndAdd = async (args: Array<() => ParametersAsGetters<T[K]>>): Promise<void> => {
/**
* We add all BaseItemDto's fields for consistency in what we can expect from the store.
* toValue normalizes the getters.
*/
const extendedParams = [
{ ...toValue(args[0]), fields: allFields },
...args.slice(1).map((a) => toValue(a))
] as Parameters<T[K]>;
try {
loading.value = true;
const response = await remote.sdk.newUserApi(api)[methodName](...extendedParams) as Awaited<ReturnType<T[K]>>;
if (response.data) {
requestData.value = response.data;
items.rawAdd(response.data as BaseItemDto | BaseItemDto[]);
} else {
requestData.value = undefined;
}
} catch {} finally {
loading.value = false;
initialFetchDone = true;
}
};
return async function (this: any, ...args: ParametersAsGetters<T[K]>) {
const run = async (args: ParametersAsGetters<T[K]>): Promise<void> => {
try {
await resolveAndAdd(unref(api), unref(methodName), false, requestData, loading, ...args);
initialFetchDone = true;
} catch {}
};
if (getCurrentScope() !== undefined) {
watch([args, calledFunctions], async () => {
// eslint-disable-next-line sonarjs/no-identical-functions
const cbk = async (): Promise<void> => {
if (initialFetchDone) {
await resolveAndAdd(args);
await run(args);
}
});
};
watch(args, cbk);
watch(calledFunctions, cbk);
}
await resolveAndAdd(args);
await run(args);
return { loading, data };
};
}
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment */
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unsafe-return */