refactor(data-center): remove side effect in affine provider (#1106)

This commit is contained in:
Himself65
2023-02-17 16:25:08 -06:00
committed by GitHub
parent 7849254785
commit 4647d44972
12 changed files with 343 additions and 296 deletions

View File

@@ -1,14 +1,14 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import { getApis, MessageCenter } from '@affine/datacenter'; import { MessageCenter } from '@affine/datacenter';
import { AffineProvider } from '@affine/datacenter';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect } from 'react'; import { ReactNode, useCallback, useEffect } from 'react';
export function MessageCenterHandler({ import { useGlobalState } from '@/store/app';
children,
}: { export function MessageCenterHandler({ children }: { children?: ReactNode }) {
children?: React.ReactNode;
}) {
const router = useRouter(); const router = useRouter();
const dataCenter = useGlobalState(useCallback(store => store.dataCenter, []));
useEffect(() => { useEffect(() => {
const instance = MessageCenter.getInstance(); const instance = MessageCenter.getInstance();
if (instance) { if (instance) {
@@ -18,7 +18,15 @@ export function MessageCenterHandler({
// todo: more specific message for accessing different resources // todo: more specific message for accessing different resources
// todo: error toast style // todo: error toast style
toast('You have no permission to access this workspace'); toast('You have no permission to access this workspace');
getApis().auth.clear(); // todo(himself65): remove dynamic lookup
const affineProvider = dataCenter.providers.find(
p => p.id === 'affine'
);
if (affineProvider && affineProvider instanceof AffineProvider) {
affineProvider.apis.auth.clear();
} else {
console.error('cannot find affine provider, please fix this ASAP');
}
// the status of the app right now is unknown, and it won't help if we let // the status of the app right now is unknown, and it won't help if we let
// the app continue and let the user auth the app. // the app continue and let the user auth the app.
// that's why so we need to reload the page for now. // that's why so we need to reload the page for now.
@@ -30,7 +38,7 @@ export function MessageCenterHandler({
} }
}); });
} }
}, [router]); }, [dataCenter?.providers, router]);
return <>{children}</>; return <>{children}</>;
} }

View File

@@ -78,19 +78,19 @@ const App = ({ Component, pageProps }: AppPropsWithLayout) => {
<ConfirmProvider key="ConfirmProvider" />, <ConfirmProvider key="ConfirmProvider" />,
]} ]}
> >
<MessageCenterHandler>
{NoNeedAppStatePageList.includes(router.route) ? ( {NoNeedAppStatePageList.includes(router.route) ? (
getLayout(<Component {...pageProps} />) getLayout(<Component {...pageProps} />)
) : ( ) : (
<Suspense fallback={<PageLoading />}> <Suspense fallback={<PageLoading />}>
<DataCenterPreloader> <DataCenterPreloader>
<MessageCenterHandler>
<AppDefender> <AppDefender>
{getLayout(<Component {...pageProps} />)} {getLayout(<Component {...pageProps} />)}
</AppDefender> </AppDefender>
</MessageCenterHandler>
</DataCenterPreloader> </DataCenterPreloader>
</Suspense> </Suspense>
)} )}
</MessageCenterHandler>
</ProviderComposer> </ProviderComposer>
</GlobalAppProvider> </GlobalAppProvider>
</> </>

View File

@@ -28,6 +28,7 @@ export const getDataCenter = _initializeDataCenter();
export type { DataCenter }; export type { DataCenter };
export { getLogger } from './logger'; export { getLogger } from './logger';
export * from './message'; export * from './message';
export { AffineProvider } from './provider/affine';
export * from './provider/affine/apis'; export * from './provider/affine/apis';
export * from './types'; export * from './types';
export { WorkspaceUnit } from './workspace-unit'; export { WorkspaceUnit } from './workspace-unit';

View File

@@ -1,5 +1,6 @@
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
import assert from 'assert'; import assert from 'assert';
import { KyInstance } from 'ky/distribution/types/ky';
import { MessageCenter } from '../../message'; import { MessageCenter } from '../../message';
import type { User } from '../../types'; import type { User } from '../../types';
@@ -12,16 +13,12 @@ import type {
} from '../base'; } from '../base';
import { BaseProvider } from '../base'; import { BaseProvider } from '../base';
import type { Apis, WorkspaceDetail } from './apis'; import type { Apis, WorkspaceDetail } from './apis';
// import { IndexedDBProvider } from '../local/indexeddb';
import { getApis, Workspace } from './apis'; import { getApis, Workspace } from './apis';
import { createGoogleAuth } from './apis/google';
import { createAuthClient, createBareClient } from './apis/request';
import { WebsocketClient } from './channel'; import { WebsocketClient } from './channel';
import { WebsocketProvider } from './sync'; import { WebsocketProvider } from './sync';
import { import { createWorkspaceUnit, loadWorkspaceUnit, migrateBlobDB } from './utils';
createBlocksuiteWorkspaceWithAuth,
createWorkspaceUnit,
loadWorkspaceUnit,
migrateBlobDB,
} from './utils';
type ChannelMessage = { type ChannelMessage = {
ws_list: Workspace[]; ws_list: Workspace[];
@@ -39,6 +36,9 @@ const {
} = BlocksuiteWorkspace; } = BlocksuiteWorkspace;
export class AffineProvider extends BaseProvider { export class AffineProvider extends BaseProvider {
private readonly bareClient: KyInstance;
private readonly authClient: KyInstance;
public id = 'affine'; public id = 'affine';
private _wsMap: Map<BlocksuiteWorkspace, WebsocketProvider> = new Map(); private _wsMap: Map<BlocksuiteWorkspace, WebsocketProvider> = new Map();
private _apis: Apis; private _apis: Apis;
@@ -52,7 +52,14 @@ export class AffineProvider extends BaseProvider {
constructor({ apis, ...params }: AffineProviderConstructorParams) { constructor({ apis, ...params }: AffineProviderConstructorParams) {
super(params); super(params);
this._apis = apis || getApis(); this.bareClient = createBareClient('/');
const googleAuth = createGoogleAuth(this.bareClient);
this.authClient = createAuthClient(this.bareClient, googleAuth);
this._apis = apis || getApis(this.bareClient, this.authClient, googleAuth);
}
public get apis() {
return Object.freeze(this._apis);
} }
override async init() { override async init() {
@@ -80,6 +87,7 @@ export class AffineProvider extends BaseProvider {
window.location.host window.location.host
}/api/global/sync/`, }/api/global/sync/`,
this._logger, this._logger,
this._apis.auth,
{ {
params: { params: {
token: this._apis.auth.refresh, token: this._apis.auth.refresh,
@@ -370,7 +378,8 @@ export class AffineProvider extends BaseProvider {
public override async createWorkspace( public override async createWorkspace(
meta: CreateWorkspaceInfoParams meta: CreateWorkspaceInfoParams
): Promise<WorkspaceUnit | undefined> { ): Promise<WorkspaceUnit | undefined> {
const workspaceUnitForUpload = await createWorkspaceUnit({ const workspaceUnitForUpload = await createWorkspaceUnit(
{
id: '', id: '',
name: meta.name, name: meta.name,
avatar: undefined, avatar: undefined,
@@ -379,7 +388,9 @@ export class AffineProvider extends BaseProvider {
memberCount: 1, memberCount: 1,
provider: this.id, provider: this.id,
syncMode: 'core', syncMode: 'core',
}); },
this._apis
);
const { id } = await this._apis.createWorkspace( const { id } = await this._apis.createWorkspace(
new Blob([ new Blob([
encodeStateAsUpdate(workspaceUnitForUpload.blocksuiteWorkspace!.doc) encodeStateAsUpdate(workspaceUnitForUpload.blocksuiteWorkspace!.doc)
@@ -387,7 +398,8 @@ export class AffineProvider extends BaseProvider {
]) ])
); );
const workspaceUnit = await createWorkspaceUnit({ const workspaceUnit = await createWorkspaceUnit(
{
id, id,
name: meta.name, name: meta.name,
avatar: undefined, avatar: undefined,
@@ -396,7 +408,9 @@ export class AffineProvider extends BaseProvider {
memberCount: 1, memberCount: 1,
provider: this.id, provider: this.id,
syncMode: 'core', syncMode: 'core',
}); },
this._apis
);
this._workspaces.add(workspaceUnit); this._workspaces.add(workspaceUnit);
@@ -446,7 +460,8 @@ export class AffineProvider extends BaseProvider {
}); });
await migrateBlobDB(workspaceUnit.id, id); await migrateBlobDB(workspaceUnit.id, id);
const blocksuiteWorkspace = await createBlocksuiteWorkspaceWithAuth(id); const blocksuiteWorkspace =
await this._apis.createBlockSuiteWorkspaceWithAuth(id);
assert(workspaceUnit.blocksuiteWorkspace); assert(workspaceUnit.blocksuiteWorkspace);
await applyUpdate( await applyUpdate(
blocksuiteWorkspace, blocksuiteWorkspace,

View File

@@ -1,11 +1,11 @@
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from 'vitest';
import { Auth } from '../auth'; import { GoogleAuth } from '../google';
describe('class Auth', () => { describe('class Auth', () => {
test('parse tokens', () => { test('parse tokens', () => {
const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NzU2Nzk1MjAsImlkIjo2LCJuYW1lIjoidGVzdCIsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJhdmF0YXJfdXJsIjoiaHR0cHM6Ly90ZXN0LmNvbS9hdmF0YXIiLCJjcmVhdGVkX2F0IjoxNjc1Njc4OTIwMzU4fQ.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`; const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NzU2Nzk1MjAsImlkIjo2LCJuYW1lIjoidGVzdCIsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJhdmF0YXJfdXJsIjoiaHR0cHM6Ly90ZXN0LmNvbS9hdmF0YXIiLCJjcmVhdGVkX2F0IjoxNjc1Njc4OTIwMzU4fQ.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`;
expect(Auth.parseIdToken(tokenString)).toEqual({ expect(GoogleAuth.parseIdToken(tokenString)).toEqual({
avatar_url: 'https://test.com/avatar', avatar_url: 'https://test.com/avatar',
created_at: 1675678920358, created_at: 1675678920358,
email: 'test@gmail.com', email: 'test@gmail.com',
@@ -17,6 +17,6 @@ describe('class Auth', () => {
test('parse invalid tokens', () => { test('parse invalid tokens', () => {
const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.aaa.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`; const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.aaa.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`;
expect(Auth.parseIdToken(tokenString)).toEqual(null); expect(GoogleAuth.parseIdToken(tokenString)).toEqual(null);
}); });
}); });

View File

@@ -8,10 +8,10 @@ import {
signOut, signOut,
} from 'firebase/auth'; } from 'firebase/auth';
import { decode } from 'js-base64'; import { decode } from 'js-base64';
import { KyInstance } from 'ky/distribution/types/ky';
import { getLogger } from '../../../logger'; import { getLogger } from '../../../logger';
import { storage } from '../storage'; import { storage } from '../storage';
import { bareClient } from './request';
export interface AccessTokenMessage { export interface AccessTokenMessage {
created_at: number; created_at: number;
@@ -43,20 +43,24 @@ const AFFINE_LOGIN_STORAGE_KEY = 'affine:login';
* Use refresh token to get a new access token (JWT) * Use refresh token to get a new access token (JWT)
* The returned token also contains the user info payload. * The returned token also contains the user info payload.
*/ */
const doLogin = (params: LoginParams): Promise<LoginResponse> => const createDoLogin =
(bareClient: KyInstance) =>
(params: LoginParams): Promise<LoginResponse> =>
bareClient.post('api/user/token', { json: params }).json(); bareClient.post('api/user/token', { json: params }).json();
export class Auth { export class GoogleAuth {
private readonly _logger; private readonly _logger;
private _accessToken = ''; // idtoken (JWT) private _accessToken = ''; // idtoken (JWT)
private _refreshToken = ''; private _refreshToken = '';
private _user: AccessTokenMessage | null = null; private _user: AccessTokenMessage | null = null;
private _padding?: Promise<LoginResponse>; private _padding?: Promise<LoginResponse>;
private readonly _doLogin: ReturnType<typeof createDoLogin>;
constructor() { constructor(bareClient: KyInstance) {
this._logger = getLogger('token'); this._logger = getLogger('token');
this._logger.enabled = true; this._logger.enabled = true;
this._doLogin = createDoLogin(bareClient);
this.restoreLogin(); this.restoreLogin();
} }
@@ -64,7 +68,7 @@ export class Auth {
setLogin(login: LoginResponse) { setLogin(login: LoginResponse) {
this._accessToken = login.token; this._accessToken = login.token;
this._refreshToken = login.refresh; this._refreshToken = login.refresh;
this._user = Auth.parseIdToken(this._accessToken); this._user = GoogleAuth.parseIdToken(this._accessToken);
this.triggerChange(this._user); this.triggerChange(this._user);
this.storeLogin(); this.storeLogin();
@@ -94,14 +98,14 @@ export class Auth {
} }
async initToken(token: string) { async initToken(token: string) {
const res = await doLogin({ token, type: 'Google' }); const res = await this._doLogin({ token, type: 'Google' });
this.setLogin(res); this.setLogin(res);
return this._user; return this._user;
} }
async refreshToken(refreshToken?: string) { async refreshToken(refreshToken?: string) {
if (!this._padding) { if (!this._padding) {
this._padding = doLogin({ this._padding = this._doLogin({
type: 'Refresh', type: 'Refresh',
token: refreshToken || this._refreshToken, token: refreshToken || this._refreshToken,
}); });
@@ -181,9 +185,11 @@ export class Auth {
} }
} }
export const auth = new Auth(); export function createGoogleAuth(bareAuth: KyInstance): GoogleAuth {
return new GoogleAuth(bareAuth);
}
export const getAuthorizer = () => { export const getAuthorizer = (googleAuth: GoogleAuth) => {
let _firebaseAuth: FirebaseAuth | null = null; let _firebaseAuth: FirebaseAuth | null = null;
const logger = getLogger('authorizer'); const logger = getLogger('authorizer');
@@ -225,7 +231,7 @@ export const getAuthorizer = () => {
const idToken = await getToken(); const idToken = await getToken();
let loginUser: AccessTokenMessage | null = null; let loginUser: AccessTokenMessage | null = null;
if (idToken) { if (idToken) {
loginUser = await auth.initToken(idToken); loginUser = await googleAuth.initToken(idToken);
} else { } else {
const firebaseAuth = getAuth(); const firebaseAuth = getAuth();
if (firebaseAuth) { if (firebaseAuth) {
@@ -237,7 +243,7 @@ export const getAuthorizer = () => {
}); });
const user = await signInWithPopup(firebaseAuth, googleAuthProvider); const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
const idToken = await user.user.getIdToken(); const idToken = await user.user.getIdToken();
loginUser = await auth.initToken(idToken); loginUser = await googleAuth.initToken(idToken);
} }
} }
return loginUser; return loginUser;

View File

@@ -1,10 +1,10 @@
// export { token } from './token.js'; export type { Callback } from './google';
export type { Callback } from './auth';
import { getAuthorizer } from './auth'; import { KyInstance } from 'ky/distribution/types/ky';
import { auth } from './auth';
import * as user from './user'; import { createGoogleAuth, getAuthorizer, GoogleAuth } from './google';
import * as workspace from './workspace'; import { createUserApis } from './user';
import { createWorkspaceApis } from './workspace';
// See https://twitter.com/mattpocockuk/status/1622730173446557697 // See https://twitter.com/mattpocockuk/status/1622730173446557697
// TODO: move to ts utils? // TODO: move to ts utils?
@@ -14,27 +14,31 @@ type Prettify<T> = {
} & {}; } & {};
export type Apis = Prettify< export type Apis = Prettify<
typeof user & ReturnType<typeof createUserApis> &
Omit<typeof workspace, 'WorkspaceType' | 'PermissionType'> & { ReturnType<typeof createWorkspaceApis> & {
signInWithGoogle: ReturnType<typeof getAuthorizer>[0]; signInWithGoogle: ReturnType<typeof getAuthorizer>[0];
onAuthStateChanged: ReturnType<typeof getAuthorizer>[1]; onAuthStateChanged: ReturnType<typeof getAuthorizer>[1];
signOutFirebase: ReturnType<typeof getAuthorizer>[2]; signOutFirebase: ReturnType<typeof getAuthorizer>[2];
} & { auth: typeof auth } } & { auth: ReturnType<typeof createGoogleAuth> }
>; >;
export const getApis = (): Apis => { export const getApis = (
bareClient: KyInstance,
authClient: KyInstance,
googleAuth: GoogleAuth
): Apis => {
const [signInWithGoogle, onAuthStateChanged, signOutFirebase] = const [signInWithGoogle, onAuthStateChanged, signOutFirebase] =
getAuthorizer(); getAuthorizer(googleAuth);
return { return {
...user, ...createUserApis(bareClient, authClient),
...workspace, ...createWorkspaceApis(bareClient, authClient, googleAuth),
signInWithGoogle, signInWithGoogle,
signOutFirebase, signOutFirebase,
onAuthStateChanged, onAuthStateChanged,
auth, auth: googleAuth,
}; };
}; };
export type { AccessTokenMessage } from './auth'; export type { AccessTokenMessage } from './google';
export type { Member, Workspace, WorkspaceDetail } from './workspace'; export type { Member, Workspace, WorkspaceDetail } from './workspace';
export * from './workspace'; export * from './workspace';

View File

@@ -1,7 +1,7 @@
import ky from 'ky-universal'; import ky from 'ky-universal';
import { MessageCenter } from '../../../message'; import { MessageCenter } from '../../../message';
import { auth } from './auth'; import { GoogleAuth } from './google';
type KyInstance = typeof ky; type KyInstance = typeof ky;
@@ -9,8 +9,9 @@ const messageCenter = MessageCenter.getInstance();
const _sendMessage = messageCenter.getMessageSender('affine'); const _sendMessage = messageCenter.getMessageSender('affine');
export const bareClient: KyInstance = ky.extend({ export const createBareClient = (prefixUrl: string): KyInstance =>
prefixUrl: '/', ky.extend({
prefixUrl: prefixUrl,
retry: 1, retry: 1,
// todo: report timeout error // todo: report timeout error
timeout: 60000, timeout: 60000,
@@ -25,25 +26,29 @@ export const bareClient: KyInstance = ky.extend({
}, },
], ],
}, },
}); });
const refreshTokenIfExpired = async () => { const refreshTokenIfExpired = async (googleAuth: GoogleAuth) => {
if (auth.isLogin && auth.isExpired) { if (googleAuth.isLogin && googleAuth.isExpired) {
try { try {
await auth.refreshToken(); await googleAuth.refreshToken();
} catch (err) { } catch (err) {
return new Response('Unauthorized', { status: 401 }); return new Response('Unauthorized', { status: 401 });
} }
} }
}; };
export const client: KyInstance = bareClient.extend({ export const createAuthClient = (
bareClient: KyInstance,
googleAuth: GoogleAuth
): KyInstance =>
bareClient.extend({
hooks: { hooks: {
beforeRequest: [ beforeRequest: [
async request => { async request => {
if (auth.isLogin) { if (googleAuth.isLogin) {
await refreshTokenIfExpired(); await refreshTokenIfExpired(googleAuth);
request.headers.set('Authorization', auth.token); request.headers.set('Authorization', googleAuth.token);
} else { } else {
return new Response('Unauthorized', { status: 401 }); return new Response('Unauthorized', { status: 401 });
} }
@@ -52,9 +57,9 @@ export const client: KyInstance = bareClient.extend({
beforeRetry: [ beforeRetry: [
async ({ request }) => { async ({ request }) => {
await refreshTokenIfExpired(); await refreshTokenIfExpired(googleAuth);
request.headers.set('Authorization', auth.token); request.headers.set('Authorization', googleAuth.token);
}, },
], ],
}, },
}); });

View File

@@ -1,4 +1,4 @@
import { client } from './request'; import { KyInstance } from 'ky/distribution/types/ky';
export interface GetUserByEmailParams { export interface GetUserByEmailParams {
email: string; email: string;
@@ -13,9 +13,13 @@ export interface User {
create_at: string; create_at: string;
} }
export async function getUserByEmail( export function createUserApis(bareClient: KyInstance, authClient: KyInstance) {
return {
getUserByEmail: async (
params: GetUserByEmailParams params: GetUserByEmailParams
): Promise<User[] | null> { ): Promise<User[] | null> => {
const searchParams = new URLSearchParams({ ...params }); const searchParams = new URLSearchParams({ ...params });
return client.get('api/user', { searchParams }).json<User[] | null>(); return authClient.get('api/user', { searchParams }).json<User[] | null>();
},
} as const;
} }

View File

@@ -1,5 +1,8 @@
import { KyInstance } from 'ky/distribution/types/ky';
import { MessageCenter } from '../../../message'; import { MessageCenter } from '../../../message';
import { bareClient, client } from './request'; import { createBlocksuiteWorkspace as _createBlocksuiteWorkspace } from '../../../utils';
import { GoogleAuth } from './google';
import type { User } from './user'; import type { User } from './user';
const messageCenter = MessageCenter.getInstance(); const messageCenter = MessageCenter.getInstance();
@@ -15,6 +18,7 @@ class RequestError extends Error {
this.cause = cause; this.cause = cause;
} }
} }
export interface GetWorkspaceDetailParams { export interface GetWorkspaceDetailParams {
id: string; id: string;
} }
@@ -39,37 +43,11 @@ export interface Workspace {
create_at: number; create_at: number;
} }
export async function getWorkspaces(): Promise<Workspace[]> {
try {
return await client
.get('api/workspace', {
headers: {
'Cache-Control': 'no-cache',
},
})
.json();
} catch (error) {
sendMessage(messageCode.loadListFailed);
throw new RequestError('load list failed', error);
}
}
export interface WorkspaceDetail extends Workspace { export interface WorkspaceDetail extends Workspace {
owner: User; owner: User;
member_count: number; member_count: number;
} }
export async function getWorkspaceDetail(
params: GetWorkspaceDetailParams
): Promise<WorkspaceDetail | null> {
try {
return await client.get(`api/workspace/${params.id}`).json();
} catch (error) {
sendMessage(messageCode.getDetailFailed);
throw new RequestError('get detail failed', error);
}
}
export interface Permission { export interface Permission {
id: string; id: string;
type: PermissionType; type: PermissionType;
@@ -97,42 +75,93 @@ export interface GetWorkspaceMembersParams {
id: string; id: string;
} }
export async function getWorkspaceMembers(
params: GetWorkspaceDetailParams
): Promise<Member[]> {
try {
return await client.get(`api/workspace/${params.id}/permission`).json();
} catch (error) {
sendMessage(messageCode.getMembersFailed);
throw new RequestError('get members failed', error);
}
}
export interface CreateWorkspaceParams { export interface CreateWorkspaceParams {
name: string; name: string;
} }
export async function createWorkspace(
encodedYDoc: Blob
): Promise<{ id: string }> {
try {
return await client.post('api/workspace', { body: encodedYDoc }).json();
} catch (error) {
sendMessage(messageCode.createWorkspaceFailed);
throw new RequestError('create workspace failed', error);
}
}
export interface UpdateWorkspaceParams { export interface UpdateWorkspaceParams {
id: string; id: string;
public: boolean; public: boolean;
} }
export async function updateWorkspace( export interface DeleteWorkspaceParams {
params: UpdateWorkspaceParams id: string;
): Promise<{ public: boolean | null }> { }
export interface InviteMemberParams {
id: string;
email: string;
}
export interface RemoveMemberParams {
permissionId: number;
}
export interface AcceptInvitingParams {
invitingCode: string;
}
export interface LeaveWorkspaceParams {
id: number | string;
}
export function createWorkspaceApis(
bareClient: KyInstance,
authClient: KyInstance,
googleAuth: GoogleAuth
) {
return {
getWorkspaces: async (): Promise<Workspace[]> => {
try { try {
return await client return await authClient
.get('api/workspace', {
headers: {
'Cache-Control': 'no-cache',
},
})
.json();
} catch (error) {
sendMessage(messageCode.loadListFailed);
throw new RequestError('load list failed', error);
}
},
getWorkspaceDetail: async (
params: GetWorkspaceDetailParams
): Promise<WorkspaceDetail | null> => {
try {
return await authClient.get(`api/workspace/${params.id}`).json();
} catch (error) {
sendMessage(messageCode.getDetailFailed);
throw new RequestError('get detail failed', error);
}
},
getWorkspaceMembers: async (
params: GetWorkspaceDetailParams
): Promise<Member[]> => {
try {
return await authClient
.get(`api/workspace/${params.id}/permission`)
.json();
} catch (error) {
sendMessage(messageCode.getMembersFailed);
throw new RequestError('get members failed', error);
}
},
createWorkspace: async (encodedYDoc: Blob): Promise<{ id: string }> => {
try {
return await authClient
.post('api/workspace', { body: encodedYDoc })
.json();
} catch (error) {
sendMessage(messageCode.createWorkspaceFailed);
throw new RequestError('create workspace failed', error);
}
},
updateWorkspace: async (
params: UpdateWorkspaceParams
): Promise<{ public: boolean | null }> => {
try {
return await authClient
.post(`api/workspace/${params.id}`, { .post(`api/workspace/${params.id}`, {
json: { json: {
public: params.public, public: params.public,
@@ -143,34 +172,22 @@ export async function updateWorkspace(
sendMessage(messageCode.updateWorkspaceFailed); sendMessage(messageCode.updateWorkspaceFailed);
throw new RequestError('update workspace failed', error); throw new RequestError('update workspace failed', error);
} }
} },
deleteWorkspace: async (params: DeleteWorkspaceParams): Promise<void> => {
export interface DeleteWorkspaceParams {
id: string;
}
export async function deleteWorkspace(
params: DeleteWorkspaceParams
): Promise<void> {
try { try {
await client.delete(`api/workspace/${params.id}`); await authClient.delete(`api/workspace/${params.id}`);
} catch (error) { } catch (error) {
sendMessage(messageCode.deleteWorkspaceFailed); sendMessage(messageCode.deleteWorkspaceFailed);
throw new RequestError('delete workspace failed', error); throw new RequestError('delete workspace failed', error);
} }
} },
export interface InviteMemberParams { /**
id: string;
email: string;
}
/**
* Notice: Only support normal(contrast to private) workspace. * Notice: Only support normal(contrast to private) workspace.
*/ */
export async function inviteMember(params: InviteMemberParams): Promise<void> { inviteMember: async (params: InviteMemberParams): Promise<void> => {
try { try {
await client.post(`api/workspace/${params.id}/permission`, { await authClient.post(`api/workspace/${params.id}/permission`, {
json: { json: {
email: params.email, email: params.email,
}, },
@@ -179,28 +196,18 @@ export async function inviteMember(params: InviteMemberParams): Promise<void> {
sendMessage(messageCode.inviteMemberFailed); sendMessage(messageCode.inviteMemberFailed);
throw new RequestError('invite member failed', error); throw new RequestError('invite member failed', error);
} }
} },
removeMember: async (params: RemoveMemberParams): Promise<void> => {
export interface RemoveMemberParams {
permissionId: number;
}
export async function removeMember(params: RemoveMemberParams): Promise<void> {
try { try {
await client.delete(`api/permission/${params.permissionId}`); await authClient.delete(`api/permission/${params.permissionId}`);
} catch (error) { } catch (error) {
sendMessage(messageCode.removeMemberFailed); sendMessage(messageCode.removeMemberFailed);
throw new RequestError('remove member failed', error); throw new RequestError('remove member failed', error);
} }
} },
acceptInviting: async (
export interface AcceptInvitingParams {
invitingCode: string;
}
export async function acceptInviting(
params: AcceptInvitingParams params: AcceptInvitingParams
): Promise<Permission> { ): Promise<Permission> => {
try { try {
return await bareClient return await bareClient
.post(`api/invitation/${params.invitingCode}`) .post(`api/invitation/${params.invitingCode}`)
@@ -209,49 +216,53 @@ export async function acceptInviting(
sendMessage(messageCode.acceptInvitingFailed); sendMessage(messageCode.acceptInvitingFailed);
throw new RequestError('accept inviting failed', error); throw new RequestError('accept inviting failed', error);
} }
} },
uploadBlob: async (params: { blob: Blob }): Promise<string> => {
export async function uploadBlob(params: { blob: Blob }): Promise<string> { return authClient.put('api/blob', { body: params.blob }).text();
return client.put('api/blob', { body: params.blob }).text(); },
} getBlob: async (params: { blobId: string }): Promise<ArrayBuffer> => {
export async function getBlob(params: {
blobId: string;
}): Promise<ArrayBuffer> {
try { try {
return await client.get(`api/blob/${params.blobId}`).arrayBuffer(); return await authClient.get(`api/blob/${params.blobId}`).arrayBuffer();
} catch (error) { } catch (error) {
sendMessage(messageCode.getBlobFailed); sendMessage(messageCode.getBlobFailed);
throw new RequestError('get blob failed', error); throw new RequestError('get blob failed', error);
} }
} },
leaveWorkspace: async ({ id }: LeaveWorkspaceParams) => {
export interface LeaveWorkspaceParams {
id: number | string;
}
export async function leaveWorkspace({ id }: LeaveWorkspaceParams) {
try { try {
await client.delete(`api/workspace/${id}/permission`); await authClient.delete(`api/workspace/${id}/permission`);
} catch (error) { } catch (error) {
sendMessage(messageCode.leaveWorkspaceFailed); sendMessage(messageCode.leaveWorkspaceFailed);
throw new RequestError('leave workspace failed', error); throw new RequestError('leave workspace failed', error);
} }
} },
downloadWorkspace: async (
export async function downloadWorkspace(
workspaceId: string, workspaceId: string,
published = false published = false
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> => {
try { try {
if (published) { if (published) {
return await bareClient return await bareClient
.get(`api/public/doc/${workspaceId}`) .get(`api/public/doc/${workspaceId}`)
.arrayBuffer(); .arrayBuffer();
} }
return await client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer(); return await authClient
.get(`api/workspace/${workspaceId}/doc`)
.arrayBuffer();
} catch (error) { } catch (error) {
sendMessage(messageCode.downloadWorkspaceFailed); sendMessage(messageCode.downloadWorkspaceFailed);
throw new RequestError('download workspace failed', error); throw new RequestError('download workspace failed', error);
} }
},
createBlockSuiteWorkspaceWithAuth: async (newWorkspaceId: string) => {
if (googleAuth.isExpired && googleAuth.isLogin) {
await googleAuth.refreshToken();
}
return _createBlocksuiteWorkspace(newWorkspaceId, {
blobOptionsGetter: (k: string) =>
// token could be expired
({ api: '/api/workspace', token: googleAuth.token }[k]),
});
},
} as const;
} }

View File

@@ -2,7 +2,7 @@ import * as url from 'lib0/url';
import * as websocket from 'lib0/websocket'; import * as websocket from 'lib0/websocket';
import { Logger } from '../../types'; import { Logger } from '../../types';
import { auth } from './apis/auth'; import { GoogleAuth } from './apis/google';
const RECONNECT_INTERVAL_TIME = 500; const RECONNECT_INTERVAL_TIME = 500;
const MAX_RECONNECT_TIMES = 50; const MAX_RECONNECT_TIMES = 50;
@@ -11,9 +11,11 @@ export class WebsocketClient extends websocket.WebsocketClient {
public shouldReconnect = false; public shouldReconnect = false;
private _logger: Logger; private _logger: Logger;
private _retryTimes = 0; private _retryTimes = 0;
private _auth: GoogleAuth;
constructor( constructor(
serverUrl: string, serverUrl: string,
logger: Logger, logger: Logger,
auth: GoogleAuth,
options?: ConstructorParameters<typeof websocket.WebsocketClient>[1] & { options?: ConstructorParameters<typeof websocket.WebsocketClient>[1] & {
params: Record<string, string>; params: Record<string, string>;
} }
@@ -27,6 +29,7 @@ export class WebsocketClient extends websocket.WebsocketClient {
const newUrl = const newUrl =
serverUrl + '/' + (encodedParams.length === 0 ? '' : '?' + encodedParams); serverUrl + '/' + (encodedParams.length === 0 ? '' : '?' + encodedParams);
super(newUrl, options); super(newUrl, options);
this._auth = auth;
this._logger = logger; this._logger = logger;
this._setupChannel(); this._setupChannel();
} }
@@ -41,7 +44,7 @@ export class WebsocketClient extends websocket.WebsocketClient {
this.on('disconnect', ({ error }: { error: Error }) => { this.on('disconnect', ({ error }: { error: Error }) => {
if (error) { if (error) {
// Try reconnect if connect error has occurred // Try reconnect if connect error has occurred
if (this.shouldReconnect && auth.isLogin && !this.connected) { if (this.shouldReconnect && this._auth.isLogin && !this.connected) {
try { try {
setTimeout(() => { setTimeout(() => {
if (this._retryTimes <= MAX_RECONNECT_TIMES) { if (this._retryTimes <= MAX_RECONNECT_TIMES) {

View File

@@ -1,29 +1,16 @@
import { createBlocksuiteWorkspace as _createBlocksuiteWorkspace } from '../../utils';
import { applyUpdate } from '../../utils'; import { applyUpdate } from '../../utils';
import type { WorkspaceUnitCtorParams } from '../../workspace-unit'; import type { WorkspaceUnitCtorParams } from '../../workspace-unit';
import { WorkspaceUnit } from '../../workspace-unit'; import { WorkspaceUnit } from '../../workspace-unit';
import { setDefaultAvatar } from '../utils'; import { setDefaultAvatar } from '../utils';
import type { Apis } from './apis'; import type { Apis } from './apis';
import { auth } from './apis/auth';
import { getDatabase } from './idb-kv'; import { getDatabase } from './idb-kv';
export const createBlocksuiteWorkspaceWithAuth = async (id: string) => {
if (auth.isExpired && auth.isLogin) {
await auth.refreshToken();
}
return _createBlocksuiteWorkspace(id, {
blobOptionsGetter: (k: string) =>
// token could be expired
({ api: '/api/workspace', token: auth.token }[k]),
});
};
export const loadWorkspaceUnit = async ( export const loadWorkspaceUnit = async (
params: WorkspaceUnitCtorParams, params: WorkspaceUnitCtorParams,
apis: Apis apis: Apis
) => { ) => {
const workspaceUnit = new WorkspaceUnit(params); const workspaceUnit = new WorkspaceUnit(params);
const blocksuiteWorkspace = await createBlocksuiteWorkspaceWithAuth( const blocksuiteWorkspace = await apis.createBlockSuiteWorkspaceWithAuth(
workspaceUnit.id workspaceUnit.id
); );
@@ -54,10 +41,13 @@ export const loadWorkspaceUnit = async (
return workspaceUnit; return workspaceUnit;
}; };
export const createWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => { export const createWorkspaceUnit = async (
params: WorkspaceUnitCtorParams,
apis: Apis
) => {
const workspaceUnit = new WorkspaceUnit(params); const workspaceUnit = new WorkspaceUnit(params);
const blocksuiteWorkspace = await createBlocksuiteWorkspaceWithAuth( const blocksuiteWorkspace = await apis.createBlockSuiteWorkspaceWithAuth(
workspaceUnit.id workspaceUnit.id
); );