@yoltra/core 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.es.md +127 -11
- package/README.md +126 -12
- package/dist/types/index.d.ts +2 -1
- package/dist/types/persistence/persist.d.ts +2 -2
- package/dist/types/store/Store.d.ts +368 -14
- package/dist/types/store/fingerprint.d.ts +62 -0
- package/dist/types/types.d.ts +524 -14
- package/dist/yoltra.cjs +2 -2
- package/dist/yoltra.cjs.map +1 -1
- package/dist/yoltra.mjs +1668 -880
- package/dist/yoltra.mjs.map +1 -1
- package/dist/yoltra.umd.js +2 -2
- package/dist/yoltra.umd.js.map +1 -1
- package/package.json +3 -2
package/README.es.md
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
> 👉 🇲🇽 Versión en Español |
|
|
6
6
|
> [ 🇺🇸 English Versión](./README.md)
|
|
7
7
|
|
|
8
|
-
](https://www.npmjs.com/package/@yoltra/core)
|
|
9
|
+
[](https://www.npmjs.com/package/@yoltra/core)
|
|
10
|
+
[](https://www.npmjs.com/package/@yoltra/core)
|
|
11
|
+
[](https://github.com/yoltra/yoltra/blob/main/LICENSE)
|
|
10
12
|
|
|
11
13
|
**Contenedor de estado orientado a eventos, agnóstico de framework, con suscripciones de grano
|
|
12
14
|
fino por ruta.**
|
|
@@ -231,7 +233,8 @@ const adminGuard: MiddlewareSpec<AppState, AppEM> = {
|
|
|
231
233
|
meta: { type: "middleware", name: "adminGuard" },
|
|
232
234
|
};
|
|
233
235
|
|
|
234
|
-
// Middleware global: se ejecuta para todos los eventos
|
|
236
|
+
// Middleware global: se ejecuta para todos los eventos. Sincrono, nunca una Promise: solo un
|
|
237
|
+
// `false` explicito veta, asi que un middleware que solo observa puede no devolver nada.
|
|
235
238
|
const logger = (state, event) => {
|
|
236
239
|
console.log("Event:", event.channel, event.type);
|
|
237
240
|
return true;
|
|
@@ -331,6 +334,25 @@ store.onEvent(
|
|
|
331
334
|
);
|
|
332
335
|
```
|
|
333
336
|
|
|
337
|
+
### Suscriptores de eventos y viaje en el tiempo
|
|
338
|
+
|
|
339
|
+
**El replay no llama a tus handlers.** Recorrer una línea de tiempo de DevTools vuelve a reducir
|
|
340
|
+
los eventos, así que el estado sigue el recorrido, pero los handlers de `onEvent` permanecen en
|
|
341
|
+
silencio. Antes se ejecutaban igual que con un evento real, así que arrastrar la línea de tiempo
|
|
342
|
+
volvía a publicar a los pares, a escribir en sockets y a disparar analítica por eventos que no
|
|
343
|
+
estaban ocurriendo de nuevo, sin nada dentro del handler que permitiera notar la diferencia.
|
|
344
|
+
|
|
345
|
+
Un handler que deriva estado de vista puramente del flujo de eventos, y que no hace E/S, puede
|
|
346
|
+
activarlo:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
store.onEvent("ui", "save", handler, "committed", { duringReplay: true });
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`store.isReplaying` existe para lo que deba ramificar en lugar de simplemente omitirse. Los
|
|
353
|
+
suscriptores gruesos de `subscribe` y las suscripciones de `connect` siguen disparándose, porque
|
|
354
|
+
el estado sí cambió y la interfaz tiene que seguir el recorrido.
|
|
355
|
+
|
|
334
356
|
---
|
|
335
357
|
|
|
336
358
|
## Los commits son atómicos entre slices
|
|
@@ -577,6 +599,65 @@ const dispose = store.registerReducer("filters", {
|
|
|
577
599
|
dispose();
|
|
578
600
|
```
|
|
579
601
|
|
|
602
|
+
### Decorar un store, con sus tipos
|
|
603
|
+
|
|
604
|
+
Una slice agregada en runtime era invisible para el sistema de tipos: `registerReducer`
|
|
605
|
+
recibía un `string` y devolvía un disposer, así que nada aguas abajo sabía que la slice
|
|
606
|
+
existía ni qué forma tenía. `withSlice` devuelve **el mismo store, re-tipado**:
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
type TransferEM = { transfer: { granted: { id: string } } };
|
|
610
|
+
|
|
611
|
+
const transfers = defineSlice<TransferEM>()({
|
|
612
|
+
state: { granted: [] as string[] },
|
|
613
|
+
when: { keys: [["transfer", "granted"]] },
|
|
614
|
+
reducer: (s, e) => (e.type === "granted" ? { granted: [...s.granted, e.payload.id] } : s),
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const app = store.withSlice("transfers", transfers, { owner: "@scope/transfers" });
|
|
618
|
+
|
|
619
|
+
app.getState().transfers.granted; // string[]
|
|
620
|
+
app.emit("transfer", "granted", { id: "a1" }); // el canal nuevo ya es emitible
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
`withMiddleware` y `withEffect` hacen lo mismo para el mapa de eventos. Las llamadas se
|
|
624
|
+
encadenan, y una librería publica un decorador tomando un store y devolviendo otro:
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
export function withTransfers<R extends string, S extends Record<R, any>, EM extends EventMapBase>(
|
|
628
|
+
store: StoreInstance<R, S, EM>,
|
|
629
|
+
config: TransfersConfig,
|
|
630
|
+
) {
|
|
631
|
+
return store.withSlice("transfers", transfers, { owner: "@scope/transfers" });
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Los decoradores se anidan, en cualquier orden.
|
|
635
|
+
const decorated = withTransfers(withDevtools(store, dtConfig), config);
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
**Por qué los builders.** El `when` de un spec lleva cadenas de canal y tipo, no tipos de
|
|
639
|
+
payload, así que el mapa de eventos que aporta una decoración no puede inferirse de ahí, y
|
|
640
|
+
TypeScript no tiene inferencia parcial de argumentos de tipo. `defineSlice<EM>()` lo coloca en
|
|
641
|
+
posición de valor, donde la inferencia sí funciona, así que ningún sitio de registro necesita
|
|
642
|
+
un argumento de tipo ni un cast. Una consecuencia que conviene conocer: **una función de
|
|
643
|
+
middleware sin spec nunca puede ampliar el mapa de eventos**, porque el parámetro de evento de
|
|
644
|
+
`MiddlewareFunction` es un tipo mapeado del que no se puede inferir nada de vuelta. Solo la
|
|
645
|
+
forma de spec de `defineMiddleware` puede.
|
|
646
|
+
|
|
647
|
+
**Es el mismo objeto.** Nada se vuelve a suscribir, ningún estado se mueve, y una llamada
|
|
648
|
+
`store.call()` en vuelo no se ve afectada. Solo cambia el tipo.
|
|
649
|
+
|
|
650
|
+
**Orden.** Decora en el ámbito del módulo, una vez, antes del primer render. Entre
|
|
651
|
+
`createStore` y la decoración la slice realmente no existe, y un componente que la lea verá
|
|
652
|
+
`undefined` hasta que exista.
|
|
653
|
+
|
|
654
|
+
**Disposición.** `withSlice` no devuelve disposer a propósito: después de ejecutarlo, el tipo
|
|
655
|
+
ampliado sigue prometiendo una slice que ya no está, y ningún sistema de tipos puede expresar
|
|
656
|
+
"válido hasta esa llamada". Usa `registerSlice` cuando la slice sea tuya y necesites
|
|
657
|
+
desmontarla, y mantén ese disposer privado a la librería. Leer una slice desmontada lanza un
|
|
658
|
+
error con nombre en desarrollo, en lugar de devolver `undefined` desde un tipo que prometía un
|
|
659
|
+
valor.
|
|
660
|
+
|
|
580
661
|
---
|
|
581
662
|
|
|
582
663
|
## Hot Module Replacement
|
|
@@ -605,6 +686,32 @@ if (import.meta.hot) {
|
|
|
605
686
|
}
|
|
606
687
|
```
|
|
607
688
|
|
|
689
|
+
### `replace*` reemplaza lo que tú escribiste, no lo que agregó una librería
|
|
690
|
+
|
|
691
|
+
Un reducer, middleware o efecto registrado **después** de la construcción, con
|
|
692
|
+
`registerReducer`, `registerMiddleware` o `registerEffect`, sobrevive a una llamada a
|
|
693
|
+
`replace*`. Esos registros nunca formaron parte del conjunto que estás reemplazando: nadie que
|
|
694
|
+
escribe `replaceReducers(myReducers)` quiere decir "y ademas borra la slice que montó devtools,
|
|
695
|
+
junto con su estado".
|
|
696
|
+
|
|
697
|
+
Antes ocurría lo contrario, lo que hacía que la línea de HMR de arriba borrara la slice de una
|
|
698
|
+
librería y su estado al primer guardado de archivo, sin error y sin advertencia. Es también la
|
|
699
|
+
razón por la que una llamada `store.call()` en vuelo ya no muere a mitad de recarga: su
|
|
700
|
+
listener de respuesta pertenece al propio store.
|
|
701
|
+
|
|
702
|
+
Pasa `{ scope: "all" }` para el comportamiento anterior, que un arnés de pruebas que reinicia un
|
|
703
|
+
store entre casos sí puede querer:
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
store.replaceReducers(nextReducers, { scope: "all" });
|
|
707
|
+
store.hotReplace({ reducer: nextReducers, scope: "all" }); // se reenvía a los tres
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
Una aplicación que declara una slice que una librería ya montó recibe un error que nombra la
|
|
711
|
+
slice, en lugar de una apropiación silenciosa que deja a la librería con un disposer de algo que
|
|
712
|
+
ya no es suyo. En desarrollo, `replace*` registra en nivel debug cuando preservó algo, así que
|
|
713
|
+
"por qué sigue disparándose ese efecto tras la recarga" tiene respuesta.
|
|
714
|
+
|
|
608
715
|
---
|
|
609
716
|
|
|
610
717
|
## Mejores Prácticas
|
|
@@ -667,7 +774,8 @@ store.registerEffect({
|
|
|
667
774
|
| `store.getState()` | Obtener snapshot del estado actual (solo lectura) |
|
|
668
775
|
| `store.subscribe(listener)` | Suscripción gruesa (cualquier cambio de estado) |
|
|
669
776
|
| `store.connect(spec, handler)` | Suscripción de grano fino por ruta con wildcards |
|
|
670
|
-
| `store.onEvent(channel, type, handler, phase?)` | Suscripción a eventos (committed/uncommitted/all)
|
|
777
|
+
| `store.onEvent(channel, type, handler, phase?, options?)` | Suscripción a eventos (committed/uncommitted/written/all). Silenciosa durante el replay salvo `{ duringReplay: true }` |
|
|
778
|
+
| `store.onRegistrationChange(observer, opts?)` | Avisa cuando el store gana o pierde un reducer, middleware o efecto |
|
|
671
779
|
| `store.onEffect(channel, type, handler)` | Shorthand de efecto para un solo evento |
|
|
672
780
|
| `store.dispose()` | Limpiar timers y recursos |
|
|
673
781
|
|
|
@@ -675,6 +783,10 @@ store.registerEffect({
|
|
|
675
783
|
|
|
676
784
|
| API | Descripción |
|
|
677
785
|
| ----------------------------------- | ----------------------------------------- |
|
|
786
|
+
| `store.registerSlice(name, spec, opts?)` | Agrega un slice en runtime; devuelve el store re-tipado y un disposer |
|
|
787
|
+
| `store.withSlice(name, spec, opts?)` | Igual, devolviendo el store re-tipado para encadenar |
|
|
788
|
+
| `store.withMiddleware(mw)`, `store.withEffect(spec)` | Registra y amplía el mapa de eventos |
|
|
789
|
+
| `defineSlice<EM>()`, `defineMiddleware<EM>()`, `defineEffect<EM>()` | Declara el mapa de eventos que aporta un spec |
|
|
678
790
|
| `store.registerReducer(name, spec)` | Agregar un slice en tiempo de ejecución |
|
|
679
791
|
| `store.registerMiddleware(fn)` | Agregar middleware en tiempo de ejecución |
|
|
680
792
|
| `store.registerEffect(spec)` | Agregar un efecto en tiempo de ejecución |
|
|
@@ -683,10 +795,10 @@ store.registerEffect({
|
|
|
683
795
|
|
|
684
796
|
| API | Descripción |
|
|
685
797
|
| --------------------------------------- | ------------------------------------------- |
|
|
686
|
-
| `store.replaceReducers(reducers, opts)`
|
|
687
|
-
| `store.replaceMiddleware(middleware)`
|
|
688
|
-
| `store.replaceEffects(effects)`
|
|
689
|
-
| `store.hotReplace(partial)`
|
|
798
|
+
| `store.replaceReducers(reducers, opts)` | Reemplaza los reducers del spec; los de runtime sobreviven salvo `{ scope: "all" }` |
|
|
799
|
+
| `store.replaceMiddleware(middleware, opts)` | Reemplaza el middleware del spec; misma regla |
|
|
800
|
+
| `store.replaceEffects(effects, opts)` | Reemplaza los efectos del spec; misma regla |
|
|
801
|
+
| `store.hotReplace(partial)` | Reemplaza cualquier subconjunto; reenvía `scope` |
|
|
690
802
|
|
|
691
803
|
### Helpers
|
|
692
804
|
|
|
@@ -810,9 +922,9 @@ La cifra que importa es lo que importas, no lo que el paquete exporta:
|
|
|
810
922
|
<!-- size-table:start -->
|
|
811
923
|
| Import | Tamaño | Presupuesto |
|
|
812
924
|
| --- | --- | --- |
|
|
813
|
-
| `{ createStore }` |
|
|
814
|
-
| `{ createStore, hydrate, persist }` |
|
|
815
|
-
| todo |
|
|
925
|
+
| `{ createStore }` | 11.5 KB | 14 KB |
|
|
926
|
+
| `{ createStore, hydrate, persist }` | 12.8 KB | 16 KB |
|
|
927
|
+
| todo | 14.2 KB | 18 KB |
|
|
816
928
|
<!-- size-table:end -->
|
|
817
929
|
|
|
818
930
|
Estas son cifras de **producción**: lo que públicas una vez que tu empaquetador define
|
|
@@ -837,6 +949,10 @@ es algo que nadie escriba.
|
|
|
837
949
|
Hooks de React y Suspense
|
|
838
950
|
- **[Guia de Inicio Rápido](https://github.com/yoltra/yoltra/blob/main/docs/en/QUICK_START_GUIDE.md)**:
|
|
839
951
|
Cinco pasos hacia una app funcional
|
|
952
|
+
- **[Actualizar a 0.8.0](https://github.com/yoltra/yoltra/blob/main/docs/es/UPGRADE_0.8.md)**:
|
|
953
|
+
Cinco cambios de comportamiento, y un riesgo si haces rollback
|
|
954
|
+
- **[Guía de Decoración](https://github.com/yoltra/yoltra/blob/main/docs/es/DECORATION_GUIDE.md)**:
|
|
955
|
+
Agregar una slice, middleware o efecto al store de alguien más, con los tipos
|
|
840
956
|
- **[Arquitectura de Cola de Eventos](https://github.com/yoltra/yoltra/blob/main/docs/en/design/event-queue-architecture.md)**:
|
|
841
957
|
Inmersión técnica profunda
|
|
842
958
|
- **[Comparación de Bibliotecas](https://github.com/yoltra/yoltra/blob/main/docs/en/design/state-management-library-comparison.md)**:
|
package/README.md
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
> [ 🇲🇽 Versión en Español](./README.es.md)
|
|
6
6
|
> | 👉 🇺🇸 English Version
|
|
7
7
|
|
|
8
|
-
](https://www.npmjs.com/package/@yoltra/core)
|
|
9
|
+
[](https://www.npmjs.com/package/@yoltra/core)
|
|
10
|
+
[](https://www.npmjs.com/package/@yoltra/core)
|
|
11
|
+
[](https://github.com/yoltra/yoltra/blob/main/LICENSE)
|
|
10
12
|
|
|
11
13
|
**Framework-agnostic event-driven state container with fine-grained path subscriptions.**
|
|
12
14
|
|
|
@@ -212,7 +214,10 @@ const globalLogger = {
|
|
|
212
214
|
## Middleware
|
|
213
215
|
|
|
214
216
|
Middleware runs **synchronously, before** reducers and can cancel event propagation (return
|
|
215
|
-
`false` to reject → "uncommitted" event). Async work belongs in
|
|
217
|
+
`false` to reject → "uncommitted" event; returning nothing allows it). Async work belongs in
|
|
218
|
+
effects, not middleware. When an event does not commit, `emit` says why: `reason` is
|
|
219
|
+
`"vetoed"`, `"deduped"` or `"cascade"`, and a veto names the middleware in `vetoedBy`, so a
|
|
220
|
+
guard refusing an action is distinguishable from a double-click being collapsed. Supports
|
|
216
221
|
both raw functions (legacy) and `MiddlewareSpec` objects with targeting:
|
|
217
222
|
|
|
218
223
|
```typescript
|
|
@@ -228,7 +233,8 @@ const adminGuard: MiddlewareSpec<AppState, AppEM> = {
|
|
|
228
233
|
meta: { type: "middleware", name: "adminGuard" },
|
|
229
234
|
};
|
|
230
235
|
|
|
231
|
-
// Global middleware: runs for all events
|
|
236
|
+
// Global middleware: runs for all events. Synchronous, never a Promise: only an explicit
|
|
237
|
+
// `false` vetoes, so middleware that just observes can return nothing at all.
|
|
232
238
|
const logger = (state, event) => {
|
|
233
239
|
console.log("Event:", event.channel, event.type);
|
|
234
240
|
return true;
|
|
@@ -343,6 +349,24 @@ all. `written` is the stricter fact, added rather than substituted, so toasts an
|
|
|
343
349
|
working unchanged. `all` stays `committed | uncommitted`; folding `written` in would hand existing
|
|
344
350
|
subscribers a second notification per event.
|
|
345
351
|
|
|
352
|
+
### Event subscribers and time-travel
|
|
353
|
+
|
|
354
|
+
**Replay does not call your handlers.** Scrubbing a DevTools timeline reduces the events again,
|
|
355
|
+
so state follows the scrub, but `onEvent` handlers stay silent. They used to run exactly as they
|
|
356
|
+
do for a live event, which meant dragging a timeline re-published to peers, re-wrote to sockets
|
|
357
|
+
and re-fired analytics for events that were not happening again, with nothing available inside a
|
|
358
|
+
handler to tell the difference.
|
|
359
|
+
|
|
360
|
+
A handler that derives view state purely from the event stream, and performs no I/O, can opt in:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
store.onEvent("ui", "save", handler, "committed", { duringReplay: true });
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`store.isReplaying` is there for anything that has to branch rather than simply skip. Coarse
|
|
367
|
+
`subscribe` listeners and `connect` subscriptions keep firing throughout, because the state
|
|
368
|
+
genuinely did change and the UI has to follow the scrub.
|
|
369
|
+
|
|
346
370
|
---
|
|
347
371
|
|
|
348
372
|
## Commits are atomic across slices
|
|
@@ -605,6 +629,63 @@ const dispose = store.registerReducer("filters", {
|
|
|
605
629
|
dispose();
|
|
606
630
|
```
|
|
607
631
|
|
|
632
|
+
### Decorating a store, with its types
|
|
633
|
+
|
|
634
|
+
A slice added at runtime used to be invisible to the type system: `registerReducer` took a
|
|
635
|
+
plain `string` and returned a bare disposer, so nothing downstream knew the slice existed or
|
|
636
|
+
what shape it had. `withSlice` returns **the same store, re-typed**:
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
type TransferEM = { transfer: { granted: { id: string } } };
|
|
640
|
+
|
|
641
|
+
const transfers = defineSlice<TransferEM>()({
|
|
642
|
+
state: { granted: [] as string[] },
|
|
643
|
+
when: { keys: [["transfer", "granted"]] },
|
|
644
|
+
reducer: (s, e) => (e.type === "granted" ? { granted: [...s.granted, e.payload.id] } : s),
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const app = store.withSlice("transfers", transfers, { owner: "@scope/transfers" });
|
|
648
|
+
|
|
649
|
+
app.getState().transfers.granted; // string[]
|
|
650
|
+
app.emit("transfer", "granted", { id: "a1" }); // the new channel is emittable
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
`withMiddleware` and `withEffect` do the same for the event map. Calls chain, and a library
|
|
654
|
+
publishes a decorator by taking a store and returning one:
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
export function withTransfers<R extends string, S extends Record<R, any>, EM extends EventMapBase>(
|
|
658
|
+
store: StoreInstance<R, S, EM>,
|
|
659
|
+
config: TransfersConfig,
|
|
660
|
+
) {
|
|
661
|
+
return store.withSlice("transfers", transfers, { owner: "@scope/transfers" });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Decorators nest, in any order.
|
|
665
|
+
const decorated = withTransfers(withDevtools(store, dtConfig), config);
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Why the builders.** A spec's `when` carries channel and type strings and no payload types,
|
|
669
|
+
so the event map a decoration contributes cannot be inferred from it, and TypeScript has no
|
|
670
|
+
partial type-argument inference. `defineSlice<EM>()` puts it in a value position, where
|
|
671
|
+
inference works, so no registration site needs a type argument or a cast. One consequence
|
|
672
|
+
worth knowing: **a bare middleware function can never widen the event map**, because
|
|
673
|
+
`MiddlewareFunction`'s event parameter is a mapped type nothing can be inferred back out of.
|
|
674
|
+
Only the spec form from `defineMiddleware` can.
|
|
675
|
+
|
|
676
|
+
**It is the same object.** Nothing re-subscribes, no state moves, and any in-flight
|
|
677
|
+
`store.call()` is unaffected. Only the type changes.
|
|
678
|
+
|
|
679
|
+
**Ordering.** Decorate at module scope, once, before the first render. Between `createStore`
|
|
680
|
+
and the decoration the slice genuinely does not exist, and a component reading it sees
|
|
681
|
+
`undefined` until it does.
|
|
682
|
+
|
|
683
|
+
**Disposal.** `withSlice` hands back no disposer on purpose: after one runs, the widened type
|
|
684
|
+
still promises a slice that is gone, and no type system can express "valid until that call".
|
|
685
|
+
Use `registerSlice` when you own the slice and need teardown, and keep that disposer private
|
|
686
|
+
to the library. Reading a disposed slice throws a named error in development rather than
|
|
687
|
+
returning `undefined` from a type that promised a value.
|
|
688
|
+
|
|
608
689
|
---
|
|
609
690
|
|
|
610
691
|
## Hot Module Replacement
|
|
@@ -633,6 +714,30 @@ if (import.meta.hot) {
|
|
|
633
714
|
}
|
|
634
715
|
```
|
|
635
716
|
|
|
717
|
+
### `replace*` replaces what you authored, not what a library added
|
|
718
|
+
|
|
719
|
+
A reducer, middleware or effect registered **after** construction, with `registerReducer`,
|
|
720
|
+
`registerMiddleware` or `registerEffect`, survives a `replace*` call. Those registrations were
|
|
721
|
+
never part of the set you are replacing: nobody writing `replaceReducers(myReducers)` means "and
|
|
722
|
+
also delete the slice devtools mounted, along with its state".
|
|
723
|
+
|
|
724
|
+
This used to go the other way, which made the HMR line above delete a library's slice and its
|
|
725
|
+
state on the first file save, with no error and no warning. It is also why an in-flight
|
|
726
|
+
`store.call()` no longer dies mid-reload: its reply listener belongs to the store itself.
|
|
727
|
+
|
|
728
|
+
Pass `{ scope: "all" }` for the old wholesale behaviour, which a test harness resetting a store
|
|
729
|
+
between cases may genuinely want:
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
store.replaceReducers(nextReducers, { scope: "all" });
|
|
733
|
+
store.hotReplace({ reducer: nextReducers, scope: "all" }); // forwards to all three
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
An application that authors a slice a library already mounted gets an error naming the slice,
|
|
737
|
+
rather than a silent takeover that leaves the library holding a disposer for something no longer
|
|
738
|
+
its own. In development, `replace*` logs at debug level when it preserved anything, so "why is
|
|
739
|
+
that effect still firing after a reload" has an answer.
|
|
740
|
+
|
|
636
741
|
---
|
|
637
742
|
|
|
638
743
|
## Best Practices
|
|
@@ -694,7 +799,8 @@ store.registerEffect({
|
|
|
694
799
|
| `store.getState()` | Get current readonly state snapshot |
|
|
695
800
|
| `store.subscribe(listener)` | Coarse subscription (any state change) |
|
|
696
801
|
| `store.connect(spec, handler)` | Fine-grained path subscription with wildcards |
|
|
697
|
-
| `store.onEvent(channel, type, handler, phase?)` | Event subscription (committed/uncommitted/all) |
|
|
802
|
+
| `store.onEvent(channel, type, handler, phase?, options?)` | Event subscription (committed/uncommitted/written/all). Silent during replay unless `{ duringReplay: true }` |
|
|
803
|
+
| `store.onRegistrationChange(observer, opts?)` | Fires when the store gains or loses a reducer, middleware or effect |
|
|
698
804
|
| `store.onEffect(channel, type, handler)` | Single-event effect shorthand |
|
|
699
805
|
| `store.dispose()` | Cleanup timers and resources |
|
|
700
806
|
|
|
@@ -702,6 +808,10 @@ store.registerEffect({
|
|
|
702
808
|
|
|
703
809
|
| API | Description |
|
|
704
810
|
| ----------------------------------- | ------------------------- |
|
|
811
|
+
| `store.registerSlice(name, spec, opts?)` | Add a slice at runtime; returns the widened store plus a disposer |
|
|
812
|
+
| `store.withSlice(name, spec, opts?)` | Same, returning the widened store for chaining |
|
|
813
|
+
| `store.withMiddleware(mw)`, `store.withEffect(spec)` | Register and widen the event map |
|
|
814
|
+
| `defineSlice<EM>()`, `defineMiddleware<EM>()`, `defineEffect<EM>()` | Declare the event map a spec contributes |
|
|
705
815
|
| `store.registerReducer(name, spec)` | Add a slice at runtime |
|
|
706
816
|
| `store.registerMiddleware(fn)` | Add middleware at runtime |
|
|
707
817
|
| `store.registerEffect(spec)` | Add an effect at runtime |
|
|
@@ -710,10 +820,10 @@ store.registerEffect({
|
|
|
710
820
|
|
|
711
821
|
| API | Description |
|
|
712
822
|
| --------------------------------------- | -------------------------- |
|
|
713
|
-
| `store.replaceReducers(reducers, opts)`
|
|
714
|
-
| `store.replaceMiddleware(middleware)`
|
|
715
|
-
| `store.replaceEffects(effects)`
|
|
716
|
-
| `store.hotReplace(partial)`
|
|
823
|
+
| `store.replaceReducers(reducers, opts)` | Replace spec reducers; runtime ones survive unless `{ scope: "all" }` |
|
|
824
|
+
| `store.replaceMiddleware(middleware, opts)` | Replace spec middleware; same rule |
|
|
825
|
+
| `store.replaceEffects(effects, opts)` | Replace spec effects; same rule |
|
|
826
|
+
| `store.hotReplace(partial)` | Replace any subset at once; forwards `scope` |
|
|
717
827
|
|
|
718
828
|
### Helpers
|
|
719
829
|
|
|
@@ -830,9 +940,9 @@ The number that matters is what you import, not what the package exports:
|
|
|
830
940
|
<!-- size-table:start -->
|
|
831
941
|
| Import | Size | Budget |
|
|
832
942
|
| --- | --- | --- |
|
|
833
|
-
| `{ createStore }` |
|
|
834
|
-
| `{ createStore, hydrate, persist }` |
|
|
835
|
-
| everything |
|
|
943
|
+
| `{ createStore }` | 11.5 KB | 14 KB |
|
|
944
|
+
| `{ createStore, hydrate, persist }` | 12.8 KB | 16 KB |
|
|
945
|
+
| everything | 14.2 KB | 18 KB |
|
|
836
946
|
<!-- size-table:end -->
|
|
837
947
|
|
|
838
948
|
These are **production** figures: what you ship once your bundler defines
|
|
@@ -861,6 +971,10 @@ stops a runaway from hanging the tab.
|
|
|
861
971
|
React hooks and Suspense
|
|
862
972
|
- **[Quick Start Guide](https://github.com/yoltra/yoltra/blob/main/docs/en/QUICK_START_GUIDE.md)**:
|
|
863
973
|
Five steps to a working app
|
|
974
|
+
- **[Upgrading to 0.8.0](https://github.com/yoltra/yoltra/blob/main/docs/en/UPGRADE_0.8.md)**:
|
|
975
|
+
Five behaviour changes, and one hazard if you roll back
|
|
976
|
+
- **[Decoration Guide](https://github.com/yoltra/yoltra/blob/main/docs/en/DECORATION_GUIDE.md)**:
|
|
977
|
+
Adding a slice, middleware or effect to somebody else's store, with the types
|
|
864
978
|
- **[Event Queue Architecture](https://github.com/yoltra/yoltra/blob/main/docs/en/design/event-queue-architecture.md)**:
|
|
865
979
|
Technical deep-dive
|
|
866
980
|
- **[Library Comparison](https://github.com/yoltra/yoltra/blob/main/docs/en/design/state-management-library-comparison.md)**:
|
package/dist/types/index.d.ts
CHANGED
|
@@ -17,7 +17,8 @@ export { detectChangedProps } from './utils/detectChangedProps.js';
|
|
|
17
17
|
export { freezeState } from './utils/immutability.js';
|
|
18
18
|
export type { AliasWatch } from './utils/immutability.js';
|
|
19
19
|
export { eventKeys } from './types.js';
|
|
20
|
-
export
|
|
20
|
+
export { defineSlice, defineMiddleware, defineEffect } from './types.js';
|
|
21
|
+
export type { Prettify, Merge, WidenNames, WidenState, EventMapCarrier, EMAddOf, Decoration, Decorated, StoreDecorator, Origin, RegistrationChange, RegistrationObserver, ReplaceScope, EmptyEventMap, NotCommittedReason, StateOfSpec, SatisfiesSlices, DecoratableStore, WidenedSlice, StoreDecoration, EventMapBase, EventKey, Event, EventUnion, Change, Emit, EmitOptions, EmitResult, ConnectOptions, EventMeta, InstrumentedEvent, CascadeInfo, InstrumentationObserver, Unsubscribe, StoreSpec, StoreInstance, ReducerSpec, ReducerFunction, ReducersMapAny, StateFromReducers, EMFromReducersStrict, EffectSpec, EffectFunction, MiddlewareFunction, MiddlewareSpec, MiddlewareInput, DeepReadonly, DeepRO, Primitive, RootValue, Path, PathValue, WithGlob, Dotted, EventPhase, NotifiedPhase, EventSubscriptionHandler, NarrowedEventHandler, When, EventFromWhen, EventConsumerType, EventConsumerMeta, } from './types.js';
|
|
21
22
|
export { createEntityAdapter } from './entity/entityAdapter.js';
|
|
22
23
|
export type { EntityAdapter, EntityAdapterOptions, EntityId, EntityState, EntityUpdate, } from './entity/entityAdapter.js';
|
|
23
24
|
export { decodeState, encodeState, encodeStateBounded } from './serialize/codec.js';
|
|
@@ -20,7 +20,7 @@ export interface PersistenceAdapter {
|
|
|
20
20
|
remove(key: string): void | Promise<void>;
|
|
21
21
|
}
|
|
22
22
|
/** Where a failure happened, so a handler can tell a bad write from a bad payload. */
|
|
23
|
-
export type PersistencePhase = "read" | "write" | "decode" | "migrate";
|
|
23
|
+
export type PersistencePhase = "read" | "write" | "decode" | "migrate" | "encode";
|
|
24
24
|
/** Shared configuration. */
|
|
25
25
|
export interface PersistOptions {
|
|
26
26
|
/** Storage key. */
|
|
@@ -120,4 +120,4 @@ export declare function persist(store: PersistableStore, options: PersistOptions
|
|
|
120
120
|
*
|
|
121
121
|
* @public
|
|
122
122
|
*/
|
|
123
|
-
export declare function dehydrate(store: Pick<PersistableStore, "getState">, options: Pick<PersistOptions, "version" | "slices">): string;
|
|
123
|
+
export declare function dehydrate(store: Pick<PersistableStore, "getState">, options: Pick<PersistOptions, "version" | "slices" | "onError">): string;
|