mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-30 00:29:46 +08:00
Merge remote-tracking branch 'refs/remotes/origin/feat/cloud-sync-saika'
Conflicts: package.json packages/data-center/src/datacenter.ts packages/data-center/src/provider/base.ts pnpm-lock.yaml
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
import {
|
||||
getWorkspaces,
|
||||
getWorkspaceDetail,
|
||||
WorkspaceDetail,
|
||||
downloadWorkspace,
|
||||
deleteWorkspace,
|
||||
leaveWorkspace,
|
||||
inviteMember,
|
||||
removeMember,
|
||||
createWorkspace,
|
||||
updateWorkspace,
|
||||
} from './apis/workspace';
|
||||
import { BaseProvider } from '../base';
|
||||
import type { ProviderConstructorParams } from '../base';
|
||||
import { User, Workspace as WS, WorkspaceMeta } from '../../types';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import { BlockSchema } from '@blocksuite/blocks/models';
|
||||
import { applyUpdate } from 'yjs';
|
||||
import { token, Callback } from './apis';
|
||||
import { varStorage as storage } from 'lib0/storage';
|
||||
import assert from 'assert';
|
||||
import { getAuthorizer } from './apis/token';
|
||||
import { WebsocketProvider } from './sync';
|
||||
import { IndexedDBProvider } from '../indexeddb';
|
||||
import { getDefaultHeadImgBlob } from '../../utils';
|
||||
|
||||
export class AffineProvider extends BaseProvider {
|
||||
public id = 'affine';
|
||||
private _workspacesCache: Map<string, Workspace> = new Map();
|
||||
private _onTokenRefresh?: Callback = undefined;
|
||||
private readonly _authorizer = getAuthorizer();
|
||||
private _user: User | undefined = undefined;
|
||||
private _wsMap: Map<string, WebsocketProvider> = new Map();
|
||||
private _idbMap: Map<string, IndexedDBProvider> = new Map();
|
||||
|
||||
constructor(params: ProviderConstructorParams) {
|
||||
super(params);
|
||||
}
|
||||
|
||||
override async init() {
|
||||
this._onTokenRefresh = () => {
|
||||
if (token.refresh) {
|
||||
storage.setItem('token', token.refresh);
|
||||
}
|
||||
};
|
||||
|
||||
token.onChange(this._onTokenRefresh);
|
||||
|
||||
// initial login token
|
||||
if (token.isExpired) {
|
||||
try {
|
||||
const refreshToken = storage.getItem('token');
|
||||
await token.refreshToken(refreshToken);
|
||||
|
||||
if (token.refresh) {
|
||||
storage.set('token', token.refresh);
|
||||
}
|
||||
|
||||
assert(token.isLogin);
|
||||
} catch (_) {
|
||||
// this._logger('Authorization failed, fallback to local mode');
|
||||
}
|
||||
} else {
|
||||
storage.setItem('token', token.refresh);
|
||||
}
|
||||
}
|
||||
|
||||
override async warpWorkspace(workspace: Workspace) {
|
||||
const { doc, room } = workspace;
|
||||
assert(room);
|
||||
this._initWorkspaceDb(workspace);
|
||||
const updates = await downloadWorkspace(room);
|
||||
if (updates) {
|
||||
await new Promise(resolve => {
|
||||
doc.once('update', resolve);
|
||||
applyUpdate(doc, new Uint8Array(updates));
|
||||
});
|
||||
}
|
||||
const ws = new WebsocketProvider('/', room, doc);
|
||||
this._wsMap.set(room, ws);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// TODO: synced will also be triggered on reconnection after losing sync
|
||||
// There needs to be an event mechanism to emit the synchronization state to the upper layer
|
||||
assert(ws);
|
||||
ws.once('synced', () => resolve());
|
||||
ws.once('lost-connection', () => resolve());
|
||||
ws.once('connection-error', () => reject());
|
||||
});
|
||||
return workspace;
|
||||
}
|
||||
|
||||
override async loadWorkspaces() {
|
||||
if (!token.isLogin) {
|
||||
return [];
|
||||
}
|
||||
const workspacesList = await getWorkspaces();
|
||||
const workspaces: WS[] = workspacesList.map(w => {
|
||||
return {
|
||||
...w,
|
||||
memberCount: 0,
|
||||
name: '',
|
||||
provider: 'affine',
|
||||
};
|
||||
});
|
||||
const workspaceInstances = workspaces.map(({ id }) => {
|
||||
const workspace =
|
||||
this._workspacesCache.get(id) ||
|
||||
new Workspace({
|
||||
room: id,
|
||||
}).register(BlockSchema);
|
||||
this._workspacesCache.set(id, workspace);
|
||||
if (workspace) {
|
||||
return new Promise<Workspace>(resolve => {
|
||||
downloadWorkspace(id).then(data => {
|
||||
applyUpdate(workspace.doc, new Uint8Array(data));
|
||||
resolve(workspace);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
(await Promise.all(workspaceInstances)).forEach((workspace, i) => {
|
||||
if (workspace) {
|
||||
workspaces[i] = {
|
||||
...workspaces[i],
|
||||
name: workspace.doc.meta.name,
|
||||
avatar: workspace.doc.meta.avatar,
|
||||
};
|
||||
}
|
||||
});
|
||||
const getDetailList = workspacesList.map(w => {
|
||||
const { id } = w;
|
||||
return new Promise<{ id: string; detail: WorkspaceDetail | null }>(
|
||||
resolve => {
|
||||
getWorkspaceDetail({ id }).then(data => {
|
||||
resolve({ id, detail: data || null });
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
const ownerList = await Promise.all(getDetailList);
|
||||
(await Promise.all(ownerList)).forEach(detail => {
|
||||
if (detail) {
|
||||
const { id, detail: workspaceDetail } = detail;
|
||||
if (workspaceDetail) {
|
||||
const { owner, member_count } = workspaceDetail;
|
||||
const currentWorkspace = workspaces.find(w => w.id === id);
|
||||
if (currentWorkspace) {
|
||||
currentWorkspace.owner = {
|
||||
id: owner.id,
|
||||
name: owner.name,
|
||||
avatar: owner.avatar_url,
|
||||
email: owner.email,
|
||||
};
|
||||
currentWorkspace.memberCount = member_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
workspaces.forEach(workspace => {
|
||||
this._workspaces.add(workspace);
|
||||
});
|
||||
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
override async auth() {
|
||||
const refreshToken = await storage.getItem('token');
|
||||
if (refreshToken) {
|
||||
await token.refreshToken(refreshToken);
|
||||
if (token.isLogin && !token.isExpired) {
|
||||
// login success
|
||||
return;
|
||||
}
|
||||
}
|
||||
const user = await this._authorizer[0]?.();
|
||||
assert(user);
|
||||
this._user = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
avatar: user.avatar_url,
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
|
||||
public override async getUserInfo(): Promise<User | undefined> {
|
||||
return this._user;
|
||||
}
|
||||
|
||||
public override async deleteWorkspace(id: string): Promise<void> {
|
||||
await this.closeWorkspace(id);
|
||||
IndexedDBProvider.delete(id);
|
||||
await deleteWorkspace({ id });
|
||||
this._workspaces.remove(id);
|
||||
}
|
||||
|
||||
public override async clear(): Promise<void> {
|
||||
for (const w of this._workspacesCache.values()) {
|
||||
if (w.room) {
|
||||
try {
|
||||
await this.deleteWorkspace(w.room);
|
||||
this._workspaces.remove(w.room);
|
||||
} catch (e) {
|
||||
this._logger('has a problem of delete workspace ', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._workspacesCache.clear();
|
||||
}
|
||||
|
||||
public override async closeWorkspace(id: string) {
|
||||
const idb = this._idbMap.get(id);
|
||||
idb?.destroy();
|
||||
const ws = this._wsMap.get(id);
|
||||
ws?.disconnect();
|
||||
}
|
||||
|
||||
public override async leaveWorkspace(id: string): Promise<void> {
|
||||
await leaveWorkspace({ id });
|
||||
}
|
||||
|
||||
public override async invite(id: string, email: string): Promise<void> {
|
||||
return await inviteMember({ id, email });
|
||||
}
|
||||
|
||||
public override async removeMember(permissionId: number): Promise<void> {
|
||||
return await removeMember({ permissionId });
|
||||
}
|
||||
|
||||
private async _initWorkspaceDb(workspace: Workspace) {
|
||||
assert(workspace.room);
|
||||
let idb = this._idbMap.get(workspace.room);
|
||||
idb?.destroy();
|
||||
idb = new IndexedDBProvider(workspace.room, workspace.doc);
|
||||
this._idbMap.set(workspace.room, idb);
|
||||
await idb.whenSynced;
|
||||
this._logger('Local data loaded');
|
||||
return idb;
|
||||
}
|
||||
|
||||
public override async createWorkspace(
|
||||
meta: WorkspaceMeta
|
||||
): Promise<Workspace | undefined> {
|
||||
assert(meta.name, 'Workspace name is required');
|
||||
if (!meta.avatar) {
|
||||
// set default avatar
|
||||
const blob = await getDefaultHeadImgBlob(meta.name);
|
||||
meta.avatar = (await this.setBlob(blob)) || '';
|
||||
}
|
||||
const { id } = await createWorkspace(meta as Required<WorkspaceMeta>);
|
||||
this._logger('Creating affine workspace');
|
||||
const nw = new Workspace({
|
||||
room: id,
|
||||
}).register(BlockSchema);
|
||||
nw.meta.setName(meta.name);
|
||||
nw.meta.setAvatar(meta.avatar);
|
||||
this._initWorkspaceDb(nw);
|
||||
|
||||
const workspaceInfo: WS = {
|
||||
name: meta.name,
|
||||
id,
|
||||
isPublish: false,
|
||||
avatar: '',
|
||||
owner: undefined,
|
||||
isLocal: true,
|
||||
memberCount: 1,
|
||||
provider: 'local',
|
||||
};
|
||||
|
||||
this._workspaces.add(workspaceInfo);
|
||||
return nw;
|
||||
}
|
||||
|
||||
public override async publish(id: string, isPublish: boolean): Promise<void> {
|
||||
await updateWorkspace({ id, public: isPublish });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export { token } from './token.js';
|
||||
export type { Callback } from './token.js';
|
||||
|
||||
import { getAuthorizer } from './token.js';
|
||||
import * as user from './user.js';
|
||||
import * as workspace from './workspace.js';
|
||||
import * as business from './business.js';
|
||||
|
||||
export type Apis = typeof user &
|
||||
typeof workspace & {
|
||||
business: typeof business;
|
||||
signInWithGoogle: ReturnType<typeof getAuthorizer>[0];
|
||||
onAuthStateChanged: ReturnType<typeof getAuthorizer>[1];
|
||||
};
|
||||
|
||||
export const getApis = (): Apis => {
|
||||
const [signInWithGoogle, onAuthStateChanged] = getAuthorizer();
|
||||
return {
|
||||
...user,
|
||||
...workspace,
|
||||
business,
|
||||
signInWithGoogle,
|
||||
onAuthStateChanged,
|
||||
};
|
||||
};
|
||||
|
||||
export type { AccessTokenMessage } from './token';
|
||||
export type { Member, Workspace } from './workspace';
|
||||
export { WorkspaceType } from './workspace.js';
|
||||
@@ -0,0 +1,45 @@
|
||||
import kyOrigin from 'ky';
|
||||
import ky from 'ky-universal';
|
||||
import { token } from './token.js';
|
||||
|
||||
export const bareClient = ky.extend({
|
||||
prefixUrl: 'http://localhost:8080',
|
||||
retry: 1,
|
||||
hooks: {
|
||||
// afterResponse: [
|
||||
// async (_request, _options, response) => {
|
||||
// if (response.status === 200) {
|
||||
// const data = await response.json();
|
||||
// if (data.error) {
|
||||
// return new Response(data.error.message, {
|
||||
// status: data.error.code,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// return response;
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
});
|
||||
|
||||
export const client = bareClient.extend({
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
async request => {
|
||||
if (token.isLogin) {
|
||||
if (token.isExpired) await token.refreshToken();
|
||||
request.headers.set('Authorization', token.token);
|
||||
} else {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
beforeRetry: [
|
||||
async ({ request }) => {
|
||||
await token.refreshToken();
|
||||
request.headers.set('Authorization', token.token);
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
|
||||
import type { User } from 'firebase/auth';
|
||||
|
||||
import { getLogger } from '../../../logger';
|
||||
import { bareClient } from './request';
|
||||
|
||||
export interface AccessTokenMessage {
|
||||
create_at: number;
|
||||
exp: number;
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
export type Callback = (user: AccessTokenMessage | null) => void;
|
||||
|
||||
type LoginParams = {
|
||||
type: 'Google' | 'Refresh';
|
||||
token: string;
|
||||
};
|
||||
|
||||
type LoginResponse = {
|
||||
// access token, expires in a very short time
|
||||
token: string;
|
||||
// Refresh token
|
||||
refresh: string;
|
||||
};
|
||||
|
||||
const login = (params: LoginParams): Promise<LoginResponse> =>
|
||||
bareClient.post('api/user/token', { json: params }).json();
|
||||
|
||||
class Token {
|
||||
private readonly _logger;
|
||||
private _accessToken!: string;
|
||||
private _refreshToken!: string;
|
||||
|
||||
private _user!: AccessTokenMessage | null;
|
||||
private _padding?: Promise<LoginResponse>;
|
||||
|
||||
constructor() {
|
||||
this._logger = getLogger('token');
|
||||
this._logger.enabled = true;
|
||||
|
||||
this._setToken(); // fill with default value
|
||||
}
|
||||
|
||||
private _setToken(login?: LoginResponse) {
|
||||
this._accessToken = login?.token || '';
|
||||
this._refreshToken = login?.refresh || '';
|
||||
|
||||
this._user = Token.parse(this._accessToken);
|
||||
if (login) {
|
||||
this._logger('set login', login);
|
||||
this.triggerChange(this._user);
|
||||
} else {
|
||||
this._logger('empty login');
|
||||
}
|
||||
}
|
||||
|
||||
async initToken(token: string) {
|
||||
const tokens = await login({ token, type: 'Google' });
|
||||
this._setToken(tokens);
|
||||
return this._user;
|
||||
}
|
||||
|
||||
async refreshToken(token?: string) {
|
||||
if (!this._refreshToken && !token) {
|
||||
throw new Error('No authorization token.');
|
||||
}
|
||||
if (!this._padding) {
|
||||
this._padding = login({
|
||||
type: 'Refresh',
|
||||
token: this._refreshToken || token!,
|
||||
});
|
||||
}
|
||||
this._setToken(await this._padding);
|
||||
this._padding = undefined;
|
||||
}
|
||||
|
||||
get token() {
|
||||
return this._accessToken;
|
||||
}
|
||||
|
||||
get refresh() {
|
||||
return this._refreshToken;
|
||||
}
|
||||
|
||||
get isLogin() {
|
||||
return !!this._refreshToken;
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
if (!this._user) return true;
|
||||
return Date.now() - this._user.create_at > this._user.exp;
|
||||
}
|
||||
|
||||
static parse(token: string): AccessTokenMessage | null {
|
||||
try {
|
||||
return JSON.parse(
|
||||
String.fromCharCode.apply(
|
||||
null,
|
||||
Array.from(
|
||||
Uint8Array.from(
|
||||
window.atob(
|
||||
// split jwt
|
||||
token.split('.')[1]
|
||||
),
|
||||
c => c.charCodeAt(0)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private callbacks: Callback[] = [];
|
||||
private lastState: AccessTokenMessage | null = null;
|
||||
|
||||
triggerChange(user: AccessTokenMessage | null) {
|
||||
this.lastState = user;
|
||||
this.callbacks.forEach(callback => callback(user));
|
||||
}
|
||||
|
||||
onChange(callback: Callback) {
|
||||
this.callbacks.push(callback);
|
||||
callback(this.lastState);
|
||||
}
|
||||
|
||||
offChange(callback: Callback) {
|
||||
const index = this.callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const token = new Token();
|
||||
|
||||
export const getAuthorizer = () => {
|
||||
const app = initializeApp({
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
||||
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
|
||||
});
|
||||
try {
|
||||
const firebaseAuth = getAuth(app);
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider();
|
||||
|
||||
const getToken = async () => {
|
||||
const currentUser = firebaseAuth.currentUser;
|
||||
if (currentUser) {
|
||||
await currentUser.getIdTokenResult(true);
|
||||
if (!currentUser.isAnonymous) {
|
||||
return currentUser.getIdToken();
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
const idToken = await getToken();
|
||||
let loginUser: AccessTokenMessage | null = null;
|
||||
if (idToken) {
|
||||
loginUser = await token.initToken(idToken);
|
||||
} else {
|
||||
const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
|
||||
const idToken = await user.user.getIdToken();
|
||||
loginUser = await token.initToken(idToken);
|
||||
}
|
||||
return loginUser;
|
||||
};
|
||||
|
||||
const onAuthStateChanged = (callback: (user: User | null) => void) => {
|
||||
firebaseAuth.onAuthStateChanged(callback);
|
||||
};
|
||||
|
||||
return [signInWithGoogle, onAuthStateChanged] as const;
|
||||
} catch (e) {
|
||||
return [] as const;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { client } from './request.js';
|
||||
|
||||
export interface GetUserByEmailParams {
|
||||
email: string;
|
||||
workspace_id: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
create_at: string;
|
||||
}
|
||||
|
||||
export async function getUserByEmail(
|
||||
params: GetUserByEmailParams
|
||||
): Promise<User | null> {
|
||||
const searchParams = new URLSearchParams({ ...params });
|
||||
return client.get('api/user', { searchParams }).json<User | null>();
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { bareClient, client } from './request.js';
|
||||
import type { User } from './user';
|
||||
|
||||
export interface GetWorkspaceDetailParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export enum WorkspaceType {
|
||||
Private = 0,
|
||||
Normal = 1,
|
||||
}
|
||||
|
||||
export enum PermissionType {
|
||||
Read = 0,
|
||||
Write = 1,
|
||||
Admin = 2,
|
||||
Owner = 99,
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
id: string;
|
||||
type: WorkspaceType;
|
||||
public: boolean;
|
||||
permission_type: PermissionType;
|
||||
create_at: number;
|
||||
}
|
||||
|
||||
export async function getWorkspaces(): Promise<Workspace[]> {
|
||||
return client
|
||||
.get('api/workspace', {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
})
|
||||
.json();
|
||||
}
|
||||
|
||||
export interface WorkspaceDetail extends Workspace {
|
||||
owner: User;
|
||||
member_count: number;
|
||||
}
|
||||
|
||||
export async function getWorkspaceDetail(
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<WorkspaceDetail | null> {
|
||||
return client.get(`api/workspace/${params.id}`).json();
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
id: number;
|
||||
type: PermissionType;
|
||||
workspace_id: string;
|
||||
accepted: boolean;
|
||||
create_at: number;
|
||||
}
|
||||
|
||||
export interface RegisteredUser extends User {
|
||||
type: 'Registered';
|
||||
}
|
||||
|
||||
export interface UnregisteredUser {
|
||||
type: 'Unregistered';
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface Member extends Permission {
|
||||
user: RegisteredUser | UnregisteredUser;
|
||||
}
|
||||
|
||||
export interface GetWorkspaceMembersParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function getWorkspaceMembers(
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<Member[]> {
|
||||
return client.get(`api/workspace/${params.id}/permission`).json();
|
||||
}
|
||||
|
||||
export interface CreateWorkspaceParams {
|
||||
name: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export async function createWorkspace(
|
||||
params: CreateWorkspaceParams
|
||||
): Promise<{ id: string }> {
|
||||
return client.post('api/workspace', { json: params }).json();
|
||||
}
|
||||
|
||||
export interface UpdateWorkspaceParams {
|
||||
id: string;
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
export async function updateWorkspace(
|
||||
params: UpdateWorkspaceParams
|
||||
): Promise<{ public: boolean | null }> {
|
||||
return client
|
||||
.post(`api/workspace/${params.id}`, {
|
||||
json: {
|
||||
public: params.public,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
}
|
||||
|
||||
export interface DeleteWorkspaceParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function deleteWorkspace(
|
||||
params: DeleteWorkspaceParams
|
||||
): Promise<void> {
|
||||
await client.delete(`api/workspace/${params.id}`);
|
||||
}
|
||||
|
||||
export interface InviteMemberParams {
|
||||
id: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
export interface RemoveMemberParams {
|
||||
permissionId: number;
|
||||
}
|
||||
|
||||
export async function removeMember(params: RemoveMemberParams): Promise<void> {
|
||||
await client.delete(`api/permission/${params.permissionId}`);
|
||||
}
|
||||
|
||||
export interface AcceptInvitingParams {
|
||||
invitingCode: string;
|
||||
}
|
||||
|
||||
export async function acceptInviting(
|
||||
params: AcceptInvitingParams
|
||||
): Promise<void> {
|
||||
await bareClient.post(`api/invitation/${params.invitingCode}`);
|
||||
}
|
||||
|
||||
export async function uploadBlob(params: { blob: Blob }): Promise<string> {
|
||||
return client.put('api/blob', { body: params.blob }).text();
|
||||
}
|
||||
|
||||
export async function getBlob(params: {
|
||||
blobId: string;
|
||||
}): Promise<ArrayBuffer> {
|
||||
return client.get(`api/blob/${params.blobId}`).arrayBuffer();
|
||||
}
|
||||
|
||||
export interface LeaveWorkspaceParams {
|
||||
id: number | string;
|
||||
}
|
||||
|
||||
export async function leaveWorkspace({ id }: LeaveWorkspaceParams) {
|
||||
await client.delete(`api/workspace/${id}/permission`).json();
|
||||
}
|
||||
|
||||
export async function downloadWorkspace(
|
||||
workspaceId: string
|
||||
): Promise<ArrayBuffer> {
|
||||
return client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer();
|
||||
}
|
||||
@@ -1,175 +1 @@
|
||||
import assert from 'assert';
|
||||
import { applyUpdate, Doc } from 'yjs';
|
||||
|
||||
import type {
|
||||
ConfigStore,
|
||||
DataCenterSignals,
|
||||
InitialParams,
|
||||
Logger,
|
||||
} from '../index.js';
|
||||
import { token, Callback, getApis } from '../../apis/index.js';
|
||||
import { LocalProvider } from '../local/index.js';
|
||||
|
||||
import { WebsocketProvider } from './sync.js';
|
||||
import { IndexedDBProvider } from '../local/indexeddb.js';
|
||||
|
||||
export class AffineProvider extends LocalProvider {
|
||||
static id = 'affine';
|
||||
private _onTokenRefresh?: Callback = undefined;
|
||||
private _ws?: WebsocketProvider;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async init(params: InitialParams) {
|
||||
super.init(params);
|
||||
|
||||
this._onTokenRefresh = () => {
|
||||
if (token.refresh) {
|
||||
this._config.set('token', token.refresh);
|
||||
}
|
||||
};
|
||||
assert(this._onTokenRefresh);
|
||||
|
||||
token.onChange(this._onTokenRefresh);
|
||||
|
||||
// initial login token
|
||||
if (token.isExpired) {
|
||||
try {
|
||||
const refreshToken = await this._config.get('token');
|
||||
await token.refreshToken(refreshToken);
|
||||
|
||||
if (token.refresh) {
|
||||
this._config.set('token', token.refresh);
|
||||
}
|
||||
|
||||
assert(token.isLogin);
|
||||
} catch (_) {
|
||||
this._logger('Authorization failed, fallback to local mode');
|
||||
}
|
||||
} else {
|
||||
this._config.set('token', token.refresh);
|
||||
}
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
if (this._onTokenRefresh) {
|
||||
token.offChange(this._onTokenRefresh);
|
||||
}
|
||||
this._ws?.disconnect();
|
||||
}
|
||||
|
||||
async initData() {
|
||||
const databases = await indexedDB.databases();
|
||||
await super.initData(
|
||||
// set locally to true if exists a same name db
|
||||
databases
|
||||
.map(db => db.name)
|
||||
.filter(v => v)
|
||||
.includes(this._workspace.room)
|
||||
);
|
||||
|
||||
const workspace = this._workspace;
|
||||
const doc = workspace.doc;
|
||||
|
||||
this._logger(`Login: ${token.isLogin}`);
|
||||
|
||||
if (workspace.room && token.isLogin) {
|
||||
try {
|
||||
// init data from cloud
|
||||
await AffineProvider._initCloudDoc(
|
||||
workspace.room,
|
||||
doc,
|
||||
this._logger,
|
||||
this._signals
|
||||
);
|
||||
|
||||
// Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later
|
||||
this._ws = new WebsocketProvider('/', workspace.room, doc);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// TODO: synced will also be triggered on reconnection after losing sync
|
||||
// There needs to be an event mechanism to emit the synchronization state to the upper layer
|
||||
assert(this._ws);
|
||||
this._ws.once('synced', () => resolve());
|
||||
this._ws.once('lost-connection', () => resolve());
|
||||
this._ws.once('connection-error', () => reject());
|
||||
});
|
||||
this._signals.listAdd.emit({
|
||||
workspace: workspace.room,
|
||||
provider: this.id,
|
||||
locally: true,
|
||||
});
|
||||
} catch (e) {
|
||||
this._logger('Failed to init cloud workspace', e);
|
||||
}
|
||||
}
|
||||
|
||||
// if after update, the space:meta is empty
|
||||
// then we need to get map with doc
|
||||
// just a workaround for yjs
|
||||
doc.getMap('space:meta');
|
||||
}
|
||||
|
||||
private static async _initCloudDoc(
|
||||
workspace: string,
|
||||
doc: Doc,
|
||||
logger: Logger,
|
||||
signals: DataCenterSignals
|
||||
) {
|
||||
const apis = getApis();
|
||||
logger(`Loading ${workspace}...`);
|
||||
const updates = await apis.downloadWorkspace(workspace);
|
||||
if (updates) {
|
||||
await new Promise(resolve => {
|
||||
doc.once('update', resolve);
|
||||
applyUpdate(doc, new Uint8Array(updates));
|
||||
});
|
||||
logger(`Loaded: ${workspace}`);
|
||||
|
||||
// only add to list as online workspace
|
||||
signals.listAdd.emit({
|
||||
workspace,
|
||||
provider: this.id,
|
||||
// at this time we always download full workspace
|
||||
// but after we support sub doc, we can only download metadata
|
||||
locally: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async auth(
|
||||
config: Readonly<ConfigStore<string>>,
|
||||
logger: Logger,
|
||||
signals: DataCenterSignals
|
||||
) {
|
||||
const refreshToken = await config.get('token');
|
||||
if (refreshToken) {
|
||||
await token.refreshToken(refreshToken);
|
||||
if (token.isLogin && !token.isExpired) {
|
||||
logger('check login success');
|
||||
// login success
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
logger('start login');
|
||||
// login with google
|
||||
const apis = getApis();
|
||||
assert(apis.signInWithGoogle);
|
||||
const user = await apis.signInWithGoogle();
|
||||
assert(user);
|
||||
logger(`login success: ${user.displayName}`);
|
||||
|
||||
// TODO: refresh local workspace data
|
||||
const workspaces = await apis.getWorkspaces();
|
||||
await Promise.all(
|
||||
workspaces.map(async ({ id }) => {
|
||||
const doc = new Doc();
|
||||
const idb = new IndexedDBProvider(id, doc);
|
||||
await idb.whenSynced;
|
||||
await this._initCloudDoc(id, doc, logger, signals);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
export * from './affine';
|
||||
|
||||
@@ -1,82 +1,158 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { BlobStorage, Workspace } from '@blocksuite/store';
|
||||
import { Logger, User, Workspace as WS, WorkspaceMeta } from '../types';
|
||||
import type { WorkspacesScope } from '../workspaces';
|
||||
|
||||
import type { BlobURL } from '@blocksuite/store/dist/blob/types';
|
||||
import type {
|
||||
Apis,
|
||||
DataCenterSignals,
|
||||
Logger,
|
||||
InitialParams,
|
||||
ConfigStore,
|
||||
} from './index';
|
||||
const defaultLogger = () => {
|
||||
return;
|
||||
};
|
||||
|
||||
export interface ProviderConstructorParams {
|
||||
logger?: Logger;
|
||||
workspaces: WorkspacesScope;
|
||||
}
|
||||
|
||||
export class BaseProvider {
|
||||
static id = 'base';
|
||||
protected _apis!: Readonly<Apis>;
|
||||
protected _config!: Readonly<ConfigStore>;
|
||||
public readonly id: string = 'base';
|
||||
protected _workspaces!: WorkspacesScope;
|
||||
protected _logger!: Logger;
|
||||
protected _signals!: DataCenterSignals;
|
||||
protected _workspace!: Workspace;
|
||||
protected _blobs!: BlobStorage;
|
||||
|
||||
constructor() {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return (this.constructor as any).id;
|
||||
}
|
||||
|
||||
async init(params: InitialParams) {
|
||||
this._apis = params.apis;
|
||||
this._config = params.config;
|
||||
this._logger = params.logger;
|
||||
this._signals = params.signals;
|
||||
this._workspace = params.workspace;
|
||||
this._logger.enabled = params.debug;
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this.destroy();
|
||||
await this._config.clear();
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
async initData() {
|
||||
throw Error('Not implemented: initData');
|
||||
public constructor({ logger, workspaces }: ProviderConstructorParams) {
|
||||
this._logger = (logger || defaultLogger) as Logger;
|
||||
this._workspaces = workspaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* should return a blob url
|
||||
* hook after provider registered
|
||||
*/
|
||||
async getBlob(_id: string): Promise<BlobURL | null> {
|
||||
throw Error('Not implemented: getBlob');
|
||||
public async init() {
|
||||
return;
|
||||
}
|
||||
|
||||
// should return a blob unique id
|
||||
async setBlob(_blob: Blob): Promise<string> {
|
||||
throw Error('Not implemented: setBlob');
|
||||
/**
|
||||
* auth provider
|
||||
*/
|
||||
public async auth() {
|
||||
return;
|
||||
}
|
||||
|
||||
get workspace() {
|
||||
return this._workspace;
|
||||
/**
|
||||
* logout provider
|
||||
*/
|
||||
public async logout() {
|
||||
return;
|
||||
}
|
||||
|
||||
static async auth(
|
||||
_config: Readonly<ConfigStore>,
|
||||
logger: Logger,
|
||||
_signals: DataCenterSignals
|
||||
) {
|
||||
logger("This provider doesn't require authentication");
|
||||
/**
|
||||
* warp workspace with provider functions
|
||||
* @param workspace
|
||||
* @returns
|
||||
*/
|
||||
public async warpWorkspace(workspace: Workspace): Promise<Workspace> {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
// get workspace list,return a map of workspace id and boolean
|
||||
// if value is true, it exists locally, otherwise it does not exist locally
|
||||
static async list(
|
||||
_config: Readonly<ConfigStore>
|
||||
): Promise<Map<string, boolean> | undefined> {
|
||||
throw Error('Not implemented: list');
|
||||
/**
|
||||
* load workspaces
|
||||
**/
|
||||
public async loadWorkspaces(): Promise<WS[]> {
|
||||
throw new Error(`provider: ${this.id} loadWorkSpace Not implemented`);
|
||||
}
|
||||
|
||||
/**
|
||||
* get auth user info
|
||||
* @returns
|
||||
*/
|
||||
public async getUserInfo(): Promise<User | undefined> {
|
||||
return;
|
||||
}
|
||||
|
||||
async getBlob(id: string): Promise<string | null> {
|
||||
return await this._blobs.get(id);
|
||||
}
|
||||
|
||||
async setBlob(blob: Blob): Promise<string> {
|
||||
return await this._blobs.set(blob);
|
||||
}
|
||||
|
||||
/**
|
||||
* clear all local data in provider
|
||||
*/
|
||||
async clear() {
|
||||
this._blobs.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* delete workspace include all data
|
||||
* @param id workspace id
|
||||
*/
|
||||
public async deleteWorkspace(id: string): Promise<void> {
|
||||
id;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* leave workspace by workspace id
|
||||
* @param id workspace id
|
||||
*/
|
||||
public async leaveWorkspace(id: string): Promise<void> {
|
||||
id;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* close db link and websocket connection and other resources
|
||||
* @param id workspace id
|
||||
*/
|
||||
public async closeWorkspace(id: string) {
|
||||
id;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* invite workspace member
|
||||
* @param id workspace id
|
||||
*/
|
||||
public async invite(id: string, email: string): Promise<void> {
|
||||
id;
|
||||
email;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* remove workspace member by permission id
|
||||
* @param permissionId
|
||||
*/
|
||||
public async removeMember(permissionId: number): Promise<void> {
|
||||
permissionId;
|
||||
return;
|
||||
}
|
||||
|
||||
public async publish(id: string, isPublish: boolean): Promise<void> {
|
||||
id;
|
||||
isPublish;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* change workspace meta by workspace id , work for cached list in different provider
|
||||
* @param id
|
||||
* @param meta
|
||||
* @returns
|
||||
*/
|
||||
public async updateWorkspaceMeta(
|
||||
id: string,
|
||||
meta: Partial<WorkspaceMeta>
|
||||
): Promise<void> {
|
||||
id;
|
||||
meta;
|
||||
return;
|
||||
}
|
||||
|
||||
public async createWorkspace(
|
||||
meta: WorkspaceMeta
|
||||
): Promise<Workspace | undefined> {
|
||||
meta;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1 @@
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
|
||||
import type { Apis } from '../apis';
|
||||
import type { DataCenterSignals } from '../datacenter';
|
||||
import type { getLogger } from '../index';
|
||||
import type { ConfigStore } from '../store';
|
||||
|
||||
export type Logger = ReturnType<typeof getLogger>;
|
||||
|
||||
export type InitialParams = {
|
||||
apis: Apis;
|
||||
config: Readonly<ConfigStore>;
|
||||
debug: boolean;
|
||||
logger: Logger;
|
||||
signals: DataCenterSignals;
|
||||
workspace: Workspace;
|
||||
};
|
||||
|
||||
export type { Apis, ConfigStore, DataCenterSignals, Workspace };
|
||||
export type { BaseProvider } from './base.js';
|
||||
export { AffineProvider } from './affine/index.js';
|
||||
export { LocalProvider } from './local/index.js';
|
||||
export { SelfHostedProvider } from './selfhosted/index.js';
|
||||
export * from './affine/affine';
|
||||
|
||||
+4
@@ -196,4 +196,8 @@ export class IndexedDBProvider extends Observable<string> {
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
static delete(name: string): Promise<void> {
|
||||
return idb.deleteDB(name);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +1 @@
|
||||
import type { BlobStorage } from '@blocksuite/store';
|
||||
import assert from 'assert';
|
||||
|
||||
import type { ConfigStore, InitialParams } from '../index.js';
|
||||
import { BaseProvider } from '../base.js';
|
||||
import { IndexedDBProvider } from './indexeddb.js';
|
||||
|
||||
export class LocalProvider extends BaseProvider {
|
||||
static id = 'local';
|
||||
private _blobs!: BlobStorage;
|
||||
private _idb?: IndexedDBProvider = undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
async init(params: InitialParams) {
|
||||
super.init(params);
|
||||
|
||||
const blobs = await this._workspace.blobs;
|
||||
assert(blobs);
|
||||
this._blobs = blobs;
|
||||
}
|
||||
|
||||
async initData(locally = true) {
|
||||
assert(this._workspace.room);
|
||||
this._logger('Loading local data');
|
||||
this._idb = new IndexedDBProvider(
|
||||
this._workspace.room,
|
||||
this._workspace.doc
|
||||
);
|
||||
|
||||
await this._idb.whenSynced;
|
||||
this._logger('Local data loaded');
|
||||
|
||||
this._signals.listAdd.emit({
|
||||
workspace: this._workspace.room,
|
||||
provider: this.id,
|
||||
locally,
|
||||
});
|
||||
}
|
||||
|
||||
async clear() {
|
||||
assert(this._workspace.room);
|
||||
await super.clear();
|
||||
await this._blobs.clear();
|
||||
await this._idb?.clearData();
|
||||
this._signals.listRemove.emit(this._workspace.room);
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
super.destroy();
|
||||
await this._idb?.destroy();
|
||||
}
|
||||
|
||||
async getBlob(id: string): Promise<string | null> {
|
||||
return this._blobs.get(id);
|
||||
}
|
||||
|
||||
async setBlob(blob: Blob): Promise<string> {
|
||||
return this._blobs.set(blob);
|
||||
}
|
||||
|
||||
static async list(
|
||||
config: Readonly<ConfigStore<boolean>>
|
||||
): Promise<Map<string, boolean> | undefined> {
|
||||
const entries = await config.entries();
|
||||
return new Map(
|
||||
entries
|
||||
.filter(([key]) => key.startsWith('list:'))
|
||||
.map(([key, value]) => [key.slice(5), value])
|
||||
);
|
||||
}
|
||||
}
|
||||
export * from './local';
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { Workspaces } from '../../workspaces';
|
||||
import { LocalProvider } from './local';
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
describe('local provider', () => {
|
||||
const workspaces = new Workspaces();
|
||||
const provider = new LocalProvider({
|
||||
workspaces: workspaces.createScope(),
|
||||
});
|
||||
|
||||
const workspaceName = 'workspace-test';
|
||||
let workspaceId: string | undefined;
|
||||
|
||||
test('create workspace', async () => {
|
||||
const w = await provider.createWorkspace({
|
||||
name: workspaceName,
|
||||
avatar: 'avatar-url-test',
|
||||
});
|
||||
workspaceId = w?.room;
|
||||
|
||||
expect(workspaces.workspaces.length).toEqual(1);
|
||||
expect(workspaces.workspaces[0].name).toEqual(workspaceName);
|
||||
});
|
||||
|
||||
test('workspace list cache', async () => {
|
||||
const workspaces1 = new Workspaces();
|
||||
const provider1 = new LocalProvider({
|
||||
workspaces: workspaces1.createScope(),
|
||||
});
|
||||
await provider1.loadWorkspaces();
|
||||
expect(workspaces1.workspaces.length).toEqual(1);
|
||||
expect(workspaces1.workspaces[0].name).toEqual(workspaceName);
|
||||
expect(workspaces1.workspaces[0].id).toEqual(workspaceId);
|
||||
});
|
||||
|
||||
test('update workspace', async () => {
|
||||
await provider.updateWorkspaceMeta(workspaceId!, {
|
||||
name: '1111',
|
||||
});
|
||||
expect(workspaces.workspaces[0].name).toEqual('1111');
|
||||
});
|
||||
|
||||
test('delete workspace', async () => {
|
||||
expect(workspaces.workspaces.length).toEqual(1);
|
||||
await provider.deleteWorkspace(workspaces.workspaces[0].id);
|
||||
expect(workspaces.workspaces.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { BaseProvider } from '../base';
|
||||
import type { ProviderConstructorParams } from '../base';
|
||||
import { varStorage as storage } from 'lib0/storage';
|
||||
import { Workspace as WS, WorkspaceMeta } from '../../types';
|
||||
import { Workspace, uuidv4 } from '@blocksuite/store';
|
||||
import { IndexedDBProvider } from '../indexeddb';
|
||||
import assert from 'assert';
|
||||
import { getDefaultHeadImgBlob } from '../../utils';
|
||||
|
||||
const WORKSPACE_KEY = 'workspaces';
|
||||
|
||||
export class LocalProvider extends BaseProvider {
|
||||
public id = 'local';
|
||||
private _idbMap: Map<string, IndexedDBProvider> = new Map();
|
||||
|
||||
constructor(params: ProviderConstructorParams) {
|
||||
super(params);
|
||||
this.loadWorkspaces();
|
||||
}
|
||||
|
||||
private _storeWorkspaces(workspaces: WS[]) {
|
||||
storage.setItem(WORKSPACE_KEY, JSON.stringify(workspaces));
|
||||
}
|
||||
|
||||
private async _initWorkspaceDb(workspace: Workspace) {
|
||||
assert(workspace.room);
|
||||
let idb = this._idbMap.get(workspace.room);
|
||||
idb?.destroy();
|
||||
idb = new IndexedDBProvider(workspace.room, workspace.doc);
|
||||
this._idbMap.set(workspace.room, idb);
|
||||
this._logger('Local data loaded');
|
||||
return idb;
|
||||
}
|
||||
|
||||
public override async warpWorkspace(
|
||||
workspace: Workspace
|
||||
): Promise<Workspace> {
|
||||
assert(workspace.room);
|
||||
await this._initWorkspaceDb(workspace);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
override loadWorkspaces(): Promise<WS[]> {
|
||||
const workspaceStr = storage.getItem(WORKSPACE_KEY);
|
||||
let workspaces: WS[] = [];
|
||||
if (workspaceStr) {
|
||||
try {
|
||||
workspaces = JSON.parse(workspaceStr) as WS[];
|
||||
workspaces.forEach(workspace => {
|
||||
this._workspaces.add(workspace);
|
||||
});
|
||||
} catch (error) {
|
||||
this._logger(`Failed to parse workspaces from storage`);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(workspaces);
|
||||
}
|
||||
|
||||
public override async deleteWorkspace(id: string): Promise<void> {
|
||||
const workspace = this._workspaces.get(id);
|
||||
if (workspace) {
|
||||
IndexedDBProvider.delete(id);
|
||||
this._workspaces.remove(id);
|
||||
this._storeWorkspaces(this._workspaces.list());
|
||||
} else {
|
||||
this._logger(`Failed to delete workspace ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
public override async updateWorkspaceMeta(
|
||||
id: string,
|
||||
meta: Partial<WorkspaceMeta>
|
||||
) {
|
||||
this._workspaces.update(id, meta);
|
||||
this._storeWorkspaces(this._workspaces.list());
|
||||
}
|
||||
|
||||
public override async createWorkspace(
|
||||
meta: WorkspaceMeta
|
||||
): Promise<Workspace | undefined> {
|
||||
assert(meta.name, 'Workspace name is required');
|
||||
if (!meta.avatar) {
|
||||
// set default avatar
|
||||
const blob = await getDefaultHeadImgBlob(meta.name);
|
||||
meta.avatar = (await this.setBlob(blob)) || '';
|
||||
}
|
||||
this._logger('Creating affine workspace');
|
||||
|
||||
const workspaceInfo: WS = {
|
||||
name: meta.name,
|
||||
id: uuidv4(),
|
||||
isPublish: false,
|
||||
avatar: '',
|
||||
owner: undefined,
|
||||
isLocal: true,
|
||||
memberCount: 1,
|
||||
provider: 'local',
|
||||
};
|
||||
|
||||
const workspace = new Workspace({ room: workspaceInfo.id });
|
||||
this._initWorkspaceDb(workspace);
|
||||
workspace.meta.setName(meta.name);
|
||||
workspace.meta.setAvatar(meta.avatar);
|
||||
|
||||
this._workspaces.add(workspaceInfo);
|
||||
this._storeWorkspaces(this._workspaces.list());
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
public override async clear(): Promise<void> {
|
||||
const workspaces = await this.loadWorkspaces();
|
||||
workspaces.forEach(ws => IndexedDBProvider.delete(ws.id));
|
||||
this._storeWorkspaces([]);
|
||||
this._workspaces.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user