diff --git a/client-app/src-tauri/Cargo.lock b/client-app/src-tauri/Cargo.lock index 30308a5df..393263659 100644 --- a/client-app/src-tauri/Cargo.lock +++ b/client-app/src-tauri/Cargo.lock @@ -21,6 +21,7 @@ dependencies = [ "tauri", "tauri-build", "tokio", + "yrs", ] [[package]] diff --git a/client-app/src-tauri/Cargo.toml b/client-app/src-tauri/Cargo.toml index 92a8e3a53..0ec16b033 100644 --- a/client-app/src-tauri/Cargo.toml +++ b/client-app/src-tauri/Cargo.toml @@ -28,6 +28,7 @@ serde_json = "1.0" dotenvy = "0.15.6" tauri = {version = "1.2", features = ["api-all", "devtools"] } tokio = { version = "1.23.0", features = ["rt", "macros"] } +yrs = { path = "../src-OctoBase/libs/yrs" } [features] # by default Tauri runs in production mode diff --git a/client-app/src-tauri/examples/generate-jsonschema.rs b/client-app/src-tauri/examples/generate-jsonschema.rs index cdb6fb70b..4ef8f1efb 100644 --- a/client-app/src-tauri/examples/generate-jsonschema.rs +++ b/client-app/src-tauri/examples/generate-jsonschema.rs @@ -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 * with way to optimize @@ -27,7 +27,7 @@ fn main() { &mono_repo_root, "packages/data-center/src/provider/tauri-ipc/ipc/types", ); - generate::(Path::join(&data_center_ipc_type_folder, "document.json")); + generate::(Path::join(&data_center_ipc_type_folder, "document.json")); generate::(Path::join(&data_center_ipc_type_folder, "workspace.json")); generate::(Path::join(&data_center_ipc_type_folder, "blob.json")); } diff --git a/client-app/src-tauri/src/commands.rs b/client-app/src-tauri/src/commands.rs index be79a7540..c416b3b8e 100644 --- a/client-app/src-tauri/src/commands.rs +++ b/client-app/src-tauri/src/commands.rs @@ -1,14 +1,17 @@ pub mod blob; pub mod workspace; +pub mod document; use blob::*; use workspace::*; +use document::*; pub fn invoke_handler() -> impl Fn(tauri::Invoke) + Send + Sync + 'static { tauri::generate_handler![ update_y_document, create_workspace, update_workspace, + get_doc, put_blob, get_blob ] diff --git a/client-app/src-tauri/src/commands/document.rs b/client-app/src-tauri/src/commands/document.rs new file mode 100644 index 000000000..36f945ced --- /dev/null +++ b/client-app/src-tauri/src/commands/document.rs @@ -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 { + // 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 { + 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() + )) + } +} diff --git a/client-app/src-tauri/src/commands/workspace.rs b/client-app/src-tauri/src/commands/workspace.rs index 772640f5b..fcc3972c2 100644 --- a/client-app/src-tauri/src/commands/workspace.rs +++ b/client-app/src-tauri/src/commands/workspace.rs @@ -1,7 +1,4 @@ -use ipc_types::{ - document::YDocumentUpdate, - workspace::{CreateWorkspace, CreateWorkspaceResult, UpdateWorkspace}, -}; +use ipc_types::workspace::{CreateWorkspace, CreateWorkspaceResult, UpdateWorkspace}; use jwst::{DocStorage, Workspace as OctoBaseWorkspace}; use lib0::any::Any; @@ -24,8 +21,10 @@ pub async fn create_workspace<'s>( let workspace_doc = OctoBaseWorkspace::new(new_workspace.id.to_string()); workspace_doc.with_trx(|mut workspace_doc_transaction| { - workspace_doc_transaction - .set_metadata("name", Any::String(parameters.name.clone().into_boxed_str())); + workspace_doc_transaction.set_metadata( + "name", + Any::String(parameters.name.clone().into_boxed_str()), + ); }); if let Err(error_message) = &state .0 @@ -52,11 +51,7 @@ pub async fn update_workspace<'s>( state: tauri::State<'s, AppState>, parameters: UpdateWorkspace, ) -> Result { + // TODO: check user permission // No thing to update now. The avatar is update in YDoc using websocket or yrs.update Ok(true) } - -#[tauri::command] -pub fn update_y_document(parameters: YDocumentUpdate) -> Result { - Ok(true) -} diff --git a/client-app/src-tauri/types/src/document.rs b/client-app/src-tauri/types/src/document.rs index 0a0e66f6f..c3be57eac 100644 --- a/client-app/src-tauri/types/src/document.rs +++ b/client-app/src-tauri/types/src/document.rs @@ -2,7 +2,23 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct YDocumentUpdate<'a> { - update: Vec, - room: &'a str, +pub struct YDocumentUpdate { + pub update: Vec, + 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, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub enum IDocumentParameters { + YDocumentUpdate(YDocumentUpdate), + GetDocumentParameter(GetDocumentParameter), + GetDocumentResponse(GetDocumentResponse), } diff --git a/packages/data-center/src/provider/tauri-ipc/index.ts b/packages/data-center/src/provider/tauri-ipc/index.ts index 606ab2d52..447e758b8 100644 --- a/packages/data-center/src/provider/tauri-ipc/index.ts +++ b/packages/data-center/src/provider/tauri-ipc/index.ts @@ -16,7 +16,12 @@ export class TauriIPCProvider extends LocalProvider { async initData() { assert(this._workspace.room); this._logger('Loading local data'); - const { doc, room } = this._workspace; + const { + doc, + room, + meta: { id }, + } = this._workspace; + this.#initDocFromIPC(id, doc); doc.on( 'update', async ( @@ -30,7 +35,7 @@ export class TauriIPCProvider extends LocalProvider { // TODO: update seems too frequent upon each keydown, why no batching? const success = await this.#ipc.updateYDocument({ update: Array.from(update), - room, + id: Number(id), }); if (!success) { throw new Error( @@ -44,8 +49,25 @@ export class TauriIPCProvider extends LocalProvider { } ); 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() { diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/TauriIPCProvider.ts b/packages/data-center/src/provider/tauri-ipc/ipc/TauriIPCProvider.ts deleted file mode 100644 index b01d36e79..000000000 --- a/packages/data-center/src/provider/tauri-ipc/ipc/TauriIPCProvider.ts +++ /dev/null @@ -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 - 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() {} -} diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/methods.ts b/packages/data-center/src/provider/tauri-ipc/ipc/methods.ts index 015739de7..2fa482016 100644 --- a/packages/data-center/src/provider/tauri-ipc/ipc/methods.ts +++ b/packages/data-center/src/provider/tauri-ipc/ipc/methods.ts @@ -1,5 +1,9 @@ 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 { GetBlob, PutBlob } from './types/blob'; @@ -8,6 +12,11 @@ export const updateYDocument = async (parameters: YDocumentUpdate) => parameters, }); +export const getYDocument = async (parameters: GetDocumentParameter) => + await invoke('get_doc', { + parameters, + }); + export const createWorkspace = async (parameters: CreateWorkspace) => await invoke('create_workspace', { parameters, diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/types/document.json b/packages/data-center/src/provider/tauri-ipc/ipc/types/document.json index a5c2f8f12..49f8caae3 100644 --- a/packages/data-center/src/provider/tauri-ipc/ipc/types/document.json +++ b/packages/data-center/src/provider/tauri-ipc/ipc/types/document.json @@ -1,18 +1,79 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "YDocumentUpdate", - "type": "object", - "required": ["room", "update"], - "properties": { - "room": { - "type": "string" + "title": "IDocumentParameters", + "oneOf": [ + { + "type": "object", + "required": ["YDocumentUpdate"], + "properties": { + "YDocumentUpdate": { + "$ref": "#/definitions/YDocumentUpdate" + } + }, + "additionalProperties": false }, - "update": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 + { + "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": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } } } } diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/types/document.ts b/packages/data-center/src/provider/tauri-ipc/ipc/types/document.ts index fd6dd9160..9b652b6ce 100644 --- a/packages/data-center/src/provider/tauri-ipc/ipc/types/document.ts +++ b/packages/data-center/src/provider/tauri-ipc/ipc/types/document.ts @@ -5,8 +5,27 @@ * and run json-schema-to-typescript to regenerate this file. */ +export type IDocumentParameters = + | { + YDocumentUpdate: YDocumentUpdate; + } + | { + GetDocumentParameter: GetDocumentParameter; + } + | { + GetDocumentResponse: GetDocumentResponse; + }; + 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[]; [k: string]: unknown; }