---
metadata:
  - name: generator
    content: Diplodoc Platform v5.63.0
  - property: og:type
    content: article
  - property: article:section
    content: Платформа плагинов
  - property: og:title
    content: Хранилище данных плагина
  - property: article:tag
    content: Техническая инструкция
alternate:
  - https://www.yandex.ru/support/tracker/en/plugins/storage.md
  - https://www.yandex.ru/support/tracker/ru/plugins/storage.md
  - href: https://www.yandex.ru/support/tracker/ru/plugins/storage.md
    type: text/markdown
    title: Markdown version
  - href: https://www.yandex.ru/support/tracker/ru/llms.txt
    rel: describedby
---
> **Documentation Index:** Fetch the complete configuration index at https://www.yandex.ru/support/tracker/ru/llms.txt


# Хранилище данных плагина

Помимо взаимодействия с [публичным API Трекера](https://www.yandex.ru/support/tracker/ru/plugins/trackerApi.md), плагину доступно собственное JSON-хранилище — `storageApi`. Оно работает на стороне платформы, поэтому данные не зависят от устройства пользователя и доступны во всех вкладках, где открыт плагин.

Хранилище удобно для настроек плагина, кешей справочников и любых данных, которые не нужно сохранять в Трекере как сущность. Область видимости записи можно ограничить организацией, текущим пользователем или отдельным ресурсом Трекера.

## Контексты {#contexts}

Каждая запись хранится в **контексте** — он определяет область видимости данных.

Доступны следующие контексты:

- **`orgShared`** — данные общие на всю организацию. Запись видят все пользователи плагина в этой организации. Подходит для общих настроек плагина и кешей справочников.
- **`user`** — персональные данные текущего пользователя. Идентификатор пользователя передавать не нужно: платформа определяет его по авторизованной сессии. Подходит для личных настроек интерфейса, выбранных фильтров и состояния элементов.
- **`resource`** — данные конкретного ресурса Трекера. Ресурс задается обязательным параметром `resourceId`, например `issue:MYQUEUE-123`, `queue:MYQUEUE`, `board:42`, `project:<идентификатор>`, `portfolio:<идентификатор>` или `goal:<идентификатор>`. Запись общая для пользователей, которые обращаются к тому же ресурсу.

Во всех контекстах право текущего пользователя изменять прочитанную запись указано в поле `canWrite`.

{% note warning "Общая запись" %}

Контекст `orgShared` — это **общая** запись на всю организацию. Не сохраняйте в ней персональные данные пользователя и помните, что параллельные процессы записи видят изменения друг друга. Для совместной работы используйте [версионирование](#versioning) и merge-семантику `patch`.

{% endnote %}

{% note warning "Ресурсная запись" %}

Контекст `resource` не привязан к пользователю. Все процессы записи с одинаковыми `resourceId` и `bucket` видят изменения друг друга. Передавайте канонический идентификатор ресурса с типом: `issue:<ключ>`, `queue:<ключ>`, `board:<идентификатор>`, `project:<идентификатор>`, `portfolio:<идентификатор>` или `goal:<идентификатор>`.

{% endnote %}

{% note warning "Не храните чувствительные данные" %}

`storageApi` не является защищенным хранилищем секретов. Не сохраняйте в нем токены, пароли, ключи доступа и другие чувствительные данные независимо от выбранного контекста.

{% endnote %}

## Базовое использование {#basic-usage}

Объект `storageApi` экспортируется из SDK:

```typescript
import { storageApi } from '@weavix/tracker-plugin-sdk-react';
// или
import { storageApi } from '@weavix/tracker-plugin-sdk-core';

// Чтение
const record = await storageApi.orgShared.get('settings');
// record: { data: { theme: 'dark' }, version: 5, canWrite: true, ... } | null

// Создание записи на пустом бакете
await storageApi.orgShared.patch({
    bucket: 'settings',
    data: { theme: 'dark', notifications: true },
    version: 0,
});

// Обновление без знания текущей версии — SDK сам прочитает ее и повторит запрос при конфликтах
await storageApi.orgShared.patch({
    bucket: 'settings',
    data: { count: 42 },
});

// Удаление отдельного поля — передайте null
await storageApi.orgShared.patch({
    bucket: 'settings',
    data: { theme: null },
});

// Персональные настройки текущего пользователя
await storageApi.user.patch({
    bucket: 'preferences',
    data: { compactMode: true },
});

// Данные, связанные с конкретной задачей
await storageApi.resource.patch({
    resourceId: 'issue:MYQUEUE-123',
    bucket: 'panel',
    data: { collapsed: true },
});
```

## Бакеты {#buckets}

Внутри одного контекста плагин может держать несколько независимых записей — каждая идентифицируется параметром `bucket`. Это просто строковый ключ. Запись с одним бакетом не пересекается с записью под другим.

```typescript
await storageApi.orgShared.patch({ bucket: 'settings', data: { theme: 'dark' } });
await storageApi.orgShared.patch({ bucket: 'cache', data: { fetchedAt: Date.now() } });
```

Если `bucket` не передан, платформа подставит `'default'`. Ключ некорректного формата или длины приведет к ошибке [`BAD_KEY`](#errors).

## API {#api}

### storageApi.orgShared.get(bucket?) {#storage-api-org-shared-get}

Возвращает текущую запись по бакету или `null`, если записи еще нет.

**Параметры:**

| Параметр | Тип                   | По умолчанию                  | Описание                     |
| -------- | --------------------- | ----------------------------- | ---------------------------- |
| `bucket` | `string | undefined` | Платформа подставляет `'default'` | Ключ записи внутри контекста |

**Возвращает:** `Promise<StorageRecord | null>`

```typescript
const record = await storageApi.orgShared.get('settings');

if (record) {
    console.log(record.data); // содержимое записи
    console.log(record.version); // текущая версия (для оптимистических обновлений)
    console.log(record.canWrite); // есть ли у текущего пользователя право писать
} else {
    // записи еще нет — ее можно создать через patch с version: 0
}
```

### storageApi.orgShared.patch(options) {#storage-api-org-shared-patch}

Merge-patch: поля из `data` накладываются на текущую запись поверх, значение `null` удаляет ключ. Возвращает полный объединенный `StorageRecord` с новой версией — включая поля, дописанные параллельными процессами записи.

**Параметры:**

| Параметр  | Тип                       | По умолчанию                 | Описание                       |
| --------- | ------------------------- | ---------------------------- | ------------------------------ |
| `bucket`  | `string | undefined`      | Платформа подставляет `'default'` | Ключ записи внутри контекста         |
| `data`    | `Record<string, unknown>` | —                            | Merge-doc; `null` удаляет поле |
| `version` | `number | undefined`     | auto-resolve                 | Ожидаемая текущая версия       |

**Возвращает:** `Promise<StorageRecord>` — полная запись после применения патча.

```typescript
const updated = await storageApi.orgShared.patch({
    bucket: 'settings',
    data: { theme: 'dark' },
});

updated.version; // версия после патча
updated.data; // полное содержимое записи (включая чужие поля)
```

### storageApi.user.get(bucket?) {#storage-api-user-get}

Возвращает персональную запись текущего пользователя по бакету или `null`, если записи еще нет. Платформа определяет пользователя по авторизованной сессии, поэтому идентификатор пользователя в метод не передается.

Параметры и возвращаемое значение совпадают с [`storageApi.orgShared.get`](#storage-api-org-shared-get).

```typescript
const record = await storageApi.user.get('preferences');

if (record) {
    console.log(record.data); // настройки только текущего пользователя
}
```

### storageApi.user.patch(options) {#storage-api-user-patch}

Применяет merge-patch к персональной записи текущего пользователя. Параметры, версионирование и возвращаемое значение совпадают с [`storageApi.orgShared.patch`](#storage-api-org-shared-patch).

```typescript
const updated = await storageApi.user.patch({
    bucket: 'preferences',
    data: { compactMode: true },
});
```

### storageApi.resource.get(options) {#storage-api-resource-get}

Возвращает запись конкретного ресурса Трекера или `null`, если записи еще нет.

**Параметры:**

| Параметр    | Тип                   | По умолчанию                      | Описание                                    |
| ----------- | --------------------- | --------------------------------- | ------------------------------------------- |
| `resourceId` | `string`             | —                                 | Канонический идентификатор ресурса Трекера  |
| `bucket`    | `string | undefined` | Платформа подставляет `'default'` | Ключ записи внутри контекста                 |

**Возвращает:** `Promise<StorageRecord | null>`

```typescript
const record = await storageApi.resource.get({
    resourceId: 'issue:MYQUEUE-123',
    bucket: 'panel',
});
```

### storageApi.resource.patch(options) {#storage-api-resource-patch}

Применяет merge-patch к записи конкретного ресурса. В отличие от `orgShared` и `user`, параметр `resourceId` обязателен в каждом вызове.

**Параметры:**

| Параметр    | Тип                       | По умолчанию                      | Описание                                    |
| ----------- | ------------------------- | --------------------------------- | ------------------------------------------- |
| `resourceId` | `string`                 | —                                 | Канонический идентификатор ресурса Трекера  |
| `bucket`    | `string | undefined`     | Платформа подставляет `'default'` | Ключ записи                                 |
| `data`      | `Record<string, unknown>` | —                                 | Merge-doc; `null` удаляет поле              |
| `version`   | `number | undefined`     | auto-resolve                      | Ожидаемая текущая версия                    |

**Возвращает:** `Promise<StorageRecord>` — полная запись после применения патча.

```typescript
const updated = await storageApi.resource.patch({
    resourceId: 'issue:MYQUEUE-123',
    bucket: 'panel',
    data: { collapsed: false },
});
```

## Версионирование {#versioning}

Хранилище работает по схеме оптимистических обновлений: у каждой записи есть `version`, и `patch` должен передавать ожидаемую версию. Если в хранилище уже есть более свежая версия, операция завершается с ошибкой [`VERSION_CONFLICT`](#errors).

Поведение `patch` зависит от того, передаете ли вы `version`:

- **С явным `version`** — отправляется один запрос. Любая ошибка, включая `VERSION_CONFLICT`, пробрасывается вызывающему. Используйте этот режим, если приложение само держит актуальную версию и должно реагировать на конфликт — например, показать пользователю «данные изменились, перечитайте». Для создания записи на пустом бакете передавайте `version: 0`.

- **Без `version`** — SDK сначала читает текущую версию через `get` (для пустого бакета берется `0`), затем выполняет `patch`. На `VERSION_CONFLICT` цикл повторяется до двух раз, каждый повтор заново читает версию. Любая другая ошибка пробрасывается мгновенно. В худшем случае SDK обращается к платформе 6 раз: выполняет 3 пары запросов GET и PATCH. При каждом повторе в консоль записывается `console.warn`.

{% note info "Когда какой режим выбирать" %}

- Передавайте `version` явно, если вы загрузили запись на экран и пользователь ее редактирует — так конфликт с другим писателем не будет потерян молча.
- Не передавайте `version`, если патч идемпотентен и его значения не зависят от текущего содержимого записи — например, устанавливаете отдельный флаг в заранее известное значение. Для счетчиков и других операций read-modify-write после конфликта перечитайте запись и пересчитайте патч самостоятельно.

{% endnote %}

## Право на запись {#can-write}

Поле `canWrite` в `StorageRecord` показывает, может ли текущий пользователь делать `patch` в этой записи. Используйте его, чтобы заранее показать пользователю, что у него нет прав, не дожидаясь ошибки платформы.

```typescript
const record = await storageApi.orgShared.get('settings');

if (!record?.canWrite) {
    // спрятать или отключить элементы редактирования
}
```

## Коды ошибок {#errors}

Все ошибки `storageApi` — экземпляры `PluginActionError` с числовыми кодами. Константы экспортируются из SDK:

```typescript
import {
    VERSION_CONFLICT,
    DATA_TOO_LARGE,
    BAD_KEY,
} from '@weavix/tracker-plugin-sdk-react';
```

| Константа          | Код    | Когда                                                       |
| ------------------ | ------ | ----------------------------------------------------------- |
| `VERSION_CONFLICT` | `1010` | Переданный `version` не совпал с текущим                    |
| `DATA_TOO_LARGE`   | `1011` | Размер записи после операции превысил 256 KiB               |
| `BAD_KEY`          | `1012` | `bucket` не прошел валидацию формата или длины              |

Общие коды ошибок API (валидация, scope и т.д.) — в разделе [Коды ошибок API](https://www.yandex.ru/support/tracker/ru/plugins/trackerApi.md#error-codes).

**Пример обработки `VERSION_CONFLICT`:**

```typescript
import {
    storageApi,
    PluginActionError,
    VERSION_CONFLICT,
} from '@weavix/tracker-plugin-sdk-react';

try {
    await storageApi.orgShared.patch({
        bucket: 'settings',
        data: { theme: 'dark' },
        version: knownVersion,
    });
} catch (e) {
    if (e instanceof PluginActionError && e.code === VERSION_CONFLICT) {
        // данные изменились — перечитайте запись и предложите пользователю объединить изменения
        const fresh = await storageApi.orgShared.get('settings');
        // ...
    } else {
        throw e;
    }
}
```

## Типы {#types}

```typescript
import type {
    StorageRecord,
    StorageContextType,
    StorageGetPayload,
    StoragePatchPayload,
} from '@weavix/tracker-plugin-sdk-react';
```

### StorageRecord {#storage-record}

Запись в хранилище.

```typescript
type StorageRecord = {
    /** Внутренний составной ключ записи (контекст + бакет). */
    key: Array<
        | { organization_shared: string }
        | { user: string; bucket: string }
        | { resource_id: string; service_type: string; bucket: string }
    >;
    /** Версия записи. Передавайте ее в patch для оптимистических обновлений. */
    version: number;
    /** Полезные данные записи. */
    data: Record<string, unknown>;
    /** Может ли текущий пользователь делать patch. */
    canWrite: boolean;
    /** Дата создания записи (ISO 8601). */
    createdAt: string;
    /** Дата последнего обновления (ISO 8601). */
    updatedAt: string;
};
```

### StorageContextType {#storage-context-type}

```typescript
type StorageContextType = 'orgShared' | 'user' | 'resource';
```

## Ограничения {#limits}

По умолчанию действуют следующие квоты:

| Что ограничивается | Область действия | Лимит | Ошибка при превышении |
| ------------------ | ---------------- | ----: | --------------------- |
| Размер одной записи после применения операции | Один бакет | 256 KiB | `DATA_TOO_LARGE` |
| Общий объем данных | Все контексты и бакеты одного плагина в организации | 100 MiB | `STORAGE_QUOTA_EXCEEDED` |
| Количество бакетов `orgShared` | Один плагин в организации | 2000 | `BUCKET_LIMIT_EXCEEDED` |
| Количество бакетов `user` | Один плагин для каждого пользователя в организации | 100 | `BUCKET_LIMIT_EXCEEDED` |
| Количество бакетов `resource` | Один плагин для каждого ресурса в организации | 200 | `BUCKET_LIMIT_EXCEEDED` |

- Неиспользуемая запись в бакете хранится 3 календарных года.
- Длина `bucket` — от 1 до 64 символов. Разрешены латинские буквы, цифры, точка, дефис и подчеркивание. Невалидный ключ — `BAD_KEY`.
- В контексте `resource` параметр `resourceId` обязателен, не может быть пустым и должен быть не длиннее 256 символов.
- `patch` без явной `version` делает до 2 повторов на `VERSION_CONFLICT`. Дальше ошибка пробрасывается вызывающему.
