save doc delta & cache the connect user information

This commit is contained in:
appflowy
2021-09-26 20:04:47 +08:00
parent 77313ab431
commit bd4caa2e99
22 changed files with 331 additions and 533 deletions

View File

@ -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);

View File

@ -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',

View File

@ -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);
}

View File

@ -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=');

View File

@ -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(()) 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 { } else {
let delta = Delta::from_bytes(&revision.delta).map_err(internal_error)?; let delta = Delta::from_bytes(&revision.delta).map_err(internal_error)?;
let _ = self.update_document_delta(delta)?; let _ = self.update_document_delta(delta)?;
socket.do_send(mk_acked_ws_message(&revision)); 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 // Opti: save with multiple revisions
let _ = self.save_revision(&revision).await?; let _ = self.save_revision(&revision).await?;
}
Ok(()) Ok(())
} else {
log::error!("Client rev_id should not equal to server rev_id");
} }
} }
@ -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(())

View File

@ -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(())

View File

@ -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()),

View File

@ -1,40 +1,50 @@
use crate::{ use crate::{
config::{HEARTBEAT_INTERVAL, PING_TIMEOUT}, config::{HEARTBEAT_INTERVAL, PING_TIMEOUT},
service::ws::{ service::{
user::LoggedUser,
ws::{
entities::{Connect, Disconnect, SessionId, Socket}, entities::{Connect, Disconnect, SessionId, Socket},
WsBizHandler, WsBizHandler,
WsBizHandlers, WsBizHandlers,
WsMessageAdaptor, WsMessageAdaptor,
WsServer, WsServer,
}, },
},
}; };
use actix::*; use actix::*;
use actix_web::web::Data; 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

View File

@ -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"

View File

@ -8,6 +8,7 @@ use std::convert::TryInto;
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 {

View File

@ -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;

View File

@ -8,4 +8,5 @@ message WsDocumentData {
enum WsDataType { enum WsDataType {
Acked = 0; Acked = 0;
Rev = 1; Rev = 1;
Conflict = 2;
} }

View File

@ -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);
} }

View File

@ -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(&params.doc_id) == false { if self.cache.is_opened(&params.doc_id) == false {
return match self._open(params, pool).await { let edit_ctx = self.make_edit_context(&params.doc_id, pool.clone()).await?;
Ok(doc) => Ok(doc), return Ok(edit_ctx);
Err(error) => Err(error),
};
} }
let edit_doc_ctx = self.cache.get(&params.doc_id)?; let edit_doc_ctx = self.cache.get(&params.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(&params.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)]

View File

@ -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(())
}; };

View File

@ -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 {

View File

@ -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)
} }

View File

@ -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 {

View File

@ -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);
}

View File

@ -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();

View File

@ -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;

View File

@ -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;
}