Actualizar a 0.10.0
Un cambio puede aparecer al arrancar: un reducer o un efecto con un matcher que nunca podría
coincidir ahora lanza al registrarse. Lee esa sección primero. Otros tres cambian el
comportamiento en los bordes: un store liberado es inerte, la persistencia nunca escribe un estado
parcial, y un typed array en el estado cambia como un solo valor. Después vienen un aviso que ahora
nombra su store, dos arreglos de tipos, y las adiciones: tiempo y timers inyectables, una sola
costura de diagnósticos, store.signal y ctx.signal, cancelación para store.call(), canales
efímeros y una revisión de tamaños. La mayoría de las aplicaciones no necesitan cambiar código.
Antes de 1.0, así que es un incremento MINOR según la política del repositorio.
#Un reducer o un efecto con channelPattern ahora lanza
Lo notarás si: registras un reducer o un efecto con when: { channelPattern }, o con un when
que no tiene ninguna de las cinco formas. createStore, registerReducer, registerSlice,
registerEffect, replaceReducers, replaceEffects y hotReplace lanzan un Error que nombra
el registro. TypeScript lo reporta antes: ReducerSpec.when y EffectSpec.when ahora tienen el
tipo ExactWhen.
Solo el middleware ha respetado channelPattern. En un reducer o un efecto se aceptaba sin aviso
y no manejaba nada: el reducer nunca se ejecutaba, así que su slice se quedaba en su estado
inicial, y el efecto ni siquiera se reportaba a onRegistrationChange. Si tu código registraba
uno, nunca funcionó, y esta versión lo hace visible.
Los reducers siguen siendo exactos a propósito. Su conjunto de entrada tiene que seguir cerrado, para que reproducir un log con el mismo código pliegue los mismos eventos sin importar lo que una decoración agregue después. Los efectos de un evento se ejecutan en secuencia, y un patrón enrolaría a un efecto en la cadena de cada canal que coincidiera.
// Antes: se aceptaba, y el reducer nunca se ejecutaba.
store.registerReducer("plans", { state, when: { channelPattern: "*plan" }, reducer });
// Después: nombra los canales que pliega el slice.
store.registerReducer("plans", { state, when: { channels: ["plan", "bb::plan"] }, reducer });Si los canales no se pueden nombrar de antemano, deja el patrón en un middleware, donde sí está soportado.
La misma verificación cubre ahora todas las costuras. Un when que no es ninguna de las cinco
formas, como {}, { any: false } o { keys: "plan" }, lanza en reducers, efectos y middleware
por igual. Antes no coincidía con nada. { keys: [] } y { channels: [] } se siguen aceptando,
porque están bien formados y una lista puede estar vacía legítimamente.
Una llamada rechazada no cambia nada. Cada punto de entrada verifica el lote completo antes de
tocar el store, así que un replaceReducers o hotReplace que lanza deja instalados y
funcionando los reducers, efectos y middleware anteriores.
#Un store liberado es inerte
Lo notarás si: algo emite, llama o registra en un store después de dispose(), o una llamada
sigue esperando respuesta cuando su store se libera.
dispose() vaciaba los registros del store y dejaba lo demás funcionando. Un emit posterior
seguía ejecutando el middleware y los reducers que quedaran, un call pendiente esperaba hasta su
timeout de inactividad, y nada avisaba al trabajo atado al store de que ya no existía. Ahora:
emit()se resuelve con{ committed: false, written: false }sin ejecutar nada.call()se rechaza de inmediato conCallAbortedError("store disposed"), y una llamada todavía pendiente cuando el store se libera se rechaza igual.registerReducer,registerSlice,registerMiddleware,registerEffect, los helperswith*yonEffect,replace*yhotReplacelanzan unErrorque nombra el store.- Un
emito uncalltardío se reporta una vez por método en desarrollo, comouse-after-dispose. dispose()es idempotente.
Nuevo: store.signal, un AbortSignal que se aborta al final de dispose(), para atar recursos a
la vida del store. Consulta Atar recursos al store.
Un call() cuyo propio signal ya está abortado tampoco envía su petición ni arma un timer: antes
se rechazaba y la enviaba de todos modos.
#La persistencia nunca escribe un estado parcial
Lo notarás si: un estado persistido puede crecer más allá de 100 000 valores, o haces
aserciones sobre lo que recibe onError en la fase "encode", o sobre cuántas veces se escribe un
adaptador.
persist codificaba el estado hasta un presupuesto de nodos y escribía lo que cupiera,
reemplazando una instantánea anterior completa por una cortada a la mitad. Esa instantánea se
hidrataba después en un estado que ningún reducer produjo. Un estado más allá del presupuesto
ahora no se escribe: el almacenamiento conserva su valor anterior, y onError recibe un
PersistEncodeError (nuevo) con truncated: true y written: false. El presupuesto es
maxNodes, nuevo en PersistOptions, con el valor de 100 000 que siempre tuvo. dehydrate
devuelve "" en ese caso, que se hidrata como nada que restaurar.
Un valor sin representación fiel se sigue escribiendo, como antes, y ahora se reporta como un
PersistEncodeError con written: true y sus rutas en unsupported, en lugar de un Error
simple. Su mensaje sigue nombrando las rutas.
persist además escribe solo después de un evento que cambió el estado. Un evento vetado o
rechazado, o un reducer que devuelve su entrada, programaba antes una reescritura de lo que el
almacenamiento ya tenía.
#Un typed array en el estado cambia como un solo valor
Lo notarás si: el estado guarda un typed array, un DataView o un ArrayBuffer, y lees
changedPaths, prevValues o nextValues desde store.instrument(), o te conectas a una ruta
dentro de uno.
Los índices de un typed array son sus propias llaves, así que el detector de cambios lo recorría
como un objeto: reemplazar un Uint8Array de 4 bytes reportaba cuatro rutas, una por byte, y un
observador de instrumentación copiaba cada byte distinto. Ahora una vista es un solo valor en su
propia ruta, comparado por referencia, que es como ya se trataban Map y Set. Reemplazarla
reporta su ruta una vez, con la vista vieja y la nueva como valores; devolver la misma vista no es
un cambio. Una suscripción a un índice dentro de una vista, como buf.0, ya no se notifica;
suscríbete a la ruta de la vista.
El aviso de payload guardado por referencia también distingue los datos binarios. Una vista no se
puede congelar, así que el mensaje anterior ("it is now frozen ... will throw") era falso para ella:
nada lanza, y una escritura posterior en el buffer cambia el slice en su lugar, sin que los
suscriptores se enteren. Ahora el aviso dice eso, y también detecta un buffer conservado desde un
campo del payload, como hace { ...payload }.
#El aviso de colisión de claves se lleva por store
Lo notarás si: un proceso ejecuta varios stores en desarrollo, como una suite de pruebas o una
app que crea más de uno, y dos pares (channel, type) de un mismo store se unen en la misma
clave interna.
El aviso se recordaba para todo el proceso. Una vez que un store había reportado una clave, un segundo store con la misma colisión no decía nada; y dos stores que usaban cada uno uno de los pares, que no pueden interferir, se reportaban como en colisión. Ahora se lleva por store y nombra el store. No hace falta cambiar código; quizá veas un aviso que antes se suprimía, o dejes de ver uno que era incorrecto.
#replaceReducers y hotReplace tipan cada slice por separado
Lo notarás si: llamas replaceReducers o hotReplace({ reducer }) en un store con dos o más
slices, o en un store que una librería decoró.
El argumento exigía todos los nombres de slice y tipaba cada reducer con la unión de los estados de
todas las slices. Un reducer anotado no compilaba en ningún store con dos slices, un reducer de una
slice podía devolver el estado de otra, y en un store decorado la única llamada que compilaba
nombraba la slice de la librería, que el runtime rechaza. Ahora es ReducerReplacement: toda clave
opcional, cada una tipada con el estado de su propia slice, que es lo que el runtime hace desde
0.8.0. El código que compilaba sigue compilando, salvo que un reducer devolviera el estado de otra
slice.
EventFromWhen gana además su rama para channelPattern: resuelve a la unión completa de eventos,
que es lo que recibe un handler de middleware, en lugar de never.
#Adiciones que podrías querer
ExactWhen<EM>, las formas de When que aceptan un reducer o un efecto (When sin
channelPattern). Úsalo para tipar un helper que construye specs de reducers o efectos.
correlation en store.call(). "either" (por defecto, sin cambios), "causal" o "id".
Usa "id" cuando el protocolo de un Quien Responde lleva su propio id de petición y tiene varias
peticiones en curso en un canal: ahí, el vínculo con el padre puede apuntar a la petición
equivocada. Consulta la guía de Petición y Respuesta.
ReducerReplacement<R, S, EM>, el argumento que toma replaceReducers, para tipar un handler
de HMR que construye el mapa antes de llamarlo.
clock y scheduler en createStore, con los tipos Clock, Scheduler y TimerHandle. El
store lee la hora y arma todos sus timers a través de ellos: las ventanas de deduplicación, la
limpieza de la caché de deduplicación y el timeout de inactividad de store.call(). persist también
toma una opción scheduler. Los valores por defecto se comportan como antes. Consulta
Tiempo y timers.
at, parentId y depth en los eventos instrumentados. InstrumentedEvent.at es la hora del
reloj a la que el store procesó el evento, y event.parentId y event.depth llevan su posición
causal, ausentes en un evento raíz como lo están en Event. Un observador que registra o traza
eventos ya no tiene que tomar su propia marca de tiempo ni perder la cadena. Si construyes valores
InstrumentedEvent tú mismo, para alimentar un observador falso en un test, agrega at.
diagnostics en createStore, y store.onDiagnostic. Cada fallo que el store contiene, cada
rechazo y cada aviso de desarrollo es ahora un Diagnostic con un code estable. El sink
diagnostics del dueño reemplaza la salida a consola; sin él, la salida a consola no cambia.
store.onDiagnostic permite que código conectado después, como una decoración, observe los mismos
diagnósticos sin silenciar nada. Los hooks existentes se siguen disparando. Consulta
Errores y diagnósticos.
EventBus y LooseEventBus aceptan un callback opcional para errores de handlers en su
constructor. Usa la consola por defecto, como antes.
ctx para los efectos, con ctx.signal. Un efecto recibe un cuarto argumento, un
EffectContext (nuevo), cuyo signal se aborta cuando el efecto deja de estar registrado: corre
su disposer, replaceEffects o hotReplace lo quitan, o el store se libera. Los handlers de
onEffect lo reciben como quinto argumento. Los efectos escritos con tres parámetros no se ven
afectados. Si llamas un EffectFunction tú mismo, en un test, pásale un contexto:
{ signal: new AbortController().signal }.
cancel en store.call(), con los tipos CallCancellation y CancelKey. Nombra un evento y
la llamada lo emite, con el id de la petición y la razón, cuando se cancela, se aborta o expira,
para que Quien Responde pueda dejar de trabajar. Consulta la
guía de Petición y Respuesta.
Canales ephemeral, y store.instrument(observer, { ephemeral }). Los eventos de un canal
efímero se manejan como siempre pero llegan solo a los observadores de instrumentación que se
suscriben a ellos, y el replay los omite. El agente de DevTools no se suscribe; persist sí.
Tráfico que no es historia y
Valores que cambian muchas veces por segundo
en el README describen el patrón para valores de alta frecuencia. Un PersistableStore que
implementes tú mismo debe aceptar el nuevo segundo argumento opcional de instrument.
warnOnLargeValues(store, limits?), una revisión solo de desarrollo para payloads y slices que
crecieron demasiado, como un import aparte. Consulta
Valores que crecieron demasiado.
matchesWhen y describeWhenProblem, el matcher y el validador del propio store, con el tipo
WhenConsumer, para código que filtra eventos con un When fuera del store. Consulta
Coincidir fuera del store.
store.instrumentEffects(observer, options?), con los tipos InstrumentedEffects,
InstrumentedEffect y EffectsObserver: cuando todos los efectos de un evento terminaron, el
nombre, el origen, la duración de cada efecto y si falló. Consulta
Observar la fase de efectos.
store.metrics() y store.whenIdle(), con el tipo StoreMetrics: la carga actual del store, y
una promesa que se resuelve cuando ningún evento espera a ser reducido y ningún efecto corre, para un
desmontaje limpio. Consulta Carga, y esperar a que termine.
La función que detiene persist() devuelve una promesa que se resuelve cuando la última
escritura terminó, para que la escritura final pueda esperarse antes de desmontar el store. Nunca
se rechaza. El código que ignoraba el valor de retorno no se ve afectado.