Общие данные
Все участники чата открывают один экземпляр приложения. Его общие данные хранит Pairx: сервер ставит порядок записей, проверяет права и пределы и рассылает изменения всем, у кого приложение открыто. Своего сервера приложению не нужно.
Хранилища#
Хранилища объявляются в манифесте — в странице или в pairx.app.json:
<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 |
Хранилищ может быть несколько: в примере «Идеи» идеи лежат в журнале, а голоса за них — в счётчике по ID записи.
Снимок и лента изменений#
Данные приходят двумя путями:
storage.snapshot— всё состояние на одномseq(номере последнего изменения).- Событие
storage.changed— изменения всех участников после него, и ваши собственные тоже, строго по порядкуseq.
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— перечитать снимок. Он приходит, когда кто-то удалил свои данные или изменений накопилось больше, чем помнит сервер.
Формы изменений каждой операции — в справочнике хранилища. SDK делает всё это сам.
Запись#
await pairx.call("storage.write", { op: "log.append", store: "queue", value: "join" });Ответ приходит, когда сервер принял запись. Если связь пропала посреди записи, Pairx сам повторяет её один раз — сервер узнаёт повтор и не применяет запись дважды. Если и повтор не прошёл, вызов завершится ошибкой offline: проверьте по ленте, дошла ли запись, прежде чем повторять её.
Документ и конфликты#
doc — один документ на экземпляр. Запись называет версию, которую прочитала; если кто-то успел записать раньше, сервер отвечает conflict. Тогда перечитайте снимок, примените своё изменение к новой версии и повторите:
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;
}
}
}Храните в документе изменение как функцию от текущего состояния («добавить пункт», «отметить пункт»), а не готовый результат — тогда повтор не затрёт чужую правку. Так сделано в примере «Общий список».
Права на запись#
writers | Кто пишет |
|---|---|
everyone | Все участники (по умолчанию). |
admins | Тот, кто отправил приложение в чат, и администраторы чата. |
author | Только тот, кто отправил приложение в чат. |
Права проверяет сервер. «Автор» — тот, кто отправил приложение в этот чат.
Пределы#
| Что | Предел |
|---|---|
| Значение слота или документа | 16 КиБ |
| Запись журнала | 4 КиБ |
| Записей в журнале | 10 000 |
| Ключей в счётчике | 1 000 |
| Весь экземпляр | 1 МиБ |
Личные данные (localStorage) | 256 КиБ |
| Хранилищ в манифесте | 16 |
При превышении запись отклоняется с кодом too_large или rate_limited; данные не теряются.
Удаление данных#
Человек может удалить свои данные в этом экземпляре из меню приложения: его значения в slot, вклад в счётчики и личные данные удаляются, а записи журнала становятся убранными (остаются на своих местах без значения). Общий документ остаётся как есть — он не принадлежит одному человеку. Остальные участники получат storage.reset.