@yoltra/core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Yoltra β€” Copyright (c) 2026 Manu Ramirez <@pixerael>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.es.md ADDED
@@ -0,0 +1,473 @@
1
+ ![yoltra logo](../../assets/yoltra-logo.png)
2
+
3
+ # @yoltra/core
4
+
5
+ > πŸ‘‰ πŸ‡²πŸ‡½ VersiΓ³n en EspaΓ±ol&nbsp; |
6
+ > &nbsp;[ πŸ‡ΊπŸ‡Έ English Version](https://github.com/yoltra/yoltra/blob/main/packages/core/README.md)&nbsp;
7
+
8
+ ![npm downloads](https://badgen.net/npm/dm/@yoltra/core)
9
+ ![License](https://badgen.net/npm/license/@yoltra/core)
10
+
11
+ **Contenedor de estado orientado a eventos, agnostico de framework, con suscripciones de grano
12
+ fino por ruta.**
13
+
14
+ `@yoltra/core` es la base de [yoltra](https://github.com/yoltra/yoltra/blob/main/README.md).
15
+ Proporciona el store, el pipeline de eventos, middleware, efectos y el sistema de suscripciones
16
+ `connect()`. Cero dependencias de framework.
17
+
18
+ ---
19
+
20
+ ## Instalacion
21
+
22
+ ```bash
23
+ npm install @yoltra/core
24
+ ```
25
+
26
+ ---
27
+
28
+ ## El Pipeline de Eventos
29
+
30
+ Cada llamada a `emit()` fluye a traves de un pipeline determinista:
31
+
32
+ ```
33
+ emit(channel, type, payload)
34
+ β”‚
35
+ β”œβ”€ 1. Dedup ─── Omitir si la huella es identica dentro de la ventana de tiempo
36
+ β”‚
37
+ β”œβ”€ 2. Middleware ─── Hooks pre-reducer (pueden rechazar β†’ evento "no confirmado")
38
+ β”‚
39
+ β”œβ”€ 3. Reducers ─── Actualizaciones de estado sincronas, deteccion de cambios de grano fino por ruta
40
+ β”‚
41
+ β”œβ”€ 4. Suscriptores de eventos ─── Notificaciones de eventos confirmados/no confirmados
42
+ β”‚
43
+ β”œβ”€ 5. Efectos ─── Efectos secundarios async (post-reducer, indexados para busqueda O(1))
44
+ β”‚
45
+ └─ 6. Suscriptores gruesos ─── Listeners externos del store (useSyncExternalStore, etc.)
46
+ ```
47
+
48
+ Cada etapa es interceptable. El middleware puede cancelar eventos, creando eventos "no
49
+ confirmados" a los que la UI aun puede reaccionar. Los efectos se ejecutan despues de los
50
+ reducers y ven el estado final.
51
+
52
+ ---
53
+
54
+ ## Conceptos Fundamentales
55
+
56
+ ### Eventos basados en canales
57
+
58
+ Los eventos son tuplas `(channel, type, payload)`. Los canales proporcionan namespacing natural
59
+ que escala en bases de codigo grandes:
60
+
61
+ ```typescript
62
+ await store.emit("auth", "login", credentials);
63
+ await store.emit("analytics", "track", { event: "page_view" });
64
+ await store.emit("ui", "toast", { message: "Saved!" });
65
+ ```
66
+
67
+ ### Suscripciones de grano fino via `connect()`
68
+
69
+ Suscribete a rutas de estado exactas usando notacion de puntos. Soporta wildcards `*` (un
70
+ segmento) y `**` (cero o mas segmentos):
71
+
72
+ ```typescript
73
+ // Ruta exacta β€” se dispara cuando items[0].title cambia
74
+ store.connect({ reducer: "todos", property: "items.0.title" }, (change) =>
75
+ console.log("title:", change.oldValue, "β†’", change.newValue),
76
+ );
77
+
78
+ // Wildcard de un segmento β€” se dispara cuando el titulo de CUALQUIER item cambia
79
+ store.connect({ reducer: "todos", property: "items.*.title" }, (change) =>
80
+ console.log("some title changed at", change.path),
81
+ );
82
+
83
+ // Wildcard profundo β€” se dispara cuando algo bajo items cambia
84
+ store.connect({ reducer: "todos", property: "items.**" }, (change) =>
85
+ console.log("items tree changed at", change.path),
86
+ );
87
+ ```
88
+
89
+ ### Inmutabilidad
90
+
91
+ El estado se congela profundamente antes de confirmarse. Las mutaciones lanzan error en modo
92
+ estricto:
93
+
94
+ ```typescript
95
+ const state = store.getState();
96
+ state.counter.value = 999; // TypeError: Cannot assign to read-only property
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Targeting de Eventos con Matchers `When`
102
+
103
+ Los reducers, efectos y middleware usan un matcher `When` unificado para declarar a cuales
104
+ eventos responden:
105
+
106
+ ```typescript
107
+ import { createStore, eventKeys } from "@yoltra/core";
108
+
109
+ type AppEM = {
110
+ ui: { increment: number; decrement: number; reset: void };
111
+ admin: { setCounter: number };
112
+ system: { init: void; shutdown: void };
113
+ };
114
+
115
+ // Coincidir con claves de evento especificas (recomendado β€” preserva la correlacion de tipos)
116
+ const counterReducer = {
117
+ state: { value: 0 },
118
+ when: {
119
+ keys: eventKeys<AppEM>()([
120
+ ["ui", "increment"],
121
+ ["ui", "decrement"],
122
+ ]),
123
+ },
124
+ reducer: (state, event) => {
125
+ if (event.type === "increment") return { value: state.value + event.payload };
126
+ if (event.type === "decrement") return { value: state.value - event.payload };
127
+ return state;
128
+ },
129
+ };
130
+
131
+ // Coincidir con todos los eventos de un canal
132
+ const uiLogger = {
133
+ when: { channel: "ui" },
134
+ effect: (event) => console.log("UI event:", event.type),
135
+ };
136
+
137
+ // Coincidir con eventos de multiples canales
138
+ const auditTrail = {
139
+ when: { channels: ["ui", "admin"] },
140
+ effect: (event) => logToAuditTrail(event),
141
+ };
142
+
143
+ // Coincidir con TODOS los eventos
144
+ const globalLogger = {
145
+ when: { any: true },
146
+ middleware: (state, event) => {
147
+ console.log(`[${event.channel}] ${event.type}`);
148
+ return true;
149
+ },
150
+ };
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Middleware
156
+
157
+ El middleware se ejecuta **antes** de los reducers y puede cancelar la propagacion de eventos.
158
+ Soporta tanto funciones directas (legacy) como objetos `MiddlewareSpec` con targeting:
159
+
160
+ ```typescript
161
+ import type { MiddlewareSpec } from "@yoltra/core";
162
+
163
+ // Middleware con target β€” solo se ejecuta para eventos del canal admin
164
+ const adminGuard: MiddlewareSpec<AppState, AppEM> = {
165
+ when: { channel: "admin" },
166
+ middleware: (state, event) => {
167
+ if (!state.auth.isAdmin) return false; // Rechazar β†’ crea evento "no confirmado"
168
+ return true;
169
+ },
170
+ meta: { type: "middleware", name: "adminGuard" },
171
+ };
172
+
173
+ // Middleware global β€” se ejecuta para todos los eventos
174
+ const logger = async (state, event, emit) => {
175
+ console.log("Event:", event.channel, event.type);
176
+ return true;
177
+ };
178
+
179
+ const store = createStore({
180
+ name: "App",
181
+ reducer: {
182
+ /* ... */
183
+ },
184
+ middleware: [adminGuard, logger],
185
+ });
186
+ ```
187
+
188
+ ### Middleware dinamico
189
+
190
+ ```typescript
191
+ const off = store.registerMiddleware(async (state, event) => {
192
+ return event.type !== "forbidden";
193
+ });
194
+ off(); // Remover despues
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Efectos
200
+
201
+ Los efectos se ejecutan **despues** de los reducers y ven el estado final. Estan indexados por
202
+ evento para busqueda O(1):
203
+
204
+ ```typescript
205
+ // Via spec del store
206
+ const store = createStore({
207
+ name: "App",
208
+ reducer: {
209
+ /* ... */
210
+ },
211
+ effects: [
212
+ {
213
+ when: {
214
+ keys: eventKeys<AppEM>()([
215
+ ["todos", "add"],
216
+ ["todos", "delete"],
217
+ ]),
218
+ },
219
+ effect: async (event, getState, emit) => {
220
+ await saveToServer(getState());
221
+ },
222
+ meta: { type: "effect", name: "syncToServer" },
223
+ },
224
+ ],
225
+ });
226
+
227
+ // Registro dinamico
228
+ const off = store.registerEffect({
229
+ when: { channel: "analytics" },
230
+ effect: async (event) => sendToAnalytics(event),
231
+ });
232
+
233
+ // Helper de conveniencia para un solo evento
234
+ const off2 = store.onEffect("ui", "save", async (payload, getState, emit) => {
235
+ await saveToCloud(payload);
236
+ });
237
+ ```
238
+
239
+ ---
240
+
241
+ ## Suscripciones a Eventos
242
+
243
+ Suscribete a eventos (no al estado) desde la capa de vista. Util para notificaciones,
244
+ animaciones y reaccionar a eventos rechazados:
245
+
246
+ ```typescript
247
+ // Eventos confirmados (por defecto) β€” eventos que pasaron el middleware
248
+ const off = store.onEvent("ui", "save", (event, getState, emit, phase) => {
249
+ console.log("Save committed:", event.payload);
250
+ });
251
+
252
+ // Eventos no confirmados β€” eventos rechazados por el middleware
253
+ store.onEvent(
254
+ "ui",
255
+ "delete",
256
+ (event, getState, emit, phase) => {
257
+ console.log("Delete was rejected");
258
+ },
259
+ "uncommitted",
260
+ );
261
+
262
+ // Todos los eventos β€” tanto confirmados como no confirmados
263
+ store.onEvent(
264
+ "ui",
265
+ "action",
266
+ (event, getState, emit, phase) => {
267
+ console.log(`Action ${phase}:`, event.type);
268
+ },
269
+ "all",
270
+ );
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Deduplicacion de Eventos
276
+
277
+ yoltra deduplica automaticamente eventos identicos dentro de una ventana de tiempo configurable.
278
+ Esto previene el doble procesamiento en React Strict Mode:
279
+
280
+ ```typescript
281
+ const store = createStore({
282
+ name: "App",
283
+ reducer: {
284
+ /* ... */
285
+ },
286
+ dedupWindowMs: 100, // default: 50ms dev, 100ms prod
287
+ });
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Reducers Dinamicos
293
+
294
+ Agrega o elimina slices de reducer en tiempo de ejecucion:
295
+
296
+ ```typescript
297
+ const dispose = store.registerReducer("filters", {
298
+ state: { q: "" },
299
+ when: { keys: eventKeys<AppEM>()([["ui", "setQuery"]]) },
300
+ reducer: (state, event) => (event.type === "setQuery" ? { q: event.payload } : state),
301
+ });
302
+
303
+ // Despues: remover el slice y su estado
304
+ dispose();
305
+ ```
306
+
307
+ ---
308
+
309
+ ## Hot Module Replacement
310
+
311
+ ```typescript
312
+ if (import.meta.hot) {
313
+ import.meta.hot.accept("./reducers", (mod) => {
314
+ store.replaceReducers(mod.reducers, { preserveState: true });
315
+ });
316
+
317
+ import.meta.hot.accept("./middleware", (mod) => {
318
+ store.replaceMiddleware(mod.middleware);
319
+ });
320
+
321
+ import.meta.hot.accept("./effects", (mod) => {
322
+ store.replaceEffects(mod.effects);
323
+ });
324
+
325
+ // O reemplazar todo de una vez
326
+ store.hotReplace({
327
+ reducer: newReducers,
328
+ middleware: newMiddleware,
329
+ effects: newEffects,
330
+ preserveState: true,
331
+ });
332
+ }
333
+ ```
334
+
335
+ ---
336
+
337
+ ## Mejores Practicas
338
+
339
+ ### Siempre hacer await de `emit()`
340
+
341
+ ```typescript
342
+ await emit("todo", "add", todo);
343
+ const state = store.getState(); // Garantiza que refleja la nueva tarea
344
+ ```
345
+
346
+ ### Mantener los reducers rapidos
347
+
348
+ Los reducers son sincronos y bloquean la cola de eventos. Mueve el trabajo costoso a los
349
+ efectos:
350
+
351
+ ```typescript
352
+ // Reducer: solo establecer un flag de carga
353
+ reducer: ((state, event) => ({ ...state, loading: true }),
354
+ // Efecto: hacer el trabajo pesado
355
+ store.onEffect("data", "compute", async (payload, getState, emit) => {
356
+ const result = await computeAsync();
357
+ await emit("data", "computeComplete", result);
358
+ }));
359
+ ```
360
+
361
+ ### Manejar errores de efectos
362
+
363
+ ```typescript
364
+ store.registerEffect({
365
+ when: { channel: "data" },
366
+ effect: async (event, getState, emit) => {
367
+ try {
368
+ const data = await fetch(url);
369
+ await emit("data", "loadSuccess", data);
370
+ } catch (error) {
371
+ await emit("data", "loadFailure", { error: error.message });
372
+ }
373
+ },
374
+ });
375
+ ```
376
+
377
+ ---
378
+
379
+ ## Resumen de API
380
+
381
+ ### Creacion del Store
382
+
383
+ | API | Descripcion |
384
+ | ----------------------------------------------- | ----------------------------------------------------- |
385
+ | `createStore(spec)` | Crear un store (tipos inferidos de los reducers) |
386
+ | `createStore<S, EM>(spec)` | Crear un store con tipos de estado/eventos explicitos |
387
+ | `store.emit(channel, type, payload)` | Emitir un evento (retorna una promesa) |
388
+ | `store.getState()` | Obtener snapshot del estado actual (solo lectura) |
389
+ | `store.subscribe(listener)` | Suscripcion gruesa (cualquier cambio de estado) |
390
+ | `store.connect(spec, handler)` | Suscripcion de grano fino por ruta con wildcards |
391
+ | `store.onEvent(channel, type, handler, phase?)` | Suscripcion a eventos (committed/uncommitted/all) |
392
+ | `store.onEffect(channel, type, handler)` | Shorthand de efecto para un solo evento |
393
+ | `store.dispose()` | Limpiar timers y recursos |
394
+
395
+ ### Registro Dinamico
396
+
397
+ | API | Descripcion |
398
+ | ----------------------------------- | ----------------------------------------- |
399
+ | `store.registerReducer(name, spec)` | Agregar un slice en tiempo de ejecucion |
400
+ | `store.registerMiddleware(fn)` | Agregar middleware en tiempo de ejecucion |
401
+ | `store.registerEffect(spec)` | Agregar un efecto en tiempo de ejecucion |
402
+
403
+ ### HMR
404
+
405
+ | API | Descripcion |
406
+ | --------------------------------------- | ------------------------------------------- |
407
+ | `store.replaceReducers(reducers, opts)` | Reemplazar todos los reducers |
408
+ | `store.replaceMiddleware(middleware)` | Reemplazar todos los middleware |
409
+ | `store.replaceEffects(effects)` | Reemplazar todos los efectos |
410
+ | `store.hotReplace(partial)` | Reemplazar cualquier subconjunto de una vez |
411
+
412
+ ### Helpers
413
+
414
+ | API | Descripcion |
415
+ | ------------------------ | ---------------------------------------------------------------- |
416
+ | `eventKeys<EM>()([...])` | Arrays de claves de evento con seguridad de tipos sin `as const` |
417
+
418
+ ---
419
+
420
+ ## Rendimiento
421
+
422
+ | Metrica | Valor |
423
+ | --------------------- | ----------------------------------------- |
424
+ | **Tamano del bundle** | ~8KB (minificado + gzipped) |
425
+ | **Tree-shakeable** | Si (modulos ES) |
426
+ | **Dependencias** | Cero |
427
+ | **TypeScript** | Definiciones de tipos completas incluidas |
428
+
429
+ ---
430
+
431
+ ## Documentacion
432
+
433
+ - **[README raiz de yoltra](https://github.com/yoltra/yoltra/blob/main/README.md)** --
434
+ Descripcion general y configuracion rapida
435
+ - **[@yoltra/react](https://github.com/yoltra/yoltra/blob/main/packages/react/README.md)** --
436
+ Hooks de React y Suspense
437
+ - **[Guia de Inicio Rapido](https://github.com/yoltra/yoltra/blob/main/docs/en/QUICK_START_GUIDE.md)**
438
+ -- Cinco pasos hacia una app funcional
439
+ - **[Arquitectura de Cola de Eventos](https://github.com/yoltra/yoltra/blob/main/docs/en/design/event-queue-architecture.md)**
440
+ -- Inmersion tecnica profunda
441
+ - **[Comparacion de Bibliotecas](https://github.com/yoltra/yoltra/blob/main/docs/en/design/state-management-library-comparison.md)**
442
+ -- Comparacion arquitectonica
443
+
444
+ ---
445
+
446
+ ## Ejemplos
447
+
448
+ - **[App de Tareas](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-react)** --
449
+ CRUD completo con perfilado de rendimiento
450
+ - **[Logo Cinetico](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-kinetic-logo)**
451
+ -- 3000 cΓ­rculos con simulaciΓ³n fΓ­sica.
452
+ - **[Integracion con Next.js](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-nextjs)**
453
+ -- SSR + App Router + cambio de tema
454
+
455
+ ---
456
+
457
+ ## Contribuir
458
+
459
+ - [Raiz del Monorepo](https://github.com/yoltra/yoltra/blob/main/README.md)
460
+ - [Guia de Contribucion](https://github.com/yoltra/yoltra/blob/main/CONTRIBUTING.md)
461
+
462
+ ---
463
+
464
+ ## Estado
465
+
466
+ **Release Candidate** -- Las APIs son estables, usadas en produccion, cambios menores posibles
467
+ antes de v1.0.0.
468
+
469
+ ---
470
+
471
+ ## Licencia
472
+
473
+ **MIT** -- Libre para usar en proyectos comerciales y de codigo abierto.