From 5041f9f1646b1cfd7f0c8f73a2cc14169f9e9a44 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:46:30 +0800 Subject: [PATCH] feat: support subscribe token (#464) * feat: support subscribe token * feat: support get collab * feat: support browser rule for get collab * fix: update collab version --- .github/workflows/wasm_publish.yml | 29 ++++++- Cargo.lock | 17 ++++ libs/client-api-wasm/Cargo.toml | 9 +- libs/client-api-wasm/src/entities.rs | 98 ++++++++++++++++++++- libs/client-api-wasm/src/lib.rs | 117 +++++++++++++++++--------- libs/client-api/Cargo.toml | 2 +- libs/client-api/src/wasm/http_wasm.rs | 49 ++++++++++- libs/client-api/src/wasm/retry.rs | 9 ++ libs/shared-entity/Cargo.toml | 1 + libs/shared-entity/src/response.rs | 1 - libs/wasm-test/tests/main.rs | 3 + libs/wasm-test/tests/user_test.rs | 15 ++++ 12 files changed, 294 insertions(+), 56 deletions(-) diff --git a/.github/workflows/wasm_publish.yml b/.github/workflows/wasm_publish.yml index 7ca2d3ee..3ef6a56f 100644 --- a/.github/workflows/wasm_publish.yml +++ b/.github/workflows/wasm_publish.yml @@ -8,12 +8,26 @@ on: required: true default: 'libs/client-api-wasm' package_name: - description: 'Package name' + description: 'Which package to publish' required: true default: '@appflowyinc/client-api-wasm' + type: choice + options: + - '@appflowyinc/client-api-wasm' package_version: description: 'Package version' required: true + prerelease_preid: + description: 'Preid for prerelease version (e.g., alpha, beta, rc)' + required: false + type: choice + default: '' + options: + - '' + - 'alpha' + - 'beta' + - 'rc' + env: NODE_VERSION: '20.12.0' RUST_TOOLCHAIN: "1.75" @@ -43,11 +57,18 @@ jobs: run: wasm-pack build working-directory: ${{ github.event.inputs.working_directory }} - - name: Update package.json + - name: Update name + working-directory: ${{ github.event.inputs.working_directory }}/pkg run: | - cd ${{ github.event.inputs.working_directory }}/pkg - jq '.name = "${{ github.event.inputs.package_name }}" | .version = "${{ github.event.inputs.package_version }}"' package.json > package.json.tmp + jq '.name = "${{ github.event.inputs.package_name }}"' package.json > package.json.tmp mv package.json.tmp package.json + - name: Update version + working-directory: ${{ github.event.inputs.working_directory }}/pkg + run: | + npm version ${{ github.event.inputs.package_version }} + if [ "${{ github.event.inputs.prerelease_preid }}" != "" ]; then + npm version prerelease --preid ${{ github.event.inputs.prerelease_preid }} + fi - name: Configure npm for wasm-pack run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ${{ github.event.inputs.working_directory }}/pkg/.npmrc diff --git a/Cargo.lock b/Cargo.lock index 8efd1c17..faf7ca24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1510,11 +1510,17 @@ dependencies = [ name = "client-api-wasm" version = "0.1.0" dependencies = [ + "bytes", "client-api", + "collab-entity", + "collab-rt-entity", "console_error_panic_hook", + "database-entity", "lazy_static", "serde", + "serde-wasm-bindgen", "serde_json", + "serde_repr", "tracing", "tracing-core", "tracing-wasm", @@ -5076,6 +5082,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.197" diff --git a/libs/client-api-wasm/Cargo.toml b/libs/client-api-wasm/Cargo.toml index 7555464a..6a4bb251 100644 --- a/libs/client-api-wasm/Cargo.toml +++ b/libs/client-api-wasm/Cargo.toml @@ -15,18 +15,23 @@ wasm-bindgen = "0.2.84" # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for # code size when deploying. console_error_panic_hook = { version = "0.1.7", optional = true } -serde = "1.0.197" +serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.64" client-api = { path = "../client-api" } lazy_static = "1.4.0" wasm-bindgen-futures = "0.4.20" tsify = "0.4.5" tracing.workspace = true +bytes.workspace = true tracing-core = { version = "0.1.32" } tracing-wasm = "0.2.1" uuid.workspace = true +database-entity.workspace = true +collab-rt-entity.workspace = true +collab-entity.workspace = true +serde_repr = "0.1.18" wee_alloc = { version = "0.4.5", optional = true } - +serde-wasm-bindgen = "0.6.5" [dev-dependencies] wasm-bindgen-test = "0.3.34" diff --git a/libs/client-api-wasm/src/entities.rs b/libs/client-api-wasm/src/entities.rs index db397504..544e6981 100644 --- a/libs/client-api-wasm/src/entities.rs +++ b/libs/client-api-wasm/src/entities.rs @@ -1,5 +1,10 @@ -use client_api::error::ErrorCode; +use client_api::entity::AFUserProfile; +use client_api::error::{AppResponseError, ErrorCode}; +use collab_entity::CollabType; +use collab_rt_entity::EncodedCollab; +use database_entity::dto::{QueryCollab, QueryCollabParams}; use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use tsify::Tsify; use wasm_bindgen::JsValue; @@ -7,7 +12,13 @@ macro_rules! from_struct_for_jsvalue { ($type:ty) => { impl From<$type> for JsValue { fn from(value: $type) -> Self { - JsValue::from_str(&serde_json::to_string(&value).unwrap()) + match serde_wasm_bindgen::to_value(&value) { + Ok(js_value) => js_value, + Err(err) => { + tracing::error!("Failed to convert User to JsValue: {:?}", err); + JsValue::NULL + }, + } } } }; @@ -39,3 +50,86 @@ pub struct ClientResponse { } from_struct_for_jsvalue!(ClientResponse); +impl From for ClientResponse { + fn from(err: AppResponseError) -> Self { + ClientResponse { + code: err.code, + message: err.message.to_string(), + } + } +} + +#[derive(Tsify, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct User { + pub uid: String, + pub uuid: String, + pub email: Option, + pub name: Option, + pub latest_workspace_id: String, + pub icon_url: Option, +} + +from_struct_for_jsvalue!(User); +impl From for User { + fn from(profile: AFUserProfile) -> Self { + User { + uid: profile.uid.to_string(), + uuid: profile.uuid.to_string(), + email: profile.email, + name: profile.name, + latest_workspace_id: profile.latest_workspace_id.to_string(), + icon_url: None, + } + } +} + +#[derive(Tsify, Serialize, Deserialize, Default, Debug)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ClientQueryCollabParams { + pub workspace_id: String, + pub object_id: String, + #[tsify(type = "0 | 1 | 2 | 3 | 4 | 5")] + pub collab_type: i32, +} + +impl Into for ClientQueryCollabParams { + fn into(self) -> QueryCollabParams { + QueryCollabParams { + workspace_id: self.workspace_id, + inner: QueryCollab { + collab_type: CollabType::from(self.collab_type), + object_id: self.object_id, + }, + } + } +} + +#[derive(Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ClientEncodeCollab { + pub state_vector: Vec, + pub doc_state: Vec, + #[serde(default)] + pub version: ClientEncoderVersion, +} + +#[derive(Tsify, Default, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum ClientEncoderVersion { + #[default] + V1 = 0, + V2 = 1, +} + +from_struct_for_jsvalue!(ClientEncodeCollab); + +impl From for ClientEncodeCollab { + fn from(collab: EncodedCollab) -> Self { + ClientEncodeCollab { + state_vector: collab.state_vector.to_vec(), + doc_state: collab.doc_state.to_vec(), + version: ClientEncoderVersion::V1, + } + } +} diff --git a/libs/client-api-wasm/src/lib.rs b/libs/client-api-wasm/src/lib.rs index 006cbf5e..6d70b087 100644 --- a/libs/client-api-wasm/src/lib.rs +++ b/libs/client-api-wasm/src/lib.rs @@ -1,6 +1,11 @@ pub mod entities; -use crate::entities::{ClientAPIConfig, ClientResponse}; + +use crate::entities::*; +use client_api::entity::QueryCollabParams; +use client_api::notify::TokenState; use client_api::{Client, ClientConfiguration}; +use std::sync::Arc; +use tracing; use wasm_bindgen::prelude::*; #[cfg(feature = "enable_wee_alloc")] @@ -27,11 +32,16 @@ extern "C" { #[wasm_bindgen(js_namespace = console)] fn trace(msg: &str); + #[wasm_bindgen(js_namespace = window)] + fn refresh_token(token: &str); + + #[wasm_bindgen(js_namespace = window)] + fn invalid_token(); } #[wasm_bindgen] pub struct ClientAPI { - client: Client, + client: Arc, } #[wasm_bindgen] @@ -55,54 +65,77 @@ impl ClientAPI { configuration, config.client_id.as_str(), ); + tracing::debug!("Client API initialized, config: {:?}", config); - ClientAPI { client } + ClientAPI { + client: Arc::new(client), + } } - // pub async fn get_user(&self) -> ClientResponse { - // if let Err(err) = self.client.get_profile().await { - // log::error!("Get user failed: {:?}", err); - // return ClientResponse { - // code: ClientErrorCode::from(err.code), - // message: err.message.to_string(), - // data: None - // } - // } - // - // log::info!("Get user success"); - // ClientResponse { - // code: ClientErrorCode::Ok, - // message: "Get user success".to_string(), - // } - // } + pub fn subscribe(&self) { + let mut rx = self.client.subscribe_token_state(); + let client = self.client.clone(); - pub async fn sign_up_email_verified( - &self, - email: &str, - password: &str, - ) -> Result { - if let Err(err) = self.client.sign_up(email, password).await { - return Err(ClientResponse { - code: err.code, - message: err.message.to_string(), - }); + wasm_bindgen_futures::spawn_local(async move { + while let Ok(state) = rx.recv().await { + match state { + TokenState::Refresh => { + if let Ok(token) = client.get_token() { + refresh_token(token.as_str()); + } else { + invalid_token(); + } + }, + TokenState::Invalid => { + invalid_token(); + }, + } + } + }); + } + pub async fn login(&self, email: &str, password: &str) -> Result<(), ClientResponse> { + match self.client.sign_in_password(email, password).await { + Ok(_) => Ok(()), + Err(err) => Err(ClientResponse::from(err)), } - - Ok(true) } - pub async fn sign_in_password( - &self, - email: &str, - password: &str, - ) -> Result { - if let Err(err) = self.client.sign_in_password(email, password).await { - return Err(ClientResponse { - code: err.code, - message: err.message.to_string(), - }); + pub async fn sign_up(&self, email: &str, password: &str) -> Result<(), ClientResponse> { + match self.client.sign_up(email, password).await { + Ok(_) => Ok(()), + Err(err) => Err(ClientResponse::from(err)), } + } - Ok(true) + pub async fn logout(&self) -> Result<(), ClientResponse> { + match self.client.sign_out().await { + Ok(_) => Ok(()), + Err(err) => Err(ClientResponse::from(err)), + } + } + + pub async fn get_user(&self) -> Result { + match self.client.get_profile().await { + Ok(profile) => Ok(User::from(profile)), + Err(err) => Err(ClientResponse::from(err)), + } + } + + pub fn restore_token(&self, token: &str) -> Result<(), ClientResponse> { + match self.client.restore_token(token) { + Ok(_) => Ok(()), + Err(err) => Err(ClientResponse::from(err)), + } + } + + pub async fn get_collab( + &self, + params: ClientQueryCollabParams, + ) -> Result { + tracing::debug!("get_collab: {:?}", params); + match self.client.get_collab(params.into()).await { + Ok(data) => Ok(ClientEncodeCollab::from(data)), + Err(err) => Err(ClientResponse::from(err)), + } } } diff --git a/libs/client-api/Cargo.toml b/libs/client-api/Cargo.toml index 49a260fc..eaaa9eda 100644 --- a/libs/client-api/Cargo.toml +++ b/libs/client-api/Cargo.toml @@ -59,7 +59,7 @@ features = ["tungstenite"] wasm-bindgen-futures = "0.4.40" getrandom = { version = "0.2", features = ["js"]} tokio = { workspace = true, features = ["sync"]} -again = "0.1.2" +again = { version = "0.1.2" } [features] collab-sync = ["collab", "yrs"] diff --git a/libs/client-api/src/wasm/http_wasm.rs b/libs/client-api/src/wasm/http_wasm.rs index 4f0af8b2..bbacc0aa 100644 --- a/libs/client-api/src/wasm/http_wasm.rs +++ b/libs/client-api/src/wasm/http_wasm.rs @@ -1,17 +1,21 @@ use crate::http::log_request_id; use crate::ws::{WSClientHttpSender, WSError}; use crate::Client; +use crate::RefreshTokenRetryCondition; +use again::RetryPolicy; use app_error::gotrue::GoTrueError; -use app_error::ErrorCode; +use app_error::{AppError, ErrorCode}; use async_trait::async_trait; use collab_rt_entity::EncodedCollab; use database_entity::dto::{CollabParams, QueryCollabParams}; use gotrue::grant::{Grant, RefreshTokenGrant}; use reqwest::Method; +use shared_entity::dto::workspace_dto::CollabTypeParam; use shared_entity::response::{AppResponse, AppResponseError}; use std::future::Future; use std::sync::atomic::Ordering; -use tracing::instrument; +use std::time::Duration; +use tracing::{event, instrument}; impl Client { pub async fn create_collab_list( @@ -32,13 +36,14 @@ impl Client { params: QueryCollabParams, ) -> Result { let url = format!( - "{}/api/workspace/{}/collab/{}", + "{}/api/workspace/v1/{}/collab/{}", self.base_url, ¶ms.workspace_id, ¶ms.object_id ); + let collab_type = params.collab_type.clone(); let resp = self .http_client_with_auth(Method::GET, &url) .await? - .json(¶ms) + .query(&CollabTypeParam { collab_type }) .send() .await?; log_request_id(&resp); @@ -67,6 +72,42 @@ impl Client { } async fn inner_refresh_token(&self) -> Result<(), AppResponseError> { + // let policy = RetryPolicy::fixed(Duration::from_secs(2)).with_max_retries(4).with_jitter(false); + // let refresh_token = self + // .token + // .read() + // .as_ref() + // .ok_or(GoTrueError::NotLoggedIn( + // "fail to refresh user token".to_owned(), + // ))? + // .refresh_token + // .as_str() + // .to_owned(); + // match policy.retry_if(move || { + // let grant = Grant::RefreshToken(RefreshTokenGrant { refresh_token: refresh_token.clone() }); + // async move { + // self + // .gotrue_client + // .token(&grant).await + // } + // + // }, RefreshTokenRetryCondition).await { + // Ok(new_token) => { + // event!(tracing::Level::INFO, "refresh token success"); + // self.token.write().set(new_token); + // Ok(()) + // }, + // Err(err) => { + // let err = AppError::from(err); + // event!(tracing::Level::ERROR, "refresh token failed: {}", err); + // + // // If the error is an OAuth error, unset the token. + // if err.is_unauthorized() { + // self.token.write().unset(); + // } + // Err(err.into()) + // }, + // } let refresh_token = self .token .read() diff --git a/libs/client-api/src/wasm/retry.rs b/libs/client-api/src/wasm/retry.rs index 00793e49..69ed5052 100644 --- a/libs/client-api/src/wasm/retry.rs +++ b/libs/client-api/src/wasm/retry.rs @@ -1,8 +1,17 @@ use crate::ws::{ConnectInfo, CurrentConnInfo, StateNotify, WSError}; +use again::Condition; +use app_error::gotrue::GoTrueError; use client_websocket::{connect_async, WebSocketStream}; use reqwest::header::HeaderMap; use std::sync::Weak; +pub(crate) struct RefreshTokenRetryCondition; + +impl Condition for RefreshTokenRetryCondition { + fn is_retryable(&mut self, error: &GoTrueError) -> bool { + error.is_network_error() + } +} pub async fn retry_connect( url: String, info: ConnectInfo, diff --git a/libs/shared-entity/Cargo.toml b/libs/shared-entity/Cargo.toml index bcd00940..909ca4bc 100644 --- a/libs/shared-entity/Cargo.toml +++ b/libs/shared-entity/Cargo.toml @@ -25,5 +25,6 @@ actix-web = { version = "4.4.1", default-features = false, features = ["http2"], validator = { version = "0.16", features = ["validator_derive", "derive"], optional = true } rust-s3 = { version = "0.33.0", optional = true } + [features] cloud = ["actix-web", "validator", "rust-s3"] diff --git a/libs/shared-entity/src/response.rs b/libs/shared-entity/src/response.rs index dd8a1a85..129b50de 100644 --- a/libs/shared-entity/src/response.rs +++ b/libs/shared-entity/src/response.rs @@ -142,7 +142,6 @@ where Ok(resp) } } - #[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] pub struct AppResponseError { pub code: ErrorCode, diff --git a/libs/wasm-test/tests/main.rs b/libs/wasm-test/tests/main.rs index 8af29f1c..f7d83977 100644 --- a/libs/wasm-test/tests/main.rs +++ b/libs/wasm-test/tests/main.rs @@ -7,3 +7,6 @@ mod conn_test; // #[cfg(target_arch = "wasm32")] // mod user_test; + +// #[cfg(target_arch = "wasm32")] +// mod collab_test; diff --git a/libs/wasm-test/tests/user_test.rs b/libs/wasm-test/tests/user_test.rs index 6a7fdad1..833ae42e 100644 --- a/libs/wasm-test/tests/user_test.rs +++ b/libs/wasm-test/tests/user_test.rs @@ -25,3 +25,18 @@ async fn wasm_sign_in_success() { assert!(val); } + +#[wasm_bindgen_test] +async fn wasm_logout_success() { + let test_client = TestClient::new_user().await; + let user = test_client.user; + + test_client + .api_client + .sign_in_password(user.email.as_str(), user.password.as_str()) + .await + .unwrap(); + let res = test_client.api_client.sign_out().await; + + assert!(res.is_ok()); +}