Синхронизация узлов

В данном разделе описаны различные каналы и способы синхронизации данных (узлов, датасетов) между элементами системы NodaLogic

  • Обмен с внешними системами

  • Доставка документов и справочников до мобильных клиентов, в т.ч. пакетная

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

  • Выгрузка с клиентов на сервер

  • Непосредственное взаимодействие с мобильным клиентом извне

Для удобства понимания, текст разбит по сценариям

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

Этот сценарий нужно разбить на 2 части:

  1. Доставка до сервера (узлов и датасетов)

  2. Передача на мобильные клиенты

Передача узлов на сервер через API

Класс, объявленный в конфигурации, автоматически имеет API, описание которого можно взять в конструкторе (оно там сгенерировано с учетом идентификаторов), а в общем виде оно такое:

Добавление/обновление узлов на сервере:

   POST /api/config/<config_uid> /node/<class_name>

[{
   "_id": "node_id",
   "field1": "value1",
   "field2": "value2"
}]

Вернуть все узлы класса:

GET /api/config/<config_uid>/node/<class_name>

Передача документов и справочников на мобильные устройства

Есть несколько способов синхронизции на выбор:

  1. Синхронизация узлов с устройствами через механизм Rooms. Работает как широковещательная подписка через либо WebSocket либо FCM и направлена прежде всего на быструю доставку. Узлы (полученные через API или собственные) регистрируются в комнате, и рассылаются устройствам, подключенным к комнате. Комнат может быть сколько угодно. Обращение через псевдонимы. Регистрация через команду _register. Одно устройство подключено к 1й группе. Есть механизм pending, хранение сообщений

  2. Синхронизация узлов через систему мессенджинга(https://habr.com/ru/articles/1034202/) – как в человекочитаемых чатах так и не отображаемая в чатах (обработчик-обработчик). Можно отправлять p2p , сообщение в группе, сообщение для конфигурации в целом. Также ориентирован на как можно более быструю доставку одного или нескольких узлов. Тут уже роутинг привязан к пользователям (и всем устройствам пользователя) , также хранение и все механизмы гарантированной доставки

  3. Синхронизация датасетов. Этот механизм, по сути, синхронизация целиком некоего неизменяемого набора внешних данных. Т.е. по-простому – выгрузили справочник в JSON и загрузили на устройстве при запуске. Целиком, без отслеживания изменений. Чтобы это шевелилось есть индексы. Датасеты привязаны к конфигурации. Кстати, при удалении конфигуаии все что с ней связано – подчищается, в т.ч. датасеты, а контракты не связаны с конфой. В целом раньше задача передачи справочников решалась через датасеты, сейчас есть выбор какой механизм использвоать.

  4. Контракт. В отличии от п.1 и п.2 – это пакетная передача узлов, ориентированная на большие данные. Но при этом не молниеносно-быстрая. Кроме того, сразу есть система отслеживания изменений. В отличии от п.3 – это узлы а не просто JSON, т.е. их можно менять, у них есть обработчики, интерфейс и т.д. Т.е. датасеты – это неизменяемые данные (только для ссылок), а тут обычные узлы.

Синхронизация через механизм Rooms

Это быстрая широковещательная доставка узлов с сервера до клиента через WebSocket или в два такта – push через FireBase messaging +запрос к API. Тут действует принцип широковещательной рассылки на группу устройств. Этот канал доставки больше ориентирован на скорость чем на прокачку больших данных и это больше для «документов», чем для «справочников»

Для этого на сервере нужно завести Room нужного типа, а на клиентах надо присоединиться к этой комнате путем сканирования QR-кода (меню «Подключиться к комнате»). В room отображаются устройства, присоединенные к комнате. Но это еще не все. Сервер должен знать какие узлы надо отправить. Для этого узел надо зарегистрировать. Это можно сделать из кода обработчиков либо вручную пользователем либо через API

Примечание

Конкретные комнаты – это объекты конкретного пользовательского инстанса, в то время как решения (конфигурации) создаются под возможность развернуть их у любого клиента, поэтому они оперируют псевдонимами а не конкретными UID или URL. В конфигурации есть раздел Rooms в котором нужно указать соотвествие псевдонима конкретной комнате

После регистрации если устройство на связи, узел будет доставлен немедленно и сразу же отрисуется в разделах (если он выводится в разделах)

Через API

Зарегистрировать все узлы в комнате. Эти команды регистируют либо все объекты класса либо выбранные объекты на конкретный UID room (не псевдоним)

POST /api/config/<config_uid>/node/<class_name>/register/<room_uid>

Зарегистрировать определенные узлы в комнате

POST /api/config/<config_uid>/node/<class_name>/register/<room_uid>

[«node_id_1»,»node_id_2»]

Через обработчики сервера

У узла есть метод _register(room_alias) - регистрирует конкретный узел к отправке в комнате по псевдониму (см. выше о соответствии псевдонимов и реальных комнат)

И метод класса Register(cls, uids: list, room_alias: str, config_uid: str = None) регистрирует объекты класса групповым способом

Через пользовательскую команду или авто регистрацию при сохранении

В классе, на закладке Миграция можно указать псевдоним комнаты и включить «Команда Регистрация», тогда, если включены Стандартные команды, то появиться кнопка Зарегистрировать которая будет регистрировать узел в комнате.

Примечание

Важный момент Эта галка включает кнопку «Выгрузить» в мобильной форме узла.

Также в классе на закладке Миграция можно включить Зарегистрировать при сохранении, тогда автоматически будет ставиться на регистрацию при сохранении. Важный момент! В мобильном клиенте при этом будет также делаться upload на сервер при сохранении. Эту галку лучше использовать очень осторожно.

Синхронизация через механизм Контракты

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

Внешняя система скидывает данные на URL и ни о чем не заботится, а сервер раздает это на устройства, принимает ack от устройств и не отправляет только те узлы, которые еще не приняты или содержат изменения.

Работает это по запросу от клиента через файлы. Т.е. клиент подписывается на получение данных по одному или нескольким контактам и скачивает при запуске, по расписанию или вручную.

Контракты не привязаны к конкретной конфигурации, одни и те же данные могут быть использованы в разных конфигурациях.

Контракт может принимать данные для конкретных классов, может данные сразу с упакованным классом (узлы без конфигураций или «самостоятельные узлы»). Со стороны клиента просто скидывается массив объектов вида [{“_id”:<внутренний id>,…}] а система сама их нормализует и превращает в удобоваримые документы системы.

Для тогда чтобы организовать контракт и начать принимать в него данные надо:

  1. Зайти в Контракты, создать новый контракт.

  2. Выбрать классы в которые он будет доставлять данные(Источник класса = class). В контракте можно также не выбирать классы (источник класса = external_only), тогда надо присылать объекты с уже упакованным классом (в параметре _class должен быть JSON объект класса а не ссылка на класс)

  3. На устройстве зайти в меню Контракты, добавить контракт, отсканировать QR код

  4. Контракт готов для приема и синхронизации. Его API можно скопировать из карточки и использовать в своем решении

Контракт принимает данные от внешней системы через PUSH запрос, на URL из карточки контракта на роут api/contracts/<uid контракта>/push

При это в теле запроса – JSON массив вида [{“_id”:<внутренний id>, другие поля объекта…}] , в системе эти данные должны превратиться в узлы, поэтому она возьмет _id и нормализует их в формате <UID конфигурации>$<класс>$<id> и создаст узлы. Если в контакте выбрано несколько классов, то будет создано несколько узлов на каждый объект и далее они будут поддерживаться независимо.

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

После того как контракт скачивается возникает событие onContractReceived на которое при необходимости можно повесить обработчик, в _data обработчика можно получить список _id узлов в виде массива в ключе nodes, ключ contract_uid - uid контракта

Можно просмотреть узлы, полученные через контракт, просто кликнув на него в Контракатах. В целом это обычные узлы. Их можно разместить в разделах интерфейса или не размещать (а только использовать в документах как ссылки или для поиска)

Контракты, если они настроены скачиваются при входе в приложение, вручную (из карточки контракта) и можно в настройках приложения задать таймер скачивания. Так как он на воркере, не чаще чем раз в 15 минут (но зато будет качать, даже когда приложение на запущено)

Различные сценарии передачи узлов и просто данных в произвольных направлениях: Система обмена сообщениями.

Также как Rooms обмен на основе мессенджинга позволяет доставлять узлы мгновенно и гарантированно, так как он ориентирован на чаты, как любой обычный мессенджер. Но в отличии от Rooms месседжинг позволяет более гибко управлять адресацией. Адресация ориентирована не только на устройства, но и на пользователей. Пользователи регистрируются на nmaker.pw, входят в систему и по этому логину им можно адресовать сообщения. Доступны как сообщения пользователю так и группы. На nmaker.pw хранятся токены устройств и доставка пользователю (или участнику группы) учитывает это и доставляет сообщения на все устройства пользователя.

Более подробный обзор можно прочитать тут: https://habr.com/ru/articles/1034202/

Мессенджинг в NodaLogic это и пользовательские чаты и обмен между обработчиками и что то среднее: чат-чат, чат-обработчик, обработчик-чат, обработчик-обработчик. Т.е. и скрытый (вне чатов) обмен и оповещения о процессах, адресованные пользователям и группам возможно с использованием этого механизма.

HTTP API

Есть REST API для внешних систем для размещения узлов и ведения в них дискуссий

POST /api/user/<username or group>/nodes-message – отправка узла «все в одном» - узел деплоится, т.е. получает свой url и дальше уже отпарвляется сообщение пользователю или группе. Для группы формат group:<group_id>

POST /api/node-discussion/by-node/<path:node_id>/messages отправка сообщение в дискуссию по узлу.

GET /api/node-discussion/by-node/<path:node_id>/messages получение всех сообщений в дискуссию по узлу.

Функции и обработчики событий уровня клиент

Функции для отображаемых сообщений

Из обработчиков можно писать «человеческие» - т.е. отображаемые сообщения как в чаты так и в дискуссии по узлу

В получателях можно указывать как логин получателя (p2p-чаты) так и группу в формате group:<ид_группы>

sendTextMessage(String target,String text) оправка текстового сообщения

sendImageMessage(String target, String text, String filename) отправляют картинку или картинку с подписью. Картинка – ссылка на s3.

sendNodeMessage(target,id) – отправляет сам узел в чат

sendTextToNodeDiscussion(NodeEx node,String text) – отправка сообщения в дискуссию по узлу

sendImageToNodeDiscussion(NodeEx node,String text, String filename) – отправка картинки в дискуссию по узлу

Обработчики уровня узла на клиенте (в формате событие/listener)

onInput/onDiscussionMessage – событие появления в дискуссии по узлу сообщения. В _data попадают переменные: «_message_text» – текст сообщения «_message_image_url» -картинка если есть «_message_sender_user» – отправитель «_message_sender_user_display_name» – отправитель, отображаемое имя

В python-обработчиках также у объекта-узла доступны:

self._deploy(self) - опубликовать/обновить узел в режиме «самостоятельного узла» self._send(self,target) - опубликовать и послать узел получателю

Data-сообщения

sendDataToNode – отправка произвольных данных (json) в узел на стороне получателя. Сообщение не отображается в чатах, по нему срабатывает событие узла onInput c listener = onDataMessage в котором приходят в data переменная message_payload – туда приходит то, что указано в payload payload= {"action":"я нажал кнопку"}; sendDataToNode("_server",_data.my_node,payload)

sendDataToConfiguration(String target, String configurationUid, JSONObject payload) – отправка произвольных данных не узлу а целиком конфигурации. Предполагается, что у получателя стоит конфигурация, которая будет принимать сообщения (как правило это та же, что и у отправителя, но не обязательно) и в ней прописано общее событие onDataMessage – входом для него будет payload, который отправил отправитель. Напомню, что немного по-разному python-обработчики и NodaScript обработчики устроены: для NS это будет просто data, а для python это будет параметр функции inputdata

Функции и обработчики уровня Сервер. Чат боты.

В сообщении sendDataToNode можно указать предопределенное «_server» в target и тогда оно не пойдет дальше сервера. Собственно если узел хранится на сервере то обратиться к нему можно и напрямую, без системы мессенджинга, правда например может не быть связи… Но дело даже не в этом. Дело в том, что узел может не стоять на сервере. Помните в самом начале – узлом может поделиться внешняя система, это по сути JSON имеющий url для доступа и ничего более. Как же он может принять и обработать сообщение?

Оказывается может точно также как и с узлом из конфигурации. Но есть нюансы – так как это узел не развернут то и python-модули обработчиков не развернуты. Для этого случая обработчики надо писать на специальном portable-скрипте python, в системе он называется PythonScript. В чем суть? Вы пишете как скрипт обработчика в окне конфигуратора, потом он сохраняется в виде s3-ссылки (он и доступен по ней, она публичная). В класс упаковывается ссыка на скрипт, когда надо выполнить – она достаётся из s3. Естественно, оно кешируется как и сам узел в download_url чтобы не скачивать по новой каждый раз.

В итоге узел получает onInputServer/onDataMessage уже на сервере

И также сервер (по той же схеме) может перехватывать сообщения в дискуссии по узлу. По сути узел на сервере может быть своеобразным чат-ботом - перехватывать всю переписку и писать по мере надобности в чаты.

Сервер можно подписать на событие по каждому сообщению в переписке по узлу (событие уровня узла) onInputServer/onDiscussionMessage и соответственно сервер через message_text видит что вы ему написали.

При этом узел на сервере сам может писать в чаты:

sendTextMessage(target: str, text: str)

sendImageMessage(target: str, text: str, filename: str)

sendTextToNodeDiscussion(node, text: str)

sendImageToNodeDiscussion(node, text: str, filename: str)

Некоторая техническая информация

Серверная часть написана на python и доступна на Github (ссылки в конце статьи), т.е. можно скачать и развернуть у себя – это вместе с всей остальной базовой функциональностью NodaLogic – сервером узлов, веб-клиентами, конфигуратором. Если этого не делать – то все проходит через nmaker.pw. Тогда данные будут все у вас – узлы, сообщения. Но есть работа с Firebase Messaging она только на стороне nmaker так как учетка храниться там. Т.е. единственное, что недоступно – это отправка пушей. При схеме «сервер у вас» вам просто надо будет пинать nmaker чтобы он отправил пуш. Еще в этой схеме широко используются s3-хранилище – для изображений и для обработчиков. Тут такая схема – для безопасности это обернуто в серверное API, т.е. на клиенте не хранятся ключи доступа к s3 на запись (предполагается что на чтение ключей не надо – оно публичное, если нет, нужно добавить авторизацию и на чтение). Клиент получает временный токен, записывает с ним данные и получает ссылку. В nmaker свое хранилище, если вы будете разворачивать свой NL-сервер, вам понадобится также s3-хранилище (в случае, если вы будете использовать PythonScript обработчики или картинки) и надо будет прописать данные своей учетки в boto3.client

Сценарий «Выгрузка узлов с мобильного клиента на сервер.»

Это доставка новых/обновление ранее выгруженных узлов новыми данными. При проектировании клиент-серверных решений, вы можете не ограничиваться только подобной синхронизацией (которая при выгрузке, по сути прсто обновляет _data сервера, _data узла на клиенте) а передавать информацию через обращение к серверным методам. Также можно и выгружать узлы (через RomoteClass). Но для простых ситуаций можно использовать просто варианты upload описанные ниже

Примечание

Конкретный сервер – это конкретный инстанс вашего решения (он может быть развернут где угодно), а конфигурации выпускаются для всех инстансов. Поэтому решения оперируют не конкретными URL а псевдонинами серверов. И должен быть хотя бы один псевдоним основного сервера обязательно. Это настраивается в разделе Серверы конфигурации. В большинстве случаем сервер один и достаточно просто создать 1 сервер с галочкой Основной сервер.

Выгрузка может быть интерактивной или через обработчик метода.

Для выгрузки через обработчик, нужно использовать методы

_upload(server_alias=None)- метод объекта (узла). Выгружает на сервер по умолчанию, либо на заданный псевдоним result,error = self._upload() if result!=True...

_delete_from_server(server_alias=None) - метод объекта (узла). Удаляет с сервера по умолчанию, либо на заданный псевдоним

_register(room_uid) - метод объекта (узла). делает регистрацию в комнате

_upload_all() - метод класса. Выгружает все

register_all(room_uid) - метод класса. Регистрируется все

UploadMany(ids) - общий метод - выгружает синхронно массив id узлов. В PythonScript UploadIds(ids) или выгрузить_массив(ids)

QuploadMany(ids) - общий метод - выгружает асинхронно/через очередь массив узлов. В PythonScript QuploadIds(ids) или выгрузить_массив_через_очередь(ids)

Синхронная выгрузка и выгрузка через очередь

Вышеописанные способы являются синхронными - вы выполняете _upload , отправляется запрос, ожидание, если не получилось то False. Но _upload можно также делать через очередь. В классе (на закладке Миграция) можно установить Send via queue. Тогда происходит следующее: отправка пытается выполниться асинхронно, если не получилось, то помещается в очередь и дальше уже задача доставить этот узел до сервера - возлагается на воркеры, они будут пытаться даже если приложение не активно, увеличивая интервал между попытками вдвое. Очередь имеет визуальный интерфейс (главное меню/Очередь выгрузки) в котром можно оценить состояние доставки.

Также, в случае с очередью есть специальные команды и события:

onInput/onUploadSent - событие узла, узел ушел при отправки через очередь успешно

onInput/onUploadQueued - событие узла, узел не смог отправиться сразу, встал в очередь

_qupload - метод узла для выгрузки асинхронно/через очередь. В NodaScript qupload/выгрузить_через_очередь - выгружакт ткущий узел асинхронно.

UploadQueueIds - функция получения массива id узлов, поставленных в очередь

Интерактивная задается на закладке Миграция точно также как регистрация в Rooms. Просто для узла на сервере – это регистрация в Rooms, а для мобильного клиента – upload на сервер. Особенно осторожно включайте галку Регистрировать при сохранении, потому что на клиенте есть например автосохранение и

Сценарий «Забрать узлы с сервера или произвести с ними прочие действия»

Вернуть все узлы класса

GET /api/config/<config_uid>/node/<class_name>

Get specific node : GET    /api/config/<config_uid>/node/<class_name>/<node_id>

Update _data in specific node : PUT /api/config/<config_uid>/ /node/<class_name>/<node_id>

Delete specific node : DELETE /api/config/<config_uid>/node/<class_name>/<node_id>

Сченарий «Передача узлов на устройство через файлы»

Если нет интернета либо не хочется использовать rooms, можно передать файлы на устройство люым способом и либо «Открыть» либо «Поделиться» с NodaLogic.

Это должен быть *.nl файл особого формата, в обязательном порядке содержащий ссылку на класс в формате <uid конфигурации>$<имя класса> и _data.

Пример такого формата:

  [{
"_id":"1010",
"_class":"885a12de-2bb5-4222-a671-9a7286902938$MyOrder",
"_data":{
"order_number":"00-5000",
"_cover":[["@order_number"]]
}
}
]