# Общие данные

Все участники чата открывают один экземпляр приложения. Его общие данные хранит Pairx: сервер ставит порядок записей, проверяет права и пределы и рассылает изменения всем, у кого приложение открыто. Своего сервера приложению не нужно.

## Хранилища

Хранилища объявляются в [манифесте](https://dev.pairx.org/reference/manifest) — в странице или в `pairx.app.json`:

```html
<script type="application/pairx+json">
{"stores": {
  "queue": {"type": "log"},
  "votes": {"type": "slot"},
  "board": {"type": "doc", "writers": "admins"},
  "likes": {"type": "counter", "per_user_limit": 1}
}}
</script>
```

Тип хранилища — это правило, которое проверяет сервер: в `slot` человек меняет только своё значение, из `log` убирает только свою запись. Приложение не может обойти эти правила, даже если его код изменить.

## Какой тип выбрать

| Тип | Для чего | Снимок |
| --- | --- | --- |
| `log` | Журнал в порядке сервера: очередь, сообщения, ходы. Добавлять может любой с правом записи, убрать — только свою запись; убранная запись остаётся на своём месте без значения. | `{ type: "log", entries: [{ entry, user, value, at, retracted }] }` |
| `slot` | Одно значение на человека: голос, ответ, статус. Каждый меняет только своё. | `{ type: "slot", slots: { [user]: value } }` |
| `doc` | Один общий документ с версией: доска, настройки. Запись называет версию, которую прочитала; устаревшая отклоняется с `conflict`. | `{ type: "doc", value, version }` |
| `counter` | Суммы по ключам, к которым прибавляют все: лайки, очки. Ключ — до 64 символов, `amount` — целое, не ноль, до ±1 000 000. `per_user_limit` ограничивает долю одного человека по модулю: с пределом 1 его вклад в ключ — −1, 0 или 1. | `{ type: "counter", totals: { [key]: number }, mine: { [key]: number } }` |

Типичные задачи:

| Задача | Хранилище |
| --- | --- |
| Очередь, сообщения, ходы игры, история | `log` |
| Голосование, ответы на вопрос, «иду / не иду», статус готовности | `slot` |
| Общий список, доска, настройки, расписание дежурств | `doc` |
| Лайки, очки, «сколько раз нажали», бронирование мест с пределом | `counter` |

Хранилищ может быть несколько: в [примере «Идеи»](https://dev.pairx.org/examples#ideas) идеи лежат в журнале, а голоса за них — в счётчике по ID записи.

## Снимок и лента изменений

Данные приходят двумя путями:

1. `storage.snapshot` — всё состояние на одном `seq` (номере последнего изменения).
2. Событие `storage.changed` — изменения всех участников после него, и ваши собственные тоже, строго по порядку `seq`.

```js
let seq = 0, entries = [];

pairx.on("storage.changed", ({ changes }) => {
  for (const c of changes) {
    if (c.seq <= seq) continue;          // уже учтено в снимке
    seq = c.seq;
    if (c.store === "queue" && c.op === "log") entries.push({ entry: c.entry, user: c.user, value: c.value, retracted: false });
    if (c.store === "queue" && c.op === "retract") entries.find((e) => e.entry === c.entry).retracted = true;
  }
  render();
});
pairx.on("storage.reset", load);         // перечитать всё

async function load() {
  const snapshot = await pairx.call("storage.snapshot");
  seq = snapshot.seq;
  entries = snapshot.stores.queue.entries;
  render();
}
```

Правила, которые стоит соблюдать:

- **Пропускайте изменения, которые снимок уже учёл** (`c.seq <= seq`). Иначе значение может откатиться на старое.
- **Показывайте данные из ленты, а не из ответа на запись.** Ваша запись придёт событием на своём месте в порядке — вместе с записями других, которые сервер принял раньше.
- **`storage.reset` — перечитать снимок.** Он приходит, когда кто-то удалил свои данные или изменений накопилось больше, чем помнит сервер.

Формы изменений каждой операции — в [справочнике хранилища](https://dev.pairx.org/reference/storage#changes). [SDK](https://dev.pairx.org/reference/sdk) делает всё это сам.

## Запись

```js
await pairx.call("storage.write", { op: "log.append", store: "queue", value: "join" });
```

Ответ приходит, когда сервер принял запись. Если связь пропала посреди записи, Pairx сам повторяет её один раз — сервер узнаёт повтор и не применяет запись дважды. Если и повтор не прошёл, вызов завершится ошибкой `offline`: проверьте по ленте, дошла ли запись, прежде чем повторять её.

## Документ и конфликты

`doc` — один документ на экземпляр. Запись называет версию, которую прочитала; если кто-то успел записать раньше, сервер отвечает `conflict`. Тогда перечитайте снимок, примените своё изменение к новой версии и повторите:

```js
async function change(edit) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      const value = edit(doc.value);
      const { version } = await pairx.call("storage.write", { op: "doc.set", store: "board", value, version: doc.version });
      doc = { value, version };
      return;
    } catch (error) {
      if (error.code !== "conflict") throw error;
      const snapshot = await pairx.call("storage.snapshot");
      doc = snapshot.stores.board;
    }
  }
}
```

Храните в документе изменение как функцию от текущего состояния («добавить пункт», «отметить пункт»), а не готовый результат — тогда повтор не затрёт чужую правку. Так сделано в [примере «Общий список»](https://dev.pairx.org/examples#checklist).

## Права на запись

| `writers` | Кто пишет |
| --- | --- |
| `everyone` | Все участники (по умолчанию). |
| `admins` | Тот, кто отправил приложение в чат, и администраторы чата. |
| `author` | Только тот, кто отправил приложение в чат. |

Права проверяет сервер. «Автор» — тот, кто отправил приложение в этот чат.

## Пределы

| Что | Предел |
| --- | --- |
| Значение слота или документа | 16 КиБ |
| Запись журнала | 4 КиБ |
| Записей в журнале | 10 000 |
| Ключей в счётчике | 1 000 |
| Весь экземпляр | 1 МиБ |
| Личные данные (`localStorage`) | 256 КиБ |
| Хранилищ в манифесте | 16 |

При превышении запись отклоняется с кодом `too_large` или `rate_limited`; данные не теряются.

## Удаление данных

Человек может удалить свои данные в этом экземпляре из меню приложения: его значения в `slot`, вклад в счётчики и личные данные удаляются, а записи журнала становятся убранными (остаются на своих местах без значения). Общий документ остаётся как есть — он не принадлежит одному человеку. Остальные участники получат `storage.reset`.
