Endpoint
Endpoint sirve para cualquier función asíncrona (una que devuelve una Promise).
Los Endpoints definen una interfaz estándar con tipado fuerte de metadatos y ciclos de vida relevantes
útiles para Reactive Data Client y otros stores.
Paquete: @data-client/endpoint
Interfaz
- Interface
- Class
- EndpointExtraOptions
export interface EndpointInterface<
F extends FetchFunction = FetchFunction,
S extends Schema | undefined = Schema | undefined,
M extends true | undefined = true | undefined,
> extends EndpointExtraOptions<F> {
(...args: Parameters<F>): InferReturn<F, S>;
key(...args: Parameters<F>): string;
readonly sideEffect?: M;
readonly schema?: S;
}
class Endpoint<F extends (...args: any) => Promise<any>>
implements EndpointInterface
{
constructor(fetchFunction: F, options: EndpointOptions);
key(...args: Parameters<F>): string;
readonly sideEffect?: true;
readonly schema?: Schema;
fetch: F;
extend(options: EndpointOptions): Endpoint;
}
export interface EndpointOptions extends EndpointExtraOptions {
key?: (params: any) => string;
sideEffect?: true | undefined;
schema?: Schema;
}
export interface EndpointExtraOptions<F extends FetchFunction = FetchFunction> {
/** Default data expiry length, will fall back to NetworkManager default if not defined */
readonly dataExpiryLength?: number;
/** Default error expiry length, will fall back to NetworkManager default if not defined */
readonly errorExpiryLength?: number;
/** Poll with at least this frequency in milliseconds */
readonly pollFrequency?: number;
/** Marks cached resources as invalid if they are stale */
readonly invalidIfStale?: boolean;
/** Enables optimistic updates for this request - uses return value as assumed network response */
readonly getOptimisticResponse?: (
snap: SnapshotInterface,
...args: Parameters<F>
) => ResolveType<F>;
/** Determines whether to throw or fallback to */
readonly errorPolicy?: (error: any) => 'soft' | undefined;
/** User-land extra data to send */
readonly extra?: any;
}
Uso
Endpoint hace que las funciones asíncronas existentes se puedan usar en cualquier contexto de Reactive Data Client con verificación completa de TypeScript.
import { Endpoint } from '@data-client/rest'; import { Todo } from './interface'; const getTodoOriginal = (id: number): Promise<Todo> => Promise.resolve({ id, title: 'delectus aut autem ' + id, completed: false, userId: 1, }); export const getTodo = new Endpoint(getTodoOriginal);
import { useSuspense } from '@data-client/react'; import { getTodo } from './api'; function TodoDetail() { const todo = useSuspense(getTodo, 1); return <div>{todo.title}</div>; } render(<TodoDetail />);
Compartir configuración
Usa Endpoint.extend() en lugar de {...getTodo} (spread)
const getTodoNormalized = getTodo.extend({ schema: Todo });
const getTodoUpdatingEveryFiveSeconds = getTodo.extend({ pollFrequency: 5000 });
Ciclo de vida
Éxito
Error
Miembros de Endpoint
Los miembros funcionan también como opciones (segundo argumento del constructor). Aunque ninguno es obligatorio, los primeros tienen valores por defecto.
key: (params) => string
Serializa los parámetros. Se usa para construir una clave de búsqueda en stores globales.
Por defecto:
`${this.name} ${JSON.stringify(params)}`;
Al sobrescribir key, asegúrate de incluir también un testKey actualizado si
piensas usar ese método.
testKey(key): boolean
Devuelve true si la key (de fetch) proporcionada coincide con este endpoint.
Se usa para los interceptors de mock con <MockResolver />
name: string
Se usa en key para distinguir endpoints. Debe ser único a nivel global.
Por defecto es this.fetch.name
Esto puede fallar en compilaciones de producción que cambian los nombres de las funciones. Esto suele conocerse como function name mangling.
En esos casos puedes sobrescribir name o deshabilitar el mangling de funciones.
sideEffect: boolean
Se usa para indicar que el endpoint podría tener efectos secundarios (no idempotente). Esto impide que se use con useSuspense() o useFetch(), ya que estos pueden llamar al endpoint un número impredecible de veces.
schema: Schema
Definición declarativa de cómo procesar las respuestas
- dónde esperar Entities
- Funciones para deserializar campos
No proporcionar esta opción significa que no se extraerá ninguna entidad.
import { Endpoint, Entity } from '@data-client/endpoint';
class User extends Entity {
id = '';
username = '';
}
const getUser = new Endpoint(
({ id }) => fetch(`/users/${id}`),
{ schema: User }
);
dataExpiryLength?: number
Tiempo de vida personalizado en la caché de los datos del recurso obtenido. Reemplazará el valor establecido en NetworkManager.
Más información sobre el tiempo de caducidad
errorExpiryLength?: number
Tiempo de vida personalizado de los errores del recurso obtenido. Reemplazará el valor establecido en NetworkManager.
errorPolicy?: (error: any) => 'soft' | undefined
'soft' usará los datos obsoletos (si existen) en caso de error; undefined o no proporcionar la opción provocará un error.
Más información sobre errorPolicy
errorPolicy(error) {
return error.status >= 500 ? 'soft' : undefined;
}
invalidIfStale: boolean
Indica que los datos obsoletos deben considerarse inutilizables y, por tanto, no devolverse desde la caché. Esto significa que useSuspense() se suspenderá cuando los datos estén obsoletos aunque ya existan en la caché.
pollFrequency: number
Frecuencia en milisegundos con la que se realiza el sondeo. Requiere usar useSubscription() o useLive() para tener efecto.
getOptimisticResponse: (snap, ...args) => expectedResponse
Cuando se proporciona, cualquier fetch con este endpoint se comportará como si el valor de retorno expectedResponse
de esta función fuera una respuesta de red exitosa. Cuando el fetch real se completa (ya sea
con fallo o con éxito), la actualización optimista se reemplaza por la respuesta de red real.
import { resource } from '@data-client/rest'; import { Post } from './Post'; export { Post }; export const PostResource = resource({ path: '/posts/:id', searchParams: {} as { userId?: string | number } | undefined, schema: Post, }).extend('vote', { path: '/posts/:id/vote', method: 'POST', body: undefined, schema: Post, getOptimisticResponse(snapshot, { id }) { const post = snapshot.get(Post, { id }); if (!post) throw snapshot.abort; return { id, votes: post.votes + 1, }; }, });
update()
(normalizedResponseOfThis, ...args) =>
({ [endpointKey]: (normalizedResponseOfEndpointToUpdate) => updatedNormalizedResponse) })
Prueba a usar Collections en su lugar.
¡Son mucho más fáciles de usar y más robustas!
type UpdateFunction<
Source extends EndpointInterface,
Updaters extends Record<string, any> = Record<string, any>,
> = (
source: ResultEntry<Source>,
...args: Parameters<Source>
) => { [K in keyof Updaters]: (result: Updaters[K]) => Updaters[K] };
El caso más sencillo:
const createUser = new RestEndpoint({
path: '/user',
method: 'POST',
schema: User,
update: (newUserId: string) => ({
[userList.key()]: (users = []) => [newUserId, ...users],
}),
});
Más actualizaciones:
const allusers = useSuspense(userList);
const adminUsers = useSuspense(userList, { admin: true });
El endpoint siguiente garantiza que el nuevo usuario aparezca de inmediato en los usos anteriores.
const createUser = new RestEndpoint({
path: '/user',
method: 'POST',
schema: User,
update: (newUserId, newUser) => {
const updates = {
[userList.key()]: (users = []) => [newUserId, ...users],
];
if (newUser.isAdmin) {
updates[userList.key({ admin: true })] = (users = []) => [newUserId, ...users];
}
return updates;
},
});
extend(options): Endpoint
Se puede usar para personalizar aún más la definición del endpoint
const getUser = new Endpoint(({ id }) => fetch(`/users/${id}`));
const getUserNormalized = getUser.extend({ schema: User });
Además de los miembros, se puede enviar fetch para sobrescribir la función fetch.
Ejemplos
- Basic
- With Schema
- List
import { Endpoint } from '@data-client/endpoint';
const UserDetail = new Endpoint(
({ id }) => fetch(`/users/${id}`).then(res => res.json())
);
import { Endpoint, Entity } from '@data-client/endpoint';
class User extends Entity {
id = '';
username = '';
}
const UserDetail = new Endpoint(
({ id }) => fetch(`/users/${id}`).then(res => res.json()),
{ schema: User }
);
import { Endpoint, Entity } from '@data-client/endpoint';
class User extends Entity {
id = '';
username = '';
}
const UserList = new Endpoint(
() => fetch(`/users/`).then(res => res.json()),
{ schema: [User] }
);
- React
- JS/Node Schema
import { useSuspense, useController } from '@data-client/react';
import { UserDetail } from './api/User';
import UserForm from './UserForm';
function UserProfile({ id }: { id: string }) {
const user = useSuspense(UserDetail, { id });
const ctrl = useController();
return <UserForm user={user} onSubmit={() => ctrl.fetch(UserDetail)} />;
}
const user = await UserDetail({ id: '5' });
console.log(user);
Adicional
Motivación
Hay una distinción entre
- Qué es una API de red
- Cómo hacer una petición, qué campos se esperan en la respuesta, etc.
- Cómo se usa
- Enlazar datos, sondeo (polling), disparar un fetch imperativo, etc.
Por lo tanto, separar claramente las responsabilidades de estos dos conceptos tiene muchos beneficios.
Con los TypeScript Standard Endpoints definimos un estándar para declarar en
TypeScript la definición de una API de red.
- Permite a los autores de APIs publicar paquetes npm con las interfaces de su API
- Las definiciones las puede consumir cualquier librería compatible, lo que facilita su uso en librerías como Vue, React o Angular
- Escribir pipelines de generación de código se vuelve mucho más fácil, ya que la salida es mínima
- Los desarrolladores de producto pueden usar las definiciones en multitud de contextos donde los comportamientos varían
- Los desarrolladores de producto pueden compartir código fácilmente entre plataformas con necesidades de comportamiento distintas, como React Native y React Web
Qué hay en un Endpoint
- Una función que resuelve los resultados
- Una función para almacenar esos resultados de forma única
- Opcional: información sobre cómo almacenar los datos en una caché normalizada
- Opcional: si la petición podría tener efectos secundarios, para evitar llamadas repetidas