1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-07 21:37:04 +09:00

GO-1733 Improve readme

This commit is contained in:
Mikhail Iudin 2023-08-14 14:21:20 +02:00
parent 0eef68097a
commit 9a263e8f33
No known key found for this signature in database
GPG key ID: FAAAA8BAABDFF1C0
9 changed files with 210 additions and 387 deletions

187
README.md
View file

@ -1,185 +1,12 @@
# Anytype Heart
Middleware library for Anytype, distributed as part of the Anytype clients.
## Build from Source
1. Install Golang 1.21.x [from here](http://golang.org/dl/) or using preferred package manager
2. Follow instructions below for the target systems
### Install local deps
#### Mac
As of 16.01.23 last protobuf version (21.12) broke the JS plugin support, so you can use the v3 branch:
```
brew install protobuf@3
```
To generate Swift protobuf:
```
brew install swift-protobuf
```
#### Debian/Ubuntu
We need to have protoc binary (3.x version) and libprotoc headers in orderto build the grpc-web plugin
```
apt install protobuf-compiler libprotoc-dev
```
### Build and install for the [desktop client](https://github.com/anyproto/anytype-ts)
`make install-dev-js` — build the local server and copy it and protobuf binding into `../anytype-ts`
Parameters:
- `ANY_SYNC_NETWORK=/path/to/network.yml` — build using self-hosted [network configuration](https://tech.anytype.io/anytype-heart/configuration)
### Build for iOS
Instructions to set up environment for iOS: [here](https://github.com/anyproto/anytype-swift/blob/main/docs/Setup_For_Middleware.md)
1. `make build-ios` to build the framework into `dist/ios` folder
Parameters:
- `ANY_SYNC_NETWORK=/path/to/network.yml` — build using self-hosted [network configuration](https://tech.anytype.io/anytype-heart/configuration)
2. `make protos-swift` to generate swift protobuf bindings into `dist/ios/pb`
### Build for Android
Instructions to setup environment for Android: [here](https://github.com/anyproto/anytype-kotlin/blob/main/docs/Setup_For_Middleware.md)
1. `make build-android` to build the library into `dist/android` folder
Parameters:
- `ANY_SYNC_NETWORK=/path/to/network.yml` — build using self-hosted [network configuration](https://tech.anytype.io/anytype-heart/configuration)
2. `make protos-java` to generate java protobuf bindings into `dist/android/pb`
## Rebuild protobuf generated files
First, you need to install [protobuf](https://github.com/anyproto/anytype-heart#install-local-deps-mac) pkg using your preferred package manager.
This repo uses custom protoc located at [anyproto/protobuf](https://github.com/anyproto/protobuf/tree/master/protoc-gen-gogo). It adds `gomobile` plugin and some env-controlled options to control the generated code style.
This protobuf generator will replace your `protoc` binary, BTW it doesn't have any breaking changes for other protobuf and grpc code
You can override the binary with a simple command:
```
make setup-protoc
```
Then you can easily regenerate proto files:
```
make protos
```
## Run tests
Install dependencies for running tests and generate mocks:
```
make test-deps
```
GO test:
```
make test
```
You'll need to install latest (at least clang 15)
```
brew install llvm
echo 'export PATH="/<homebrew location>/llvm/bin:$PATH"' >> ~/.zshrc
```
### Integration tests
First you need to start a docker container via docker-compose:
```
export ANYTYPE_TEST_GRPC_PORT=31088
docker-compose up -d
```
Then you can run the basic integration tests:
```
make test-integration
```
## Run local gRPC server to debug
⚠️ Make sure to update/install protobuf compiler from [this repo](https://github.com/anyproto/protobuf) using `make setup-protoc`
Commands:
- `make run-server` - builds proto files for grpc server, builds the binary and runs it
- `make build-server` - builds proto files for grpc server and builds the binary into `dist/server`
If you want to change the default port(9999):
`ANYTYPE_GRPC_ADDR=127.0.0.1:8888 make run-debug`
----
## Useful tools for debug
### Debug server
Use env var ANYDEBUG=address to enable debugging HTTP server. For example: `ANYDEBUG=:6061` will start debug server on port 6061
You can find all endpoints in `/debug` page. For example: http://localhost:6061/debug
### gRPC logging
In order to log mw gRPC requests/responses use `ANYTYPE_GRPC_LOG` env var:
- `ANYTYPE_LOG_LEVEL="grpc=DEBUG" ANYTYPE_GRPC_LOG=1` - log only method names
- `ANYTYPE_LOG_LEVEL="grpc=DEBUG" ANYTYPE_GRPC_LOG=2` - log method names + payloads for commands
- `ANYTYPE_LOG_LEVEL="grpc=DEBUG" ANYTYPE_GRPC_LOG=2` - log method names + payloads for commands&events
### gRPC tracing
1. Run jaeger UI on the local machine:
```docker run --rm -d -p6832:6832/udp -p6831:6831/udp -p16686:16686 -p5778:5778 -p5775:5775/udp jaegertracing/all-in-one:latest```
2. Run mw with `ANYTYPE_GRPC_TRACE` env var:
- `ANYTYPE_GRPC_TRACE=1` - log only method names/times
- `ANYTYPE_GRPC_TRACE=2` - log method names + payloads for commands
- `ANYTYPE_GRPC_TRACE=2` - log method names + payloads for commands&events
3. Open Jaeger UI at http://localhost:16686
### Debug tree
1. You can use `cmd/debugtree.go` to perform different operations with tree exported in zip archive (`rpc DebugTree`)
2. The usage looks like this `go run debugtree.go -j -t -f [path to zip archive]` where `-t` tells the cmd to generate tree graph view and `-j` - to generate json representation of the tree (i.e. data in each individual block)
3. You can use flag `-r` to build the tree from its root, that way you will see all the changes in the tree, and not only those from the common snapshot
3. For more info please check the command usage in `debugtree.go`
### gRPC clients
#### GUI
https://github.com/uw-labs/bloomrpc
HowTo: Set the import path to the middleware root, then select commands.proto file
#### CLI
https://github.com/fullstorydev/grpcurl
You should specify import-path to the root of anytype-heart repository and gRPC port of running application
Command examples:
- List available methods
```
grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto localhost:31007 describe
```
- Describe method signature
```
grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto localhost:31007 describe anytype.ClientCommands.ObjectCreate
```
- Describe structure of specified protobuf message
```
grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto localhost:31007 describe .anytype.Rpc.Object.Create.Request
```
- Call method with specified plain-text payload
```
grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d '{"details": {"name": "hello there", "type": "ot-page"}}' localhost:31007 anytype.ClientCommands.ObjectCreate
```
- Call method using unix pipe
```
echo '{"details": {"name": "hello there", "type": "ot-page"}}' | grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d @ localhost:31007 anytype.ClientCommands.ObjectCreate
```
## Running with prometheus and grafana
- `cd metrics/docker` cd into folder with docker-compose file
- `docker-compose up` - run the prometheus/grafana
- use `ANYTYPE_PROM=0.0.0.0:9094` when running middleware to enable metrics collection. Client commands metrics available only in gRPC mode
- open http://127.0.0.1:3000 to view collected metrics in Grafana. You can find several dashboards there:
- **MW** internal middleware metrics such as changes, added and created threads histograms
- **MW commands server** metrics for clients commands. Works only in grpc-server mode
## Docs
- [Build instructions](docs/Build.md)
- [Protobuf generation](docs/Protogen.md)
- [Testing instructions](docs/Testing.md)
- [Debug instructions](docs/Debug.md)
- [Project architecture](docs/Architecture.md)
## Contribution
Thank you for your desire to develop Anytype together!
@ -195,4 +22,4 @@ Thank you for your desire to develop Anytype together!
---
Made by Any — a Swiss association 🇨🇭
Licensed under [Any Source Available License 1.0](./LICENSE.md).
Licensed under [Any Source Available License 1.0](LICENSE.md).

View file

@ -1,27 +0,0 @@
# Development
## Services/components
### Bootstrapping
If you need your component to be visible to other components, you need to register it in Service Locator's registry.
To do it go to `Bootstrap` function in `core/anytype/bootstrap.go` and `Register` it
### Dependency injection
We use our own implementation of Service Locator pattern: `github.com/anyproto/any-sync/app`.
## Writing tests
### Structure of tests
Prefer structuring your tests in Act-Arrange-Assert style. Use comments for visual separation of those test parts, like:
```go
// Given
...
// When
...
// Then
```
### Fixtures for services under test
Define `fixture` structure to easily bootstrap service and its dependencies. You can find examples in our code.
### Mocking
Prefer using Mockery for new mocks. It's configured in `.mockery.yaml`

9
docs/Architecture.md Normal file
View file

@ -0,0 +1,9 @@
# Architecture
## Services/components
### Bootstrapping
If you need your component to be visible to other components, you need to register it in Service Locator's registry.
To do it go to `Bootstrap` function in `core/anytype/bootstrap.go` and `Register` it
### Dependency injection
We use our own implementation of Service Locator pattern: `github.com/anyproto/any-sync/app`.

45
docs/Build.md Normal file
View file

@ -0,0 +1,45 @@
# Build instructions
## Build from Source
1. Install Golang 1.21.x [from here](http://golang.org/dl/) or using preferred package manager
2. Follow instructions below for the target systems
### Install local deps
#### Mac
As of 16.01.23 last protobuf version (21.12) broke the JS plugin support, so you can use the v3 branch:
```
brew install protobuf@3
```
To generate Swift protobuf:
```
brew install swift-protobuf
```
#### Debian/Ubuntu
We need to have protoc binary (3.x version) and libprotoc headers in orderto build the grpc-web plugin
```
apt install protobuf-compiler libprotoc-dev
```
### Build and install for the [desktop client](https://github.com/anyproto/anytype-ts)
`make install-dev-js` — build the local server and copy it and protobuf binding into `../anytype-ts`
Parameters:
- `ANY_SYNC_NETWORK=/path/to/network.yml` — build using self-hosted [network configuration](https://tech.anytype.io/anytype-heart/configuration)
### Build for iOS
Instructions to set up environment for iOS: [here](https://github.com/anyproto/anytype-swift/blob/main/docs/Setup_For_Middleware.md)
1. `make build-ios` to build the framework into `dist/ios` folder
Parameters:
- `ANY_SYNC_NETWORK=/path/to/network.yml` — build using self-hosted [network configuration](https://tech.anytype.io/anytype-heart/configuration)
2. `make protos-swift` to generate swift protobuf bindings into `dist/ios/pb`
### Build for Android
Instructions to setup environment for Android: [here](https://github.com/anyproto/anytype-kotlin/blob/main/docs/Setup_For_Middleware.md)
1. `make build-android` to build the library into `dist/android` folder
Parameters:
- `ANY_SYNC_NETWORK=/path/to/network.yml` — build using self-hosted [network configuration](https://tech.anytype.io/anytype-heart/configuration)
2. `make protos-java` to generate java protobuf bindings into `dist/android/pb`

89
docs/Debug.md Normal file
View file

@ -0,0 +1,89 @@
# Debug
## Run local gRPC server to debug
⚠️ Make sure to update/install protobuf compiler from [this repo](https://github.com/anyproto/protobuf) using `make setup-protoc`
Commands:
- `make run-server` - builds proto files for grpc server, builds the binary and runs it
- `make build-server` - builds proto files for grpc server and builds the binary into `dist/server`
If you want to change the default port(9999):
`ANYTYPE_GRPC_ADDR=127.0.0.1:8888 make run-debug`
----
## Useful tools for debug
### Debug server
Use env var ANYDEBUG=address to enable debugging HTTP server. For example: `ANYDEBUG=:6061` will start debug server on port 6061
You can find all endpoints in `/debug` page. For example: http://localhost:6061/debug
### gRPC logging
In order to log mw gRPC requests/responses use `ANYTYPE_GRPC_LOG` env var:
- `ANYTYPE_LOG_LEVEL="grpc=DEBUG" ANYTYPE_GRPC_LOG=1` - log only method names
- `ANYTYPE_LOG_LEVEL="grpc=DEBUG" ANYTYPE_GRPC_LOG=2` - log method names + payloads for commands
- `ANYTYPE_LOG_LEVEL="grpc=DEBUG" ANYTYPE_GRPC_LOG=2` - log method names + payloads for commands&events
### gRPC tracing
1. Run jaeger UI on the local machine:
```docker run --rm -d -p6832:6832/udp -p6831:6831/udp -p16686:16686 -p5778:5778 -p5775:5775/udp jaegertracing/all-in-one:latest```
2. Run mw with `ANYTYPE_GRPC_TRACE` env var:
- `ANYTYPE_GRPC_TRACE=1` - log only method names/times
- `ANYTYPE_GRPC_TRACE=2` - log method names + payloads for commands
- `ANYTYPE_GRPC_TRACE=2` - log method names + payloads for commands&events
3. Open Jaeger UI at http://localhost:16686
### Debug tree
1. You can use `cmd/debugtree.go` to perform different operations with tree exported in zip archive (`rpc DebugTree`)
2. The usage looks like this `go run debugtree.go -j -t -f [path to zip archive]` where `-t` tells the cmd to generate tree graph view and `-j` - to generate json representation of the tree (i.e. data in each individual block)
3. You can use flag `-r` to build the tree from its root, that way you will see all the changes in the tree, and not only those from the common snapshot
3. For more info please check the command usage in `debugtree.go`
### gRPC clients
#### GUI
https://github.com/uw-labs/bloomrpc
HowTo: Set the import path to the middleware root, then select commands.proto file
#### CLI
https://github.com/fullstorydev/grpcurl
You should specify import-path to the root of anytype-heart repository and gRPC port of running application
Command examples:
- List available methods
```
grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto localhost:31007 describe
```
- Describe method signature
```
grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto localhost:31007 describe anytype.ClientCommands.ObjectCreate
```
- Describe structure of specified protobuf message
```
grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto localhost:31007 describe .anytype.Rpc.Object.Create.Request
```
- Call method with specified plain-text payload
```
grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d '{"details": {"name": "hello there", "type": "ot-page"}}' localhost:31007 anytype.ClientCommands.ObjectCreate
```
- Call method using unix pipe
```
echo '{"details": {"name": "hello there", "type": "ot-page"}}' | grpcurl -import-path ../anytype-heart/ -proto pb/protos/service/service.proto -plaintext -d @ localhost:31007 anytype.ClientCommands.ObjectCreate
```
## Running with prometheus and grafana
- `cd metrics/docker` cd into folder with docker-compose file
- `docker-compose up` - run the prometheus/grafana
- use `ANYTYPE_PROM=0.0.0.0:9094` when running middleware to enable metrics collection. Client commands metrics available only in gRPC mode
- open http://127.0.0.1:3000 to view collected metrics in Grafana. You can find several dashboards there:
- **MW** internal middleware metrics such as changes, added and created threads histograms
- **MW commands server** metrics for clients commands. Works only in grpc-server mode

14
docs/Protogen.md Normal file
View file

@ -0,0 +1,14 @@
## Rebuild protobuf generated files
First, you need to install [protobuf](https://github.com/anyproto/anytype-heart#install-local-deps-mac) pkg using your preferred package manager.
This repo uses custom protoc located at [anyproto/protobuf](https://github.com/anyproto/protobuf/tree/master/protoc-gen-gogo). It adds `gomobile` plugin and some env-controlled options to control the generated code style.
This protobuf generator will replace your `protoc` binary, BTW it doesn't have any breaking changes for other protobuf and grpc code
You can override the binary with a simple command:
```
make setup-protoc
```
Then you can easily regenerate proto files:
```
make protos
```

46
docs/Testing.md Normal file
View file

@ -0,0 +1,46 @@
# Testing
## Run tests
Install dependencies for running tests and generate mocks:
```
make test-deps
```
GO test:
```
make test
```
You'll need to install latest (at least clang 15)
```
brew install llvm
echo 'export PATH="/<homebrew location>/llvm/bin:$PATH"' >> ~/.zshrc
```
### Integration tests
First you need to start a docker container via docker-compose:
```
export ANYTYPE_TEST_GRPC_PORT=31088
docker-compose up -d
```
Then you can run the basic integration tests:
```
make test-integration
```
## Writing tests
### Structure of tests
Prefer structuring your tests in Act-Arrange-Assert style. Use comments for visual separation of those test parts, like:
```go
// Given
...
// When
...
// Then
```
### Fixtures for services under test
Define `fixture` structure to easily bootstrap service and its dependencies. You can find examples in our code.
### Mocking
Prefer using Mockery for new mocks. It's configured in `.mockery.yaml`

View file

@ -1,55 +0,0 @@
### Store
Должна быть некоторая структура, которая будет хранить состояние приложения, а именно:
1. Какие документы/блоки с какими версиями сохранены.
2. Какие экраны были открыты в последний раз, какие окна.
3. Все настройки клиента, которые не сохраняются в IPFS, должны быть в этой структуре состояния.
4. В каких поля ввода что содержится.
Когда Middleware штатно/аварийно завершает своё выполнение, при следующем запуске состояние должно восстанавливаться из этого файла.
Раз в несколько секунд middle записывает своё состояние в этот файл.
#### Наверное, плохая идея: store описан в .proto, клиент имеет к нему доступ
Плохая, потому что иначе на клиенте придется продублировать много функционала с middle, клиент должен быть абстрагирован от этой логики.
#### Получится-ли сходу реализовать соответствие files <--> documents/blocks
А что, если вот так:
1. Есть центральная директория /Anytype, в которой лежат папки документов – на каждый документ по папке.
2. В папке документа лежат файлы по файлу на каждый блок. Если у документа есть внутренние документы, то там же, соответственно, лежат соответствующие им папки. Плюс лежит .json файл, который содержит структурную информацию документа. Тоже типа такой блок.
3. В папке лежит .git папка, в которой содержится история версий.
Так вот, пишем watcher, который следит за изменениями как внутри файловой системы в директории /Anytype, так и за изменениями, сделанными в приложени Anytype, и автоматически коммитит изменения, меняет файлы.
То есть, например, скидываем картинки в папку /Anytype/kitties, и автоматом все юзеры, кто работает с этим документом, получают новые блоки с картинками.
Текстовые файлы, например, хранятся в виде markdown файлов.
Информация о том, как отображать картинку, может хранится в meta-блоке изображения.
В идеале сделать так, чтобы информация по минимуму дублировалась. Однако мы не ограничены в использовании дополнительных файлов, которые будут хранить какие-то промежуточные представления, если они не будут содержать тяжелый контент.
#### Как это заимплементить, MVP
Документ состоит только из текстовых блоков.
Блоки и документ каждый имеет свою историю версий.
Есть чейн изменений блока, и есть его скомпилированный стейт. Скомпилированный стейт .md файл. Чейн изменений файлы в специальной директории.
Или так блоки и документы хранятся на одном уровне иерархии, а в папке документа хранятся ссылки на эти блоки/файлы.
Есть директория /anytype.
CLI-клиент, в котором есть команды:
```js
block_create() -> docId
block_set(id, 'text')
block_remove(id)
doc_create() -> docId
doc_addBlock(docId, id, prior)
doc_setPrior(docId, id, prior)
doc_removeBlock(docId, id)
```
Есть watcher. Это JS-скрипт, который чекает изменения файлов в папке.

View file

@ -1,125 +0,0 @@
### User stories
#### 0. Как ответы задавать
```js
// 1. Избыточная информация, плюс Result получится слишком кастомным
Package (id:'0x765', Reply {to:'0x123', error:Error{ type:WRONG_MNEMONIC, message:'Mnemonic is wrong' }, Result:{ type:FAILURE }})
// 2. Сообщение тоже получается ибыточным. из Error.type мы можем его получать на клиенте, плюс локализация
Package (id:'0x765', Status{ type:WRONG_MNEMONIC, message:'Mnemonic is wrong' })
// 3.
Package (id:'0x765', Error{ type:WRONG_MNEMONIC })
Package (id:'0x765', Success{})
// 4.
Package (id:'0x765', Status{ type:WRONG_MNEMONIC })
Package (id:'0x765', Status{ type:SUCCESS })
```
Выбрали четвертый вариант
#### 1. Log in
```js
// 1. Клиент передает мнемонику в middle, которую ввел пользователь
Front: Package (id:'0x123', WalletLogin { mnemonic:'abc def ... xyz', pin:'12345'} )
Middle: Package (id:'0x980', Status { replyTo:'0x123', type:SUCCESS })
// 2. Middle начинает слать аккаунты
Middle: Package (id:'0x789', AccountFound { Account {name:'Pablo', id:'0xabcabc', icon:'0x123123'}}})
Middle: Package (id:'0x678', AccountFound{ Account {name:'Carlito', id:'0xabcabc', icon:'0x123123'}})
// 2.B. Middle сообщает об ошибке
Middle: Package (id:'0x765', Status { replyTo'0x123': type: WRONG_MNEMONIC })
// 3. Клиент отправляет аккаунт, под которым хочет работать
Front: Package (id:'0x789', AccountSelect {id:'0xabcabc'}})
Middle: Package (id:'0x777', Status { replyTo'0x789': type: SUCCESS })
```
#### 2. Sign up
```js
// 1. Просим создать аккаунт
Front: Package (id:'0x123', WalletCreate {} )
Middle: Package (id:'0x980', Status { replyTo'0x123': type: SUCCESS })
Front: Package (id:'0x345', AccountCreate { name:'Carlos', icon:'0x1231243257', pin:'1232724'} )
Middle: Package (id:'0x456', Status { replyTo'0x345': type: SUCCESS })
```
#### 3A. Получение списка документов (если store контролирует клиент)
Нужно получить список id документов, их имена, аватарки, хеши последних актуальных версий
Когда нужен этот сценарий? Когда юзер хочет запустить главный экран.
1. Юзер запустил приложение. Middle уже авторизован, пока ничего не отрисовано
2. Фронт сообщает о том, какие у него документы есть
```js
Front: Message StartUp (docs: [
{root:0x345, last_ver:0x123},
{root:0x456, last_ver:0x234},
...])
```
3. Миддл сообщает, какие документы поменяли имена/аватарки, присылает их, актуальная ли версия хранимого документа, и если нет, то какая актуальная (или массив хешей CRDT-изменений, которые нужно скачать для восстановления до актуальной версии)
```js
Middle: Message StartUp reply (docs: [
{root:0x345, status:last_version},
{root:0x456, status:outdated, name:same, icon:b64(newIcon.png), lastVersion:0x789},
...])
```
4. Клиент применяет полученные изменения и отображает список документов
#### 3B. Получение списка документов (если store контролирует middle)
Не вижу проблемы, если middle будет контролировать store. Плюсы логика с клиента переходит на middle.
1. Юзер запустил приложение. Middle уже авторизован, пока ничего не отрисовано
2. Клиент сообщает, что он запустился
```js
Front: Message StartUp ()
```
3. Middle отдает данные, которые нужно отрисовать на главной странице список документов
```js
Middle: Message DocumentsOrganizier (docs: [
{name:'Doc 1', version:0x123, icon:icon1.png},
{name:'Doc 2', version:0x234, icon:icon2.png},
...])
```
Логика по получению актуальных версий, сверки и прочего полностью абстрагирована от клиента.
4. Клиент просто отрисовывает полученные данные.
##### Cообщения сценария
1. Сообщение, которым клиент сообщает, что ему нужен отрисовать список документов. Возникает в сценариях, когда мы на главном меню, плюс, возможно, в других сценариях (например, какое-то всплывающее контекстное меню, в котором отображаются документы).
2. Сообщение, в котором middle передает список всех документов.
```js
// С помощью запроса с entity == docHeaders можно запросить список документов
// Выделять отдельное в сообщение DocumentsRequest не вижу смысла, оно слишком тривиальное получится
message Request {
string id = 0;
string entity = 1;
string target = 2;
}
// когда приходит DocHeaders, автоматом на фронте отрисовывается соответствующий target с docHeaders.
message DocHeaders {
string id = 0;
repeated DocHeader docHeaders = 1;
}
message DocHeader {
string id = 0;
string name = 1;
string root = 2;
string version = 3;
string iconName = 4;
}
```
#### 4. Получение документа
1. Юзер находится в главном меню и видит список документов. Юзер нажимает на один из них
2. Клиент отправляет сообщение `Request { entity:document, target:0x123123 }`
3. Middle отправляет сообщение `Document { root:0x123123, ..., blocks:[...] }`
4. Клиент отрисовывает документ.