Веб-мессенджеры и эвент 'beforeunload': как сохранить миллион сообщений при закрытии страницы


    В конце лета мы добавили в наше облако Voximplant поддержку месседжинга. Теперь с помощью него и россыпи SDK под разные платформы можно делать собственные мобильные или веб-мессенджеры: голосовые звонки в любых комбинациях между телефонными сетями и SDK — есть, видеозвонки между SDK — есть, месседжинг — есть. А еще у текстовых сообщений есть ключевое отличие от голосовых и видеозвонков: их контент должен оставаться. Voximplant может записать голосовой и видеозвонок на стороне облака и отдать URL с получившимся файлом, но это «медленная» история для CRM, систем управления заказами и колл-центров. А сообщения — это быстрая история. Пользователь очень огорчается, когда клик по «старому» чату в Skype вызывает зависание мобильного или веб-приложения, которое пытается выкачать хоть сколько-нибудь истории с нагруженных серверов по неустойчивому 3G. В наших SDK мы предусмотрели несколько механизмов для максимально быстрой работы с историей сообщений, о которых под катом.

    В чем, собственно, проблема?


    Новый пользователь мессенджера начинает с единственным объектом Messenger, который дает доступ к API и позволяет получать эвенты. Общение между пользователями начинается, когда один из них создает объект Conversation («беседа» или «чат» на двоих и более) и они начинают обмениваться сообщениями с помощью метода этого объекта sendMessage. О происходящих событиях клиенты узнают с помощью эвентов. Например, если пользователь «А» хочет отправить пользователю «Б» сообщение в первый раз, то он создает conversation на двоих, после чего им обоим приходит эвент CreateConversation, по которому пользователь «Б» узнает, что с ним хотят общаться. Также эвенты сигнализируют о новых сообщениях, присоединяющихся к conversations и покидающим их пользователям, смене админского статуса или о том, что пользователь печатает текст.

    Вся эта идиллия длится ровно до того момента, пока один из пользователей не закрывает вкладку с чатом. И не открывает ее снова через день. Или через месяц. Или через год. Что нужно сделать разработчику, чтобы пользователь увидел все то новое, что произошло за время его отсутствия? И желательно так, чтобы не подвесить браузер.

    Сериализация и нумерация — два кита истории сообщений


    Главная деталь механизма — это последовательная нумерация всех сообщений в conversation. В эвенте SendMessage есть поле seq, которое содержит уникальный идентификатор сообщения. Идентификатор уникален в рамках conversation и постоянно увеличивается. Соответственно, если мы закрыли страницу браузера, открыли ее через год и хотим узнать, какие новые сообщения за это время пришли, все что нужно сделать – это хранить где-нибудь sequence id последнего полученного сообщения, а после открытия страницы запросить у облака недостающие сообщения. Или, например, последние несколько десятков, и подгружать остальные, только если пользователь решил посмотреть лог.

    Вспомогательная деталь — это сериализация. SDK высокоуровневый и работает с объектами. Например, если мы хотим получить новые сообщения для conversation, то вначале нужно получить объект для этого conversation с помощью getConversation, а затем — сообщения с помощью метода этого объекта, retransmitEvents.

    Но если мы только загрузили страницу, то откуда у нас объекты? У нас кучка id'шек, предусмотрительно сохраненных в localStorage. А объекты придется создавать, и каждое такое создание объекта – это запрос к облаку для получения нужной информации.

    Решение — встроенный механизм сериализации объектов с помощью методов toCache и createConversationFromCache, которые создают JSON-представление внутренностей объекта и могут восстановить объект из такого JSON без обращения к серверу. А JSON можно хранить в localStorage, мгновенно восстанавливая при загрузке страницы сотню каналов и миллион сообщений.

    Миллион сообщений — а JavaScript или localStorage не лопнут?


    С веб-страницами, в отличии от desktop и мобильных приложений, все сложно. Когда пользователь командует «закрыть вкладку» или «закрыть браузер», срабатывает эвент «beforeunload», на который можно подписаться. Сообщения «у вас есть несохраненные данные» в google docs — это строка, которую разработчик вернул из обработчика. Раньше в нем можно было делать даже alert'ы, но скам-страницы «ваш браузер заблокирован, дайте денег» мягко намекнули разработчикам браузеров, что многое позволять в обработчике «beforeunload» не стоит.

    Тем не менее, современные браузеры дают нашему коду несколько секунд, прежде чем покажут такое сообщение и пользователь начнет беспокоиться:


    А за несколько секунд вполне можно сериализовать в localstorage несколько сотен conversations с миллионом сообщений. Но тут важно помнить, что по умолчанию localStorage ограничен 5-10 мегабайтами, и даже меньше для мобильных браузеров или если пользователь копался в настройках.

    Лучшие практики, чтобы ничего не лопнуло


    Если вы делаете новый «Skype for Web» и планируете действительно большое количество сообщений у ваших пользователей, то для хранения сериализованных объектов лучше использовать indexedDB, которое сейчас поддерживают все популярные браузеры. Квоты там по умолчанию намного больше и можно явно попросить у пользователя еще с помощью «Quota Management API» и специфичных браузерных штук.

    Второй момент — если в каком-то conversation с последнего посещения накопилось много сообщений, то будет разумно запросить у сервера последние несколько десятков, а остальные подгрузить, только если пользователь поскроллил лог. Получается разновидность «обратного бесконечного скролла» — новые элементы будут возникать не снизу, как при скролле страницы фейсбука, а сверху.

    В этом году мы планируем существенно расширить наш messaging, добавив управление через HTTP и вебхуки. Это позволит разработчикам делать интеграцию с другими мессенджерами, программное управление сообщениями вроде «чата с операторами» и другие интересные штуки.
    Voximplant
    Облачная платформа голосовой и видеотелефонии

    Комментарии 25

      +2
      Ваш текст so cute. Write ещё :)
        +2
        Англицизмы? ИМХО, разработчикам с ними проще читать.
          +2
          Я разработчик, мне не проще (имхо).
          Приведу пример из вашего же текста.
          Нормально: «В конце лета мы добавили в наше облако Voximplant поддержку месседжинга».
          Странно: «В этом году мы планируем существенно расширить наш messaging».
          Ну, и в целом, одно дело, если это название технологии, другое, — писать «conversation».
            +3
            Я долго думал над адекватным переводом «Conversation». «Чат»? Но если на двоих — это нифига не чат. «Беседа»? «Канал»? Нет хорошего перевода.
              0
              Диалог?
                0
                Между двумя. А в conversation можно тыщу запихнуть и сделать аналог «Channel» или «Group» в телеграме.
                +2
                Проблема отпадёт, если вы признаете где-то у себя внутри, что на двоих это тоже чат. :)
                  +2
                  Беседа — самое оно :)
                    +2
                    И беседа, и канал, и диалог — вполне нормальные варианты, каждый из которых используется в существующих мессенджерах\соц сетях… Другой вопрос, что «Conversation» тоже вполне себе удобоварим для человека, который знает перевод слова и привык обсуждать код с коллегами, используя имена классов. Разве что для тех, кто перевода не знает, можно было в пером случае указать и перевод в скобках, чтоб и ежу понятно было)
                      +1
                      Легко:

                      image
                    +1

                    Выдуманная проблема.
                    В среде разработчиков большинство привыкло общаться с использованием названий классов.
                    В 99% случае названия классов на английском.
                    В чем проблема?

                  0

                  Посмотрел историю ваших комментариев там и "экзампловые", и "непофикшенный", и "бранч"

                  –2
                  Скайп очень часто подвисает при использовании, как и все продукты майрософт. Да и в целом он уже больше подходит для видеозвонков и отживает свое. В плане оперативной беседы с пользователем и интеграцией с CRM я бы посоветовал интегрировать сервис типа chat2desk.com — насколько я помню он поддерживает в районе 5 мессенджеров
                    +1

                    Вы бы перед тем как оставлять коммент посмотрели о Skype ли статья

                    0
                    А что произойдет если _seq превзойдет MAX_SAFE_INTEGER?
                      +1
                      А на сервере ограничение на количество обновлений от одного клиента в минуту :) За 100 лет не произойдет, не переживайте. Они же не на всю систему, а только для conversation уникальны.
                        0

                        Почему не timeuuid? Если причина не в оптимизации трафика? Кто счётчик увеличивает, приложение или база?

                          0
                          В эвенте уже есть timestamp — зачем две разные сущности в одну запихивать? Увеличивает, конечно же, сервер.
                            +1

                            Прост как будете поддерживать консистентность счетчика, если у вас будет больше 1 сервера?
                            На уровне балансировщика, привязывать чат к серверу?

                              0
                              У нас намного больше одного сервера :) Это не самая сложная техническая проблема.
                                0

                                Согласен. И все же как ее решили, если не секрет? Очень интересен ваш опыт.
                                Мы вот именно по причине распределённости системы выбрали uuidv1. А вернее ему подобную реализацию с сортировкой. Плюсов много: уникальность в рамках всей системы, содержит метку времени, сортировка. Но вот есть существенный для нас минус: существенно увеличивают размер пакета при обмене данными с клиентом.
                                Как идея — отдельный микрометрами, который будет заняться увеличением счетчика.

                                  0
                                  • микросервис
                                    0
                                    Непосредственно у нас — шардинг. Но, как я уже говорил, есть множество разных способов и нужные выбирается под архитектуру, требования итд. У нас, к примеру, sequence id уникальны только в рамках conversation.

                                    У timeuuid есть вопросы к синхронизации времени между серверами и задержками между сообщениями. Эвенты разные бывают, и если быстро случилась последовательность, к примеру, «отправил сообщение а затем вышел из канала» то хочется чтобы они были именно в такой последовательности, иначе возможно странное :)
                          +1
                          проблема beforeunload в том, что он не работает для всех случаев и для всех браузеров. В IOSafari при refresh page он не срабатывает. Методы с localStorage или IndexDB не надежны, так как браузер успеет прервать код исполнения javascript. Если послать ajax, то многие браузеры, особенно мобильные, обрывают соединение закрытой страницы со стороны клиента, но запрос вы успеете послать и сервер скорее сделает свое дело (нужна проверка на больших запросах)
                            0
                            Все верно. Тем не менее, большинство крупных игроков вроде Google Docs им активно пользуются!

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое