Controller
Controller es un singleton que proporciona acceso seguro al store flux y su ciclo de vida de Reactive Data Client.
Controller memoiza todo el acceso al store, lo que permite una garantía de igualdad referencial global y el máximo rendimiento
de renderizado y de recuperación de datos.
Controller se proporciona:
- A los Managers como primer argumento de Manager.middleware
- A Vue con useController()
- En las pruebas unitarias de composables con
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>;
}
Despachadores de Actions
fetch(endpoint, ...args)
Hace fetch del endpoint con los args dados y actualiza la caché de Reactive Data Client con la respuesta o el error al completarse.
- 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 tiene el mismo valor de retorno que el Endpoint que se le pasa.
Al usar schemas, se devuelve el valor desnormalizado
const controller = useController();
const post = await controller.fetch(
PostResource.getList.push,
createPayload,
);
post.title;
post.pk();
Endpoint.sideEffect
sideEffect cambia el comportamiento
true
- Se resuelve antes de confirmar (commit) las actualizaciones de la caché de Reactive Data Client. (React 16, 17)
- Cada llamada siempre provocará un nuevo fetch.
false | undefined
- Se resuelve después de confirmar (commit) las actualizaciones de la caché de Reactive Data Client.
- Las solicitudes idénticas se deduplican globalmente; solo se permite una solicitud en curso a la vez.
- Para asegurar que se inicie una solicitud nueva, asegúrate de abortar cualquier solicitud en curso existente.
fetchIfStale(endpoint, ...args)
Hace fetch solo si el endpoint se considera 'obsoleto'.
Esto puede ser útil al precargar datos, ya que evita obtener de más datos que aún están actualizados.
Un ejemplo con un router de 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 })
Establece el estado de caducidad de todas las respuestas que coincidan con testKey como obsoleto.
A veces es útil para activar la actualización solo de los datos que se muestran actualmente cuando hay muchas parametrizaciones en la caché.
<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 reducir la carga, mejorar el rendimiento y mejorar la consistencia del estado, a menudo es mejor incluir los efectos secundarios de la mutación en la respuesta de la mutación.
invalidate(endpoint, ...args)
Fuerza un nuevo fetch en useSuspense con el mismo Endpoint y los mismos parámetros. Los componentes montados siguen mostrando sus datos actuales hasta que se resuelva el nuevo fetch.
<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>
Usa schema.Invalidate para invalidar todos los endpoints que contengan una entity determinada.
Para REST prueba a 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 las claves de endpoint que coincidan con 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>
Aquí borramos solo los endpoints GET que usan el dominio test.com. Esto significa que los demás dominios permanecen en la caché.
const myDomain = 'http://test.com';
const testKey = (key: string) => key.startsWith(`GET ${myDomain}`);
function useLogout() {
const ctrl = useController();
return () => ctrl.invalidateAll({ testKey });
}
Normalmente también es buena idea borrar la caché ante un 401 (no autorizado) con 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()
Restablece/borra toda la caché de Reactive Data Client. Las solicitudes en curso no se resolverán.
Normalmente se usa al cerrar sesión o al cambiar de usuario 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)
Actualiza cualquier Schema Queryable, o muchas entities a la vez con un schema Array o 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' },
);
El valor se tipa según el schema: una Entity toma sus campos (los números y los strings pueden ser cualquiera de los dos),
mientras que una Collection o All toma una lista de filas. Una Query
toma la entrada del schema que envuelve, ya que set() normaliza ese schema en lugar de revertir process().
ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]);
Cuando cada miembro declara su discriminador como un literal (como readonly type = 'first'), una fila de
Union se comprueba contra el miembro que selecciona, por lo que { type: 'first', secondField: 1 } es un
error. Solo se aceptan los campos declarados, así que una clave leída por una
función schemaAttribute debe declararse en cada miembro.
Se pueden usar funciones como valor cuando se utilizan datos derivados. Esto evita condiciones de carrera.
const id = '2';
ctrl.set(Article, { id }, article => ({ id, votes: article.votes + 1 }));
set([Entity], rows)
Pasa un schema Array ([Todo] o new schema.Array(Todo)) y una lista de filas para actualizar
muchas entities en una sola actualización del store. Cada fila se combina con su entity almacenada; las entities que no están en la lista no se modifican.
ctrl.set(
[Todo],
[
{ id: '5', completed: true },
{ id: '6', completed: false },
],
);
Las filas se tipan según los campos de la Entity; los números y los strings pueden ser cualquiera de los dos, y los valores de tipo objeto, array y Date no se comprueban, ya que las filas son entrada sin procesar.
Para listas que mezclan tipos de Entity, usa una Union; cada fila se almacena según su 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 eliminar muchas entities a la vez, usa Invalidate; las filas solo necesitan sus campos pk:
ctrl.set([new schema.Invalidate(Todo)], [{ id: '5' }, { id: '6' }]);
Para eliminar una sola, pasa el schema Invalidate y su fila:
ctrl.set(new schema.Invalidate(Todo), { id: '5' });
Los schemas Values, en cambio, toman un objeto de filas:
ctrl.set(new schema.Values(Todo), {
'5': { id: '5', completed: true },
'6': { id: '6', completed: false },
});
Los schemas Array, Values e Invalidate no toman args (por lo que Entity.pk() y Entity.process()
reciben []) ni función de actualización. Las filas que comparten una pk se combinan en el orden de la lista, sin
Entity.shouldReorder(). Usa esto en lugar de llamar a set() una vez por fila, por ejemplo al
agrupar en lotes actualizaciones de streams de alta frecuencia.
setResponse(endpoint, ...args, response)
Almacena response en la caché para el Endpoint y los args dados.
Cualquier componente que esté en suspense para el Endpoint y los args dados se resolverá.
Si ya existen datos para el Endpoint y los args dados, se actualizarán.
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());
Esto muestra una prueba de concepto en Vue; sin embargo, una implementación de websockets con un Manager sería mucho más robusta.
setError(endpoint, ...args, error)
Almacena el resultado de Endpoint y args como el error proporcionado.
resolve(endpoint, { args, response, fetchedAt, error })
Resuelve un fetch específico y almacena response en la caché.
Es similar a setResponse, salvo que activa la resolución de un fetch en curso. Esto significa que la actualización optimista correspondiente dejará de aplicarse.
Se usa en NetworkManager y debe usarse al procesar solicitudes de fetch.
subscribe(endpoint, ...args)
Marca una nueva suscripción a un Endpoint determinado. Esto debe incrementar la suscripción.
useSubscription y useLive lo llaman al montarse.
Puede ser útil para composables personalizados que se suscriban o cancelen la suscripción según otros factores.
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 la finalización de la suscripción a un Endpoint determinado. Esto debe decrementar la suscripción y, si el contador llega a 0, ya no se recibirán más actualizaciones automáticamente.
useSubscription y useLive lo llaman al desmontarse.
Acceso a datos
get(schema, ...args, state)
Busca cualquier Schema Queryable en state.
Ejemplo
Se usa en useQuery y puede usarse en los Managers para acceder al store de forma segura.
En los componentes, useQuery() mantiene el resultado reactivo. En los manejadores de eventos, pasa getState() para leer el store más reciente:
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;
}
Obtiene la respuesta (globalmente estable a nivel referencial) para un par endpoint/args dado a partir del state proporcionado.
data
Los datos de la respuesta desnormalizados. Garantiza estabilidad referencial global para todos los miembros.
expiryStatus
export enum ExpiryStatus {
Invalid = 1,
InvalidIfStale,
Valid,
}
Valid
- Nunca entrará en suspense.
- Podría hacer fetch si los datos están obsoletos
InvalidIfStale
- Entrará en suspense si los datos están obsoletos.
- Podría hacer fetch si los datos están obsoletos
Invalid
- Siempre entrará en suspense
- Siempre hará fetch
expiresAt
Un número que representa el momento en que caduca. Compáralo con Date.now().
Ejemplo
Se usa en useCache y useSuspense, y puede usarse en los Managers para buscar una respuesta con el state proporcionado.
En los manejadores de eventos, pasa getState() para leer el store más reciente, como en el ejemplo 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)
Obtiene el error, si lo hay, de un endpoint determinado. Devuelve undefined si no hay errores.
snapshot(state, fetchedAt)
Returns a Snapshot.
getState()
Obtiene el estado interno de Reactive Data Client que ya se ha confirmado (commit).
Esto solo debe usarse en manejadores de eventos o en Managers.
Usar getState() en un computed() o en una plantilla no se actualizará cuando cambie el store. Usa
en su lugar useQuery() o useCache().
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 });
};
Las mutaciones se resuelven antes de que se actualice el store, así que lee su resultado a partir
del valor con el que se resuelve fetch() en lugar de getState().