<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrej Kirejeŭ</title>
    <description>The latest articles on DEV Community by Andrej Kirejeŭ (@andreik).</description>
    <link>https://dev.to/andreik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F276658%2F60ed9ead-7595-459e-876a-6ecbcf91bad2.jpg</url>
      <title>DEV Community: Andrej Kirejeŭ</title>
      <link>https://dev.to/andreik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andreik"/>
    <language>en</language>
    <item>
      <title>Отказоустойчивые видеозвонки на WebRTC</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Sun, 26 Apr 2026 09:51:30 +0000</pubDate>
      <link>https://dev.to/andreik/otkazoustoichivyie-vidieozvonki-na-webrtc-cln</link>
      <guid>https://dev.to/andreik/otkazoustoichivyie-vidieozvonki-na-webrtc-cln</guid>
      <description>&lt;p&gt;&lt;em&gt;Как мы спроектировали, реализовали и проверили в реальных условиях модуль видеосвязи для видеоинтервью и самостоятельных видеотестов в &lt;a href="https://recruiter-ai.andersenlab.com/" rel="noopener noreferrer"&gt;Recruiter.AI&lt;/a&gt; — и какие технические проблемы пришлось решить по пути.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Сразу обозначим рамки статьи. WebRTC — большая тема, по которой написаны целые книги. Это не учебник по WebRTC, а разбор конкретной архитектуры и инженерных решений, которые мы приняли для видеозвонков в Recruiter.AI.&lt;/p&gt;

&lt;p&gt;Код видеозвонков доступен в &lt;a href="https://github.com/gsbelarus/gdmn-meet" rel="noopener noreferrer"&gt;репозитории GDMN Meet&lt;/a&gt;. Его можно попробовать на &lt;a href="https://meet.gdmn.app" rel="noopener noreferrer"&gt;meet.gdmn.app&lt;/a&gt; или увидеть в работе в &lt;a href="https://recruiter-ai.andersenlab.com/" rel="noopener noreferrer"&gt;сценарии интервью Recruiter.AI&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Почему понадобилось свое решение
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://recruiter-ai.andersenlab.com/" rel="noopener noreferrer"&gt;Recruiter.AI&lt;/a&gt; от &lt;strong&gt;Andersen Lab&lt;/strong&gt; — платформа для найма с поддержкой ИИ. Две ее ключевые функции — &lt;strong&gt;видеоинтервью&lt;/strong&gt; между кандидатом и интервьюером и &lt;strong&gt;самостоятельный видеотест&lt;/strong&gt;, где кандидат записывает ответы на вопросы один перед камерой, — зависят от видеопотока, который должен стабильно работать не только в идеальных условиях.&lt;/p&gt;

&lt;p&gt;Пользователь может подключаться из дома, из гостиницы, через нестабильный мобильный интернет, за корпоративным файрволом или из сети с национальной DPI-фильтрацией. Для такой среды недостаточно просто вызвать WebRTC API и рассчитывать, что браузер все сделает сам.&lt;/p&gt;

&lt;p&gt;В статье разберем:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;общую архитектуру и причины, по которым она устроена именно так;&lt;/li&gt;
&lt;li&gt;собственный минимальный сигнальный сервер на Socket.IO;&lt;/li&gt;
&lt;li&gt;STUN, TURN и &lt;strong&gt;проблему юрисдикций&lt;/strong&gt;, в том числе ограничения сетей, фильтруемых российским РКН;&lt;/li&gt;
&lt;li&gt;паттерн «идеального согласования» (Perfect Negotiation) и буферизацию ICE-кандидатов;&lt;/li&gt;
&lt;li&gt;многоуровневое восстановление соединения для нестабильных сетей;&lt;/li&gt;
&lt;li&gt;обнаружение замершего медиапотока, когда браузер все еще считает соединение активным;&lt;/li&gt;
&lt;li&gt;различия между Chrome, Firefox и Safari и конкретные обходные решения;&lt;/li&gt;
&lt;li&gt;проверку оборудования и горячую замену камер и микрофонов во время звонка;&lt;/li&gt;
&lt;li&gt;конвейер записи: компоновку через &lt;code&gt;canvas&lt;/code&gt;, загрузку фрагментами и исправление для Safari, на поиск которого ушло два дня;&lt;/li&gt;
&lt;li&gt;инструменты диагностики, которые помогают проверять все это в реальных сетях.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Общая картина
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2c3srfzb0q321692mufp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2c3srfzb0q321692mufp.png" alt="Обзор архитектуры: два браузера, Socket.IO для сигнального обмена, STUN, TURN поверх TLS в нескольких юрисдикциях и загрузка записи фрагментами" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;На верхнем уровне в системе есть четыре плоскости:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Медиаплоскость (P2P).&lt;/strong&gt; Два &lt;code&gt;RTCPeerConnection&lt;/code&gt; обмениваются зашифрованным DTLS-SRTP-трафиком. При удачном сценарии медиаданные идут напрямую между браузерами, без промежуточного сервера.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Плоскость сигнального обмена.&lt;/strong&gt; Минимальный Socket.IO-сервер на стороне приложения пересылает типизированные сообщения между участниками комнаты и хранит только минимальное состояние присутствия, нужное для защиты от повторных входов.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ICE-плоскость.&lt;/strong&gt; STUN-серверы Google, Cloudflare и наш собственный coturn помогают проходить NAT. Если прямое P2P-соединение невозможно, медиатрафик идет через TURN-серверы поверх TLS на порту 443.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Приемник записи.&lt;/strong&gt; API загрузки фрагментами принимает 30-секундные медиаблоки, которые создает &lt;code&gt;CallRecorder&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;На клиенте ответственность разделена между двумя хуками, связанными небольшим интеграционным слоем. Первый управляет &lt;code&gt;RTCPeerConnection&lt;/code&gt;, сигнальным обменом и восстановлением соединения. Второй отвечает за камеры, микрофоны, подключение и отключение устройств, а также за локальный медиапоток. Такое разделение оказалось важным: замена устройств во время звонка сама по себе достаточно сложна, и смешивать ее с состоянием сигнального обмена было бы ошибкой.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Сигнальный сервер: минимальный, восстанавливаемый, свой
&lt;/h2&gt;

&lt;p&gt;Для сигнального обмена мы используем точку подключения Socket.IO как часть сервера приложения. У нее три задачи:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Пересылать сообщения.&lt;/strong&gt; Сервер не хранит состояние звонка, а только передает сообщения между участниками. Благодаря этому протокол можно развивать без изменений на серверной стороне.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Восстанавливать сессию после краткого обрыва.&lt;/strong&gt; Если у кандидата на несколько секунд пропал Wi-Fi, страница не должна требовать ручной перезагрузки.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Отслеживать присутствие в минимальном объеме.&lt;/strong&gt; Сервер должен понимать, кто находится в комнате, чтобы отклонять повторный вход одного и того же участника.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Именно второй пункт стал причиной, по которой мы выбрали Socket.IO, а не обычный WebSocket. В Socket.IO есть &lt;code&gt;connectionStateRecovery&lt;/code&gt;: механизм буферизует события в течение заданного окна и восстанавливает сессию, если клиент успевает переподключиться. У нас это окно равно &lt;strong&gt;пяти минутам&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Сам протокол компактный: девять типов сообщений — JOIN/LEAVE, ENUM, CALLING, ANSWER_CALL, SIGNAL, SOCKET_PING/PONG, RESTART_CALL, END_CALL, MAKE_CALL. &lt;code&gt;SIGNAL&lt;/code&gt; передает и SDP, и ICE-кандидаты. В каждом сообщении есть &lt;code&gt;fromId&lt;/code&gt;/&lt;code&gt;toId&lt;/code&gt;, поэтому серверу не нужно хранить сведения о том, кто именно с кем разговаривает.&lt;/p&gt;

&lt;p&gt;Последовательность успешной установки звонка:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmt8oit0vt2ande6dmeun.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmt8oit0vt2ande6dmeun.png" alt="Последовательность сигнального обмена: JOIN, CALLING, предложение/ответ (offer/answer), постепенная передача ICE-кандидатов и пульсовая проверка через SOCKET_PING/PONG" width="800" height="1471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;В начальной фазе подключения есть две важные детали:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Каждая попытка подключения отправляет новый &lt;code&gt;instanceId&lt;/code&gt; (UUID v4) вместе с &lt;code&gt;participantId&lt;/code&gt;. Если вторая вкладка входит с тем же &lt;code&gt;participantId&lt;/code&gt;, сервер отвечает &lt;code&gt;already_in_room&lt;/code&gt;, и вторая вкладка прекращает подключение. Так мы обнаруживаем и отклоняем &lt;strong&gt;дублирующиеся входы&lt;/strong&gt; — частую причину жалоб вроде «я вижу себя в сетке два раза».&lt;/li&gt;
&lt;li&gt;При закрытии вкладки клиент отправляет LEAVE через &lt;code&gt;navigator.sendBeacon()&lt;/code&gt;. &lt;code&gt;window.onbeforeunload&lt;/code&gt; в мобильных браузерах ненадежен; &lt;code&gt;sendBeacon&lt;/code&gt; работает лучше.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. STUN, TURN и проблема юрисдикций
&lt;/h2&gt;

&lt;p&gt;Получить ICE-кандидаты сравнительно просто: достаточно указать для &lt;code&gt;RTCPeerConnection&lt;/code&gt; несколько публичных STUN-серверов. Конфигурация по умолчанию:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stun:stun.l.google.com:19302&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stun:stun.cloudflare.com:3478&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;наш собственный сервер для резервирования&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Сложная часть — TURN. Существенная доля пользователей не может установить прямое P2P-соединение: корпоративные сети, симметричные NAT, мобильные операторы с NAT операторского класса (carrier-grade NAT) и, что особенно важно для Recruiter.AI, пользователи внутри национально фильтруемых сетей. В таких случаях медиатрафик должен идти через промежуточный TURN-сервер.&lt;/p&gt;

&lt;p&gt;Поэтому мы подняли собственный &lt;strong&gt;coturn&lt;/strong&gt; и открыли TURN на порту &lt;strong&gt;443&lt;/strong&gt;. URL ICE-сервера выглядит так:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;turns:coturn.our_server.com:443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Порт 443 важен. Многие корпоративные файрволы и национальные DPI-фильтры пропускают TLS по TCP/443, потому что блокировка этого трафика нарушит работу HTTPS. TURN-сервер на этом порту, работающий внутри корректного TLS-туннеля, для большинства DPI выглядит как обычная HTTPS-сессия. Поэтому мы используем &lt;strong&gt;TURN поверх TLS на :443&lt;/strong&gt;, а не стандартные транспорты UDP 3478 или TCP 5349.&lt;/p&gt;

&lt;p&gt;Но при работе с пользователями из стран с жесткой фильтрацией трафика возникает еще одна проблема.&lt;/p&gt;

&lt;h3&gt;
  
  
  Проблема российской фильтрации
&lt;/h3&gt;

&lt;p&gt;Российская инфраструктура национальной фильтрации трафика — система РКН, включая ТСПУ на точках пиринга провайдеров, — блокирует не только &lt;em&gt;известные&lt;/em&gt; адреса. Соединения могут прерываться и для узлов, чьи IP-адреса или SNI-имена попали в списки фильтрации. Кроме того, фильтрация становится все агрессивнее к трафику, который &lt;em&gt;похож&lt;/em&gt; на WebRTC: долгоживущим UDP-потокам или характерным STUN-пакетам поверх TCP. TURN-сервер, который безупречно работает из Амстердама, может за ночь стать недоступным из Москвы, если соседний сервис из того же IP-диапазона попал под фильтрацию.&lt;/p&gt;

&lt;p&gt;Практический вывод: &lt;strong&gt;одного TURN-развертывания, даже качественного, недостаточно&lt;/strong&gt;. Мы стали размещать TURN-серверы в разных юрисдикциях, чтобы у пользователей из конкретного региона всегда был доступный сервер-посредник с IP-адресом и hostname, которые не блокируются на их сетевом маршруте. Конфигурация WebRTC вычисляется для каждой организации во время выполнения через внутренний реестр сервисов, а конфигурация из переменных окружения служит запасным вариантом. Это позволяет направлять клиентов в подходящий TURN-пул без нового развертывания.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgbixl2r9t99otyf2p93.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgbixl2r9t99otyf2p93.png" alt="TURN в достижимой юрисдикции передает медиатрафик по TLS :443 для пользователя за национальным DPI-фильтром" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Сборка конфигурации
&lt;/h3&gt;

&lt;p&gt;Сборщик конфигурации превращает JSON из переменных окружения в &lt;code&gt;RTCConfiguration&lt;/code&gt;. Он небольшой, но ошибки в значениях по умолчанию здесь приводят к особенно неприятным последствиям, поэтому мы сделали его строгим:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getRTCConfiguration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;stun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;icePolicy&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;WebRTCConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;RTCConfiguration&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RTCConfiguration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;iceServers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})),&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;stun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;iceTransportPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;icePolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;icePolicy&lt;/code&gt; настраивается: по умолчанию это &lt;code&gt;'all'&lt;/code&gt;, но на уровне конфигурации его можно переключить в &lt;code&gt;'relay'&lt;/code&gt;. Это удобно для отладки и для клиентов, которым по юридическим причинам нужен единый маршрут через TURN. Код звонка при этом не меняется.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Идеальное согласование (Perfect Negotiation) с буферизацией ICE
&lt;/h2&gt;

&lt;p&gt;Оба участника могут одновременно попытаться создать предложение соединения (offer). В WebRTC такая коллизия называется конфликтом предложений (glare). Паттерн W3C &lt;a href="https://w3c.github.io/webrtc-pc/#perfect-negotiation-example" rel="noopener noreferrer"&gt;Perfect Negotiation&lt;/a&gt; назначает одну сторону «вежливой» (polite peer) и позволяет ей откатить собственное локальное описание, если входящее предложение конфликтует с уже созданным локальным предложением. Мы реализуем этот подход через стандартные флаги &lt;code&gt;makingOffer&lt;/code&gt;, &lt;code&gt;ignoreOffer&lt;/code&gt; и &lt;code&gt;polite&lt;/code&gt;. Вежливой стороной у нас выступает принимающий участник.&lt;/p&gt;

&lt;p&gt;Важная практическая деталь: ICE-кандидаты могут прийти &lt;em&gt;раньше&lt;/em&gt;, чем будет установлено удаленное описание (&lt;code&gt;remoteDescription&lt;/code&gt;). Это особенно часто проявляется в медленных сетях: SDP доходит с задержкой, а ICE-кандидаты начинают поступать почти сразу. В современных браузерах вызов &lt;code&gt;addIceCandidate&lt;/code&gt; до &lt;code&gt;setRemoteDescription&lt;/code&gt; приводит к ошибке. Поэтому мы &lt;strong&gt;буферизуем&lt;/strong&gt; ICE-кандидаты для каждого участника и очищаем буфер только после успешного &lt;code&gt;setRemoteDescription&lt;/code&gt;. На практике в буфере обычно всего несколько кандидатов, но без этого механизма звонок на нестабильных сетях может не установиться без явной ошибки.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Восстановление: шаг за шагом
&lt;/h2&gt;

&lt;p&gt;Эта часть потребовала больше всего итераций. WebRTC дает &lt;code&gt;connectionState&lt;/code&gt;, &lt;code&gt;iceConnectionState&lt;/code&gt;, &lt;code&gt;iceGatheringState&lt;/code&gt;, &lt;code&gt;signalingState&lt;/code&gt;, но ни одно из этих состояний нельзя считать исчерпывающим источником правды. Соединение с &lt;code&gt;connectionState === 'connected'&lt;/code&gt; все еще может передавать ноль байт, если сетевой маршрут перестал пропускать трафик. А состояние &lt;code&gt;iceConnectionState === 'disconnected'&lt;/code&gt; иногда восстанавливается само через секунду, если дать браузеру время.&lt;/p&gt;

&lt;p&gt;Поэтому мы объединили несколько сигналов и сделали ступенчатую схему восстановления с пятью порогами:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heartbeatDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heartbeatThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heartbeatDelay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 5 s&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statsDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recoveryDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;// full restart&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iceRestartDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;recoveryDelay&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;//  9 s — ICE restart&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videoWarmUpDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;waitingForInternetDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// give up&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Схема выглядит так:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feewli769y5pim2t7eg2i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feewli769y5pim2t7eg2i.png" alt="Машина состояний восстановления: Healthy, PreProblem, Degraded, IceRestart, FullRestart, WaitInternet и Ended" width="800" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;В текстовом виде это работает так:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Каждые 2,5 секунды&lt;/strong&gt; каждая сторона отправляет &lt;code&gt;SOCKET_PING&lt;/code&gt;, а вторая отвечает &lt;code&gt;SOCKET_PONG&lt;/code&gt;. Два пропуска подряд, то есть 5 секунд, означают, что путь через сигнальный сервер, скорее всего, недоступен.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Примерно каждые 5 секунд&lt;/strong&gt; вызывается &lt;code&gt;pc.getStats()&lt;/code&gt;, после чего сравниваются счетчики RTP-байтов. Если не изменились ни входящие, ни исходящие байты, медиапоток считается замершим независимо от состояния WebRTC.&lt;/li&gt;
&lt;li&gt;При &lt;strong&gt;первом&lt;/strong&gt; признаке проблемы (&lt;code&gt;connectionState === 'failed'&lt;/code&gt;, ICE disconnected/closed, неактивный медиапоток или отсутствие передачи данных) мы сохраняем отметку &lt;code&gt;preProblemDiscovered&lt;/code&gt;. Если на следующем цикле проблема сохраняется, она повышается до &lt;code&gt;problemDiscovered&lt;/code&gt;. Такая задержка на один цикл убрала большую часть ложных срабатываний в тестах.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Через 9 секунд&lt;/strong&gt; подтвержденной проблемы, если соединение с сигнальным сервером живо и текущая сторона является инициатором звонка, вызывается &lt;code&gt;pc.restartIce()&lt;/code&gt;. Это запускает повторное ICE-согласование без пересоздания &lt;code&gt;RTCPeerConnection&lt;/code&gt;: операция требует мало ресурсов и обычно выполняется быстро.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Через 18 секунд&lt;/strong&gt; выполняется полный &lt;code&gt;restartCall()&lt;/code&gt;: текущий &lt;code&gt;RTCPeerConnection&lt;/code&gt; закрывается, через сокет отправляется &lt;code&gt;RESTART_CALL&lt;/code&gt;, вторая сторона также очищает свое состояние, после чего соединение создается заново. Это более ресурсоемкий, но надежный вариант восстановления.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Если недоступен сам сокет&lt;/strong&gt; или &lt;code&gt;navigator.onLine === false&lt;/code&gt;, перезапуск не запускается: для него все равно нет рабочего канала управления. В этом состоянии мы ждем до 60 секунд; только после этого интерфейс показывает сообщение: «Connection lost. Please check internet connection.»&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;У этой логики много условий и переходов, поэтому мы многократно проверяли ее в условиях искусственно ограниченной сети. Ключевой блок из цикла проверки статистики:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;recoveryDelay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;connected&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;missedHeartbeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socketPingReceived&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;waitingForInternetDelay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;endCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connection lost. Please check internet connection.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;endCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;updateParticipant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;healthCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;caller&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;restartCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// caller must manage the call, but if it avoids its responsibilities...&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;waitingForInternetDelay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;recoveryDelay&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;endCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;updateParticipant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;healthCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Есть два важных ограничения:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Перезапуск инициирует только вызывающая сторона.&lt;/strong&gt; Если обе стороны одновременно начнут пересоздавать соединение, они будут постоянно конфликтовать на уровне SDP. Принимающая сторона реагирует на сигнал перезапуска, но сама его не запускает. Если же инициатор перестал выполнять эту роль — например, браузер заморозил его вкладку, — принимающая сторона завершает звонок самостоятельно после более длинного таймаута (&lt;code&gt;waitingForInternetDelay + recoveryDelay = 78 s&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Время «с момента проблемы» считается от &lt;code&gt;Math.max(problemDiscovered, lastSocketConnected, socketPingRestored, lastIceStateChange)&lt;/code&gt;. Если сокет кратко восстановился или ICE-состояние на секунду вернулось в норму, таймер восстановления сбрасывается. Без этого звонок можно было бы завершить уже после того, как кратковременный сбой фактически прошел.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Как мы находим замерший медиапоток
&lt;/h2&gt;

&lt;p&gt;Один из самых полезных фрагментов кода в проекте оказался очень небольшим. Мы отслеживаем изменение счетчиков RTP-байтов из &lt;code&gt;pc.getStats()&lt;/code&gt;. Если за полный интервал проверки не изменились ни отправленные, ни полученные байты, соединение считается неработоспособным независимо от значения &lt;code&gt;iceConnectionState&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isDataTransmitted&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Participant&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statInterval&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesReceived&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesTime&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevReceived&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevTime&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesSent&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevSent&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deltaTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deltaReceived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesReceived&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevReceived&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deltaSent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesSent&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevSent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// only judge if there were enough time to transmit some data&lt;/span&gt;
    &lt;span class="c1"&gt;// Connection is alive if EITHER data is being received OR sent (not both required)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;deltaTime&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;statsDelay&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deltaReceived&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;deltaSent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Проверка &lt;code&gt;deltaReceived &amp;gt; 0 || deltaSent &amp;gt; 0&lt;/code&gt;, а не &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, была осознанным решением. У одного из участников может быть выключен микрофон, а часть медиаданных может временно идти только в одном направлении. Если считать соединение живым только при одновременном росте обоих счетчиков, система будет ошибочно завершать нормальные звонки. Поэтому для проверки достаточно, чтобы данные передавались хотя бы в одну сторону.&lt;/p&gt;

&lt;p&gt;Также есть короткий запас времени на появление первого медиапотока после установки звонка (&lt;code&gt;videoWarmUpDelay = 8_000&lt;/code&gt;). Некоторые браузеры, особенно Safari, заметно задерживают первый видеокадр даже после &lt;code&gt;connectionState === 'connected'&lt;/code&gt;. Если проверять отсутствие видео раньше восьми секунд, система будет выдавать ложные срабатывания.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Браузерные войны: Chrome, Firefox, Safari
&lt;/h2&gt;

&lt;p&gt;Больше всего времени в проекте заняли различия между браузерами. Вот конкретные случаи, которые стоит зафиксировать.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safari: &lt;code&gt;MediaRecorder&lt;/code&gt; требует &lt;code&gt;timeSlice&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;В Chrome и Firefox &lt;code&gt;MediaRecorder.start()&lt;/code&gt; без аргументов возвращает один большой &lt;code&gt;Blob&lt;/code&gt; при вызове &lt;code&gt;stop()&lt;/code&gt;; если передать &lt;code&gt;timeSlice&lt;/code&gt; в миллисекундах, данные будут приходить периодически. В Safari вызов &lt;code&gt;start()&lt;/code&gt; без &lt;code&gt;timeSlice&lt;/code&gt; может не отдавать данные вообще. При этом рекордер выглядит рабочим: нет ошибок, промисы не отклоняются, но &lt;code&gt;ondataavailable&lt;/code&gt; не приносит полезных данных.&lt;/p&gt;

&lt;p&gt;На этом мы потеряли два дня в сценарии самостоятельного видеотеста. Исправление — флаг &lt;code&gt;useTimeSlice&lt;/code&gt;, включаемый только для нужного браузера, в обоих классах рекордера:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useTimeSlice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;browserName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Safari&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useTimeSlice&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TIME_SLICE&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TIME_SLICE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1000 ms&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Есть и второй уровень проблемы: на границе 30-секундного фрагмента рекордер останавливается и запускается снова. В Safari данные снова появляются только если передавать &lt;code&gt;timeSlice&lt;/code&gt; в &lt;code&gt;start()&lt;/code&gt; &lt;em&gt;при каждом запуске&lt;/em&gt;. Поэтому тот же флаг используется и при перезапуске рекордера.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firefox: названия устройств пустые, пока не запросишь доступ
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;navigator.mediaDevices.enumerateDevices()&lt;/code&gt; возвращает &lt;code&gt;label: ""&lt;/code&gt; для каждого устройства, пока пользователь хотя бы раз не выдал доступ через &lt;code&gt;getUserMedia()&lt;/code&gt;. Chrome может заполнить подписи до запроса, если сайт уже был авторизован раньше; Firefox — нет. Поэтому мы сначала перечисляем устройства, затем запрашиваем доступ, после чего &lt;strong&gt;перечисляем устройства повторно&lt;/strong&gt; и показываем обновленные подписи в списках выбора.&lt;/p&gt;

&lt;h3&gt;
  
  
  Выбор MIME
&lt;/h3&gt;

&lt;p&gt;Chrome и Firefox предпочитают WebM: VP8/VP9 для видео и Opus для аудио. Safari надежно кодирует только MP4 с AVC и AAC. Мы при загрузке проверяем поддержку и выбираем лучшую доступную комбинацию, а для Safari запасным вариантом используем &lt;code&gt;'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'&lt;/code&gt;. Это решение влияет и на расширение загружаемого файла (&lt;code&gt;.webm&lt;/code&gt; или &lt;code&gt;.mp4&lt;/code&gt;), по которому серверная часть выбирает нужный конвейер последующей обработки.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS
&lt;/h3&gt;

&lt;p&gt;На iOS в ограничениях для видео мы всегда указываем &lt;code&gt;facingMode: 'user'&lt;/code&gt;, чтобы по умолчанию открывалась фронтальная камера. &lt;code&gt;deviceId: { exact: … }&lt;/code&gt; на iOS иногда выбирает не тот объектив, который ожидает пользователь. Это был небольшой, но устойчивый источник ошибок, пока мы не стандартизировали поведение через &lt;code&gt;facingMode&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Оборудование: проверить, заменить, восстановить
&lt;/h2&gt;

&lt;p&gt;Интервью и самостоятельные видеотесты — это &lt;em&gt;записываемая&lt;/em&gt; оценка реальных людей, поэтому оборудование должно работать правильно. Здесь помогают три вещи.&lt;/p&gt;

&lt;h3&gt;
  
  
  9.1 Проверка микрофона перед звонком
&lt;/h3&gt;

&lt;p&gt;Перед входом в звонок пользователь проходит проверку звука в модальном окне с живым индикатором громкости. Мы создаем &lt;code&gt;AudioContext&lt;/code&gt; + &lt;code&gt;AnalyserNode&lt;/code&gt; на отдельном потоке &lt;code&gt;getUserMedia({ audio: { deviceId: { exact } } })&lt;/code&gt;, снимаем частотные данные, отслеживаем пиковый уровень в dB и сравниваем его с порогом −35 dB. Если микрофон пользователя не достигает этого порога, мы предупреждаем его до начала звонка, а не тогда, когда интервьюер уже ждет.&lt;/p&gt;

&lt;h3&gt;
  
  
  9.2 Горячая замена во время звонка
&lt;/h3&gt;

&lt;p&gt;Если кандидат подключил USB-гарнитуру во время звонка, соединение не должно прерываться. Для этого нужны две вещи:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Хук медиаустройств слушает &lt;code&gt;navigator.mediaDevices.ondevicechange&lt;/code&gt; с задержкой в 1 секунду: при подключении устройство часто порождает несколько событий подряд.&lt;/li&gt;
&lt;li&gt;Когда пользователь выбирает новое устройство, &lt;code&gt;RTCPeerConnection&lt;/code&gt; не пересогласуется. Мы заменяем трек через &lt;code&gt;RTCRtpSender.replaceTrack()&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// useVideoCall switchTrack (wired from useMediaDevices)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;participantsRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in-call&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSenders&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;newTrack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceTrack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTrack&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// no renegotiation&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;localStreamRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;replaceTrack&lt;/code&gt; здесь именно тот инструмент, который нужен: новое согласование по схеме «предложение/ответ» (offer/answer) не требуется, удаленная сторона видит непрерывный трек, а рекордер, если он уже работает, продолжает запись.&lt;/p&gt;

&lt;h3&gt;
  
  
  9.3 Восстановление, когда устройство исчезло
&lt;/h3&gt;

&lt;p&gt;Если активная камера или микрофон &lt;em&gt;исчезают&lt;/em&gt; — пользователь отключил USB-устройство, разрешение было отозвано на уровне ОС или браузер выбрал другое устройство, — включается двухступенчатое восстановление:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Мягкое восстановление.&lt;/strong&gt; &lt;code&gt;track.onended&lt;/code&gt; помечает тип устройства как требующий замены. Затем срабатывает обработчик &lt;code&gt;devicechange&lt;/code&gt; с задержкой, мы заново перечисляем устройства, выбираем первое доступное устройство нужного типа и вызываем &lt;code&gt;switchDevices&lt;/code&gt; через тот же путь &lt;code&gt;replaceTrack&lt;/code&gt;, что и при обычной ручной замене. Для удаленного участника звонок продолжается без заметного разрыва.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Жесткое восстановление.&lt;/strong&gt; Если замены нет — все камеры отключены или разрешения отозваны, — локальный медиапоток корректно останавливается, сохраняется контекст восстановления со старыми и новыми device IDs, а наверх поднимается типизированная ошибка (&lt;code&gt;isNotAllowedError&lt;/code&gt;, &lt;code&gt;isNotFoundError&lt;/code&gt;, &lt;code&gt;isNotReadableError&lt;/code&gt;, &lt;code&gt;isOverconstrainedError&lt;/code&gt;). Ее принимает отдельный интерфейс для ошибок медиаустройств и показывает правильное действие: «Разрешите доступ к камере», «Подключите камеру», «Закройте другое приложение, которое использует камеру» и так далее.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Есть еще одна важная деталь: виртуальный фон создает &lt;strong&gt;синтетический трек&lt;/strong&gt; с искусственным &lt;code&gt;deviceId&lt;/code&gt;. Если записать этот ID обратно в состояние выбранной камеры, в следующий раз пользователь откроет настройки и увидит UUID, который не соответствует ни одному реальному устройству. Исправление — разделить &lt;strong&gt;выбор пользователя&lt;/strong&gt; (&lt;code&gt;selectedCameraId&lt;/code&gt;) и &lt;strong&gt;текущее рабочее состояние&lt;/strong&gt; (&lt;code&gt;currentCamera&lt;/code&gt;). Когда виртуальный фон включен, мы сохраняем исходный ID выбранной камеры, а не ID синтетического трека.&lt;/p&gt;

&lt;p&gt;Все операции с устройствами сериализованы через &lt;code&gt;Semaphore&lt;/code&gt;, чтобы быстрые клики по выпадающему списку устройств не создавали перекрывающиеся &lt;code&gt;getUserMedia&lt;/code&gt;-вызовы и брошенные треки.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Конвейер записи
&lt;/h2&gt;

&lt;p&gt;У записи был собственный набор сложностей: видеозвонок &lt;em&gt;многопользовательский&lt;/em&gt; — интервьюер, кандидат, иногда больше людей, — а на выходе нужен один удобный для просмотра артефакт.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2964xxugv74rp0dzzi9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2964xxugv74rp0dzzi9f.png" alt="Конвейер записи: компоновка видео через canvas, отдельные аудиорекордеры по участникам, исправление для Safari с timeSlice и загрузка фрагментами" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Для звонка (&lt;code&gt;CallRecorder&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; каждого участника отрисовывается в &lt;strong&gt;сетку 2×2 на canvas&lt;/strong&gt; на каждом кадре анимации. &lt;code&gt;canvas.captureStream()&lt;/code&gt; превращает результат в &lt;code&gt;MediaStream&lt;/code&gt; с одним составным видеотреком.&lt;/li&gt;
&lt;li&gt;Этот составной поток передается в один &lt;code&gt;MediaRecorder&lt;/code&gt; для видео.&lt;/li&gt;
&lt;li&gt;Параллельно аудиотрек каждого участника передается в &lt;strong&gt;свой&lt;/strong&gt; &lt;code&gt;MediaRecorder&lt;/code&gt;. Раздельное аудио упрощает последующую транскрибацию по спикерам — не нужно разделять уже смешанный звук — и позволяет не сводить WebRTC-аудиотреки на лету с потерей качества.&lt;/li&gt;
&lt;li&gt;Каждые 30 секунд рекордеры переключаются на новый фрагмент: &lt;code&gt;stop()&lt;/code&gt; отдает последний &lt;code&gt;Blob&lt;/code&gt; через &lt;code&gt;ondataavailable&lt;/code&gt;, затем &lt;code&gt;start(TIME_SLICE)&lt;/code&gt; начинает следующий фрагмент. Каждый &lt;code&gt;Blob&lt;/code&gt; сразу загружается с &lt;code&gt;interviewId&lt;/code&gt;, &lt;code&gt;participantId&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt; и расширением медиатипа. Если загрузка не удалась, повторяется отправка только этого фрагмента: один сетевой сбой не должен уничтожить всю запись.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Для самостоятельного видеотеста (&lt;code&gt;AssessmentRecorder&lt;/code&gt;) компоновка не нужна: говорит один человек, поэтому достаточно одного рекордера для объединенного потока. Дополнительно &lt;code&gt;AssessmentRecorder&lt;/code&gt; дает &lt;strong&gt;барьер остановки на основе промиса&lt;/strong&gt;: &lt;code&gt;stopRecording()&lt;/code&gt; возвращает объект &lt;code&gt;Promise&lt;/code&gt;, который завершается только после того, как последний &lt;code&gt;Blob&lt;/code&gt; прошел через &lt;code&gt;ondataavailable&lt;/code&gt;. Благодаря этому код интерфейса, который хочет заменить устройство, точно знает, что поток уже можно безопасно разбирать.&lt;/p&gt;

&lt;p&gt;Оба рекордера учитывают обходное решение для Safari с &lt;code&gt;timeSlice&lt;/code&gt; из §8.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Тестирование в реальном мире
&lt;/h2&gt;

&lt;p&gt;Юнит-тесты ловят баги протокола, но ни один юнит-тест не скажет, что coturn во Франкфурте на этой неделе недоступен из сети конкретного российского провайдера. Поэтому мы сделали клиентскую диагностическую проверку.&lt;/p&gt;

&lt;p&gt;Для каждого сервера она создает одноразовый &lt;code&gt;RTCPeerConnection&lt;/code&gt;, указывает ему ровно один STUN- или TURN-сервер, открывает канал данных (data channel), чтобы принудительно запустить сбор кандидатов, и ждет результата ICE-процедуры. Возможные результаты:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Рабочий STUN-сервер должен дать кандидата типа &lt;code&gt;srflx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Рабочий TURN-сервер должен дать кандидата типа &lt;code&gt;relay&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Все остальное — тайм-аут через 10 секунд или ошибка соединения — означает, что этот сервер недоступен &lt;em&gt;для этого клиента&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Результат приводится к простой форме &lt;code&gt;{ url, type, status, candidates[], duration }&lt;/code&gt; и показывается на внутренней диагностической странице. Когда пользователь пишет «звонок не подключается», он может нажать одну кнопку и сразу увидеть, какие серверы доступны из его сети. Это сократило отладку с многодневной переписки до одного скриншота.&lt;/p&gt;

&lt;p&gt;В сценарии интервью поверх серверных проверок есть еще слой контроля нарушений (&lt;strong&gt;anti-cheating&lt;/strong&gt;), который отмечает переключения вкладок во время интервью. Это не совсем WebRTC-задача, но механизм работает через тот же сокетный канал, а пропавший сигнал о переключении вкладки бывает полезным косвенным признаком проблемы с соединением.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Что мы вынесли
&lt;/h2&gt;

&lt;p&gt;Коротко и без особого порядка:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Не доверяйте ни одной отдельной машине состояний WebRTC.&lt;/strong&gt; Перепроверяйте ее по изменениям RTP-счетчиков через &lt;code&gt;getStats()&lt;/code&gt;. &lt;code&gt;'connected'&lt;/code&gt; — это заявление, а не факт.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Буферизуйте ICE-кандидаты.&lt;/strong&gt; На медленном канале они действительно приходят раньше, чем завершается &lt;code&gt;setRemoteDescription&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Восстанавливайте соединение постепенно.&lt;/strong&gt; Пульсовая проверка → ICE restart → полный перезапуск → ожидание восстановления сети. Слишком ранний полный перезапуск хуже, чем еще несколько секунд работы в деградировавшем состоянии.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Восстановлением должна управлять одна сторона.&lt;/strong&gt; Перезапуск запускает инициатор звонка, а принимающая сторона обрабатывает полученный сигнал. Если перезапуск запускают оба участника, начинаются конфликты при согласовании SDP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Поднимайте собственный coturn на :443 с TLS.&lt;/strong&gt; Бесплатные STUN-серверы годятся; бесплатных TURN-серверов либо нет, либо они не масштабируются, либо находятся не там, где нужно.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Размещайте TURN в нескольких юрисдикциях&lt;/strong&gt;, если ваши пользователи находятся за национальными фильтрами. Российский случай сделал это для нас обязательным, но та же логика работает для корпоративного DPI и строгих мобильных операторов где угодно. Конфигурация на уровне клиента позволяет выбрать подходящий TURN-пул без нового развертывания.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Используйте &lt;code&gt;replaceTrack&lt;/code&gt; для замены устройств.&lt;/strong&gt; Пересогласовывать весь &lt;code&gt;RTCPeerConnection&lt;/code&gt; на каждый клик «сменить микрофон» медленно и ненадежно.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Разделяйте выбор пользователя и текущее рабочее состояние&lt;/strong&gt;, когда появляются виртуальные фоны или другие синтетические треки. Это избавляет от трудноуловимых ошибок в настройках устройств.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safari требует &lt;code&gt;timeSlice&lt;/code&gt; для &lt;code&gt;MediaRecorder&lt;/code&gt;.&lt;/strong&gt; Всегда. И после каждого stop/start его нужно передавать снова.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firefox требует повторного &lt;code&gt;enumerateDevices()&lt;/code&gt; после выдачи разрешения.&lt;/strong&gt; Иначе список устройств будет заполнен пустыми подписями.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Сделайте собственную проверку доступности серверов.&lt;/strong&gt; Это небольшая задача, но она позволяет диагностировать проблемы в пользовательской сети за 30 секунд.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Видеозвонки Recruiter.AI продолжают развиваться: впереди настройка предпочтительных кодеков, одновременная передача нескольких видеопотоков разного качества (simulcast) и автоматическая адаптация качества к доступной пропускной способности канала. Но описанная здесь архитектура уже выдержала тысячи реальных интервью и самостоятельных видеотестов, в том числе в сетях, где WebRTC обычно ведет себя непредсказуемо. Если вы создаете похожую систему, начинать стоит не с интерфейса, а со схемы восстановления соединения и TURN-стратегии. От этих двух решений зависит все остальное.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webrtc</category>
      <category>javascript</category>
      <category>network</category>
    </item>
    <item>
      <title>Building a Resilient WebRTC Video Call</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Wed, 22 Apr 2026 23:49:54 +0000</pubDate>
      <link>https://dev.to/andreik/building-a-resilient-webrtc-video-call-12j4</link>
      <guid>https://dev.to/andreik/building-a-resilient-webrtc-video-call-12j4</guid>
      <description>&lt;p&gt;&lt;em&gt;How we designed, built, and battle‑tested the video calling engine that powers video interviews and video self‑assessment inside &lt;a href="https://recruiter-ai.andersenlab.com/" rel="noopener noreferrer"&gt;Recruiter.AI&lt;/a&gt; — and the long list of problems we had to beat along the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before we dive in, a quick note on the scope of this article. WebRTC is a huge topic, and there are entire books written about it. This is not a WebRTC tutorial. It’s a postmortem of the specific architecture and design decisions we made for Recruiter.AI’s video calling features, and the rationale behind them. &lt;/p&gt;

&lt;p&gt;You can find the video call code in the &lt;a href="https://github.com/gsbelarus/gdmn-meet" rel="noopener noreferrer"&gt;GDMN Meet repository&lt;/a&gt;, try it out on &lt;a href="https://meet.gdmn.app" rel="noopener noreferrer"&gt;meet.gdmn.app&lt;/a&gt;, or see it in action in the &lt;a href="https://recruiter-ai.andersenlab.com/" rel="noopener noreferrer"&gt;Recruiter.AI interview flow&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why this was built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://recruiter-ai.andersenlab.com/" rel="noopener noreferrer"&gt;Recruiter.AI&lt;/a&gt; from &lt;strong&gt;Andersen Lab&lt;/strong&gt; is a platform for AI‑assisted hiring. Two of its most visible features — &lt;strong&gt;video interviews&lt;/strong&gt; between a candidate and an interviewer, and &lt;strong&gt;video self‑assessment&lt;/strong&gt; where a candidate records answers to questions alone in front of the camera — depend on a live video stream that has to &lt;em&gt;just work&lt;/em&gt;. Not in the lab. In a candidate's bedroom, on a hotel Wi‑Fi, on a flaky mobile network, behind a corporate firewall, behind a national DPI filter.&lt;/p&gt;

&lt;p&gt;What we will cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the end‑to‑end architecture and why it looks the way it does;&lt;/li&gt;
&lt;li&gt;why we built our own thin Socket.IO signaling server;&lt;/li&gt;
&lt;li&gt;STUN, TURN, and the &lt;strong&gt;jurisdiction problem&lt;/strong&gt; (for example, the one that stops calls working inside Russian RKN‑filtered networks);&lt;/li&gt;
&lt;li&gt;Perfect Negotiation with buffered ICE candidates;&lt;/li&gt;
&lt;li&gt;the multi‑tier connection recovery needed to survive bad links;&lt;/li&gt;
&lt;li&gt;how we detect &lt;em&gt;frozen&lt;/em&gt; media that every browser still calls "connected";&lt;/li&gt;
&lt;li&gt;Chrome vs. Firefox vs. Safari — concrete workarounds;&lt;/li&gt;
&lt;li&gt;equipment checks, hot‑swap of cameras and microphones mid‑call;&lt;/li&gt;
&lt;li&gt;the recording pipeline (canvas compositing + chunked upload + a Safari fix that took two days to find);&lt;/li&gt;
&lt;li&gt;the tooling we built to test it all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. The big picture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzolji7co58gw26t2ezlm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzolji7co58gw26t2ezlm.png" alt="Architecture overview — peers, Socket.IO signaling, STUN, TURN over TLS in multiple jurisdictions, and the chunked‑upload recording sink" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At a high level there are four planes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Media plane (P2P).&lt;/strong&gt; Two &lt;code&gt;RTCPeerConnection&lt;/code&gt;s exchange encrypted DTLS‑SRTP traffic, ideally peer‑to-peer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signaling plane.&lt;/strong&gt; A thin Socket.IO server — provided by the application server — that forwards typed messages between participants in a room and keeps just enough presence state to reject duplicate joins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ICE plane.&lt;/strong&gt; STUN servers (Google, Cloudflare, and our own coturn server) for NAT traversal, plus a set of &lt;strong&gt;TURN over TLS&lt;/strong&gt; relays on port 443 that act as media hairpins when direct P2P can't be established — which is more often than you'd think.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recording sink.&lt;/strong&gt; A chunked upload API that accepts 30‑second media blobs produced by &lt;code&gt;CallRecorder&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the client, responsibilities are split across two cooperating hooks wired together by a thin integration layer: one owns the peer connection, signaling and recovery; the other owns cameras, microphones, device hot‑plug handling and the local stream. Keeping those responsibilities separate was one of the first good decisions — swapping devices mid‑call is already tricky enough without also owning signaling state.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The signaling server: thin, recoverable, ours
&lt;/h2&gt;

&lt;p&gt;For signaling we run a Socket.IO endpoint as part of the application server. Its job is narrow and strictly defined:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dumb relay.&lt;/strong&gt; The server forwards messages and nothing else. No call state lives on the server, which means the signaling protocol can evolve without server‑side changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self‑recovering&lt;/strong&gt; at the socket layer so a candidate who blips off Wi‑Fi for ten seconds does not have to refresh the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presence‑aware, minimally.&lt;/strong&gt; It tracks room membership just enough to reject duplicate joins from the same participant (more on that below).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second point is the reason we picked Socket.IO over a raw WebSocket: Socket.IO ships &lt;code&gt;connectionStateRecovery&lt;/code&gt;, which buffers events for a configurable window and resumes the session if the client reconnects in time. The window is set to &lt;strong&gt;five minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The protocol itself is tiny: nine message types — JOIN/LEAVE, ENUM, CALLING, ANSWER_CALL, SIGNAL (which wraps both SDP and ICE), SOCKET_PING/PONG, RESTART_CALL, END_CALL, MAKE_CALL. Each message carries &lt;code&gt;fromId&lt;/code&gt;/&lt;code&gt;toId&lt;/code&gt; so the server can stay stateless about who is talking to whom.&lt;/p&gt;

&lt;p&gt;Sequence of a successful call setup:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv51h7k122m2lnvhwhy8u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv51h7k122m2lnvhwhy8u.png" alt="Signaling sequence — JOIN, CALLING, offer/answer, ICE trickle, heartbeat" width="800" height="1471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two small but important details in the connection bootstrap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every connection attempt sends a fresh &lt;code&gt;instanceId&lt;/code&gt; (UUID v4) alongside the &lt;code&gt;participantId&lt;/code&gt;. If a second tab joins with the same &lt;code&gt;participantId&lt;/code&gt;, the server replies with &lt;code&gt;already_in_room&lt;/code&gt; and the second tab backs off. This is how we detect and reject &lt;strong&gt;duplicate entries&lt;/strong&gt; (the most common cause of "I see two of myself in the grid" reports from testers).&lt;/li&gt;
&lt;li&gt;On tab unload, the client fires a LEAVE message through &lt;code&gt;navigator.sendBeacon()&lt;/code&gt;. &lt;code&gt;window.onbeforeunload&lt;/code&gt; is not reliable on mobile browsers; &lt;code&gt;sendBeacon&lt;/code&gt; is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. STUN, TURN, and the jurisdiction problem
&lt;/h2&gt;

&lt;p&gt;Getting ICE candidates is the easy part — point &lt;code&gt;RTCPeerConnection&lt;/code&gt; at a few public STUN servers and you're done. The default config:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stun:stun.l.google.com:19302&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stun:stun.cloudflare.com:3478&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;our own server, for redundancy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hard part is TURN. A non‑trivial fraction of users — corporate networks, symmetric NATs, mobile carriers with carrier‑grade NAT, and, crucially for Recruiter.AI, &lt;strong&gt;users inside nationally filtered networks&lt;/strong&gt; — cannot establish direct peer connections. They need a relay.&lt;/p&gt;

&lt;p&gt;So we stood up our own &lt;strong&gt;coturn&lt;/strong&gt; instance and crucially exposed TURN on port &lt;strong&gt;443&lt;/strong&gt;. The ICE server URL looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;turns&lt;/span&gt;:&lt;span class="n"&gt;coturn&lt;/span&gt;.&lt;span class="n"&gt;our_server&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;:&lt;span class="m"&gt;443&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 443 matters. A lot of corporate firewalls and national DPI filters let TCP/443 TLS through because blocking it would break HTTPS. A TURN server listening on that port, inside a proper TLS tunnel, is indistinguishable from a normal HTTPS session to most DPI. That is why we use &lt;strong&gt;TURN over TLS on :443&lt;/strong&gt; rather than the default UDP 3478 or TCP 5349 transports.&lt;/p&gt;

&lt;p&gt;But there is a further twist, particular to running Recruiter.AI for users inside restrictive countries.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Russian filter problem
&lt;/h3&gt;

&lt;p&gt;Russia's national traffic filtering infrastructure (the RKN system, including the TSPU boxes deployed at ISP peering points) does not only block &lt;em&gt;well‑known&lt;/em&gt; addresses. It also drops connections to endpoints whose IPs or SNI hostnames have been added to the filter lists, and it has become increasingly aggressive about throttling or blocking traffic that &lt;em&gt;looks&lt;/em&gt; like WebRTC — long‑lived UDP flows, or STUN packet patterns over TCP. A TURN server that works flawlessly from Amsterdam can suddenly be unreachable from Moscow overnight, because &lt;em&gt;somebody else's&lt;/em&gt; traffic from the same IP range poisoned the filter.&lt;/p&gt;

&lt;p&gt;The practical consequence is that &lt;strong&gt;a single TURN deployment — even a good one — is not enough&lt;/strong&gt;. What we ended up doing is placing TURN servers in different jurisdictions so that users from any particular region always have a reachable relay whose IP/hostname is clean for &lt;em&gt;their&lt;/em&gt; network path. The WebRTC config is resolved per organization at runtime through an internal service registry, with an env‑based default as a fallback. That lets us route tenants to the TURN pool whose jurisdiction and IP hygiene actually work for them, without requiring a redeploy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F326l24vz1wawv0qxishl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F326l24vz1wawv0qxishl.png" alt="TURN in a reachable jurisdiction, relaying media on :443 for a peer behind a national DPI filter" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the config
&lt;/h3&gt;

&lt;p&gt;The config builder is where env JSON becomes &lt;code&gt;RTCConfiguration&lt;/code&gt;. It's small but it's the place that gets wrong defaults wrong in exciting ways, so we kept it strict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getRTCConfiguration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;stun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;icePolicy&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;WebRTCConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;RTCConfiguration&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RTCConfiguration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;iceServers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})),&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;stun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;iceTransportPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;icePolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;icePolicy&lt;/code&gt; is configurable: default is &lt;code&gt;'all'&lt;/code&gt;, but it can be flipped to &lt;code&gt;'relay'&lt;/code&gt; at config level — useful for debugging, or for tenants where a uniform TURN path is required for jurisdictional reasons — without touching the call code.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Perfect Negotiation with buffered ICE
&lt;/h2&gt;

&lt;p&gt;Two peers can try to create an offer at the same time — classic WebRTC glare. The W3C &lt;a href="https://w3c.github.io/webrtc-pc/#perfect-negotiation-example" rel="noopener noreferrer"&gt;Perfect Negotiation pattern&lt;/a&gt; assigns one side as "polite" and lets it roll back its own local description if an incoming offer collides. We implement it with the usual &lt;code&gt;makingOffer&lt;/code&gt;, &lt;code&gt;ignoreOffer&lt;/code&gt; and &lt;code&gt;polite&lt;/code&gt; flags. The callee is the polite peer.&lt;/p&gt;

&lt;p&gt;There is one detail that bites: ICE candidates can arrive &lt;em&gt;before&lt;/em&gt; the remote description is set, especially on a slow network where SDP takes a while to traverse but ICE candidates start trickling immediately. Calling &lt;code&gt;addIceCandidate&lt;/code&gt; before &lt;code&gt;setRemoteDescription&lt;/code&gt; throws in modern browsers. So we &lt;strong&gt;buffer&lt;/strong&gt; ICE candidates per peer and flush them after &lt;code&gt;setRemoteDescription&lt;/code&gt; resolves. The buffer is not large in practice — usually a handful of candidates — but without it, the call silently fails to connect on bad links.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Recovery, step by step
&lt;/h2&gt;

&lt;p&gt;This was the part that took the most iteration. WebRTC gives you &lt;code&gt;connectionState&lt;/code&gt;, &lt;code&gt;iceConnectionState&lt;/code&gt;, &lt;code&gt;iceGatheringState&lt;/code&gt;, &lt;code&gt;signalingState&lt;/code&gt; — and at least one of them lies to you at any given moment. A &lt;code&gt;connectionState === 'connected'&lt;/code&gt; peer can still be sending zero bytes because the path got silently blackholed. A peer in &lt;code&gt;iceConnectionState === 'disconnected'&lt;/code&gt; can recover on its own in a second if you just wait.&lt;/p&gt;

&lt;p&gt;So we layered several signals together and built an escalating recovery ladder with five thresholds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heartbeatDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heartbeatThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heartbeatDelay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 5 s&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statsDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recoveryDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;// full restart&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iceRestartDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;recoveryDelay&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;//  9 s — ICE restart&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videoWarmUpDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;waitingForInternetDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// give up&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ladder:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzhejr9gctxnlh0hxcluw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzhejr9gctxnlh0hxcluw.png" alt="Recovery state machine — Healthy → PreProblem → Degraded → IceRestart → FullRestart → WaitInternet → Ended" width="800" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Translated into words:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every 2.5 s&lt;/strong&gt; each side sends a &lt;code&gt;SOCKET_PING&lt;/code&gt;; the counterpart replies with &lt;code&gt;SOCKET_PONG&lt;/code&gt;. Two missed in a row (5 s) means the socket path is likely dead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every ~5 s&lt;/strong&gt; a stats poll runs &lt;code&gt;pc.getStats()&lt;/code&gt; and diffs RTP byte counters. If neither inbound nor outbound bytes moved, media is frozen regardless of what the state machine says.&lt;/li&gt;
&lt;li&gt;On the &lt;strong&gt;first&lt;/strong&gt; tick where something looks wrong (&lt;code&gt;connectionState === 'failed'&lt;/code&gt;, ICE disconnected/closed, stream inactive, or no bytes moved), we stamp a &lt;code&gt;preProblemDiscovered&lt;/code&gt;. If the next tick still looks bad, it promotes to &lt;code&gt;problemDiscovered&lt;/code&gt;. This debounce by itself removed the majority of false alarms in testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After 9 s&lt;/strong&gt; of real trouble, if the socket is still alive and we are the caller, &lt;code&gt;pc.restartIce()&lt;/code&gt; is called. That triggers ICE renegotiation without tearing down the peer connection — cheap and fast when it works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After 18 s&lt;/strong&gt;, we escalate to a full &lt;code&gt;restartCall()&lt;/code&gt;: close the &lt;code&gt;RTCPeerConnection&lt;/code&gt;, send &lt;code&gt;RESTART_CALL&lt;/code&gt; over the socket so the other side does the same, and rebuild from scratch. Expensive but reliable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the socket itself is down&lt;/strong&gt; or &lt;code&gt;navigator.onLine === false&lt;/code&gt;, we wait instead of restarting — there is nothing to restart &lt;em&gt;over&lt;/em&gt;. The wait runs up to 60 s; only then does the UI say "Connection lost. Please check internet connection."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The branching logic is dense and it took a lot of trial runs on a throttled network to get right. The key block, from the stats‑tick loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;recoveryDelay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;connected&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;missedHeartbeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socketPingReceived&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;waitingForInternetDelay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;endCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connection lost. Please check internet connection.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;endCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;updateParticipant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;healthCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;caller&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;restartCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// caller must manage the call, but if it avoids its responsibilities...&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;waitingForInternetDelay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;recoveryDelay&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;endCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;updateParticipant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;healthCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two subtleties worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only the caller initiates restart.&lt;/strong&gt; If both sides raced to restart, they'd endlessly step on each other's SDP. The callee passively follows. But if the caller &lt;em&gt;abdicates&lt;/em&gt; — for example, its tab was suspended — the callee eventually gives up on its own after a longer timeout (&lt;code&gt;waitingForInternetDelay + recoveryDelay = 78 s&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The "elapsed since problem" time is computed against &lt;code&gt;Math.max(problemDiscovered, lastSocketConnected, socketPingRestored, lastIceStateChange)&lt;/code&gt;. If the socket flaps or the ICE state briefly recovers, the recovery clock resets. Without that, a call that bounced through a short outage would get killed even after it stabilized.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Detecting frozen media
&lt;/h2&gt;

&lt;p&gt;Possibly the most satisfying bit of code in the whole project is also the smallest. We keep a running delta of RTP bytes from &lt;code&gt;pc.getStats()&lt;/code&gt;. If both sent and received deltas are zero over a full stats window, the connection is dead — no matter what &lt;code&gt;iceConnectionState&lt;/code&gt; claims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isDataTransmitted&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Participant&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statInterval&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesReceived&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesTime&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevReceived&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevTime&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesSent&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevSent&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deltaTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deltaReceived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesReceived&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevReceived&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deltaSent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesSent&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytesPrevSent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// only judge if there were enough time to transmit some data&lt;/span&gt;
    &lt;span class="c1"&gt;// Connection is alive if EITHER data is being received OR sent (not both required)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;deltaTime&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;statsDelay&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deltaReceived&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;deltaSent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;deltaReceived &amp;gt; 0 || deltaSent &amp;gt; 0&lt;/code&gt; (rather than &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;) was a conscious call. One side can legitimately be muted; demanding &lt;em&gt;both&lt;/em&gt; byte streams to flow would produce spurious "dead call" alarms when a participant stops talking. As long as &lt;em&gt;something&lt;/em&gt; is moving, the link is alive.&lt;/p&gt;

&lt;p&gt;There is also a tiny grace period for the &lt;em&gt;initial&lt;/em&gt; media flow after a call is established (&lt;code&gt;videoWarmUpDelay = 8_000&lt;/code&gt;). Some browsers — Safari in particular — take a noticeable time to push the first video frame even after &lt;code&gt;connectionState === 'connected'&lt;/code&gt;. Complaining about "no video" before eight seconds produces a false positive.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Browser wars: Chrome, Firefox, Safari
&lt;/h2&gt;

&lt;p&gt;Nothing in this project ate more time than cross‑browser quirks. The concrete ones worth writing down:&lt;/p&gt;

&lt;h3&gt;
  
  
  Safari: &lt;code&gt;MediaRecorder&lt;/code&gt; is silent until you ask
&lt;/h3&gt;

&lt;p&gt;In Chrome and Firefox, &lt;code&gt;MediaRecorder.start()&lt;/code&gt; with no argument produces one big blob on &lt;code&gt;stop()&lt;/code&gt;, or periodic blobs if you give it a &lt;code&gt;timeSlice&lt;/code&gt; in ms. In Safari, &lt;code&gt;start()&lt;/code&gt; without a &lt;code&gt;timeSlice&lt;/code&gt; can silently never emit anything. The recorder &lt;em&gt;thinks&lt;/em&gt; it is recording — no errors, no rejected promises, just an eternal nothing.&lt;/p&gt;

&lt;p&gt;This one cost us two days in the self‑assessment flow. The fix is a browser‑gated &lt;code&gt;useTimeSlice&lt;/code&gt; flag in both recorder classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useTimeSlice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;browserName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Safari&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useTimeSlice&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TIME_SLICE&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TIME_SLICE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1000 ms&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a second layer: when the 30‑second chunk boundary fires and the recorder stops and restarts, Safari only starts producing data again if &lt;code&gt;timeSlice&lt;/code&gt; is passed to &lt;code&gt;start()&lt;/code&gt; &lt;em&gt;every&lt;/em&gt; time. So the same flag governs the restart path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firefox: device labels are empty until you ask
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;navigator.mediaDevices.enumerateDevices()&lt;/code&gt; returns &lt;code&gt;label: ""&lt;/code&gt; for every device until &lt;code&gt;getUserMedia()&lt;/code&gt; has been granted at least once. Chrome fills labels pre‑permission if the site has been granted before; Firefox does not. So we enumerate once, request permission, then &lt;strong&gt;re‑enumerate&lt;/strong&gt; and use the new labels to populate the device pickers.&lt;/p&gt;

&lt;h3&gt;
  
  
  MIME negotiation
&lt;/h3&gt;

&lt;p&gt;Chrome and Firefox prefer WebM (VP8/VP9 video, Opus audio). Safari only reliably encodes MP4 with AVC and AAC. We run a support check at load time and pick the best supported combination, falling back to &lt;code&gt;'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'&lt;/code&gt; for Safari. That decision ripples into the uploaded file extension (&lt;code&gt;.webm&lt;/code&gt; vs &lt;code&gt;.mp4&lt;/code&gt;), which the backend uses to route to the right post‑processing pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS
&lt;/h3&gt;

&lt;p&gt;On iOS the video constraints always include &lt;code&gt;facingMode: 'user'&lt;/code&gt; so the front camera comes up by default; &lt;code&gt;deviceId: { exact: … }&lt;/code&gt; on iOS sometimes picks an unexpected lens. A small but persistent source of bug reports until standardising on &lt;code&gt;facingMode&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Hardware: check, swap, recover
&lt;/h2&gt;

&lt;p&gt;Because interviews and self‑assessments are &lt;em&gt;recorded&lt;/em&gt; evaluations of real people, getting the hardware right matters. Three things help here.&lt;/p&gt;

&lt;h3&gt;
  
  
  9.1 The pre‑call sound check
&lt;/h3&gt;

&lt;p&gt;Before the user joins a call, a pre‑call sound‑check modal paired with a live volume meter walks them through a microphone test. It spins up an &lt;code&gt;AudioContext&lt;/code&gt; + &lt;code&gt;AnalyserNode&lt;/code&gt; on a dedicated &lt;code&gt;getUserMedia({ audio: { deviceId: { exact } } })&lt;/code&gt; stream, captures frequency data, tracks a peak dB value, and compares against a −35 dB threshold. If the user's microphone peaks below that, we warn them before the call starts rather than after the interviewer is already waiting.&lt;/p&gt;

&lt;h3&gt;
  
  
  9.2 Hot‑swap during a call
&lt;/h3&gt;

&lt;p&gt;If a candidate plugs in USB headphones mid‑call, the call must not die. That has two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The media‑devices hook listens to &lt;code&gt;navigator.mediaDevices.ondevicechange&lt;/code&gt; with a 1‑second debounce (the event fires several times in quick succession during a plug).&lt;/li&gt;
&lt;li&gt;When the user picks a new device, the peer connection is not renegotiated — the track is swapped underneath it using &lt;code&gt;RTCRtpSender.replaceTrack()&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// useVideoCall switchTrack (wired from useMediaDevices)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;participantsRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in-call&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSenders&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;newTrack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceTrack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTrack&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// no renegotiation&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;localStreamRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;replaceTrack&lt;/code&gt; is the right tool: no new offer/answer, the remote side sees an uninterrupted track, and the recorder (if running) keeps going.&lt;/p&gt;

&lt;h3&gt;
  
  
  9.3 Recovery when a device vanishes
&lt;/h3&gt;

&lt;p&gt;If the active camera or microphone &lt;em&gt;disappears&lt;/em&gt; — USB unplug, OS‑level permission revoked, the browser decides to put a different device in your face — a two‑stage recovery kicks in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Soft recovery.&lt;/strong&gt; The &lt;code&gt;track.onended&lt;/code&gt; handler marks the device type as "needs replacement". The &lt;code&gt;devicechange&lt;/code&gt; debounce fires, we re‑enumerate, pick the first available device of that kind, and call &lt;code&gt;switchDevices&lt;/code&gt; through the same &lt;code&gt;replaceTrack&lt;/code&gt; path as a normal user swap. To the remote peer, nothing happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard recovery.&lt;/strong&gt; If no replacement exists (all cameras unplugged; permissions revoked), the local stream is stopped cleanly, a recovery context is preserved with the old/new device IDs, and a typed error (&lt;code&gt;isNotAllowedError&lt;/code&gt;, &lt;code&gt;isNotFoundError&lt;/code&gt;, &lt;code&gt;isNotReadableError&lt;/code&gt;, &lt;code&gt;isOverconstrainedError&lt;/code&gt;) surfaces up to a dedicated media‑error UI. It uses the error flags to show the right remediation — "Grant camera permission", "Plug in a camera", "Close the other app using your camera", etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One extra wrinkle: background removal (the virtual‑background feature) generates its own &lt;strong&gt;synthetic track&lt;/strong&gt; with a fake &lt;code&gt;deviceId&lt;/code&gt;. Naively writing that synthetic ID back to the selected‑camera state would leave the user with a UUID that doesn't map to any real device the next time they open the settings panel. The fix was to separate &lt;strong&gt;user intent&lt;/strong&gt; (&lt;code&gt;selectedCameraId&lt;/code&gt;) from &lt;strong&gt;runtime state&lt;/strong&gt; (&lt;code&gt;currentCamera&lt;/code&gt;): when background is on, we remember the originally selected camera ID rather than the synthetic one.&lt;/p&gt;

&lt;p&gt;All device operations are serialized through a &lt;code&gt;Semaphore&lt;/code&gt; so rapid user clicks on the device dropdown can't produce overlapping &lt;code&gt;getUserMedia&lt;/code&gt; calls and orphaned tracks.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. The recording pipeline
&lt;/h2&gt;

&lt;p&gt;Recording had its own set of challenges because the video call is &lt;em&gt;multi‑party&lt;/em&gt; (interviewer + candidate, sometimes more) but the output needs to be a single watchable asset.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkf943p6l7531jlq2i6rc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkf943p6l7531jlq2i6rc.png" alt="Recording pipeline — canvas compositing + per‑participant audio + 30 s chunked upload" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a call (&lt;code&gt;CallRecorder&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every participant's &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element is drawn into a &lt;strong&gt;2×2 canvas grid&lt;/strong&gt; every animation frame. &lt;code&gt;canvas.captureStream()&lt;/code&gt; turns that into a &lt;code&gt;MediaStream&lt;/code&gt; with a single composite video track.&lt;/li&gt;
&lt;li&gt;That composite stream feeds one &lt;code&gt;MediaRecorder&lt;/code&gt; for video.&lt;/li&gt;
&lt;li&gt;In parallel, each participant's audio track feeds &lt;strong&gt;its own&lt;/strong&gt; &lt;code&gt;MediaRecorder&lt;/code&gt;. Keeping audio separate means per‑speaker transcription downstream is straightforward (no need to separate mixed audio), and it sidesteps the issue of WebRTC audio tracks being difficult to mix losslessly on the fly.&lt;/li&gt;
&lt;li&gt;Every 30 s the recorders rotate: &lt;code&gt;stop()&lt;/code&gt; emits the last blob via &lt;code&gt;ondataavailable&lt;/code&gt;, then &lt;code&gt;start(TIME_SLICE)&lt;/code&gt; begins the next chunk. Each blob is uploaded immediately, tagged with &lt;code&gt;interviewId&lt;/code&gt;, &lt;code&gt;participantId&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt;, and the media type extension. If the upload fails, the chunk is retried independently — no single network blip can lose the whole recording.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a self‑assessment (&lt;code&gt;AssessmentRecorder&lt;/code&gt;), there is no compositing — it's a single speaker — so a single recorder for the combined stream is enough. What &lt;code&gt;AssessmentRecorder&lt;/code&gt; adds is a &lt;strong&gt;promise‑based stop barrier&lt;/strong&gt;: &lt;code&gt;stopRecording()&lt;/code&gt; returns a promise that only resolves once the last blob has fired through &lt;code&gt;ondataavailable&lt;/code&gt;, so UI code that wants to swap devices knows it's safe to tear down the stream.&lt;/p&gt;

&lt;p&gt;Both recorders honour the Safari &lt;code&gt;timeSlice&lt;/code&gt; workaround from §8.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Testing in the wild
&lt;/h2&gt;

&lt;p&gt;Unit tests can catch protocol bugs, but no unit test will tell you that coturn in Frankfurt is unreachable from a specific Russian ISP this week. So we shipped a client‑side probe.&lt;/p&gt;

&lt;p&gt;Per‑server, it spins up a disposable &lt;code&gt;RTCPeerConnection&lt;/code&gt;, points it at just that one STUN or TURN server, creates a data channel to force candidate gathering, and waits for the ICE process to complete or fail. The result is one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A working STUN server should produce a &lt;code&gt;srflx&lt;/code&gt; candidate.&lt;/li&gt;
&lt;li&gt;A working TURN server should produce a &lt;code&gt;relay&lt;/code&gt; candidate.&lt;/li&gt;
&lt;li&gt;Anything else — timeout after 10 s, connection error — means that server is broken &lt;em&gt;for this client&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a simple &lt;code&gt;{ url, type, status, candidates[], duration }&lt;/code&gt; shape, surfaced in an internal diagnostics page. When a user reports "the call won't connect", they can click one button and instantly see which servers are reachable from their network. This has cut our debug cycle from "multi‑day email thread" to "one screenshot".&lt;/p&gt;

&lt;p&gt;On top of the server probes, the interview flow also has an &lt;strong&gt;anti‑cheating&lt;/strong&gt; layer that flags tab switches during an interview — not a WebRTC issue per se, but it rides on the same socket channel, and a missing tab‑switch signal is a useful side‑channel indicator of a broken connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Lessons learned
&lt;/h2&gt;

&lt;p&gt;Condensed, in no particular order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust any single WebRTC state machine.&lt;/strong&gt; Cross‑check with RTP byte deltas over &lt;code&gt;getStats()&lt;/code&gt;. &lt;code&gt;'connected'&lt;/code&gt; is a claim, not a fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buffer ICE candidates.&lt;/strong&gt; On a slow link they &lt;em&gt;will&lt;/em&gt; arrive before &lt;code&gt;setRemoteDescription&lt;/code&gt; resolves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalate recovery gradually.&lt;/strong&gt; Heartbeat → ICE restart → full restart → wait for internet. Full restart too eagerly is worse than letting the call limp for 10 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only one side drives recovery.&lt;/strong&gt; Caller restarts, callee follows. Both restarting is chaos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run your own coturn on :443 with TLS.&lt;/strong&gt; Free STUN servers are fine; free TURN servers either don't exist, don't scale, or are in the wrong country.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Place TURN in multiple jurisdictions&lt;/strong&gt; when your users live behind national filters. The Russian case made this non‑negotiable for us, but the same logic applies to corporate DPI and strict mobile carriers everywhere. Per‑tenant config lets you route the TURN pool to the client without a redeploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;replaceTrack&lt;/code&gt; for device swaps.&lt;/strong&gt; Renegotiating the whole peer connection on every "switch microphone" click is slow and fragile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate user intent from runtime state&lt;/strong&gt; when virtual backgrounds or other synthetic tracks enter the picture. Future‑you will thank you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safari's &lt;code&gt;MediaRecorder&lt;/code&gt; needs &lt;code&gt;timeSlice&lt;/code&gt;.&lt;/strong&gt; Always. And pass it again after every stop/start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firefox needs a device re‑enumeration after permission grant.&lt;/strong&gt; Otherwise your device picker is full of blank labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship your own server‑reachability probe.&lt;/strong&gt; The cost is a Saturday; the payoff is being able to diagnose a user's broken network in 30 seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recruiter.AI's video call still evolves — codec preferences, simulcast, and bandwidth‑adaptive quality are on the roadmap — but the bones described here have held up through thousands of real interviews and self‑assessments, including plenty conducted on networks that would make any WebRTC engineer wince. For anyone building something similar: start with the recovery ladder and the TURN story, not with the UI. Everything else is downstream of getting those two right.&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>messenger</category>
      <category>network</category>
      <category>recruiting</category>
    </item>
    <item>
      <title>What do you do while waiting for the code?</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Tue, 21 Apr 2026 00:15:37 +0000</pubDate>
      <link>https://dev.to/andreik/what-do-you-do-while-waiting-for-the-code-ca8</link>
      <guid>https://dev.to/andreik/what-do-you-do-while-waiting-for-the-code-ca8</guid>
      <description>&lt;p&gt;Waiting for the LLM to produce the next patch is painful. The last time I spent so much time just waiting was in the late 80s, when the MK-61 programmable calculator took me to the Moon and back. It blinked while crunching the digits (the same way older people murmur numbers when doing addition or subtraction in their heads), running a 105-step program in over a minute.&lt;/p&gt;

&lt;p&gt;Yes, there were games for programmable calculators. Plenty of them, actually. There was even a section in a popular Soviet technical magazine where new games were published, discussed, and sometimes accompanied by sci-fi stories based on them.&lt;/p&gt;

&lt;p&gt;What do other people do in those moments of waiting now? Just watch the streaming progress, like the heroes in &lt;em&gt;The Matrix&lt;/em&gt; watching green falling hieroglyphs? Open another IDE in parallel, trying to match the heroics of Alexei Stakhanov? (They weren’t actually heroic, we know.) Keep an eye on the hopeless Phoenix Suns losing to the Oklahoma City Thunder? Learn a foreign language? Enjoy cute Korean cheerleaders on YouTube? Or just wonder what else you could do once it’s finally done with the programming?&lt;/p&gt;

&lt;p&gt;Just curious.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>programming</category>
    </item>
    <item>
      <title>A Correspondence Match</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Thu, 22 Jan 2026 21:52:42 +0000</pubDate>
      <link>https://dev.to/andreik/a-correspondence-match-2op1</link>
      <guid>https://dev.to/andreik/a-correspondence-match-2op1</guid>
      <description>&lt;p&gt;In the old Polish comedy "Mistrz zawsze traci" from 1976, a crafty French pharmacist arranges a correspondence match between two grandmasters who have never met each other.&lt;/p&gt;

&lt;p&gt;For the past couple of days, I’ve been feeling like that very swindler. I explained to Copilot what needed to be done, waited for it to write the code, then wrapped it all up into a pull request. Then I waited for Cursor BugBot to do the review, copied its recommendations, and forwarded them back to Copilot for fixes. And so it goes, round and round.&lt;/p&gt;

&lt;p&gt;Alas, exposure awaits us all sooner or later.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>vibecoding</category>
      <category>jokes</category>
    </item>
    <item>
      <title>How to connect the Next.js MCP server to VS Code Copilot Chat</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Sat, 22 Nov 2025 21:35:49 +0000</pubDate>
      <link>https://dev.to/andreik/how-to-connect-the-nextjs-mcp-server-to-vs-code-copilot-chat-3pii</link>
      <guid>https://dev.to/andreik/how-to-connect-the-nextjs-mcp-server-to-vs-code-copilot-chat-3pii</guid>
      <description>&lt;p&gt;The MCP server functionality is built in starting from Next.js 16 and doesn't require any additional configuration in the settings files. The path to the MCP server API is &lt;code&gt;/_next/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Assuming your dev server is running on the port number 5002, do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;code&gt;.vscode&lt;/code&gt; folder in the root of your project, create a &lt;code&gt;mcp.json&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Put the following content inside it:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nextjs-mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:5002/_next/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Save the file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run your project's dev server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open Copilot Chat.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ask something specific to Next.js, for example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;nextjs devtools, are there any build errors?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If prompted, allow running the MCP server and executing actions. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In our case, the answer was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No Next.js devtools build errors are currently reported. Let me know if you want me to keep monitoring or run a build/test explicitly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now let's ask what the MCP server could do for us:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;nextjs devtools, tell me what you are able to do?&lt;/p&gt;

&lt;p&gt;I’m connected to the Next.js devtools MCP server, so I can pull diagnostics like build/runtime errors, fetch the dev-server logs, list app/pages routes, and inspect server actions. If you need anything along those lines—checking status, seeing routes, digging into errors—just let me know which detail you want.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Enjoy, and happy vibe coding!&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>mcp</category>
      <category>githubcopilot</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>Unit Testing React Hooks with NextJS Back-End: Challenges and Solutions</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Sat, 31 May 2025 10:30:11 +0000</pubDate>
      <link>https://dev.to/andreik/unit-testing-react-hooks-with-nextjs-back-end-challenges-and-solutions-4pk5</link>
      <guid>https://dev.to/andreik/unit-testing-react-hooks-with-nextjs-back-end-challenges-and-solutions-4pk5</guid>
      <description>&lt;p&gt;Implementing unit tests for React hooks that interact with a back-end can be challenging, especially when the back-end server is built with NextJS. As of Spring 2025, Copilot does not provide assistance for this task, so developers must rely on their own expertise. We hope that the insights shared in this document will save others in similar situations from spending excessive time searching through resources and attempting to integrate various libraries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial state:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A NextJS application where the NextJS server also is the application’s back-end server.&lt;/li&gt;
&lt;li&gt;A React hook to be tested that heavily interacts with the back-end server.&lt;/li&gt;
&lt;li&gt;Node test runner used as testing framework.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Custom server.ts
&lt;/h3&gt;

&lt;p&gt;In our case the &lt;code&gt;server.ts&lt;/code&gt; file is customized to accommodate specific back-end logic and provide global data structures shared among Next.js server pages and route handlers.&lt;/p&gt;

&lt;p&gt;In development mode, the server is run using the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cross-env NODE_ENV=development tsx watch --conditions=typescript --tsconfig tsconfig.server.json ./server.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production mode, the server is built and then executed as a JavaScript script using bare Node.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cross-env NODE_ENV=production next build &amp;amp;&amp;amp; tsc --project tsconfig.server.json"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cross-env NODE_ENV=production node dist/server.js"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test React hooks outside the browser environment, install the following libraries as development dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @testing-library/dom @testing-library/react global-jsdom jsdom &lt;span class="nt"&gt;-D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Writing a unit test
&lt;/h3&gt;

&lt;p&gt;For unit testing, a separate instance of the server should be launched before the test begins. The React hook is then tested, and upon completion of the test, the server instance is shut down.&lt;/p&gt;

&lt;p&gt;Running node directly appears to be the main issue. Problems were encountered when running the server through &lt;code&gt;pnpm run&lt;/code&gt; or &lt;code&gt;tsx&lt;/code&gt;; the node test runner would hang at the end until &lt;code&gt;Ctrl-C&lt;/code&gt; was manually pressed in the terminal window.&lt;/p&gt;

&lt;p&gt;Here are functions to start and safely shut down the server as a child process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server setup&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChildProcess&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Start the backend server before tests
 */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startServer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Starting server...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;serverProcess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist/server.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;serverReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;serverReady&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server failed to start within timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server stdout:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Look for server ready indicator&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server is running on&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;serverReady&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;serverReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// Small delay to ensure server is fully ready&lt;/span&gt;
            &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server stderr:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server process error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server process exited with code:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;serverReady&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server exited with code &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Stop the backend server after tests
 */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stopServer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cleaning up test environment...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Stopping server...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Set up exit handler before killing&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exitHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server stopped&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;serverProcess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;exitHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Try graceful shutdown first&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sending SIGTERM to server...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Force kill after timeout&lt;/span&gt;
      &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serverProcess&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;killed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Force killing server...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGKILL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="c1"&gt;// Give it a moment to clean up&lt;/span&gt;
          &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;serverProcess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unit test is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Wait for server to be ready by making health check requests
 */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForServer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server is ready!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server failed to become ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Server not ready yet&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server failed to become ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;useObject Hook Tests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;startServer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;stopServer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;


  &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Server is Running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should ensure the test environment is set up&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server process should be running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForServer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serverProcess&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;killed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server process should not be killed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Entity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Data Loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should load data successfully when entityParam is provided&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderHook&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nf"&gt;useObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;entityParam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntityName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Test&lt;/span&gt;
          &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Initially should be in loading state&lt;/span&gt;
        &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Wait for data to load&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// Verify final state&lt;/span&gt;
        &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To conduct the test, it is necessary to initialize a simulated DOM beforehand. This is achieved by passing the &lt;code&gt;–import&lt;/code&gt; switch to the tsx utility. The command required is as follows (adjust for the actual .env location in your case):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm run build &amp;amp;&amp;amp; tsx --enable-source-maps --import=global-jsdom/register --test --env-file ../../.env.local"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All sources are compiled in this &lt;a href="https://gist.github.com/gsbelarus/c2d87221bc8f6b09e67f081516e2ff59" rel="noopener noreferrer"&gt;gist&lt;/a&gt; for your convenience.&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>testing</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Высококачественная транскрипция зашумлённых двухканальных телефонных звонков</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Fri, 02 May 2025 07:24:18 +0000</pubDate>
      <link>https://dev.to/andreik/vysokokachiestviennaia-transkriptsiia-zashumlionnykh-dvukhkanalnykh-tieliefonnykh-zvonkov-pbl</link>
      <guid>https://dev.to/andreik/vysokokachiestviennaia-transkriptsiia-zashumlionnykh-dvukhkanalnykh-tieliefonnykh-zvonkov-pbl</guid>
      <description>&lt;h3&gt;
  
  
  Высококачественная транскрипция зашумлённых двухканальных телефонных звонков
&lt;/h3&gt;

&lt;p&gt;В одном из наших недавних проектов возникла необходимость транскрибировать телефонные звонки с крайне низким качеством звука. Аудиозаписи было трудно разобрать: речь часто прерывалась перекрывающимися голосами оператора и клиента. Кроме того, на стороне клиента часто присутствовали фоновый шум и посторонние звуки, улавливаемые микрофоном. Вдобавок к этому, речь часто содержала смесь языков и суржик.&lt;/p&gt;

&lt;p&gt;В предыдущих проектах мы успешно использовали модель &lt;code&gt;WhisperX&lt;/code&gt;, которая показала достойные результаты при удовлетворительном качестве аудио. Однако в данном случае полученные транскрипции оказались слишком низкого качества и непригодными для использования.&lt;/p&gt;

&lt;p&gt;С выходом модели &lt;code&gt;GPT-4o-transcribe&lt;/code&gt; мы решили оценить её возможности. В отличие от Whisper, эта модель принимает детализированные подсказки (prompts), что позволяет значительно улучшить качество транскрипции. Кроме того, &lt;code&gt;GPT-4o-transcribe&lt;/code&gt; можно инструктировать распознавать реплики Оператора и Клиента. К сожалению, из-за отсутствия полноценной диаризации модель часто путает, кто из участников говорит.&lt;/p&gt;

&lt;p&gt;Клиент возлагал большие надежды на проект, но остался недоволен первым результатом. Он требовал точной транскрипции, но не был готов выделить время или бюджет на сбор обучающих данных, аннотирование аудио или дообучение Whisper. Единственным улучшением по сравнению с исходными записями было то, что аудиоканалы оператора и клиента записывались отдельно.&lt;/p&gt;

&lt;h3&gt;
  
  
  Первая попытка (неудачная)
&lt;/h3&gt;

&lt;p&gt;Первым логичным шагом было транскрибировать каждый канал отдельно с помощью Whisper, а затем объединить транскрипции по временным меткам. Этот подход не сработал. Whisper сильно «галлюцинирует» во время длинных пауз и на коротких репликах из одного слова. Кроме того, возвращаемые временные метки часто включали не только речь, но и окружающие участки тишины. Попытка скорректировать эти метки с помощью фильтра тишины из &lt;code&gt;ffmpeg&lt;/code&gt; тоже не дала удовлетворительных результатов.&lt;/p&gt;

&lt;h3&gt;
  
  
  Гибридный подход
&lt;/h3&gt;

&lt;p&gt;Мы пересмотрели наш подход. &lt;code&gt;GPT-4o-transcribe&lt;/code&gt; выдаёт качественный текст, но без временных меток. &lt;code&gt;Whisper&lt;/code&gt;, напротив, даёт временные метки, но при этом сильно теряет в точности. Внимательный человек, сравнивая оба результата, вполне может восстановить точную структуру речи.&lt;/p&gt;

&lt;p&gt;Так мы пришли к следующему пайплайну:&lt;/p&gt;

&lt;h3&gt;
  
  
  Финальный алгоритм
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Транскрибировать канал клиента с помощью &lt;code&gt;gpt-4o-transcribe-mini&lt;/code&gt;. Затем определить язык речи с помощью &lt;code&gt;GPT-4o&lt;/code&gt;. Язык оператора известен заранее.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Транскрибировать оба канала с помощью &lt;code&gt;whisper-1&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Установить температуру в &lt;code&gt;0.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Использовать языковой prompt с несколькими типичными фразами.&lt;/li&gt;
&lt;li&gt;Результат — в формате VTT с временными метками.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Транскрибировать оба канала с помощью &lt;code&gt;gpt-4o-transcribe&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Установить температуру в &lt;code&gt;0.1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Использовать подробные prompts, адаптированные к содержанию каждого канала.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Prompt для Оператора:

   Ты эксперт в области аудиотранскрипции.

   1. Предоставленный аудиофайл содержит только реплики оператора из телефонного разговора.
   2. Оператор представляет компанию "XXXXXX", работающую под торговой маркой "YYYYYYY", веб-сайт "ZZZZZZZZ".
   3. Оператор рекламирует услуги кредитования. Он описывает условия, процесс выдачи кредита, процентные ставки, комиссии и сборы.
   4. Клиент может задавать вопросы, соглашаться с условиями, отказываться от них или просить больше не звонить.
   5. Разговор может вестись на языке Language1, языке Language2 или быть их сочетанием. Сохраняйте оригинальный язык реплик.
   6. Качество аудио низкое. Исправляйте галлюцинации и нерелевантный контент.

   Предоставь полную транскрипцию.

   Промпт для Клиента:

   Ты эксперт по расшифровке аудио.

   1. Аудио содержит только реплики клиента.
   2. Оператор продвигает кредитные услуги.
   3. Клиент может спрашивать об условиях, ставках, комиссиях или отклонить предложение.
   4. Клиент может запросить дополнительную информацию или отказаться от дальнейшего общения.
   5. Язык может быть украинским, русским или смешанным. Сохраняйте оригинальные языки.
   6. Качество аудио низкое. Исключайте галлюцинации и несвязанный контент.

   Предоставьте полную расшифровку.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4. Сопоставить транскрипт &lt;code&gt;GPT-4o&lt;/code&gt; с временными метками &lt;code&gt;Whisper&lt;/code&gt; с помощью &lt;code&gt;GPT-4.1&lt;/code&gt;, используя следующий prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Ты эксперт по анализу и восстановлению транскрипций телефонных звонков, связанных с продвижением кредитных услуг.

   Я предоставлю два текста:

   1. Транскрипция, созданная моделью gpt-4o-transcribe.
   2. Транскрипция в формате VTT с помеченными по времени репликами от whisper-1.

   Твоя задача — выровнять первую транскрипцию, используя временные метки из второй.

   * Если тексты отличаются из-за галлюцинаций, используйте версию GPT-4o.
   * Включите весь контент из GPT-4o.
   * Если временная метка отсутствует, добавьте текст к следующей строке   и скорректируйте время начала соответствующим образом.

   Верни только итоговую транскрипцию в формате VTT.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5. Скорректировать временные интервалы реплик Оператора с использованием периодов тишины, полученных через фильтры &lt;code&gt;ffmpeg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;6. Повторить шаги 4–5 для транскрипта Клиента.&lt;/p&gt;

&lt;p&gt;7. Объединить финальные VTT-транскрипты обоих каналов, обеспечив правильное распределение реплик по времени и по участникам разговора.&lt;/p&gt;

&lt;h3&gt;
  
  
  Заключение
&lt;/h3&gt;

&lt;p&gt;Это решение не является ни быстрым, ни дешёвым — оно требует многократных вызовов моделей OpenAI. Однако результат — &lt;strong&gt;почти идеальная транскрипция&lt;/strong&gt;, даже в тех случаях, когда &lt;code&gt;Whisper&lt;/code&gt; полностью проваливается.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Исходный код, адаптированный для работы с украинским и русским языками, можно найти &lt;a href="https://gist.github.com/gsbelarus/6cecb9ce4f6bb37db4956a0ccc4a41bf" rel="noopener noreferrer"&gt;здесь&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>whisper</category>
      <category>openai</category>
      <category>challenge</category>
      <category>promptengineering</category>
    </item>
    <item>
      <title>High-Quality Transcription of Noisy Dual-Channel Phone Calls</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Thu, 01 May 2025 10:16:52 +0000</pubDate>
      <link>https://dev.to/andreik/high-quality-transcription-of-noisy-dual-channel-phone-calls-92</link>
      <guid>https://dev.to/andreik/high-quality-transcription-of-noisy-dual-channel-phone-calls-92</guid>
      <description>&lt;p&gt;In one of our recent projects, we needed to transcribe phone calls with extremely poor audio quality. The recordings were hard to decipher, often interrupted by overlapping voices from both the operator and the client. Additionally, the client’s side frequently featured noisy backgrounds and extraneous sounds captured by the microphone. To make matters worse, the speech often contained a mix of languages and Surzhyk.&lt;/p&gt;

&lt;p&gt;In our previous work, we had successfully used &lt;code&gt;WhisperX&lt;/code&gt;, which performed reasonably well under decent conditions. However, in this case, the quality of the transcriptions was too poor to be useful.&lt;/p&gt;

&lt;p&gt;With the release of &lt;code&gt;GPT-4o-transcribe&lt;/code&gt;, we decided to evaluate its capabilities. This model accepts detailed prompts, unlike Whisper, which relies on keyword-based prompts. This flexibility significantly improves transcription quality. Moreover, &lt;code&gt;GPT-4o-transcribe&lt;/code&gt; can be instructed to distinguish between the Operator and the Client. Unfortunately, due to its lack of true diarization, this often results in confusion between speakers.&lt;/p&gt;

&lt;p&gt;The client had high expectations but was dissatisfied with the initial output. They requested accurate transcription but were unwilling to allocate time or budget for collecting training samples, annotating audio, or fine-tuning Whisper. The only improvement we had over raw recordings was that the operator’s and client’s audio channels were recorded separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial Attempt (Failed)
&lt;/h3&gt;

&lt;p&gt;The first logical approach was to transcribe each channel separately using Whisper, and then merge the transcripts based on the timestamps. This did not work well. Whisper tends to hallucinate heavily during long silences and with short, one-word utterances. Additionally, the returned timestamps often included stretches of surrounding silence rather than isolating the actual speech. We attempted to refine these timestamps using silence detection via &lt;code&gt;ffmpeg&lt;/code&gt;, but the results were still inaccurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Hybrid Strategy
&lt;/h3&gt;

&lt;p&gt;We took a step back and reassessed. &lt;code&gt;GPT-4o-transcribe&lt;/code&gt; produces high-quality text but lacks timestamps. &lt;code&gt;Whisper&lt;/code&gt;, on the other hand, provides timestamps but delivers lower-quality text. An attentive human could easily deduce the correct structure by comparing both.&lt;/p&gt;

&lt;p&gt;Thus, we built the following pipeline:&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Algorithm
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Transcribe the Client’s channel using &lt;code&gt;gpt-4o-transcribe-mini&lt;/code&gt;. Then, use &lt;code&gt;GPT-4o&lt;/code&gt; to detect the spoken language. The Operator's language is known in advance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Transcribe both channels using &lt;code&gt;whisper-1&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set temperature to &lt;code&gt;0.0&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;Use a language-specific prompt with a few representative phrases.
&lt;/li&gt;
&lt;li&gt;The output is in VTT format with timestamps.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Transcribe both channels using &lt;code&gt;gpt-4o-transcribe&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set temperature to &lt;code&gt;0.1&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;Use detailed prompts tailored to each channel’s content.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Operator’s Prompt:

   You are an expert in the field of audio transcription.
   1. The provided audio file contains only the operator’s lines from a phone conversation.
   2. The operator represents the company "XXXXXX", operating under the trade name "YYYYYYY", website "ZZZZZZZZ".
   3. The operator is advertising credit services.
   4. They describe terms, loan issuance process, interest rates, fees, and commissions.
   5. The client may ask questions, accept or reject terms, or request no further contact.
   6. The language may be Language1, Language2, or a mixture. Preserve original languages.
   7. Audio quality is poor. Fix hallucinations and unrelated content.

   Provide the full transcription.

   Client’s Prompt:

   You are an expert in audio transcription.
   1. The audio contains only the client’s lines.
   2. The operator promotes credit services.
   3. The client may ask about terms, rates, fees, or reject the offer.
   4. The client may request more info or refuse further contact.
   5. Language may be Ukrainian, Russian, or mixed. Preserve original languages.
   6. Audio quality is poor. Fix hallucinations and unrelated content.

   Provide the full transcription.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4. Align the &lt;code&gt;GPT-4o&lt;/code&gt; transcript to Whisper’s timestamps using &lt;code&gt;GPT-4.1&lt;/code&gt;. Use the following prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   You are an expert in analyzing and reconstructing transcripts of phone calls promoting credit services.
   I will provide two texts:
   1. A transcript generated using the gpt-4o-transcribe model.
   2. A transcript in VTT format with timestamped utterances from whisper-1.
   Your task: align the first transcript using timestamps from the second.
   - If text differs due to hallucinations, use GPT-4o’s version.
   - Include all content from GPT-4o.
   - If a timestamp is missing, prepend the text to the next line and adjust the start time accordingly.

   Return only the final VTT transcript.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5. Correct the Operator’s timestamps using silence periods detected via &lt;code&gt;ffmpeg&lt;/code&gt; filters.&lt;/p&gt;

&lt;p&gt;6. Repeat steps 4–5 for the Client’s transcript.&lt;/p&gt;

&lt;p&gt;7. Merge the final VTT transcripts from both channels, ensuring accurate timestamp ordering and correct speaker attribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;This solution is neither fast nor cheap — it involves multiple calls to OpenAI models — but it yields &lt;strong&gt;nearly perfect transcripts&lt;/strong&gt; even in cases where Whisper alone fails dramatically.&lt;/p&gt;

&lt;p&gt;You can find the code adapted for handling Ukrainian and Russian languages &lt;a href="https://gist.github.com/gsbelarus/6cecb9ce4f6bb37db4956a0ccc4a41bf" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>whisper</category>
      <category>openai</category>
      <category>challenge</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>Trees in SQL (part 2)</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Wed, 25 Dec 2024 16:28:43 +0000</pubDate>
      <link>https://dev.to/andreik/trees-in-sql-part-2-15pa</link>
      <guid>https://dev.to/andreik/trees-in-sql-part-2-15pa</guid>
      <description>&lt;p&gt;This is the second and final part of the article &lt;a href="https://dev.to/andreik/trees-in-sql-4fp"&gt;“Trees in SQL”&lt;/a&gt;. Following the principles of a good narrative, the most interesting part is saved for the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interval Tree
&lt;/h3&gt;

&lt;p&gt;Up until now, we have tried to work with a tree structure as if it were a graph. The challenges we faced—such as using recursion, an additional table, or a bulky string field—arose because SQL is inherently designed for working with sets rather than graphs.&lt;/p&gt;

&lt;p&gt;To represent a tree structure as nested sets, each node must be assigned two integer parameters: a left boundary and a right boundary. These parameters must satisfy the following conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The right boundary of a node must be greater than or equal to the left boundary.&lt;/li&gt;
&lt;li&gt;The left boundaries of descendants must be greater than the left boundary of their parent node.&lt;/li&gt;
&lt;li&gt;The right boundaries of descendants must be less than or equal to the right boundary of their parent node.&lt;/li&gt;
&lt;li&gt;The intervals defined by the left and right boundaries of nodes with the same parent must not overlap.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The following diagram illustrates the representation of a tree as nested sets:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrxtu63k6d5hjbnkgbzu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrxtu63k6d5hjbnkgbzu.png" alt="Image description" width="420" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To store an interval-based tree structure, we create the following table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To improve data access speed, two indexes are required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;test4_x_lb&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;DESCENDING&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;test4_x_rb&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Extracting Subtree Nodes
&lt;/h3&gt;

&lt;p&gt;A query to extract all nodes of a subtree with root node &lt;code&gt;P&lt;/code&gt; would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we replace the strict inequality on the left boundary with a non-strict one, the root of the subtree will also be included in the result set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manipulating Tree Data
&lt;/h3&gt;

&lt;p&gt;Compared to previously discussed structures, interval trees require the most effort when adding a new node or moving a node from one branch to another. In addition to two triggers, a stored procedure is also needed.&lt;/p&gt;

&lt;p&gt;We start with the procedure &lt;code&gt;EL_TEST4&lt;/code&gt;, which identifies and returns the left boundary for a descendant of a given node (&lt;code&gt;PARENT&lt;/code&gt;). If the parent’s interval is exhausted, it will be expanded by at least the value specified by the &lt;code&gt;DELTA&lt;/code&gt; parameter. During the interval expansion, all higher-level intervals are also expanded, and intervals to the right are shifted. Intervals falling within the boundaries defined by &lt;code&gt;LB2&lt;/code&gt; and &lt;code&gt;RB2&lt;/code&gt; are excluded from processing to prevent infinite loops when moving a subtree from one parent to another.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;EL_TEST4&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;PARENT&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;DELTA&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;LB2&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;RB2&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;LEFTBORDER&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;R2&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;MKey&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;MultiDelta&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="cm"&gt;/* Retrieve parent boundaries */&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;Parent&lt;/span&gt;
  &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="cm"&gt;/* Get the rightmost boundary of the parent's descendants, if any */&lt;/span&gt;
  &lt;span class="n"&gt;R2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;Parent&lt;/span&gt;
    &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;R2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="cm"&gt;/* If there are no descendants, use the parent's left boundary */&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;R2&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;R2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="cm"&gt;/* Check if there is enough space for another descendant */&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;R2&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;Delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="cm"&gt;/* If not, expand the interval */&lt;/span&gt;
    &lt;span class="n"&gt;MultiDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cm"&gt;/* Ensure the new range is sufficient */&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;Delta&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MultiDelta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="n"&gt;MultiDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;Delta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cm"&gt;/* Shift the right boundary of parent intervals */&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;LB2&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MultiDelta&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;LB2&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;RB2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt;
      &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MultiDelta&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cm"&gt;/* Shift both boundaries of intervals to the right of the parent interval */&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;LB2&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MultiDelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MultiDelta&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;
          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;LB2&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;RB2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt;
      &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MultiDelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MultiDelta&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;

  &lt;span class="cm"&gt;/* Return the found boundary */&lt;/span&gt;
  &lt;span class="n"&gt;LeftBorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;R2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trigger &lt;code&gt;TEST4_BI&lt;/code&gt; is called after inserting a record and assigns its left and right boundaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;test4_bi&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt;
  &lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
  &lt;span class="k"&gt;POSITION&lt;/span&gt; &lt;span class="mi"&gt;32000&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="cm"&gt;/* Check if adding the root of a new tree or a descendant to an existing node */&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="cm"&gt;/* If adding a new tree, set its left boundary after the highest known right boundary */&lt;/span&gt;
    &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="cm"&gt;/* Adding the first element to the table; use integers &amp;gt;0 for interval boundaries */&lt;/span&gt;
      &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt;
      &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="cm"&gt;/* Call the procedure to find a possible left boundary for the descendant. 
       If the parent's interval is exhausted, it will be expanded. */&lt;/span&gt;
    &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;el_test4&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;RETURNING_VALUES&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;

  &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trigger &lt;code&gt;TEST4_BU&lt;/code&gt; adjusts node intervals when moving a node to a different parent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="n"&gt;tree_e_invalid_parent&lt;/span&gt;
  &lt;span class="s1"&gt;'Invalid parent'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;test4_bu&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt;
  &lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
  &lt;span class="k"&gt;POSITION&lt;/span&gt; &lt;span class="mi"&gt;32000&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;OldDelta&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;L&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;NewL&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="cm"&gt;/* Check for changes in the parent */&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="cm"&gt;/* Check for circular references */&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;EXISTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt;
      &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="n"&gt;tree_e_invalid_parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt;
      &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="k"&gt;BEGIN&lt;/span&gt;
        &lt;span class="cm"&gt;/* Retrieve the rightmost boundary */&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
        &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NewL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="cm"&gt;/* Assume at least one element with a null parent exists */&lt;/span&gt;
        &lt;span class="n"&gt;NewL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NewL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt;
      &lt;span class="k"&gt;BEGIN&lt;/span&gt;
        &lt;span class="cm"&gt;/* Retrieve the new left boundary */&lt;/span&gt;
        &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;el_test4&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;RETURNING_VALUES&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NewL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;END&lt;/span&gt;

      &lt;span class="cm"&gt;/* Calculate the shift */&lt;/span&gt;
      &lt;span class="n"&gt;OldDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NewL&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="cm"&gt;/* Shift main branch boundaries */&lt;/span&gt;
      &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;OldDelta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;OldDelta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="cm"&gt;/* Shift child boundaries */&lt;/span&gt;
      &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;test4&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;OldDelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;OldDelta&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deleting all nested elements when a node is deleted is ensured by the &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; rule on the &lt;code&gt;PARENT&lt;/code&gt; field, so no additional actions are required to compress the intervals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;A reasonable question might arise: is there a universally optimal way to organize tree structures in SQL that suits all applications? Unfortunately, the answer is no. Each structure has its strengths and weaknesses that must be evaluated based on the specific requirements of the task. For instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What is the total number of records in the table?&lt;/strong&gt; For small trees, performance differences between methods are minimal and insignificant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How often will the tree data be modified?&lt;/strong&gt; Interval trees achieve the best results for data retrieval, but demand significant computational resources for inserting new records or moving nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is the maximum depth of the tree?&lt;/strong&gt; For depths of 10 levels or more, the second and third methods (mentioned earlier) result in a significant increase in database size due to the need to store all node connections along the path to the root.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What is more critical for the task: retrieval speed or data storage size?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ultimately, the choice is left to the database designer, and we hope this article serves as a helpful guide.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>computerscience</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Trees in SQL</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Wed, 25 Dec 2024 16:27:47 +0000</pubDate>
      <link>https://dev.to/andreik/trees-in-sql-4fp</link>
      <guid>https://dev.to/andreik/trees-in-sql-4fp</guid>
      <description>&lt;p&gt;Business applications often need to represent and store information such as a company's organizational hierarchy, a country's administrative divisions, an inventory structure, or simply a set of folders and files. Despite their apparent differences, these examples share a common feature— they are all trees. Not the green, wood-made kind that provide pleasant shade on a sunny day, but, mathematically speaking, connected acyclic graphs. In computer science, the links between nodes are described in terms of parent-child relationships. Each node in the tree, except the root, has exactly one parent and zero, one, or many children.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff4feagkie5p2x08kmdz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff4feagkie5p2x08kmdz6.png" alt="Tree example" width="176" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The figure above shows an example of a tree. Vertex A is called the root of the tree. Nodes without subtrees — C, D, F, G, and H — are called leaves. Edges of the tree show parent-child relationships between nodes. A rooted tree is k-ary if each node has no more than k children. A binary tree is a well-known particular case of a k-ary tree where k = 2.&lt;/p&gt;

&lt;p&gt;One of the common tasks performed on tree data is the retrieval of all child nodes at any nesting level, given a parent node. For example, calculating the total turnover of an account involves determining the turnovers of all its subaccounts, or compiling a list of goods belonging to a given goods group includes lists from all the nested subgroups.&lt;/p&gt;

&lt;p&gt;In imperative programming languages, we usually use a graph traversal algorithm to find all subnodes. Although most modern database servers support SQL queries with recursive common table expressions, using the recursive part inside complex queries on large amounts of data can significantly degrade performance.&lt;/p&gt;

&lt;p&gt;The reason is that SQL deals primarily with sets, not graphs, so one should implement dedicated data structures to represent nodes and edges as nested sets for efficient data selection.&lt;/p&gt;

&lt;p&gt;In this article, we will consider four ways of storing tree-like structures inside a relational database and evaluate them in terms of query performance and the space needed for storing the underlying data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link to the parent
&lt;/h2&gt;

&lt;p&gt;Perhaps the most intuitive and easiest way to represent tree-like data is to use the adjacency matrix in a relational table. Every record in the table corresponds to a node in the tree and holds the unique ID of the node (primary key) and the ID of its parent (for root nodes, the parent is null).&lt;/p&gt;

&lt;p&gt;Below is a DDL query that creates the &lt;code&gt;test1&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;test1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;test1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pay attention to the &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; clause of the &lt;code&gt;FOREIGN KEY&lt;/code&gt; constraint declared on the &lt;code&gt;PARENT&lt;/code&gt; field. It guarantees that the entire subtree will be deleted at once if the parent node is deleted. To protect the subtree from accidental deletion, the rule should be changed from &lt;code&gt;CASCADE&lt;/code&gt; to &lt;code&gt;RESTRICT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Corresponding data for the tree shown at the beginning of the article looks like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;parent&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;H&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Operations on tree data
&lt;/h3&gt;

&lt;p&gt;Addition of a new tree node is done by the &lt;code&gt;INSERT INTO&lt;/code&gt; operator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;test1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;unique&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To move the node from one parent to another, a value of the &lt;code&gt;PARENT&lt;/code&gt; field has to be changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;test1&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="n"&gt;being&lt;/span&gt; &lt;span class="n"&gt;moved&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Obviously, to delete the node, one should invoke &lt;code&gt;DELETE FROM&lt;/code&gt; operator providing ID of the node.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Selecting whole subtree of the given node
&lt;/h3&gt;

&lt;p&gt;With such an organization of the data, a simple &lt;code&gt;SELECT&lt;/code&gt; operator allows us to fetch only one level of nesting of node's descendants.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, what should we do if the task is to extract all subnodes, regardless of how deep they lie in the subtree?&lt;/p&gt;

&lt;p&gt;An unconventional solution would be to write a query for every possible level of nesting and then merge their outputs using the &lt;code&gt;UNION&lt;/code&gt; operator. Fortunately, we can use either recursive stored procedures or recursive common table expressions to assist us.&lt;/p&gt;

&lt;p&gt;The procedure below returns the IDs of all subnodes of a given node. The second parameter specifies whether the parent node will be included in the resulting set.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;RecTest1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AnID&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;AnID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;SUSPEND&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;

  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test1&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;AnID&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;
    &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;RecTest1&lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;
      &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="n"&gt;SUSPEND&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can use the procedure on its own or &lt;code&gt;JOIN&lt;/code&gt; its output with the tree data to select the required columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test1&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;RecTest1&lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a specific SQL server supports recursive common table expressions, we can write a query that selects all nested subnodes of the given node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt;
  &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test1&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;given&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test1&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
      &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="n"&gt;gt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Storing auxiliary links
&lt;/h2&gt;

&lt;p&gt;In the previous example, we placed an id-parent link in the table with tree data. Since we only know about the relationship between a given node and its children, we need recursive queries to scan the tree in depth, which is not efficient when working with large amounts of data.&lt;/p&gt;

&lt;p&gt;To overcome this limitation, an auxiliary table should be introduced. Such a table holds pairs of connected nodes along with the distance between them, where a distance of 1 indicates a parent-child relationship, and distances greater than 1 correspond to deeper levels of ancestry.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4hyjlc4r8nr75jmf1jcy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4hyjlc4r8nr75jmf1jcy.png" alt="Image description" width="236" height="236"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;test2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id_from&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;id_to&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_to&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;test2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id_to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;test2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is data for the tree:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id_from&lt;/th&gt;
&lt;th&gt;id_to&lt;/th&gt;
&lt;th&gt;distance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's get back to our business. Selection of the subtree now done much easier. No recursion required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;test2&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;distance&lt;/code&gt; field, we can arrange the resulting set by node's closeness to the given root node. &lt;/p&gt;

&lt;h3&gt;
  
  
  Manipulating Tree Data
&lt;/h3&gt;

&lt;p&gt;While we have managed to avoid recursion when retrieving data, this has significantly complicated the processes of inserting and modifying records.&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding a Node
&lt;/h4&gt;

&lt;p&gt;To add a node, you need to perform two operations: insert into the tree nodes table and the links table.&lt;/p&gt;

&lt;p&gt;For example, to add a child node with an identifier &lt;code&gt;2&lt;/code&gt; to a parent node with an identifier &lt;code&gt;1&lt;/code&gt;, execute the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;test2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Creating Triggers
&lt;/h4&gt;

&lt;p&gt;To automate the maintenance of tree relationships, it's advisable to create two triggers.&lt;/p&gt;

&lt;p&gt;The first trigger fires after inserting a record into the &lt;code&gt;test2&lt;/code&gt; table and adds a self-referential link with a distance of &lt;code&gt;0&lt;/code&gt; into &lt;code&gt;test2_link&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;test2_ai&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;test2&lt;/span&gt;
&lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
&lt;span class="k"&gt;POSITION&lt;/span&gt; &lt;span class="mi"&gt;32000&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second trigger fires after inserting a record into the &lt;code&gt;test2_link&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;Its purpose is to create links between the new node and its descendants (if adding a subtree) and all ancestors of the parent node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;test2_link_ai&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt;
&lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
&lt;span class="k"&gt;POSITION&lt;/span&gt; &lt;span class="mi"&gt;32000&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;AnID&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;ADistance&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_to&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Moving a Node
&lt;/h4&gt;

&lt;p&gt;To move a node (or subtree) from one parent to another, follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Remove the link to the current parent&lt;/strong&gt;, detaching the subtree:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;   &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt;
   &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id_from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;subtree&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;current&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add the subtree to the new parent&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;   &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;subtree&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="mi"&gt;1&lt;/span&gt;
   &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The previously created insert trigger will handle the new relationships automatically.&lt;/p&gt;

&lt;p&gt;Create a trigger that, upon deleting a link with a distance of &lt;code&gt;1&lt;/code&gt;, removes all links between the nodes of the subtree and the nodes along the path from the parent node to the tree root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;test2_link_bd&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt;
&lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;
&lt;span class="k"&gt;POSITION&lt;/span&gt; &lt;span class="mi"&gt;32000&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;AnIDFrom&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;AnIDTo&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;FOR&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_to&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;
      &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;
        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;
       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;
      &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;AnIDFrom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;AnIDTo&lt;/span&gt;
    &lt;span class="k"&gt;DO&lt;/span&gt;
      &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id_from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;AnIDFrom&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;AnIDTo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Deleting a Node
&lt;/h4&gt;

&lt;p&gt;Deleting a node doesn't require special handling, as foreign keys in the &lt;code&gt;test2_link&lt;/code&gt; table are configured for cascading deletions.&lt;/p&gt;

&lt;p&gt;When a node is deleted from the &lt;code&gt;test2&lt;/code&gt; table, all related records in &lt;code&gt;test2_link&lt;/code&gt; are automatically removed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test2&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Ensuring Data Integrity
&lt;/h4&gt;

&lt;p&gt;To maintain logical data integrity and adherence to our model, create two additional triggers.&lt;/p&gt;

&lt;p&gt;The first trigger prevents editing records in the &lt;code&gt;test2_link&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="n"&gt;tree_e_incorrect_operation&lt;/span&gt;
  &lt;span class="s1"&gt;'Incorrect operation'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;test2_link_au&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt;
&lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;POSITION&lt;/span&gt; &lt;span class="mi"&gt;32000&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="n"&gt;tree_e_incorrect_operation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second trigger deletes a node if a record with a distance of &lt;code&gt;0&lt;/code&gt; is removed from the links table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;test2_link_ad&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;test2_link&lt;/span&gt;
&lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;
&lt;span class="k"&gt;POSITION&lt;/span&gt; &lt;span class="mi"&gt;32000&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test2&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_from&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These measures will help maintain data integrity and ensure the correct tree structure within the database. &lt;/p&gt;

&lt;h2&gt;
  
  
  Composite Path
&lt;/h2&gt;

&lt;p&gt;It's not necessary to use a separate table to store the list of nodes leading to the root of a tree. Instead, you can concatenate the ordered list of node identifiers into a string, using a chosen delimiter, and store it directly within the node records table. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To optimize retrieval operations using the &lt;code&gt;STARTING WITH&lt;/code&gt; operator, it's essential to create the following index to enhance performance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;test3_x_path&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5bdrve5atqk6zsa7eff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5bdrve5atqk6zsa7eff.png" alt="Image description" width="240" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you choose a forward slash (&lt;code&gt;/&lt;/code&gt;) as the delimiter, the &lt;code&gt;test3&lt;/code&gt; table for the tree depicted in the figure above would be populated as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;parent&lt;/th&gt;
&lt;th&gt;path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;/A/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;/A/B/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;/A/B/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;/A/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;/A/E/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;/A/E/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;/A/&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Retrieving Subtree Nodes
&lt;/h3&gt;

&lt;p&gt;To retrieve all nodes in the subtree rooted at node &lt;code&gt;P&lt;/code&gt;, execute the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="n"&gt;STARTING&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you wish to exclude the root node itself from the results, modify the query accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="n"&gt;STARTING&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Manipulating Tree Data
&lt;/h3&gt;

&lt;p&gt;Similar to the simplest method of organizing hierarchical data discussed earlier, adding, moving, or deleting a node can be accomplished using standard SQL commands: &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, or &lt;code&gt;DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The automatic generation of the path string can be managed by implementing the following triggers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;test3_bi&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt;
  &lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
  &lt;span class="k"&gt;POSITION&lt;/span&gt; &lt;span class="mi"&gt;32000&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;WHILE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;
      &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;
    &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
  &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="n"&gt;tree_e_incorrect_operation2&lt;/span&gt;
  &lt;span class="s1"&gt;'Incorrect operation'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;test3_bu&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt;
  &lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
  &lt;span class="k"&gt;POSITION&lt;/span&gt; &lt;span class="mi"&gt;32000&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;NP&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="k"&gt;VARIABLE&lt;/span&gt; &lt;span class="n"&gt;OP&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;WHILE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt;
      &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;
        &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;
      &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;
    &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cm"&gt;/* Check for circular references */&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;POSITION&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="n"&gt;tree_e_incorrect_operation2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;NP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;OP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;test3&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NP&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;SUBSTRING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="n"&gt;STARTING&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;OP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Storing the composite path from the tree root to the current node imposes a limitation on the maximum tree depth, approximately equal to &lt;code&gt;L div (K + 1)&lt;/code&gt;, where &lt;code&gt;L&lt;/code&gt; is the length of the string field used to store the path, and &lt;code&gt;K&lt;/code&gt; is the average length of the string representation of the record key. &lt;/p&gt;

&lt;p&gt;Go to the &lt;a href="https://dev.to/andreik/trees-in-sql-part-2-15pa"&gt;part 2&lt;/a&gt;...&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>tutorial</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Using Node's built-in test runner with Turborepo</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Sun, 22 Dec 2024 11:00:15 +0000</pubDate>
      <link>https://dev.to/andreik/using-nodes-built-in-test-runner-with-turborepo-3509</link>
      <guid>https://dev.to/andreik/using-nodes-built-in-test-runner-with-turborepo-3509</guid>
      <description>&lt;p&gt;It takes only five easy steps to add unit tests to a Turborepo monorepo. No external testing frameworks, such as Jest or Mocha, are needed.&lt;/p&gt;

&lt;p&gt;1. Add &lt;code&gt;tsx&lt;/code&gt; at the monorepo’s root level to run TypeScript code without issues related to different module systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm add &lt;span class="nt"&gt;--dev&lt;/span&gt; tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2. Add a &lt;code&gt;test&lt;/code&gt; task to &lt;code&gt;turbo.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3. Add a &lt;code&gt;test&lt;/code&gt; script in every &lt;code&gt;package.json&lt;/code&gt; where necessary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsx --test"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4. Add a &lt;code&gt;test&lt;/code&gt; script into the monorepo's root &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; 
   &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"turbo run build &amp;amp;&amp;amp; turbo run test"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build step is optional, but it ensures the project compiles successfully before running tests. Alternatively, add a dependency on the &lt;code&gt;build&lt;/code&gt; step in &lt;code&gt;turbo.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;5. That is it. Now add your first test using the built-in Node.js test runner functionality:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;add.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;add.test.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:assert/strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;add&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./add.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test addition&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should add two positive numbers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should add two negative numbers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, give it a try by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>node</category>
      <category>turborepo</category>
      <category>testing</category>
      <category>monorepo</category>
    </item>
    <item>
      <title>What about starting a new platform?</title>
      <dc:creator>Andrej Kirejeŭ</dc:creator>
      <pubDate>Tue, 30 Jan 2024 20:58:45 +0000</pubDate>
      <link>https://dev.to/andreik/what-about-starting-a-new-platform-27mi</link>
      <guid>https://dev.to/andreik/what-about-starting-a-new-platform-27mi</guid>
      <description>&lt;p&gt;Twenty-five years ago, a significant leap in software development speed occurred when Borland Delphi entered the market with an entirely new approach: component-driven visual programming. Around that time, ideas for integrating relational databases into the object-oriented world evolved into robust ORM concepts. While numerous developers were stuck with either the laborious task of C++ development using the complicated MFC library or the clumsy and limited Visual Basic interpreter, we were eager to dive into this brave new world. With a small team of ten developers, we won a competition over more formidable and larger rivals, becoming one of the IT leaders in our country for the next two decades.&lt;/p&gt;

&lt;p&gt;Over the last two years, I have dedicated my time to studying new technologies and observing the trends in modern software development. Now, I have a strong sense of déjà vu. We are once again on the cusp of a new technological shift, and those who quickly board this accelerating train will reap the most significant benefits, vastly outperforming competitors who cling to obsolete platforms and tools.&lt;/p&gt;

&lt;p&gt;If I were to start a new platform right now, I would undoubtedly use the following strategies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Full-stack development using Node.js, Next.js, and React Native. This approach is a significant time and effort saver. In our recent projects, we often have developers single-handedly implementing a complete feature—including server-side code, web application, and mobile applications for both Android and iOS—in just one to two weeks. I know teams that adhere to the classic back-end/front-end separation and spend more time just discussing data exchange protocol details.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Decouple the ER model from the physical database. This flexibility allows easy switching and mixing of SQL/NoSQL databases. When we were laying the first modules of our Gedemin platform in the late '90s, the low-level integration with the database driver code allowed us to utilize every bit of memory (which was scarce at that time) and maximize CPU cycle efficiency. Times have changed, and computational power is much cheaper now; there's no need to be as frugal as we once were.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid manually drawing the UI. Well, JSX is great, but instead of mixing your code with HTML tags, build an engine that renders screens and forms based on ER entities and provided rules. This approach allows you to change the program's look and feel as easily and quickly as a snake sheds its skin during molting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Eliminate even the slightest compatibility concerns. Embrace every novel feature CSS/HTML/JS has to offer without fear. By the time your program hits the market, these technologies will be implemented in even the most basic devices. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Integrate AI services extensively. Functions like natural language queries, human-like summaries and reports, and interpretation of commands and instructions, once the stuff of science fiction books 15–20 years ago, are now just a matter of making calls to the OpenAI API.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>programming</category>
      <category>nextjs</category>
      <category>nosql</category>
      <category>openai</category>
    </item>
  </channel>
</rss>
