# Estado compartido

Lecturas separadas, escrituras copy-on-write, suscripciones por path, batch y sincronización entre réplicas.

<span class="section-anchor" id="section-1"></span>

## Un dato, varios renderers

El store permite que React, Vue y el shell proyecten el mismo estado. Sus datos se direccionan con paths separados por puntos. El dato compartido debe describir el dominio o la interacción; nodos DOM, roots de React e instancias de Vue pertenecen al lifecycle de cada app.

Para implementarlo desde un workspace real, sigue [Un contador entre React y Vue](/es/docs/guides/shared-counter/). Encontrarás los componentes completos, lectura inicial, suscripción, limpieza y los dos lados de los permisos: manifest y shell.

Los fragmentos siguientes van en el módulo cliente del **shell**, donde `wu` se importa de `wu-framework`. Dentro del lifecycle de una **microapp**, usa `context.wu` y solicita los permisos `store.read` y `store.write` para los paths necesarios; el shell debe concederlos. [Configuración y permisos](/es/docs/framework/security/) muestra ambas partes. Para observar una escritura compartida, abre [React y Vue](/es/labs/composition/) y pulsa **+1 desde React**.

```js
wu.store.set('catalog.selection', { id: 'p-42', quantity: 1 });

const stop = wu.store.on('catalog.selection', (selection) => {
  console.log(selection.id, selection.quantity);
});

wu.store.update('catalog.selection', (selection) => ({
  ...selection,
  quantity: selection.quantity + 1,
}));

await Promise.resolve(); // Permite entregar las notificaciones de la microtask.
stop();
```

`set` y `update` devuelven números de secuencia. `update` entrega al updater un snapshot separado y exige un resultado síncrono; una promesa se rechaza. Para leer datos asíncronos realiza fetch fuera y luego escribe el resultado.

<span class="section-anchor" id="section-2"></span>

## Copias, identidad y reactividad

`get(path)` y `getSnapshot(path)` devuelven datos separados del estado interno. Mutar el objeto recibido no altera el store. Las escrituras hacen copy-on-write y el estado interno se congela, de modo que una escritura descendiente no modifica retrospectivamente entradas anteriores del historial.

```js
const selection = wu.store.get('catalog.selection');
selection.quantity = 99;
console.log(wu.store.get('catalog.selection').quantity); // Sigue en el store
```

Cada lectura de un objeto produce un snapshot nuevo. **No conectes `store.get()` directamente como un `getSnapshot` de React `useSyncExternalStore` sin conservar identidad entre notificaciones.** Para un bridge manual, guarda un snapshot local y reemplázalo solo cuando llegue una notificación; también puedes usar estado del framework y actualizarlo desde `on`.

Las notificaciones se programan en microtasks. Inmediatamente después de `set`, una lectura ve el nuevo estado, pero el callback no necesariamente se ha ejecutado. Las suscripciones no entregan automáticamente un valor inicial: lee una vez al conectar.

<span class="section-anchor" id="section-3"></span>

## Suscripción exacta y wildcard

| Suscripción | Qué observa | Callback |
| --- | --- | --- |
| `on('catalog', fn)` | Escritura de catalog y de paths descendientes | `(valorDeCatalog, 'catalog')` |
| `on('catalog.selection', fn)` | Ese path y sus descendientes | `(valorDelPath, pathSuscrito)` |
| `on('catalog.*', fn)` | Un segmento inmediatamente debajo de catalog | `({ path, value })` |
| `on('*', fn)` | Todas las escrituras | `({ path, value })` |

`catalog.*` no coincide con `catalog` ni con `catalog.selection.quantity`: el wildcard sustituye un segmento, no todo el subárbol. Al reemplazar un padre no se notifican automáticamente todas las suscripciones exactas de hijos como si se hubieran escrito por separado. Para un componente que proyecta un objeto completo, suscríbelo al path del objeto y vuelve a leerlo.

Selecciona el payload según la suscripción, incluso cuando una firma genérica TypeScript no distinga ambos casos:

```js
const stopValue = wu.store.on('catalog.selection', (selection) => {
  console.log('Selección actual', selection);
});
const stopChanges = wu.store.on('catalog.*', ({ path, value }) => {
  console.log('Campo escrito', path, value);
});

wu.store.set('catalog.selection', { id: 'p-42', quantity: 2 });
await Promise.resolve();
// Al retirar el consumidor, libera ambas suscripciones.
stopValue();
stopChanges();
```

<span class="section-anchor" id="section-4"></span>

## Valores admitidos

El codec local admite `null`, booleanos, strings, números finitos, bigint escalar, arrays densos y objetos planos de datos. Rechaza funciones, símbolos, `undefined` como valor escrito, ciclos, accessors, arrays dispersos, instancias como Date/Map y estructuras que exceden 256 niveles o 100 000 nodos.

Una lectura de un path inexistente sí puede devolver `undefined`. Para fechas compartidas usa ISO strings; para IDs grandes que saldrán por JSON usa strings. Bigint local no implica compatibilidad con sync, cuyos mensajes exigen datos JSON seguros.

Los paths rechazan segmentos vacíos y escrituras en `__proto__`, `prototype` o `constructor`. Una lectura peligrosa devuelve `undefined` sin exponer objetos internos.

<span class="section-anchor" id="section-5"></span>

## Batch y límites de atomicidad

```js
wu.store.batch({
  'catalog.selection': { id: 'p-42', quantity: 2 },
  'checkout.step': 'review',
});
```

`batch` valida y separa todos los paths/valores antes de realizar una escritura. Si falla ese preflight, no cambia estado, secuencia ni métricas. Una vez validado, cada entrada conserva su propia escritura y secuencia; los taps de bajo nivel observan commits individuales. No es una transacción de base de datos ni una transacción sobre efectos externos.

<span class="section-anchor" id="section-6"></span>

## Sincronización optativa

```js
const sync = wu.store.sync({ transport: 'broadcast', room: 'docs-demo' });
await sync.ready();

// Escribir después de conectar registra estos paths para otros participantes.
wu.store.set('demo.counter', 1);
console.log(sync.status());

// Al abandonar la sesión:
sync.stop();
```

El módulo se carga de forma diferida. BroadcastChannel permite comunicación entre contextos compatibles del navegador; WebSocket y transportes custom amplían el transporte. La resolución de conflictos usa Last Writer Wins por path, ordenado por reloj Lamport e identificador de sitio.

Solo se registra lo escrito después de activar sync. Los paths padre/hijo comparten el orden Lamport: un padre más nuevo oculta hijos anteriores y un hijo más nuevo sobrevive a un padre tardío. Los snapshots de incorporación conservan ese orden. No se sincronizan reemplazos de raíz ni se obtiene persistencia automática.

El transporte WebSocket incorporado exige `authorizePeer(message)` síncrono salvo que el host declare `trustTransport: true` porque un relay ya autenticó a los participantes. Compartir un nombre de sala no autoriza datos.

<span class="section-anchor" id="section-7"></span>

## Referencia de implementación

Codec: `src/core/wu-store.js`; lectura/escritura: `src/core/wu-store.js`; actualización: `src/core/wu-store.js`; batch: `src/core/wu-store.js`; notificaciones: `src/core/wu-store.js`; sync: `src/core/wu-store-sync.js`. Pruebas: `tests/unit/store.test.js`, `tests/unit/store-snapshot-identity.test.js`, `tests/unit/store-subscription-semantics.test.js`, `tests/unit/store-sync.test.js`.

<span class="section-anchor" id="section-8"></span>

## Alcance de las réplicas

`paths` limita entradas y salidas mediante prefijos de segmentos: `cart` incluye ese path y sus descendientes. `authorizeOp(op, message)` debe devolver `true` de forma síncrona para aceptar una operación entrante. Las escrituras remotas van directamente al store; los grants de una app no las autorizan. El runtime también limita los saltos del reloj Lamport.

```js
const sync = wu.store.sync({
  transport: 'broadcast', room: 'cart-demo', paths: ['cart'],
  authorizeOp: op => op.path === 'cart.count' && Number.isSafeInteger(op.value),
});
await sync.ready();
wu.store.set('cart.count', 1);
// Teardown:
sync.stop();
```