Браузерный клиент
darkws — ESM-клиент без зависимостей, с объявлениями типов TypeScript.
npm install darkws@4
import DarkWs, { ErrorResponse } from "darkws";
const client = new DarkWs({
secure: location.protocol === "https:",
path: "/ws",
}).connect();
await client.authenticate(auth.accessToken());
const user = await client.request<User>("user:get", { id: "42" });
const unsubscribe = client.on("message", message => console.log(message));
unsubscribe();
client.dispose();
Конструктор не открывает сокет и не запускает таймеры. Сокет открывает connect();
запросы, сделанные до открытия, ждут подключения. Запрос к клиенту без сокета сам
открывает сокет.
Запросы
const result = await client.request<Result, Input>("handler:action", input);
- Promise разрешается полем
dataиз ответа. - Ответ с ошибкой отклоняет promise с
ErrorResponse:message— код ошибки,data— необязательные подробности,request— отправленный запрос. - Потеря соединения или отсутствие открытого сокета в течение
waitConnectionTimeoutотклоняет его сConnectionClosedError; отсутствие ответа — сRequestTimeoutError.
try {
await client.request("message:delete", { id });
} catch (error) {
if (error instanceof ErrorResponse && error.message === "message:forbidden") {
showForbidden();
} else {
throw error;
}
}
Ответ считается ошибкой, если в нём есть поле error, даже пустое.
Запросы никогда не повторяются: запрос, сокет которого закрылся, завершается ошибкой, и приложение само решает, безопасно ли отправить его снова.
Таймауты
Ожидание запроса состоит из двух фаз:
waitConnectionTimeout(30 с) ограничивает ожидание открытого сокета.requestTimeout(5 мин) ограничивает ожидание ответа и отсчитывается с момента отправки.
Вызов может занять до суммы обоих значений. requestTimeout: 0 отключает истечение
ожидания ответа: запрос остаётся в ожидании до ответа, разрыва соединения или освобождения клиента.
Таймаут ответа можно переопределить для отдельного вызова, передав число миллисекунд третьим аргументом:
await client.request("report:build", input, 60000);
Рассылки и события
Рассылки приходят через событие message в виде полного конверта
{ id: "@", action, data }. Фильтруйте по действию сами:
const off = client.on("message", message => {
const { action, data } = message as { action: string; data?: unknown };
if (action === "message:created") append(data as Message);
});
off();
Конверт во время выполнения не проверяется. Имена действий чувствительны к регистру. Обработчики принадлежат клиенту, поэтому сохраняются при переподключении.
on(event, listener) подписывает на события клиента и возвращает функцию отписки;
off(event, listener) тоже удаляет обработчик:
| Событие | Аргументы | Когда |
|---|---|---|
open | Event | Текущий сокет открылся |
close | CloseEvent | Сокет закрылся |
error | Event | Ошибка сокета |
message | unknown, MessageEvent | Любая рассылка в виде полного конверта { id: "@", action, data } |
send | unknown | Данные отправлены: запрос для request(), сырые данные для send(), локальные метаданные без токена для команд |
open срабатывает только для текущего сокета. close и error срабатывают и для
сокета, который connect() уже заменил. Исключение в обработчике не мешает вызову
остальных обработчиков.
Аутентификация
Вручную
await client.authenticate(token); // sends auth:<token>, waits for auth:success
await client.logout(); // sends logout, waits for logout:success
auth:failedотклоняет вызов сErrorResponse.- Команды выполняются по одной.
requestTimeout(5 мин,0отключает) ограничивает ожидание каждого ответа после отправки. Таймаут отклоняет вызов сRequestTimeoutError, закрывает сокет и отклоняет остальные запросы и команды в очереди на нём сConnectionClosedError, чтобы запоздавший ответ не подтвердил не ту команду. - Клиент не хранит токен. После переподключения серверной сессии нет, если вы её не восстановите.
Восстановление сессии после переподключения
У клиента нет опции автоматической аутентификации. Чтобы восстанавливать сессию на каждом сокете, аутентифицируйтесь заново при его открытии:
client.on("open", () => {
const token = tokenStore.current();
if (token) client.authenticate(token).catch(() => redirectToLogin());
});
Запросы, поставленные в очередь во время переподключения, отправляются при открытии
сокета, ещё до вызова обработчиков open, поэтому они могут дойти до сервера раньше этой
аутентификации и выполниться без сессии. Если это важно, передавайте токен с запросом
upgrade (см. ниже).
open и connected сообщают о состоянии транспорта; они не доказывают, что сессия
восстановлена.
Токены в URL
query добавляет параметры к URL сокета, и сервер при upgrade передаёт token своему
аутентификатору. Функция query вызывается для каждого сокета, поэтому каждое
переподключение отправляет актуальное значение:
new DarkWs({ secure: true, path: "/ws", query: () => ({ token: ticket() }) });
URL попадают в логи прокси и логи доступа. Лучше использовать короткоживущий тикет или оставить URL
чистым и вызывать authenticate() после подключения. После выхода очистите источник, из
которого читают query или ваш обработчик open, чтобы переподключение не восстановило
старые учётные данные.
Жизненный цикл соединения
| Метод | Поведение |
|---|---|
connect() | Открывает сокет; существующий сокет закрывается и заменяется, а его ожидающие запросы отклоняются |
reconnect() | Сбрасывает backoff и подключается, если сокет не открыт и не открывается |
close(code = 1000) | Закрывает сокет и прекращает переподключение |
dispose() | Окончательно: останавливает таймеры, закрывает сокет, отклоняет ожидающие запросы |
send(data, jsonify = true) | Отправляет сырые данные, не дожидаясь ответа; сначала ждёт открытого сокета |
Геттеры состояния: connected, closing, closed, pendingRequestCount (ожидающие
запросы и команды, включая те, что ждут сокета).
- После неожиданного закрытия клиент переподключается с экспоненциальным backoff и
jitter, начиная с
reconnectTimeout(5 с) и не более 30 с. - При
reconnect: falseфоновых повторов нет, но следующий запрос илиsend()откроет новый сокет. - После явного
close()запросы иsend()отклоняются сConnectionClosedError; вызовитеconnect(), прежде чем снова пользоваться клиентом.
Heartbeat
Клиент отправляет текст ping каждые pingTimeout (30 с), пока сокет открыт. Несмотря на
название, pingTimeout — это интервал: клиент не ждёт и не проверяет ответ pong, поэтому
сам по себе не обнаруживает полуоткрытые соединения. Сервер на .NET 8 полагается на этот
трафик, чтобы не закрывать простаивающие соединения.
Условия подключения
canConnect()проверяется перед каждой попыткой; пока он возвращает false, клиент повторяет проверку каждые 100 мс. Используйте его, чтобы дождаться условия, например появления сети.beforeConnect()ожидается перед каждой попыткой, например чтобы обновить токен. Отклонение планирует переподключение.
Оба повтора планируются только при включённом reconnect.
Опции
| Опция | По умолчанию | Назначение |
|---|---|---|
secure | обязательна | wss при true, ws при false |
path | обязательна | Путь endpoint, например /ws |
host | location.host | Хост и порт; обязателен вне окна браузера |
query | нет | Объект или функция, возвращающая параметры запроса URL |
requestTimeout | 300000 | Срок ожидания ответа в мс для запросов, authenticate() и logout(); 0 отключает |
waitConnectionTimeout | 30000 | Ожидание открытого сокета, в мс |
reconnect | true | Переподключаться в фоне после обрыва |
reconnectTimeout | 5000 | Базовая задержка переподключения в мс |
pingTimeout | 30000 | Интервал между ping heartbeat в мс |
canConnect | нет | Условие, проверяемое перед подключением |
beforeConnect | нет | Асинхронный хук, ожидаемый перед подключением |
debug | false | Выводить в консоль данные send(), некорректные сообщения и ошибки обработчиков |
Id запросов формируются через crypto.randomUUID(), который браузеры предоставляют только в
безопасных контекстах (HTTPS или localhost).
Сравнение с .NET-клиентом
Оба клиента используют один протокол, но различаются некоторыми правилами жизненного цикла. См. Протокол.
- .NET-клиент может автоматически аутентифицировать каждый сокет через
AuthenticationTokenProvider; браузерный клиент аутентифицируется, только когда вы вызываетеauthenticate(). - .NET-клиент считает соединение неудачным, если
pongне пришёл в течениеPongTimeout; браузерный клиент только отправляетping. - .NET-клиент прекращает повторы после постоянных сбоев, таких как неудачная
автоматическая аутентификация, HTTP 401 или 403 и ошибки протокола; браузерный клиент
переподключается после каждого неожиданного закрытия, пока включён
reconnect. - При выключенном переподключении запросы .NET завершаются ошибкой до явного
ConnectAsync; следующий запрос браузерного клиента открывает новый сокет.