Open nin-jin opened 1 year ago
Вот вы тут думаете о протоколе будущего, а обычные пацаны берут json, кодируют в base64 что б хоть как-то ужать, и делают GET запрос, вот где "удобство" дебажить и что происходит одно удовольствие :)
Если говорить о HARP, плоское преставление тяжело анализировать
[person[name;name;article[title;author[name;_len[follower[vip=true]]]]];me]
, я мысленно пытаюсь его конвертировать его в json.
Видна слишком большая нацеленность на комфортное преставление для GET запросов. Хотя по сути проблем с ним нету в дев тулз в хроме, он отлично парситься и показываеться список декодированых get запаметров. Логи на стороне бекенда, да, надо думать как декодировать что б понять что там кто запросил. Для POST запросов уже можно слать что угодно, json, или tree в payload.
обычные пацаны берут json, кодируют в base64 что б хоть как-то ужать
Но ведь base64 наоборот увеличивает размер на 25%..
проблем с ним нету в дев тулз в хроме
Не Хромом единым..
он отлично парситься и показываеться список декодированых get запаметров.
Оно не поддерживает иерархию.
https://page.hyoo.ru/#!=uhgyst_zfa8t3
Здравствуйте, меня зовут Дмитрий Карловский и я.. как скульптор, отрезаю всё лишнее, чтобы оставить лишь самую мякотку, которая в наиболее лаконичной и практичной форме решает широкий круг задач. Вот лишь несколько спроектированных мною вещей:
На этот же раз мы спроектируем удобный клиент-серверный API, призванный убрать кровавую пелену с глаз фронтендеров и стальные мозоли с пальцев бэкендеров..
Application Programming Interface
Архитектурно можно выделить три основных подхода: RPC, REST и протоколы синхронизации. Разберём их подробнее..
Remote Procedure Call
Тут мы сначала выбираем какую процедуру вызвать. Потом передаём на сервер её имя и аргументы. Сервер её выполняет и возвращает результат.
Известные примеры RPC протоколов:
Общей особенностью таких протоколов является огромное число процедур с уникальными сигнатурами, для каждой из которых нужна документация и поддержка в коде (пример). Как следствие, подобные протоколы весьма сложны в поддержке, а использование сопряжено с постоянным штудированием документации.
Другой особенностью является невозможность кеширования запросов на прокси, так как они не имеют информации о том, что и как можно кешировать.
REpresentational State Transfer
Тут идея в том, чтобы выделить объекты с которыми можно взаимодействовать (ресурсы), стандартизировать их идентификаторы (URI) и процедуры для взаимодействия с ними (методы). Число типов ресурсов получается таким образом в несколько раз меньше, чем число процедур в случае RPC, что существенно проще в использовании человеком.
Известные примеры REST протоколов:
Так как число REST методов весьма ограничено и каждый имеет чёткую семантику, то становится довольно легко писать приложения, умеющие работать с любыми (даже заранее не известными) ресурсами (пример), понимающими как их читать, обновлять, кешировать и тд.
Synchronization protocols
Тут на уровне протокола вообще нет методов, а узлы сети просто обмениваются дельтами внесённых локально изменений. Эти виды протоколов характерны для децентрализованных систем, поддерживающих работу в оффлайне. Известные представители данного типа протоколов.. мне не известны. Но сейчас я разрабатываю один из таких, который вскоре перевернёт весь мир. Но пока что мы остановимся на чём-то более традиционном - REST..
Architecture
Прежде чем браться за детали протокола, давайте определимся с архитектурными ограничениями, на которые мы будем ориентироваться..
Pseudo-Static
Важно отметить, что REST - это совсем не про URI похожие на пути к файлам вида:
Эти две ссылки фактически ссылаются на один и тот же ресурс. То есть первая же проблема с ними - выбор каноничной ссылки из множества вариантов.
Другая проблема заключается в том, что нам необходимо знать информацию о всём пути, которой у нас может и не быть. Например, есть у нас идентификатор сообщения, но мы не можем получить его лайки так как не знаем идентификаторов чата и организации.
Следующая проблема - раздутие размера из-за избыточной информации, что приводит либо к переносу на несколько строк в случайных местах, либо вообще к обрезанию.
Наконец, при переносе чата, например, в другую организацию, ссылки на все сообщения вдруг поменяются, а пришедший по старым ссылкам пользователь зачастую увидит издевательски красивую страницу 404. Если разработчик, конечно, не запарился серьёзно над редиректами.
Резюмируя: URI должен содержать лишь минимально необходимую информацию для идентификации самого ресурса, но не его положение в той или иной иерархии.
Create Read Update Delete
Не менее важно отметить, что REST не только и не столько про CRUD, не смотря на то, что CRUD хорошо выражается через основные HTTP методы:
Create
-POST
Read
-GET
Update
-PUT
/PATCH
Delete
-DELETE
У CRUD тем не менее есть ряд существенных недостатков..
Создание ресурса не является идемпотентным. Если наш запрос на вызов такси потерялся на пол пути, то попытка его повторить может привести к приезду сразу нескольких такси. Кроме того, до создания ресурса у него нет идентификатора, который необходим для адекватной работы UI, что вынуждает клиента вставлять костыли с присвоением временных идентификаторов, и последующей заменой их на постоянные после создания ресурса. Ввиду всего этого предпочтительнее формировать глобально уникальный идентификатор ещё на клиентской стороне, а на сервере создавать ресурс на лету, когда клиент пришлёт свои обновления для ещё не созданного ресурса.
Удаление ресурса нарушает ссылочную целостность. И если в рамках нашей системы мы можем обновить или удалить все ссылки, то внешние системы так и продолжат ссылаться в никуда. Поэтому предпочтительнее ресурсы не удалять полностью, а лишь помечать скрытыми.
Таким образом для нашего протокола хватит лишь двух HTTP-методов:
GET
для чтения.PATCH
для обновления.Важно отметить, что ресурс может быть довольно большим, поэтому важны механизмы как частичного чтения (fetch plan), так и частичного обновления (
PATCH
, но неPUT
).Real Time
Подход HTTP с запросом/ответом плохо подходит для современных приложений, которым нужно в реальном времени реагировать на изменения, не заваливая сервер запросами вида "а не изменилось ли что?". Для таких приложений необходимо поднимать двустороннее WebSocket соединение. А чтобы не повторять одну и ту же логику дважды, HTTP запросы можно делать через него:
В дополнение к стандартным
GET
иPATCH
, при соединении по WebSocket стоит поддержать ещё пару методов:WATCH
- это то же самое, что иGET
, но дополнительно подписывает клиента на обновления ресурса.FORGET
- просто отписывает от обновлений.Keys
При выборе способа идентификации сущности можно выделить два основных подхода:
Ключевое свойство тут - неизменность. Идентификатор не должен меняться со временем, иначе его сложно назвать идентификатором. Поэтому нам подойдёт любой неизменный ключ. И зачастую натурального неизменного ключа попросту не удаётся придумать. Поэтому как правило это должен быть именно суррогатный.
Model
Как правило, прикладная область представляет из себя граф, где узлами выступают несколько десятков типов сущностей, а рёбрами - несколько сотен типов отношений между ними.
Часть этих сущностей и отношений представлена в базе данных в явном виде, что характерно для рёбер в графовых СУБД. Часть - в неявном, что характерно для отношений в реляционных СУБД. А часть может быть виртуальными, вычисляемыми на лету.
Кстати, на тему графовых СУБД у меня есть пара интересных статей:
Хорошей практикой является абстрагирование API от деталей хранения и внутреннего представления данных. Это позволяет менять внутренности без изменения внешнего контракта, и не усложнять его низкоуровневыми деталями.
Итак, опишем наиболее простую, но гибкую модель:
Entity
- документ, хранящий различные данные.Type
- тип сущности, который определяет какие у неё есть свойства и какого они типа.ID
- суррогатный идентификатор сущности, уникальный в рамках её типа.URI
- уникальный идентификатор сущности в рамках всего API, представляющий из себя ссылку относительно базового URI API.Fetch Plan
Часто при реализации REST API ресурс возвращается целиком. Хороший анти пример - поиск через GitHub API, выдача которого запросто может весить полтора мегабайта вместо требуемых для отображения меню пары килобайт. Это типичная проблема, называемая overfetch.
Если в некоторых ответах ресурс будет возвращаться в сокращённом виде, то в ряде случаев это приведёт к необходимости дозапрашивать полное представление ресурса ради одного недостающего поля. В примере с GitHub поиском данные пользователя выдаются в сокращённом виде. А это значит, что если нам нужно рядом с пользователем показывать ещё и список организаций, в которых он состоит, то нам придётся сделать ещё N запросов за всеми данными пользователя. Это не менее типичная проблема, называемая underfetch.
Так же тут можно заметить, что если один и тот же пользователь встречается в нескольких местах, то одни и те же его данные дублируются многократно. В моей практике был курьёзный случай с менеджером задач, где каждая задача находилась в нескольких папках, те в нескольких других, и так далее до корня. И когда приложение при старте запрашивало дерево папок, то вместо десятка килобайт данных, оно получало ответ в десятки мегабайт. А это мало того, что нагружало сервер и сеть, так ещё и Internet Explorer просто падал при попытке распарсить столь большой JSON.
Последняя проблема является следствием денормализации данных, не являющихся по своей сути ориентированным деревом. Особую пикантность этой ситуации придавало то, что сервер получал данные из базы в нормальной форме, но для выдачи клиенту производил денормализацию. А клиент, получая данные в денормализованной форме, делал обратную нормализацию, чтобы избавиться от дубликатов.
Отчасти поэтому GitHub со временем перешёл на более модный GraphGL, который решает первые две проблемы, но не последнюю. Мы же решим их все. А значит нам нужно следующее:
Query
Итак, ближе к делу, пришла пора разработать язык запросов ко графу в рамках REST архитектуры..
Applications
Прежде всего надо определиться где и как будут использоваться запросы:
Special Symbols
Так как запрос может содержать пользовательские данные, то в них придётся экранировать все спецсимволы. В формате URL уже есть ряд стандартных спец символов, поддерживаемых любыми инструментами:
Они экранируются при использовании
encodURIComponent
, но не при использованииencodeURI
, то есть с этими символами мы точно не получим неожиданного экранирования. Однако, с некоторыми из них всё же есть проблемы:: / ?
- не допустимы в именах файлов.:
- не допустим в начале пути URL./
- ряд инструментов показывает лишь последний сегмент пути после него, что порой не информативно.? #
- ряд инструментов экранирует множественные вхождения этих символов в URL.#
- всё, что после этого символа, браузер на сервер не передаёт.&
- требует неуклюжего экранирования в XML:&
.Таким образом, ряд допустимых спецсимволов сокращается до:
Так как нам надо делать глубокие выборки, то нам нужны те или иные формы скобок. Однако, эти символы на роль скобок совершенно не подходят. Давайте проанализируем какие виды наглядных скобок вообще есть:
()
- не экранируются в пользовательских данных стандартными инструментами (encodeURIComponent
), так что совсем не подходят.<>
- экранируются при отображении в Chrome Dev Tools, что резко снижает наглядность. Не допустимы в именах файлов. Требует неуклюжего экранирования в XML.{}
- экранируются при использовании до?
в Chrome, но если размещать запрос после?
, то всё хорошо.[]
- не экранируется ни в адресной строке браузеров, ни в их логах запросов. Вообще супер!Так что дополним допустимое множество спецсимволов квадратными скобками:
Отдельно стоит отметить символы, которые не экранируются в пользовательских данных:
Их допустимо использовать, но лишь в тех местах, где синтаксически не может быть пользовательских данных. Впритык к ним такие символы тоже лучше не использовать, чтобы визуально они не сливались.
Syntax
Так как запрос может быть довольно сложным, но представляет из себя одну строку, то крайне важно, чтобы синтаксис был на столько компактным, на сколько это возможно. Но не в ущерб наглядности, конечно же.
Чтобы полностью идентифицировать сущность нам надо указать её тип и идентификатор. Нет ничего естественнее, чем соединить их через
=
:Как можно заметить, это не полный URI, а его сокращённая форма. Если базовый URI API
https://example.org/
, то полный URI сущности получится такой:Теперь, если в выдаче по этой ссылке мы получим, например
article=123
, то такой URI тоже правильно отрезолвится в:Относительные URI хороши не только тем, что они короткие, но и тем, что мы можем работать с одним и тем же графом через разные API Enpoints, что очень полезно, например, при переезде API.
Что если нам нужен не один пользователь, а все? Просто убираем идентификатор и получаем всю коллекцию:
Да, в общем случае, имя - это не тип, а имя коллекции или поля. Воспользуемся
;
, чтобы выбрать сразу несколько коллекций:Важно отметить, что по таком запросу возвращены будут лишь списки идентификаторов сущностей, но не их данные. Поэтому воспользуемся скобками, чтобы указать, какие именно поля мы хотим видеть в выдаче:
Скобки можно использовать рекурсивно, чтобы делать глубокие выборки:
Тут мы выбрали имена и возраста конкретного пользователя и всех его друзей.
Предикат после имени в общем случае является не указанием ID, а произвольным фильтром. Просто для сущностей он интерпретируется как фильтр по ID. Для примера, загрузим не всех пользователей, а только девушек:
Тут важно отметить, что фильтрация по какому-либо полю обычно сопряжена с загрузкой этого поля. Поэтому для каждой девушки тут будет выдано не только имя и номер телефона, но и пол. Это может показаться избыточным в данном примере. Но только до тех пор, пока мы не узнаем, что под
female
может скрываться иtrap
, и было бы не плохо по выдаче это распознать. Так что клиенту лучше не строить гипотез касательно фактических значений полей, а просто получать их от сервера.Предикат может быть как позитивным, так и негативным. Так что оставим лишь незамужних девушек, используя
!=
:В качестве значения можно указать не только конкретное значение, но и диапазон, используя
@
. Диапазоны могут быть следующих видов:lo@
@hi
lo@hi
val@val
или простоval
Да, любое одиночное значение - это на самом деле диапазон. Уточним, что нас интересуют лишь взрослые девушки:
А диапазон может быть не один, а несколько, разделённых
,
. Так что добавим, что помолвленные девушки нас тоже не интересуют:Выдачу мы можем сразу отсортировать по загружаемым полям, используя префикс
+
для сортировки по возрастанию и-
для сортировки по убыванию. Для примера, выведем в начало молодых девушек, с максимальным числом талантов:Приоритет сортировки полей определяется расположением их в запросе. Кто первый встал - того и тапки.
Как и в случае с фильтрами, сортировки тоже приводят к автоматической загрузке соответствующих полей, что позволяет сохранять URI коротким.
У каждой сущности есть обобщённые поля, начинающиеся с
_
, через которые можно, например, получать агрегированную информацию, вместо детальной. Если вместо числового поля с числом талантов у нас есть лишь поле со ссылками на сущности описывающие эти таланты, но мы не хотим их все загружать, то можем просто получить их число, используя функцию_len
:В функцию агрегации передаётся не просто имя поля, а подзапрос, размер выдачи которого и будет возвращён для каждой девушки. Например, уточним, что нас интересуют лишь таланты по воспитанию детей:
Другие агрегационные функции:
_sum
,_min
,_max
. И этот список будет расширяться. Каждая функция сама определяет сколько и каких параметров ей надо передавать.Если же мы хотим получить не весь список, а, например, лишь первые 20, то можем воспользоваться другим обобщённым полем -
_num
, которое содержит номер сущности в конкретном списке (сам номер при этом не возвращается):Вот и весь язык запросов. Как видите, весьма короткий запрос позволяет довольно точно указать, что мы хотим. Если в последнем URI вы узнали себя - срочно пишите мне телеграмы. А с теми кто остался мы продолжаем..
Back Compatibility
Символ
;
для разделения параметров выбран из соображений удобочитаемости и универсальности. Однако, не сложно заметить, что если парсер будет поддерживать также и&
, то его можно будет использовать и для для обычных QueryString. Это позволяет, плавно мигрировать с QueryString на HARP:Но и это ещё не всё, добавив
/
с той же семантикой, мы сможем разбирать и pathname:А добавив ещё и
?
с#
, можем всё это комбинировать:TypeScript API
Строковое представление запросов удобно, когда работаешь с ними вручную. Но когда надо формировать их программно, подставляя динамические значения, работать со строками уже не так классно. Поэтому я реализовал пару функций:
Используются они так:
Эти функции слабо типизированы. В том смысле, что ничего не знают про структуру графа. Но мы можем объявить схему, используя, например, $hyoo_harp_scheme, основанном на $mol_data:
Теперь мы можем собирать URI и тайп чекер будет гарантировать, что мы нигде не ошибёмся в именах, и даже будет подсказывать нам при вводе:
И наоборот, полученный от клиента URI мы легко можем распарсить, получив строго типизированный JSON:
Наконец, даже если у нас уже есть какое-то JSON представление запроса, то мы можем его статикодинамически провалидировать:
Осталось научиться генерировать TS схему из её декларативного описания, чтобы не приходилось её заново описывать для каждого языка отдельно.
Response
Так как модель прикладной области представляет из себя граф, а в ответе нужно возвращать её подграф, то нам нужна возможность представления графа в виде дерева без дублирования. Для этого разделим представление графа на 4 уровня:
Type
- Определяет типы хранящихся в них сущностей. Это важно для языков со статической типизацией, чтобы использовались соответствующие структуры данных для обработки ответа.ID
- Идентифицируют сущность в рамках типа.Field
- Имя поля сущности.Value
- Значение поля, тип которого определяется схемой и именем поля.В качестве значений могут быть URI других сущностей. Именно URI, а не ID, так как в общем случае в одном списке могут идти разные типы сущностей вперемешку.
Также, помимо собственно данных ответа, стоит возвращать и сам запрос (
_query
) в том виде, как его понял сервер, чтобы разработчик клиента мог понимать всё ли он делает правильно и кому чинить проблему, когда возвращается что-то не то.Наконец, при получении любого поля любой сущности, может произойти исключительная ситуация. Возвращать ошибку для всей сущности и уж тем более для всего запроса при этом было бы не практично. Поэтому использовать HTTP коды для выражения ошибок формирования ответа не стоит. А нужно быть готовым, что на любом уровне вместо собственно данных, может прийти описание ошибки (
_error
).Format
Разным клиентам может быть удобно работать с разными форматами представления данных, поэтому используя Content Negotiation позволим ему выбирать один из следующих:
Accept: application/json
(самый популярный)Accept: application/x-harp.tree
(наиболее наглядный)Accept: application/xml
(по умолчанию)Разберём их по подробнее на примере следующего запроса:
JSON
Начнём с самого популярного сейчас формата, для лучшего понимания:
У JSON, однако, есть множество недостатков, таких как:
Поэтому я бы рекомендовал использовать следующий формат, когда это возможно..
Tree
Как видите, в этой форме выдача более компактная и менее зашумлённая. Однако, по умолчанию всё же предпочтительнее использовать следующий формат, из-за одной его уникальной возможности..
XML
Обратите внимание на подключение XSL шаблона в самом начале. Он нужен для того, чтобы при открытии URI в браузере показывался не голый дамп XML или JSON, а красивая HTML страница с иконками, кнопками и рабочими гиперссылками. Это делает URI полностью самодостаточным: вам не нужно искать где-то актуальную документацию - она доступна ровно там же, где и сами данные, по которым можно легко ходить туда-сюда (HATEOAS на максималках).
Тут я сделал небольшой пример, но можно гораздо, гораздо лучше! Как минимум, что хотелось бы видеть:
Create & Update
Обновлять ресурсы проще простого: откуда и что мы получили, то и туда отправляем. При этом отправлять имеет смысл лишь те поля, что хочется изменить, а не все подряд. Все отправляемые обновления сущностей применяются в рамках одной транзакции, так что запрос либо проходит, либо нет. В URI же указывается, что запрос должен вернуть.
Например, перешлём немного донатов автору данного опуса, и получим актуальные данные сразу по обоим пользователям:
И в случае успеха будет такой ответ:
Или создадим одной транзакцией сообщение с голосовалкой:
А в ответе будет пусто, так как мы тут ничего не запросили.
Comparison
Что ж, давайте теперь сравним наш гуманный протокол с ближайшими альтернативами..
Architecture
REST архитектура предпочтительнее. Не даром именно под неё когда-то и разрабатывался HTTP.
Common Query String
HARP обратно совместим с традиционным представлением HTTP запросов. OData же совместима полностью, но какой ценой..
Вы только сравните OData запрос:
И эквивалентный HARP запрос:
GraphQL же вообще не совместим, если, конечно, не считать совместимостью засовывание всего запроса в один get-параметр.
Single Line
GraphQL, конечно, тоже можно в одну строку упаковать, но читать его тогда крайне сложно:
Всё же он ориентирован именно на двухмерное представление и никак иначе:
Pseudo Static
HARP обратно совместим. В OData пути используются для идентификации ресурсов:
GraphQL же тут совсем не при делах.
Request vs Response
В REST протоколах достаточно разобраться в модели предметной области и ты уже имеешь всю полноту возможностей. В случае же RPC, помимо модели ответа, необходимо так же знать и кучу сигнатур процедур, и постоянно выпрашивать новые у бэкендеров. А в случае GraphQL нужно ещё знать и поддерживать отдельную модель запросов.
File Names
Не то чтобы возможность вставлять запросы в имена файлов была всем необходима, но в ряде случаев она здорово упрощает жизнь, а достаётся совсем бесплатно.
Web Tools
Возможность не ломать глаза об экранирование в разных местах использования URI экономит не очень много времени и нервов, но делает это часто.
Collection Manipulations
Не то, чтобы в GraphQL это не выразимо. Как-то же его используют. Важно понимать, что на уровне протокола эти возможности никак не специфицированы и каждый разработчик реализует их по своему. Это осложняет создание обобщённых программных решений, умеющих работать с коллекциями. Однако, этот вопрос может быть решён протоколами более высокого уровня над GraphQL.
Limitations
Когда гибкости не хватает, мы не можем нормально выразить наши потребности. Когда гибкости наоборот в избытке, то серверу сложно проанализировать сложность запроса. Поэтому тут важен баланс: запросы должны быть достаточно простыми для программного анализа, но достаточно выразительными для покрытия 99% потребностей. В OData же реализовали целый язык программирования со своим уникальным синтаксисом:
Круто, конечно, но поди разберись: этот запрос можно СУБД отдавать, или он положит её на лопатки, а чинить потом мне в 2 часа ночи?
Metadata
Классно, когда мы можем программно исследовать реальный работающий API, а не вручную ковыряться в устаревшей документации. Это позволяет писать обобщённый код для работы с любыми API, реализующими протокол. Для каждой сущности мы можем запрашивать у API много всего интересного:
В HARP у нас этот вопрос пока почти не проработан. Разве что в том примере я набросал, как это могло бы выглядеть. Остальные протоколы предоставляют лишь часть из перечисленной мета информации. Пока что этот вопрос больше всего проработан в OData.
Idempotency
HARP возводит идею идемпотентности в абсолют. OData придерживается более традиционного подхода с CRUD. Ну а GraphQL вообще не про идемпотентность. Куда-то не туда индустрия повернула. Опять.
Normal Form
Денормализованная выдача может экспоненциально размножить ваши данные. Это проще один раз увидеть, чем 100 раз услышать. Поэтому возьмём не сложный GraphQL запрос за друзьями друзей:
И получаем такую портянку:
И это всего лишь на трёх собутыльниках. Что будет твориться со школьным классом на 20 человек, я вам не покажу, чтобы не перегружать магистральные каналы связи.
Не смотря на своё название, модель данных GraphQL на самом деле не граф, а.. динамическое дерево. Со всеми отсюда вытекающими последствиями.
Крупная корпорация распиарила свою кривую поделку, а все схавали. И, причмокивая, начали пилить костыли, рассказывая остальным, как правильно её готовить:
Куда-то не туда индустрия повернула. Снова.
А вот что мы получим через HARP:
Да, когда вам будут присылать дампы ответов, вам больше не придётся переспрашивать: а запрос-то какой был? Вот он, тут же в ответе.
Post Scriptum
Как видите, это всё не тянет пока что ни на спецификацию, ни даже на какое-то законченное решение. У меня нет цели убедить вас следовать принятым мной решениям и срочно бросаться инкрементить версию вашего API.
По роду деятельности я использовал множество разнообразных API, и каждый раз их использование вгоняло меня в уныние из-за детских болезней, разложенных тут и там граблей, и просто бездумного копирования друг у друга кривых решений. Поэтому для меня было важно поделиться с вами своей болью и идеями, которые, я уверен, пригодятся вам, для проектирования своих собственных API. А если это будет что-то похожее на HARP - я буду только счастлив.
Если вас заинтересовали идеи HARP и вы видите в нём пользу для себя и для других, то приглашаю вас присоединиться к его обсуждению и доведению до уровня индустриального стандарта. Один я не справлюсь, но вместе мы можем сделать мир чуточку лучше. Хотя, признаться честно, я ставлю больше на третий тип архитектуры - бесконфликтная синхронизация сделанных локально изменений. Но это уже совсем другая история, о которой вы скоро определённо услышите. А пока..
Спасибо за внимание. Держите сердце горячим, а задницу холодной!