@yoltra/core 0.6.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 CHANGED
@@ -3,12 +3,14 @@
3
3
  # @yoltra/core
4
4
 
5
5
  > 👉 🇲🇽 Versión en Español  |
6
- >  [ 🇺🇸 English Version](./README.md) 
6
+ >  [ 🇺🇸 English Versión](./README.md) 
7
7
 
8
- ![npm downloads](https://badgen.net/npm/dm/@yoltra/core)
9
- ![License](https://badgen.net/npm/license/@yoltra/core)
8
+ [![versión npm](https://img.shields.io/npm/v/@yoltra/core)](https://www.npmjs.com/package/@yoltra/core)
9
+ [![descargas npm](https://img.shields.io/npm/dm/@yoltra/core)](https://www.npmjs.com/package/@yoltra/core)
10
+ [![tipos](https://img.shields.io/npm/types/@yoltra/core)](https://www.npmjs.com/package/@yoltra/core)
11
+ [![Licencia](https://img.shields.io/npm/l/@yoltra/core)](https://github.com/yoltra/yoltra/blob/main/LICENSE)
10
12
 
11
- **Contenedor de estado orientado a eventos, agnostico de framework, con suscripciones de grano
13
+ **Contenedor de estado orientado a eventos, agnóstico de framework, con suscripciones de grano
12
14
  fino por ruta.**
13
15
 
14
16
  `@yoltra/core` es la base de [yoltra](../../README.md).
@@ -17,7 +19,7 @@ Proporciona el store, el pipeline de eventos, middleware, efectos y el sistema d
17
19
 
18
20
  ---
19
21
 
20
- ## Instalacion
22
+ ## Instalación
21
23
 
22
24
  ```bash
23
25
  npm install @yoltra/core
@@ -27,14 +29,14 @@ npm install @yoltra/core
27
29
 
28
30
  ## El Pipeline de Eventos
29
31
 
30
- Cada llamada a `emit()` fluye a traves de un pipeline determinista:
32
+ Cada llamada a `emit()` fluye a través de un pipeline determinista:
31
33
 
32
34
  ```
33
35
  emit(channel, type, payload)
34
36
  │
35
37
  ├─ 0. Dedup (opt-in) ─── Omite un duplicado solo si dedupWindowMs > 0 o se pasa un dedupKey
36
38
  │
37
- │ ══ fase de reduccion SINCRONA — corre antes de que emit() retorne ══
39
+ │ ══ fase de reduccion SINCRONA: corre antes de que emit() retorne ══
38
40
  ├─ 1. Middleware ─── Hooks pre-reducer sincronos (devolver false para rechazar → evento "no confirmado")
39
41
  ├─ 2. Reducers ─── Cada slice que aplica se prepara, y todas se confirman bajo una sola raiz
40
42
  ├─ 3. Suscriptores de eventos ─── Notificaciones de eventos confirmados/no confirmados
@@ -43,11 +45,11 @@ emit(channel, type, payload)
43
45
  └─ 5. Efectos ─── Efectos secundarios ASYNC, una tarea independiente por evento (indexados para busqueda O(1))
44
46
  ```
45
47
 
46
- La fase de reduccion (1–4) es **sincrona**, asi que `getState()` es correcto en el instante en que
47
- `emit()` retorna — incluso con middleware. Los efectos (5) corren despues como una tarea async
48
+ La fase de reducción (1–4) es **síncrona**, así que `getState()` es correcto en el instante en que
49
+ `emit()` retorna, incluso con middleware. Los efectos (5) corren después como una tarea async
48
50
  independiente; la promesa de `emit()` se resuelve cuando terminan los efectos de ese evento. Cada
49
- etapa es interceptable, y `store.instrument()` expone todo el flujo — rutas hoja cambiadas, tiempos
50
- de reduccion, fase confirmado/rechazado — a las DevTools sin ningun `as any`. Ver la
51
+ etapa es interceptable, y `store.instrument()` expone todo el flujo (rutas hoja cambiadas, tiempos
52
+ de reducción, fase confirmado/rechazado) a las DevTools sin ningún `as any`. Ver la
51
53
  [Arquitectura del Pipeline de Eventos](../../docs/es/design/event-queue-architecture.md) para el
52
54
  modelo completo.
53
55
 
@@ -58,7 +60,7 @@ modelo completo.
58
60
  ### Eventos basados en canales
59
61
 
60
62
  Los eventos son tuplas `(channel, type, payload)`. Los canales proporcionan namespacing natural
61
- que escala en bases de codigo grandes:
63
+ que escala en bases de código grandes:
62
64
 
63
65
  ```typescript
64
66
  await store.emit("auth", "login", credentials);
@@ -66,28 +68,85 @@ await store.emit("analytics", "track", { event: "page_view" });
66
68
  await store.emit("ui", "toast", { message: "Saved!" });
67
69
  ```
68
70
 
69
- ### Suscripciones de grano fino via `connect()`
71
+ ### Suscripciones de grano fino vía `connect()`
70
72
 
71
- Suscribete a rutas de estado exactas usando notacion de puntos. Soporta wildcards `*` (un
72
- segmento) y `**` (cero o mas segmentos):
73
+ Suscríbete a rutas de estado exactas usando notación de puntos. Soporta wildcards `*` (un
74
+ segmento) y `**` (cero o más segmentos):
73
75
 
74
76
  ```typescript
75
- // Ruta exacta — se dispara cuando items[0].title cambia
77
+ // Ruta exacta: se dispara cuando items[0].title cambia
76
78
  store.connect({ reducer: "todos", property: "items.0.title" }, (change) =>
77
79
  console.log("title:", change.oldValue, "→", change.newValue),
78
80
  );
79
81
 
80
- // Wildcard de un segmento — se dispara cuando el titulo de CUALQUIER item cambia
82
+ // Wildcard de un segmento: se dispara cuando el titulo de CUALQUIER item cambia
81
83
  store.connect({ reducer: "todos", property: "items.*.title" }, (change) =>
82
84
  console.log("some title changed at", change.path),
83
85
  );
84
86
 
85
- // Wildcard profundo — se dispara cuando algo bajo items cambia
87
+ // Wildcard profundo: se dispara cuando algo bajo items cambia
86
88
  store.connect({ reducer: "todos", property: "items.**" }, (change) =>
87
89
  console.log("items tree changed at", change.path),
88
90
  );
89
91
  ```
90
92
 
93
+ ### Slices que contienen un solo valor
94
+
95
+ Una slice no tiene por qué ser un objeto. Un primitivo, un `Map`, un `Set` o una `Date` es un
96
+ estado de slice válido, y se confirma igual que cualquier otro:
97
+
98
+ ```typescript
99
+ const store = createStore({
100
+ name: "session",
101
+ reducer: {
102
+ token: {
103
+ state: null as string | null,
104
+ when: { keys: [["auth", "login"]] },
105
+ reducer: (_state, event) => event.payload.token,
106
+ },
107
+ },
108
+ });
109
+
110
+ await store.emit("auth", "login", { token: "abc123" });
111
+ store.getState().token; // "abc123"
112
+ ```
113
+
114
+ Una slice así no tiene ninguna propiedad debajo, así que sus cambios se reportan en la **raíz de
115
+ la slice**, la ruta vacía. Suscríbete a ella con `property: ""`:
116
+
117
+ ```typescript
118
+ store.connect({ reducer: "token", property: "" }, (change) =>
119
+ console.log("token:", change.oldValue, " --> ", change.newValue),
120
+ );
121
+ ```
122
+
123
+ Los tipos conocen la diferencia. `property` en una slice de valor raíz acepta `""` y nada más,
124
+ porque no hay ninguna clave que direccionar, y el valor vuelve correctamente tipado:
125
+
126
+ ```typescript
127
+ const token = useAtomicProp({ reducer: "token", property: "" }); // string | null
128
+ ```
129
+
130
+ ### `""` frente a `"**"`: observar una slice completa
131
+
132
+ Dos suscripciones que suenan iguales y no lo son:
133
+
134
+ | Patrón | Se dispara cuando |
135
+ |---|---|
136
+ | `""` | el **valor completo** de la slice se reemplaza: cambia un primitivo, se reconstruye un `Map`, una slice de objeto pasa a `null` |
137
+ | `"**"` | cambia **cualquier cosa** dentro de la slice, a cualquier profundidad. También coincide con la raíz, porque `**` coincide con cero segmentos |
138
+ | `"*"` | exactamente un nivel más abajo. Nunca coincide con la raíz |
139
+
140
+ **`"**"` es la suscripción a la slice completa, y funciona para toda slice sin importar su forma.**
141
+ Recurre a `""` solo cuando te refieras al valor raíz en sí; en una slice de objeto se queda
142
+ callada, porque una slice así reporta sus cambios en las hojas.
143
+
144
+ `Map` y `Set` se comparan por referencia, no por entrada: un reducer que devuelve un `Map` nuevo
145
+ es un cambio, mutar uno en el sitio no lo es. Eso se desprende del contrato de inmutabilidad en
146
+ vez de ser un caso especial. Construye una colección nueva en lugar de mutar la almacenada. Es
147
+ también la razón de que no tengan rutas debajo: `"byId"` es suscribible, `"byId.get"` no, y los
148
+ tipos lo dicen.
149
+
91
150
  ### Inmutabilidad
92
151
 
93
152
  El estado se congela profundamente antes de confirmarse. Las mutaciones lanzan error en modo
@@ -114,7 +173,7 @@ type AppEM = {
114
173
  system: { init: void; shutdown: void };
115
174
  };
116
175
 
117
- // Coincidir con claves de evento especificas (recomendado — preserva la correlacion de tipos)
176
+ // Coincidir con claves de evento especificas (recomendado: preserva la correlacion de tipos)
118
177
  const counterReducer = {
119
178
  state: { value: 0 },
120
179
  when: {
@@ -156,7 +215,7 @@ const globalLogger = {
156
215
 
157
216
  ## Middleware
158
217
 
159
- El middleware se ejecuta **sincronamente, antes** de los reducers y puede cancelar la propagacion
218
+ El middleware se ejecuta **sincronamente, antes** de los reducers y puede cancelar la propagación
160
219
  de eventos (devolver `false` para rechazar → evento "no confirmado"). El trabajo async va en los
161
220
  efectos, no en el middleware. Soporta tanto funciones directas (legacy) como objetos
162
221
  `MiddlewareSpec` con targeting:
@@ -164,7 +223,7 @@ efectos, no en el middleware. Soporta tanto funciones directas (legacy) como obj
164
223
  ```typescript
165
224
  import type { MiddlewareSpec } from "@yoltra/core";
166
225
 
167
- // Middleware con target — solo se ejecuta para eventos del canal admin
226
+ // Middleware con target: solo se ejecuta para eventos del canal admin
168
227
  const adminGuard: MiddlewareSpec<AppState, AppEM> = {
169
228
  when: { channel: "admin" },
170
229
  middleware: (state, event) => {
@@ -174,7 +233,8 @@ const adminGuard: MiddlewareSpec<AppState, AppEM> = {
174
233
  meta: { type: "middleware", name: "adminGuard" },
175
234
  };
176
235
 
177
- // Middleware global — se ejecuta para todos los eventos (sincrono: devuelve un boolean, nunca una Promise)
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.
178
238
  const logger = (state, event) => {
179
239
  console.log("Event:", event.channel, event.type);
180
240
  return true;
@@ -189,7 +249,7 @@ const store = createStore({
189
249
  });
190
250
  ```
191
251
 
192
- ### Middleware dinamico
252
+ ### Middleware dinámico
193
253
 
194
254
  ```typescript
195
255
  const off = store.registerMiddleware((state, event) => {
@@ -202,8 +262,8 @@ off(); // Remover despues
202
262
 
203
263
  ## Efectos
204
264
 
205
- Los efectos se ejecutan **despues** de los reducers y ven el estado final. Estan indexados por
206
- evento para busqueda O(1):
265
+ Los efectos se ejecutan **después** de los reducers y ven el estado final. Están indexados por
266
+ evento para búsqueda O(1):
207
267
 
208
268
  ```typescript
209
269
  // Via spec del store
@@ -244,16 +304,16 @@ const off2 = store.onEffect("ui", "save", async (payload, getState, emit) => {
244
304
 
245
305
  ## Suscripciones a Eventos
246
306
 
247
- Suscribete a eventos (no al estado) desde la capa de vista. Util para notificaciones,
307
+ Suscríbete a eventos (no al estado) desde la capa de vista. Útil para notificaciones,
248
308
  animaciones y reaccionar a eventos rechazados:
249
309
 
250
310
  ```typescript
251
- // Eventos confirmados (por defecto) — eventos que pasaron el middleware
311
+ // Eventos confirmados (por defecto): eventos que pasaron el middleware
252
312
  const off = store.onEvent("ui", "save", (event, getState, emit, phase) => {
253
313
  console.log("Save committed:", event.payload);
254
314
  });
255
315
 
256
- // Eventos no confirmados — eventos rechazados por el middleware
316
+ // Eventos no confirmados: eventos rechazados por el middleware
257
317
  store.onEvent(
258
318
  "ui",
259
319
  "delete",
@@ -263,7 +323,7 @@ store.onEvent(
263
323
  "uncommitted",
264
324
  );
265
325
 
266
- // Todos los eventos — tanto confirmados como no confirmados
326
+ // Todos los eventos: tanto confirmados como no confirmados
267
327
  store.onEvent(
268
328
  "ui",
269
329
  "action",
@@ -274,15 +334,34 @@ store.onEvent(
274
334
  );
275
335
  ```
276
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
+
277
356
  ---
278
357
 
279
- ## Los commits son atomicos entre slices
358
+ ## Los commits son atómicos entre slices
280
359
 
281
- Un evento que toca varias slices las escribe todas y despues notifica. Nadie observa un evento
282
- aplicado a medias: un suscriptor de una slice que lee `getState()` ve todas las demas slices del
360
+ Un evento que toca varias slices las escribe todas y después notifica. Nadie observa un evento
361
+ aplicado a medias: un suscriptor de una slice que lee `getState()` ve todas las demás slices del
283
362
  mismo evento ya aplicadas.
284
363
 
285
- Esto importa sobre todo donde un cambio se usa como senal para volver a leer, que es lo que hacen
364
+ Esto importa sobre todo donde un cambio se usa como señal para volver a leer, que es lo que hacen
286
365
  los hooks de React.
287
366
 
288
367
  ---
@@ -290,7 +369,7 @@ los hooks de React.
290
369
  ## Rechazar una escritura
291
370
 
292
371
  Un reducer devuelve `Rejected(reason)` en lugar de estado para declinar. **Se rechaza el evento
293
- completo**: ninguna slice escribe, no se emite ninguna notificacion de cambio, y quien llamo sabe
372
+ completo**: ninguna slice escribe, no se emite ninguna notificación de cambio, y quien llamo sabe
294
373
  por que.
295
374
 
296
375
  ```typescript
@@ -313,36 +392,36 @@ const store = createStore({
313
392
 
314
393
  const result = await store.emit("plan", "patch", { steps, expectedVersion: 1 });
315
394
 
316
- result.committed; // true — el middleware lo permitio
317
- result.written; // false — pero no se escribio nada
395
+ result.committed; // true: el middleware lo permitio
396
+ result.written; // false: no se escribio nada
318
397
  result.rejected?.reason;
319
398
  ```
320
399
 
321
400
  Rechazar **no** es lo mismo que devolver el estado sin cambios, que es indistinguible de "este
322
- evento no me concierne". Tampoco es lo mismo que lanzar: un reducer que lanza tiene un bug, asi
323
- que su slice queda aislada y las demas si escriben, mientras que un reducer que rechaza ha tomado
324
- una decision a la que cede el evento entero.
401
+ evento no me concierne". Tampoco es lo mismo que lanzar: un reducer que lanza tiene un bug, así
402
+ que su slice queda aislada y las demás sí escriben, mientras que un reducer que rechaza ha tomado
403
+ una decisión a la que cede el evento entero.
325
404
 
326
405
  `emit` resuelve a un `EmitResult` cuando terminan los efectos:
327
406
 
328
407
  | | |
329
408
  |---|---|
330
- | `committed` | el middleware no lo veto |
331
- | `written` | un reducer cambio el estado de verdad |
332
- | `rejected` | presente cuando un reducer rechazo, con su `reason` |
409
+ | `committed` | el middleware no lo vetó |
410
+ | `written` | un reducer cambió el estado de verdad |
411
+ | `rejected` | presente cuando un reducer rechazó, con su `reason` |
333
412
 
334
413
  La fase `written` de `onEvent` reporta lo mismo a los suscriptores. `committed` sigue
335
414
  significando **no vetado** y no se estrecho a proposito: se dispara para todo evento que el
336
- middleware permite, incluidos todos los eventos de un store sin reducers — la forma que toma un
337
- bus de notificaciones o de analitica.
415
+ middleware permite, incluidos todos los eventos de un store sin reducers, la forma que toma un
416
+ bus de notificaciones o de analítica.
338
417
 
339
418
  ---
340
419
 
341
- ## Peticion y respuesta — `store.call()`
420
+ ## Petición y respuesta: `store.call()`
342
421
 
343
- Todo consumidor de un bus de eventos acaba escribiendo peticion/respuesta a mano: generar un id,
344
- suscribirse, emparejar, expirar, desuscribirse. Son unas ochenta lineas y siempre traen los
345
- mismos dos bugs: la suscripcion sobrevive a la llamada, y `Quien Responde` que olvida devolver el
422
+ Todo consumidor de un bus de eventos acaba escribiendo petición/respuesta a mano: generar un id,
423
+ suscribirse, emparejar, expirar, desuscribirse. Son unas ochenta líneas y siempre traen los
424
+ mismos dos bugs: la suscripción sobrevive a la llamada, y `Quien Responde` que olvida devolver el
346
425
  id produce un timeout sin nada a lo que apuntar.
347
426
 
348
427
  ```typescript
@@ -364,7 +443,7 @@ store.registerEffect({
364
443
 
365
444
  ### Una llamada resuelve al evento, no al payload
366
445
 
367
- Porque muchas veces quien llama no sabe *cual* respuesta va a recibir. `reply` nombra los tipos
446
+ Porque muchas veces quien llama no sabe *cuál* respuesta va a recibir. `reply` nombra los tipos
368
447
  **terminales**, y el evento trae el discriminante:
369
448
 
370
449
  ```typescript
@@ -388,45 +467,45 @@ for await (const step of call) await render(step.payload);
388
467
  const { payload } = await call;
389
468
  ```
390
469
 
391
- La contrapresion es real, no un buffer con limite. `emit` resuelve solo cuando terminan sus
392
- efectos, y el colector es un efecto que no retorna hasta que el consumidor tomo el elemento — asi
470
+ La contrapresión es real, no un buffer con límite. `emit` resuelve solo cuando terminan sus
471
+ efectos, y el colector es un efecto que no retorna hasta que el consumidor tomo el elemento, así
393
472
  que un `Quien Responde` que escribe `await emit("job", "tick", chunk)` **va al ritmo del lector**.
394
473
 
395
- La contrapresion entra en juego **cuando empiezas a iterar**. Una llamada que solo se espera con
396
- `await` nunca extrae nada, asi que bloquear a su productor causaria un interbloqueo de la propia
397
- llamada: el progreso que nadie lee impediria que se enviara el evento terminal. Por eso el
398
- progreso no iterado se almacena hasta `highWaterMark` y despues se cuenta en `call.dropped`.
474
+ La contrapresión entra en juego **cuando empiezas a iterar**. Una llamada que solo se espera con
475
+ `await` nunca extrae nada, así que bloquear a su productor causaría un interbloqueo de la propia
476
+ llamada: el progreso que nadie lee impediría que se enviara el evento terminal. Por eso el
477
+ progreso no iterado se almacena hasta `highWaterMark` y después se cuenta en `call.dropped`.
399
478
 
400
479
  ### Retroceso
401
480
 
402
481
  | | |
403
482
  |---|---|
404
483
  | `timeoutMs` | **Inactividad**, no total: todo evento correlacionado lo reinicia, incluido el progreso. Por defecto 30s. |
405
- | `signal` | Un `AbortSignal`, para una fecha limite real o una accion cancelada. |
484
+ | `signal` | Un `AbortSignal`, para una fecha límite real o una acción cancelada. |
406
485
  | `call.cancel(reason)` | Deja de escuchar y liquida la llamada. |
407
486
 
408
- Termine como termine, la suscripcion se elimina y se libera cualquier productor detenido por la
409
- contrapresion. Un `Quien Responde` atascado es peor que el buffer sin limite que esto reemplazo.
487
+ Termine como termine, la suscripción se elimina y se libera cualquier productor detenido por la
488
+ contrapresión. Un `Quien Responde` atascado es peor que el buffer sin límite que esto reemplazo.
410
489
 
411
490
  ---
412
491
 
413
492
  ## Leer un valor al suscribirse
414
493
 
415
- `connect` empieza en "de ahora en adelante", asi que la primera lectura habia que repetirla en
416
- otro lado — la misma ruta en dos sitios, libres de divergir:
494
+ `connect` empieza en "de ahora en adelante", así que la primera lectura había que repetirla en
495
+ otro lado: la misma ruta en dos sitios, libres de divergir:
417
496
 
418
497
  ```typescript
419
498
  store.connect({ reducer: "todos", property: "items.0.title" }, render, { immediate: true });
420
499
  ```
421
500
 
422
- El primer cambio sintetico trae `oldValue: undefined` y **sin procedencia**, porque ningun evento
423
- lo causo. React no lo necesita: `useSyncExternalStore` ya lee una instantanea al montar.
501
+ El primer cambio sintético trae `oldValue: undefined` y **sin procedencia**, porque ningún evento
502
+ lo causó. React no lo necesita: `useSyncExternalStore` ya lee una instantánea al montar.
424
503
 
425
504
  ---
426
505
 
427
- ## De donde vino un cambio
506
+ ## De dónde vino un cambio
428
507
 
429
- Un `Change` nombra el evento que lo causo, asi que un suscriptor ya no tiene que duplicar la causa
508
+ Un `Change` nombra el evento que lo causó, así que un suscriptor ya no tiene que duplicar la causa
430
509
  dentro del estado:
431
510
 
432
511
  ```typescript
@@ -438,19 +517,41 @@ store.connect({ reducer: "orders", property: "status" }, (change) => {
438
517
  });
439
518
  ```
440
519
 
441
- La procedencia esta **ausente** cuando ningun evento causo el cambio — un salto de time-travel de
442
- DevTools, o la entrega `immediate` de arriba. La ausencia es la senal, en vez de un id inventado.
520
+ La procedencia está **ausente** cuando ningún evento causó el cambio: un salto de time-travel de
521
+ DevTools, o la entrega `immediate` de arriba. La ausencia es la señal, en vez de un id inventado.
443
522
 
444
523
  ---
445
524
 
446
- ## Proteccion contra cascadas (activada por defecto)
525
+ ## Deduplicación de Eventos (opt-in)
526
+
527
+ La deduplicación está **desactivada por defecto**. Yoltra nunca descarta en silencio eventos
528
+ idénticos legítimos y rápidos (doble-clics, `+1` repetidos). Actívala solo cuando de verdad quieras
529
+ coalescer:
530
+
531
+ ```typescript
532
+ // Por contenido: coalescer (channel, type, payload) identicos dentro de una ventana.
533
+ const store = createStore({
534
+ name: "App",
535
+ reducer: {
536
+ /* ... */
537
+ },
538
+ dedupWindowMs: 100, // default: 0 (desactivado)
539
+ });
540
+
541
+ // Por identidad: dedup por una clave explicita, p. ej. un doble-invoke de React Strict Mode en un efecto.
542
+ await store.emit("analytics", "pageView", { page }, { dedupKey: `pageView:${page}` });
543
+ ```
447
544
 
448
- Dos consumidores conectados entre si — un suscriptor que emite lo que su propio reducer atiende, o
449
- dos slices que atienden los eventos de la otra — producen una cadena de eventos sin final. La cola
450
- de reduccion se drena de forma **sincrona**, asi que eso no es un programa lento: es una pestana
545
+ ---
546
+
547
+ ## Protección contra cascadas (activada por defecto)
548
+
549
+ Dos consumidores conectados entre sí, ya sea un suscriptor que emite lo que su propio reducer atiende o
550
+ dos slices que atienden los eventos de la otra, producen una cadena de eventos sin final. La cola
551
+ de reducción se drena de forma **síncrona**, así que eso no es un programa lento: es una pestana
451
552
  congelada, o un core al 100%, sin error ni stack al que apuntar.
452
553
 
453
- Por eso cada evento lleva su posicion causal, y el store se niega a extender una cadena mas alla
554
+ Por eso cada evento lleva su posición causal, y el store se niega a extender una cadena más allá
454
555
  de un tope:
455
556
 
456
557
  ```typescript
@@ -468,58 +569,95 @@ const store = createStore({
468
569
  });
469
570
  ```
470
571
 
471
- Un evento emitido mientras se atiende otro esta un nivel mas abajo que su causa, y lleva
472
- `parentId` y `depth` para que el ciclo sea legible despues. Ambos campos estan **ausentes** en un
473
- evento raiz, asi que los eventos que emite tu aplicacion siguen siendo identicos byte a byte.
572
+ Un evento emitido mientras se atiende otro está un nivel más abajo que su causa, y lleva
573
+ `parentId` y `depth` para que el ciclo sea legible después. Ambos campos están **ausentes** en un
574
+ evento raíz, así que los eventos que emite tu aplicación siguen siendo idénticos byte a byte.
474
575
 
475
576
  Superar el tope no lanza. El emit ofensor se rechaza, lo ya confirmado se mantiene, y `onCascade`
476
- (mas un error en consola) lo nombra — lanzar apareceria en el suscriptor o efecto que casualmente
577
+ (más un error en consola) lo nombra. Lanzar aparecería en el suscriptor o efecto que casualmente
477
578
  estuviera emitiendo, que es justo el fallo inatribuible que el tope existe para evitar.
478
579
 
479
580
  **Una rafaga ancha no es una cascada.** Un evento cuyo suscriptor emite quinientos hermanos es una
480
- forma legitima; la profundidad es lo que la distingue de un ciclo, y un bucle normal de
581
+ forma legítima; la profundidad es lo que la distingue de un ciclo, y un bucle normal de
481
582
  `store.emit` nunca acumula profundidad. `maxTransitionsPerDrain` acota el *ancho* y por eso viene
482
583
  desactivado.
483
584
 
484
585
  ---
485
586
 
486
- ## Deduplicacion de Eventos (opt-in)
587
+ ## Reducers Dinámicos
487
588
 
488
- La deduplicacion esta **desactivada por defecto** — yoltra nunca descarta en silencio eventos
489
- identicos legitimos y rapidos (doble-clics, `+1` repetidos). Actívala solo cuando de verdad quieras
490
- coalescer:
589
+ Agrega o elimina slices de reducer en tiempo de ejecución:
491
590
 
492
591
  ```typescript
493
- // Por contenido: coalescer (channel, type, payload) identicos dentro de una ventana.
494
- const store = createStore({
495
- name: "App",
496
- reducer: {
497
- /* ... */
498
- },
499
- dedupWindowMs: 100, // default: 0 (desactivado)
592
+ const dispose = store.registerReducer("filters", {
593
+ state: { q: "" },
594
+ when: { keys: eventKeys<AppEM>()([["ui", "setQuery"]]) },
595
+ reducer: (state, event) => (event.type === "setQuery" ? { q: event.payload } : state),
500
596
  });
501
597
 
502
- // Por identidad: dedup por una clave explicita — p. ej. un doble-invoke de React Strict Mode en un efecto.
503
- await store.emit("analytics", "pageView", { page }, { dedupKey: `pageView:${page}` });
598
+ // Despues: remover el slice y su estado
599
+ dispose();
504
600
  ```
505
601
 
506
- ---
507
-
508
- ## Reducers Dinamicos
602
+ ### Decorar un store, con sus tipos
509
603
 
510
- Agrega o elimina slices de reducer en tiempo de ejecucion:
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**:
511
607
 
512
608
  ```typescript
513
- const dispose = store.registerReducer("filters", {
514
- state: { q: "" },
515
- when: { keys: eventKeys<AppEM>()([["ui", "setQuery"]]) },
516
- reducer: (state, event) => (event.type === "setQuery" ? { q: event.payload } : state),
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),
517
615
  });
518
616
 
519
- // Despues: remover el slice y su estado
520
- dispose();
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
521
621
  ```
522
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
+
523
661
  ---
524
662
 
525
663
  ## Hot Module Replacement
@@ -548,26 +686,52 @@ if (import.meta.hot) {
548
686
  }
549
687
  ```
550
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
+
551
715
  ---
552
716
 
553
- ## Mejores Practicas
717
+ ## Mejores Prácticas
554
718
 
555
- ### El estado es sincrono; haz `await` solo por los efectos
719
+ ### El estado es síncrono; haz `await` solo por los efectos
556
720
 
557
- La fase de reduccion es sincrona, asi que el estado refleja tu evento en el instante en que `emit()`
558
- retorna — sin `await` para leerlo. Haz `await` de `emit()` cuando ademas quieras que los efectos de
721
+ La fase de reducción es síncrona, así que el estado refleja tu evento en el instante en que `emit()`
722
+ retorna, sin `await` para leerlo. Haz `await` de `emit()` cuando además quieras que los efectos de
559
723
  _ese evento_ hayan terminado:
560
724
 
561
725
  ```typescript
562
726
  emit("todo", "add", todo);
563
- store.getState(); // Ya refleja la nueva tarea — sin await
727
+ store.getState(); // Ya refleja la nueva tarea. Sin await
564
728
 
565
729
  await emit("todo", "save", todo); // se resuelve cuando terminan los efectos de save
566
730
  ```
567
731
 
568
- ### Mantener los reducers rapidos
732
+ ### Mantener los reducers rápidos
569
733
 
570
- Los reducers son sincronos y corren en el mismo tick que `emit()`. Mueve el trabajo costoso a los
734
+ Los reducers son síncronos y corren en el mismo tick que `emit()`. Mueve el trabajo costoso a los
571
735
  efectos:
572
736
 
573
737
  ```typescript
@@ -600,96 +764,227 @@ store.registerEffect({
600
764
 
601
765
  ## Resumen de API
602
766
 
603
- ### Creacion del Store
767
+ ### Creación del Store
604
768
 
605
- | API | Descripcion |
769
+ | API | Descripción |
606
770
  | ----------------------------------------------- | ----------------------------------------------------- |
607
771
  | `createStore(spec)` | Crear un store (tipos inferidos de los reducers) |
608
- | `createStore<S, EM>(spec)` | Crear un store con tipos de estado/eventos explicitos |
772
+ | `createStore<S, EM>(spec)` | Crear un store con tipos de estado/eventos explícitos |
609
773
  | `store.emit(channel, type, payload)` | Emitir un evento (retorna una promesa) |
610
774
  | `store.getState()` | Obtener snapshot del estado actual (solo lectura) |
611
- | `store.subscribe(listener)` | Suscripcion gruesa (cualquier cambio de estado) |
612
- | `store.connect(spec, handler)` | Suscripcion de grano fino por ruta con wildcards |
613
- | `store.onEvent(channel, type, handler, phase?)` | Suscripcion a eventos (committed/uncommitted/all) |
775
+ | `store.subscribe(listener)` | Suscripción gruesa (cualquier cambio de estado) |
776
+ | `store.connect(spec, handler)` | Suscripción de grano fino por ruta con wildcards |
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 |
614
779
  | `store.onEffect(channel, type, handler)` | Shorthand de efecto para un solo evento |
615
780
  | `store.dispose()` | Limpiar timers y recursos |
616
781
 
617
- ### Registro Dinamico
782
+ ### Registro Dinámico
618
783
 
619
- | API | Descripcion |
784
+ | API | Descripción |
620
785
  | ----------------------------------- | ----------------------------------------- |
621
- | `store.registerReducer(name, spec)` | Agregar un slice en tiempo de ejecucion |
622
- | `store.registerMiddleware(fn)` | Agregar middleware en tiempo de ejecucion |
623
- | `store.registerEffect(spec)` | Agregar un efecto en tiempo de ejecucion |
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 |
790
+ | `store.registerReducer(name, spec)` | Agregar un slice en tiempo de ejecución |
791
+ | `store.registerMiddleware(fn)` | Agregar middleware en tiempo de ejecución |
792
+ | `store.registerEffect(spec)` | Agregar un efecto en tiempo de ejecución |
624
793
 
625
794
  ### HMR
626
795
 
627
- | API | Descripcion |
796
+ | API | Descripción |
628
797
  | --------------------------------------- | ------------------------------------------- |
629
- | `store.replaceReducers(reducers, opts)` | Reemplazar todos los reducers |
630
- | `store.replaceMiddleware(middleware)` | Reemplazar todos los middleware |
631
- | `store.replaceEffects(effects)` | Reemplazar todos los efectos |
632
- | `store.hotReplace(partial)` | Reemplazar cualquier subconjunto de una vez |
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` |
633
802
 
634
803
  ### Helpers
635
804
 
636
- | API | Descripcion |
805
+ | API | Descripción |
637
806
  | ------------------------ | ---------------------------------------------------------------- |
638
807
  | `eventKeys<EM>()([...])` | Arrays de claves de evento con seguridad de tipos sin `as const` |
639
808
 
640
809
  ---
641
810
 
811
+ ## Guardar y restaurar estado
812
+
813
+ Dos funciones, porque las dos mitades ocurren en lados opuestos de la existencia del store.
814
+ `hydrate` produce el *estado inicial de las slices*, así que el store nace con él:
815
+
816
+ ```ts
817
+ import { createStore, createWebStorageAdapter, hydrate, persist, withHydration } from '@yoltra/core';
818
+
819
+ const adapter = createWebStorageAdapter(localStorage);
820
+ const hydration = await hydrate({ key: 'app', adapter, version: 3 });
821
+
822
+ const store = createStore({
823
+ name: 'App',
824
+ reducer: withHydration({ todos: todosSpec, ui: uiSpec }, hydration),
825
+ });
826
+
827
+ const stop = persist(store, { key: 'app', adapter, version: 3, slices: ['todos'] });
828
+ ```
829
+
830
+ Restaurar *después* de construir es la alternativa obvia y la equivocada: aplicar una
831
+ instantánea a un store vivo emite un cambio en todas las rutas, lo que en el arranque es un
832
+ parpadeo, una ráfaga de entradas de instrumentación que describen cambios que nadie hizo, y
833
+ efectos observando una transición que nunca ocurrió.
834
+
835
+ **Nada lanza en el arranque.** Un payload ausente, ilegible o no migrable recae en los valores
836
+ por defecto que declaraste y se reporta por `onError`. Un store que no arranca porque el
837
+ almacenamiento guarda JSON obsoleto es peor que uno que arranca de cero, y un disco lleno no
838
+ debería tumbar una página, así que los fallos de escritura se reportan igual en vez de lanzarse.
839
+
840
+ **Las versiones que no coinciden se rechazan, no se asumen.** Los reducers cambian, y una
841
+ instantánea escrita contra una forma anterior puede no ser estado válido para este build en
842
+ absoluto. Aporta `migrate` para actualizarla, o se descarta.
843
+
844
+ Las escrituras las dirige la instrumentación, así que un cambio confinado a una slice que no
845
+ estás persistiendo no cuesta nada, y una ráfaga se agrupa en una sola escritura. `Map`, `Set`,
846
+ `Date`, `BigInt`, `undefined` y las referencias circulares sobreviven al viaje de ida y vuelta:
847
+ `JSON.stringify` no falla con eso, los destruye en silencio.
848
+
849
+ Para un render en servidor, `dehydrate(store, { version })` produce el payload y
850
+ `hydrate({ source, version })` lo consume.
851
+
852
+ ---
853
+
854
+ ## Listas que se reordenan
855
+
856
+ La notificación por ruta es posicional para los arrays. `items.0.title` nombra un *hueco*, no
857
+ una cosa, así que `unshift`, `splice(0, 1)` y `sort` mueven casi todos los elementos a un hueco
858
+ distinto, y el diff reporta correctamente que casi todas las hojas cambiaron. Insertar una fila
859
+ al principio de mil despierta a mil suscriptores.
860
+
861
+ Eso es honesto en vez de ruidoso: con rutas posicionales el valor de casi cada índice cambió de
862
+ verdad. El remedio es la forma del estado, no un diff que se calle.
863
+
864
+ ```ts
865
+ import { createEntityAdapter } from '@yoltra/core';
866
+
867
+ const todos = createEntityAdapter<Todo>();
868
+
869
+ // state is { ids: [...], entities: { abc: {...} } }
870
+ todos.updateOne(state, { id: 'abc', changes: { done: true } });
871
+
872
+ // and the adapter hands out the paths, so they are never typed by hand
873
+ todos.pathTo('abc', 'title'); // "entities.abc.title"
874
+ todos.idsPath; // "ids"
875
+ ```
876
+
877
+ `entities.abc.title` sobrevive a insertar, eliminar y reordenar. Un contenedor de lista se
878
+ suscribe a `ids` y reordena sus hijos; las filas se suscriben a su propia entidad y siguen
879
+ dormidas durante un `sort`.
880
+
881
+ `ids` sigue siendo un array, así que un reordenamiento todavía reporta `ids.0`, `ids.1` y así
882
+ sucesivamente. Ese costo queda confinado, no eliminado. Lo que ganas es un costo proporcional a
883
+ lo que realmente cambió.
884
+
885
+ Para una lista pequeña que solo crece por el final, `items.0.title` está bien y es más simple.
886
+ El adapter es para colecciones que se reordenan, o que son lo bastante grandes como para que la
887
+ diferencia se note.
888
+
889
+ ### Lo que cuesta, medido
890
+
891
+ Con 1000 filas, hacer el diff después de una inserción al principio cuesta 1200 µs para un array
892
+ y 371 µs normalizado, y el array reporta alrededor de mil rutas cambiadas frente a dos. Ese es
893
+ el caso para el que existe el adapter.
894
+
895
+ Una actualización de un solo campo va al revés: 20 µs para el array frente a 470 µs normalizado.
896
+ `detectChangedProps` indexa un array pero enumera las claves de un objeto, construyendo dos
897
+ arrays de claves y un `Set` por comparación, así que un mapa de entidades ancho es más caro de
898
+ recorrer aunque casi nada dentro se haya movido. Los números están en `benchmarks/`, y cerrar
899
+ esa brecha es trabajo con seguimiento, no una propiedad de normalizar como tal.
900
+
901
+ Así que: normaliza las colecciones que se reordenan o que rotan mucho. Una colección grande a la
902
+ que solo se le editan campos individuales está mejor como array hoy.
903
+
904
+ ---
905
+
642
906
  ## Rendimiento
643
907
 
644
- | Metrica | Valor |
645
- | --------------------- | ------------------------------------------- |
646
- | **Tamano del bundle** | 9.2 KB para el store (minificado + gzipped) |
647
- | **Tree-shakeable** | Si (modulos ES) |
648
- | **Dependencias** | Cero |
649
- | **TypeScript** | Definiciones de tipos completas incluidas |
908
+ | Métrica | Valor |
909
+ | --------------------- | ----------------------------------------- |
910
+ | **Tamaño del bundle** | Medido en cada build, ver la tabla abajo |
911
+ | **Tree-shakeable** | Sí (módulos ES) |
912
+ | **Dependencias** | Cero |
913
+ | **TypeScript** | Definiciones de tipos completas incluidas |
914
+
915
+ El tamaño del bundle se verifica, no se afirma: `rush size` empaqueta el paquete como lo haría
916
+ un consumidor (sacudido, minificado, comprimido con gzip) y falla cuando excede el
917
+ presupuesto declarado en `package.json`. La tabla de abajo la escribe esa misma verificación,
918
+ así que no puede desviarse de lo que se midió; editarla a mano hace fallar el CI.
919
+
920
+ La cifra que importa es lo que importas, no lo que el paquete exporta:
921
+
922
+ <!-- size-table:start -->
923
+ | Import | Tamaño | Presupuesto |
924
+ | --- | --- | --- |
925
+ | `{ createStore }` | 11.5 KB | 14 KB |
926
+ | `{ createStore, hydrate, persist }` | 12.8 KB | 16 KB |
927
+ | todo | 14.2 KB | 18 KB |
928
+ <!-- size-table:end -->
929
+
930
+ Estas son cifras de **producción**: lo que públicas una vez que tu empaquetador define
931
+ `NODE_ENV=production` y las guardas exclusivas de desarrollo desaparecen. La columna de
932
+ presupuesto es el techo que `rush size` impone, y se verifica contra un build de desarrollo,
933
+ que es el mayor de los dos: el código exclusivo de desarrollo no puede crecer sin que nadie lo
934
+ note solo porque nunca llega a un usuario. Por eso el margen que se infiere aquí es
935
+ deliberadamente conservador.
936
+
937
+ La **distancia entre filas** es la afirmación de tree-shaking, y es lo que hay que vigilar: la
938
+ persistencia añade 1.5 KB a quienes la importan y nada a los demás, y el barrel completo está
939
+ 2.9 KB por encima del store. La última fila es un detector de crecimiento; `import * as all` no
940
+ es algo que nadie escriba.
650
941
 
651
942
  ---
652
943
 
653
- ## Documentacion
944
+ ## Documentación
654
945
 
655
- - **[README raiz de yoltra](../../README.md)** --
656
- Descripcion general y configuracion rapida
657
- - **[@yoltra/react](../react/README.md)** --
946
+ - **[README raíz de yoltra](../../README.md)**:
947
+ Descripción general y configuración rápida
948
+ - **[@yoltra/react](../react/README.md)**:
658
949
  Hooks de React y Suspense
659
- - **[Guia de Inicio Rapido](https://github.com/yoltra/yoltra/blob/main/docs/en/QUICK_START_GUIDE.md)**
660
- -- Cinco pasos hacia una app funcional
661
- - **[Arquitectura de Cola de Eventos](https://github.com/yoltra/yoltra/blob/main/docs/en/design/event-queue-architecture.md)**
662
- -- Inmersion tecnica profunda
663
- - **[Comparacion de Bibliotecas](https://github.com/yoltra/yoltra/blob/main/docs/en/design/state-management-library-comparison.md)**
664
- -- Comparacion arquitectonica
950
+ - **[Guia de Inicio Rápido](https://github.com/yoltra/yoltra/blob/main/docs/en/QUICK_START_GUIDE.md)**:
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
956
+ - **[Arquitectura de Cola de Eventos](https://github.com/yoltra/yoltra/blob/main/docs/en/design/event-queue-architecture.md)**:
957
+ Inmersión técnica profunda
958
+ - **[Comparación de Bibliotecas](https://github.com/yoltra/yoltra/blob/main/docs/en/design/state-management-library-comparison.md)**:
959
+ Comparación arquitectónica
665
960
 
666
961
  ---
667
962
 
668
963
  ## Ejemplos
669
964
 
670
- - **[App de Tareas](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-react)** --
965
+ - **[App de Tareas](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-react)**:
671
966
  CRUD completo con perfilado de rendimiento · [▶ Abrir la demo en vivo](https://yoltra.dev/es/demos/in-react)
672
- - **[Logo Cinetico](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-kinetic-logo)**
673
- -- 3000 círculos con simulación física. · [▶ Abrir la demo en vivo](https://yoltra.dev/es/demos/kinetic-logo)
674
- - **[Integracion con Next.js](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-nextjs)**
675
- -- Pages Router, estado de cliente + cambio de tema · [▶ Abrir la demo en vivo](https://yoltra.dev/es/demos/in-nextjs)
967
+ - **[Logo Cinético](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-kinetic-logo)**:
968
+ 3000 círculos con simulación física. · [▶ Abrir la demo en vivo](https://yoltra.dev/es/demos/kinetic-logo)
969
+ - **[Integración con Next.js](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-nextjs)**:
970
+ Pages Router, estado de cliente + cambio de tema · [▶ Abrir la demo en vivo](https://yoltra.dev/es/demos/in-nextjs)
676
971
 
677
972
  ---
678
973
 
679
974
  ## Contribuir
680
975
 
681
- - [Raiz del Monorepo](../../README.md)
682
- - [Guia de Contribucion](https://github.com/yoltra/yoltra/blob/main/CONTRIBUTING.md)
976
+ - [Raíz del Monorepo](../../README.md)
977
+ - [Guia de Contribución](https://github.com/yoltra/yoltra/blob/main/CONTRIBUTING.md)
683
978
 
684
979
  ---
685
980
 
686
981
  ## Estado
687
982
 
688
- **Release Candidate** -- Las APIs son estables, usadas en produccion, cambios menores posibles
983
+ **Release Candidate**. Las APIs son estables, usadas en producción, cambios menores posibles
689
984
  antes de v1.0.0.
690
985
 
691
986
  ---
692
987
 
693
988
  ## Licencia
694
989
 
695
- **MIT** -- Libre para usar en proyectos comerciales y de codigo abierto.
990
+ **MIT**. Libre para usar en proyectos comerciales y de código abierto.