Shared state
Detached reads, copy-on-write updates, path subscriptions, batch operations, and synchronization between replicas.
One value, several renderers
The store lets React, Vue, and the shell project the same state. Data is addressed through dot-separated paths. Shared data should describe the domain or interaction; DOM nodes, React roots, and Vue instances belong to each app’s lifecycle.
To implement it in a real workspace, follow A counter shared by React and Vue. You will find complete components, initial reads, subscriptions, cleanup, and both permission sides: manifest and shell.
The following snippets belong in the shell’s client module, where wu is imported from wu-framework. Inside a microapp lifecycle, use context.wu and request store.read and store.write permissions for the required paths; the shell must grant them. Configuration and permissions shows both sides. To observe a shared write, open React and Vue and click +1 from 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 and update return sequence numbers. update gives the updater a detached snapshot and requires a synchronous result; a promise is rejected. To read asynchronous data, fetch it outside and then write the result.
Copies, identity, and reactivity
get(path) and getSnapshot(path) return data detached from internal state. Mutating the received object does not change the store. Writes use copy-on-write and internal state is frozen, so a descendant write does not retroactively modify earlier history entries.
const selection = wu.store.get('catalog.selection');
selection.quantity = 99;
console.log(wu.store.get('catalog.selection').quantity); // Sigue en el store
Every object read produces a new snapshot. Do not connect store.get() directly as a React useSyncExternalStore getSnapshot without preserving identity between notifications. For a manual bridge, keep a local snapshot and replace it only when a notification arrives; you can also use framework state and update it from on.
Notifications are scheduled in microtasks. Immediately after set, a read sees the new state, but the callback may not have run yet. Subscriptions do not automatically deliver an initial value: read once when connecting.
Exact and wildcard subscriptions
| Subscription | What it observes | Callback |
|---|---|---|
on('catalog', fn) |
Writes to catalog and descendant paths | (valorDeCatalog, 'catalog') |
on('catalog.selection', fn) |
That path and its descendants | (valorDelPath, pathSuscrito) |
on('catalog.*', fn) |
One segment immediately beneath catalog | ({ path, value }) |
on('*', fn) |
All writes | ({ path, value }) |
catalog.* matches neither catalog nor catalog.selection.quantity: the wildcard replaces one segment, not the entire subtree. Replacing a parent does not automatically notify all exact child subscriptions as though they had been written separately. For a component projecting an entire object, subscribe to the object’s path and read it again.
Select the payload according to the subscription, even when a generic TypeScript signature does not distinguish the two cases:
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();
Supported values
The local codec accepts null, booleans, strings, finite numbers, scalar bigint, dense arrays, and plain data objects. It rejects functions, symbols, undefined as a written value, cycles, accessors, sparse arrays, instances such as Date/Map, and structures exceeding 256 levels or 100,000 nodes.
Reading a nonexistent path can return undefined. Use ISO strings for shared dates; use strings for large IDs that will pass through JSON. Local bigint support does not imply sync compatibility: sync messages require JSON-safe data.
Paths reject empty segments and writes to __proto__, prototype, or constructor. A dangerous read returns undefined without exposing internal objects.
Batch and atomicity limits
wu.store.batch({
'catalog.selection': { id: 'p-42', quantity: 2 },
'checkout.step': 'review',
});
batch validates and detaches all paths/values before performing a write. If preflight fails, state, sequence, and metrics remain unchanged. Once validated, each entry retains its own write and sequence; low-level taps observe individual commits. It is neither a database transaction nor a transaction over external effects.
Optional synchronization
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();
The module loads lazily. BroadcastChannel enables communication between compatible browser contexts; WebSocket and custom transports extend transport options. Conflict resolution uses Last Writer Wins per path, ordered by Lamport clock and site identifier.
Only writes made after sync starts are tracked. Parent/child paths share Lamport ordering: a newer parent masks older children, and a newer child survives a late parent. Join snapshots preserve that order. Root replacements are not synchronized, and persistence is not automatic.
The built-in WebSocket transport requires synchronous authorizePeer(message) unless the host declares trustTransport: true because a relay has already authenticated participants. Sharing a room name does not authorize data.
Implementation references
Codec: src/core/wu-store.js; reads/writes: src/core/wu-store.js; updates: src/core/wu-store.js; batch: src/core/wu-store.js; notifications: src/core/wu-store.js; sync: src/core/wu-store-sync.js. Tests: 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.
Replica scope
paths limits inbound and outbound operations by segment prefixes: cart includes that path and its descendants. authorizeOp(op, message) must synchronously return true to accept an inbound operation. Remote writes go directly to the store; app grants do not authorize them. The runtime also bounds Lamport clock jumps.
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();