Как вернуть ответ из очереди сообщений WebSocket

Короткий ответ: WebSocket не работает как fetch, где вызвал функцию и сразу получил конкретный ответ. Сообщения приходят асинхронно и могут прийти не в том порядке, в котором вы их отправили. Чтобы вернуть ответ из очереди сообщений, добавьте каждому запросу requestId, храните ожидающие запросы в Map, а при получении ответа находите нужный requestId и завершайте соответствующий Promise

Клиентский request-response поверх WebSocket

class WebSocketRpcClient {
  constructor(socket) {
    this.socket = socket;
    this.pending = new Map();

    this.socket.addEventListener("message", (event) => {
      this.handleMessage(event.data);
    });
  }

  request(type, payload, timeoutMs = 5000) {
    const requestId = crypto.randomUUID();

    const message = {
      type,
      requestId,
      payload,
    };

    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        this.pending.delete(requestId);
        reject(new Error(`WebSocket request timeout: ${type}`));
      }, timeoutMs);

      this.pending.set(requestId, { resolve, reject, timeout });
      this.socket.send(JSON.stringify(message));
    });
  }

  handleMessage(raw) {
    const message = JSON.parse(raw);

    if (!message.requestId) {
      return;
    }

    const pending = this.pending.get(message.requestId);

    if (!pending) {
      return;
    }

    clearTimeout(pending.timeout);
    this.pending.delete(message.requestId);

    if (message.error) {
      pending.reject(new Error(message.error.message));
      return;
    }

    pending.resolve(message.payload);
  }
}

Использование:

const socket = new WebSocket("wss://example.com/ws");

socket.addEventListener("open", async () => {
  const client = new WebSocketRpcClient(socket);

  const profile = await client.request("get_profile", {
    userId: 42,
  });

  console.log(profile);
});

Серверный ответ

Сервер должен вернуть тот же requestId:

socket.on("message", async (raw) => {
  const message = JSON.parse(raw.toString());

  if (message.type === "get_profile") {
    socket.send(JSON.stringify({
      type: "get_profile_response",
      requestId: message.requestId,
      payload: {
        id: message.payload.userId,
        name: "Dinar",
      },
    }));
  }
});

Если сервер не возвращает requestId, клиент не сможет понять, какой Promise нужно завершить

Почему очередь нужна

Представьте, что клиент отправил три запроса:

get_profile requestId=a
get_orders  requestId=b
get_stats   requestId=c

Ответы могут прийти так:

get_stats   requestId=c
get_profile requestId=a
get_orders  requestId=b

Если сопоставлять ответы только по порядку, все сломается. requestId решает эту проблему

Обработка событий без ответа

Не все сообщения являются ответами. Некоторые события сервер присылает сам:

{
  "type": "notification",
  "payload": {
    "text": "Новый заказ"
  }
}

Для таких сообщений нет requestId. Их нужно обрабатывать отдельно:

if (!message.requestId) {
  handleServerEvent(message);
  return;
}

Что делать с повторным ответом

Иногда из-за повторной отправки, reconnect-логики или ошибки сервера ответ может прийти два раза. В этом случае клиент не должен повторно завершать Promise. Именно поэтому после первого ответа мы делаем:

this.pending.delete(message.requestId);

Если второй ответ придет позже, pending уже не найдется, и сообщение можно проигнорировать или залогировать как поздний дубль. Это нормальное защитное поведение для очереди ответов

Если операции критичные, добавьте на сервере идемпотентность: один requestId должен приводить к одному логическому результату, даже если сообщение было отправлено повторно

Частые ошибки

Первая ошибка — ждать return прямо из socket.send(). Метод send() только отправляет данные, он не возвращает ответ сервера

Вторая ошибка — хранить один общий callback. Если одновременно уйдет два запроса, ответы перепутаются

Третья ошибка — не ставить timeout. Если сервер не ответил, Promise будет висеть вечно

Четвертая ошибка — не удалять pending-запрос из Map. Это приводит к утечке памяти

Пятая ошибка — смешивать ответы и broadcast-события в одном обработчике без признака сообщения. У ответа должен быть requestId, у события может быть только type и payload

Самопроверка

Сделайте сервер, который отвечает на три запроса с разными задержками. Клиент должен правильно сопоставить ответы по requestId, даже если они пришли не по порядку. Потом отключите один ответ и проверьте, что timeout сработал и pending-запись удалилась

Что почитать дальше по WebSocket

Если нужен общий маршрут по теме, откройте рубрику WebSocket. Для соседних задач пригодятся эти разборы:

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x