Controller
Controller é um singleton que fornece acesso seguro ao store flux e ao ciclo de vida do Reactive Data Client.
Controller memoiza todo o acesso ao store, permitindo uma garantia global de igualdade referencial e o melhor desempenho
de renderização e de obtenção de dados.
Controller é fornecido:
- Aos Managers, como o primeiro argumento em Manager.middleware
- No Vue, com useController()
- Em testes unitários de composables, com
renderDataCompose()de@data-client/vue/test
class Controller {
/*************** Action Dispatchers ***************/
fetch(endpoint, ...args): ReturnType<E>;
fetchIfStale(endpoint, ...args): ReturnType<E> | undefined;
expireAll({ testKey }): Promise<void>;
invalidate(endpoint, ...args): Promise<void>;
invalidateAll({ testKey }): Promise<void>;
resetEntireStore(): Promise<void>;
set(queryable, ...args, value): Promise<void>;
set([Entity], rows): Promise<void>;
setResponse(endpoint, ...args, response): Promise<void>;
setError(endpoint, ...args, error): Promise<void>;
resolve(endpoint, { args, response, fetchedAt, error }): Promise<void>;
subscribe(endpoint, ...args): Promise<void>;
unsubscribe(endpoint, ...args): Promise<void>;
/*************** Data Access ***************/
get(queryable, ...args, state): Denormalized<typeof queryable>;
getResponse(endpoint, ...args, state): { data; expiryStatus; expiresAt };
getError(endpoint, ...args, state): ErrorTypes | undefined;
snapshot(state: State<unknown>, fetchedAt?: number): SnapshotInterface;
getState(): State<unknown>;
}
Dispatchers de actions
fetch(endpoint, ...args)
Faz o fetch do endpoint com os args fornecidos, atualizando o cache do Reactive Data Client com a resposta ou o erro ao concluir.
- Create
- Update
- Delete
<script setup lang="ts">
import { useController } from '@data-client/vue';
import { PostResource } from './PostResource';
const ctrl = useController();
const handleSubmit = (e: Event) =>
ctrl.fetch(
PostResource.getList.push,
new FormData(e.target as HTMLFormElement),
);
</script>
<template>
<form @submit.prevent="handleSubmit"><!-- ... --></form>
</template>
<script setup lang="ts">
import { useController } from '@data-client/vue';
import { PostResource } from './PostResource';
const props = defineProps<{ id: string }>();
const ctrl = useController();
const handleSubmit = (e: Event) =>
ctrl.fetch(
PostResource.update,
{ id: props.id },
new FormData(e.target as HTMLFormElement),
);
</script>
<template>
<form @submit.prevent="handleSubmit"><!-- ... --></form>
</template>
<script setup lang="ts">
import { useController } from '@data-client/vue';
import { useRouter } from 'vue-router';
import { Post, PostResource } from './PostResource';
const props = defineProps<{ post: Post }>();
const ctrl = useController();
const router = useRouter();
const handleDelete = async () => {
await ctrl.fetch(PostResource.delete, { id: props.post.id });
router.push('/');
};
</script>
<template>
<div>
<h3>{{ post.title }}</h3>
<button @click="handleDelete">X</button>
</div>
</template>
fetch tem o mesmo valor de retorno que o Endpoint passado a ele.
Ao usar schemas, o valor desnormalizado é retornado
const controller = useController();
const post = await controller.fetch(
PostResource.getList.push,
createPayload,
);
post.title;
post.pk();
Endpoint.sideEffect
sideEffect altera o comportamento
true
- Resolve antes de confirmar (commit) as atualizações de cache do Reactive Data Client. (React 16, 17)
- Cada chamada sempre causará um novo fetch.
false | undefined
- Resolve depois de confirmar (commit) as atualizações de cache do Reactive Data Client.
- Requisições idênticas são deduplicadas globalmente, permitindo apenas uma requisição em andamento por vez.
- Para garantir que uma nova requisição seja iniciada, certifique-se de abortar quaisquer requisições em andamento.
fetchIfStale(endpoint, ...args)
Faz o fetch apenas se o endpoint for considerado 'desatualizado (stale)'.
Isso pode ser útil ao fazer prefetch de dados, pois evita buscar em excesso dados que ainda estão atualizados.
Um exemplo com um roteador fetch-as-you-render:
{
name: 'IssueList',
component: lazyPage('IssuesPage'),
title: 'issue list',
resolveData: async (
controller: Controller,
{ owner, repo }: { owner: string; repo: string },
searchParams: URLSearchParams,
) => {
const q = searchParams?.get('q') || 'is:issue is:open';
await controller.fetchIfStale(IssueResource.search, {
owner,
repo,
q,
});
},
},
expireAll({ testKey })
Define o status de expiração de todas as respostas que correspondem a testKey como Stale.
Isso às vezes é útil para disparar a atualização apenas dos dados exibidos no momento quando há muitas parametrizações em cache.
<script setup lang="ts">
import { useController } from '@data-client/vue';
import { AccountResource, TradeResource, type Trade } from './resources';
import TradeForm from './TradeForm.vue';
const props = defineProps<{ userId: string }>();
const ctrl = useController();
const handleTrade = async (trade: Trade) => {
await ctrl.fetch(
TradeResource.getList.push,
{ user: props.userId },
trade,
);
ctrl.expireAll(AccountResource.get);
ctrl.expireAll(AccountResource.getList);
};
</script>
<template>
<TradeForm @submit="handleTrade" />
</template>
Para reduzir a carga, melhorar o desempenho e melhorar a consistência do estado, muitas vezes é melhor incluir os efeitos colaterais da mutação na resposta da mutação.
invalidate(endpoint, ...args)
Força o refetch em useSuspense com o mesmo Endpoint e os mesmos parâmetros. Componentes montados continuam exibindo seus dados atuais até que o refetch seja resolvido.
<script setup lang="ts">
import { useController, useSuspense } from '@data-client/vue';
import { ArticleResource } from './ArticleResource';
const props = defineProps<{ id: string }>();
const ctrl = useController();
const article = await useSuspense(ArticleResource.get, () => ({
id: props.id,
}));
</script>
<template>
<div>
<h1>{{ article.title }}</h1>
<button @click="ctrl.invalidate(ArticleResource.get, { id })">
Refetch
</button>
</div>
</template>
Use schema.Invalidate para invalidar todos os endpoints que contêm uma determinada entity.
Para REST, experimente usar Resource.delete
// deletes MyResource(5)
// this will refetch MyResource.get({id: '5'})
// and remove it from MyResource.getList
controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' });
invalidateAll({ testKey })
Invalida todas as chaves de endpoint que correspondem a testKey.
<script setup lang="ts">
import { useController, useSuspense } from '@data-client/vue';
import { ArticleResource } from './ArticleResource';
const props = defineProps<{ id: string }>();
const ctrl = useController();
const article = await useSuspense(ArticleResource.get, () => ({
id: props.id,
}));
</script>
<template>
<div>
<h1>{{ article.title }}</h1>
<button @click="ctrl.invalidateAll(ArticleResource.get)">
Refetch
</button>
</div>
</template>
Aqui limpamos apenas os endpoints GET que usam o domínio test.com. Isso significa que outros domínios permanecem em cache.
const myDomain = 'http://test.com';
const testKey = (key: string) => key.startsWith(`GET ${myDomain}`);
function useLogout() {
const ctrl = useController();
return () => ctrl.invalidateAll({ testKey });
}
Geralmente também é uma boa ideia limpar o cache em um 401 (não autorizado) com o LogoutManager.
import { createApp } from 'vue';
import {
DataClientPlugin,
LogoutManager,
getDefaultManagers,
} from '@data-client/vue';
import App from './App.vue';
import { unAuth } from '../authentication';
const myDomain = 'http://test.com';
const testKey = (key: string) => key.startsWith(`GET ${myDomain}`);
const managers = [
new LogoutManager({
handleLogout(controller) {
// call custom unAuth function we defined
unAuth();
// still reset the store
controller.invalidateAll({ testKey });
},
}),
...getDefaultManagers(),
];
const app = createApp(App);
app.use(DataClientPlugin, { managers });
app.mount('#app');
resetEntireStore()
Redefine/limpa todo o cache do Reactive Data Client. Nenhuma requisição em andamento será resolvida.
Isso normalmente é usado ao fazer logout ou ao trocar de usuário autenticado.
<script setup lang="ts">
import { useController, useSuspense } from '@data-client/vue';
import { CurrentUserResource } from './CurrentUserResource';
import { impersonateUser } from './auth';
const USER_NUMBER_ONE: string = '1111';
const user = await useSuspense(CurrentUserResource.get);
const ctrl = useController();
const becomeAdmin = () => {
// Changes the current user
impersonateUser(USER_NUMBER_ONE);
ctrl.resetEntireStore();
};
</script>
<template>
<div>
<h1>{{ user.name }}</h1>
<button @click="becomeAdmin">Be Number One</button>
</div>
</template>
set(queryable, ...args, value)
Atualiza qualquer Schema Queryable ou várias entities de uma vez com um schema Array ou Values.
ctrl.set(
Todo,
// which Todo to update
{ id: '5' },
// merge this data into the Todo in the store
{ id: '5', title: 'tell me friends how great Data Client is' },
);
O valor é tipado pelo schema: uma Entity recebe seus campos (números e strings podem ser qualquer um dos dois),
enquanto uma Collection ou All recebe uma lista de linhas. Uma Query
recebe a entrada do schema que envolve, já que set() normaliza esse schema em vez de reverter process().
ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]);
Quando cada membro declara seu discriminador como um literal (como readonly type = 'first'), uma linha de
Union é verificada em relação ao membro que ela seleciona, então { type: 'first', secondField: 1 } é um
erro. Apenas campos declarados são aceitos, então uma chave lida por uma
função schemaAttribute deve ser declarada em cada membro.
Funções podem ser usadas no valor quando são usados dados derivados. Isso evita condições de corrida.
const id = '2';
ctrl.set(Article, { id }, article => ({ id, votes: article.votes + 1 }));
set([Entity], rows)
Passe um schema Array ([Todo] ou new schema.Array(Todo)) e uma lista de linhas para atualizar
várias entities em uma única atualização do store. Cada linha é mesclada com a entity armazenada; entities que não estão na lista não são alteradas.
ctrl.set(
[Todo],
[
{ id: '5', completed: true },
{ id: '6', completed: false },
],
);
As linhas são tipadas pelos campos da Entity; números e strings podem ser qualquer um dos dois, e valores de objeto, array e Date não são verificados, já que as linhas são entrada bruta.
Para listas que misturam tipos de Entity, use uma Union; cada linha é armazenada de acordo com seu type:
const Feed = new schema.Union({ post: Post, comment: Comment }, 'type');
ctrl.set(
[Feed],
[
{ id: '1', type: 'post', title: 'Hello' },
{ id: '7', type: 'comment', body: 'Nice!' },
],
);
Para excluir várias entities de uma vez, use Invalidate; as linhas só precisam dos campos de pk:
ctrl.set([new schema.Invalidate(Todo)], [{ id: '5' }, { id: '6' }]);
Para excluir uma, passe o schema Invalidate e sua linha:
ctrl.set(new schema.Invalidate(Todo), { id: '5' });
Schemas Values recebem, em vez disso, um objeto de linhas:
ctrl.set(new schema.Values(Todo), {
'5': { id: '5', completed: true },
'6': { id: '6', completed: false },
});
Schemas Array, Values e Invalidate não recebem args (então Entity.pk() e Entity.process()
recebem []) nem função de atualização. Linhas que compartilham uma pk são mescladas na ordem da lista, sem
Entity.shouldReorder(). Use isso em vez de chamar set() uma vez por linha, como ao
agrupar em lote atualizações de stream de alta frequência.
setResponse(endpoint, ...args, response)
Armazena response no cache para o Endpoint e os args fornecidos.
Quaisquer componentes suspensos aguardando o Endpoint e os args fornecidos serão resolvidos.
Se já existirem dados para o Endpoint e os args fornecidos, eles serão atualizados.
const ctrl = useController();
let websocket: WebSocket;
onMounted(() => {
websocket = new WebSocket(url);
websocket.onmessage = event =>
ctrl.setResponse(
EndpointLookup[event.endpoint],
...event.args,
event.data,
);
});
onUnmounted(() => websocket.close());
Isto mostra uma prova de conceito em Vue; no entanto, uma implementação de websockets com Manager seria muito mais robusta.
setError(endpoint, ...args, error)
Armazena o resultado do Endpoint e dos args como o erro fornecido.
resolve(endpoint, { args, response, fetchedAt, error })
Resolve um fetch específico, armazenando a response no cache.
É semelhante a setResponse, exceto que dispara a resolução de um fetch em andamento. Isso significa que a atualização otimista correspondente deixará de ser aplicada.
É usado no NetworkManager e deve ser usado ao processar requisições de fetch.
subscribe(endpoint, ...args)
Marca uma nova subscription a um Endpoint. Isso deve incrementar a subscription.
useSubscription e useLive chamam isso na montagem.
Isso pode ser útil para composables personalizados que façam subscribe/unsubscribe com base em outros fatores.
const controller = useController();
// args can be a ref, computed or getter; this re-runs when it changes
watchEffect(onCleanup => {
const currentArgs = toValue(args);
controller.subscribe(endpoint, ...currentArgs);
onCleanup(() => controller.unsubscribe(endpoint, ...currentArgs));
});
unsubscribe(endpoint, ...args)
Marca o fim da subscription a um Endpoint. Isso deve decrementar a subscription e, se a contagem chegar a 0, novas atualizações não serão mais recebidas automaticamente.
useSubscription e useLive chamam isso na desmontagem.
Acesso a dados
get(schema, ...args, state)
Busca qualquer Schema Queryable em state.
Exemplo
Isso é usado em useQuery e pode ser usado em Managers para acessar o store com segurança.
Em componentes, useQuery() mantém o resultado reativo. Em handlers de eventos, passe getState() para ler o store mais recente:
const ctrl = useController();
const toggle = (id: string) => {
const todo = ctrl.get(Todo, { id }, ctrl.getState());
if (todo) ctrl.set(Todo, { id }, { id, completed: !todo.completed });
};
getResponse(endpoint, ...args, state)
{
data: DenormalizeNullable<E['schema']>;
expiryStatus: ExpiryStatus;
expiresAt: number;
}
Obtém a resposta (globalmente estável em termos de referência) para um determinado par endpoint/args a partir do state fornecido.
data
Os dados da resposta desnormalizados. Garante estabilidade referencial global para todos os membros.
expiryStatus
export enum ExpiryStatus {
Invalid = 1,
InvalidIfStale,
Valid,
}
Valid
- Nunca suspenderá.
- Pode fazer fetch se os dados estiverem desatualizados
InvalidIfStale
- Suspenderá se os dados estiverem desatualizados.
- Pode fazer fetch se os dados estiverem desatualizados
Invalid
- Sempre suspenderá
- Sempre fará fetch
expiresAt
Um número que representa o momento em que expira. Compare com Date.now().
Example
Isso é usado em useCache, useSuspense e pode ser usado em Managers para buscar uma resposta com o state fornecido.
Em handlers de eventos, passe getState() para ler o store mais recente, como no exemplo de getState().
import {
type Manager,
type Middleware,
actionTypes,
} from '@data-client/vue';
export default class MyManager implements Manager {
declare protected websocket: WebSocket;
middleware: Middleware = controller => {
return next => async action => {
if (action.type === actionTypes.FETCH) {
console.log('The existing response of the requested fetch');
console.log(
controller.getResponse(
action.endpoint,
...action.args,
controller.getState(),
).data,
);
}
next(action);
};
};
cleanup() {
this.websocket.close();
}
}
getError(endpoint, ...args, state)
Obtém o erro, se houver, de um determinado endpoint. Retorna undefined quando não há erros.
snapshot(state, fetchedAt)
Returns a Snapshot.
getState()
Obtém o estado interno do Reactive Data Client que já foi confirmado (commit).
Isso deve ser usado apenas em handlers de eventos ou Managers.
Usar getState() em um computed() ou template não atualizará quando o store mudar. Use
useQuery() ou useCache() nesses casos.
const controller = useController();
const handleShare = () => {
// reads the latest store without making this handler reactive
const { data: article } = controller.getResponse(
ArticleResource.get,
{ id: props.id },
controller.getState(),
);
if (article) navigator.share({ title: article.title, url: article.url });
};
Mutações resolvem antes de o store ser atualizado, então leia o resultado delas a partir
do valor com que fetch() resolve, em vez de getState().