@yoltra/core 0.5.0 → 0.6.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
@@ -36,7 +36,7 @@ emit(channel, type, payload)
36
36
  │
37
37
  │ ══ fase de reduccion SINCRONA — corre antes de que emit() retorne ══
38
38
  ├─ 1. Middleware ─── Hooks pre-reducer sincronos (devolver false para rechazar → evento "no confirmado")
39
- ├─ 2. Reducers ─── Actualizaciones de estado sincronas, deteccion de cambios de grano fino por ruta
39
+ ├─ 2. Reducers ─── Cada slice que aplica se prepara, y todas se confirman bajo una sola raiz
40
40
  ├─ 3. Suscriptores de eventos ─── Notificaciones de eventos confirmados/no confirmados
41
41
  ├─ 4. Suscriptores gruesos ─── Listeners externos del store (useSyncExternalStore, etc.), si el estado cambio
42
42
  │
@@ -276,6 +276,213 @@ store.onEvent(
276
276
 
277
277
  ---
278
278
 
279
+ ## Los commits son atomicos entre slices
280
+
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
283
+ mismo evento ya aplicadas.
284
+
285
+ Esto importa sobre todo donde un cambio se usa como senal para volver a leer, que es lo que hacen
286
+ los hooks de React.
287
+
288
+ ---
289
+
290
+ ## Rechazar una escritura
291
+
292
+ 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
294
+ por que.
295
+
296
+ ```typescript
297
+ import { createStore, Rejected } from "@yoltra/core";
298
+
299
+ const store = createStore({
300
+ name: "plan",
301
+ reducer: {
302
+ plan: {
303
+ state: { steps: [], version: 1 },
304
+ when: { keys: [["plan", "patch"]] },
305
+ reducer: (state, event) =>
306
+ event.payload.expectedVersion === state.version
307
+ ? { ...state, steps: event.payload.steps, version: state.version + 1 }
308
+ : Rejected(`escritura obsoleta: esperaba v${event.payload.expectedVersion}`),
309
+ },
310
+ },
311
+ onRejected: (rejection, event, slice) => metrics.increment("write.refused", { slice }),
312
+ });
313
+
314
+ const result = await store.emit("plan", "patch", { steps, expectedVersion: 1 });
315
+
316
+ result.committed; // true — el middleware lo permitio
317
+ result.written; // false — pero no se escribio nada
318
+ result.rejected?.reason;
319
+ ```
320
+
321
+ 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.
325
+
326
+ `emit` resuelve a un `EmitResult` cuando terminan los efectos:
327
+
328
+ | | |
329
+ |---|---|
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` |
333
+
334
+ La fase `written` de `onEvent` reporta lo mismo a los suscriptores. `committed` sigue
335
+ 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.
338
+
339
+ ---
340
+
341
+ ## Peticion y respuesta — `store.call()`
342
+
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
346
+ id produce un timeout sin nada a lo que apuntar.
347
+
348
+ ```typescript
349
+ const res = await store.call("rpc", "ask", { q: "quien?" }, { reply: ["rpc", "answer"] });
350
+ res.payload.text;
351
+ ```
352
+
353
+ `Quien Responde` no hace nada especial. Responde con el `emit` que recibio, y la marca causal del
354
+ store correlaciona ambos: **no hay id que generar, devolver ni olvidar**.
355
+
356
+ ```typescript
357
+ store.registerEffect({
358
+ when: { keys: [["rpc", "ask"]] },
359
+ effect: async (event, _get, emit) => {
360
+ await emit("rpc", "answer", await lookup(event.payload.q));
361
+ },
362
+ });
363
+ ```
364
+
365
+ ### Una llamada resuelve al evento, no al payload
366
+
367
+ Porque muchas veces quien llama no sabe *cual* respuesta va a recibir. `reply` nombra los tipos
368
+ **terminales**, y el evento trae el discriminante:
369
+
370
+ ```typescript
371
+ const res = await store.call("rpc", "ask", { q }, { reply: ["rpc", ["answer", "error"]] });
372
+
373
+ switch (res.type) {
374
+ case "answer": return res.payload.text;
375
+ case "error": throw new Error(res.payload.reason);
376
+ }
377
+ ```
378
+
379
+ ### El progreso se transmite, y el productor espera
380
+
381
+ Cualquier evento correlacionado que **no** sea terminal es progreso. Itera la llamada para
382
+ consumirlo:
383
+
384
+ ```typescript
385
+ const call = store.call("job", "start", { id }, { reply: ["job", "done"], highWaterMark: 4 });
386
+
387
+ for await (const step of call) await render(step.payload);
388
+ const { payload } = await call;
389
+ ```
390
+
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
393
+ que un `Quien Responde` que escribe `await emit("job", "tick", chunk)` **va al ritmo del lector**.
394
+
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`.
399
+
400
+ ### Retroceso
401
+
402
+ | | |
403
+ |---|---|
404
+ | `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. |
406
+ | `call.cancel(reason)` | Deja de escuchar y liquida la llamada. |
407
+
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.
410
+
411
+ ---
412
+
413
+ ## Leer un valor al suscribirse
414
+
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:
417
+
418
+ ```typescript
419
+ store.connect({ reducer: "todos", property: "items.0.title" }, render, { immediate: true });
420
+ ```
421
+
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.
424
+
425
+ ---
426
+
427
+ ## De donde vino un cambio
428
+
429
+ Un `Change` nombra el evento que lo causo, asi que un suscriptor ya no tiene que duplicar la causa
430
+ dentro del estado:
431
+
432
+ ```typescript
433
+ store.connect({ reducer: "orders", property: "status" }, (change) => {
434
+ audit.record(change.path, change.newValue, {
435
+ causedBy: change.eventId,
436
+ via: `${change.channel}/${change.type}`,
437
+ });
438
+ });
439
+ ```
440
+
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.
443
+
444
+ ---
445
+
446
+ ## Proteccion contra cascadas (activada por defecto)
447
+
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
451
+ congelada, o un core al 100%, sin error ni stack al que apuntar.
452
+
453
+ Por eso cada evento lleva su posicion causal, y el store se niega a extender una cadena mas alla
454
+ de un tope:
455
+
456
+ ```typescript
457
+ const store = createStore({
458
+ name: "app",
459
+ reducer: { ... },
460
+
461
+ // Por defecto 64. Acotado configures o no: un fallo tan grave no deberia exigir
462
+ // configuracion para evitarse. Usa Infinity para renunciar a el conscientemente.
463
+ maxReduceDepth: 64,
464
+
465
+ onCascade: ({ event, depth, chain }) => {
466
+ report(`cascada en ${event.channel}/${event.type}, profundidad ${depth}`, chain);
467
+ },
468
+ });
469
+ ```
470
+
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.
474
+
475
+ 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
477
+ estuviera emitiendo, que es justo el fallo inatribuible que el tope existe para evitar.
478
+
479
+ **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
481
+ `store.emit` nunca acumula profundidad. `maxTransitionsPerDrain` acota el *ancho* y por eso viene
482
+ desactivado.
483
+
484
+ ---
485
+
279
486
  ## Deduplicacion de Eventos (opt-in)
280
487
 
281
488
  La deduplicacion esta **desactivada por defecto** — yoltra nunca descarta en silencio eventos
@@ -434,12 +641,12 @@ store.registerEffect({
434
641
 
435
642
  ## Rendimiento
436
643
 
437
- | Metrica | Valor |
438
- | --------------------- | ----------------------------------------- |
439
- | **Tamano del bundle** | ~8KB (minificado + gzipped) |
440
- | **Tree-shakeable** | Si (modulos ES) |
441
- | **Dependencias** | Cero |
442
- | **TypeScript** | Definiciones de tipos completas incluidas |
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 |
443
650
 
444
651
  ---
445
652
 
package/README.md CHANGED
@@ -36,7 +36,7 @@ emit(channel, type, payload)
36
36
  │
37
37
  │ ══ SYNCHRONOUS reduce phase — runs before emit() returns ══
38
38
  ├─ 1. Middleware ─── Synchronous pre-reducer hooks (return false to reject → "uncommitted" event)
39
- ├─ 2. Reducers ─── Synchronous state updates, fine-grained path change detection
39
+ ├─ 2. Reducers ─── Every matching slice staged, then all committed under one root
40
40
  ├─ 3. Event subscribers ─── Committed/uncommitted event notifications
41
41
  ├─ 4. Coarse subscribers ─── External store listeners (useSyncExternalStore, etc.), if state changed
42
42
  │
@@ -316,7 +316,17 @@ store.onEvent(
316
316
  "uncommitted",
317
317
  );
318
318
 
319
- // All events — both committed and uncommitted
319
+ // Written events — state actually changed. Fires after the commit, so getState() is current.
320
+ store.onEvent(
321
+ "plan",
322
+ "patch",
323
+ (event, getState) => {
324
+ console.log("applied:", getState().plan);
325
+ },
326
+ "written",
327
+ );
328
+
329
+ // All events — both committed and uncommitted (not written; see below)
320
330
  store.onEvent(
321
331
  "ui",
322
332
  "action",
@@ -327,6 +337,185 @@ store.onEvent(
327
337
  );
328
338
  ```
329
339
 
340
+ `committed` means **not vetoed**, and always has: it fires for every event middleware let through,
341
+ whether or not a reducer wrote anything — including every event in a store with no reducers at
342
+ all. `written` is the stricter fact, added rather than substituted, so toasts and analytics keep
343
+ working unchanged. `all` stays `committed | uncommitted`; folding `written` in would hand existing
344
+ subscribers a second notification per event.
345
+
346
+ ---
347
+
348
+ ## Commits are atomic across slices
349
+
350
+ An event that touches several slices writes all of them, then notifies. Nothing observes a
351
+ half-applied event — a subscriber to one slice reading `getState()` sees every other slice of the
352
+ same event already applied.
353
+
354
+ That matters most where a change is used as a signal to re-read, which is what the React hooks do.
355
+
356
+ ---
357
+
358
+ ## Refusing a write
359
+
360
+ A reducer returns `Rejected(reason)` instead of state to decline. **The whole event is rejected**:
361
+ no slice writes, no change notification fires, and the caller is told why.
362
+
363
+ ```typescript
364
+ import { createStore, Rejected } from "@yoltra/core";
365
+
366
+ const store = createStore({
367
+ name: "plan",
368
+ reducer: {
369
+ plan: {
370
+ state: { steps: [], version: 1 },
371
+ when: { keys: [["plan", "patch"]] },
372
+ reducer: (state, event) =>
373
+ event.payload.expectedVersion === state.version
374
+ ? { ...state, steps: event.payload.steps, version: state.version + 1 }
375
+ : Rejected(`stale write: expected v${event.payload.expectedVersion}, have v${state.version}`),
376
+ },
377
+ },
378
+ onRejected: (rejection, event, slice) => metrics.increment("write.refused", { slice }),
379
+ });
380
+
381
+ const result = await store.emit("plan", "patch", { steps, expectedVersion: 1 });
382
+
383
+ result.committed; // true — middleware allowed it
384
+ result.written; // false — but nothing was written
385
+ result.rejected?.reason; // "stale write: expected v1, have v3"
386
+ ```
387
+
388
+ Refusing is **not** the same as returning the state unchanged, which is indistinguishable from
389
+ "this event did not concern me". It is also not the same as throwing: a reducer that throws has a
390
+ bug, so its slice is isolated and every other slice still commits, while a reducer that refuses
391
+ has made a decision and the whole event yields to it.
392
+
393
+ `emit` resolves to an `EmitResult` once effects have run:
394
+
395
+ | | |
396
+ |---|---|
397
+ | `committed` | middleware did not veto |
398
+ | `written` | a reducer actually changed state |
399
+ | `rejected` | present when a reducer refused, carrying `reason` |
400
+
401
+ ---
402
+
403
+ ## Request and reply — `store.call()`
404
+
405
+ Every event-bus consumer eventually writes request/reply by hand: mint an id, subscribe, match,
406
+ time out, unsubscribe. It is about eighty lines and it has the same two bugs every time — the
407
+ subscription outlives the call, and a responder that forgets to echo the id produces a timeout
408
+ with nothing to point at.
409
+
410
+ ```typescript
411
+ const res = await store.call("rpc", "ask", { q: "who?" }, { reply: ["rpc", "answer"] });
412
+ res.payload.text;
413
+ ```
414
+
415
+ The responder does nothing special. It replies through the `emit` it was handed, and the store's
416
+ causal stamp correlates the two — **there is no id to mint, echo, or forget**:
417
+
418
+ ```typescript
419
+ store.registerEffect({
420
+ when: { keys: [["rpc", "ask"]] },
421
+ effect: async (event, _get, emit) => {
422
+ await emit("rpc", "answer", await lookup(event.payload.q));
423
+ },
424
+ });
425
+ ```
426
+
427
+ ### A call resolves to the event, not the payload
428
+
429
+ Because a caller often cannot know *which* reply it will get. `reply` names the **terminal**
430
+ types, and the event carries the discriminant:
431
+
432
+ ```typescript
433
+ const res = await store.call("rpc", "ask", { q }, { reply: ["rpc", ["answer", "error"]] });
434
+
435
+ switch (res.type) {
436
+ case "answer": return res.payload.text;
437
+ case "error": throw new Error(res.payload.reason);
438
+ }
439
+ ```
440
+
441
+ ### Progress streams, and the producer waits
442
+
443
+ Any correlated event that is **not** terminal is progress. Iterate the call to consume it:
444
+
445
+ ```typescript
446
+ const call = store.call("job", "start", { id }, {
447
+ reply: ["job", "done"],
448
+ highWaterMark: 4,
449
+ });
450
+
451
+ for await (const step of call) await render(step.payload);
452
+ const { payload } = await call;
453
+ ```
454
+
455
+ The backpressure is real, not a buffer with a limit. `emit` resolves only once its effects have
456
+ run, and the collector is an effect that does not return until the consumer has taken the item —
457
+ so a responder writing `await emit("job", "tick", chunk)` is **paced by the reader**:
458
+
459
+ ```typescript
460
+ effect: async (_event, _get, emit) => {
461
+ for (const chunk of chunks) {
462
+ await emit("job", "tick", chunk); // waits here while the consumer is behind
463
+ }
464
+ await emit("job", "done", { ok: true });
465
+ }
466
+ ```
467
+
468
+ Backpressure engages **once you begin iterating**. A call that is only awaited never pulls, so
469
+ blocking its producer would deadlock the call itself — progress nobody reads would stop the
470
+ terminal event from ever being sent. Un-iterated progress therefore buffers to `highWaterMark`
471
+ and is then counted on `call.dropped` rather than blocking.
472
+
473
+ ### Giving up
474
+
475
+ | | |
476
+ |---|---|
477
+ | `timeoutMs` | **Idle**, not total — every correlated event resets it, progress included. A job that streams for two minutes will not fail a thirty-second call. Default 30s. |
478
+ | `signal` | An `AbortSignal`, for a real deadline or a cancelled action. |
479
+ | `call.cancel(reason)` | Stops listening and settles. Safe to call twice. |
480
+
481
+ However a call ends — resolved, timed out, aborted — the subscription is removed and any producer
482
+ parked on backpressure is released. A wedged responder is worse than the unbounded buffer this
483
+ replaced.
484
+
485
+ ## Reading a value as you subscribe
486
+
487
+ `connect` starts at "from now on", so a subscriber's first read had to repeat the path elsewhere —
488
+ the same path in two places, free to drift:
489
+
490
+ ```typescript
491
+ store.connect({ reducer: "todos", property: "items.0.title" }, render, { immediate: true });
492
+ ```
493
+
494
+ The synthetic first change has `oldValue: undefined` and **no provenance**, because no event
495
+ caused it. For a wildcard pattern, which has no single current value, the slice root is delivered
496
+ with `path: ""`.
497
+
498
+ React does not need this: `useSyncExternalStore` already reads a snapshot on mount.
499
+
500
+ ---
501
+
502
+ ## Where a change came from
503
+
504
+ A `Change` names the event that caused it, so a subscriber no longer has to mirror the cause into
505
+ state and keep it in two places:
506
+
507
+ ```typescript
508
+ store.connect({ reducer: "orders", property: "status" }, (change) => {
509
+ audit.record(change.path, change.newValue, {
510
+ causedBy: change.eventId,
511
+ via: `${change.channel}/${change.type}`,
512
+ });
513
+ });
514
+ ```
515
+
516
+ Provenance is **absent** when no event caused the change — a DevTools time-travel jump, or the
517
+ `immediate` delivery above. Absence is the signal, rather than a fabricated id.
518
+
330
519
  ---
331
520
 
332
521
  ## Event Deduplication (opt-in)
@@ -350,6 +539,57 @@ await store.emit("analytics", "pageView", { page }, { dedupKey: `pageView:${page
350
539
 
351
540
  ---
352
541
 
542
+ ## Cascade protection (on by default)
543
+
544
+ Two consumers wired into each other — a subscriber that emits what its own reducer answers, or
545
+ two slices that answer each other's events — produce an event chain with no end. The reduce queue
546
+ drains **synchronously**, so that is not a slow program: it is a frozen tab, or a pinned core,
547
+ with no error and no stack to point at.
548
+
549
+ Every event therefore carries its causal position, and the store refuses to extend a chain past a
550
+ ceiling:
551
+
552
+ ```typescript
553
+ const store = createStore({
554
+ name: "app",
555
+ reducer: { ... },
556
+
557
+ // Defaults to 64. Bounded whether or not you configure it — a failure mode this bad
558
+ // should not require configuration to avoid. Set Infinity to opt out and own it.
559
+ maxReduceDepth: 64,
560
+
561
+ onCascade: ({ event, depth, chain }) => {
562
+ report(`cascade at ${event.channel}/${event.type}, depth ${depth}`, chain);
563
+ },
564
+ });
565
+ ```
566
+
567
+ An event emitted while another is being handled is one deeper than its cause, and carries
568
+ `parentId` and `depth` so the cycle is legible after the fact:
569
+
570
+ ```typescript
571
+ store.onEvent("plan", "patch", (event) => {
572
+ event.depth; // 0 for an event emitted by application code
573
+ event.parentId; // undefined at depth 0; the causing event's id below it
574
+ });
575
+ ```
576
+
577
+ Both fields are **absent** on a root event rather than present as `0`/`undefined`, so events your
578
+ application emits stay byte-identical to before this existed.
579
+
580
+ Breaching does not throw. The offending emit is refused, everything already committed stands, and
581
+ `onCascade` (plus a console error) names it — a throw would surface in whichever subscriber or
582
+ effect happened to be emitting, which is the same unattributable failure the ceiling exists to
583
+ prevent.
584
+
585
+ **A wide burst is not a cascade.** One event whose subscriber fans out to five hundred siblings
586
+ is a legitimate shape; depth is what separates it from a cycle, and a plain loop of `store.emit`
587
+ never accumulates depth at all — each call drains to completion before the next, so every one is
588
+ a root. `maxTransitionsPerDrain` bounds burst *width* and is off by default for that reason; the
589
+ event that starts a drain is never refused by it.
590
+
591
+ ---
592
+
353
593
  ## Dynamic Reducers
354
594
 
355
595
  Add or remove reducer slices at runtime:
@@ -575,7 +815,7 @@ individual fields edited is better off as an array today.
575
815
 
576
816
  | Metric | Value |
577
817
  | ------------------ | ----------------------------------------- |
578
- | **Bundle size** | 6.7 KB for the store (minified + gzipped) |
818
+ | **Bundle size** | 9.2 KB for the store (minified + gzipped) |
579
819
  | **Tree-shakeable** | Yes (ES modules) |
580
820
  | **Dependencies** | Zero |
581
821
  | **TypeScript** | Full type definitions included |
@@ -586,16 +826,21 @@ would — tree-shaken, minified, gzipped — and fails when it exceeds the budge
586
826
 
587
827
  The number that matters is what you import, not what the package exports:
588
828
 
589
- | Import | Size |
590
- | ----------------------------------- | ------ |
591
- | `{ createStore }` | 6.7 KB |
592
- | `{ createStore, hydrate, persist }` | 8.2 KB |
593
- | everything | 9.5 KB |
594
-
595
- Persistence and the entity adapter cost nothing to anyone who does not import them — the
596
- first row has not moved as either was added, which is the tree-shaking claim being checked
597
- rather than repeated. The last row is a growth tripwire; `import * as all` is not something
598
- anybody writes.
829
+ | Import | Size | Budget |
830
+ | ----------------------------------- | ------- | ------ |
831
+ | `{ createStore }` | 9.2 KB | 14 KB |
832
+ | `{ createStore, hydrate, persist }` | 10.7 KB | 16 KB |
833
+ | everything | 12.1 KB | 18 KB |
834
+
835
+ The **gap between rows** is the tree-shaking claim, and it is what to watch: persistence adds
836
+ 1.5 KB to the people who import it and nothing to anyone else, and the whole barrel is 2.9 KB
837
+ past the store. The last row is a growth tripwire; `import * as all` is not something anybody
838
+ writes.
839
+
840
+ The first row moves only when the store itself grows, and it has: bounding cascades, staging
841
+ commits so they apply atomically, and `store.call()` are all store machinery rather than
842
+ opt-in modules, so they are paid by everyone. That is the honest trade for a default that
843
+ stops a runaway from hanging the tab.
599
844
 
600
845
  ---
601
846
 
@@ -9,10 +9,15 @@ export { EventBus } from './eventBus/EventBus.js';
9
9
  export { LooseEventBus } from './eventBus/LooseEventBus.js';
10
10
  export { Reducer } from './reducer/Reducer.js';
11
11
  export { Store, createStore, typedEvents } from './store/Store.js';
12
+ export { Rejected, isRejected } from './store/rejection.js';
13
+ export { CallAbortedError, CallTimeoutError } from './store/call.js';
14
+ export type { CallHandle, CallOptions, ReplySpec } from './store/call.js';
15
+ export type { Rejection } from './store/rejection.js';
12
16
  export { detectChangedProps } from './utils/detectChangedProps.js';
13
17
  export { freezeState } from './utils/immutability.js';
18
+ export type { AliasWatch } from './utils/immutability.js';
14
19
  export { eventKeys } from './types.js';
15
- export type { EventMapBase, EventKey, Event, EventUnion, Change, Emit, EmitOptions, EventMeta, InstrumentedEvent, InstrumentationObserver, Unsubscribe, StoreSpec, StoreInstance, ReducerSpec, ReducerFunction, ReducersMapAny, StateFromReducers, EMFromReducersStrict, EffectSpec, EffectFunction, MiddlewareFunction, MiddlewareSpec, MiddlewareInput, DeepReadonly, DeepRO, Primitive, RootValue, Path, PathValue, WithGlob, Dotted, EventPhase, EventSubscriptionHandler, NarrowedEventHandler, When, EventFromWhen, EventConsumerType, EventConsumerMeta, } from './types.js';
20
+ export type { 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';
16
21
  export { createEntityAdapter } from './entity/entityAdapter.js';
17
22
  export type { EntityAdapter, EntityAdapterOptions, EntityId, EntityState, EntityUpdate, } from './entity/entityAdapter.js';
18
23
  export { decodeState, encodeState, encodeStateBounded } from './serialize/codec.js';
@@ -84,10 +84,6 @@ export interface Hydration {
84
84
  export declare function hydrate(options: PersistOptions & {
85
85
  readonly source?: string;
86
86
  }): Promise<Hydration>;
87
- /** A reducer spec, as far as hydration cares: something carrying an initial `state`. */
88
- interface HasState {
89
- state: unknown;
90
- }
91
87
  /**
92
88
  * Replaces each reducer's initial state with what was restored for it.
93
89
  *
@@ -97,7 +93,9 @@ interface HasState {
97
93
  *
98
94
  * @public
99
95
  */
100
- export declare function withHydration<R extends Record<string, HasState>>(reducers: R, hydration: Hydration): R;
96
+ export declare function withHydration<R extends Record<string, {
97
+ state: unknown;
98
+ }>>(reducers: R, hydration: Hydration): R;
101
99
  /** The store surface persistence needs, which is two methods wide. */
102
100
  export interface PersistableStore {
103
101
  getState(): unknown;
@@ -123,4 +121,3 @@ export declare function persist(store: PersistableStore, options: PersistOptions
123
121
  * @public
124
122
  */
125
123
  export declare function dehydrate(store: Pick<PersistableStore, "getState">, options: Pick<PersistOptions, "version" | "slices">): string;
126
- export {};
@@ -1,4 +1,5 @@
1
1
  import { EventMapBase, EventUnion, ReducerFunction } from '../types.js';
2
+ import { Rejection } from '../store/rejection.js';
2
3
  /**
3
4
  * Thin wrapper around a pure reducer function (stateful event consumer):
4
5
  * given a state `S` and an event (from {@link EventUnion | `EventUnion<EM>`}),
@@ -68,7 +69,7 @@ export declare class Reducer<S, EM extends EventMapBase = EventMapBase> {
68
69
  *
69
70
  * @param state - Current state.
70
71
  * @param event - An event drawn from {@link EventUnion | `EventUnion<EM>`}.
71
- * @returns The next state produced by the underlying reducer function.
72
+ * @returns The next state, or a {@link Rejection} if the reducer refused the write.
72
73
  *
73
74
  * @example
74
75
  * ```ts
@@ -77,5 +78,5 @@ export declare class Reducer<S, EM extends EventMapBase = EventMapBase> {
77
78
  *
78
79
  * @public
79
80
  */
80
- reduce(state: S, event: EventUnion<EM>): S;
81
+ reduce(state: S, event: EventUnion<EM>): S | Rejection;
81
82
  }