Проверка входящих сообщений
До этого состояния сервер распознавал JSON, но почти не проверял его смысл. Между корректным синтаксисом JSON и допустимой командой большая разница: число 42, массив и объект с неизвестным type тоже могут успешно разобраться. Добавим явную границу ввода для «Линии», чтобы обработчики комнаты получали известную форму сообщения и не пытались угадывать намерение клиента.
В пятом снимке результатом становится функция parseCommand(). Она принимает данные события WebSocket, проверяет договор первой версии и возвращает нормализованную команду либо выбрасывает ошибку с понятным кодом. Используйте целиком stages/05. Код и ожидаемые ответы не проверялись запуском, статус not_run; серверные ограничения рассмотрены по первичной документации ws и браузерной платформы.
Синтаксис, форма и предметные правила
Начнём с трёх разных вопросов. Синтаксическая проверка выясняет, можно ли разобрать байты как JSON. Проверка формы требует объект с полями нужных типов. Предметная проверка выясняет, подходит ли значение для нашего чата: известна ли комната, допустима ли длина фразы, имеет ли идентификатор ожидаемый вид. Ошибка на любом уровне прекращает обработку до создания события сообщения.
Полный файл protocol.js определяет проверку входа. В нём уже описаны команды join, send и resume, но доступность операции определяется текущим обработчиком. В пятом снимке сервер отправляет сообщения общей комнаты; подключение комнат появится следующим шагом, а восстановление по курсору — отдельным последующим состоянием.
import { roomIds } from './contract.js';
export function parseCommand(data, isBinary) {
if (isBinary) throw new Error('binary_not_allowed');
let value;
try {
value = JSON.parse(data.toString('utf8'));
} catch {
throw new Error('invalid_json');
}
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('object_required');
}
if (value.version !== 1) throw new Error('unsupported_version');
if (!['join', 'send', 'resume'].includes(value.type)) {
throw new Error('unknown_command');
}
if (!roomIds.includes(value.roomId)) throw new Error('unknown_room');
if (value.type === 'send') {
if (typeof value.text !== 'string') throw new Error('text_required');
const text = value.text.trim();
if (!text || text.length > 1000) throw new Error('text_length');
if (typeof value.clientMessageId !== 'string' ||
!/^[a-zA-Z0-9-]{1,80}$/.test(value.clientMessageId)) {
throw new Error('invalid_client_message_id');
}
return { ...value, text };
}
if (value.type === 'resume') {
if (!Number.isSafeInteger(value.after) || value.after < 0) {
throw new Error('invalid_cursor');
}
if (typeof value.epoch !== 'string') throw new Error('invalid_epoch');
}
return value;
}
Проверка Array.isArray() нужна отдельно от typeof value === 'object': массив является объектом JavaScript. Значение null тоже требует отдельного исключения. Без этих условий программа могла бы перейти к чтению полей у формы, которую мы не поддерживаем, и вернуть случайную внутреннюю ошибку вместо объяснимого отказа. Ранняя проверка делает последующие ветви короткими и предметными.
Тип поля проверяется до вызова строковых методов. Выражение value.text.trim() без проверки упадёт для числа или отсутствующего поля. Мы не преобразуем число в текст автоматически, поскольку договор требует строку. Автоматическое приведение кажется дружелюбным, но скрывает ошибки клиента: другой участник получит «123», хотя отправитель мог случайно прислать идентификатор вместо сообщения.
Нормализация и длина
Пробелы по краям сообщения удаляются, а пустая после этого строка отклоняется. Это одна определённая нормализация, которую повторяет браузер. Внутренние пробелы и переводы строк сохраняются: они могут быть частью инструкции оператора. Если удалить все пробельные символы или заменить несколько пробелов незаметно для пользователя, переданный текст станет отличаться от того, что человек набирал и ожидал увидеть.
Предел в тысячу относится к свойству length JavaScript. Оно считает кодовые единицы UTF-16, поэтому сложный символ или emoji может занимать больше одной единицы. Для полноценного продукта нужно отдельно решить, считает ли интерфейс графемы, Unicode-кодовые точки или байты. В учебном договоре важнее использовать одно ясно описанное правило на обеих сторонах, чем объявлять тысячу «символов» без уточнения механизма подсчёта.
Транспортный предел maxPayload остаётся равным 8192 байтам. Он проверяет входящее сообщение до прикладного разбора и включает JSON-оболочку. При превышении этого предела библиотека может закрыть соединение, поэтому клиент не обязательно получит обычное событие error от приложения. При превышении длины текста внутри небольшого JSON соединение сохраняется, а сервер отправляет код text_length. Это разные ожидаемые эффекты.
Идентификатор команды допускает только латинские буквы, цифры и дефисы, от одного до восьмидесяти знаков. У браузерного UUID форма подходит этому правилу. Ограниченный алфавит облегчает будущий составной ключ дедупликации и диагностический поиск. Это не секрет и не доказательство подлинности отправителя: идентификатор нужен для связи логического действия с подтверждением, а автора сервер определяет из сессии.
Ошибочный ввод остаётся отдельным результатом
Если отправить объект без версии, ожидается код unsupported_version. Если прислать text: 25, ожидается text_required. Пустая строка после trim даёт text_length. Ни один из этих случаев не должен создать событие message. На странице ошибка отображается отдельной надписью, поэтому история не содержит фразы, которую сервер не принял. Ниже представлены ожидаемые формы ответов, а не результаты выполненного запроса.
{"version":1,"type":"error","code":"text_required"}
{"version":1,"type":"error","code":"unknown_room"}
Распознавание известной комнаты пока не означает наличие права на неё. Проверка формы отвечает, что ticket-18 существует в договоре, но не выясняет, является ли Алиса участником второго обращения. Авторизация будет отдельным шагом после введения комнат. Это разделение полезно сохранить и в большой системе: ошибка структуры запроса и отказ по праву относятся к разным слоям, хотя обе запрещают изменение.
Неизвестные дополнительные поля сейчас игнорируются обработчиком, поскольку он использует только описанные значения. Это допустимая политика первой версии, но её нужно сохранять осознанно. Если позднее появится поле с похожим названием или опасной семантикой, не следует автоматически копировать весь входной объект в событие. В «Линии» сервер собирает событие самостоятельно и задаёт автора, время и идентификатор записи.
Функция выбрасывает Error, а внешняя граница обработки возвращает код. В большом приложении ожидаемые ошибки ввода удобнее представлять собственным типом и отделять от внутренних исключений. Иначе неожиданная ошибка хранилища может случайно стать публичным текстом. В этом снимке источники ошибок ограничены, однако контракт производства должен запрещать передачу stack trace и подробностей серверной инфраструктуры посетителю.
Полезное самостоятельное изменение — добавить поле версии со значением 2, не меняя остальной объект. Ожидайте отказ даже при правильной фразе: договор проверяется целиком. Затем верните версию 1 и убедитесь по ожидаемой модели, что команда снова допустима. Такой разбор показывает, почему клиентский интерфейс и серверная проверка развиваются согласованно, а не независимо по названиям полей.
Поведение данных и пределов транспорта сверяется с документацией ws. После пятого состояния вход превращается в проверенную команду. Дальше обработчик сможет выбирать комнату и проверять права, опираясь на известные значения, а не на произвольный JSON неизвестного происхождения.