May 21st, 2014

усы2

DataScript и сервер

В понедельник я выступил на Hangout с рассказом про DataScript, и одним из частых вопросов остается вопрос про ленивую подгрузку данных. Я написал в Readme, что у Датаскрипта нет и не планируется никакой истории про синхронизацию с сервером, ленивость, localStorage, и так далее. Причина — потому что я не знаю как удовлетворить всех, поэтому вынес это за рамки. Тем не менее, пусть не внутри Датаскрипта, а снаружи, но вопрос остается.

У меня сейчас такое понимание вариантов использования. Данные, которыми оперирует web-приложение, могут быть большими или маленькими, частными (personal for user) или публичными, требующими real-time sync (server push) или нет.

Первый случай, когда данных мало, мы загружаем их при загрузке страницы одним вызовом API «дай мне всё про эту страницу». Это такие применения как github issues, доска trello. Это то, про что я говорил на hangout, логика работы интерфейса и view model уходят из server-side api cовсем и сосредоточены теперь в js. Это очень простой случай и от DataScript тут требуется только разве что batch import быстрый (думаю, сделаю).

Кстати, тут можно поиграться, если данных средне, скажем. У проекта, например, 10000 issues и они занимают, сериализованные, 5 мегабайт, скажем. Можно грузить их кусками по 128, запихивать в DataScript по мере загрузки, а интерфейс никак не тюнить, просто дать ему перерендериться на каждый такой «запих». Получится, что ты быстро открываешь пустую страницу и у тебя на глазах в ней появляется все больше и больше issues, счетчик total issues крутится, paginator толстеет, и так далее. Наглядно и красиво, короче, и позволит скрыть некоторую тормознутость подсасывания. И писать ничего специально не надо, потому что, как мы знаем, в React рендер это f(Store)→Dom, чистая функция, мы просто инкрементально увеличиваем Store.

Опционально, можно сделать server sync через transaction reports формат. Тогда, скажем, такая вещь как многопользовательская игра будет тривиальной — каждый участник загружает себе начальное состояние, шлет на сервер свои транзакции и получает все чужие. В итоге все видят одно и то же. Прелесть в том, что это можно сделать переиспользуемым компонентом, не зная ничего о специфике игры. Это может и не игра быть, а софт для совместного редактирования чего-то, Trello скажем. Важно, чтобы клиент имел открытый двухсторонний канал с сервером (веб-сокет), а на сервере стоял fan-out по всем подключенным сейчас клиентам.

Второй случай это когда данных много, но они публичные, т.е. одни и те же для всех. Андрей Рублев как раз про это спрашивал, можно ли поиметь доступ ко всей 2Gis базе организаций запросом из браузера. Таких приложений, с одной стороны, можно мало насчитать, но можно.

С другой стороны, конечно, «а что считать данными». Потому что можно было бы взять IMDb и положить его целиком в датомик. Датомик сегменты закачать на s3. Далее DataScript получает с сервера (или с того же s3) только URL на root index node и браузером уже ходит в s3 за сегментами.

Получается в точности модель Datomic, только peer in-browser, а transactor это server-side api, которое отдает current index root, novelty и пушит real-time изменения (если такая степень динамизма нужна, для IMDb не нужна, скажем — каждая страница может работать на слепке БД в момент её, страницы, загрузки).

Это всё очень интересно, это будет ощущаться как «вся база у меня в руках», а сегменты будут прозрачно докачиваться и кешироваться где-то за занавеской. Но это вот уже нереальное количество работы, так что я пока не буду подписываться даже на то, что это когда-нибудь возможно будет. Это реально сделать весь Датомик, только пир в джаваскрипте должен работать. На такое даже Рич Хики пока не готов.

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

Это можно свести к первому случаю (оперируем письмами только за последние 3 дня, например, iOS Mail так делает).

Можно свести к случаю второму — для каждого юзера иметь свою Datomic DB, корневым индексом и сегментами которой оперирует in-browser peer. В принципе вариант работает, если данные действительно персональные и их никто больше не видит (письма — да, форум уже нет, любой collaboration софт тоже нет, так что скорее почти всегда нет).

Тут наверное остается только решение о ленивой подгрузке. Из-за особенностей модели данных, у Датаскрипта нету никакого куска, который бы мог быть обозначен «ленивым» и подгрузиться по необходимости с сервера. Датомы слишком маленькие — проще загрузить датом, чем делать его ленивым. Entity не очень удобны — ленивый entity можно сделать, но он тоже слишком маленький (конечное число атрибутов), и интереснее скорее ленивые коллекции entity, чем один конкретный. Плюс, ленивые entity это сразу проблема N+1 select, от которой пытались уйти. Короче, на уровне Датаскрипта сделать ленивость правильно и универсально — это делать через сегменты, а это непросто.

Остается ручная ленивость. Запоминать, что загрузили, а что еще нет, запоминать докуда догрузили. Эту информацию можно прям атрибутами в БД хранить. Концептуально не очень правильно (есть данные или нет это не особенность БД, это особенность способа к ней доступиться; откат по истории будет абсурдно удалять подгруженные позже данные; возможны дыры в консистентости), но в приницпе терпимо. Не удобно, да, но это всегда было неудобно. Датаскрипт в этом случае не помогает, но и не мешает вроде.

Более того, хоть я и привык так делать, смирился, мне кажется что все равно с Датаскриптом получается лучше среднего. Если мы делаем статус ленивости атрибутом entity, скажем, есть у нас [188 :label/title "label:social"] и [188 :loaded :no], потом понимаем что нам этот label нужен (пользователь щелкнул), меняем в БД статус на [:db/add 188 :loaded :loading] и пуляем AJAX-запрос. Первый важный момент, это что именно этот атрибут статуса подхватится нашим рендером и он нарисует прогрессбар. Второй, что когда придет AJAX-запрос, нам нужно сделать ровно одну, простейшую, вещь: запихнуть пришедшие данные в БД и поменять статус на [:db/add 188 :loaded :yes]. Рендрер, во-первых, грохнет прогрессбар, а во-вторых нарисует то что пришло, потому что он на ту же базу подписан, блин. Если пользователь за это время что-то нащелкал там, перешел в другой таб, ничего страшного, ничего не отрисуется, зато потом, когда он вернется в наш label, он уже подгруженные данные увидит. Если он вышел, потом вернулся, а запрос все еще грузится, он опять прогресс бар увидит, потому что статус всё еще :loading. Космос же?

Короче, естественным образом, DataScript ориентирован на первый случай: данных мало/умеренно, они помещаются в браузер. Я надеюсь (программа максимум), что даже это уже изменит привычки веб-разработчиков. Согласитесь, большинству приложений не нужно иметь дело со стотысячными базами в браузере. Обычно то, на что смотрит пользователь, умеренных объемов, и это именно тот случай, developer experience которого можно сильно улучшить. Быстрая загрузка всей базы на старте + real-time updates через двухсторонний web socket и получится, что real-time веб-интерфейсы, collaboration tools, многопользовательские игры тривиально писать. Моделируешь схему данных, пишешь рендер и логику, остальное покрыто generic компонентами, сервер-сайд тупой.

Живой пример: загрузить 100 issues через API github занимает 50 kB (gzipped) и 600–800 мс. Загрузить 10 issues, как они делают это сейчас, в text/html, занимает 6.3 KB и 300 мс (упирается, естественно, в latency). Естественно, первый response гораздо лучше кешируертся, потому что у него нету всей этой вариабельности в URL а-ля direction=desc&page=1&sort=created&state=open. Плюс user experience лучше — мы не шлем server-side запрос на каждый клик в интерфейсе. В проекте уровня Om issues всего ~200 за всё время существования. В Trello-доске вряд ли будет больше 100 карточек. Это все реальные приложения и прекрасные use cases для DataScript, bigdata в браузер пока не пришла, а вот interaction очень сложный и есть что улучшать.