SHA256
Сохранить текущий снимок DM-синхронизации
This commit is contained in:
@@ -62,11 +62,12 @@
|
||||
| `UpsertPushToken` | `12_Direct_Messages_Push_Calls_API.md` | регистрация WebPush-токена |
|
||||
| `SendTestWebPush` | `12_Direct_Messages_Push_Calls_API.md` | тестовая push-доставка |
|
||||
| `SendMessagePair` | `12_Direct_Messages_Push_Calls_API.md` | отправка пары входящий/исходящий DM |
|
||||
| `ReceiveOutcomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | алиас `SendMessagePair` |
|
||||
| `ReceiveOutcomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | старая полная DM-пара для второго access-сервера отправителя |
|
||||
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
|
||||
| `DeleteMessage` | `12_Direct_Messages_Push_Calls_API.md` | tombstone одного личного сообщения у обеих сторон |
|
||||
| `DeleteConversation` | `12_Direct_Messages_Push_Calls_API.md` | tombstone удаления истории переписки |
|
||||
| `DmSyncBatch` | `12_Direct_Messages_Push_Calls_API.md` | межсерверная догоняющая синхронизация DM по курсору |
|
||||
| `DmSyncBatch` | `12_Direct_Messages_Push_Calls_API.md` | pull событий `synced=false` с ACK предыдущей страницы |
|
||||
| `GetDmDeliveryStatus` | `12_Direct_Messages_Push_Calls_API.md` | read-only статус доставки по существующему `messageKey` |
|
||||
| `UserSettingsSyncBatch` | `13_User_Settings_API.md` | межсерверная догоняющая синхронизация пользовательских настроек по курсору |
|
||||
| `MarkAllUserSettingsUnsynced` | `13_User_Settings_API.md` | служебная пометка всех настроек как несинхронизированных |
|
||||
| `GetDirectMessages` | `12_Direct_Messages_Push_Calls_API.md` | постраничная загрузка истории личного диалога |
|
||||
@@ -76,7 +77,8 @@
|
||||
|
||||
## Важные замечания
|
||||
|
||||
- `ReceiveOutcomingMessage` сейчас зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`.
|
||||
- `ReceiveOutcomingMessage` зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`, и сохраняет прежний межсерверный payload.
|
||||
- Межсерверные DM-операции пока доверяют `sourceServerLogin`; отдельная межсерверная авторизация запланирована позднее.
|
||||
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
|
||||
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
|
||||
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.
|
||||
|
||||
@@ -107,12 +107,17 @@
|
||||
"baseKey": "from|to|time|nonce",
|
||||
"incomingKey": "from|to|time|nonce|1",
|
||||
"outgoingKey": "from|to|time|nonce|2",
|
||||
"deliveryState": "accepted",
|
||||
"deliveredWsSessions": 1,
|
||||
"deliveredWebPushSessions": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Успешный `status=200` подтверждает локальное сохранение. До ответа клиенту сервер параллельно пробует оба актуальных сервера получателя. Поэтому `deliveryState` уже может быть `delivered`; если никто не ответил, возвращается `accepted`.
|
||||
|
||||
Возможные `deliveryState`: `accepted`, `delivered`, `failed`.
|
||||
|
||||
### Ошибки
|
||||
|
||||
- `400 / BAD_FIELDS` — пустой `incomingBlobB64` или `outgoingBlobB64`
|
||||
@@ -147,7 +152,9 @@
|
||||
}
|
||||
```
|
||||
|
||||
`sourceServerLogin` необязателен. Если поле есть, сервер использует его как подсказку, чтобы не отправлять событие обратно серверу-источнику.
|
||||
`sourceServerLogin` необязателен для совместимости. Пока межсерверная авторизация отложена, это поле считается доверенным. Пользовательская подпись signed-блока проверяется всегда.
|
||||
|
||||
Успешный ответ содержит существующие `messageKey`, `baseKey` и счётчики realtime-доставки. Повтор уже сохранённой той же ревизии обрабатывается идемпотентно.
|
||||
|
||||
### Примечание
|
||||
|
||||
@@ -243,6 +250,7 @@
|
||||
"reencryptedAtMs": 0,
|
||||
"createdAtMs": 1774700001123,
|
||||
"readAtMs": 1774700001456,
|
||||
"deliveryState": "delivered",
|
||||
"blobB64": "BASE64_SIGNED_BLOCK"
|
||||
}
|
||||
]
|
||||
@@ -252,11 +260,50 @@
|
||||
|
||||
Для следующей страницы клиент должен передать `nextBeforeTimeMs` и `nextBeforeMessageKey` из предыдущего ответа.
|
||||
|
||||
## 8. `DmSyncBatch`
|
||||
Поле `deliveryState` заполняется для исходящих элементов `type=2`. У входящих `type=1` оно отсутствует/равно `null`.
|
||||
|
||||
Межсерверная операция для догоняющей синхронизации истории одного пользователя. В текущей реализации не требует авторизации сервера-источника, но удалённый сервер отдаёт данные только если сам является access-сервером `ownerLogin` по `user_access_servers_current`.
|
||||
## 8. Межсерверные операции DM
|
||||
|
||||
### Запрос
|
||||
До отдельного этапа server-auth поля `sourceServerLogin` доверяются. Каждый `SHiNE_DM` всё равно заново проходит проверку формата и пользовательской подписи.
|
||||
|
||||
### 8.1. `ReceiveOutcomingMessage`
|
||||
|
||||
Прежняя операция передачи полной пары второму access-серверу отправителя. Delivery-state в межсерверный запрос не входит.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "ReceiveOutcomingMessage",
|
||||
"requestId": "dm-peer-001",
|
||||
"payload": {
|
||||
"incomingBlobB64": "BASE64_INCOMING",
|
||||
"outgoingBlobB64": "BASE64_OUTGOING",
|
||||
"sourceServerLogin": "server-a"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ имеет прежний формат `SendMessagePair`. Принимающий сервер сохраняет пару идемпотентно и самостоятельно ставит локальную задачу доставки.
|
||||
|
||||
### 8.2. `ReceiveIncomingMessage`
|
||||
|
||||
Прежняя операция передачи одной входящей копии серверу получателя или второму серверу самого получателя.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "ReceiveIncomingMessage",
|
||||
"requestId": "dm-incoming-001",
|
||||
"payload": {
|
||||
"incomingBlobB64": "BASE64_INCOMING",
|
||||
"sourceServerLogin": "server-a"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Успешный 2xx-ответ означает, что signed-блок проверен и находится в БД (новая или идемпотентно повторённая запись).
|
||||
|
||||
### 8.3. `DmSyncBatch`
|
||||
|
||||
Pull-синхронизация событий владельца с `synced=false`. `ackSyncIds` подтверждает версии событий, успешно сохранённые из предыдущего ответа.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -264,55 +311,58 @@
|
||||
"requestId": "dm-sync-001",
|
||||
"payload": {
|
||||
"ownerLogin": "alice",
|
||||
"afterStoredAtMs": 1774700000000,
|
||||
"afterMessageKey": "alice|bob|1774699999000|123456780|2",
|
||||
"afterStoredAtMs": 0,
|
||||
"afterMessageKey": "",
|
||||
"limit": 500,
|
||||
"maxBytes": 3000000
|
||||
"maxBytes": 3000000,
|
||||
"ackSyncIds": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`afterStoredAtMs` и `afterMessageKey` образуют курсор. Если курсора нет, сервер передаёт `0` и пустую строку. `limit` ограничен максимумом `500`.
|
||||
|
||||
### Успешный ответ
|
||||
Ответ возвращает страницу несинхронизированных событий и курсор внутри текущего цикла:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "DmSyncBatch",
|
||||
"requestId": "dm-sync-001",
|
||||
"status": 200,
|
||||
"ok": true,
|
||||
"nextStoredAtMs": 1774700001123,
|
||||
"nextMessageKey": "alice|bob|1774700000123|123456789|2",
|
||||
"hasMore": false,
|
||||
"items": [
|
||||
{
|
||||
"syncId": "alice|bob|1774700000123|123456789|2:0:0",
|
||||
"primaryMessageKey": "alice|bob|1774700000123|123456789|2",
|
||||
"storedAtMs": 1774700001123,
|
||||
"blobsB64": ["BASE64_INCOMING", "BASE64_OUTGOING"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Для пары порядок всегда incoming/outgoing; для входящей копии и tombstone массив содержит один blob. `syncId` — технический идентификатор конкретной ревизии только для ACK синхронизации, а не второй ID сообщения. После сохранения вызывающий сервер передаёт `syncId` в `ackSyncIds` следующего запроса. Только тогда источник ставит `synced=true`. Новый цикл начинается с `afterStoredAtMs=0`; полный сброс флагов поэтому повторно отдаёт всю историю.
|
||||
|
||||
### 8.4. `GetDmDeliveryStatus`
|
||||
|
||||
Единственная новая межсерверная операция доставки. Read-only проверка существующего `messageKey`; не изменяет состояние отвечающего сервера.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "GetDmDeliveryStatus",
|
||||
"requestId": "dm-status-001",
|
||||
"payload": {
|
||||
"ownerLogin": "alice",
|
||||
"limit": 500,
|
||||
"rawBytes": 84512,
|
||||
"hasMore": true,
|
||||
"nextStoredAtMs": 1774700100000,
|
||||
"nextMessageKey": "alice|bob|1774700000123|123456789|1",
|
||||
"items": [
|
||||
{
|
||||
"messageKey": "alice|bob|1774700000123|123456789|1",
|
||||
"baseKey": "alice|bob|1774700000123|123456789",
|
||||
"targetLogin": "alice",
|
||||
"fromLogin": "bob",
|
||||
"toLogin": "alice",
|
||||
"messageType": 1,
|
||||
"timeMs": 1774700000123,
|
||||
"storedAtMs": 1774700100000,
|
||||
"blobB64": "BASE64_SIGNED_BLOCK"
|
||||
}
|
||||
]
|
||||
"messageKey": "alice|bob|1774700000123|123456789|2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
События в `items` идут по `storedAtMs ASC, messageKey ASC`. В пачке могут быть сообщения любых диалогов пользователя, read-receipt и delete/tombstone типов `5/6/7/8`.
|
||||
Ответ содержит `messageKey`, `known` и `delivered`. `delivered=true` означает доставку хотя бы одному серверу получателя.
|
||||
|
||||
Ошибки:
|
||||
Периодический процесс использует одно WS-соединение с peer: сначала синхронизирует настройки, затем вызывает `DmSyncBatch` до завершения страниц и ACK. Существующий `MarkAllUserSettingsUnsynced` также сбрасывает DM-флаги и возвращает `dmUpdated`; отдельной операции `MarkAllDmUnsynced` нет.
|
||||
|
||||
- `400 / EMPTY_OWNER_LOGIN` — не передан `ownerLogin`
|
||||
- `403 / LOCAL_SERVER_NOT_ACCESS_SERVER` — этот сервер не является access-сервером пользователя
|
||||
- `500 / LOCAL_SERVER_NOT_CONFIGURED` — не настроен `server.SHiNE.login`
|
||||
Основные ошибки межсерверных операций:
|
||||
|
||||
- `400 / EMPTY_OWNER_LOGIN`, `EMPTY_MESSAGE_KEY`;
|
||||
- `403 / LOCAL_SERVER_NOT_ACCESS_SERVER`;
|
||||
- `500 / LOCAL_SERVER_NOT_CONFIGURED`.
|
||||
|
||||
## 9. `AckSessionDelivery`
|
||||
|
||||
@@ -355,6 +405,25 @@
|
||||
|
||||
Для типов `5/6/7/8` событие тоже приходит в таком же конверте, но логика применения определяется `messageType` и бинарным `blobB64`.
|
||||
|
||||
## 10.1. Событие `DmDeliveryStateChanged`
|
||||
|
||||
Сервер отправляет событие активным сессиям отправителя при изменении сетевого состояния исходящей пары.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "DmDeliveryStateChanged",
|
||||
"event": true,
|
||||
"status": 200,
|
||||
"payload": {
|
||||
"baseKey": "alice|bob|1774700000123|123456789",
|
||||
"outgoingKey": "alice|bob|1774700000123|123456789|2",
|
||||
"deliveryState": "delivered"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`failed` приходит после последней неудачной попытки через час. Событие прочтения остаётся отдельным существующим read-receipt.
|
||||
|
||||
## 11. `CallInviteBroadcast`
|
||||
|
||||
Требует авторизации. Шлёт приглашение к звонку в активные сессии `toLogin`.
|
||||
|
||||
@@ -77,15 +77,17 @@
|
||||
Внутренний служебный запрос.
|
||||
|
||||
- помечает все настройки пользователя или все настройки сразу как `synced=false`;
|
||||
- одновременно сбрасывает DM-outbox того же пользователя (или всех пользователей), чтобы операция замены/добавления access-сервера не оставила историю DM несинхронизированной;
|
||||
- ответ дополнительно содержит `dmUpdated` — количество сброшенных DM-событий;
|
||||
- нужен после добавления нового sync-сервера или при потере локальной БД.
|
||||
|
||||
## 5. Синхронизация
|
||||
|
||||
Синхронизация настроек работает отдельно от DM.
|
||||
Настройки и DM имеют раздельные таблицы и правила ACK, но периодический процесс открывает один последовательный WS-сеанс с peer: сначала синхронизирует настройки, затем забирает `DmSyncBatch`.
|
||||
|
||||
- локальная запись создаётся с `synced=false`, если её ещё не подтвердил второй сервер;
|
||||
- если запись пришла с другого сервера, она сохраняется сразу как `synced=true`;
|
||||
- периодический sync раз в 6 часов проверяет несинхронизированные записи и догружает новые записи по курсору;
|
||||
- периодический sync раз в 6 часов проверяет несинхронизированные настройки, догружает их по курсору и затем забирает несинхронизированные DM;
|
||||
- если появляется новый sync-сервер или локальная БД была потеряна, нужно пометить все настройки несинхронизированными и заново догрузить batch с нуля.
|
||||
|
||||
## 6. Текущий UI-кейс
|
||||
|
||||
@@ -32,9 +32,11 @@
|
||||
### 3.1 Личные сообщения (DM)
|
||||
|
||||
- Все DM-блоки форматов типов `1/2` (текст) и `3/4` (read-receipt).
|
||||
- Сервер-отправитель: при получении пары блоков от клиента перенаправляет их серверу получателя.
|
||||
- Сервер-получатель: сохраняет блоки в `signed_messages_v2`, доставляет в активные сессии.
|
||||
- Сервер-отправитель: сохраняет пару и ставит асинхронную delivery-задачу.
|
||||
- Сервер-получатель: сохраняет входящий блок в `signed_messages`, затем доставляет его активным сессиям.
|
||||
- Дедупликация по уникальному `message_key = from|to|timeMs|nonce|type`.
|
||||
- Репликация между двумя access-серверами пользователя работает через `dm_sync_outbox.synced`, без постоянного time-cursor.
|
||||
- Полная актуальная схема: `docs/Personal_Messages/Доставка_и_синхронизация_DM.md`.
|
||||
|
||||
### 3.2 Блоки пользовательского блокчейна
|
||||
|
||||
@@ -181,7 +183,9 @@ Full resync запускается только тогда, когда:
|
||||
|
||||
Настройка влияет именно на этап подготовки отсутствующей локальной цепочки во время periodic sync.
|
||||
|
||||
## 5. Целевой протокол следующего этапа
|
||||
## 5. Возможное развитие server-to-server транспорта
|
||||
|
||||
Этот раздел не описывает текущую DM-доставку. DM уже использует короткие one-shot WebSocket-вызовы, indexed outbox и ACK. Ниже остаётся возможное развитие постоянного транспорта и server-auth.
|
||||
|
||||
### 5.1 Межсерверное соединение
|
||||
|
||||
@@ -192,14 +196,15 @@ Full resync запускается только тогда, когда:
|
||||
|
||||
### 5.2 Доставка новых данных (push)
|
||||
|
||||
- При получении нового блока или DM сервер немедленно пушит его всем подключённым партнёрам.
|
||||
- При получении нового блока сервер может немедленно пушить его всем подключённым партнёрам.
|
||||
- Партнёр подтверждает приём (ACK). Без ACK — повтор с backoff.
|
||||
- DM использует отдельное расписание, описанное в `docs/Personal_Messages/Доставка_и_синхронизация_DM.md`.
|
||||
|
||||
### 5.3 Начальная синхронизация (backfill)
|
||||
|
||||
- При первом подключении к партнёру серверы обмениваются «курсорами» состояния:
|
||||
последний глобальный номер блока, последний известный DM-ключ.
|
||||
- При первом подключении к партнёру серверы могут обмениваться курсорами состояния блокчейнов.
|
||||
- Сервер с более полной историей досылает недостающее партнёру.
|
||||
- DM time-cursor удалён: новый peer получает историю после сброса `dm_sync_outbox.synced=false`.
|
||||
|
||||
### 5.4 Разрешение конфликтов
|
||||
|
||||
@@ -212,19 +217,21 @@ Full resync запускается только тогда, когда:
|
||||
При отправке DM от пользователя A к пользователю B:
|
||||
|
||||
1. Клиент A отправляет пару блоков на свой сервер X.
|
||||
2. Сервер X определяет, на каком сервере зарегистрирован пользователь B.
|
||||
- Сначала проверяет локально (если B зарегистрирован на X).
|
||||
- Иначе читает PDA пользователя B из Solana и смотрит `access_servers`.
|
||||
- Выбирает первый доступный сервер из `access_servers` и перенаправляет туда DM.
|
||||
3. Сервер Y (из `access_servers` B) сохраняет и доставляет блоки.
|
||||
2. Сервер X валидирует и локально сохраняет пару.
|
||||
3. До ответа клиенту X параллельно отправляет входящую копию максимум двум актуальным `access_servers` B.
|
||||
4. ACK хотя бы одного маршрута B означает терминальный `delivered`; второй сервер B догоняется собственной DM-синхронизацией.
|
||||
5. После первой попытки X передаёт полную пару второму access-серверу A старым `ReceiveOutcomingMessage`, без delivery-state.
|
||||
6. При нулевой доставке выполняются повторы через 30 секунд, 5 минут, 25 минут и 1 час. Перед тремя последними X спрашивает peer A через `GetDmDeliveryStatus(messageKey)`.
|
||||
7. После неудачной попытки через час ставится терминальный `failed`.
|
||||
|
||||
Кэш адресов серверов: обновляется раз в сессию (при ошибке соединения).
|
||||
Изменения routing B учитываются до терминального состояния, потому что список маршрутов перечитывается на каждой попытке.
|
||||
|
||||
## 7. Безопасность
|
||||
|
||||
- Все блоки подписаны ключами пользователя на клиенте — сервер не может подделать содержимое.
|
||||
- Серверы не расшифровывают DM-контент (шифрование — задача следующего этапа).
|
||||
- Серверы не расшифровывают DM-контент; E2EE уже выполняется клиентами.
|
||||
- При синхронизации каждый блок проходит валидацию подписи на принимающем сервере.
|
||||
- Межсерверная авторизация DM-операций пока отложена; `sourceServerLogin` временно считается доверенным.
|
||||
|
||||
## 8. Статус реализации
|
||||
|
||||
@@ -240,15 +247,18 @@ Full resync запускается только тогда, когда:
|
||||
| Обход Solana RPC через `sync.importUserProfileFromPartner.enabled` | ✅ Реализовано |
|
||||
| Обычный `AddBlock` через `tmp_bch`/`write_check`/`write_pending` | ✅ Реализовано |
|
||||
| Межсерверный постоянный WebSocket-канал | Нужна реализация |
|
||||
| Push новых DM партнёрам | Нужна реализация |
|
||||
| Асинхронная доставка DM на access-серверы получателя | ✅ Реализовано |
|
||||
| Retry DM до 1 часа + UI-state | ✅ Реализовано |
|
||||
| Репликация DM на второй access-сервер по `synced` | ✅ Реализовано |
|
||||
| Read-only `GetDmDeliveryStatus` | ✅ Реализовано |
|
||||
| Push блоков блокчейна партнёрам | ✅ Реализована базовая one-shot версия |
|
||||
| Periodic backfill отсутствующего хвоста | ✅ Реализовано |
|
||||
| Разрешение рассинхрона / divergence | ✅ Реализована базовая full-resync схема во время periodic sync |
|
||||
| Startup recovery по `*.resync_pending` marker-file | ✅ Реализовано |
|
||||
| Маршрутизация DM через access_servers | Нужна реализация (заглушка) |
|
||||
| Маршрутизация DM через один/два `access_servers` | ✅ Реализовано |
|
||||
| Криптографическая server-to-server авторизация DM | Нужна реализация |
|
||||
|
||||
Текущая версия сервера уже умеет базовую синхронизацию блокчейнов между партнёрами.
|
||||
Не реализованы ещё DM-sync и постоянные server-to-server соединения.
|
||||
Текущая версия сервера умеет синхронизацию блокчейнов и DM. Постоянные server-to-server соединения не требуются для текущей one-shot WS-реализации; отдельной будущей задачей остаётся криптографическая авторизация DM-вызовов.
|
||||
|
||||
Следующие отдельные шаги после текущего этапа:
|
||||
- отдельно проверить full-resync и startup-recovery на реальном тестовом прогоне после ручного удаления БД/файлов.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md` — логика протокола, роли API, серверное поведение, routing по `access_servers`
|
||||
- `docs/Personal_Messages/Формат_DM_v1.md` — точный бинарный формат контейнера `SHiNE_DM`
|
||||
- `docs/Personal_Messages/Доставка_и_синхронизация_DM.md` — состояния доставки, retry-воркер, репликация между двумя access-серверами и UI-статусы
|
||||
- `docs/Personal_Messages/Технические_вставки_DM_v1.md` — формат специальных `<S:...>` вставок внутри plaintext DM после расшифровки
|
||||
|
||||
Исторический устаревший документ сохранён отдельно:
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# Доставка и синхронизация личных сообщений
|
||||
|
||||
## 1. Главный принцип
|
||||
|
||||
DM считается доставленным, когда signed-входящую копию сохранил хотя бы один актуальный access-сервер получателя. Доставка на оба сервера не требуется: сервер получателя самостоятельно синхронизирует входящее сообщение со своим вторым сервером.
|
||||
|
||||
Клиентский `status=200` от `SendMessagePair` означает, что собственный сервер сохранил пару. Это отдельный факт от доставки получателю.
|
||||
|
||||
Формат подписанного контейнера `SHiNE_DM` не меняется. Состояние доставки и флаг синхронизации — изменяемые серверные метаданные.
|
||||
|
||||
## 2. Идентификаторы
|
||||
|
||||
- `baseKey` связывает входящую и исходящую копии одного логического сообщения;
|
||||
- `incomingKey` идентифицирует копию получателя;
|
||||
- `outgoingKey`/`messageKey` идентифицирует копию отправителя и используется для проверки доставки;
|
||||
- дополнительный публичный `eventId` для доставки не создаётся;
|
||||
- `syncId` используется только внутри `DmSyncBatch` как ACK конкретной ревизии, потому что редактирование сохраняет прежний `messageKey`.
|
||||
|
||||
## 3. Состояния доставки
|
||||
|
||||
| Состояние | Смысл | Повторные попытки |
|
||||
|---|---|---|
|
||||
| `accepted` | пара сохранена сервером отправителя, но ни один сервер получателя ещё не подтвердил запись | да |
|
||||
| `delivered` | хотя бы один сервер получателя подтвердил запись | нет |
|
||||
| `failed` | последняя попытка через час завершилась без доставки | никогда |
|
||||
|
||||
Состояние `delivered` терминальное. Сервер отправителя не пытается отдельно добиться второго ACK получателя.
|
||||
|
||||
## 4. Обычная отправка
|
||||
|
||||
1. Клиент отправляет `SendMessagePair` на один свой access-сервер.
|
||||
2. Сервер проверяет формат, пользователей, подписи и согласованность пары.
|
||||
3. Сервер атомарно сохраняет входящую и исходящую копии.
|
||||
4. В том же request-процессе сервер читает до двух актуальных маршрутов получателя.
|
||||
5. Оба вызова `ReceiveIncomingMessage` запускаются параллельно.
|
||||
6. Если хотя бы один вызов успешен, устанавливается `delivered`.
|
||||
7. После этой попытки сервер передаёт полную пару своему второму access-серверу старой операцией `ReceiveOutcomingMessage`.
|
||||
8. `SendMessagePair` возвращает существующие ключи и единственное новое поле `deliveryState`.
|
||||
|
||||
Успешный повтор уже сохранённого signed-блока считается ACK. Все операции должны быть идемпотентными.
|
||||
|
||||
## 5. Расписание повторов
|
||||
|
||||
Воркер запускается каждые 5 секунд и выбирает только due-строки по индексу. Он не сканирует всю таблицу сообщений.
|
||||
|
||||
Попытки привязаны к времени первоначального принятия:
|
||||
|
||||
| Номер | Время от старта | Сначала спросить второй сервер отправителя |
|
||||
|---:|---:|---|
|
||||
| 1 | сразу | нет |
|
||||
| 2 | 30 секунд | нет |
|
||||
| 3 | 5 минут | да |
|
||||
| 4 | 25 минут | да |
|
||||
| 5 | 1 час | да |
|
||||
|
||||
Из-за шага воркера повтор может начаться на несколько секунд позже указанного времени. Первая попытка выполняется немедленно и от воркера не зависит.
|
||||
|
||||
На 5-й, 25-й и 60-й минутах сервер сначала вызывает у второго сервера отправителя:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "GetDmDeliveryStatus",
|
||||
"payload": {
|
||||
"messageKey": "alice|bob|1774700000123|123456789|2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"messageKey": "alice|bob|1774700000123|123456789|2",
|
||||
"known": true,
|
||||
"delivered": true
|
||||
}
|
||||
```
|
||||
|
||||
Операция read-only. Если peer отвечает `delivered=true`, локальный сервер устанавливает `delivered` и не обращается к серверам получателя. Ошибка или отсутствие операции на старом peer не блокирует собственную попытку.
|
||||
|
||||
Перед каждой попыткой маршруты получателя заново читаются из `user_access_servers_current`. Изменение серверов учитывается только пока сообщение находится в `accepted`. После `delivered` или `failed` состояние больше не открывается.
|
||||
|
||||
Если последняя проверка и попытка через час не дали ACK, устанавливается `failed`, `next_attempt_at_ms` очищается и сообщение больше никогда автоматически не отправляется.
|
||||
|
||||
## 6. Старая межсерверная доставка
|
||||
|
||||
Форматы существующих операций не расширяются данными о результате доставки:
|
||||
|
||||
- `ReceiveOutcomingMessage` получает прежнюю пару `incomingBlobB64` + `outgoingBlobB64` и необязательный `sourceServerLogin`;
|
||||
- `ReceiveIncomingMessage` получает один `incomingBlobB64` и необязательный `sourceServerLogin`.
|
||||
|
||||
Второй сервер отправителя после получения пары создаёт собственное локальное состояние доставки и самостоятельно пробует маршруты получателя. Серверы обмениваются результатом только через read-only `GetDmDeliveryStatus`.
|
||||
|
||||
## 7. Догоняющая синхронизация двух серверов пользователя
|
||||
|
||||
Для каждого владельца сервер ведёт outbox с флагом `synced`:
|
||||
|
||||
- исходящая пара отправителя — один элемент с двумя blob в порядке incoming/outgoing;
|
||||
- входящая копия получателя — один элемент с одним blob;
|
||||
- read-receipt и tombstone применяются теми же проверенными обработчиками.
|
||||
|
||||
`DmSyncBatch` возвращает только элементы с `synced=false`. Получатель проверяет signed-блоки, сохраняет их идемпотентно и в следующем запросе подтверждает `syncId` через `ackSyncIds`. Источник ставит `synced=true` только после ACK.
|
||||
|
||||
При обрыве соединения неподтверждённый элемент остаётся `synced=false` и безопасно приходит повторно. Элемент, полученный от peer, локально сразу считается синхронизированным, чтобы не образовалась петля.
|
||||
|
||||
Синхронизация настроек и DM проходит последовательно через один WS-сеанс: сначала настройки, затем все страницы DM. Если второй сервер был выключен, после включения он сам догружает пропущенные элементы.
|
||||
|
||||
Существующий `MarkAllUserSettingsUnsynced` также сбрасывает DM-флаги. После сброса история повторно передаётся как синхронизация, но старые сообщения получателю заново не отправляются: delivery-состояние создаётся с учётом их возраста и не открывает завершённую часовую очередь.
|
||||
|
||||
## 8. UI
|
||||
|
||||
| Вид | Значение |
|
||||
|---|---|
|
||||
| одна серая галочка | собственный сервер принял сообщение (`accepted`) |
|
||||
| одна светлая галочка | хотя бы один сервер получателя принял сообщение (`delivered`) |
|
||||
| две светлые галочки | пришёл существующий read-receipt |
|
||||
| красный `!` и «Сообщение не доставлено» | окончательное состояние `failed` |
|
||||
|
||||
Read-receipt имеет приоритет над delivery-индикатором: если сообщение прочитано, оно заведомо было доставлено.
|
||||
|
||||
Сервер сообщает поздние изменения через `DmDeliveryStateChanged` с полями `outgoingKey`, `baseKey`, `deliveryState`. При повторном открытии чата то же состояние приходит в `GetDirectMessages`.
|
||||
|
||||
## 9. Нагрузка и отказоустойчивость
|
||||
|
||||
- воркер выбирает только due-записи по частичному индексу;
|
||||
- терминальные строки не попадают в рабочую выборку;
|
||||
- два маршрута первой попытки выполняются параллельно;
|
||||
- ограниченный пул потоков и очередь защищают сервер при всплеске отправок;
|
||||
- сетевой lease и сравнение версии строки предотвращают одновременную обработку одной задачи несколькими worker-потоками;
|
||||
- повторная запись одного signed-блока безопасна;
|
||||
- отсутствие второго сервера отправителя не мешает собственной доставке;
|
||||
- отсутствие обоих серверов получателя завершает задачу через час.
|
||||
|
||||
## 10. Граница доверия
|
||||
|
||||
Межсерверная авторизация пока отложена. `sourceServerLogin` временно принимается на доверии, но каждый контейнер `SHiNE_DM` всё равно проходит проверку пользовательской подписи. `GetDmDeliveryStatus` сообщает только факт локального delivery-state и не изменяет данные.
|
||||
@@ -26,6 +26,10 @@
|
||||
|
||||
- `docs/Personal_Messages/Технические_вставки_DM_v1.md`
|
||||
|
||||
Изменяемое состояние доставки, retry-расписание и репликация между двумя access-серверами подробно описаны отдельно:
|
||||
|
||||
- `docs/Personal_Messages/Доставка_и_синхронизация_DM.md`
|
||||
|
||||
Устаревшая предыдущая версия сохранена отдельно:
|
||||
|
||||
- `docs/Personal_Messages/Спецификация_DM_v0.5_устаревшая.md`
|
||||
@@ -300,12 +304,17 @@
|
||||
|
||||
`sync_servers` не являются списком пользовательских серверов доставки DM.
|
||||
|
||||
### 7.3. Несколько серверов у отправителя и получателя
|
||||
### 7.3. Два сервера у отправителя и получателя
|
||||
|
||||
Протокол должен поддерживать ситуацию, когда:
|
||||
Актуальный runtime исходит из ограничения:
|
||||
|
||||
- у отправителя несколько `access_servers`;
|
||||
- у получателя несколько `access_servers`;
|
||||
- у одного пользователя не более двух `access_servers`;
|
||||
- следовательно, у локального сервера есть не более одного peer для репликации данных пользователя.
|
||||
|
||||
Протокол поддерживает ситуацию, когда:
|
||||
|
||||
- у отправителя один или два `access_servers`;
|
||||
- у получателя один или два `access_servers`;
|
||||
- часть серверов у сторон совпадает;
|
||||
- часть серверов уникальна.
|
||||
|
||||
@@ -332,7 +341,14 @@
|
||||
- `ReceiveIncomingMessage` — приём одной входящей копии, входящих редактирований и входящего read-receipt;
|
||||
- `ReceiveOutcomingMessage` — алиас `SendMessagePair`.
|
||||
|
||||
### 8.2. Новые методы, которые нужны
|
||||
### 8.2. Межсерверная синхронизация
|
||||
|
||||
- `ReceiveOutcomingMessage` — прежняя полная пара второго access-сервера отправителя;
|
||||
- `ReceiveIncomingMessage` — прежняя одиночная входящая копия;
|
||||
- `DmSyncBatch` — pull событий с `synced=false` и ACK сохранённой предыдущей страницы;
|
||||
- `GetDmDeliveryStatus` — единственная новая read-only проверка доставки по существующему `messageKey`.
|
||||
|
||||
Delivery-state между серверами не передаётся и не объединяется.
|
||||
|
||||
### 8.3. Серверный слой диалогов
|
||||
|
||||
@@ -446,8 +462,12 @@ Request:
|
||||
Правила:
|
||||
|
||||
- клиенту достаточно отправить пару на один любой доступный сервер;
|
||||
- успешный `status=200` означает локальное сохранение;
|
||||
- первая сетевая попытка выполняется до ответа клиенту, параллельно для двух серверов получателя;
|
||||
- сервер после принятия сам отвечает за дальнейшую межсерверную доставку.
|
||||
|
||||
Ответ сохраняет прежние `baseKey`, `incomingKey`, `outgoingKey` и счётчики доставки в клиентские сессии. Единственное новое поле — `deliveryState`: `accepted`, `delivered` или `failed`.
|
||||
|
||||
### 10.2. `ReceiveIncomingMessage`
|
||||
|
||||
Назначение:
|
||||
@@ -469,7 +489,7 @@ Request:
|
||||
}
|
||||
```
|
||||
|
||||
`sourceServerLogin` необязателен и используется как best-effort подсказка, чтобы сервер при дальнейшей пересылке не отправлял то же событие обратно серверу-источнику.
|
||||
`sourceServerLogin` пока доверяется без отдельной межсерверной подписи. Сервер всё равно проверяет пользовательскую подпись самого `SHiNE_DM`. Успешный ответ сохраняет старые поля `messageKey`, `baseKey` и счётчики realtime-доставки.
|
||||
|
||||
### 10.3. `DeleteMessage`
|
||||
|
||||
@@ -558,6 +578,8 @@ UI-следствие для клиента:
|
||||
- если точное время для старого сообщения неизвестно, но по более новым данным видно, что сообщение уже точно прочитано, UI может показывать его как прочитанное без точного времени;
|
||||
- read-receipt при этом остаётся отдельным DM-событием синхронизации, но в обычную историю страницы не подмешивается.
|
||||
|
||||
Для исходящих элементов сервер также возвращает единственное поле `deliveryState`.
|
||||
|
||||
## 11. Межсерверная доставка
|
||||
|
||||
### 11.1. Клиентская сторона
|
||||
@@ -568,47 +590,23 @@ UI-следствие для клиента:
|
||||
|
||||
### 11.2. Серверная сторона
|
||||
|
||||
После принятия валидного события сервер должен отправлять его:
|
||||
После локального принятия пара получает изменяемое состояние `accepted`. Сервер сразу вызывает до двух текущих маршрутов получателя параллельно. Успех хотя бы одного маршрута переводит сообщение в терминальное `delivered`; ждать второй маршрут не требуется.
|
||||
|
||||
- на все серверы из `access_servers` отправителя;
|
||||
- на все серверы из `access_servers` получателя.
|
||||
После первой попытки полная пара передаётся единственному второму access-серверу отправителя через прежний `ReceiveOutcomingMessage`. Peer самостоятельно пробует актуальные маршруты получателя; delivery-state в запрос не входит.
|
||||
|
||||
Если часть серверов совпадает, это допустимо.
|
||||
|
||||
Если один и тот же сервер присутствует у обеих сторон, он не должен слать сообщение сам себе повторно, но обязан локально сохранить событие и доставить его в нужные пользовательские сессии.
|
||||
|
||||
Идемпотентность обязательна.
|
||||
Идемпотентность обязательна. ACK означает запись в БД; повтор уже известной ревизии считается успешным ACK.
|
||||
|
||||
### 11.3. Догоняющая синхронизация истории
|
||||
|
||||
Для восстановления пропущенных DM-событий между access-серверами используется отдельная операция:
|
||||
Для восстановления пропущенных DM-событий между access-серверами используется pull-операция:
|
||||
|
||||
- `DmSyncBatch`
|
||||
|
||||
Сервер-получатель синхронизации запрашивает у другого access-сервера историю одного пользователя по курсору:
|
||||
Второй сервер запрашивает outbox-события владельца с `synced=false`, сохраняет их и передаёт подтверждённые `syncId` в `ackSyncIds` следующего запроса. Источник ставит `synced=true` только после ACK. Каждый цикл начинается с начала списка; курсор нужен только для страниц текущего цикла.
|
||||
|
||||
- `ownerLogin`;
|
||||
- `afterStoredAtMs`;
|
||||
- `afterMessageKey`;
|
||||
- `limit`, максимум `500`;
|
||||
- `maxBytes`, ограничение суммарного размера raw-блоков пачки.
|
||||
Полная пара отправителя передаётся двумя blob. Входящая копия получателя и tombstone передаются одним blob.
|
||||
|
||||
Удалённый сервер отдаёт все DM-события, относящиеся к этому пользователю:
|
||||
|
||||
- контентные копии и read-receipt по `target_login`;
|
||||
- tombstone типов `5/6/7/8`, где пользователь участвует как `fromLogin` или `toLogin`.
|
||||
|
||||
Порядок пачки:
|
||||
|
||||
- `created_at_ms ASC`;
|
||||
- `message_key ASC`.
|
||||
|
||||
Курсор хранится локально для пары:
|
||||
|
||||
- пользователь;
|
||||
- удалённый access-сервер.
|
||||
|
||||
При первом добавлении сервера или отсутствии курсора синхронизация стартует с `0` и постепенно подтягивает всю доступную историю пачками.
|
||||
Полная пара отправителя передаётся одним элементом с двумя blob в порядке incoming/outgoing. Входящая копия получателя и tombstone передаются одним blob.
|
||||
|
||||
При применении событий, полученных через `DmSyncBatch`, сервер:
|
||||
|
||||
@@ -618,17 +616,17 @@ UI-следствие для клиента:
|
||||
- не отправляет realtime/push-уведомления клиентам;
|
||||
- не запускает повторный fan-out, чтобы не создавать циклы.
|
||||
|
||||
Плановый sync запускается фоном после старта WebSocket-сервера и повторяется раз в 6 часов.
|
||||
Синхронизация настроек и DM выполняется одним периодическим процессом и через один последовательный WS-сеанс с peer. Выборка DM использует частичный индекс по `synced=false`.
|
||||
|
||||
В текущей реализации межсерверная авторизация для `DmSyncBatch` ещё не включена. Сервер отдаёт пачку только если сам локально является access-сервером `ownerLogin` по актуальной таблице `user_access_servers_current`.
|
||||
В текущей реализации межсерверная авторизация DM ещё не включена. Принимающий сервер проверяет, что сам является access-сервером `ownerLogin`, и всегда проверяет пользовательские подписи signed-блоков.
|
||||
|
||||
### 11.4. Ошибки доставки
|
||||
|
||||
Если часть серверов временно недоступна:
|
||||
Если ни один сервер получателя не подтвердил запись, попытки выполняются сразу, через 30 секунд, 5 минут, 25 минут и 1 час от первоначального принятия.
|
||||
|
||||
- это не должно отменять локальное принятие уже валидного сообщения;
|
||||
- повторная доставка может делаться отдельным retry-механизмом;
|
||||
- повторное получение того же события должно быть безопасным.
|
||||
Перед попытками через 5 минут, 25 минут и час сервер read-only спрашивает peer отправителя через `GetDmDeliveryStatus(messageKey)`. Если peer уже доставил хотя бы на один сервер, сообщение считается доставленным.
|
||||
|
||||
После неудачной последней попытки устанавливается `failed`; дальнейших автоматических попыток и кнопки ручного повтора нет.
|
||||
|
||||
## 12. Хранение в БД
|
||||
|
||||
@@ -653,11 +651,13 @@ UI-следствие для клиента:
|
||||
|
||||
Сообщение об удалении переписки тоже хранится в БД, а старые сообщения до его времени из БД удаляются.
|
||||
|
||||
Для догоняющей межсерверной синхронизации дополнительно используются:
|
||||
Изменяемая сетевая часть хранится отдельно:
|
||||
|
||||
- индекс по `target_login`, `created_at_ms`, `message_key`;
|
||||
- отдельные индексы по delete-событиям для `from_login` и `to_login`;
|
||||
- таблица `dm_sync_peer_state` с курсором чтения для пары `ownerLogin + remoteServerLogin`.
|
||||
- `dm_delivery_state` — состояние и расписание доставки исходящей пары;
|
||||
- `dm_sync_outbox` — событие владельца и единственный флаг ACK `synced`;
|
||||
- частичные индексы содержат только due/unsynced строки.
|
||||
|
||||
Legacy-таблица `dm_sync_peer_state` после миграции v12 физически остаётся для безопасной установки ZIP-накладки, но новым DM-кодом не используется.
|
||||
|
||||
## 13. Что обязательно должно измениться в коде относительно v0.5
|
||||
|
||||
@@ -670,7 +670,7 @@ UI-следствие для клиента:
|
||||
- межсерверная маршрутизация DM должна идти через `access_servers`;
|
||||
- сервер должен добирать отсутствующих пользователей из Solana PDA до проверки подписи DM;
|
||||
- при выборе актуальной версии должен учитываться `reencryptedAtMs`, если `revisionTimeMs` совпадает;
|
||||
- логика должна быть безопасна для нескольких серверов у каждой стороны.
|
||||
- логика должна быть безопасна для одного или двух серверов у каждой стороны.
|
||||
|
||||
## 14. Что в v1 пока не входит
|
||||
|
||||
@@ -678,4 +678,4 @@ UI-следствие для клиента:
|
||||
- хранение отдельного `keyId` шифрования в DM;
|
||||
- ротация `clientKey`;
|
||||
- финальная конкретная UI-реализация массовой перешифровки;
|
||||
- межсерверная авторизация `DmSyncBatch`.
|
||||
- межсерверная авторизация DM-синхронизации и доставки.
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
Логика протокола, API и поведение сервера описаны отдельно:
|
||||
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`
|
||||
- `docs/Personal_Messages/Доставка_и_синхронизация_DM.md`
|
||||
|
||||
`deliveryState`, retry-времена и sync-флаг не входят в `SHiNE_DM` и не подписываются пользователем. Это изменяемые серверные метаданные, хранящиеся отдельно от raw-контейнера. Для корреляции доставки используется уже существующий `messageKey`; добавление delivery-воркера не меняет ни одного байта формата ниже.
|
||||
|
||||
## 1. Общие правила
|
||||
|
||||
@@ -337,4 +340,4 @@ ReadReceiptBody_v1_0
|
||||
|
||||
В версии DM v1 все типы `1..8` используют единый контейнер `SHiNE_DM`.
|
||||
|
||||
Межсерверная операция `DmSyncBatch` не вводит новый байтовый формат DM. Она передаёт уже сохранённые raw-контейнеры `SHiNE_DM` в Base64 вместе с серверными метаданными курсора (`storedAtMs`, `messageKey`), а принимающий сервер заново проверяет подпись и применяет тот же контейнер по его `messageType`.
|
||||
Межсерверные операции `ReceiveOutcomingMessage`, `ReceiveIncomingMessage` и `DmSyncBatch` не вводят новый байтовый формат DM. Они передают уже сохранённые raw-контейнеры `SHiNE_DM` в Base64. `ackSyncIds` подтверждает только факт сохранения синхронизированной ревизии; delivery-state между серверами не передаётся. Принимающий сервер заново проверяет подпись и применяет контейнер по его `messageType`.
|
||||
|
||||
Reference in New Issue
Block a user