mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-06 03:49:56 +08:00
Merge remote-tracking branch 'refs/remotes/origin/feat/datacenter'
Conflicts: packages/app/src/components/workspace-modal/languageMenu.tsx packages/data-center/package.json pnpm-lock.yaml
This commit is contained in:
@@ -23,7 +23,7 @@ export class DataCenter {
|
||||
private readonly _workspaceUnitCollection = new WorkspaceUnitCollection();
|
||||
private readonly _logger = getLogger('dc');
|
||||
private _workspaceInstances: Map<string, BlocksuiteWorkspace> = new Map();
|
||||
private _messageCenter = new MessageCenter();
|
||||
private _messageCenter = MessageCenter.getInstance();
|
||||
/**
|
||||
* A mainProvider must exist as the only data trustworthy source.
|
||||
*/
|
||||
@@ -159,7 +159,9 @@ export class DataCenter {
|
||||
* @param {string} workspaceId workspace id
|
||||
* @returns {Promise<WorkspaceUnit>}
|
||||
*/
|
||||
public async loadWorkspace(workspaceId: string) {
|
||||
public async loadWorkspace(
|
||||
workspaceId: string
|
||||
): Promise<WorkspaceUnit | null> {
|
||||
const workspaceUnit = this._workspaceUnitCollection.find(workspaceId);
|
||||
assert(workspaceUnit, 'Workspace not found');
|
||||
const currentProvider = this.providerMap.get(workspaceUnit.provider);
|
||||
@@ -425,13 +427,14 @@ export class DataCenter {
|
||||
/**
|
||||
* accept invitation
|
||||
* @param {string} inviteCode
|
||||
* @returns {Promise<Permission | null>} permission
|
||||
*/
|
||||
async acceptInvitation(inviteCode: string, providerStr = 'affine') {
|
||||
const provider = this.providerMap.get(providerStr);
|
||||
if (provider) {
|
||||
return await provider.acceptInvitation(inviteCode);
|
||||
}
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
|
||||
onMessage(cb: (message: Message) => void) {
|
||||
|
||||
@@ -1,3 +1,61 @@
|
||||
export enum MessageCode {
|
||||
loginError,
|
||||
noPermission,
|
||||
loadListFailed,
|
||||
getDetailFailed,
|
||||
createWorkspaceFailed,
|
||||
getMembersFailed,
|
||||
updateWorkspaceFailed,
|
||||
deleteWorkspaceFailed,
|
||||
inviteMemberFailed,
|
||||
removeMemberFailed,
|
||||
acceptInvitingFailed,
|
||||
getBlobFailed,
|
||||
leaveWorkspaceFailed,
|
||||
downloadWorkspaceFailed,
|
||||
}
|
||||
|
||||
export const messages = {
|
||||
[MessageCode.loginError]: {
|
||||
message: 'Login failed',
|
||||
},
|
||||
[MessageCode.noPermission]: {
|
||||
message: 'No permission',
|
||||
},
|
||||
[MessageCode.loadListFailed]: {
|
||||
message: 'Load list failed',
|
||||
},
|
||||
[MessageCode.getDetailFailed]: {
|
||||
message: 'Get detail failed',
|
||||
},
|
||||
[MessageCode.createWorkspaceFailed]: {
|
||||
message: 'Create workspace failed',
|
||||
},
|
||||
[MessageCode.getMembersFailed]: {
|
||||
message: 'Get members failed',
|
||||
},
|
||||
[MessageCode.updateWorkspaceFailed]: {
|
||||
message: 'Update workspace failed',
|
||||
},
|
||||
[MessageCode.deleteWorkspaceFailed]: {
|
||||
message: 'Delete workspace failed',
|
||||
},
|
||||
[MessageCode.inviteMemberFailed]: {
|
||||
message: 'Invite member failed',
|
||||
},
|
||||
[MessageCode.removeMemberFailed]: {
|
||||
message: 'Remove member failed',
|
||||
},
|
||||
[MessageCode.acceptInvitingFailed]: {
|
||||
message: 'Accept inviting failed',
|
||||
},
|
||||
[MessageCode.getBlobFailed]: {
|
||||
message: 'Get blob failed',
|
||||
},
|
||||
[MessageCode.leaveWorkspaceFailed]: {
|
||||
message: 'Leave workspace failed',
|
||||
},
|
||||
[MessageCode.downloadWorkspaceFailed]: {
|
||||
message: 'Download workspace failed',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
import { Observable } from 'lib0/observable';
|
||||
import { Message } from '../types';
|
||||
import { MessageCode } from './code.js';
|
||||
import { MessageCode, messages } from './code.js';
|
||||
|
||||
export class MessageCenter extends Observable<string> {
|
||||
private _messages: Record<number, Omit<Message, 'provider' | 'code'>> =
|
||||
messages;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public send(message: MessageCode) {
|
||||
this.emit('message', [message]);
|
||||
static instance: MessageCenter;
|
||||
|
||||
static getInstance() {
|
||||
if (!MessageCenter.instance) {
|
||||
MessageCenter.instance = new MessageCenter();
|
||||
}
|
||||
return MessageCenter.instance;
|
||||
}
|
||||
|
||||
static messageCode = MessageCode;
|
||||
|
||||
public getMessageSender(provider: string) {
|
||||
return this._send.bind(this, provider);
|
||||
}
|
||||
|
||||
private _send(provider: string, messageCode: MessageCode) {
|
||||
this.emit('message', [
|
||||
{ ...this._messages[messageCode], provider, code: messageCode },
|
||||
]);
|
||||
}
|
||||
|
||||
public onMessage(callback: (message: Message) => void) {
|
||||
this.on('message', callback);
|
||||
}
|
||||
|
||||
private messages: Record<number, Message> = {
|
||||
[MessageCode.loginError]: {
|
||||
code: MessageCode.loginError,
|
||||
message: 'Login failed',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { BaseProvider } from '../base.js';
|
||||
import type {
|
||||
ProviderConstructorParams,
|
||||
CreateWorkspaceInfoParams,
|
||||
WorkspaceMeta0,
|
||||
} from '../base';
|
||||
import type { User } from '../../types';
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
@@ -12,18 +11,13 @@ import { WebsocketProvider } from './sync.js';
|
||||
// import { IndexedDBProvider } from '../local/indexeddb';
|
||||
import { getApis, Workspace } from './apis/index.js';
|
||||
import type { Apis, WorkspaceDetail, Callback } from './apis';
|
||||
import { setDefaultAvatar } from '../utils.js';
|
||||
import { MessageCode } from '../../message/index.js';
|
||||
import { token } from './apis/token.js';
|
||||
import { WebsocketClient } from './channel';
|
||||
import {
|
||||
loadWorkspaceUnit,
|
||||
createWorkspaceUnit,
|
||||
syncToCloud,
|
||||
} from './utils.js';
|
||||
import { loadWorkspaceUnit, createWorkspaceUnit } from './utils.js';
|
||||
import { WorkspaceUnit } from '../../workspace-unit.js';
|
||||
import { createBlocksuiteWorkspace, applyUpdate } from '../../utils/index.js';
|
||||
import type { SyncMode } from '../../workspace-unit';
|
||||
import { MessageCenter } from '../../message/index.js';
|
||||
|
||||
type ChannelMessage = {
|
||||
ws_list: Workspace[];
|
||||
@@ -42,9 +36,8 @@ const {
|
||||
|
||||
export class AffineProvider extends BaseProvider {
|
||||
public id = 'affine';
|
||||
private _workspacesCache: Map<string, BlocksuiteWorkspace> = new Map();
|
||||
private _onTokenRefresh?: Callback = undefined;
|
||||
private _wsMap: Map<string, WebsocketProvider> = new Map();
|
||||
private _wsMap: Map<BlocksuiteWorkspace, WebsocketProvider> = new Map();
|
||||
private _apis: Apis;
|
||||
private _channel?: WebsocketClient;
|
||||
// private _idbMap: Map<string, IndexedDBProvider> = new Map();
|
||||
@@ -105,11 +98,23 @@ export class AffineProvider extends BaseProvider {
|
||||
});
|
||||
}
|
||||
|
||||
private _handlerAffineListMessage({ ws_details, metadata }: ChannelMessage) {
|
||||
private async _handlerAffineListMessage({
|
||||
ws_details,
|
||||
metadata,
|
||||
}: ChannelMessage) {
|
||||
this._logger('receive server message');
|
||||
Object.entries(ws_details).forEach(async ([id, detail]) => {
|
||||
const addedWorkspaces: WorkspaceUnit[] = [];
|
||||
const removeWorkspaceList = this._workspaces.list().map(w => w.id);
|
||||
for (const [id, detail] of Object.entries(ws_details)) {
|
||||
const { name, avatar } = metadata[id];
|
||||
assert(name);
|
||||
const index = removeWorkspaceList.indexOf(id);
|
||||
if (index !== -1) {
|
||||
removeWorkspaceList.splice(index, 1);
|
||||
}
|
||||
assert(
|
||||
name,
|
||||
'workspace name not found by id when receive server message'
|
||||
);
|
||||
const workspace = {
|
||||
name: name,
|
||||
avatar,
|
||||
@@ -121,26 +126,31 @@ export class AffineProvider extends BaseProvider {
|
||||
},
|
||||
published: detail.public,
|
||||
memberCount: detail.member_count,
|
||||
provider: 'affine',
|
||||
provider: this.id,
|
||||
syncMode: 'core' as SyncMode,
|
||||
};
|
||||
if (this._workspaces.get(id)) {
|
||||
// update workspaces
|
||||
this._workspaces.update(id, workspace);
|
||||
} else {
|
||||
const workspaceUnit = await loadWorkspaceUnit(
|
||||
{ id, ...workspace },
|
||||
this._apis
|
||||
);
|
||||
this._workspaces.add(workspaceUnit);
|
||||
addedWorkspaces.push(workspaceUnit);
|
||||
}
|
||||
});
|
||||
}
|
||||
// add workspaces
|
||||
this._workspaces.add(addedWorkspaces);
|
||||
// remove workspaces
|
||||
this._workspaces.remove(removeWorkspaceList);
|
||||
}
|
||||
|
||||
private _getWebsocketProvider(workspace: BlocksuiteWorkspace) {
|
||||
const { doc, room } = workspace;
|
||||
assert(room);
|
||||
assert(doc);
|
||||
let ws = this._wsMap.get(room);
|
||||
let ws = this._wsMap.get(workspace);
|
||||
if (!ws) {
|
||||
const wsUrl = `${
|
||||
window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
@@ -148,7 +158,7 @@ export class AffineProvider extends BaseProvider {
|
||||
ws = new WebsocketProvider(wsUrl, room, doc, {
|
||||
params: { token: this._apis.token.refresh },
|
||||
});
|
||||
this._wsMap.set(room, ws);
|
||||
this._wsMap.set(workspace, ws);
|
||||
}
|
||||
return ws;
|
||||
}
|
||||
@@ -175,8 +185,8 @@ export class AffineProvider extends BaseProvider {
|
||||
this.linkLocal(workspace);
|
||||
const ws = this._getWebsocketProvider(workspace);
|
||||
// close all websocket links
|
||||
Array.from(this._wsMap.entries()).forEach(([id, ws]) => {
|
||||
if (id !== room) {
|
||||
Array.from(this._wsMap.entries()).forEach(([blocksuiteWorkspace, ws]) => {
|
||||
if (blocksuiteWorkspace !== workspace) {
|
||||
ws.disconnect();
|
||||
}
|
||||
});
|
||||
@@ -207,7 +217,7 @@ export class AffineProvider extends BaseProvider {
|
||||
owner: undefined,
|
||||
published: w.public,
|
||||
memberCount: 1,
|
||||
provider: 'affine',
|
||||
provider: this.id,
|
||||
syncMode: 'core',
|
||||
},
|
||||
this._apis
|
||||
@@ -232,14 +242,12 @@ export class AffineProvider extends BaseProvider {
|
||||
this._connectChannel();
|
||||
}
|
||||
if (!user) {
|
||||
this._messageCenter.send(MessageCode.loginError);
|
||||
this._sendMessage(MessageCenter.messageCode.loginError);
|
||||
}
|
||||
}
|
||||
|
||||
public override async getUserInfo(): Promise<User | undefined> {
|
||||
await this.init();
|
||||
const user = this._apis.token.user;
|
||||
await this.init;
|
||||
return user
|
||||
? {
|
||||
id: user.id,
|
||||
@@ -258,23 +266,29 @@ export class AffineProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
public override async clear(): Promise<void> {
|
||||
for (const w of this._workspacesCache.values()) {
|
||||
if (w.room) {
|
||||
for (const w of this._workspaces.list()) {
|
||||
if (w.id) {
|
||||
try {
|
||||
await this.deleteWorkspace(w.room);
|
||||
this._workspaces.remove(w.room);
|
||||
await this.deleteWorkspace(w.id);
|
||||
this._workspaces.remove(w.id);
|
||||
} catch (e) {
|
||||
this._logger('has a problem of delete workspace ', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._workspacesCache.clear();
|
||||
this._workspaces.clear();
|
||||
}
|
||||
|
||||
public override async closeWorkspace(id: string) {
|
||||
// const idb = this._idbMap.get(id);
|
||||
// idb?.destroy();
|
||||
const ws = this._wsMap.get(id);
|
||||
const workspaceUnit = this._workspaces.get(id);
|
||||
const ws = workspaceUnit?.blocksuiteWorkspace
|
||||
? this._wsMap.get(workspaceUnit?.blocksuiteWorkspace)
|
||||
: null;
|
||||
if (!ws) {
|
||||
console.error('close workspace websocket which not exist.');
|
||||
}
|
||||
ws?.disconnect();
|
||||
}
|
||||
|
||||
@@ -305,7 +319,22 @@ export class AffineProvider extends BaseProvider {
|
||||
public override async createWorkspace(
|
||||
meta: CreateWorkspaceInfoParams
|
||||
): Promise<WorkspaceUnit | undefined> {
|
||||
const { id } = await this._apis.createWorkspace(meta);
|
||||
const workspaceUnitForUpload = await createWorkspaceUnit({
|
||||
id: '',
|
||||
name: meta.name,
|
||||
avatar: undefined,
|
||||
owner: await this.getUserInfo(),
|
||||
published: false,
|
||||
memberCount: 1,
|
||||
provider: this.id,
|
||||
syncMode: 'core',
|
||||
});
|
||||
const { id } = await this._apis.createWorkspace(
|
||||
new Blob([
|
||||
encodeStateAsUpdate(workspaceUnitForUpload.blocksuiteWorkspace!.doc)
|
||||
.buffer,
|
||||
])
|
||||
);
|
||||
|
||||
const workspaceUnit = await createWorkspaceUnit({
|
||||
id,
|
||||
@@ -314,14 +343,10 @@ export class AffineProvider extends BaseProvider {
|
||||
owner: await this.getUserInfo(),
|
||||
published: false,
|
||||
memberCount: 1,
|
||||
provider: 'affine',
|
||||
provider: this.id,
|
||||
syncMode: 'core',
|
||||
});
|
||||
|
||||
await syncToCloud(
|
||||
workspaceUnit.blocksuiteWorkspace!,
|
||||
this._apis.token.refresh
|
||||
);
|
||||
this._workspaces.add(workspaceUnit);
|
||||
|
||||
return workspaceUnit;
|
||||
@@ -349,9 +374,11 @@ export class AffineProvider extends BaseProvider {
|
||||
public override async extendWorkspace(
|
||||
workspaceUnit: WorkspaceUnit
|
||||
): Promise<WorkspaceUnit> {
|
||||
const { id } = await this._apis.createWorkspace({
|
||||
name: workspaceUnit.name,
|
||||
});
|
||||
const { id } = await this._apis.createWorkspace(
|
||||
new Blob([
|
||||
encodeStateAsUpdate(workspaceUnit.blocksuiteWorkspace!.doc).buffer,
|
||||
])
|
||||
);
|
||||
const newWorkspaceUnit = new WorkspaceUnit({
|
||||
id,
|
||||
name: workspaceUnit.name,
|
||||
@@ -359,7 +386,7 @@ export class AffineProvider extends BaseProvider {
|
||||
owner: await this.getUserInfo(),
|
||||
published: false,
|
||||
memberCount: 1,
|
||||
provider: 'affine',
|
||||
provider: this.id,
|
||||
syncMode: 'core',
|
||||
});
|
||||
|
||||
@@ -370,8 +397,6 @@ export class AffineProvider extends BaseProvider {
|
||||
encodeStateAsUpdate(workspaceUnit.blocksuiteWorkspace.doc)
|
||||
);
|
||||
|
||||
await syncToCloud(blocksuiteWorkspace, this._apis.token.refresh);
|
||||
|
||||
newWorkspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
|
||||
this._workspaces.add(newWorkspaceUnit);
|
||||
@@ -382,6 +407,7 @@ export class AffineProvider extends BaseProvider {
|
||||
token.clear();
|
||||
this._channel?.disconnect();
|
||||
this._wsMap.forEach(ws => ws.disconnect());
|
||||
this._workspaces.clear();
|
||||
storage.removeItem('token');
|
||||
}
|
||||
|
||||
@@ -389,7 +415,7 @@ export class AffineProvider extends BaseProvider {
|
||||
return this._apis.getWorkspaceMembers({ id });
|
||||
}
|
||||
|
||||
public override async acceptInvitation(invitingCode: string): Promise<void> {
|
||||
await this._apis.acceptInviting({ invitingCode });
|
||||
public override async acceptInvitation(invitingCode: string) {
|
||||
return await this._apis.acceptInviting({ invitingCode });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import kyOrigin from 'ky';
|
||||
import ky from 'ky-universal';
|
||||
import { MessageCenter } from '../../../message/index.js';
|
||||
import { token } from './token.js';
|
||||
|
||||
export const bareClient = ky.extend({
|
||||
prefixUrl: 'http://localhost:8080',
|
||||
type KyInstance = typeof ky;
|
||||
|
||||
const messageCenter = MessageCenter.getInstance();
|
||||
|
||||
const _sendMessage = messageCenter.getMessageSender('affine');
|
||||
|
||||
export const bareClient: KyInstance = ky.extend({
|
||||
prefixUrl: '/',
|
||||
retry: 1,
|
||||
hooks: {
|
||||
// afterResponse: [
|
||||
@@ -22,7 +28,7 @@ export const bareClient = ky.extend({
|
||||
},
|
||||
});
|
||||
|
||||
export const client = bareClient.extend({
|
||||
export const client: KyInstance = bareClient.extend({
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
async request => {
|
||||
@@ -41,5 +47,15 @@ export const client = bareClient.extend({
|
||||
request.headers.set('Authorization', token.token);
|
||||
},
|
||||
],
|
||||
|
||||
beforeError: [
|
||||
error => {
|
||||
const { response } = error;
|
||||
if (response.status === 401) {
|
||||
_sendMessage(MessageCenter.messageCode.noPermission);
|
||||
}
|
||||
return error;
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -70,9 +70,6 @@ class Token {
|
||||
}
|
||||
|
||||
async refreshToken(token?: string) {
|
||||
if (!this._refreshToken && !token) {
|
||||
throw new Error('No authorization token.');
|
||||
}
|
||||
if (!this._padding) {
|
||||
this._padding = login({
|
||||
type: 'Refresh',
|
||||
@@ -194,6 +191,7 @@ export const getAuthorizer = () => {
|
||||
return [signInWithGoogle, onAuthStateChanged] as const;
|
||||
} catch (e) {
|
||||
getLogger('getAuthorizer')(e);
|
||||
console.error('getAuthorizer', e);
|
||||
return [] as const;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import { MessageCenter } from '../../../message/index.js';
|
||||
import { bareClient, client } from './request.js';
|
||||
import type { User } from './user';
|
||||
|
||||
const messageCenter = MessageCenter.getInstance();
|
||||
|
||||
const sendMessage = messageCenter.getMessageSender('affine');
|
||||
|
||||
const { messageCode } = MessageCenter;
|
||||
|
||||
class RequestError extends Error {
|
||||
constructor(message: string, cause: unknown | null = null) {
|
||||
super(message);
|
||||
this.name = 'RequestError';
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
export interface GetWorkspaceDetailParams {
|
||||
id: string;
|
||||
}
|
||||
@@ -26,13 +40,18 @@ export interface Workspace {
|
||||
}
|
||||
|
||||
export async function getWorkspaces(): Promise<Workspace[]> {
|
||||
return client
|
||||
.get('api/workspace', {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
})
|
||||
.json();
|
||||
try {
|
||||
return 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 {
|
||||
@@ -43,7 +62,13 @@ export interface WorkspaceDetail extends Workspace {
|
||||
export async function getWorkspaceDetail(
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<WorkspaceDetail | null> {
|
||||
return client.get(`api/workspace/${params.id}`).json();
|
||||
try {
|
||||
const response = client.get(`api/workspace/${params.id}`);
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.getDetailFailed);
|
||||
throw new RequestError('get detail failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
@@ -74,7 +99,12 @@ export interface GetWorkspaceMembersParams {
|
||||
export async function getWorkspaceMembers(
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<Member[]> {
|
||||
return client.get(`api/workspace/${params.id}/permission`).json();
|
||||
try {
|
||||
return client.get(`api/workspace/${params.id}/permission`).json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.getMembersFailed);
|
||||
throw new RequestError('get members failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CreateWorkspaceParams {
|
||||
@@ -82,12 +112,14 @@ export interface CreateWorkspaceParams {
|
||||
}
|
||||
|
||||
export async function createWorkspace(
|
||||
params: CreateWorkspaceParams
|
||||
encodedYDoc: Blob
|
||||
): Promise<{ id: string }> {
|
||||
// FIXME: avatar should be optional
|
||||
return client
|
||||
.post('api/workspace', { json: { ...params, avatar: '123' } })
|
||||
.json();
|
||||
try {
|
||||
return client.post('api/workspace', { body: encodedYDoc }).json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.createWorkspaceFailed);
|
||||
throw new RequestError('create workspace failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
export interface UpdateWorkspaceParams {
|
||||
@@ -98,13 +130,18 @@ export interface UpdateWorkspaceParams {
|
||||
export async function updateWorkspace(
|
||||
params: UpdateWorkspaceParams
|
||||
): Promise<{ public: boolean | null }> {
|
||||
return client
|
||||
.post(`api/workspace/${params.id}`, {
|
||||
json: {
|
||||
public: params.public,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
try {
|
||||
return client
|
||||
.post(`api/workspace/${params.id}`, {
|
||||
json: {
|
||||
public: params.public,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.updateWorkspaceFailed);
|
||||
throw new RequestError('update workspace failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeleteWorkspaceParams {
|
||||
@@ -114,7 +151,10 @@ export interface DeleteWorkspaceParams {
|
||||
export async function deleteWorkspace(
|
||||
params: DeleteWorkspaceParams
|
||||
): Promise<void> {
|
||||
await client.delete(`api/workspace/${params.id}`);
|
||||
await client.delete(`api/workspace/${params.id}`).catch(error => {
|
||||
sendMessage(messageCode.deleteWorkspaceFailed);
|
||||
throw new RequestError('delete workspace failed', error);
|
||||
});
|
||||
}
|
||||
|
||||
export interface InviteMemberParams {
|
||||
@@ -126,13 +166,18 @@ export interface InviteMemberParams {
|
||||
* Notice: Only support normal(contrast to private) workspace.
|
||||
*/
|
||||
export async function inviteMember(params: InviteMemberParams): Promise<void> {
|
||||
return client
|
||||
.post(`api/workspace/${params.id}/permission`, {
|
||||
json: {
|
||||
email: params.email,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
try {
|
||||
return client
|
||||
.post(`api/workspace/${params.id}/permission`, {
|
||||
json: {
|
||||
email: params.email,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.inviteMemberFailed);
|
||||
throw new RequestError('invite member failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RemoveMemberParams {
|
||||
@@ -140,7 +185,10 @@ export interface RemoveMemberParams {
|
||||
}
|
||||
|
||||
export async function removeMember(params: RemoveMemberParams): Promise<void> {
|
||||
await client.delete(`api/permission/${params.permissionId}`);
|
||||
await client.delete(`api/permission/${params.permissionId}`).catch(error => {
|
||||
sendMessage(messageCode.removeMemberFailed);
|
||||
throw new RequestError('remove member failed', error);
|
||||
});
|
||||
}
|
||||
|
||||
export interface AcceptInvitingParams {
|
||||
@@ -149,8 +197,13 @@ export interface AcceptInvitingParams {
|
||||
|
||||
export async function acceptInviting(
|
||||
params: AcceptInvitingParams
|
||||
): Promise<void> {
|
||||
await bareClient.post(`api/invitation/${params.invitingCode}`);
|
||||
): Promise<Permission> {
|
||||
try {
|
||||
return bareClient.post(`api/invitation/${params.invitingCode}`).json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.acceptInvitingFailed);
|
||||
throw new RequestError('accept inviting failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadBlob(params: { blob: Blob }): Promise<string> {
|
||||
@@ -160,7 +213,12 @@ export async function uploadBlob(params: { blob: Blob }): Promise<string> {
|
||||
export async function getBlob(params: {
|
||||
blobId: string;
|
||||
}): Promise<ArrayBuffer> {
|
||||
return client.get(`api/blob/${params.blobId}`).arrayBuffer();
|
||||
try {
|
||||
return client.get(`api/blob/${params.blobId}`).arrayBuffer();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.getBlobFailed);
|
||||
throw new RequestError('get blob failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
export interface LeaveWorkspaceParams {
|
||||
@@ -168,15 +226,26 @@ export interface LeaveWorkspaceParams {
|
||||
}
|
||||
|
||||
export async function leaveWorkspace({ id }: LeaveWorkspaceParams) {
|
||||
await client.delete(`api/workspace/${id}/permission`).json();
|
||||
await client
|
||||
.delete(`api/workspace/${id}/permission`)
|
||||
.json()
|
||||
.catch(error => {
|
||||
sendMessage(messageCode.leaveWorkspaceFailed);
|
||||
throw new RequestError('leave workspace failed', error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadWorkspace(
|
||||
workspaceId: string,
|
||||
published = false
|
||||
): Promise<ArrayBuffer> {
|
||||
if (published) {
|
||||
return bareClient.get(`api/public/doc/${workspaceId}`).arrayBuffer();
|
||||
try {
|
||||
if (published) {
|
||||
return bareClient.get(`api/public/doc/${workspaceId}`).arrayBuffer();
|
||||
}
|
||||
return client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.downloadWorkspaceFailed);
|
||||
throw new RequestError('download workspace failed', error);
|
||||
}
|
||||
return client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer();
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ import { Logger } from 'src/types';
|
||||
import { token } from './apis/token';
|
||||
import * as url from 'lib0/url';
|
||||
|
||||
const RECONNECT_INTERVAL_TIME = 5000;
|
||||
const RECONNECT_INTERVAL_TIME = 500;
|
||||
const MAX_RECONNECT_TIMES = 50;
|
||||
|
||||
export class WebsocketClient extends websocket.WebsocketClient {
|
||||
public shouldReconnect = false;
|
||||
private _reconnectInterval: number | null = null;
|
||||
private _logger: Logger;
|
||||
private _retryTimes = 0;
|
||||
constructor(
|
||||
serverUrl: string,
|
||||
logger: Logger,
|
||||
@@ -34,30 +34,28 @@ export class WebsocketClient extends websocket.WebsocketClient {
|
||||
this.on('connect', () => {
|
||||
this._logger('Affine channel connected');
|
||||
this.shouldReconnect = true;
|
||||
if (this._reconnectInterval) {
|
||||
window.clearInterval(this._reconnectInterval);
|
||||
}
|
||||
this._retryTimes = 0;
|
||||
});
|
||||
|
||||
this.on('disconnect', ({ error }: { error: Error }) => {
|
||||
if (error) {
|
||||
let times = 0;
|
||||
// Try reconnect if connect error has occurred
|
||||
this._reconnectInterval = window.setInterval(() => {
|
||||
if (this.shouldReconnect && token.isLogin && !this.connected) {
|
||||
try {
|
||||
this.connect();
|
||||
this._logger(`try reconnect channel ${++times} times`);
|
||||
if (times > MAX_RECONNECT_TIMES) {
|
||||
if (this.shouldReconnect && token.isLogin && !this.connected) {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
if (this._retryTimes <= MAX_RECONNECT_TIMES) {
|
||||
this.connect();
|
||||
this._logger(
|
||||
`try reconnect channel ${++this._retryTimes} times`
|
||||
);
|
||||
} else {
|
||||
this._logger('reconnect failed, max reconnect times reached');
|
||||
this._reconnectInterval &&
|
||||
window.clearInterval(this._reconnectInterval);
|
||||
}
|
||||
} catch (e) {
|
||||
this._logger('reconnect failed', e);
|
||||
}
|
||||
}, RECONNECT_INTERVAL_TIME);
|
||||
} catch (e) {
|
||||
this._logger('reconnect failed', e);
|
||||
}
|
||||
}, RECONNECT_INTERVAL_TIME);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import assert from 'assert';
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import { WorkspaceUnit } from '../../workspace-unit.js';
|
||||
import type { WorkspaceUnitCtorParams } from '../../workspace-unit';
|
||||
import { createBlocksuiteWorkspace } from '../../utils/index.js';
|
||||
import type { Apis } from './apis';
|
||||
import { WebsocketProvider } from './sync.js';
|
||||
import { setDefaultAvatar } from '../utils.js';
|
||||
import { applyUpdate } from '../../utils/index.js';
|
||||
|
||||
@@ -42,37 +39,6 @@ export const loadWorkspaceUnit = async (
|
||||
return workspaceUnit;
|
||||
};
|
||||
|
||||
export const syncToCloud = async (
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace,
|
||||
refreshToken: string
|
||||
) => {
|
||||
const workspaceId = blocksuiteWorkspace.room;
|
||||
assert(workspaceId, 'Blocksuite workspace without room(workspaceId).');
|
||||
|
||||
const wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${
|
||||
window.location.host
|
||||
}/api/sync/`;
|
||||
|
||||
const ws = new WebsocketProvider(
|
||||
wsUrl,
|
||||
workspaceId,
|
||||
blocksuiteWorkspace.doc,
|
||||
{
|
||||
params: { token: refreshToken },
|
||||
}
|
||||
);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.once('synced', () => {
|
||||
// FIXME: we don't when send local data to cloud successfully, so hack to wait 1s.
|
||||
// Server will support this by add a new api.
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
ws.once('lost-connection', () => reject());
|
||||
ws.once('connection-error', () => reject());
|
||||
});
|
||||
};
|
||||
|
||||
export const createWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => {
|
||||
const workspaceUnit = new WorkspaceUnit(params);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Logger, User } from '../types';
|
||||
import type { WorkspaceUnitCollectionScope } from '../workspace-unit-collection';
|
||||
import type { WorkspaceUnitCtorParams, WorkspaceUnit } from '../workspace-unit';
|
||||
import { Member } from './affine/apis';
|
||||
import { Permission } from './affine/apis/workspace.js';
|
||||
|
||||
const defaultLogger = () => {
|
||||
return;
|
||||
@@ -22,10 +23,15 @@ export type UpdateWorkspaceMetaParams = Partial<
|
||||
>;
|
||||
|
||||
export class BaseProvider {
|
||||
/** provider id */
|
||||
public readonly id: string = 'base';
|
||||
/** workspace unit collection */
|
||||
protected _workspaces!: WorkspaceUnitCollectionScope;
|
||||
protected _logger!: Logger;
|
||||
protected _messageCenter!: MessageCenter;
|
||||
/** send message with message center */
|
||||
protected _sendMessage!: ReturnType<
|
||||
InstanceType<typeof MessageCenter>['getMessageSender']
|
||||
>;
|
||||
|
||||
public constructor({
|
||||
logger,
|
||||
@@ -34,7 +40,7 @@ export class BaseProvider {
|
||||
}: ProviderConstructorParams) {
|
||||
this._logger = (logger || defaultLogger) as Logger;
|
||||
this._workspaces = workspaces;
|
||||
this._messageCenter = messageCenter;
|
||||
this._sendMessage = messageCenter.getMessageSender(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,8 +234,10 @@ export class BaseProvider {
|
||||
* @param {string} inviteCode
|
||||
* @returns
|
||||
*/
|
||||
public async acceptInvitation(inviteCode: string): Promise<void> {
|
||||
public async acceptInvitation(
|
||||
inviteCode: string
|
||||
): Promise<Permission | null> {
|
||||
inviteCode;
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export const writeUpdatesToLocal = async (
|
||||
if (updatesStore) {
|
||||
await idb.addAutoKey(updatesStore, currState);
|
||||
}
|
||||
db.close();
|
||||
};
|
||||
|
||||
export const applyLocalUpdates = async (
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
import { varStorage as storage } from 'lib0/storage';
|
||||
import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store';
|
||||
import { IndexedDBProvider } from './indexeddb/indexeddb.js';
|
||||
import { applyLocalUpdates } from './indexeddb/utils.js';
|
||||
import assert from 'assert';
|
||||
import { loadWorkspaceUnit, createWorkspaceUnit } from './utils.js';
|
||||
import type { WorkspaceUnit } from '../../workspace-unit';
|
||||
@@ -48,6 +49,7 @@ export class LocalProvider extends BaseProvider {
|
||||
workspace: BlocksuiteWorkspace
|
||||
): Promise<BlocksuiteWorkspace> {
|
||||
assert(workspace.room);
|
||||
await applyLocalUpdates(workspace);
|
||||
await this.linkLocal(workspace);
|
||||
return workspace;
|
||||
}
|
||||
@@ -101,7 +103,7 @@ export class LocalProvider extends BaseProvider {
|
||||
owner: undefined,
|
||||
syncMode: 'core',
|
||||
memberCount: 1,
|
||||
provider: 'local',
|
||||
provider: this.id,
|
||||
});
|
||||
this._workspaces.add(workspaceUnit);
|
||||
this._storeWorkspaces(this._workspaces.list());
|
||||
|
||||
@@ -32,4 +32,5 @@ export type Logger = ReturnType<typeof getLogger>;
|
||||
export type Message = {
|
||||
code: number;
|
||||
message: string;
|
||||
provider: string;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface WorkspaceUnitCollectionScope {
|
||||
get: (workspaceId: string) => WorkspaceUnit | undefined;
|
||||
list: () => WorkspaceUnit[];
|
||||
add: (workspace: WorkspaceUnit | WorkspaceUnit[]) => void;
|
||||
remove: (workspaceId: string) => boolean;
|
||||
remove: (workspaceId: string | string[]) => boolean;
|
||||
clear: () => void;
|
||||
update: (
|
||||
workspaceId: string,
|
||||
@@ -18,7 +18,7 @@ export interface WorkspaceUnitCollectionScope {
|
||||
|
||||
export interface WorkspaceUnitCollectionChangeEvent {
|
||||
added?: WorkspaceUnit[];
|
||||
deleted?: WorkspaceUnit;
|
||||
deleted?: WorkspaceUnit[];
|
||||
updated?: WorkspaceUnit;
|
||||
}
|
||||
|
||||
@@ -62,16 +62,22 @@ export class WorkspaceUnitCollection {
|
||||
const workspaceUnits = Array.isArray(workspaceUnit)
|
||||
? workspaceUnit
|
||||
: [workspaceUnit];
|
||||
let added = false;
|
||||
|
||||
workspaceUnits.forEach(workspaceUnit => {
|
||||
if (this._workspaceUnitMap.has(workspaceUnit.id)) {
|
||||
// FIXME: multiple add same workspace
|
||||
return;
|
||||
}
|
||||
added = true;
|
||||
this._workspaceUnitMap.set(workspaceUnit.id, workspaceUnit);
|
||||
scopedWorkspaceIds.add(workspaceUnit.id);
|
||||
});
|
||||
|
||||
if (!added) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._events.emit('change', [
|
||||
{
|
||||
added: workspaceUnits,
|
||||
@@ -79,34 +85,44 @@ export class WorkspaceUnitCollection {
|
||||
]);
|
||||
};
|
||||
|
||||
const remove = (workspaceId: string) => {
|
||||
if (!scopedWorkspaceIds.has(workspaceId)) {
|
||||
return true;
|
||||
}
|
||||
const remove = (workspaceId: string | string[]) => {
|
||||
const workspaceIds = Array.isArray(workspaceId)
|
||||
? workspaceId
|
||||
: [workspaceId];
|
||||
const workspaceUnits: WorkspaceUnit[] = [];
|
||||
|
||||
const workspaceUnit = this._workspaceUnitMap.get(workspaceId);
|
||||
if (workspaceUnit) {
|
||||
const ret = this._workspaceUnitMap.delete(workspaceId);
|
||||
// If deletion failed, return.
|
||||
if (!ret) {
|
||||
return ret;
|
||||
workspaceIds.forEach(workspaceId => {
|
||||
if (!scopedWorkspaceIds.has(workspaceId)) {
|
||||
return;
|
||||
}
|
||||
const workspaceUnit = this._workspaceUnitMap.get(workspaceId);
|
||||
if (workspaceUnit) {
|
||||
const ret = this._workspaceUnitMap.delete(workspaceId);
|
||||
// If deletion failed, return.
|
||||
if (!ret) {
|
||||
return;
|
||||
}
|
||||
|
||||
scopedWorkspaceIds.delete(workspaceId);
|
||||
workspaceUnits.push(workspaceUnit);
|
||||
scopedWorkspaceIds.delete(workspaceId);
|
||||
}
|
||||
});
|
||||
|
||||
this._events.emit('change', [
|
||||
{
|
||||
deleted: workspaceUnit,
|
||||
} as WorkspaceUnitCollectionChangeEvent,
|
||||
]);
|
||||
if (!workspaceUnits.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._events.emit('change', [
|
||||
{
|
||||
deleted: workspaceUnits,
|
||||
} as WorkspaceUnitCollectionChangeEvent,
|
||||
]);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
scopedWorkspaceIds.forEach(id => {
|
||||
remove(id);
|
||||
});
|
||||
remove(Array.from(scopedWorkspaceIds));
|
||||
};
|
||||
|
||||
const update = (workspaceId: string, meta: UpdateWorkspaceUnitParams) => {
|
||||
|
||||
@@ -52,7 +52,7 @@ export class WorkspaceUnit {
|
||||
}
|
||||
|
||||
setBlocksuiteWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace | null) {
|
||||
if (blocksuiteWorkspace && blocksuiteWorkspace?.room !== this.id) {
|
||||
if (blocksuiteWorkspace && blocksuiteWorkspace.room !== this.id) {
|
||||
throw new Error('Workspace id inconsistent.');
|
||||
}
|
||||
this._blocksuiteWorkspace = blocksuiteWorkspace;
|
||||
|
||||
Reference in New Issue
Block a user