feat: update yrs doc using ipc from yjs doc
This commit is contained in:
1
client-app/src-tauri/Cargo.lock
generated
1
client-app/src-tauri/Cargo.lock
generated
@@ -21,6 +21,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"yrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ serde_json = "1.0"
|
|||||||
dotenvy = "0.15.6"
|
dotenvy = "0.15.6"
|
||||||
tauri = {version = "1.2", features = ["api-all", "devtools"] }
|
tauri = {version = "1.2", features = ["api-all", "devtools"] }
|
||||||
tokio = { version = "1.23.0", features = ["rt", "macros"] }
|
tokio = { version = "1.23.0", features = ["rt", "macros"] }
|
||||||
|
yrs = { path = "../src-OctoBase/libs/yrs" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use ipc_types::{blob::IBlobParameters, document::YDocumentUpdate, workspace::IWorkspaceParameters};
|
use ipc_types::{blob::IBlobParameters, document::IDocumentParameters, workspace::IWorkspaceParameters};
|
||||||
/**
|
/**
|
||||||
* convert serde to jsonschema: https://imfeld.dev/writing/generating_typescript_types_from_rust
|
* convert serde to jsonschema: https://imfeld.dev/writing/generating_typescript_types_from_rust
|
||||||
* with way to optimize
|
* with way to optimize
|
||||||
@@ -27,7 +27,7 @@ fn main() {
|
|||||||
&mono_repo_root,
|
&mono_repo_root,
|
||||||
"packages/data-center/src/provider/tauri-ipc/ipc/types",
|
"packages/data-center/src/provider/tauri-ipc/ipc/types",
|
||||||
);
|
);
|
||||||
generate::<YDocumentUpdate>(Path::join(&data_center_ipc_type_folder, "document.json"));
|
generate::<IDocumentParameters>(Path::join(&data_center_ipc_type_folder, "document.json"));
|
||||||
generate::<IWorkspaceParameters>(Path::join(&data_center_ipc_type_folder, "workspace.json"));
|
generate::<IWorkspaceParameters>(Path::join(&data_center_ipc_type_folder, "workspace.json"));
|
||||||
generate::<IBlobParameters>(Path::join(&data_center_ipc_type_folder, "blob.json"));
|
generate::<IBlobParameters>(Path::join(&data_center_ipc_type_folder, "blob.json"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
pub mod blob;
|
pub mod blob;
|
||||||
pub mod workspace;
|
pub mod workspace;
|
||||||
|
pub mod document;
|
||||||
|
|
||||||
use blob::*;
|
use blob::*;
|
||||||
use workspace::*;
|
use workspace::*;
|
||||||
|
use document::*;
|
||||||
|
|
||||||
pub fn invoke_handler() -> impl Fn(tauri::Invoke) + Send + Sync + 'static {
|
pub fn invoke_handler() -> impl Fn(tauri::Invoke) + Send + Sync + 'static {
|
||||||
tauri::generate_handler![
|
tauri::generate_handler![
|
||||||
update_y_document,
|
update_y_document,
|
||||||
create_workspace,
|
create_workspace,
|
||||||
update_workspace,
|
update_workspace,
|
||||||
|
get_doc,
|
||||||
put_blob,
|
put_blob,
|
||||||
get_blob
|
get_blob
|
||||||
]
|
]
|
||||||
|
|||||||
55
client-app/src-tauri/src/commands/document.rs
Normal file
55
client-app/src-tauri/src/commands/document.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use ipc_types::document::{GetDocumentParameter, GetDocumentResponse, YDocumentUpdate};
|
||||||
|
use jwst::DocStorage;
|
||||||
|
use yrs::StateVector;
|
||||||
|
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_doc<'s>(
|
||||||
|
state: tauri::State<'s, AppState>,
|
||||||
|
parameters: GetDocumentParameter,
|
||||||
|
) -> Result<GetDocumentResponse, String> {
|
||||||
|
// TODO: check user permission
|
||||||
|
|
||||||
|
if let Some(doc) = &state
|
||||||
|
.0
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.doc_storage
|
||||||
|
.get(parameters.id)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
{
|
||||||
|
Ok(GetDocumentResponse {
|
||||||
|
update: doc.encode_state_as_update_v1(&StateVector::default()),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"Failed to get yDoc from {}",
|
||||||
|
parameters.id.to_string()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_y_document<'s>(
|
||||||
|
state: tauri::State<'s, AppState>,
|
||||||
|
parameters: YDocumentUpdate,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
if let Some(doc) = &state
|
||||||
|
.0
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.doc_storage
|
||||||
|
.write_update(parameters.id, ¶meters.update)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
{
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"Failed to update yDoc to {}",
|
||||||
|
parameters.id.to_string()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
use ipc_types::{
|
use ipc_types::workspace::{CreateWorkspace, CreateWorkspaceResult, UpdateWorkspace};
|
||||||
document::YDocumentUpdate,
|
|
||||||
workspace::{CreateWorkspace, CreateWorkspaceResult, UpdateWorkspace},
|
|
||||||
};
|
|
||||||
use jwst::{DocStorage, Workspace as OctoBaseWorkspace};
|
use jwst::{DocStorage, Workspace as OctoBaseWorkspace};
|
||||||
use lib0::any::Any;
|
use lib0::any::Any;
|
||||||
|
|
||||||
@@ -24,8 +21,10 @@ pub async fn create_workspace<'s>(
|
|||||||
let workspace_doc = OctoBaseWorkspace::new(new_workspace.id.to_string());
|
let workspace_doc = OctoBaseWorkspace::new(new_workspace.id.to_string());
|
||||||
|
|
||||||
workspace_doc.with_trx(|mut workspace_doc_transaction| {
|
workspace_doc.with_trx(|mut workspace_doc_transaction| {
|
||||||
workspace_doc_transaction
|
workspace_doc_transaction.set_metadata(
|
||||||
.set_metadata("name", Any::String(parameters.name.clone().into_boxed_str()));
|
"name",
|
||||||
|
Any::String(parameters.name.clone().into_boxed_str()),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
if let Err(error_message) = &state
|
if let Err(error_message) = &state
|
||||||
.0
|
.0
|
||||||
@@ -52,11 +51,7 @@ pub async fn update_workspace<'s>(
|
|||||||
state: tauri::State<'s, AppState>,
|
state: tauri::State<'s, AppState>,
|
||||||
parameters: UpdateWorkspace,
|
parameters: UpdateWorkspace,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
|
// TODO: check user permission
|
||||||
// No thing to update now. The avatar is update in YDoc using websocket or yrs.update
|
// No thing to update now. The avatar is update in YDoc using websocket or yrs.update
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn update_y_document(parameters: YDocumentUpdate) -> Result<bool, String> {
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,23 @@ use schemars::JsonSchema;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct YDocumentUpdate<'a> {
|
pub struct YDocumentUpdate {
|
||||||
update: Vec<u8>,
|
pub update: Vec<u8>,
|
||||||
room: &'a str,
|
pub id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct GetDocumentParameter {
|
||||||
|
pub id: i64,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct GetDocumentResponse {
|
||||||
|
pub update: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub enum IDocumentParameters {
|
||||||
|
YDocumentUpdate(YDocumentUpdate),
|
||||||
|
GetDocumentParameter(GetDocumentParameter),
|
||||||
|
GetDocumentResponse(GetDocumentResponse),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ export class TauriIPCProvider extends LocalProvider {
|
|||||||
async initData() {
|
async initData() {
|
||||||
assert(this._workspace.room);
|
assert(this._workspace.room);
|
||||||
this._logger('Loading local data');
|
this._logger('Loading local data');
|
||||||
const { doc, room } = this._workspace;
|
const {
|
||||||
|
doc,
|
||||||
|
room,
|
||||||
|
meta: { id },
|
||||||
|
} = this._workspace;
|
||||||
|
this.#initDocFromIPC(id, doc);
|
||||||
doc.on(
|
doc.on(
|
||||||
'update',
|
'update',
|
||||||
async (
|
async (
|
||||||
@@ -30,7 +35,7 @@ export class TauriIPCProvider extends LocalProvider {
|
|||||||
// TODO: update seems too frequent upon each keydown, why no batching?
|
// TODO: update seems too frequent upon each keydown, why no batching?
|
||||||
const success = await this.#ipc.updateYDocument({
|
const success = await this.#ipc.updateYDocument({
|
||||||
update: Array.from(update),
|
update: Array.from(update),
|
||||||
room,
|
id: Number(id),
|
||||||
});
|
});
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -44,8 +49,25 @@ export class TauriIPCProvider extends LocalProvider {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
this._logger('Local data loaded');
|
this._logger('Local data loaded');
|
||||||
|
}
|
||||||
|
|
||||||
await this._globalConfig.set(this._workspace.room, true);
|
async #initDocFromIPC(workspaceID: string, doc: Y.Doc) {
|
||||||
|
this._logger(`Loading ${workspaceID}...`);
|
||||||
|
const updates = await this.#ipc.getYDocument({ id: Number(workspaceID) });
|
||||||
|
if (updates) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
doc.once('update', resolve);
|
||||||
|
Y.applyUpdate(doc, new Uint8Array(updates.update));
|
||||||
|
});
|
||||||
|
this._logger(`Loaded: ${workspaceID}`);
|
||||||
|
|
||||||
|
// only add to list as online workspace
|
||||||
|
this._signals.listAdd.emit({
|
||||||
|
workspace: workspaceID,
|
||||||
|
provider: this.id,
|
||||||
|
locally: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear() {
|
async clear() {
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
||||||
import * as Y from 'yjs';
|
|
||||||
import { Observable } from 'lib0/observable';
|
|
||||||
import { DocProvider } from '@blocksuite/store';
|
|
||||||
import type { Awareness } from 'y-protocols/awareness';
|
|
||||||
import { invoke } from '@tauri-apps/api';
|
|
||||||
import { updateYDocument } from './methods';
|
|
||||||
|
|
||||||
export class TauriIPCProvider
|
|
||||||
extends Observable<string>
|
|
||||||
implements DocProvider
|
|
||||||
{
|
|
||||||
#yDocument: Y.Doc;
|
|
||||||
constructor(
|
|
||||||
room: string,
|
|
||||||
yDocument: Y.Doc,
|
|
||||||
options?: { awareness?: Awareness }
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
this.#yDocument = yDocument;
|
|
||||||
this.#yDocument.on(
|
|
||||||
'update',
|
|
||||||
async (
|
|
||||||
update: Uint8Array,
|
|
||||||
origin: any,
|
|
||||||
_yDocument: Y.Doc,
|
|
||||||
_transaction: Y.Transaction
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
// TODO: need handle potential data race when update is frequent?
|
|
||||||
// TODO: update seems too frequent upon each keydown, why no batching?
|
|
||||||
const success = await updateYDocument({
|
|
||||||
update: Array.from(update),
|
|
||||||
room,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// TODO: write error log to disk, and add button to open them in settings panel
|
|
||||||
console.error("#yDocument.on('update'", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {}
|
|
||||||
|
|
||||||
destroy() {}
|
|
||||||
disconnect() {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearData() {}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import { YDocumentUpdate } from './types/document';
|
import {
|
||||||
|
GetDocumentParameter,
|
||||||
|
GetDocumentResponse,
|
||||||
|
YDocumentUpdate,
|
||||||
|
} from './types/document';
|
||||||
import { CreateWorkspace, CreateWorkspaceResult } from './types/workspace';
|
import { CreateWorkspace, CreateWorkspaceResult } from './types/workspace';
|
||||||
import { GetBlob, PutBlob } from './types/blob';
|
import { GetBlob, PutBlob } from './types/blob';
|
||||||
|
|
||||||
@@ -8,6 +12,11 @@ export const updateYDocument = async (parameters: YDocumentUpdate) =>
|
|||||||
parameters,
|
parameters,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getYDocument = async (parameters: GetDocumentParameter) =>
|
||||||
|
await invoke<GetDocumentResponse>('get_doc', {
|
||||||
|
parameters,
|
||||||
|
});
|
||||||
|
|
||||||
export const createWorkspace = async (parameters: CreateWorkspace) =>
|
export const createWorkspace = async (parameters: CreateWorkspace) =>
|
||||||
await invoke<CreateWorkspaceResult>('create_workspace', {
|
await invoke<CreateWorkspaceResult>('create_workspace', {
|
||||||
parameters,
|
parameters,
|
||||||
|
|||||||
@@ -1,11 +1,70 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "YDocumentUpdate",
|
"title": "IDocumentParameters",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["room", "update"],
|
"required": ["YDocumentUpdate"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"room": {
|
"YDocumentUpdate": {
|
||||||
"type": "string"
|
"$ref": "#/definitions/YDocumentUpdate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["GetDocumentParameter"],
|
||||||
|
"properties": {
|
||||||
|
"GetDocumentParameter": {
|
||||||
|
"$ref": "#/definitions/GetDocumentParameter"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["GetDocumentResponse"],
|
||||||
|
"properties": {
|
||||||
|
"GetDocumentResponse": {
|
||||||
|
"$ref": "#/definitions/GetDocumentResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"definitions": {
|
||||||
|
"GetDocumentParameter": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id"],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GetDocumentResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["update"],
|
||||||
|
"properties": {
|
||||||
|
"update": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint8",
|
||||||
|
"minimum": 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"YDocumentUpdate": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "update"],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -16,4 +75,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,27 @@
|
|||||||
* and run json-schema-to-typescript to regenerate this file.
|
* and run json-schema-to-typescript to regenerate this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export type IDocumentParameters =
|
||||||
|
| {
|
||||||
|
YDocumentUpdate: YDocumentUpdate;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
GetDocumentParameter: GetDocumentParameter;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
GetDocumentResponse: GetDocumentResponse;
|
||||||
|
};
|
||||||
|
|
||||||
export interface YDocumentUpdate {
|
export interface YDocumentUpdate {
|
||||||
room: string;
|
id: number;
|
||||||
|
update: number[];
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
export interface GetDocumentParameter {
|
||||||
|
id: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
export interface GetDocumentResponse {
|
||||||
update: number[];
|
update: number[];
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user