solovyov.net

Clojure Cup 2014

· · life, programming, clojure

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

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

Before

Идея того, что писать, в голове оформилась довольно давно. Хотя она придумалась и не специально для соревнования, а в общем “неплохо бы написать”, но так как рабочего кода еще не было, мы решили еë использовать.

Идея родилась из того, что мне очень понравилось и оформление, и редактор, и общий минимализм при достаточной функциональности в medium.com, но очень напрягли две вещи: то, что я всë написанное опять отдаю в руки непонятно кому и зачем, и как его оттуда доставать в случае их продажи неудачной (для меня), и то, что мой блог как бы не мой, а медиума, и вообще везде вокруг медиум медиум медиум. В самом деле, это же просто хостинг для блога удобный (по крайней мере, так его вижу я). Я не против где-нибудь в футере ссылочку иметь, но домен я хочу видеть свой.

Вот так и родилась идея сделать блог-платформу а-ля медиум, но только чтоб данные она хранила в дропбоксе (и аналогичных ему сервисах), и генерировала статические сайты - всë равно от блога динамики не требуется. Заодно решили это делать всë без сервера - браузеры уже позволяют обойтись без этого, апи дропбокса тоже, так почему бы и не перенести всë на компьютер пользователя? Полный контроль над своими данными и всë такое. :)

Тем более результат в теории можно запаковать в node-webkit, получить полноценное приложение и редактировать блог прямо на локальном диске. Короче, пройти полный круг и получить в результате то, что было в конце 90-х. :)

Из эдак скажем 30 альтернатив было выбрано имя: Movyv. Українською: “мовив”, тобто “сказав”, “have said”. Оно одновременно символизирует суть сервиса, плюс является свободным пятибуквенным .com-ом. :) Интересно, что от англоязычных знакомых я получил пока два комментария: “looks fun” и “I like it”. Зато от друзей, для которых английский не первый язык, я получил кучу комментариев про то, что ни один американец никогда не сможет его произнести. :)

Конечно же, мы решили подготовиться куда основательнее, чем в прошлом году, и таки сделали это. :) У нас был приличный список на workflowy - какие библиотеки будем использовать для чего, более-менее обсудили архитектуру и то, как хранить данные (и, конечно, в четверг-пятницу перед соревнованием мне внезапно пришла в голову идея, что json’a в дропбоксе быть не должно и всë должно быть в html), всякие идеи, что можно сделать дополнительно.

Вдобавок в четверг мы посидели с Витей, а в пятницу с Валерой, чтоб левел кложуры в крови повысить. :)

Короче, подготовились как могли. :) Когда я смотрел на фотки подготовки других команд в твиттере - мокапы интерфейса, потоки данных, и так далее, я чуточку переживал, но не сильно - мне легче решать всë на ходу, чем строить какой-то план. Всë равно что-то забудется, и вся архитектура пойдëт к чëрту. :)

Go!

Итак, суббота, 9 утра, мы собрались перед офисом Global Logic (спасибо компании и Вите, и кто там еще за это отвечает за предоставленное помещение, оно было удобное и клëвое), и бегом побежали писать код. С бешеной скоростью, конечно - базовая архитектура приложения с двумя вьюхами и роутером на cond‘e у нас появился аж к 11 утра. Не знаю, почему так долго, но что-то мы точно делали! :) Джойнились во флоудок, ха-ха.

Но зато к половине первого у нас уже был коннект к дропбоксу, прикрученный редактор (который умел только сериализовать свои данные и потом лежал без движения заметно больше суток :), базовые стили и зачатки сервера.

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

К концу дня у нас было:

  • 45 коммитов
  • сервер
    • который умел принимать токен от клиента
    • и проксировать запросы на дропбокс - чтоб сервить сгенерированные статические файлы
  • клиент
    • умел находить существующие сайты в дропбоксе
    • создавать новые
    • имел симпатичную страницу для незалогиненных пользователей
    • а также поддавался advanced режиму минификации

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

(defn create-site [site-name author-name site-title]
  (go
   (let [[error stat] (<! (dropbox/make-dir site-name))]
     (if error
       [error nil]
       (let [path (str site-name "/src.html")
             data (create-index-src site-name author-name site-title)
             [error stat] (<! (dropbox/write-file path data))]
         [error stat])))))

Мы попереживали, поискали решений в интернетах, но решили, что всë равно уже половина одиннадцатого, так что надо ехать домой. Я решил быстренько задеплоить и обнаружил нечто, о чëм никто не подумал заранее - Dropbox позволяет иметь редиректить пользователя обратно только на https, а http работает только для локалхоста.

В состоянии лëгкой паники я написал письмо с призывом о помощи организатору - Теро, и мы разъехались по домам. К счастью, выход из положения нашëлся - у него был wildcard-сертификат на *.clojurecup.com, и он насетапил нам прокси. Так что логин в дропбокс заработал. :)

Don’t stop

С утра в воскресенье мне в голову пришло, что то, что мы видели в интернете, вполне ок, и я быстренько написал решение проблемы с хендлингом ошибок:

(defn throw-err [rv]
  (if (instance? rv js/Error)
    (throw rv)
    rv))

(defmacro <? [ch]
  `(movyv.utils/throw-err (cljs.core.async/<! ~ch)))

Которое Сева доработал еще одним макросом:

(defmacro go-rethrow [body]
  `(go (try
         ~body
         (catch :default e
           (js/console.log e)
           e))))

И в результате выходило:

(defn create-site [site-name author-name site-title]
  (go-rethrow
   (let [_    (<? (dropbox/make-dir site-name))
         path (str site-name "/src.html")
         data (create-index-src site-name author-name site-title)
         stat (<? (dropbox/write-file path data))]
     stat)))

В принципе вышло терпимо, если нужно обработать ошибку - try/catch, если нужно отдать это коду выше по стеку - go-rethrow. Не мечта, но по крайней мере такой код можно читать.

Добрался до GL, и всë продолжалось в довольно спокойном темпе - сменили везде обработку ошибок на новый апи, выдумали интерфейс списка из 3 колонок (список сайтов, список постов, превью поста), и начали допиливать.

Фиксили понемногу баги, подпиливали интерфейс, немного рефакторинга, я отвлëкся на написание текстов (reasons to use на индексе, плюс описания для clojurecup’a). Ничего особенно глобального не происходило первую половину дня, и мы пошли пообедаать - с 3 до 4 было сделано всего 2 коммита.

А вот потом всë как-то ускорилось. Я сделал темплейты для рендеринга собственно уже результирующих сайтов, интерфейс трëхколоночный доделался, появилась возможность открыть редактор, стили для него, опять же, генератор слагов, парсинг постов из сгенерированного сайта, сохранение постов, и так далее.

Но где-то к 6-7 вечера нам пришло в голову аж два озарения сразу. Первое - что наш трëхколоночный интерфейс чрезвычайно страшен и не сильно удобен, и срочно надо его уничтожить и сделать всë значительно проще. Второе - что наш сервер, который проксирует запросы к дропбоксу, никуда не годится. Причины озарением не были, конечно - он тормозил (в самых лучших условиях максимум 120 запросов в секунду и любой ответ не быстрее полусекунды), и ускорить его просто так никак не удавалось.

Ну как обычно. “Время на исходе, давайте всë переделаем”. В результате мы выбросили 3 колонки, и заодно роутинг по урлу - у нас с самого начала роутинг был частично по урлу, частично по состоянию, и так как в урл все состояния вносить не удавалось, мы решили просто плюнуть и переехать просто на состояние. Не так красиво, зато куда проще.

Ну и заодно переписали сервер, который теперь реагирует на POST-запрос от дропбокса (который тот делает по изменению данных) и скачивает весь сайт на диск локально, чтоб потом его отдавать с помощью nginx’a.

После 9 вечера наши 5-8 коммитов в час превратились в 12-16, частично из-за уменьшения размера задач, частично из-за горячки. К этому времени у нас уже работало сохранение, и вообще смотрю на лог сейчас - один за другим фиксы каких-то проблем, а не фичи.

Полдесятого ночи наконец появились спиннеры! :) С ними всë, по-моему, немного оживилось и стало веселее. :)

К 12 ночи начали наконец рендериться сайты, посты стало можно переименовывать, валидация формы создания сайта, куча фиксов стилей, скачивание сайтов переехало в ThreadExecutor, и я начал деплоить, пока чуваки делали последние (ха-ха) правки.

Я очень, очень рад, что занялся этим за 3 часа до конца соревнования, а не за полчаса, как в прошлый раз. Количество траблов при деплое, как всегда, жжот. Я уже научился даже немного читать код после его минификации в адвансед режиме. :) Непонятно почему, но иногда еще у Closure слетает крыша и если не очистить кеш перед минификацией, оно выдаëт нереальный хлам. По типу того, что в таком коде case вылетает с ошибкой, что условия :auth не существует! Удаление кеша и рекомпиляция помогает.

    (case (:auth data)
      (:not :interactive) (Index data)
      :authed             (Auth data)
      :loading            (html [:div.loader (ui/icon "spinner spin 5x")]))

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

Энивей, к 2 часам ночи всë наконец поехало, я в последний момент добавил Google Analytics, и мы разъехались спать. Фух.

Ощущения

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

ClojureScript хорош. Вот просто хорош. Писать можно довольно быстро, преобразовывать данные - удобнее, чем в любом другом окружении, что я пробовал, paredit делает передвижение кода простым, а возможность улучшить синтаксис просто на ходу - без комментариев.

core.async козëл. Зажëвывает ошибки и репортит хлам. Его дебагать - это прямо отдельное умение. С другой стороны, мне кажется, что без него код был бы заметно стремнее - честных доказательств нет, но сейчас код довольно читаемый, по-моему. Хотя там есть даже объединение одновременных запросов, всë такое.

Еще мы замутили кое-какой недо-Flux, и я вот совершенно не уверен, какой с него прок. Ту куцую информацию о типах (имя функции и количество еë аргументов), которая у нас есть, мы теряем, если начинаем пользоваться :keyword‘ами, а наличия серьëзного плюса у общей шины я что-то не осознал. Может я что-то не заметил? Но пока что у меня план переделать все наши хендлеры сообщений из канала на функции, которые просто сами запускают go-блоки.

Кода в этот раз вышло в полтора раза меньше - 150 строк на сервере и 850 на клиенте, но и объëм задачи был поменьше. Много времени ушло скорее на вылизывание, чем на дописывание фич.

Что в результате

Вышел совсем маленький сервер, у которого есть всего три урла:

(defroutes routes
  (GET "/webhook" [challenge] challenge)
  (POST "/webhook" {:keys [body]} (process-webhook body))
  (POST "/save-token/" [site-name token] (save-token site-name token)))

POST /save-token/ - сохраняет авторизационный токен на сервере, для того, чтоб сервер мог доступиться к пользовательскому дропбоксу. GET /webhook/ - дропбокс проверяет, что мы умеет его вебхук, и POST /webhook/ - дропбокс нотифицирует, что что-то изменилось, и мы скачивает сайт пользователя, чтоб его мог отдавать nginx. Никаких преобразований сервер не делает.

И клиент, который работает с дропбоксом - создаëт там директории с сайтом и постами, сохраняет исходники (смотрите на src.html любой) и рендерит готовые страницы (index.html). Ему сервер не нужен, и он к нему обращается только в один момент - когда создаëт новый сайт, чтоб дать серверу токен для доступа к этому сайту (чтоб он его мог скачать, когда дропбокс скажет).

Brave new app

По-моему, у нас получилось сделать не только рабочее, а еще и полезное приложение! Или наоборот, не только полезное, но еще и рабочее. :) Оно не везде симпатичное, и умеет куда меньше, чем я хотел бы, и деплоить фиксы всю эту неделю, пока идëт голосование и судьи определяют победителя, нельзя, но в целом вроде бы всë ок!

Проснулся в понедельник и сразу пошëл смотреть, нет ли там чего, и обнаружил прекрасный пост. Очень приятно прочитать было. :) И, похоже, в этом году мы сделали больше всех коммитов - 185 (на 20 меньше, чем в прошлом).

А еще пару часов назад зарегистрировал movyv.com и получил на него бесплатный сертификат на StartSSL - это всë вместе с сетапом у меня заняло с полчаса и я вполне мог бы это сделать еще в субботу, что спасло бы пару нервных клеток.

Энивей, всë это было серьëзно круто. Всем рекомендую как-нибудь попробовать такое, эмоциональный подъëм в конце воскресенья у меня был безумный, как, по-моему, и у всех остальных. :)

И я буду очень-очень благодарен, если вы за нас проголосуете! К сожалению, в этом году голосование только с помощью Твиттера. И спасибо, что дочитали до конца. :)