Skip to content

Vaynerov Technologies

Мы не просто разрабатываем — мы создаём каждую строку кода и пиксель.

Все статьиИстории сборки

Музей, в котором есть другие люди

Первая версия была музеем для одного человека. Вторая — то же здание, но по нему ходят другие: четыре символа на входе, до восьми посетителей, голоса, затихающие, когда собеседник уходит дальше, — и ни одного байта всего этого на сервере.

Edward AmirainОснователь Vaynerov Technologies
Опубликовано 12 мин чтения
На этой странице
  1. Комната — это канал, а не запись
  2. Ретранслятор без полномочий
  3. Восемь человек в шерсти и льне
  4. Голоса, затихающие, когда вы уходите
  5. Визит без записи в ваше хранилище
  6. Шов, который оставила первая версия
  7. Само здание стало лучше
  8. Что вышло тихо

Шесть недель назад я описал музей для всего, у чего есть ссылка — защищённый запрос, каскад разбора, планировку, которая никогда не двигается. Он работал, и это было здание ровно для одного человека. Можно было вставить ссылку, повесить вещь, побродить по собственным залам, — а дальше с построенным оставалось делать только одно: рассказывать о нём кому-нибудь.

Поэтому вторая версия — тот же музей, но с людьми внутри. Вы открываете комнату, зачитываете четыре символа, и в ваше здание входит ещё до семи посетителей — фигурами, которые видно через галерею, с голосами, звучащими тише по мере ухода в соседний зал. Само здание за это время тоже стало лучше: три типа галереи вместо одного, скульптура на постаментах, небо, меняющееся день ото дня, и экскурсия, ставшая наконец настоящим контроллером камеры.

Ничего из этого нигде не хранится. Это ограничение сделало больше проектной работы, чем любая функция, — с него и начнём.

8
посетителей в комнате
0
байт хранится на сервере
246
строк интерфейса ×6 локалей
303
юнит-теста

Рабочая форма Reliquary v2. Второе число — то, с которым вышла v1; сохранить его, впустив в здание других людей, и есть смысл почти всего, что следует дальше.

Комната — это канал, а не запись

Очевидный способ построить совместные визиты — таблица комнат: строка на комнату, список участников, история чата. Это же и точка, в которой ваш музей никогда не покидает браузер тихо превращается в …кроме тех частей, которые мы теперь храним. Поэтому записи о комнате нет нигде. Комната — это топик канала, vt:reliquary:v1:<CODE>, на широковещательном ретрансляторе, и браузер хозяина и есть комната. Она живёт, пока ключ присутствия хозяина сидит в канале; закрыл вкладку — всё кончилось.

У отсутствия записи есть последствие, которого я не предвидел: спросить, существует ли комната, невозможно. Топик, на котором никого нет, подписывается ровно так же охотно, как и оживлённый. Поэтому на вопрос отвечают присутствие и дедлайн: входящий гость ждёт шесть секунд появления ключа хозяина, и этот таймаут и есть определение «такой комнаты нет». А до того, как хозяин займёт код, то же отсутствие заставляет его посидеть на топике 900 мс и посмотреть, не живёт ли там уже кто-то.

Код — четыре символа, показывается как REL-7K2M. В алфавите тридцать знаков: нет 0 и O, нет 1, I и L, нет U — первые две пары дают классическую путаницу при чтении вслух, а U убран, чтобы никакой розыгрыш не сложился в слово, которое посетителю не захочется произносить вслух. Тридцать в четвёртой — это 810,000 кодов, берутся из crypto.getRandomValues. Это приватность через неизвестность, и она названа своим именем: угаданный код — единственная дверь в чужой музей, поэтому у хозяина есть кнопка закрытия комнаты, а каждый приходящий байт всё равно проверяется заново.

Ретранслятор без полномочий

Учётные данные, которые пускают браузер в этот канал, публичны по замыслу — маркетинговые страницы и так отдают их каждому посетителю. Честное описание транспорта: тупая труба, в которую любой, у кого есть четыре символа, может отправить что угодно, и всё ниже по течению построено на этом допущении.

Каждое сообщение проверяется заново при получении — по типу и по границам — в одной функции, которая и есть вся граница доверия этой возможности. Экспонаты, пришедшие от собеседника, снова проходят через sanitizeExhibit, ту же самую функцию, которой фильтруется собственное хранилище посетителя: она импортируется, а не копируется, чтобы не разъехаться. URL, отвергнутый с диска, отвергается и с провода. Идентификатор отправителя — это заявление, а не удостоверение, и слоя подписи здесь намеренно нет, в отличие от протокола 4D-шахмат, где подделанный ход мог бы украсть партию. Здесь красть нечего.

Отправитель никогда не бывает санитайзером. Монтирование от гостя — это просьба; хозяин очищает её заново, применяет через собственное хранилище и рассылает то, что действительно легло.

Защищаться пришлось от усиления. Тридцатибайтный hello, покупающий ресинхронизацию на шестьдесят экспонатов, — это рычаг, направленный на комнату, поэтому у каждого сообщения, ответ на которое стоит дороже самого сообщения, есть порог на отправителя: две секунды между повторными синхронизациями, 400 мс между строками чата, три секунды между запросами на монтирование. Печатать медленнее 400 мс; цикл — нет. Синхронизация нарезается по 48 КБ или 24 элемента, что наступит раньше, — и пустая коллекция всё равно выдаёт один фрагмент с пустым списком, потому что гость, не получивший ничего, не отличит «у хозяина ничего нет» от «синхронизация ещё идёт».

Канал несёт знакомства и ничего, что имело бы власть. Звука он не касается: ретранслятор передаёт двум браузерам непрозрачные блобы соединения друг друга, а медиа идут напрямую.

Позиция — единственное сообщение, которое повторяется: десять раз в секунду на посетителя, всё время, пока комната открыта, — поэтому она квантуется до сантиметров и миллирадиан. Это не только сжатие: квантователь заодно и детектор простоя, ведь две позы, квантующиеся в одни и те же целые числа, и есть одна поза, и стояние на месте останавливает вещание без отдельной проверки.

Восемь человек в шерсти и льне

В спецификации v1 сверху стояли две строки: никаких новых npm-зависимостей, никаких бинарных ассетов. Вторая и решила, что такое посетитель. Ни загрузок, ни скачиваемых персонажей — посетитель это четыре небольших целых числа и имя, и весь вывод примерочной умещается в полезной нагрузке присутствия.

Восемь палитр, названных по ткани, а не по цвету: сланец, кэмел, слива, мох, ржавчина, чернила, лён, патина. Словарь галереи — орех, известковая побелка и латунь под низким солнцем, и каждый наряд лежит внутри этого диапазона, а не спорит с ним. Ровно одна палитра тянется к бирюзе, на две ступени ниже по насыщенности: посетитель в пальто с бирюзинкой, а не источник света, идущий через зал. Бирюза здесь по-прежнему скальпель.

Четыре формы головы — одна кривая. Суперэллипс — |y/ry|^n + |x/rx|^n = 1 — при n = 2 даёт эллипс и приближается к прямоугольнику с ростом n; круглая, овальная, квадратная и гребень — четыре набора его параметров, квадратная голова на 3.6, мягкий куб вращения, а не коробка с углами. Одна функция выдаёт силуэт, трёхмерная фигура вытачивает его на токарном станке, плоское превью в примерочной отражает его в контур, — так что выбор не может пообещать голову, которую фигура не сохранит. Рост — узкая полоса, от 0.9× до 1.1× фигуры в 1.70 м.

Решение, с которым я ожидал спорить сам с собой, — столкновения. Собеседники здесь призраки: ни один аватар не даёт коллайдера, и контроллеру ходьбы вообще не сообщают об их существовании. То, что мы рисуем, — оценка, сглаженная пружиной догадка о том, где человек был пару сотен миллисекунд назад, а быть отпихнутым оценкой хуже, чем пройти сквозь того, кто действительно тут стоит. К тому же дверной проём шириной 2.3 м: один праздно стоящий в нём посетитель, будь он твёрдым, запечатает целое крыло чужого музея — и попросить его отойти нечем. Наложиться друг на друга — неловкость на две секунды; застрять — сломанный визит.

Голоса, затихающие, когда вы уходите

Текстовый чат занял утро. Голос — то, что превращает общую страницу в общую комнату. Звук идёт от браузера к браузеру по прямым соединениям; канал лишь передаёт двум посетителям непрозрачные блобы сессий друг друга. Именно это закрепляет потолок на восьми, включая хозяина: полная сетка из восьми — это 28 соединений и семь исходящих потоков на человека, примерно столько, сколько выдерживают аплинк ноутбука и кодировщик телефона. Поднять планку значило бы завести медиасервер, а не увеличить константу.

Оба конца пары узнают друг о друге из одного и того же события присутствия, в одно и то же мгновение, поэтому оба пытаются начать разговор и оба встают намертво — классический glare. Починка стоит одного сравнения: предлагает тот, чей идентификатор идёт раньше по порядку, второй ждёт. Идентификаторы — 32 шестнадцатеричных символа, так что порядок полный и одинаковый с обеих сторон. Голос каждый посетитель включает сам, поэтому отвечающая сторона, не услышавшая ничего за 2.5 секунды, повышает себя и предлагает первой.

Дальше то, что вы действительно слышите. Голос звучит на полной громкости в пределах двух метров — расстояние разговора — и дальше спадает по простому закону обратного расстояния. Физическая обратная зависимость, впрочем, никого не заглушает: на четырнадцати метрах голос всё ещё около 14%, а семь таких под фоновым гулом зала дают бормотание, звучащее как неисправность, а не как музей. Поэтому на последнем отрезке кривая загоняется ровно в ноль оконной функцией smoothstep, плоской на обоих концах, чтобы никто не щёлкнул, уходя. Залы здесь около десяти метров, так что затухание попадает между дальним концом этого зала и двумя залами отсюда.

Обе оси линейные, от нуля. Пунктир — один только обратный закон, ещё слышимый у дальнего края; выпущенная кривая — это тот же закон, умноженный на окно, и именно это придаёт расстоянию смысл в здании такого размера.

Панорама — боковая составляющая пеленга, ограниченная ±0.8, и ближе метра она сходится к центру, чтобы два посетителя, проходящие сквозь друг друга, не хлестнули у вас в ушах слева направо.

Голос работает в собственном аудиоконтексте, а не одалживает контекст игрового движка, который сам приостанавливается, едва документ скрыт: верно для шагов, неверно для разговора. Он всё равно подчиняется той же кнопке отключения звука, потому что человек, заглушивший музей, имел в виду сделай тише, а не сделай тише перезвон. Push-to-talk сидит на V и глушит дорожку микрофона, а не соединение.

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

Визит без записи в ваше хранилище

Гость смотрит на чужую коллекцию, а обещание из заметки о запуске состоит в том, что ваш музей — один ключ в вашем собственном браузере. Поэтому визит рисует полностью отдельный мир, а не тот, что держит ваш ключ, и в ваше хранилище, пока вы в гостях, не пишут. Подмена едет под завесой в обе стороны, и завеса опускается немедленно, ещё до того, как приедет нарезанная синхронизация: иначе гость на мгновение оставался бы стоять в собственной галерее, гадая, чьи это картины.

Режим только для чтения оказался чередой мелких решений, а не одним переключателем. Панель управления сохраняет список и переход к экспонату, но удаления и очистки в ней больше нет. Строка вставки остаётся и становится просьбой: разбор по-прежнему происходит в браузере гостя, очищенный результат едет по проводу, хозяин очищает его ещё раз и отвечает вердиктом, который приходит всплывающим уведомлением. Когда монтирование выключено, строка отключена и говорит почему, а не исчезает: отсутствующий элемент управления неотличим от сломанной страницы.

Шов, который оставила первая версия

Заметка о запуске кончалась обещанием: режим экскурсии, «намеченный за тем же швом контроллера, которым пользуется режим ходьбы». Какое-то время эта экскурсия была тем, чем такие вещи обычно и становятся, — цепочкой одноразовых твинов камеры, запускаемых изнутри цикла отрисовки, а значит, у камеры было два хозяина в зависимости от минуты.

Теперь есть вторая реализация. Рельс — заранее вычисленный список остановок: каждая работа в порядке развески, каждая — в той фронтальной позе обзора, которой уже пользуется автопроход. Между остановками камера летит по ломаной из путевых точек в дверных проёмах, а не интерполируется сквозь стену, задерживается на 3.6 секунды и в конце заходит на новый круг. Обзор перетаскиванием во время поездки добавляет ограниченное смещение поверх собственного направления рельса — на ходу оно затухает обратно к центру, поэтому следующая работа всегда приезжает по центру, а на стоянке держится. Настоящий перехват — это касание клавиш движения, и оно возвращает вас к ходьбе в текущей позе. Лучшее следствие контроллера вместо анимации: открытие работы теперь ставит экскурсию на паузу, а не заканчивает её.

Миникарта берётся из того же модуля раскладки, что и здание: вход сверху, галереи идут вниз по панели в том порядке, в котором вы их проходите. Кликните по залу или по точке — и существующая машинерия «дойти или уйти под завесу» доставит вас туда. Другие посетители показаны точками в собственных акцентных цветах; единственная бирюза на панели — сектор, показывающий, куда вы смотрите.

Само здание стало лучше

У v1 был один рецепт галереи: длинный зал с работами, чередующимися по двум простым стенам. Это хороший зал, и к тридцатому экспонату он единственный, где вы вообще бывали. Теперь архетипов три, и они чередуются по индексу зала: длинный зал, салон (более широкий парадный зал с угловыми пилястрами под потолочным окулюсом) и галерея с нишами, где между развесками из боковых стен выступают неглубокие пилоны, так что каждая пара работ стоит в собственной нише.

Деликатным всё это делает инвариант, на котором держится здание целиком: добавление экспоната N+1 никогда не должно сдвигать экспонаты с 1 по N. Поэтому всё, что меняет архетип, — чистая функция индекса зала, а зал 0 намеренно остаётся длинным залом с исходными числами, так что каждый музей, построенный в июне, воспроизводит ровно ту геометрию, какая у него была. Экспонаты без изображения раньше висели табличкой на стене; теперь они стоят гравированной стелой на постаменте, ровно в той же позиции слота.

И небо пришло в движение. Три настроения — золотой час, бледное утро и сумерки синего часа — выбираются взвешенным колесом на десять слотов по хешу локальной календарной даты. Золотой час держит пять слотов из десяти, потому что это лицо дома; на весь день у вас одна погода, завтра другая, а два человека в одном часовом поясе видят один и тот же музей. Каждое настроение несёт весь контракт освещения, а не только небо: сумерки поднимают тёплое самосвечение штукатурки, а не опускают его, потому что до стены над дверным проёмом не достают ни низкое солнце, ни софиты работ.

Что вышло тихо

Перед монтированием canvas теперь запускается проба WebGL, и настоящее отсутствие получает локализованную заставку вместо пустого прямоугольника — именно настоящее отсутствие, никогда не класс устройства: телефон с работающим WebGL сохраняет весь проходимый музей. Потеря контекста заведена на состояние восстановления и чистое перемонтирование.

Работы — это меши, поэтому клавише Tab не за что зацепиться. Холсту с обзором перетаскиванием подходит другой приём — курсор по порядку развески: [ и ] переходят между работами, камера идёт к каждой пешком или телепортируется под завесой, и каждая вежливо объявляется. Enter рассматривает — а если курсор не задан, берётся работа, ближайшая к тому месту, где вы стоите.

И у музея теперь есть тесты: 303 в 11 файлах, вместо нуля. Это скорее архитектура, чем добродетель: протокол, коды комнат, математика голоса, раскладка и рельс — чистые модули без React, без three.js и без сети внутри, и поэтому вопрос почему голос этого человека звучит не с той стороны — тест, а не вечер в браузере. Интерфейс вырос с 87 строк до 246, во всех шести локалях, с проверкой паритета.

v1 — июньv2 — сегодня
Посетители1до 8, включая хозяина
Голоссетка по расстоянию, напрямую между браузерами
Экскурсияцепочка твинов камерыконтроллер-рельс; фокус ставит на паузу
Залы галереиодин рецепт зала3 архетипа, чередуются по индексу зала
Небозолотой час3 настроения, по одному на календарный день
Лоты без изображениятабличка на стенестела на постаменте
Ориентированиеходьба или список в панели управленияминикарта-планировка с точками посетителей
Строки интерфейса87 ×6 локалей246 ×6 локалей
Юнит-тесты0303 в 11 файлах
Хранится на сервереничегопо-прежнему ничего

Последняя строка — тезис. Всё, что выше неё, — цена того, чтобы последняя строка осталась правдой, когда в здании больше одного человека.

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

Чего я не ожидал — насколько побочно улучшилось само здание. Как только в комнате может оказаться кто-то ещё, замечаешь, что все галереи одинаковы, что небо не меняется, что лоты без картинки извиняются за себя перед стеной. В одиночку это никого не беспокоит. Видно это только тогда, когда в этом стоит кто-то другой.