Client Operations
packages/client-operations/README.md
@repo/client-operations
Utilidades opt-in para estandarizar queries, mutations y errores de operaciones iniciadas desde una app cliente. Reexporta AppError desde @repo/app-errors, pero no implementa servicios ni presenta UI.
Responsabilidades
AppErrorreexportado de@repo/app-errorsofrece un contrato runtime neutral para fallos de validación, negocio, permisos, autenticación, red y resultados no encontrados.toAppErrornormaliza errores desconocidos y objetos con forma Firebase sin depender del SDK.unwrapOperationResultconvierte respuestas callable discriminadas en éxito oAppError.useAppQueryyuseAppMutationconectan definiciones de operación con TanStack Query y normalizan excepciones.ClientOperationsProviderycreateQueryClientproporcionan configuración opcional de queries y mutations.createQueryKeyFactorymantiene keys con namespace y soporte de invalidación por raíz.
Este paquete no importa @repo/ui, Firebase, Next.js ni código de apps/. Los servicios viven en @repo/app-services; los textos visibles, toasts, errores de formulario, navegación y efectos posteriores al éxito pertenecen al consumidor.
Instalación por app
El package declara React y TanStack Query como peers. Una app que lo adopta debe instalarlos y montar el provider deliberadamente. apps/system ya lo monta desde app/root-layout-client.tsx.
'use client';
import { ClientOperationsProvider } from '@repo/client-operations';
export function Providers({ children }: { children: React.ReactNode }) {
return <ClientOperationsProvider>{children}</ClientOperationsProvider>;
}
Errores de servicio
Los servicios importan AppError desde @repo/app-errors, usan códigos estables y preservan el error original en cause. message es un mensaje técnico seguro, no copy de producto. La UI traduce code/kind a copy local; nunca presenta directamente message ni texto crudo del SDK.
import { AppError } from '@repo/app-errors';
const adapterError = new Error('adapter failure');
throw new AppError({
code: 'ESTATE_PERMISSION_DENIED',
kind: 'permission',
message: 'Estate update rejected',
cause: adapterError,
});
En límites que aceptan errores desconocidos, toAppError(error, { mapError }) admite un mapper del dominio. Úsalo como normalización de borde, no para inferir por texto errores de dominio que el servicio puede tipar directamente. La normalización estructural reconoce códigos comunes como permission-denied, unauthenticated, not-found, invalid-argument y unavailable.
Composición usada por Information
apps/system/modules/information/hooks/use-information-page-data.ts combina las primitivas del
package con una suscripción de larga duración:
useAppQuerycarga la lectura inicial con una key namespaced por comunidad y rol.subscribeToInfoChangesmantiene la caché sincronizada con Firestore mediantequeryClient.setQueryData; el listener no se modela como una query de una sola ejecución.- Las escrituras usan
useAppMutation. Al asentarse, invalidan la query para reconciliar la caché, incluso cuando la mutation falla. - Si el listener termina con error, el hook conserva el
AppError;retryrefetchea y crea de nuevo la suscripción. - La UI traduce
code/kinda copy local y mantiene efectos de formulario/toast fuera del package.
El provider se monta una vez en el layout de la app. Comunidad, rol y uid son contexto del
consumidor, no responsabilidades de @repo/client-operations.
Callable responses
Un servicio puede adoptar un resultado discriminado para no devolver mensajes de error como strings:
import { unwrapOperationResult } from '@repo/client-operations';
const payload = unwrapOperationResult(await callableService(input));
El resultado debe tener forma { success: true, data } | { success: false, code?, message?, fieldErrors? }. Un fallo se convierte en AppError y sigue el mismo camino que una excepción.
Definir y ejecutar operaciones
import { createQueryKeyFactory, useAppMutation, useAppQuery } from '@repo/client-operations';
import { useQueryClient } from '@tanstack/react-query';
const estateKeys = createQueryKeyFactory<[string]>('estates');
export function useEstate(estateId: string) {
return useAppQuery({
queryKey: estateKeys(estateId),
queryFn: () => estateService.getById(estateId),
});
}
export function useUpdateEstate() {
const queryClient = useQueryClient();
return useAppMutation(
{
mutationKey: ['estates', 'update'],
mutationFn: estateService.update,
},
{
onSuccess: (_estate, variables) =>
queryClient.invalidateQueries({ queryKey: estateKeys(variables.id) }),
},
);
}
El hook de módulo/app llama mutateAsync, presenta un único toast según resultado y conserva los efectos locales, como mapear fieldErrors al formulario o cerrar un diálogo. La definición de la operación mantiene cerca del dominio sus keys, servicio, mapper de copy e invalidaciones.
Defaults de caché y reintentos
- Queries:
staleTimede 30 segundos ygcTimede 5 minutos; ambos configurables al crear el cliente. - Queries: hasta dos reintentos únicamente para errores normalizados como
network. - Mutations: sin reintentos automáticos, para evitar repetir escrituras no idempotentes.
CreateQueryClientOptions.retrypermite ajustar el criterio al producto.
Migración futura
- Instalar el package y sus peers en una app y montar
ClientOperationsProvideruna sola vez. - Añadir en esa app un adaptador que conecte
AppErrorcon@repo/uiy los mensajes del producto. - Migrar una mutation de estates y verificar invalidación, estados y feedback.
- Migrar auth conservando errores de campo, navegación y flujos especiales.
- Continuar módulo por módulo, sin convertir las suscripciones Firestore en queries de una sola ejecución.
Validación
pnpm --filter @repo/client-operations test:run
pnpm --filter @repo/client-operations check-types
pnpm --filter @repo/client-operations lint
pnpm --filter @repo/client-operations build