На днях я смотрел кино, где оператор использовал телеграф. Он знал наизусть азбуку Морзе и очень быстро нажимал свою единственную кнопку. Я задумался: с RxJS мы способны на большее! Давайте запилим телеграф, используя единственный fromEvent и массу интересных трюков. Потренируемся с Dependency Injection, директивами и операторами RxJS, чтобы собрать демо, которое выглядит круто и звучит аутентично. Телеграф работает с азбукой Морзе. Для нас будет несложно в ней разобраться, ведь это просто бинарная последовательность. Сигнал дискретный: он разбит на куски определенной длительности. Каждый кусок — либо писк, либо тишина. Можно использовать Boolean или просто 1 и 0 для краткости. Азбука следует правилам: есть два символа: точка и тире; тире — три единицы подряд; точка — одна единица; символы разбиты однократным нулем; буквы разбиты тремя нулями подряд; слова разбиты семью нулями подряд. Примем, что пробел — четыре нуля и три нуля на разделение букв. Каждая буква — последовательность из таблицы ниже. Мы видим шаг сетки сигнала. Черный квадрат равен единице, белый — нулю: Возьмем букву «H», это четыре точки. Точка — одна единица, и точки разбиты однократным нулем: 1010101. Фраза HELLO WORLD будет выглядеть так: 10101010001000101110101000101110101000111011101110000000 = HELLO[конец слова] 10111011100011101110111000101110100010111010100011101010000000 = WORLD[конец слова] Это стандартная таблица для США, но в мире есть и другие версии: в Angular это подсказывает нам, что тут стоит использовать InjectionToken. Благодаря второму параметру, в котором можно описать фабрику, мы добавим стандартную имплементацию. При необходимости проекты смогут ее подменить на свою таблицу: Мы не хотим заставлять людей нажимать одну кнопку — пусть используют обычную клавиатуру, а мы закодируем буквы самостоятельно. После того как сигнал пройдет по воображаемой телеграфной линии, мы его расшифруем и выведем на экран. Начнем с одного fromEvent(document, 'keydown') и разветвим этот Observable по полной! Каждую букву можно закодировать синхронно. Буквы моментально превратятся в массив нулей и единиц. Но поскольку мы имитируем аналоговое устройство, основанное на длительности сигнала, выделим под шаг определенное время. Пусть это тоже будет InjectionToken, чтобы его можно было менять. Это значит, что буквы превратятся в последовательности, имеющие продолжительность. Чтобы сохранить порядок и не терять значения, мы воспользуемся оператором concatMap. Он входит в семейство Higher Order Observable операторов, с его помощью мы превратим каждое значение в Observable. Когда поступает новое значение (пользователь нажал клавишу), concatMap дождется, пока прошлый Observable закомплитится. Последовательность полностью отыграет прежде, чем создать Observable новой последовательности. Так мы можем печатать очень быстро и не бояться, что какие-то буквы пропадут при передаче. Для демо давайте еще сделаем простенький сервис отправки этих последовательностей. Воспользуемся им для виртуальной экранной клавиатуры: Теперь можно сделать токен, превращающий буквы в азбуку Морзе. Для начала запросим таблицу и длительность шага сигнала. Затем преобразуем все keydown-события на document в последовательности из таблицы. Объединим их с сервисом из примера выше и с помощью concatMap переведем все в поток нулей и единиц. Каждую последовательность завершим тремя нулями: Обратите внимание, мы используем from, а не of. При работе с массивами from превращает их в последовательность эмитов, в то время как of сделает единственный эмит всего массива. В коде выше написан кастомный оператор delayEach. Он позволяет задержать каждый эмит, а не весь стрим: Теперь мы можем получить закодированный поток, взяв токен MORSE и подписавшись на него. Этот шаг интереснее. Нам нужен оператор для сравнения данных с последовательностью. Поток надо сбросить, если значение не равно соответствующему по индексу значению в последовательности. А если вся последовательность совпала и затем пролетело три нуля — надо испустить расшифрованную букву. Затем все это надо повторить. Мы хотим визуализировать все, так что давайте создадим специальный модуль для букв. Он будет состоять из компонента, директивы и сервиса. Зачем нам это? Для хорошего разделения логики. Нужно знать, с какой буквой мы работаем. Но это понадобится и в компоненте, и в сервисе. Чтобы избежать циклической зависимости, мы создадим директиву с единственной целью — знать букву, за которую все отвечают: В компоненте используем тот же селектор. Компонент будет передавать сигналы с помощью мыши и показывать прогресс расшифровки: Теперь нам нужно написать сам алгоритм расшифровки. Мы неслучайно указали у сервиса тип Observable<number | string>. Он будет репортить прогресс от 0 до 1 и букву, если расшифровка прошла успешно. Числовое значение позволит визуализировать работу в демо-приложении. Создание сервиса — самая сложная часть. Мне потребовалось немало времени, чтобы решить эту RxJS-головоломку. Труднее всего было обыграть пробел. Поначалу он неотличим от завершения буквы: и то и другое — это куча нулей подряд. Начнем с создания пары функций-помощников. Наш сервис наследуется от Observable. Внутри мы сделаем приватный стрим, обрабатывающий поток азбуки Морзе, созданный ранее: Нужно знать, с какой последовательностью мы работаем. Придется использовать геттер, а не read-only проперти, потому что на этапе создания инпуты директивы еще не обработаны: Если бы эта строка была статичной, можно было бы использовать @Attribute вместо @Input — и значение стало бы доступным в конструкторе. Но тогда не получится создавать директивы через *ngFor, динамически выставляя букву. Нам совсем не хочется пересобирать массив на каждый вызов геттера. Для этих целей воспользуемся декоратором @tuiPure из Taiga UI. Мы часто пользуемся им в своей библиотеке для реализации паттерна ленивых геттеров. Это техника мемоизации для отложенных вычислений. После того, как значение будет посчитано первый раз, декоратор подменит геттер на простое поле класса с результатом. Этот декоратор работает и с методами. Он будет проверять входные аргументы и возвращать прошлое посчитанное значение при полном их совпадении. Что-то вроде чистых пайпов в шаблонах. Дальше нужно проверять, что значение с определенным индексом равно соответствующему значению в последовательности: Посчитаем значение, которое сервис будет испускать. Напомню, это либо прогресс, либо полученная буква: Соберем внутренний поток с помощью всех этих методов: В потоке два оператора takeWhile. Один обрывает стрим, если значения не совпали. Второй обрывается, если пролетела буква, внутри isNumber простая проверка типа. Второй аргумент takeWhile позволяет пропустить значение, нарушившее условие. С помощью завершения и перезапуска можно легко проверять значения на совпадение с последовательностью, потому что индексы остаются синхронизированными. Обрыв по второму takeWhile (буква полностью расшифрована) можно просто перезапустить. А вот при несовпадении значений перед перезапуском нужно дождаться завершения буквы (три последовательных нуля). Первой мыслью было использовать repeatWhen. Но проблема в том, что функция-фабрика будет вызвана только один раз после первого завершения. Дальше поток продолжит слушать возвращенный Observable: Это значит, что repeatWhen будет правильно работать только во втором и последующих перезапусках. При первом завершении три нуля, означающие конец буквы, могут уже пройти к моменту, когда мы начинаем слушать threeZeroes$. Так что для начала давайте просто слушать три последовательных нуля. Это отличный кейс для оператора scan:  Начнем с трех нулей, чтобы запустить стрим. Затем будем считать последовательные нули и пропускать три и более. Мы не можем ориентироваться на три нуля ровно, потому что тогда пробел все сломает. И нужно разделить два takeWhile-оператора. Мы хотим, чтобы первый обрывал внутренний стрим. Это значит, нам снова понадобится concatMap: Теперь, когда внутренний стрим оборвется, три последовательных нуля его перезапустят. Символ пробела прострелит несколько раз, но concatMap все равно будет дожидаться завершения внутреннего стрима, и второй takeWhile сбросит этот процесс. Все готово, и пришло время собрать приложение. Будем слушать нажатия настоящей и виртуальной клавиатуры и выводить полученное сообщение. Также каждая кнопка будет показывать прогресс расшифровки своей буквы. Телеграф и азбука Морзе будут неполноценными без звука. Простую пищалку очень легко сделать с помощью Web Audio API и OscillatorNode. Воспользуемся моей библиотекой @ng-web-apis/audio. Она является частью open-source-инициативы Web APIs for Angular. Наша цель — создание качественных легких оберток для нативных Web API, чтобы их было комфортно использовать в Angular. Посмотрите, что у нас уже есть. С ее помощью мы можем превратить декларативные директивы в граф аудионодов: Этот код будет проигрывать писк для единиц и замолкать на нулях благодаря регулированию громкости. Теперь давайте выведем клавиатуру и расшифрованный результат. Кнопка очистки тоже не повредит: Но где же взять output$? Мы можем собрать сервисы прямо из шаблона. Вы знали, что запросы Angular позволяют получать сервисы и другие DI сущности из инжекторов? Главное — помнить, что они недоступны до ngAfterViewInit-хука. Соберем output$: Чтобы показывать прогресс расшифровки, добавим простой span в CharComponent и будем управлять его высотой и цветом с помощью двух стримов: И последний трюк, который я хотел показать, — управление длительностью CSS-переходов с помощью токена на длительность шага сигнала. После перехода на Ivy в Angular можно использовать байндинг для задания значений CSS-переменных. Давайте заведем --duration и зададим ему значение в нашем основном компоненте: Все готово, и я приглашаю вас попробовать итоговый продукт! Google Developer Expert | Angular