feat: support subscribe token (#464)

* feat: support subscribe token

* feat: support get collab

* feat: support browser rule for get collab

* fix: update collab version
This commit is contained in:
Kilu.He 2024-04-15 14:46:30 +08:00 committed by GitHub
parent 3901356e8a
commit 5041f9f164
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 294 additions and 56 deletions

View File

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

17
Cargo.lock generated
View File

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

View File

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

View File

@ -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<AppResponseError> 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<String>,
pub name: Option<String>,
pub latest_workspace_id: String,
pub icon_url: Option<String>,
}
from_struct_for_jsvalue!(User);
impl From<AFUserProfile> 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<QueryCollabParams> 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<u8>,
pub doc_state: Vec<u8>,
#[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<EncodedCollab> 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,
}
}
}

View File

@ -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<Client>,
}
#[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<bool> {
// 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<bool, ClientResponse> {
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<bool, ClientResponse> {
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<User, ClientResponse> {
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<ClientEncodeCollab, ClientResponse> {
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)),
}
}
}

View File

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

View File

@ -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<EncodedCollab, AppResponseError> {
let url = format!(
"{}/api/workspace/{}/collab/{}",
"{}/api/workspace/v1/{}/collab/{}",
self.base_url, &params.workspace_id, &params.object_id
);
let collab_type = params.collab_type.clone();
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.json(&params)
.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()

View File

@ -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<GoTrueError> for RefreshTokenRetryCondition {
fn is_retryable(&mut self, error: &GoTrueError) -> bool {
error.is_network_error()
}
}
pub async fn retry_connect(
url: String,
info: ConnectInfo,

View File

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

View File

@ -142,7 +142,6 @@ where
Ok(resp)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
pub struct AppResponseError {
pub code: ErrorCode,

View File

@ -7,3 +7,6 @@ mod conn_test;
// #[cfg(target_arch = "wasm32")]
// mod user_test;
// #[cfg(target_arch = "wasm32")]
// mod collab_test;

View File

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