Pairxдля разработчиков

Общие данные

Все участники чата открывают один экземпляр приложения. Его общие данные хранит 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 записи.

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

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

  1. storage.snapshot — всё состояние на одном seq (номере последнего изменения).
  2. Событие 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.