mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
save doc delta & cache the connect user information
This commit is contained in:
@ -12,10 +12,12 @@ import 'package:protobuf/protobuf.dart' as $pb;
|
|||||||
class WsDataType extends $pb.ProtobufEnum {
|
class WsDataType extends $pb.ProtobufEnum {
|
||||||
static const WsDataType Acked = WsDataType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Acked');
|
static const WsDataType Acked = WsDataType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Acked');
|
||||||
static const WsDataType Rev = WsDataType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rev');
|
static const WsDataType Rev = WsDataType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rev');
|
||||||
|
static const WsDataType Conflict = WsDataType._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Conflict');
|
||||||
|
|
||||||
static const $core.List<WsDataType> values = <WsDataType> [
|
static const $core.List<WsDataType> values = <WsDataType> [
|
||||||
Acked,
|
Acked,
|
||||||
Rev,
|
Rev,
|
||||||
|
Conflict,
|
||||||
];
|
];
|
||||||
|
|
||||||
static final $core.Map<$core.int, WsDataType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
static final $core.Map<$core.int, WsDataType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||||
|
@ -14,11 +14,12 @@ const WsDataType$json = const {
|
|||||||
'2': const [
|
'2': const [
|
||||||
const {'1': 'Acked', '2': 0},
|
const {'1': 'Acked', '2': 0},
|
||||||
const {'1': 'Rev', '2': 1},
|
const {'1': 'Rev', '2': 1},
|
||||||
|
const {'1': 'Conflict', '2': 2},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Descriptor for `WsDataType`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
/// Descriptor for `WsDataType`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
||||||
final $typed_data.Uint8List wsDataTypeDescriptor = $convert.base64Decode('CgpXc0RhdGFUeXBlEgkKBUFja2VkEAASBwoDUmV2EAE=');
|
final $typed_data.Uint8List wsDataTypeDescriptor = $convert.base64Decode('CgpXc0RhdGFUeXBlEgkKBUFja2VkEAASBwoDUmV2EAESDAoIQ29uZmxpY3QQAg==');
|
||||||
@$core.Deprecated('Use wsDocumentDataDescriptor instead')
|
@$core.Deprecated('Use wsDocumentDataDescriptor instead')
|
||||||
const WsDocumentData$json = const {
|
const WsDocumentData$json = const {
|
||||||
'1': 'WsDocumentData',
|
'1': 'WsDocumentData',
|
||||||
|
@ -319,64 +319,3 @@ class UpdateViewParams extends $pb.GeneratedMessage {
|
|||||||
void clearIsTrash() => clearField(5);
|
void clearIsTrash() => clearField(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DocDeltaRequest extends $pb.GeneratedMessage {
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'DocDeltaRequest', createEmptyInstance: create)
|
|
||||||
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'viewId')
|
|
||||||
..a<$core.List<$core.int>>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'data', $pb.PbFieldType.OY)
|
|
||||||
..hasRequiredFields = false
|
|
||||||
;
|
|
||||||
|
|
||||||
DocDeltaRequest._() : super();
|
|
||||||
factory DocDeltaRequest({
|
|
||||||
$core.String? viewId,
|
|
||||||
$core.List<$core.int>? data,
|
|
||||||
}) {
|
|
||||||
final _result = create();
|
|
||||||
if (viewId != null) {
|
|
||||||
_result.viewId = viewId;
|
|
||||||
}
|
|
||||||
if (data != null) {
|
|
||||||
_result.data = data;
|
|
||||||
}
|
|
||||||
return _result;
|
|
||||||
}
|
|
||||||
factory DocDeltaRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
|
||||||
factory DocDeltaRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
|
||||||
@$core.Deprecated(
|
|
||||||
'Using this can add significant overhead to your binary. '
|
|
||||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
|
||||||
'Will be removed in next major version')
|
|
||||||
DocDeltaRequest clone() => DocDeltaRequest()..mergeFromMessage(this);
|
|
||||||
@$core.Deprecated(
|
|
||||||
'Using this can add significant overhead to your binary. '
|
|
||||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
|
||||||
'Will be removed in next major version')
|
|
||||||
DocDeltaRequest copyWith(void Function(DocDeltaRequest) updates) => super.copyWith((message) => updates(message as DocDeltaRequest)) as DocDeltaRequest; // ignore: deprecated_member_use
|
|
||||||
$pb.BuilderInfo get info_ => _i;
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static DocDeltaRequest create() => DocDeltaRequest._();
|
|
||||||
DocDeltaRequest createEmptyInstance() => create();
|
|
||||||
static $pb.PbList<DocDeltaRequest> createRepeated() => $pb.PbList<DocDeltaRequest>();
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static DocDeltaRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DocDeltaRequest>(create);
|
|
||||||
static DocDeltaRequest? _defaultInstance;
|
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.String get viewId => $_getSZ(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
set viewId($core.String v) { $_setString(0, v); }
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.bool hasViewId() => $_has(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
void clearViewId() => clearField(1);
|
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
$core.List<$core.int> get data => $_getN(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
set data($core.List<$core.int> v) { $_setBytes(1, v); }
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
$core.bool hasData() => $_has(1);
|
|
||||||
@$pb.TagNumber(2)
|
|
||||||
void clearData() => clearField(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@ -48,14 +48,3 @@ const UpdateViewParams$json = const {
|
|||||||
|
|
||||||
/// Descriptor for `UpdateViewParams`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `UpdateViewParams`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List updateViewParamsDescriptor = $convert.base64Decode('ChBVcGRhdGVWaWV3UGFyYW1zEhcKB3ZpZXdfaWQYASABKAlSBnZpZXdJZBIUCgRuYW1lGAIgASgJSABSBG5hbWUSFAoEZGVzYxgDIAEoCUgBUgRkZXNjEh4KCXRodW1ibmFpbBgEIAEoCUgCUgl0aHVtYm5haWwSGwoIaXNfdHJhc2gYBSABKAhIA1IHaXNUcmFzaEINCgtvbmVfb2ZfbmFtZUINCgtvbmVfb2ZfZGVzY0ISChBvbmVfb2ZfdGh1bWJuYWlsQhEKD29uZV9vZl9pc190cmFzaA==');
|
final $typed_data.Uint8List updateViewParamsDescriptor = $convert.base64Decode('ChBVcGRhdGVWaWV3UGFyYW1zEhcKB3ZpZXdfaWQYASABKAlSBnZpZXdJZBIUCgRuYW1lGAIgASgJSABSBG5hbWUSFAoEZGVzYxgDIAEoCUgBUgRkZXNjEh4KCXRodW1ibmFpbBgEIAEoCUgCUgl0aHVtYm5haWwSGwoIaXNfdHJhc2gYBSABKAhIA1IHaXNUcmFzaEINCgtvbmVfb2ZfbmFtZUINCgtvbmVfb2ZfZGVzY0ISChBvbmVfb2ZfdGh1bWJuYWlsQhEKD29uZV9vZl9pc190cmFzaA==');
|
||||||
@$core.Deprecated('Use docDeltaRequestDescriptor instead')
|
|
||||||
const DocDeltaRequest$json = const {
|
|
||||||
'1': 'DocDeltaRequest',
|
|
||||||
'2': const [
|
|
||||||
const {'1': 'view_id', '3': 1, '4': 1, '5': 9, '10': 'viewId'},
|
|
||||||
const {'1': 'data', '3': 2, '4': 1, '5': 12, '10': 'data'},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Descriptor for `DocDeltaRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
|
||||||
final $typed_data.Uint8List docDeltaRequestDescriptor = $convert.base64Decode('Cg9Eb2NEZWx0YVJlcXVlc3QSFwoHdmlld19pZBgBIAEoCVIGdmlld0lkEhIKBGRhdGEYAiABKAxSBGRhdGE=');
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use crate::service::{
|
use crate::service::{
|
||||||
doc::update_doc,
|
doc::update_doc,
|
||||||
util::md5,
|
util::md5,
|
||||||
ws::{entities::Socket, WsMessageAdaptor},
|
ws::{entities::Socket, WsClientData, WsMessageAdaptor, WsUser},
|
||||||
};
|
};
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use byteorder::{BigEndian, WriteBytesExt};
|
use byteorder::{BigEndian, WriteBytesExt};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
use dashmap::DashMap;
|
||||||
use flowy_document::{
|
use flowy_document::{
|
||||||
entities::ws::{WsDataType, WsDocumentData},
|
entities::ws::{WsDataType, WsDocumentData},
|
||||||
protobuf::{Doc, RevType, Revision, UpdateDocParams},
|
protobuf::{Doc, RevType, Revision, UpdateDocParams},
|
||||||
@ -20,48 +21,93 @@ use flowy_ws::WsMessage;
|
|||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use protobuf::Message;
|
use protobuf::Message;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::{convert::TryInto, sync::Arc, time::Duration};
|
use std::{
|
||||||
|
convert::TryInto,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicI64, Ordering::SeqCst},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EditUser {
|
||||||
|
user: Arc<WsUser>,
|
||||||
|
socket: Socket,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct EditDocContext {
|
pub(crate) struct EditDocContext {
|
||||||
doc_id: String,
|
doc_id: String,
|
||||||
rev_id: i64,
|
rev_id: AtomicI64,
|
||||||
document: Arc<RwLock<Document>>,
|
document: Arc<RwLock<Document>>,
|
||||||
pg_pool: Data<PgPool>,
|
pg_pool: Data<PgPool>,
|
||||||
|
users: DashMap<String, EditUser>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditDocContext {
|
impl EditDocContext {
|
||||||
pub(crate) fn new(doc: Doc, pg_pool: Data<PgPool>) -> Result<Self, ServerError> {
|
pub(crate) fn new(doc: Doc, pg_pool: Data<PgPool>) -> Result<Self, ServerError> {
|
||||||
let delta = Delta::from_bytes(&doc.data).map_err(internal_error)?;
|
let delta = Delta::from_bytes(&doc.data).map_err(internal_error)?;
|
||||||
let document = Arc::new(RwLock::new(Document::from_delta(delta)));
|
let document = Arc::new(RwLock::new(Document::from_delta(delta)));
|
||||||
|
let users = DashMap::new();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
doc_id: doc.id.clone(),
|
doc_id: doc.id.clone(),
|
||||||
rev_id: doc.rev_id,
|
rev_id: AtomicI64::new(doc.rev_id),
|
||||||
document,
|
document,
|
||||||
pg_pool,
|
pg_pool,
|
||||||
|
users,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(self, socket, revision))]
|
#[tracing::instrument(level = "debug", skip(self, client_data, revision))]
|
||||||
pub(crate) async fn apply_revision(&self, socket: Socket, revision: Revision) -> Result<(), ServerError> {
|
pub(crate) async fn apply_revision(
|
||||||
|
&self,
|
||||||
|
client_data: WsClientData,
|
||||||
|
revision: Revision,
|
||||||
|
) -> Result<(), ServerError> {
|
||||||
let _ = self.verify_md5(&revision)?;
|
let _ = self.verify_md5(&revision)?;
|
||||||
|
// Rest EditUser for each client websocket message to keep the socket available.
|
||||||
|
let user = EditUser {
|
||||||
|
user: client_data.user.clone(),
|
||||||
|
socket: client_data.socket.clone(),
|
||||||
|
};
|
||||||
|
self.users.insert(client_data.user.id().to_owned(), user);
|
||||||
|
|
||||||
if self.rev_id > revision.rev_id {
|
log::debug!(
|
||||||
let (cli_prime, server_prime) = self.compose(&revision.delta).map_err(internal_error)?;
|
"cur_base_rev_id: {}, expect_base_rev_id: {} rev_id: {}",
|
||||||
|
self.rev_id.load(SeqCst),
|
||||||
|
revision.base_rev_id,
|
||||||
|
revision.rev_id
|
||||||
|
);
|
||||||
|
|
||||||
|
let cli_socket = client_data.socket;
|
||||||
|
let cur_rev_id = self.rev_id.load(SeqCst);
|
||||||
|
// Transform the revision if client rev_id less than server rev_id. Sending the
|
||||||
|
// prime delta to client.
|
||||||
|
if cur_rev_id > revision.rev_id {
|
||||||
|
let (cli_prime, server_prime) = self.transform(&revision.delta).map_err(internal_error)?;
|
||||||
let _ = self.update_document_delta(server_prime)?;
|
let _ = self.update_document_delta(server_prime)?;
|
||||||
|
|
||||||
log::debug!("{} client delta: {}", self.doc_id, cli_prime.to_json());
|
log::debug!("{} client delta: {}", self.doc_id, cli_prime.to_json());
|
||||||
let cli_revision = self.mk_revision(revision.rev_id, cli_prime);
|
let cli_revision = self.mk_revision(revision.rev_id, cli_prime);
|
||||||
let ws_cli_revision = mk_rev_ws_message(&self.doc_id, cli_revision);
|
let ws_cli_revision = mk_rev_ws_message(&self.doc_id, cli_revision);
|
||||||
socket.do_send(ws_cli_revision).map_err(internal_error)?;
|
cli_socket.do_send(ws_cli_revision).map_err(internal_error)?;
|
||||||
|
Ok(())
|
||||||
|
} else if cur_rev_id < revision.rev_id {
|
||||||
|
if cur_rev_id != revision.base_rev_id {
|
||||||
|
let missing_rev_range = revision.rev_id - cur_rev_id;
|
||||||
|
// TODO: pull the missing revs from client
|
||||||
|
} else {
|
||||||
|
let delta = Delta::from_bytes(&revision.delta).map_err(internal_error)?;
|
||||||
|
let _ = self.update_document_delta(delta)?;
|
||||||
|
cli_socket.do_send(mk_acked_ws_message(&revision));
|
||||||
|
self.rev_id.fetch_update(SeqCst, SeqCst, |_e| Some(revision.rev_id));
|
||||||
|
|
||||||
|
// Opti: save with multiple revisions
|
||||||
|
let _ = self.save_revision(&revision).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
let delta = Delta::from_bytes(&revision.delta).map_err(internal_error)?;
|
log::error!("Client rev_id should not equal to server rev_id");
|
||||||
let _ = self.update_document_delta(delta)?;
|
|
||||||
socket.do_send(mk_acked_ws_message(&revision));
|
|
||||||
|
|
||||||
// Opti: save with multiple revisions
|
|
||||||
let _ = self.save_revision(&revision).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +116,7 @@ impl EditDocContext {
|
|||||||
let md5 = md5(&delta_data);
|
let md5 = md5(&delta_data);
|
||||||
let revision = Revision {
|
let revision = Revision {
|
||||||
base_rev_id,
|
base_rev_id,
|
||||||
rev_id: self.rev_id,
|
rev_id: self.rev_id.load(SeqCst),
|
||||||
delta: delta_data,
|
delta: delta_data,
|
||||||
md5,
|
md5,
|
||||||
doc_id: self.doc_id.to_string(),
|
doc_id: self.doc_id.to_string(),
|
||||||
@ -81,10 +127,12 @@ impl EditDocContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(self, delta_data))]
|
#[tracing::instrument(level = "debug", skip(self, delta_data))]
|
||||||
fn compose(&self, delta_data: &Vec<u8>) -> Result<(Delta, Delta), OTError> {
|
fn transform(&self, delta_data: &Vec<u8>) -> Result<(Delta, Delta), OTError> {
|
||||||
log::debug!("{} document data: {}", self.doc_id, self.document.read().to_json());
|
log::debug!("Document: {}", self.document.read().to_json());
|
||||||
let doc_delta = self.document.read().delta().clone();
|
let doc_delta = self.document.read().delta().clone();
|
||||||
let cli_delta = Delta::from_bytes(delta_data)?;
|
let cli_delta = Delta::from_bytes(delta_data)?;
|
||||||
|
|
||||||
|
log::debug!("Compose delta: {}", cli_delta);
|
||||||
let (cli_prime, server_prime) = doc_delta.transform(&cli_delta)?;
|
let (cli_prime, server_prime) = doc_delta.transform(&cli_delta)?;
|
||||||
|
|
||||||
Ok((cli_prime, server_prime))
|
Ok((cli_prime, server_prime))
|
||||||
@ -99,8 +147,7 @@ impl EditDocContext {
|
|||||||
},
|
},
|
||||||
Some(mut write_guard) => {
|
Some(mut write_guard) => {
|
||||||
let _ = write_guard.compose_delta(&delta).map_err(internal_error)?;
|
let _ = write_guard.compose_delta(&delta).map_err(internal_error)?;
|
||||||
|
log::debug!("Document: {}", write_guard.to_json());
|
||||||
log::debug!("Document: {}", write_guard.to_plain_string());
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -6,6 +6,7 @@ use crate::service::{
|
|||||||
};
|
};
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
|
|
||||||
|
use crate::service::ws::WsUser;
|
||||||
use flowy_document::protobuf::{QueryDocParams, Revision, WsDataType, WsDocumentData};
|
use flowy_document::protobuf::{QueryDocParams, Revision, WsDataType, WsDocumentData};
|
||||||
use flowy_net::errors::ServerError;
|
use flowy_net::errors::ServerError;
|
||||||
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
|
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
|
||||||
@ -53,22 +54,19 @@ impl EditDocManager {
|
|||||||
|
|
||||||
async fn handle(&self, client_data: WsClientData) -> Result<(), ServerError> {
|
async fn handle(&self, client_data: WsClientData) -> Result<(), ServerError> {
|
||||||
let document_data: WsDocumentData = parse_from_bytes(&client_data.data)?;
|
let document_data: WsDocumentData = parse_from_bytes(&client_data.data)?;
|
||||||
|
|
||||||
match document_data.ty {
|
match document_data.ty {
|
||||||
WsDataType::Acked => {},
|
WsDataType::Acked => {},
|
||||||
WsDataType::Rev => {
|
WsDataType::Rev => {
|
||||||
let revision: Revision = parse_from_bytes(&document_data.data)?;
|
let revision: Revision = parse_from_bytes(&document_data.data)?;
|
||||||
let edited_doc = self.get_edit_doc(&revision.doc_id).await?;
|
let edited_doc = self.get_edit_doc(&revision.doc_id).await?;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
match edited_doc
|
match edited_doc.apply_revision(client_data, revision).await {
|
||||||
.apply_revision(client_data.socket, revision)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => {},
|
Ok(_) => {},
|
||||||
Err(e) => log::error!("Doc apply revision failed: {:?}", e),
|
Err(e) => log::error!("Doc apply revision failed: {:?}", e),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
_ => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::service::ws::{WsBizHandlers, WsClient, WsServer};
|
use crate::service::ws::{WsBizHandlers, WsClient, WsServer, WsUser};
|
||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
|
|
||||||
use crate::service::user::LoggedUser;
|
use crate::service::user::LoggedUser;
|
||||||
@ -21,7 +21,8 @@ pub async fn establish_ws_connection(
|
|||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
match LoggedUser::from_token(token.clone()) {
|
match LoggedUser::from_token(token.clone()) {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let client = WsClient::new(&user.user_id, server.get_ref().clone(), biz_handlers);
|
let ws_user = WsUser::new(user.clone());
|
||||||
|
let client = WsClient::new(ws_user, server.get_ref().clone(), biz_handlers);
|
||||||
let result = ws::start(client, &request, payload);
|
let result = ws::start(client, &request, payload);
|
||||||
match result {
|
match result {
|
||||||
Ok(response) => Ok(response.into()),
|
Ok(response) => Ok(response.into()),
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::{HEARTBEAT_INTERVAL, PING_TIMEOUT},
|
config::{HEARTBEAT_INTERVAL, PING_TIMEOUT},
|
||||||
service::ws::{
|
service::{
|
||||||
entities::{Connect, Disconnect, SessionId, Socket},
|
user::LoggedUser,
|
||||||
WsBizHandler,
|
ws::{
|
||||||
WsBizHandlers,
|
entities::{Connect, Disconnect, SessionId, Socket},
|
||||||
WsMessageAdaptor,
|
WsBizHandler,
|
||||||
WsServer,
|
WsBizHandlers,
|
||||||
|
WsMessageAdaptor,
|
||||||
|
WsServer,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use actix::*;
|
use actix::*;
|
||||||
@ -13,28 +16,35 @@ use actix_web::web::Data;
|
|||||||
use actix_web_actors::{ws, ws::Message::Text};
|
use actix_web_actors::{ws, ws::Message::Text};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flowy_ws::WsMessage;
|
use flowy_ws::WsMessage;
|
||||||
use std::{convert::TryFrom, time::Instant};
|
use std::{convert::TryFrom, sync::Arc, time::Instant};
|
||||||
|
|
||||||
|
pub struct WsUser {
|
||||||
|
inner: LoggedUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsUser {
|
||||||
|
pub fn new(inner: LoggedUser) -> Self { Self { inner } }
|
||||||
|
|
||||||
|
pub fn id(&self) -> &str { &self.inner.user_id }
|
||||||
|
}
|
||||||
|
|
||||||
pub struct WsClientData {
|
pub struct WsClientData {
|
||||||
|
pub(crate) user: Arc<WsUser>,
|
||||||
pub(crate) socket: Socket,
|
pub(crate) socket: Socket,
|
||||||
pub(crate) data: Bytes,
|
pub(crate) data: Bytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WsClient {
|
pub struct WsClient {
|
||||||
session_id: SessionId,
|
user: Arc<WsUser>,
|
||||||
server: Addr<WsServer>,
|
server: Addr<WsServer>,
|
||||||
biz_handlers: Data<WsBizHandlers>,
|
biz_handlers: Data<WsBizHandlers>,
|
||||||
hb: Instant,
|
hb: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WsClient {
|
impl WsClient {
|
||||||
pub fn new<T: Into<SessionId>>(
|
pub fn new(user: WsUser, server: Addr<WsServer>, biz_handlers: Data<WsBizHandlers>) -> Self {
|
||||||
session_id: T,
|
|
||||||
server: Addr<WsServer>,
|
|
||||||
biz_handlers: Data<WsBizHandlers>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
session_id: session_id.into(),
|
user: Arc::new(user),
|
||||||
server,
|
server,
|
||||||
biz_handlers,
|
biz_handlers,
|
||||||
hb: Instant::now(),
|
hb: Instant::now(),
|
||||||
@ -45,7 +55,7 @@ impl WsClient {
|
|||||||
ctx.run_interval(HEARTBEAT_INTERVAL, |client, ctx| {
|
ctx.run_interval(HEARTBEAT_INTERVAL, |client, ctx| {
|
||||||
if Instant::now().duration_since(client.hb) > PING_TIMEOUT {
|
if Instant::now().duration_since(client.hb) > PING_TIMEOUT {
|
||||||
client.server.do_send(Disconnect {
|
client.server.do_send(Disconnect {
|
||||||
sid: client.session_id.clone(),
|
sid: client.user.id().into(),
|
||||||
});
|
});
|
||||||
ctx.stop();
|
ctx.stop();
|
||||||
} else {
|
} else {
|
||||||
@ -63,6 +73,7 @@ impl WsClient {
|
|||||||
},
|
},
|
||||||
Some(handler) => {
|
Some(handler) => {
|
||||||
let client_data = WsClientData {
|
let client_data = WsClientData {
|
||||||
|
user: self.user.clone(),
|
||||||
socket,
|
socket,
|
||||||
data: Bytes::from(message.data),
|
data: Bytes::from(message.data),
|
||||||
};
|
};
|
||||||
@ -84,7 +95,6 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsClient {
|
|||||||
self.hb = Instant::now();
|
self.hb = Instant::now();
|
||||||
},
|
},
|
||||||
Ok(ws::Message::Binary(bytes)) => {
|
Ok(ws::Message::Binary(bytes)) => {
|
||||||
log::debug!(" Receive {} binary", &self.session_id);
|
|
||||||
let socket = ctx.address().recipient();
|
let socket = ctx.address().recipient();
|
||||||
self.handle_binary_message(bytes, socket);
|
self.handle_binary_message(bytes, socket);
|
||||||
},
|
},
|
||||||
@ -98,11 +108,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsClient {
|
|||||||
Ok(ws::Message::Continuation(_)) => {},
|
Ok(ws::Message::Continuation(_)) => {},
|
||||||
Ok(ws::Message::Nop) => {},
|
Ok(ws::Message::Nop) => {},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(
|
log::error!("[{}]: WebSocketStream protocol error {:?}", self.user.id(), e);
|
||||||
"[{}]: WebSocketStream protocol error {:?}",
|
|
||||||
self.session_id,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
ctx.stop();
|
ctx.stop();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -123,7 +129,7 @@ impl Actor for WsClient {
|
|||||||
let socket = ctx.address().recipient();
|
let socket = ctx.address().recipient();
|
||||||
let connect = Connect {
|
let connect = Connect {
|
||||||
socket,
|
socket,
|
||||||
sid: self.session_id.clone(),
|
sid: self.user.id().into(),
|
||||||
};
|
};
|
||||||
self.server
|
self.server
|
||||||
.send(connect)
|
.send(connect)
|
||||||
@ -141,7 +147,7 @@ impl Actor for WsClient {
|
|||||||
|
|
||||||
fn stopping(&mut self, _: &mut Self::Context) -> Running {
|
fn stopping(&mut self, _: &mut Self::Context) -> Running {
|
||||||
self.server.do_send(Disconnect {
|
self.server.do_send(Disconnect {
|
||||||
sid: self.session_id.clone(),
|
sid: self.user.id().into(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Running::Stop
|
Running::Stop
|
||||||
|
@ -41,7 +41,6 @@ pub fn category_from_str(type_str: &str) -> TypeCategory {
|
|||||||
| "CurrentWorkspace"
|
| "CurrentWorkspace"
|
||||||
| "UpdateViewRequest"
|
| "UpdateViewRequest"
|
||||||
| "UpdateViewParams"
|
| "UpdateViewParams"
|
||||||
| "DocDeltaRequest"
|
|
||||||
| "DeleteViewRequest"
|
| "DeleteViewRequest"
|
||||||
| "DeleteViewParams"
|
| "DeleteViewParams"
|
||||||
| "QueryViewRequest"
|
| "QueryViewRequest"
|
||||||
|
@ -6,8 +6,9 @@ use std::convert::TryInto;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, ProtoBuf_Enum, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, ProtoBuf_Enum, Eq, PartialEq, Hash)]
|
||||||
pub enum WsDataType {
|
pub enum WsDataType {
|
||||||
Acked = 0,
|
Acked = 0,
|
||||||
Rev = 1,
|
Rev = 1,
|
||||||
|
Conflict = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::default::Default for WsDataType {
|
impl std::default::Default for WsDataType {
|
||||||
|
@ -259,6 +259,7 @@ impl ::protobuf::reflect::ProtobufValue for WsDocumentData {
|
|||||||
pub enum WsDataType {
|
pub enum WsDataType {
|
||||||
Acked = 0,
|
Acked = 0,
|
||||||
Rev = 1,
|
Rev = 1,
|
||||||
|
Conflict = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ::protobuf::ProtobufEnum for WsDataType {
|
impl ::protobuf::ProtobufEnum for WsDataType {
|
||||||
@ -270,6 +271,7 @@ impl ::protobuf::ProtobufEnum for WsDataType {
|
|||||||
match value {
|
match value {
|
||||||
0 => ::std::option::Option::Some(WsDataType::Acked),
|
0 => ::std::option::Option::Some(WsDataType::Acked),
|
||||||
1 => ::std::option::Option::Some(WsDataType::Rev),
|
1 => ::std::option::Option::Some(WsDataType::Rev),
|
||||||
|
2 => ::std::option::Option::Some(WsDataType::Conflict),
|
||||||
_ => ::std::option::Option::None
|
_ => ::std::option::Option::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -278,6 +280,7 @@ impl ::protobuf::ProtobufEnum for WsDataType {
|
|||||||
static values: &'static [WsDataType] = &[
|
static values: &'static [WsDataType] = &[
|
||||||
WsDataType::Acked,
|
WsDataType::Acked,
|
||||||
WsDataType::Rev,
|
WsDataType::Rev,
|
||||||
|
WsDataType::Conflict,
|
||||||
];
|
];
|
||||||
values
|
values
|
||||||
}
|
}
|
||||||
@ -308,23 +311,26 @@ impl ::protobuf::reflect::ProtobufValue for WsDataType {
|
|||||||
static file_descriptor_proto_data: &'static [u8] = b"\
|
static file_descriptor_proto_data: &'static [u8] = b"\
|
||||||
\n\x08ws.proto\"Q\n\x0eWsDocumentData\x12\x0e\n\x02id\x18\x01\x20\x01(\t\
|
\n\x08ws.proto\"Q\n\x0eWsDocumentData\x12\x0e\n\x02id\x18\x01\x20\x01(\t\
|
||||||
R\x02id\x12\x1b\n\x02ty\x18\x02\x20\x01(\x0e2\x0b.WsDataTypeR\x02ty\x12\
|
R\x02id\x12\x1b\n\x02ty\x18\x02\x20\x01(\x0e2\x0b.WsDataTypeR\x02ty\x12\
|
||||||
\x12\n\x04data\x18\x03\x20\x01(\x0cR\x04data*\x20\n\nWsDataType\x12\t\n\
|
\x12\n\x04data\x18\x03\x20\x01(\x0cR\x04data*.\n\nWsDataType\x12\t\n\x05\
|
||||||
\x05Acked\x10\0\x12\x07\n\x03Rev\x10\x01J\xb9\x02\n\x06\x12\x04\0\0\n\
|
Acked\x10\0\x12\x07\n\x03Rev\x10\x01\x12\x0c\n\x08Conflict\x10\x02J\xe2\
|
||||||
\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x06\
|
\x02\n\x06\x12\x04\0\0\x0b\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\
|
||||||
\x01\n\n\n\x03\x04\0\x01\x12\x03\x02\x08\x16\n\x0b\n\x04\x04\0\x02\0\x12\
|
\x04\0\x12\x04\x02\0\x06\x01\n\n\n\x03\x04\0\x01\x12\x03\x02\x08\x16\n\
|
||||||
\x03\x03\x04\x12\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\n\
|
\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x12\n\x0c\n\x05\x04\0\x02\0\x05\
|
||||||
\x05\x04\0\x02\0\x01\x12\x03\x03\x0b\r\n\x0c\n\x05\x04\0\x02\0\x03\x12\
|
\x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x03\x0b\r\n\x0c\n\
|
||||||
\x03\x03\x10\x11\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x04\x04\x16\n\x0c\n\
|
\x05\x04\0\x02\0\x03\x12\x03\x03\x10\x11\n\x0b\n\x04\x04\0\x02\x01\x12\
|
||||||
\x05\x04\0\x02\x01\x06\x12\x03\x04\x04\x0e\n\x0c\n\x05\x04\0\x02\x01\x01\
|
\x03\x04\x04\x16\n\x0c\n\x05\x04\0\x02\x01\x06\x12\x03\x04\x04\x0e\n\x0c\
|
||||||
\x12\x03\x04\x0f\x11\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x04\x14\x15\n\
|
\n\x05\x04\0\x02\x01\x01\x12\x03\x04\x0f\x11\n\x0c\n\x05\x04\0\x02\x01\
|
||||||
\x0b\n\x04\x04\0\x02\x02\x12\x03\x05\x04\x13\n\x0c\n\x05\x04\0\x02\x02\
|
\x03\x12\x03\x04\x14\x15\n\x0b\n\x04\x04\0\x02\x02\x12\x03\x05\x04\x13\n\
|
||||||
\x05\x12\x03\x05\x04\t\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x03\x05\n\x0e\n\
|
\x0c\n\x05\x04\0\x02\x02\x05\x12\x03\x05\x04\t\n\x0c\n\x05\x04\0\x02\x02\
|
||||||
\x0c\n\x05\x04\0\x02\x02\x03\x12\x03\x05\x11\x12\n\n\n\x02\x05\0\x12\x04\
|
\x01\x12\x03\x05\n\x0e\n\x0c\n\x05\x04\0\x02\x02\x03\x12\x03\x05\x11\x12\
|
||||||
\x07\0\n\x01\n\n\n\x03\x05\0\x01\x12\x03\x07\x05\x0f\n\x0b\n\x04\x05\0\
|
\n\n\n\x02\x05\0\x12\x04\x07\0\x0b\x01\n\n\n\x03\x05\0\x01\x12\x03\x07\
|
||||||
\x02\0\x12\x03\x08\x04\x0e\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x08\x04\t\
|
\x05\x0f\n\x0b\n\x04\x05\0\x02\0\x12\x03\x08\x04\x0e\n\x0c\n\x05\x05\0\
|
||||||
\n\x0c\n\x05\x05\0\x02\0\x02\x12\x03\x08\x0c\r\n\x0b\n\x04\x05\0\x02\x01\
|
\x02\0\x01\x12\x03\x08\x04\t\n\x0c\n\x05\x05\0\x02\0\x02\x12\x03\x08\x0c\
|
||||||
\x12\x03\t\x04\x0c\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\t\x04\x07\n\x0c\
|
\r\n\x0b\n\x04\x05\0\x02\x01\x12\x03\t\x04\x0c\n\x0c\n\x05\x05\0\x02\x01\
|
||||||
\n\x05\x05\0\x02\x01\x02\x12\x03\t\n\x0bb\x06proto3\
|
\x01\x12\x03\t\x04\x07\n\x0c\n\x05\x05\0\x02\x01\x02\x12\x03\t\n\x0b\n\
|
||||||
|
\x0b\n\x04\x05\0\x02\x02\x12\x03\n\x04\x11\n\x0c\n\x05\x05\0\x02\x02\x01\
|
||||||
|
\x12\x03\n\x04\x0c\n\x0c\n\x05\x05\0\x02\x02\x02\x12\x03\n\x0f\x10b\x06p\
|
||||||
|
roto3\
|
||||||
";
|
";
|
||||||
|
|
||||||
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
|
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
|
||||||
|
@ -8,4 +8,5 @@ message WsDocumentData {
|
|||||||
enum WsDataType {
|
enum WsDataType {
|
||||||
Acked = 0;
|
Acked = 0;
|
||||||
Rev = 1;
|
Rev = 1;
|
||||||
|
Conflict = 2;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ impl DocCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set(&self, doc: Arc<EditDocContext>) {
|
pub(crate) fn set(&self, doc: Arc<EditDocContext>) {
|
||||||
let doc_id = doc.id.clone();
|
let doc_id = doc.doc_id.clone();
|
||||||
if self.inner.contains_key(&doc_id) {
|
if self.inner.contains_key(&doc_id) {
|
||||||
log::warn!("Doc:{} already exists in cache", &doc_id);
|
log::warn!("Doc:{} already exists in cache", &doc_id);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
entities::doc::{CreateDocParams, Doc, DocDelta, QueryDocParams, UpdateDocParams},
|
entities::doc::{CreateDocParams, Doc, DocDelta, QueryDocParams},
|
||||||
errors::{internal_error, DocError},
|
errors::{internal_error, DocError},
|
||||||
module::DocumentUser,
|
module::DocumentUser,
|
||||||
services::{
|
services::{
|
||||||
@ -21,7 +21,6 @@ use tokio::time::{interval, Duration};
|
|||||||
pub(crate) struct DocController {
|
pub(crate) struct DocController {
|
||||||
server: Server,
|
server: Server,
|
||||||
doc_sql: Arc<DocTableSql>,
|
doc_sql: Arc<DocTableSql>,
|
||||||
op_sql: Arc<OpTableSql>,
|
|
||||||
ws: Arc<RwLock<WsDocumentManager>>,
|
ws: Arc<RwLock<WsDocumentManager>>,
|
||||||
cache: Arc<DocCache>,
|
cache: Arc<DocCache>,
|
||||||
user: Arc<dyn DocumentUser>,
|
user: Arc<dyn DocumentUser>,
|
||||||
@ -30,24 +29,14 @@ pub(crate) struct DocController {
|
|||||||
impl DocController {
|
impl DocController {
|
||||||
pub(crate) fn new(server: Server, user: Arc<dyn DocumentUser>, ws: Arc<RwLock<WsDocumentManager>>) -> Self {
|
pub(crate) fn new(server: Server, user: Arc<dyn DocumentUser>, ws: Arc<RwLock<WsDocumentManager>>) -> Self {
|
||||||
let doc_sql = Arc::new(DocTableSql {});
|
let doc_sql = Arc::new(DocTableSql {});
|
||||||
let op_sql = Arc::new(OpTableSql {});
|
|
||||||
let cache = Arc::new(DocCache::new());
|
let cache = Arc::new(DocCache::new());
|
||||||
|
|
||||||
let controller = Self {
|
let controller = Self {
|
||||||
server,
|
server,
|
||||||
doc_sql,
|
doc_sql,
|
||||||
op_sql,
|
|
||||||
user,
|
user,
|
||||||
ws,
|
ws,
|
||||||
cache: cache.clone(),
|
cache: cache.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// tokio::spawn(async move {
|
|
||||||
// tokio::select! {
|
|
||||||
// _ = event_loop(cache.clone()) => {},
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
controller
|
controller
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,10 +58,8 @@ impl DocController {
|
|||||||
pool: Arc<ConnectionPool>,
|
pool: Arc<ConnectionPool>,
|
||||||
) -> Result<Arc<EditDocContext>, DocError> {
|
) -> Result<Arc<EditDocContext>, DocError> {
|
||||||
if self.cache.is_opened(¶ms.doc_id) == false {
|
if self.cache.is_opened(¶ms.doc_id) == false {
|
||||||
return match self._open(params, pool).await {
|
let edit_ctx = self.make_edit_context(¶ms.doc_id, pool.clone()).await?;
|
||||||
Ok(doc) => Ok(doc),
|
return Ok(edit_ctx);
|
||||||
Err(error) => Err(error),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let edit_doc_ctx = self.cache.get(¶ms.doc_id)?;
|
let edit_doc_ctx = self.cache.get(¶ms.doc_id)?;
|
||||||
@ -105,40 +92,6 @@ impl DocController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DocController {
|
impl DocController {
|
||||||
#[tracing::instrument(level = "debug", skip(self, params), err)]
|
|
||||||
fn update_doc_on_server(&self, params: UpdateDocParams) -> Result<(), DocError> {
|
|
||||||
let token = self.user.token()?;
|
|
||||||
let server = self.server.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
match server.update_doc(&token, params).await {
|
|
||||||
Ok(_) => {},
|
|
||||||
Err(e) => {
|
|
||||||
// TODO: retry?
|
|
||||||
log::error!("Update doc failed: {}", e);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(self, pool), err)]
|
|
||||||
async fn read_doc_from_server(
|
|
||||||
&self,
|
|
||||||
params: QueryDocParams,
|
|
||||||
pool: Arc<ConnectionPool>,
|
|
||||||
) -> Result<Arc<EditDocContext>, DocError> {
|
|
||||||
let token = self.user.token()?;
|
|
||||||
match self.server.read_doc(&token, params).await? {
|
|
||||||
None => Err(DocError::not_found()),
|
|
||||||
Some(doc) => {
|
|
||||||
let edit = self.make_edit_context(doc.clone(), pool.clone())?;
|
|
||||||
let conn = &*(pool.get().map_err(internal_error)?);
|
|
||||||
let _ = self.doc_sql.create_doc_table(doc.into(), conn)?;
|
|
||||||
Ok(edit)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||||
fn delete_doc_on_server(&self, params: QueryDocParams) -> Result<(), DocError> {
|
fn delete_doc_on_server(&self, params: QueryDocParams) -> Result<(), DocError> {
|
||||||
let token = self.user.token()?;
|
let token = self.user.token()?;
|
||||||
@ -155,32 +108,45 @@ impl DocController {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _open(&self, params: QueryDocParams, pool: Arc<ConnectionPool>) -> Result<Arc<EditDocContext>, DocError> {
|
async fn make_edit_context(
|
||||||
match self.doc_sql.read_doc_table(¶ms.doc_id, pool.clone()) {
|
&self,
|
||||||
Ok(doc_table) => Ok(self.make_edit_context(doc_table.into(), pool.clone())?),
|
doc_id: &str,
|
||||||
|
pool: Arc<ConnectionPool>,
|
||||||
|
) -> Result<Arc<EditDocContext>, DocError> {
|
||||||
|
// Opti: require upgradable_read lock and then upgrade to write lock using
|
||||||
|
// RwLockUpgradableReadGuard::upgrade(xx) of ws
|
||||||
|
let delta = self.read_doc(doc_id, pool.clone()).await?;
|
||||||
|
let ws_sender = self.ws.read().sender();
|
||||||
|
let edit_ctx = Arc::new(EditDocContext::new(&doc_id, delta, pool, ws_sender).await?);
|
||||||
|
self.ws.write().register_handler(&doc_id, edit_ctx.clone());
|
||||||
|
self.cache.set(edit_ctx.clone());
|
||||||
|
Ok(edit_ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip(self, pool), err)]
|
||||||
|
async fn read_doc(&self, doc_id: &str, pool: Arc<ConnectionPool>) -> Result<Delta, DocError> {
|
||||||
|
match self.doc_sql.read_doc_table(doc_id, pool.clone()) {
|
||||||
|
Ok(doc_table) => Ok(Delta::from_bytes(doc_table.data)?),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if error.is_record_not_found() {
|
if error.is_record_not_found() {
|
||||||
log::debug!("Doc:{} don't exist, reading from server", params.doc_id);
|
let token = self.user.token()?;
|
||||||
Ok(self.read_doc_from_server(params, pool.clone()).await?)
|
let params = QueryDocParams {
|
||||||
|
doc_id: doc_id.to_string(),
|
||||||
|
};
|
||||||
|
match self.server.read_doc(&token, params).await? {
|
||||||
|
None => Err(DocError::not_found()),
|
||||||
|
Some(doc) => {
|
||||||
|
let conn = &*pool.get().map_err(internal_error)?;
|
||||||
|
let _ = self.doc_sql.create_doc_table(doc.clone().into(), conn)?;
|
||||||
|
Ok(Delta::from_bytes(doc.data)?)
|
||||||
|
},
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_edit_context(&self, doc: Doc, pool: Arc<ConnectionPool>) -> Result<Arc<EditDocContext>, DocError> {
|
|
||||||
// Opti: require upgradable_read lock and then upgrade to write lock using
|
|
||||||
// RwLockUpgradableReadGuard::upgrade(xx) of ws
|
|
||||||
let doc_id = doc.id.clone();
|
|
||||||
let delta = Delta::from_bytes(&doc.data)?;
|
|
||||||
let ws_sender = self.ws.read().sender();
|
|
||||||
let rev_manager = RevisionManager::new(&doc_id, doc.rev_id, self.op_sql.clone(), pool, ws_sender);
|
|
||||||
let edit_ctx = Arc::new(EditDocContext::new(&doc_id, delta, rev_manager)?);
|
|
||||||
self.ws.write().register_handler(&doc_id, edit_ctx.clone());
|
|
||||||
self.cache.set(edit_ctx.clone());
|
|
||||||
Ok(edit_ctx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -6,13 +6,18 @@ use crate::{
|
|||||||
errors::*,
|
errors::*,
|
||||||
services::{
|
services::{
|
||||||
doc::{rev_manager::RevisionManager, Document},
|
doc::{rev_manager::RevisionManager, Document},
|
||||||
util::bytes_to_rev_id,
|
util::{bytes_to_rev_id, md5},
|
||||||
ws::WsDocumentHandler,
|
ws::WsDocumentHandler,
|
||||||
},
|
},
|
||||||
sql_tables::{OpTableSql, RevTable},
|
|
||||||
};
|
};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
entities::doc::RevType,
|
||||||
|
services::ws::WsDocumentSender,
|
||||||
|
sql_tables::{doc::DocTableSql, DocTableChangeset},
|
||||||
|
};
|
||||||
|
use flowy_database::ConnectionPool;
|
||||||
use flowy_ot::core::Delta;
|
use flowy_ot::core::Delta;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::{convert::TryFrom, sync::Arc};
|
use std::{convert::TryFrom, sync::Arc};
|
||||||
@ -20,27 +25,34 @@ use std::{convert::TryFrom, sync::Arc};
|
|||||||
pub type DocId = String;
|
pub type DocId = String;
|
||||||
|
|
||||||
pub(crate) struct EditDocContext {
|
pub(crate) struct EditDocContext {
|
||||||
pub(crate) id: DocId,
|
pub(crate) doc_id: DocId,
|
||||||
document: Arc<RwLock<Document>>,
|
document: Arc<RwLock<Document>>,
|
||||||
rev_manager: Arc<RevisionManager>,
|
rev_manager: Arc<RevisionManager>,
|
||||||
|
pool: Arc<ConnectionPool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditDocContext {
|
impl EditDocContext {
|
||||||
pub(crate) fn new(doc_id: &str, delta: Delta, rev_manager: RevisionManager) -> Result<Self, DocError> {
|
pub(crate) async fn new(
|
||||||
let id = doc_id.to_owned();
|
doc_id: &str,
|
||||||
let rev_manager = Arc::new(rev_manager);
|
delta: Delta,
|
||||||
|
pool: Arc<ConnectionPool>,
|
||||||
|
ws_sender: Arc<dyn WsDocumentSender>,
|
||||||
|
) -> Result<Self, DocError> {
|
||||||
|
let doc_id = doc_id.to_owned();
|
||||||
|
let rev_manager = Arc::new(RevisionManager::new(&doc_id, 1, pool.clone(), ws_sender));
|
||||||
let document = Arc::new(RwLock::new(Document::from_delta(delta)));
|
let document = Arc::new(RwLock::new(Document::from_delta(delta)));
|
||||||
let edit_context = Self {
|
let edit_context = Self {
|
||||||
id,
|
doc_id,
|
||||||
document,
|
document,
|
||||||
rev_manager,
|
rev_manager,
|
||||||
|
pool,
|
||||||
};
|
};
|
||||||
Ok(edit_context)
|
Ok(edit_context)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn doc(&self) -> Doc {
|
pub(crate) fn doc(&self) -> Doc {
|
||||||
Doc {
|
Doc {
|
||||||
id: self.id.clone(),
|
id: self.doc_id.clone(),
|
||||||
data: self.document.read().to_json(),
|
data: self.document.read().to_json(),
|
||||||
rev_id: self.rev_manager.rev_id(),
|
rev_id: self.rev_manager.rev_id(),
|
||||||
}
|
}
|
||||||
@ -48,12 +60,63 @@ impl EditDocContext {
|
|||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(self, data), err)]
|
#[tracing::instrument(level = "debug", skip(self, data), err)]
|
||||||
pub(crate) fn compose_local_delta(&self, data: Bytes) -> Result<(), DocError> {
|
pub(crate) fn compose_local_delta(&self, data: Bytes) -> Result<(), DocError> {
|
||||||
let delta = Delta::from_bytes(&data)?;
|
let (base_rev_id, rev_id) = self.rev_manager.next_rev_id();
|
||||||
self.document.write().compose_delta(&delta)?;
|
let revision = Revision::new(
|
||||||
self.rev_manager.add_delta(data);
|
base_rev_id,
|
||||||
|
rev_id,
|
||||||
|
data.to_vec(),
|
||||||
|
md5(&data),
|
||||||
|
self.doc_id.clone(),
|
||||||
|
RevType::Local,
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = self.update_document(&revision)?;
|
||||||
|
self.rev_manager.add_revision(revision);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip(self, revision), err)]
|
||||||
|
pub fn update_document(&self, revision: &Revision) -> Result<(), DocError> {
|
||||||
|
let delta = Delta::from_bytes(&revision.delta)?;
|
||||||
|
self.document.write().compose_delta(&delta)?;
|
||||||
|
let data = self.document.read().to_json();
|
||||||
|
let changeset = DocTableChangeset {
|
||||||
|
id: self.doc_id.clone(),
|
||||||
|
data,
|
||||||
|
revision: revision.rev_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let sql = DocTableSql {};
|
||||||
|
let conn = self.pool.get().map_err(internal_error)?;
|
||||||
|
sql.update_doc_table(changeset, &*conn);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||||
|
fn compose_remote_delta(&self) -> Result<(), DocError> {
|
||||||
|
self.rev_manager.next_compose_revision(|revision| {
|
||||||
|
let _ = self.update_document(revision)?;
|
||||||
|
log::debug!("😁Document: {:?}", self.document.read().to_plain_string());
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[tracing::instrument(level = "debug", skip(self, params), err)]
|
||||||
|
// fn update_doc_on_server(&self, params: UpdateDocParams) -> Result<(),
|
||||||
|
// DocError> { let token = self.user.token()?;
|
||||||
|
// let server = self.server.clone();
|
||||||
|
// tokio::spawn(async move {
|
||||||
|
// match server.update_doc(&token, params).await {
|
||||||
|
// Ok(_) => {},
|
||||||
|
// Err(e) => {
|
||||||
|
// // TODO: retry?
|
||||||
|
// log::error!("Update doc failed: {}", e);
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WsDocumentHandler for EditDocContext {
|
impl WsDocumentHandler for EditDocContext {
|
||||||
@ -64,16 +127,13 @@ impl WsDocumentHandler for EditDocContext {
|
|||||||
let bytes = Bytes::from(doc_data.data);
|
let bytes = Bytes::from(doc_data.data);
|
||||||
let revision = Revision::try_from(bytes)?;
|
let revision = Revision::try_from(bytes)?;
|
||||||
self.rev_manager.add_revision(revision);
|
self.rev_manager.add_revision(revision);
|
||||||
self.rev_manager.next_compose_delta(|delta| {
|
let _ = self.compose_remote_delta()?;
|
||||||
let _ = self.document.write().compose_delta(delta)?;
|
|
||||||
log::debug!("😁Document: {:?}", self.document.read().to_plain_string());
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
WsDataType::Acked => {
|
WsDataType::Acked => {
|
||||||
let rev_id = bytes_to_rev_id(doc_data.data)?;
|
let rev_id = bytes_to_rev_id(doc_data.data)?;
|
||||||
self.rev_manager.remove(rev_id);
|
self.rev_manager.remove(rev_id);
|
||||||
},
|
},
|
||||||
|
_ => {},
|
||||||
}
|
}
|
||||||
Result::<(), DocError>::Ok(())
|
Result::<(), DocError>::Ok(())
|
||||||
};
|
};
|
||||||
|
@ -2,14 +2,14 @@ use crate::{
|
|||||||
entities::doc::{RevType, Revision},
|
entities::doc::{RevType, Revision},
|
||||||
errors::{internal_error, DocError},
|
errors::{internal_error, DocError},
|
||||||
services::{
|
services::{
|
||||||
util::{md5, RevIdCounter},
|
util::RevIdCounter,
|
||||||
ws::{WsDocumentHandler, WsDocumentSender},
|
ws::{WsDocumentHandler, WsDocumentSender},
|
||||||
},
|
},
|
||||||
sql_tables::{OpTableSql, RevTable},
|
sql_tables::{OpTableSql, RevTable},
|
||||||
};
|
};
|
||||||
use bytes::Bytes;
|
|
||||||
use flowy_database::ConnectionPool;
|
use flowy_database::ConnectionPool;
|
||||||
use flowy_ot::core::Delta;
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, VecDeque},
|
collections::{BTreeMap, VecDeque},
|
||||||
@ -29,13 +29,8 @@ pub struct RevisionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RevisionManager {
|
impl RevisionManager {
|
||||||
pub fn new(
|
pub fn new(doc_id: &str, rev_id: i64, pool: Arc<ConnectionPool>, ws_sender: Arc<dyn WsDocumentSender>) -> Self {
|
||||||
doc_id: &str,
|
let op_sql = Arc::new(OpTableSql {});
|
||||||
rev_id: i64,
|
|
||||||
op_sql: Arc<OpTableSql>,
|
|
||||||
pool: Arc<ConnectionPool>,
|
|
||||||
ws_sender: Arc<dyn WsDocumentSender>,
|
|
||||||
) -> Self {
|
|
||||||
let rev_id_counter = RevIdCounter::new(rev_id);
|
let rev_id_counter = RevIdCounter::new(rev_id);
|
||||||
let local_rev_cache = Arc::new(RwLock::new(BTreeMap::new()));
|
let local_rev_cache = Arc::new(RwLock::new(BTreeMap::new()));
|
||||||
let remote_rev_cache = RwLock::new(VecDeque::new());
|
let remote_rev_cache = RwLock::new(VecDeque::new());
|
||||||
@ -51,39 +46,21 @@ impl RevisionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_compose_delta<F>(&self, mut f: F)
|
pub fn next_compose_revision<F>(&self, mut f: F)
|
||||||
where
|
where
|
||||||
F: FnMut(&Delta) -> Result<(), DocError>,
|
F: FnMut(&Revision) -> Result<(), DocError>,
|
||||||
{
|
{
|
||||||
if let Some(rev) = self.remote_rev_cache.write().pop_front() {
|
if let Some(rev) = self.remote_rev_cache.write().pop_front() {
|
||||||
match Delta::from_bytes(&rev.delta) {
|
match f(&rev) {
|
||||||
Ok(delta) => match f(&delta) {
|
Ok(_) => {},
|
||||||
Ok(_) => {},
|
Err(e) => {
|
||||||
Err(e) => {
|
log::error!("{}", e);
|
||||||
log::error!("{}", e);
|
self.remote_rev_cache.write().push_front(rev);
|
||||||
self.remote_rev_cache.write().push_front(rev);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Err(_) => {},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(self, delta_data))]
|
|
||||||
pub fn add_delta(&self, delta_data: Bytes) -> Result<(), DocError> {
|
|
||||||
let (base_rev_id, rev_id) = self.next_rev_id();
|
|
||||||
let revision = Revision::new(
|
|
||||||
base_rev_id,
|
|
||||||
rev_id,
|
|
||||||
delta_data.to_vec(),
|
|
||||||
md5(&delta_data),
|
|
||||||
self.doc_id.clone(),
|
|
||||||
RevType::Local,
|
|
||||||
);
|
|
||||||
let _ = self.add_revision(revision)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(self, revision))]
|
#[tracing::instrument(level = "debug", skip(self, revision))]
|
||||||
pub fn add_revision(&self, revision: Revision) -> Result<(), DocError> {
|
pub fn add_revision(&self, revision: Revision) -> Result<(), DocError> {
|
||||||
match revision.ty {
|
match revision.ty {
|
||||||
|
@ -18,21 +18,29 @@ impl DocTableSql {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_doc_table(&self, changeset: DocTableChangeset, conn: &SqliteConnection) -> Result<(), DocError> {
|
pub(crate) fn update_doc_table(
|
||||||
|
&self,
|
||||||
|
changeset: DocTableChangeset,
|
||||||
|
conn: &SqliteConnection,
|
||||||
|
) -> Result<(), DocError> {
|
||||||
diesel_update_table!(doc_table, changeset, conn);
|
diesel_update_table!(doc_table, changeset, conn);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read_doc_table(&self, doc_id: &str, pool: Arc<ConnectionPool>) -> Result<DocTable, DocError> {
|
pub(crate) fn read_doc_table(&self, doc_id: &str, pool: Arc<ConnectionPool>) -> Result<DocTable, DocError> {
|
||||||
let conn = &*pool.get().map_err(internal_error)?;
|
let conn = &*pool.get().map_err(internal_error)?;
|
||||||
let doc_table = dsl::doc_table.filter(doc_table::id.eq(doc_id)).first::<DocTable>(conn)?;
|
let doc_table = dsl::doc_table
|
||||||
|
.filter(doc_table::id.eq(doc_id))
|
||||||
|
.first::<DocTable>(conn)?;
|
||||||
|
|
||||||
Ok(doc_table)
|
Ok(doc_table)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn delete_doc(&self, doc_id: &str, conn: &SqliteConnection) -> Result<DocTable, DocError> {
|
pub(crate) fn delete_doc(&self, doc_id: &str, conn: &SqliteConnection) -> Result<DocTable, DocError> {
|
||||||
let doc_table = dsl::doc_table.filter(doc_table::id.eq(doc_id)).first::<DocTable>(conn)?;
|
let doc_table = dsl::doc_table
|
||||||
|
.filter(doc_table::id.eq(doc_id))
|
||||||
|
.first::<DocTable>(conn)?;
|
||||||
diesel_delete_table!(doc_table, doc_id, conn);
|
diesel_delete_table!(doc_table, doc_id, conn);
|
||||||
Ok(doc_table)
|
Ok(doc_table)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::entities::doc::{Doc, UpdateDocParams};
|
use crate::entities::doc::Doc;
|
||||||
use flowy_database::schema::doc_table;
|
use flowy_database::schema::doc_table;
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)]
|
#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)]
|
||||||
@ -24,15 +24,7 @@ impl DocTable {
|
|||||||
pub(crate) struct DocTableChangeset {
|
pub(crate) struct DocTableChangeset {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub data: String,
|
pub data: String,
|
||||||
}
|
pub revision: i64,
|
||||||
|
|
||||||
impl DocTableChangeset {
|
|
||||||
pub(crate) fn new(params: UpdateDocParams) -> Self {
|
|
||||||
Self {
|
|
||||||
id: params.doc_id,
|
|
||||||
data: params.data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::Into<Doc> for DocTable {
|
impl std::convert::Into<Doc> for DocTable {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::editor::{Rng, TestBuilder, TestOp::*};
|
use crate::editor::{Rng, TestBuilder, TestOp::*};
|
||||||
use bytecount::num_chars;
|
use bytecount::num_chars;
|
||||||
use flowy_document::services::doc::PlainDoc;
|
use flowy_document::services::doc::{FlowyDoc, PlainDoc};
|
||||||
use flowy_ot::core::*;
|
use flowy_ot::core::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -715,3 +715,16 @@ fn delta_invert_attribute_delta_with_attribute_delta() {
|
|||||||
];
|
];
|
||||||
TestBuilder::new().run_script::<PlainDoc>(ops);
|
TestBuilder::new().run_script::<PlainDoc>(ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn delta_compose_with_missing_delta() {
|
||||||
|
let ops = vec![
|
||||||
|
Insert(0, "123", 0),
|
||||||
|
Insert(0, "4", 3),
|
||||||
|
DocComposeDelta(1, 0),
|
||||||
|
AssertDocJson(0, r#"[{"insert":"1234\n"}]"#),
|
||||||
|
AssertStr(1, r#"4\n"#),
|
||||||
|
];
|
||||||
|
TestBuilder::new().run_script::<FlowyDoc>(ops);
|
||||||
|
}
|
||||||
|
@ -307,7 +307,12 @@ impl OperationTransformable for Delta {
|
|||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
if self.base_len != other.base_len {
|
if self.base_len != other.base_len {
|
||||||
return Err(ErrorBuilder::new(OTErrorCode::IncompatibleLength).build());
|
return Err(ErrorBuilder::new(OTErrorCode::IncompatibleLength)
|
||||||
|
.msg(format!(
|
||||||
|
"cur base length: {}, other base length: {}",
|
||||||
|
self.base_len, other.base_len
|
||||||
|
))
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut a_prime = Delta::default();
|
let mut a_prime = Delta::default();
|
||||||
|
@ -943,207 +943,6 @@ impl ::protobuf::reflect::ProtobufValue for UpdateViewParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq,Clone,Default)]
|
|
||||||
pub struct DocDeltaRequest {
|
|
||||||
// message fields
|
|
||||||
pub view_id: ::std::string::String,
|
|
||||||
pub data: ::std::vec::Vec<u8>,
|
|
||||||
// special fields
|
|
||||||
pub unknown_fields: ::protobuf::UnknownFields,
|
|
||||||
pub cached_size: ::protobuf::CachedSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> ::std::default::Default for &'a DocDeltaRequest {
|
|
||||||
fn default() -> &'a DocDeltaRequest {
|
|
||||||
<DocDeltaRequest as ::protobuf::Message>::default_instance()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DocDeltaRequest {
|
|
||||||
pub fn new() -> DocDeltaRequest {
|
|
||||||
::std::default::Default::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
// string view_id = 1;
|
|
||||||
|
|
||||||
|
|
||||||
pub fn get_view_id(&self) -> &str {
|
|
||||||
&self.view_id
|
|
||||||
}
|
|
||||||
pub fn clear_view_id(&mut self) {
|
|
||||||
self.view_id.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Param is passed by value, moved
|
|
||||||
pub fn set_view_id(&mut self, v: ::std::string::String) {
|
|
||||||
self.view_id = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutable pointer to the field.
|
|
||||||
// If field is not initialized, it is initialized with default value first.
|
|
||||||
pub fn mut_view_id(&mut self) -> &mut ::std::string::String {
|
|
||||||
&mut self.view_id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take field
|
|
||||||
pub fn take_view_id(&mut self) -> ::std::string::String {
|
|
||||||
::std::mem::replace(&mut self.view_id, ::std::string::String::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
// bytes data = 2;
|
|
||||||
|
|
||||||
|
|
||||||
pub fn get_data(&self) -> &[u8] {
|
|
||||||
&self.data
|
|
||||||
}
|
|
||||||
pub fn clear_data(&mut self) {
|
|
||||||
self.data.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Param is passed by value, moved
|
|
||||||
pub fn set_data(&mut self, v: ::std::vec::Vec<u8>) {
|
|
||||||
self.data = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutable pointer to the field.
|
|
||||||
// If field is not initialized, it is initialized with default value first.
|
|
||||||
pub fn mut_data(&mut self) -> &mut ::std::vec::Vec<u8> {
|
|
||||||
&mut self.data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take field
|
|
||||||
pub fn take_data(&mut self) -> ::std::vec::Vec<u8> {
|
|
||||||
::std::mem::replace(&mut self.data, ::std::vec::Vec::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::protobuf::Message for DocDeltaRequest {
|
|
||||||
fn is_initialized(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
|
|
||||||
while !is.eof()? {
|
|
||||||
let (field_number, wire_type) = is.read_tag_unpack()?;
|
|
||||||
match field_number {
|
|
||||||
1 => {
|
|
||||||
::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.view_id)?;
|
|
||||||
},
|
|
||||||
2 => {
|
|
||||||
::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.data)?;
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
::std::result::Result::Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute sizes of nested messages
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
fn compute_size(&self) -> u32 {
|
|
||||||
let mut my_size = 0;
|
|
||||||
if !self.view_id.is_empty() {
|
|
||||||
my_size += ::protobuf::rt::string_size(1, &self.view_id);
|
|
||||||
}
|
|
||||||
if !self.data.is_empty() {
|
|
||||||
my_size += ::protobuf::rt::bytes_size(2, &self.data);
|
|
||||||
}
|
|
||||||
my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
|
|
||||||
self.cached_size.set(my_size);
|
|
||||||
my_size
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
|
|
||||||
if !self.view_id.is_empty() {
|
|
||||||
os.write_string(1, &self.view_id)?;
|
|
||||||
}
|
|
||||||
if !self.data.is_empty() {
|
|
||||||
os.write_bytes(2, &self.data)?;
|
|
||||||
}
|
|
||||||
os.write_unknown_fields(self.get_unknown_fields())?;
|
|
||||||
::std::result::Result::Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cached_size(&self) -> u32 {
|
|
||||||
self.cached_size.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
|
|
||||||
&self.unknown_fields
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
|
|
||||||
&mut self.unknown_fields
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn (::std::any::Any) {
|
|
||||||
self as &dyn (::std::any::Any)
|
|
||||||
}
|
|
||||||
fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
|
|
||||||
self as &mut dyn (::std::any::Any)
|
|
||||||
}
|
|
||||||
fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
|
|
||||||
Self::descriptor_static()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new() -> DocDeltaRequest {
|
|
||||||
DocDeltaRequest::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
|
|
||||||
static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
|
|
||||||
descriptor.get(|| {
|
|
||||||
let mut fields = ::std::vec::Vec::new();
|
|
||||||
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
|
|
||||||
"view_id",
|
|
||||||
|m: &DocDeltaRequest| { &m.view_id },
|
|
||||||
|m: &mut DocDeltaRequest| { &mut m.view_id },
|
|
||||||
));
|
|
||||||
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>(
|
|
||||||
"data",
|
|
||||||
|m: &DocDeltaRequest| { &m.data },
|
|
||||||
|m: &mut DocDeltaRequest| { &mut m.data },
|
|
||||||
));
|
|
||||||
::protobuf::reflect::MessageDescriptor::new_pb_name::<DocDeltaRequest>(
|
|
||||||
"DocDeltaRequest",
|
|
||||||
fields,
|
|
||||||
file_descriptor_proto()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_instance() -> &'static DocDeltaRequest {
|
|
||||||
static instance: ::protobuf::rt::LazyV2<DocDeltaRequest> = ::protobuf::rt::LazyV2::INIT;
|
|
||||||
instance.get(DocDeltaRequest::new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::protobuf::Clear for DocDeltaRequest {
|
|
||||||
fn clear(&mut self) {
|
|
||||||
self.view_id.clear();
|
|
||||||
self.data.clear();
|
|
||||||
self.unknown_fields.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::std::fmt::Debug for DocDeltaRequest {
|
|
||||||
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
|
||||||
::protobuf::text_format::fmt(self, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::protobuf::reflect::ProtobufValue for DocDeltaRequest {
|
|
||||||
fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
|
|
||||||
::protobuf::reflect::ReflectValueRef::Message(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static file_descriptor_proto_data: &'static [u8] = b"\
|
static file_descriptor_proto_data: &'static [u8] = b"\
|
||||||
\n\x11view_update.proto\"\xda\x01\n\x11UpdateViewRequest\x12\x17\n\x07vi\
|
\n\x11view_update.proto\"\xda\x01\n\x11UpdateViewRequest\x12\x17\n\x07vi\
|
||||||
ew_id\x18\x01\x20\x01(\tR\x06viewId\x12\x14\n\x04name\x18\x02\x20\x01(\t\
|
ew_id\x18\x01\x20\x01(\tR\x06viewId\x12\x14\n\x04name\x18\x02\x20\x01(\t\
|
||||||
@ -1156,55 +955,47 @@ static file_descriptor_proto_data: &'static [u8] = b"\
|
|||||||
\x03\x20\x01(\tH\x01R\x04desc\x12\x1e\n\tthumbnail\x18\x04\x20\x01(\tH\
|
\x03\x20\x01(\tH\x01R\x04desc\x12\x1e\n\tthumbnail\x18\x04\x20\x01(\tH\
|
||||||
\x02R\tthumbnail\x12\x1b\n\x08is_trash\x18\x05\x20\x01(\x08H\x03R\x07isT\
|
\x02R\tthumbnail\x12\x1b\n\x08is_trash\x18\x05\x20\x01(\x08H\x03R\x07isT\
|
||||||
rashB\r\n\x0bone_of_nameB\r\n\x0bone_of_descB\x12\n\x10one_of_thumbnailB\
|
rashB\r\n\x0bone_of_nameB\r\n\x0bone_of_descB\x12\n\x10one_of_thumbnailB\
|
||||||
\x11\n\x0fone_of_is_trash\">\n\x0fDocDeltaRequest\x12\x17\n\x07view_id\
|
\x11\n\x0fone_of_is_trashJ\xc0\x06\n\x06\x12\x04\0\0\x0f\x01\n\x08\n\x01\
|
||||||
\x18\x01\x20\x01(\tR\x06viewId\x12\x12\n\x04data\x18\x02\x20\x01(\x0cR\
|
\x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x08\x01\n\n\n\x03\x04\
|
||||||
\x04dataJ\xc6\x07\n\x06\x12\x04\0\0\x13\x01\n\x08\n\x01\x0c\x12\x03\0\0\
|
\0\x01\x12\x03\x02\x08\x19\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x17\n\
|
||||||
\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x08\x01\n\n\n\x03\x04\0\x01\x12\x03\
|
\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\
|
||||||
\x02\x08\x19\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x17\n\x0c\n\x05\x04\
|
\x12\x03\x03\x0b\x12\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x15\x16\n\
|
||||||
\0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x03\
|
\x0b\n\x04\x04\0\x08\0\x12\x03\x04\x04*\n\x0c\n\x05\x04\0\x08\0\x01\x12\
|
||||||
\x0b\x12\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x15\x16\n\x0b\n\x04\x04\
|
\x03\x04\n\x15\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x04\x18(\n\x0c\n\x05\
|
||||||
\0\x08\0\x12\x03\x04\x04*\n\x0c\n\x05\x04\0\x08\0\x01\x12\x03\x04\n\x15\
|
\x04\0\x02\x01\x05\x12\x03\x04\x18\x1e\n\x0c\n\x05\x04\0\x02\x01\x01\x12\
|
||||||
\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x04\x18(\n\x0c\n\x05\x04\0\x02\x01\
|
\x03\x04\x1f#\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x04&'\n\x0b\n\x04\
|
||||||
\x05\x12\x03\x04\x18\x1e\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x04\x1f#\
|
\x04\0\x08\x01\x12\x03\x05\x04*\n\x0c\n\x05\x04\0\x08\x01\x01\x12\x03\
|
||||||
\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x04&'\n\x0b\n\x04\x04\0\x08\x01\
|
\x05\n\x15\n\x0b\n\x04\x04\0\x02\x02\x12\x03\x05\x18(\n\x0c\n\x05\x04\0\
|
||||||
\x12\x03\x05\x04*\n\x0c\n\x05\x04\0\x08\x01\x01\x12\x03\x05\n\x15\n\x0b\
|
\x02\x02\x05\x12\x03\x05\x18\x1e\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x03\
|
||||||
\n\x04\x04\0\x02\x02\x12\x03\x05\x18(\n\x0c\n\x05\x04\0\x02\x02\x05\x12\
|
\x05\x1f#\n\x0c\n\x05\x04\0\x02\x02\x03\x12\x03\x05&'\n\x0b\n\x04\x04\0\
|
||||||
\x03\x05\x18\x1e\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x03\x05\x1f#\n\x0c\n\
|
\x08\x02\x12\x03\x06\x044\n\x0c\n\x05\x04\0\x08\x02\x01\x12\x03\x06\n\
|
||||||
\x05\x04\0\x02\x02\x03\x12\x03\x05&'\n\x0b\n\x04\x04\0\x08\x02\x12\x03\
|
\x1a\n\x0b\n\x04\x04\0\x02\x03\x12\x03\x06\x1d2\n\x0c\n\x05\x04\0\x02\
|
||||||
\x06\x044\n\x0c\n\x05\x04\0\x08\x02\x01\x12\x03\x06\n\x1a\n\x0b\n\x04\
|
\x03\x05\x12\x03\x06\x1d#\n\x0c\n\x05\x04\0\x02\x03\x01\x12\x03\x06$-\n\
|
||||||
\x04\0\x02\x03\x12\x03\x06\x1d2\n\x0c\n\x05\x04\0\x02\x03\x05\x12\x03\
|
\x0c\n\x05\x04\0\x02\x03\x03\x12\x03\x0601\n\x0b\n\x04\x04\0\x08\x03\x12\
|
||||||
\x06\x1d#\n\x0c\n\x05\x04\0\x02\x03\x01\x12\x03\x06$-\n\x0c\n\x05\x04\0\
|
\x03\x07\x040\n\x0c\n\x05\x04\0\x08\x03\x01\x12\x03\x07\n\x19\n\x0b\n\
|
||||||
\x02\x03\x03\x12\x03\x0601\n\x0b\n\x04\x04\0\x08\x03\x12\x03\x07\x040\n\
|
\x04\x04\0\x02\x04\x12\x03\x07\x1c.\n\x0c\n\x05\x04\0\x02\x04\x05\x12\
|
||||||
\x0c\n\x05\x04\0\x08\x03\x01\x12\x03\x07\n\x19\n\x0b\n\x04\x04\0\x02\x04\
|
\x03\x07\x1c\x20\n\x0c\n\x05\x04\0\x02\x04\x01\x12\x03\x07!)\n\x0c\n\x05\
|
||||||
\x12\x03\x07\x1c.\n\x0c\n\x05\x04\0\x02\x04\x05\x12\x03\x07\x1c\x20\n\
|
\x04\0\x02\x04\x03\x12\x03\x07,-\n\n\n\x02\x04\x01\x12\x04\t\0\x0f\x01\n\
|
||||||
\x0c\n\x05\x04\0\x02\x04\x01\x12\x03\x07!)\n\x0c\n\x05\x04\0\x02\x04\x03\
|
\n\n\x03\x04\x01\x01\x12\x03\t\x08\x18\n\x0b\n\x04\x04\x01\x02\0\x12\x03\
|
||||||
\x12\x03\x07,-\n\n\n\x02\x04\x01\x12\x04\t\0\x0f\x01\n\n\n\x03\x04\x01\
|
\n\x04\x17\n\x0c\n\x05\x04\x01\x02\0\x05\x12\x03\n\x04\n\n\x0c\n\x05\x04\
|
||||||
\x01\x12\x03\t\x08\x18\n\x0b\n\x04\x04\x01\x02\0\x12\x03\n\x04\x17\n\x0c\
|
\x01\x02\0\x01\x12\x03\n\x0b\x12\n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\n\
|
||||||
\n\x05\x04\x01\x02\0\x05\x12\x03\n\x04\n\n\x0c\n\x05\x04\x01\x02\0\x01\
|
\x15\x16\n\x0b\n\x04\x04\x01\x08\0\x12\x03\x0b\x04*\n\x0c\n\x05\x04\x01\
|
||||||
\x12\x03\n\x0b\x12\n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\n\x15\x16\n\x0b\
|
\x08\0\x01\x12\x03\x0b\n\x15\n\x0b\n\x04\x04\x01\x02\x01\x12\x03\x0b\x18\
|
||||||
\n\x04\x04\x01\x08\0\x12\x03\x0b\x04*\n\x0c\n\x05\x04\x01\x08\0\x01\x12\
|
(\n\x0c\n\x05\x04\x01\x02\x01\x05\x12\x03\x0b\x18\x1e\n\x0c\n\x05\x04\
|
||||||
\x03\x0b\n\x15\n\x0b\n\x04\x04\x01\x02\x01\x12\x03\x0b\x18(\n\x0c\n\x05\
|
\x01\x02\x01\x01\x12\x03\x0b\x1f#\n\x0c\n\x05\x04\x01\x02\x01\x03\x12\
|
||||||
\x04\x01\x02\x01\x05\x12\x03\x0b\x18\x1e\n\x0c\n\x05\x04\x01\x02\x01\x01\
|
\x03\x0b&'\n\x0b\n\x04\x04\x01\x08\x01\x12\x03\x0c\x04*\n\x0c\n\x05\x04\
|
||||||
\x12\x03\x0b\x1f#\n\x0c\n\x05\x04\x01\x02\x01\x03\x12\x03\x0b&'\n\x0b\n\
|
\x01\x08\x01\x01\x12\x03\x0c\n\x15\n\x0b\n\x04\x04\x01\x02\x02\x12\x03\
|
||||||
\x04\x04\x01\x08\x01\x12\x03\x0c\x04*\n\x0c\n\x05\x04\x01\x08\x01\x01\
|
\x0c\x18(\n\x0c\n\x05\x04\x01\x02\x02\x05\x12\x03\x0c\x18\x1e\n\x0c\n\
|
||||||
\x12\x03\x0c\n\x15\n\x0b\n\x04\x04\x01\x02\x02\x12\x03\x0c\x18(\n\x0c\n\
|
\x05\x04\x01\x02\x02\x01\x12\x03\x0c\x1f#\n\x0c\n\x05\x04\x01\x02\x02\
|
||||||
\x05\x04\x01\x02\x02\x05\x12\x03\x0c\x18\x1e\n\x0c\n\x05\x04\x01\x02\x02\
|
\x03\x12\x03\x0c&'\n\x0b\n\x04\x04\x01\x08\x02\x12\x03\r\x044\n\x0c\n\
|
||||||
\x01\x12\x03\x0c\x1f#\n\x0c\n\x05\x04\x01\x02\x02\x03\x12\x03\x0c&'\n\
|
\x05\x04\x01\x08\x02\x01\x12\x03\r\n\x1a\n\x0b\n\x04\x04\x01\x02\x03\x12\
|
||||||
\x0b\n\x04\x04\x01\x08\x02\x12\x03\r\x044\n\x0c\n\x05\x04\x01\x08\x02\
|
\x03\r\x1d2\n\x0c\n\x05\x04\x01\x02\x03\x05\x12\x03\r\x1d#\n\x0c\n\x05\
|
||||||
\x01\x12\x03\r\n\x1a\n\x0b\n\x04\x04\x01\x02\x03\x12\x03\r\x1d2\n\x0c\n\
|
\x04\x01\x02\x03\x01\x12\x03\r$-\n\x0c\n\x05\x04\x01\x02\x03\x03\x12\x03\
|
||||||
\x05\x04\x01\x02\x03\x05\x12\x03\r\x1d#\n\x0c\n\x05\x04\x01\x02\x03\x01\
|
\r01\n\x0b\n\x04\x04\x01\x08\x03\x12\x03\x0e\x040\n\x0c\n\x05\x04\x01\
|
||||||
\x12\x03\r$-\n\x0c\n\x05\x04\x01\x02\x03\x03\x12\x03\r01\n\x0b\n\x04\x04\
|
\x08\x03\x01\x12\x03\x0e\n\x19\n\x0b\n\x04\x04\x01\x02\x04\x12\x03\x0e\
|
||||||
\x01\x08\x03\x12\x03\x0e\x040\n\x0c\n\x05\x04\x01\x08\x03\x01\x12\x03\
|
\x1c.\n\x0c\n\x05\x04\x01\x02\x04\x05\x12\x03\x0e\x1c\x20\n\x0c\n\x05\
|
||||||
\x0e\n\x19\n\x0b\n\x04\x04\x01\x02\x04\x12\x03\x0e\x1c.\n\x0c\n\x05\x04\
|
\x04\x01\x02\x04\x01\x12\x03\x0e!)\n\x0c\n\x05\x04\x01\x02\x04\x03\x12\
|
||||||
\x01\x02\x04\x05\x12\x03\x0e\x1c\x20\n\x0c\n\x05\x04\x01\x02\x04\x01\x12\
|
\x03\x0e,-b\x06proto3\
|
||||||
\x03\x0e!)\n\x0c\n\x05\x04\x01\x02\x04\x03\x12\x03\x0e,-\n\n\n\x02\x04\
|
|
||||||
\x02\x12\x04\x10\0\x13\x01\n\n\n\x03\x04\x02\x01\x12\x03\x10\x08\x17\n\
|
|
||||||
\x0b\n\x04\x04\x02\x02\0\x12\x03\x11\x04\x17\n\x0c\n\x05\x04\x02\x02\0\
|
|
||||||
\x05\x12\x03\x11\x04\n\n\x0c\n\x05\x04\x02\x02\0\x01\x12\x03\x11\x0b\x12\
|
|
||||||
\n\x0c\n\x05\x04\x02\x02\0\x03\x12\x03\x11\x15\x16\n\x0b\n\x04\x04\x02\
|
|
||||||
\x02\x01\x12\x03\x12\x04\x13\n\x0c\n\x05\x04\x02\x02\x01\x05\x12\x03\x12\
|
|
||||||
\x04\t\n\x0c\n\x05\x04\x02\x02\x01\x01\x12\x03\x12\n\x0e\n\x0c\n\x05\x04\
|
|
||||||
\x02\x02\x01\x03\x12\x03\x12\x11\x12b\x06proto3\
|
|
||||||
";
|
";
|
||||||
|
|
||||||
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
|
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
|
||||||
|
@ -14,7 +14,3 @@ message UpdateViewParams {
|
|||||||
oneof one_of_thumbnail { string thumbnail = 4; };
|
oneof one_of_thumbnail { string thumbnail = 4; };
|
||||||
oneof one_of_is_trash { bool is_trash = 5; };
|
oneof one_of_is_trash { bool is_trash = 5; };
|
||||||
}
|
}
|
||||||
message DocDeltaRequest {
|
|
||||||
string view_id = 1;
|
|
||||||
bytes data = 2;
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user