Saltar al contenido principal

v0.19: Controller.set() por lotes, TypeScript más rápido

· 29 min de lectura
Nathaniel Tucker
Creator of Reactive Data Client
nota

El original en inglés cambió después de esta traducción, así que puede estar desactualizada.

v0.19 permite que un Manager escriba un lote completo de filas recibidas por streaming en una sola actualización del store, verifica los tipos del código de endpoints unas 2 veces más rápido y corrige una ronda de problemas de TypeScript y Vue. Los agentes de programación también reciben un plugin de Claude Code, skills de Vue y llms.txt; consulta Data Client para agentes de programación.

Nuevas APIs:

Rendimiento:

Otras mejoras:

  • Se corrige la resolución de módulos de TypeScript 7 para los exports del paquete; los imports ahora resuelven a los archivos de declaración (#4019)
  • renderDataHook() ejecuta los efectos de montaje del provider cuando el primer renderizado suspende (#4099)
  • Los valores de Controller.set() se tipan según el schema, así que ctrl.set(new schema.All(Todo), 42) es un error de TypeScript (#4133)
  • Controller.set() elimina una sola entidad con un schema Invalidate sin la solución alternativa del lote de una fila (#4230)
  • process() recibe params tipados en new RestEndpoint(), .extend() y resource().extend(), así que un parámetro de ruta incorrecto es un error de TypeScript en lugar de undefined en tiempo de ejecución (#4183, #4184)
  • Los valores de retorno de getOptimisticResponse() se tipan según el schema, así que un tipo de campo incorrecto es un error de TypeScript en lugar de un destello de datos erróneos (#4198)
  • useSuspense() y useLive() de Vue conservan los datos anteriores mientras cargan los nuevos argumentos, en lugar de devolver undefined (#4131)
  • useSuspense() y useLive() de Vue envían los errores de obtención posteriores a un cambio de argumentos a onErrorCaptured() en lugar de producir un rechazo de promesa no manejado (#4135)
  • useSuspense(), useDLE() y useFetch() de Vue ya no vuelven a obtener los datos obsoletos en cada actualización del store, así que un controller.set() no se sobrescribe (#4134)
  • useSuspense() de Vue muestra los datos obsoletos al montarse y vuelve a obtenerlos en segundo plano, como en React, en lugar de mostrar el fallback de <Suspense> hasta que termina la nueva obtención (#4169)
  • useDLE() y useCache() de Vue conservan los datos invalidIfStale caducados a través de actualizaciones del store no relacionadas, en lugar de quedarse atascados cargando (#4142)
  • DataClientPlugin de Vue se instala en versiones de Vue anteriores a 3.5 en lugar de lanzar app.onUnmount is not a function (#4146)
  • DataClientPlugin de Vue recolecta basura de forma predeterminada, igual que DataProvider de React, así que ya no necesitas pasar gcPolicy; cómo decide qué eliminar (#4148)
  • Los composables de Vue aceptan argumentos getter como () => ({ id: props.id }); estaban tipados para permitirlos pero pasaban la propia función al endpoint (#4115)
  • Se corrigen los errores Cannot find name 'NoInfer' y export type en TypeScript 4.x con skipLibCheck desactivado (#4138)
  • Se corrigen los errores de tipo de Entity, Endpoint, Union y RestEndpoint en TypeScript 4.0–4.5 con skipLibCheck desactivado (#4140)
  • Se corrigen los errores ',' expected al importar @data-client/react/redux, @data-client/react/nextjs o @data-client/test en TypeScript 4.0–4.4 (#4232)
  • Se corrige que act y renderHook de @data-client/test se tiparan como any en TypeScript 4.0–4.4 (#4251)
  • Las clases Entity pueden usarse donde se espera un EntityInterface (#4149); prepara las sobrescrituras de pk() para una versión futura
  • useCache() y useDLE() devuelven undefined para una entidad eliminada cuya nueva obtención falló, en lugar de un Symbol verdadero que se colaba por las comprobaciones if (!data); elimina cualquier solución alternativa (#4150)
  • useFetch() de Vue evita que sus datos sean recolectados como basura mientras está montado, como useSuspense(), así que un gcPolicy configurado ya no expulsa los datos precargados que los componentes leen después (#4152)
  • Las aplicaciones con Redux DevTools abierto ya no se entrecortan en desarrollo con stores grandes o actualizaciones frecuentes; cada actualización se serializa entre 40 y 60 veces más rápido (#4163)
  • resource().extend() con un objeto de sobrescrituras de endpoints conserva en su tipo los endpoints añadidos antes con .extend('name', options), así que usarlos ya no es un error de TypeScript (#4184)
  • RestEndpoint.push, unshift y remove aceptan los campos de la Entity como cuerpo cuando un Query o Lazy envuelve la Collection, en lugar de solo FormData (#4226)
  • useFetch() de Vue se tipa como el Ref de solo lectura que devuelve, así que promise.resolved (siempre undefined) es ahora un error de TypeScript; lee promise.value.resolved en su lugar (#4114)
  • Una subclase de Controller pasada a DataProvider o DataClientPlugin ya no necesita una conversión de tipo para verificar los tipos (#4228)
  • El store de prepareStore() puede pasarse al <Provider> de react-redux sin una conversión de tipo, y tiene dispatch() en su tipo (#4238)
  • resource().create acepta FormData sin una conversión de tipo, como update, partialUpdate y getList.push (#4235)
  • RestEndpoint.push, unshift y remove envían un cuerpo de tipo array como JSON, así que crear o eliminar varios elementos a la vez ya no envía "[object Object]" a tu servidor; resource().create también acepta un array (#4262)
  • createDataClient() de Vue queda obsoleto, ya que es interno de DataClientPlugin; instala el plugin con app.use(DataClientPlugin, options) en lugar de llamarlo directamente (#4193)
  • waitForNextUpdate() de renderDataCompose() de Vue queda obsoleto, ya que se rinde en silencio después de 1 segundo y deja pasar una prueba mientras sigue suspendida; usa await result en su lugar, o await allSettled() después de un cambio, que ahora también espera las obtenciones que inicia un cambio de prop (pruebas unitarias de composables, #4221)

Cambios incompatibles:

Prepárate para versiones futuras:

Controller.set() por lotes​

Un Manager que recibe un flujo de filas (como cotizaciones de precios por un websocket) ahora puede escribirlas todas con un solo Controller.set() pasando un schema Array. Antes de v0.19, [Ticker] era un error de TypeScript, así que la solución alternativa habitual era un set() por fila:

Before
ws.onmessage = event => {
const rows = JSON.parse(event.data);
for (const row of rows) {
ctrl.set(Ticker, { product_id: row.product_id }, row);
}
};
After
ws.onmessage = event => {
const rows = JSON.parse(event.data);
ctrl.set([Ticker], rows);
};

Cada fila se fusiona con su entidad almacenada, y las entidades que no están en la lista quedan intactas. Los schemas Array no reciben args ni función actualizadora. Para agrupar en un lote tipos de Entity mixtos, eliminaciones o filas identificadas por id, pasa un schema Union, Invalidate o Values; consulta Controller.set() para ver ejemplos. #4103

Prueba ambos botones a continuación. Esta comprobación en el navegador parte de un store vacío y mide el tiempo de un Promise.all de 500 llamadas a set() frente a un único set() por lotes. Ambos caminos son un solo commit de React, y cada uno escribe 500 precios nuevos.

import React from 'react';
import { useController, useQuery } from '@data-client/react';
import { Ticker, newPrices } from './Ticker';

function PriceStream() {
  const ctrl = useController();
  const [timing, setTiming] = React.useState('');
  const first = useQuery(Ticker, { product_id: 'COIN-0' });

  const time = async (
    label: string,
    write: (rows: ReturnType<typeof newPrices>) => Promise<unknown>,
  ) => {
    const rows = newPrices();
    const start = performance.now();
    await write(rows);
    setTiming(`${label}: ${(performance.now() - start).toFixed(1)} ms`);
  };
  const perRow = () =>
    time('500 set() calls', rows =>
      Promise.all(
        rows.map(row => ctrl.set(Ticker, { product_id: row.product_id }, row)),
      ),
    );
  // highlight-next-line
  const batch = () => time('1 batch set()', rows => ctrl.set([Ticker], rows));

  return (
    <div>
      <button onClick={perRow}>set() per row</button>{' '}
      <button onClick={batch}>batch set()</button>
      <p>COIN-0: {first ? `$${first.price}` : 'no data yet'}</p>
      <p>{timing}</p>
    </div>
  );
}
render(<PriceStream />);
Resultado

Rendimiento​

Cada set() es una actualización del store independiente, y cada actualización del store copia la tabla de ese tipo de entidad. Escribir las filas una por una repite esa copia por cada fila, así que el costo crece tanto con el tamaño del lote como con el tamaño del store. Un lote paga esa copia una sola vez.

El gráfico es el benchmark setMany de node en examples/benchmark/core.js: un store que ya contiene 500 entidades, y luego un set() sincrónico por fila frente a un solo set([Ticker], rows). Mide la actualización del store: 20 veces más rápido para 50 filas y 95 veces más rápido para 500 filas. #4103

setMany de Node en un store de 500 entidades
50 rows20xOne set() per row 10.8 ms → set([Ticker], rows) 0.54 ms
500 rows95xOne set() per row 103 ms → set([Ticker], rows) 1.08 ms

Benchmarks a lo largo del tiempo | Ver el benchmark setMany

En un Manager, almacena en un búfer los mensajes entrantes y vacía cada lote con un solo set(), como se describe en Agrupar actualizaciones de alta frecuencia. El StreamManager de la aplicación de criptomonedas ahora vacía los mensajes de cotización de Coinbase de esta manera.

StreamManager.ts
handleMessage(msg: any) {
if (msg.type in this.entities) {
(this.buffer[msg.type] ??= {})[msg.product_id] = msg;
this.flushTimeout ??= setTimeout(this.flush, 50);
}
}

flush = () => {
const buffer = this.buffer;
this.buffer = {};
this.flushTimeout = undefined;
for (const type in buffer) {
this.controller.set([this.entities[type]], Object.values(buffer[type]));
}
};

Explora el ejemplo coin-app

Más demos

consejo

Si filtras las acciones de DevToolsManager por schema, el action.schema de una escritura por lotes es el schema Array, así que compara con action.schema[0] para [Ticker] en lugar de con la propia Entity.

TypeScript más rápido​

TypeScript vuelve a verificar tu código con cada pulsación de tecla en el editor y en cada build de CI. En aplicaciones con muchos endpoints, los tipos pesados de las bibliotecas se manifiestan como un autocompletado lento, subrayados rojos que tardan segundos en aparecer y builds más lentos. v0.19 hace que RestEndpoint, resource() y .extend() sean mucho más baratos de verificar, sin cambios de código de tu parte. TypeScript sigue reportando todos los errores que reportaba antes (#4173).

Resultados​

Nuestra prueba de estrés más pesada, un archivo de 150 RestEndpoints con rutas largas, .extend() y .paginated(), ahora se verifica 2 veces más rápido en TypeScript 6 y 2.2 veces más rápido en TypeScript 7, usando alrededor de 40% menos memoria. Todas las cifras comparan los paquetes publicados de v0.18.1 con v0.19; los tiempos de verificación son el mejor de 10 ejecuciones.

Verificación de 150 RestEndpoints con rutas largas
TypeScript 61.9xv0.18 3.83 s → v0.19 1.98 s
TypeScript 72.2xv0.18 1.5 s → v0.19 0.69 s
Memoria para el mismo archivo
TypeScript 61.7xv0.18 365 MB → v0.19 211 MB
TypeScript 71.7xv0.18 220 MB → v0.19 128 MB

El código con muchos endpoints hace mucho menos trabajo de tipos. Las instanciaciones de tipos son la unidad de trabajo de TypeScript: son deterministas, así que se comparan limpiamente entre máquinas. Union hace más trabajo que en v0.18, porque v0.19 ahora verifica los tipos de los valores de set() (ver más abajo).

Instanciaciones de tipos por prueba de estrés
Long paths2.4xv0.18 822 K → v0.19 341 K
Typical app1.5xv0.18 20.2 K → v0.19 13.4 K
React hooks1.2xv0.18 145 K → v0.19 122 K
Vue1.3xv0.18 62.6 K → v0.19 47.3 K
300 fields1.3xv0.18 17 K → v0.19 12.8 K
Schemas1.2xv0.18 118 K → v0.19 99.3 K
Union0.6xv0.18 9 K → v0.19 16.1 K

Pasa el cursor o toca una celda para ver las cifras exactas.

TS 6 check timeTS 7 check timeTS 6 memory
Long paths
150 RestEndpoints with 6-param paths, .extend() and .paginated()
-48%
3.83s → 1.98s
-54%
1.5s → 0.69s
-42%
365MB → 211MB
Typical app
A few resources with .extend(), .paginated() and hooks
same
0.42s → 0.42s
-15%
0.062s → 0.053s
+3%
106MB → 109MB
React hooks
40 resources through every hook, plus ctrl.fetch() and ctrl.set()
-10%
1.24s → 1.12s
-7%
0.41s → 0.38s
-6%
167MB → 157MB
Vue
The same 40 resources through every composable
same
0.68s → 0.68s
-12%
0.17s → 0.15s
-4%
145MB → 139MB
300 fields
One Entity with 300 fields, read and updated 100 times
-14%
0.44s → 0.38s
-40%
0.086s → 0.052s
-9%
111MB → 101MB
Schemas
All, Query, Invalidate, Array, Object and Collection
-3%
1.1s → 1.07s
-14%
0.36s → 0.31s
-4%
156MB → 150MB
Union
A 30-member Union in a Collection and Values
-2%
0.42s → 0.41s
-4%
0.071s → 0.068s
-6%
107MB → 101MB
set() values
1000 ctrl.set() calls on a 30-member Union, a Collection of it and a 300-field Entity
-28%
1.49s → 1.07s
-54%
0.54s → 0.25s
-26%
176MB → 131MB
set() updaters
1000 ctrl.set(Union, args, prev => ...) updaters on a 30-member Union
-4%
3.58s → 3.43s
+11%
1.67s → 1.85s
+1%
381MB → 384MB

Las filas de set() miden los valores tipados de set(), que v0.18 no verificaba en absoluto. Los valores simples siguen verificándose más rápido que antes, y las funciones actualizadoras sobre Unions grandes se verifican aproximadamente en el mismo tiempo aunque TypeScript ahora verifica el valor de retorno de cada actualizadora (#4179).

Los archivos pequeños están dominados por el costo fijo de arranque de TypeScript (cargar lib.dom.d.ts por sí solo ocupa unos 100MB), así que su tiempo y memoria apenas cambian. El ahorro se acumula a medida que crece una base de código.

Qué cambió​

  • .extend() y .paginated() se comparten entre todos los endpoints en lugar de recrearse para cada tipo de endpoint.
  • Las opciones de los endpoints se infieren como tipos de objeto simples, así que TypeScript deja de reconstruirlas en cada uso.
  • Los parámetros de ruta como /users/:id se leen en una sola pasada.

Antes de publicar, desactivamos todos los @ts-expect-error del conjunto de pruebas en TypeScript 4.0 a 7 y confirmamos que cada error sigue apareciendo en el mismo lugar.

En TypeScript 5.x y anteriores, un método process(value, params) pasado a .extend() tampoco falla ya con "implicitly has an 'any' type" bajo strict.

Otras mejoras​

Valores tipados de set()​

Controller.set() antes aceptaba cualquier valor para un schema, así que un error tipográfico o un tipo de campo incorrecto solo se manifestaba como datos erróneos en tiempo de ejecución. Los valores ahora se tipan según el schema: una Entity recibe sus campos, mientras que una Collection, All o Array recibe una lista de filas. Todos los campos son opcionales, ya que set() se fusiona con lo que ya está almacenado, y los números y las cadenas son intercambiables, igual que en las respuestas de la API. #4133

Pasa el cursor sobre los subrayados rojos para ver cada error.

import type { Controller } from '@data-client/react';
import { schema } from '@data-client/rest';
import { Todo, TodoResource } from './Todo';

export function updateTodos(ctrl: Controller) {
  // ✅ rows are partial Todos; ids may be strings or numbers
  ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]);
  ctrl.set(Todo, { id: 5 }, todo => ({ completed: !todo.completed }));
  ctrl.set([Todo], [{ id: 1, title: 'first' }, { id: 2 }]);

  // ❌ All takes a list of rows
  ctrl.set(new schema.All(Todo), 42);
  // ❌ completed is a boolean
  ctrl.set(Todo, { id: 5 }, { id: 5, completed: 'yes' });
  // ❌ Todo has no done field
  ctrl.set(TodoResource.getList.schema, [{ id: 5, done: true }]);
  // ❌ updaters must return Todo fields
  ctrl.set(Todo, { id: 5 }, todo => ({ title: todo.completed }));
}

Un Query recibe la entrada del schema que envuelve, ya que set() normaliza ese schema en lugar de revertir process(). Cuando cada miembro declara su discriminador como un literal (como readonly type = 'post'), una fila de Union se verifica contra el miembro que selecciona, así que no puede mezclar campos de miembros distintos; consulta Unions.

Si código que antes compilaba ahora falla aquí, estaba escribiendo datos que su schema no describe. Corrige el valor, o amplía los tipos de los campos de la Entity si los datos realmente pueden tener esa forma.

Eliminar una sola entidad con set()​

Pasar un schema Invalidate a Controller.set() elimina la entidad que identifica su fila, pero la forma individual era un error de TypeScript, así que el código la envolvía en un lote de una fila. Ahora verifica los tipos correctamente, con la fila tipada según los campos de la Entity (#4230):

Before
ctrl.set([new Invalidate(Todo)], [{ id: '5' }]);
After
ctrl.set(new Invalidate(Todo), { id: '5' });

Como el set() por lotes, no recibe args ni función actualizadora.

Params tipados de process()​

Un método process() pasado a new RestEndpoint(), .extend() o resource().extend() recibía params tipado como any, así que leer un parámetro que el endpoint no tiene compilaba y devolvía undefined en tiempo de ejecución. params y body ahora se tipan a partir del endpoint, incluido un path definido en la misma llamada a .extend() (#4183, #4184):

import { RestEndpoint } from '@data-client/rest';

const getUser = new RestEndpoint({ path: '/users/:id' });

export const getUserById = getUser.extend({
  path: '/users/by-id/:userId',
  process(value, params) {
    const userId: string | number = params.userId;
    // @ts-expect-error 'id' is not a param of '/users/by-id/:userId'
    params.id;
    return { ...value, userId };
  },
});

Cuando todos los params son opcionales, params puede ser undefined, así que léelo con params?.page.

Esto puede hacer aparecer nuevos errores de TypeScript en métodos process() existentes. Cada uno marca una lectura que podría ser undefined o lanzar una excepción en tiempo de ejecución, así que corrige el nombre del parámetro o añade la comprobación que falta.

Respuestas optimistas tipadas​

getOptimisticResponse() podía devolver cualquier cosa a menos que el endpoint tuviera un process() tipado, así que un tipo de campo incorrecto solo se manifestaba como datos erróneos en la interfaz hasta que el servidor respondía. El valor de retorno ahora se verifica contra el schema del endpoint, como un valor tipado de set(): una Entity recibe cualquiera de sus campos, y una Collection recibe una lista de ellos (#4198):

import { Entity, RestEndpoint } from '@data-client/rest';

class Todo extends Entity {
  id = 0;
  title = '';
  completed = false;
}

export const toggleTodo = new RestEndpoint({
  path: '/todos/:id',
  method: 'PATCH',
  schema: Todo,
  // @ts-expect-error completed is a boolean; 'false' is truthy, so the checkbox would stay checked
  getOptimisticResponse(snap, { id }) {
    const todo = snap.get(Todo, { id });
    if (!todo) throw snap.abort;
    return { id, completed: todo.completed ? 'false' : 'true' };
  },
});

Esto puede hacer aparecer nuevos errores de TypeScript en métodos getOptimisticResponse() existentes. Cada uno marca un valor que se almacenaría con el tipo incorrecto hasta que el servidor responda, así que devuelve el tipo real del campo (aquí !todo.completed).

push() tipado en Collections ordenadas​

Envolver una Collection en un Query para ordenarla o filtrarla hacía que push, unshift y remove rechazaran cualquier cuerpo excepto FormData, aunque sí agregan a la lista en tiempo de ejecución. El cuerpo ahora se tipa a partir de la Entity de la Collection, así que la conversión de tipo puede eliminarse y un campo incorrecto es un error de TypeScript. Lo mismo aplica a una Collection envuelta en Lazy (#4226).

Before
import { useController } from '@data-client/react';
import { getPosts } from './getPosts';

export function useAddPost() {
const ctrl = useController();
return (title: string, author: string) =>
ctrl.fetch(getPosts.push, { group: 'react' }, { title, author } as any);
}
After
import { useController } from '@data-client/react';
import { getPosts } from './getPosts';

export function useAddPost() {
const ctrl = useController();
return (title: string, author: string) =>
ctrl.fetch(getPosts.push, { group: 'react' }, { title, author });
}

donde getPosts ordena su Collection con un Query:

getPosts.ts
import { Collection, Entity, Query, RestEndpoint } from '@data-client/rest';

export class Post extends Entity {
id = '';
title = '';
group = '';
author = '';
}

export const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: 'title' | 'author' },
schema: new Query(
new Collection([Post], { nonFilterArgumentKeys: /orderBy/ }),
(posts, { orderBy }: { orderBy?: 'title' | 'author' } = {}) =>
orderBy ?
[...posts].sort((a, b) => a[orderBy].localeCompare(b[orderBy]))
: posts,
),
});

FormData en resource().create​

El create de resource() envía un cuerpo FormData tal cual, como update, partialUpdate y getList.push, pero su tipo solo permitía un objeto, así que enviar un formulario necesitaba una conversión de tipo (#4235). Elimina la conversión:

Before
<form
onSubmit={e => {
e.preventDefault();
ctrl.fetch(PostResource.create, new FormData(e.currentTarget) as any);
}}
>
After
<form
onSubmit={e => {
e.preventDefault();
ctrl.fetch(PostResource.create, new FormData(e.currentTarget));
}}
>

create está obsoleto; getList.push es su reemplazo y también acepta FormData.

Argumentos getter en Vue​

Los composables de Vue como useSuspense() y useLive() estaban tipados para aceptar una función getter como argumento, pero pasaban la propia función al endpoint en lugar de llamarla. Los getters ahora funcionan igual que los refs y computed, y el composable vuelve a obtener los datos cuando cambia cualquier cosa que el getter lee (#4115). Elimina el envoltorio computed() si solo lo usabas para hacer reactivos los argumentos:

Before
import { computed } from 'vue';

const props = defineProps<{ id: number }>();
const article = await useSuspense(
ArticleResource.get,
computed(() => ({ id: props.id })),
);
After
const props = defineProps<{ id: number }>();
const article = await useSuspense(ArticleResource.get, () => ({
id: props.id,
}));

Esto aplica a todos los composables que reciben argumentos de endpoint: useSuspense(), useLive(), useCache(), useDLE(), useFetch(), useQuery() y useSubscription().

Vue recolecta basura de forma predeterminada​

El DataClientPlugin de Vue solo eliminaba los datos sin uso si pasabas un gcPolicy tú mismo. Ahora usa new GCPolicy() de forma predeterminada, igual que el DataProvider de React, así que puedes eliminar la opción a menos que la personalices (#4148).

Before
import { DataClientPlugin, GCPolicy } from '@data-client/vue';

app.use(DataClientPlugin, { gcPolicy: new GCPolicy() });
After
import { DataClientPlugin } from '@data-client/vue';

app.use(DataClientPlugin);

Los datos se eliminan solo cuando ningún componente montado los lee y ha pasado el doble de su tiempo de frescura desde que se obtuvieron (al menos dos minutos). Un barrido comprueba cada cinco minutos. Todo lo que un componente vuelva a leer antes de eso vuelve a estar en uso.

Un componente que se monta después de la eliminación trata los datos como si nunca se hubieran obtenido: useSuspense() los obtiene de nuevo, mientras que useCache() devuelve undefined. Para conservar los datos sin uso más tiempo o barrer con menos frecuencia, pasa tu propio gcPolicy.

Stale-while-revalidate en Vue​

Cuando un componente se monta con datos que están obsoletos pero siguen siendo válidos, el useSuspense() de Vue ahora los renderiza de inmediato y vuelve a obtenerlos en segundo plano, como en React. Antes, mostraba el fallback de <Suspense> hasta que terminaba la nueva obtención (#4169).

Las clases Entity verifican tipos como EntityInterface​

Las funciones auxiliares tipadas para aceptar cualquier Entity con EntityInterface rechazaban las clases Entity, porque Entity.pk() tipaba sus args como un array mutable. Ahora es readonly any[], así que esto verifica los tipos correctamente (#4149):

import { Entity } from '@data-client/rest';
import type { EntityInterface } from '@data-client/react';

class User extends Entity {
  id = '';
  name = '';
}

function entityName(schema: EntityInterface) {
  return schema.key;
}

entityName(User);

Si sobrescribes static pk() y tipas args como un array mutable, todavía compila, pero una futura versión con cambios incompatibles exigirá readonly; actualízalo ahora.

Las entidades eliminadas se leen como undefined​

Cuando una entidad se eliminaba y su nueva obtención fallaba, useCache() y useDLE() (React y Vue) devolvían un Symbol interno como datos. Un Symbol es verdadero, así que la comprobación habitual de "todavía no cargado" lo dejaba pasar y el componente se renderizaba como si tuviera una entidad (#4150):

function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
// a deleted todo used to get past this guard, so todo.title.trim() threw
if (!todo) return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}

Ahora data es undefined, en consonancia con su tipo y con Controller.get(). Lo mismo aplica a Controller.getResponse() y Controller.fetchIfStale(), por ejemplo en Managers personalizados.

Si tenías una solución alternativa para esto, puedes eliminarla. Una simple comprobación de veracidad es suficiente:

Before
function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
if (!todo || typeof todo === 'symbol') return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}
After
function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
if (!todo) return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}

Para mostrar algo específico cuando la nueva obtención falló (por ejemplo, un 404 tras la eliminación), lee error de useDLE() en lugar de inspeccionar data.

Redux DevTools más rápido​

Con la extensión Redux DevTools abierta, cada actualización del store en desarrollo serializa todo el store para la extensión, y cada marca de tiempo en él se formateaba de la manera lenta. Los stores grandes o las actualizaciones frecuentes, como el sondeo (polling), los datos en vivo o muchas llamadas a controller.set(), hacían que la página se entrecortara. Cada actualización ahora se serializa entre 40 y 60 veces más rápido, y las marcas de tiempo siguen leyéndose como 10:42:07.123 AM (#4163). Los builds de producción no incluyen DevTools, así que no se ven afectados.

Serialización de DevTools por actualización del store
50 entities60xv0.18 18.6 ms → v0.19 0.31 ms
500 entities40xv0.18 201 ms → v0.19 5 ms

Guía de migración​

Esta actualización requiere actualizar todas las versiones de los paquetes al mismo tiempo.

npm install --save @data-client/react@^0.19.0 @data-client/rest@^0.19.0 @data-client/endpoint@^0.19.0 @data-client/core@^0.19.0 @data-client/vue@^0.19.0 @data-client/test@^0.19.0 @data-client/img@^0.19.0

TypeScript 4.0 o posterior​

Omite esta sección si ya usas TypeScript 4.0 o posterior.

Se eliminan las declaraciones para TypeScript 3.x, ya que dejaron de verificar tipos correctamente en todas las versiones de TypeScript 3.x. Sube typescript a ^4.0.0 o posterior en tus devDependencies. #4151

Prepárate para versiones futuras​

Nada de esto es necesario para actualizar a 0.19; tu código sigue funcionando tal cual. Cada elemento se romperá en una versión futura, así que actualizarlo ahora convierte esa actualización en algo trivial.

Los args de Entity.pk() son readonly​

Omite esto si no sobrescribes static pk() con un parámetro args tipado.

Entity.pk() ahora tipa args como readonly any[]. Las sobrescrituras que lo anotan como un array mutable todavía compilan por ahora, pero una versión futura exigirá readonly.

Before
class User extends Entity {
static pk(value: any, parent?: any, key?: string, args?: any[]) {
return `${value.id}-${args?.[0]?.org}`;
}
}
After
class User extends Entity {
static pk(value: any, parent?: any, key?: string, args?: readonly any[]) {
return `${value.id}-${args?.[0]?.org}`;
}
}

createDataClient() de Vue​

Omite esto si instalas el store con app.use(DataClientPlugin).

createDataClient() y ProvidedDataClient son internos de DataClientPlugin y dejarán de exportarse. Instala el plugin en su lugar, y lee el controller con useController(). #4193

Before
import { createDataClient } from '@data-client/vue';

const { controller } = createDataClient({ managers, initialState });
After
import { DataClientPlugin, useController } from '@data-client/vue';

app.use(DataClientPlugin, { managers, initialState });
// in a component's setup()
const controller = useController();

waitForNextUpdate() de renderDataCompose() en Vue​

Omite esto si tus pruebas de Vue no llaman a waitForNextUpdate().

waitForNextUpdate() de renderDataCompose() se rinde en silencio después de 1 segundo, así que una prueba puede pasar mientras el composable sigue suspendido. Se eliminará. Usa await sobre el resultado en su lugar, y llama a allSettled() después de cambiar props o llamar al controller; ahora también espera la obtención que inicia un cambio de prop. #4221

Before
const { result, waitForNextUpdate } = await renderDataCompose(
(props: { id: number }) =>
useSuspense(ArticleResource.get, () => ({ id: props.id })),
{ props, resolverFixtures },
);
await waitForNextUpdate();
const article = await result;

props.id = 6;
await waitForNextUpdate();
After
const { result, allSettled } = await renderDataCompose(
(props: { id: number }) =>
useSuspense(ArticleResource.get, () => ({ id: props.id })),
{ props, resolverFixtures },
);
const article = await result;

props.id = 6;
await allSettled();

Soporte para la actualización​

Como siempre, si tienes algún problema o pregunta, no dudes en unirte a nuestro Chat o reportar un error