Endpoint
Endpoint serve para qualquer função assíncrona (uma que retorna uma Promise).
Endpoints definem uma interface padrão fortemente tipada com metadados e ciclos de vida relevantes,
úteis para o Reactive Data Client e outros stores.
Pacote: @data-client/endpoint
Interface
- 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 torna funções assíncronas existentes utilizáveis em qualquer contexto do Reactive Data Client, com verificação completa do 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 />);
Compartilhamento de configuração
Use Endpoint.extend() em vez de {...getTodo} (spread)
const getTodoNormalized = getTodo.extend({ schema: Todo });
const getTodoUpdatingEveryFiveSeconds = getTodo.extend({ pollFrequency: 5000 });
Ciclo de vida
Sucesso
Erro
Membros do Endpoint
Os membros também funcionam como opções (segundo argumento do construtor). Embora nenhum seja obrigatório, os primeiros têm valores padrão.
key: (params) => string
Serializa os parâmetros. É usado para construir uma chave de busca em stores globais.
Padrão:
`${this.name} ${JSON.stringify(params)}`;
Ao sobrescrever key, não se esqueça de incluir também um testKey atualizado se
você pretende usar esse método.
testKey(key): boolean
Retorna true se a key (de fetch) fornecida corresponder a este endpoint.
Isso é usado em interceptors de mock com o <MockResolver />
name: string
Usado em key para distinguir endpoints. Deve ser globalmente único.
O padrão é this.fetch.name
Isso pode quebrar em builds de produção que alteram nomes de funções. Isso costuma ser conhecido como function name mangling.
Nesses casos, você pode sobrescrever name ou desativar o mangling de funções.
sideEffect: boolean
Usado para indicar que o endpoint pode ter efeitos colaterais (não idempotente). Isso o impede de ser usado com useSuspense() ou useFetch(), pois eles podem chamar o endpoint um número imprevisível de vezes.
schema: Schema
Definição declarativa de como processar as respostas
- onde esperar Entities
- Funções para desserializar campos
Não informar esta opção significa que nenhuma entity será extraída.
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
Tempo de vida personalizado, no cache, dos dados do recurso buscado. Substitui o valor definido no NetworkManager.
Saiba mais sobre o tempo de expiração
errorExpiryLength?: number
Tempo de vida personalizado dos erros de dados do recurso buscado. Substitui o valor definido no NetworkManager.
errorPolicy?: (error: any) => 'soft' | undefined
'soft' usará dados desatualizados (se existirem) em caso de erro; undefined, ou não informar a opção, resultará em erro.
errorPolicy(error) {
return error.status >= 500 ? 'soft' : undefined;
}
invalidIfStale: boolean
Indica que dados desatualizados devem ser considerados inutilizáveis e, portanto, não ser retornados do cache. Isso significa que useSuspense() vai suspender quando os dados estiverem desatualizados, mesmo que já existam no cache.
pollFrequency: number
Frequência, em milissegundos, do polling. Requer o uso de useSubscription() ou useLive() para ter efeito.
getOptimisticResponse: (snap, ...args) => expectedResponse
Quando informado, qualquer fetch com este endpoint se comportará como se o valor de retorno expectedResponse
desta função fosse uma resposta de rede bem-sucedida. Quando o fetch real for concluído (com falha
ou com sucesso), a atualização otimista será substituída pela resposta real da rede.
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) })
Experimente usar Collections no lugar.
Elas são muito mais fáceis de usar e mais 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] };
Caso mais simples:
const createUser = new RestEndpoint({
path: '/user',
method: 'POST',
schema: User,
update: (newUserId: string) => ({
[userList.key()]: (users = []) => [newUserId, ...users],
}),
});
Mais atualizações:
const allusers = useSuspense(userList);
const adminUsers = useSuspense(userList, { admin: true });
O endpoint abaixo garante que o novo usuário apareça imediatamente nos usos acima.
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
Pode ser usado para personalizar ainda mais a definição do endpoint
const getUser = new Endpoint(({ id }) => fetch(`/users/${id}`));
const getUserNormalized = getUser.extend({ schema: User });
Além dos membros, fetch pode ser enviado para substituir a função de fetch.
Exemplos
- 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);
Adicionais
Motivação
Existe uma distinção entre
- O que é uma API de rede
- Como fazer uma requisição, quais campos esperar na resposta, etc.
- Como ela é usada
- Vincular dados, polling, disparar fetch imperativo, etc.
Por isso, há muitos benefícios em criar uma separação clara de responsabilidades entre esses dois conceitos.
Com os TypeScript Standard Endpoints, definimos um padrão para declarar em
TypeScript a definição de uma API de rede.
- Permite que autores de APIs publiquem pacotes npm contendo as interfaces de suas APIs
- As definições podem ser consumidas por qualquer biblioteca compatível, facilitando o consumo entre bibliotecas como Vue, React e Angular
- Escrever pipelines de geração de código fica muito mais fácil, pois a saída é mínima
- Desenvolvedores de produto podem usar as definições em diversos contextos nos quais os comportamentos variam
- Desenvolvedores de produto podem compartilhar código facilmente entre plataformas com necessidades de comportamento distintas, como React Native e React Web
O que há em um Endpoint
- Uma função que resolve os resultados
- Uma função para armazenar esses resultados de forma única
- Opcional: informações sobre como armazenar os dados em um cache normalizado
- Opcional: se a requisição pode ter efeitos colaterais - para evitar chamadas repetidas