# 共享状态

独立读取副本、写时复制、按路径订阅、batch 与副本间同步。

<span id="un-dato-varios-renderers"></span>

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

## 同一数据，多个 renderers

store 让 React、Vue 和 shell 展示同一状态。数据通过点分路径寻址。共享数据应描述领域或交互；DOM 节点、React roots 和 Vue 实例属于各应用的生命周期。

要从真实 workspace 实现，参阅 [React 与 Vue 共享计数器](/zh-cn/docs/guides/shared-counter/)。其中包含完整组件、初始读取、订阅、清理，以及 manifest 和 shell 两侧的权限配置。

以下片段放在 **shell** 客户端模块中，其中 `wu` 从 `wu-framework` 导入。在**微应用**生命周期内，使用 `context.wu` 并为所需路径请求 `store.read` 和 `store.write` 权限；shell 必须授予它们。[配置与权限](/zh-cn/docs/framework/security/)展示两侧配置。要观察共享写入，打开 [React 与 Vue](/zh-cn/labs/composition/)并点击 **从 React +1**。

```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` 和 `update` 返回序号。`update` 向 updater 提供独立快照，要求同步结果；返回 Promise 会被拒绝。读取异步数据时，在外部执行 fetch，随后写入结果。

<span id="copias-identidad-y-reactividad"></span>

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

## 副本、身份与响应性

`get(path)` 和 `getSnapshot(path)` 返回与内部状态分离的数据。修改收到的对象不会改变 store。写入采用写时复制，内部状态被冻结，因此对子路径的写入不会反向修改之前的历史记录。

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

每次读取对象都会生成新快照。**不要在没有维持通知间身份稳定的情况下，将 `store.get()` 直接作为 React `useSyncExternalStore` 的 `getSnapshot`。** 手动桥接时，保存本地快照，仅在收到通知时替换；也可使用框架状态，通过 `on` 更新。

通知通过 microtasks 调度。`set` 之后立即读取会看到新状态，但回调不一定已经执行。订阅不会自动交付初始值：连接时先读取一次。

<span id="suscripción-exacta-y-wildcard"></span>

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

## 精确订阅与通配符

| 订阅 | 观察什么 | 回调 |
| --- | --- | --- |
| `on('catalog', fn)` | catalog 及其后代路径的写入 | `(valorDeCatalog, 'catalog')` |
| `on('catalog.selection', fn)` | 该路径及其后代 | `(valorDelPath, pathSuscrito)` |
| `on('catalog.*', fn)` | catalog 下的一个直接路径段 | `({ path, value })` |
| `on('*', fn)` | 所有写入 | `({ path, value })` |

`catalog.*` 不匹配 `catalog` 或 `catalog.selection.quantity`：通配符替代一个路径段，而非整棵子树。替换父路径时，不会自动向所有子路径精确订阅发送仿佛分别写入的通知。对于展示完整对象的组件，应订阅对象路径并重新读取它。

按订阅方式选择 payload，即使通用 TypeScript 签名没有区分这两种情况：

```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 id="valores-admitidos"></span>

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

## 支持的值

本地 codec 支持 `null`、布尔值、字符串、有限数字、标量 bigint、稠密数组和普通数据对象。拒绝函数、符号、作为写入值的 `undefined`、循环引用、访问器、稀疏数组、Date/Map 等实例，以及超过 256 层或 100 000 个节点的结构。

读取不存在的路径可以返回 `undefined`。共享日期使用 ISO 字符串；需要通过 JSON 传输的大整数 ID 使用字符串。本地 bigint 支持不代表与 sync 兼容，后者要求消息数据对 JSON 安全。

路径拒绝空段，也禁止写入 `__proto__`、`prototype` 或 `constructor`。危险读取返回 `undefined`，不会暴露内部对象。

<span id="batch-y-límites-de-atomicidad"></span>

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

## Batch 与原子性边界

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

`batch` 在执行任何写入前验证并分离全部路径和值。如果预检失败，状态、序号和指标均不改变。验证后，每个条目仍有独立写入和序号；底层 taps 观察到单独的 commits。它不是数据库事务，也不是外部副作用事务。

<span id="sincronización-optativa"></span>

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

## 按需同步

```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();
```

模块延迟加载。BroadcastChannel 让兼容的浏览器上下文通信；WebSocket 和自定义传输扩展传输方式。冲突解决采用按路径的 Last Writer Wins，按 Lamport 时钟和站点标识排序。

只跟踪启用 sync 后的写入。父/子路径共享 Lamport 顺序：较新的父节点覆盖旧子节点，较新的子节点在父节点延迟到达时仍保留。加入时的 snapshots 保持该顺序。不同步根替换，也不自动提供持久化。

内置 WebSocket 传输要求同步的 `authorizePeer(message)`，除非宿主因中继已经验证参与者身份而声明 `trustTransport: true`。共享房间名称不构成数据授权。

<span id="referencia-de-implementación"></span>

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

## 实现参考

Codec：`src/core/wu-store.js`；读取/写入：`src/core/wu-store.js`；更新：`src/core/wu-store.js`；batch：`src/core/wu-store.js`；通知：`src/core/wu-store.js`；sync：`src/core/wu-store-sync.js`。测试：`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 id="alcance-de-las-réplicas"></span>

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

## 副本范围

`paths` 通过路径片段前缀限制输入和输出操作：`cart` 包括该路径及其后代。`authorizeOp(op, message)` 必须同步返回 `true` 才接受传入操作。远程写入直接进入 store，不由应用 grants 授权。runtime 还限制 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();
```