Estado compartido
Lecturas separadas, escrituras copy-on-write, suscripciones por path, batch y sincronización entre réplicas.
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. 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 muestra ambas partes. Para observar una escritura compartida, abre React y Vue y pulsa +1 desde React.
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.
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.
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.
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:
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();
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.
Batch y límites de atomicidad
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.
Sincronización optativa
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.
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.
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.
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();