refactor: workspace crud

This commit is contained in:
alt0
2023-01-09 00:41:29 +08:00
parent d75e5fff92
commit 055b63382b
7 changed files with 335 additions and 167 deletions

View File

@@ -1,4 +1,5 @@
import { Workspaces } from './workspaces';
import type { WorkspacesChangeEvent } from './workspaces';
import { Workspace } from '@blocksuite/store';
import { BaseProvider } from './provider/base';
import { LocalProvider } from './provider/local/local';
@@ -14,9 +15,10 @@ import { applyUpdate, encodeStateAsUpdate } from 'yjs';
* @classdesc DataCenter is a data center, it can manage different providers for business
*/
export class DataCenter {
private readonly workspaces = new Workspaces(this);
private readonly _workspaces = new Workspaces();
private currentWorkspace: Workspace | null = null;
private readonly _logger = getLogger('dc');
private _defaultProvider?: BaseProvider;
providerMap: Map<string, BaseProvider> = new Map();
constructor(debug: boolean) {
@@ -28,32 +30,43 @@ export class DataCenter {
// TODO: switch different provider
dc.registerProvider(new LocalProvider());
dc.registerProvider(new AffineProvider());
dc.workspaces.init();
return dc;
}
/**
* Register provider.
* We will automatically set the first provider to default provider.
*/
registerProvider(provider: BaseProvider) {
if (!this._defaultProvider) {
this._defaultProvider = provider;
}
// inject data in provider
provider.inject({ logger: this._logger, workspaces: this.workspaces });
provider.inject({
logger: this._logger,
workspaces: this._workspaces.createScope(),
});
provider.init();
this.providerMap.set(provider.id, provider);
}
setDefaultProvider(providerId: string) {
this._defaultProvider = this.providerMap.get(providerId);
}
get providers() {
return Array.from(this.providerMap.values());
}
/**
* get workspaces list if focusUpdate is true, it will refresh workspaces
* @param {string} name workspace name
* @returns {Promise<WS[]>}
*/
public async getWorkspaces(focusUpdate = false) {
if (focusUpdate) {
this.workspaces.refreshWorkspaces();
}
return this.workspaces.workspaces;
public get workspaces() {
return this._workspaces.workspaces;
}
public async refreshWorkspaces() {
return Promise.allSettled(
Object.values(this.providerMap).map(provider => provider.loadWorkspaces())
);
}
/**
@@ -61,11 +74,16 @@ export class DataCenter {
* @param {string} name workspace name
* @returns {Promise<WS>}
*/
public async createWorkspace(name: string) {
const workspaceInfo = this.workspaces.addLocalWorkspace(name);
// IMP: create workspace in local provider
this.providerMap.get('local')?.createWorkspace({ name });
return workspaceInfo;
public async createWorkspace(workspaceMeta: WorkspaceMeta) {
assert(
this._defaultProvider,
'There is no provider. You should add provider first.'
);
const workspace = await this._defaultProvider.createWorkspace(
workspaceMeta
);
return workspace;
}
/**
@@ -73,14 +91,11 @@ export class DataCenter {
* @param {string} workspaceId workspace id
*/
public async deleteWorkspace(workspaceId: string) {
const workspaceInfo = this.workspaces.getWorkspace(workspaceId);
const workspaceInfo = this._workspaces.find(workspaceId);
assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider);
if (provider && this.workspaces.hasWorkspace(workspaceId)) {
await provider.delete(workspaceId);
// may be refresh all workspaces
this.workspaces.delete(workspaceId);
}
assert(provider, `Workspace exists, but we couldn't find its provider.`);
await provider.deleteWorkspace(workspaceId);
}
/**
@@ -119,11 +134,11 @@ export class DataCenter {
* @returns {Promise<Workspace>}
*/
public async loadWorkspace(workspaceId: string) {
const workspaceInfo = this.workspaces.getWorkspace(workspaceId);
const workspaceInfo = this._workspaces.find(workspaceId);
assert(workspaceInfo, 'Workspace not found');
const currentProvider = this.providerMap.get(workspaceInfo.provider);
if (currentProvider) {
currentProvider.close(workspaceId);
currentProvider.closeWorkspace(workspaceId);
}
const provider = this.providerMap.get(workspaceInfo.provider);
assert(provider, `provide '${workspaceInfo.provider}' is not registered`);
@@ -149,8 +164,10 @@ export class DataCenter {
* listen workspaces list change
* @param {Function} callback callback function
*/
public async onWorkspacesChange(callback: (workspaces: WS[]) => void) {
this.workspaces.onWorkspacesChange(callback);
public async onWorkspacesChange(
callback: (workspaces: WorkspacesChangeEvent) => void
) {
this._workspaces.on('change', callback);
}
/**
@@ -166,20 +183,18 @@ export class DataCenter {
assert(w?.room, 'No workspace to set meta');
const update: Partial<WorkspaceMeta> = {};
if (name) {
w.doc.meta.setName(name);
w.meta.setName(name);
update.name = name;
}
if (avatar) {
w.doc.meta.setAvatar(avatar);
w.meta.setAvatar(avatar);
update.avatar = avatar;
}
// may run for change workspace meta
const workspaceInfo = this.workspaces.getWorkspace(w.room);
const workspaceInfo = this._workspaces.find(w.room);
assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider);
provider?.updateWorkspaceMeta(w.room, update);
// update workspace list directly
this.workspaces.updateWorkspaceInfo(w.room, update);
}
/**
@@ -188,27 +203,26 @@ export class DataCenter {
* @param id workspace id
*/
public async leaveWorkspace(workspaceId: string) {
const workspaceInfo = this.workspaces.getWorkspace(workspaceId);
const workspaceInfo = this._workspaces.find(workspaceId);
assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider);
if (provider) {
await provider.close(workspaceId);
await provider.leave(workspaceId);
await provider.closeWorkspace(workspaceId);
await provider.leaveWorkspace(workspaceId);
}
}
public async setWorkspacePublish(workspaceId: string, isPublish: boolean) {
const workspaceInfo = this.workspaces.getWorkspace(workspaceId);
const workspaceInfo = this._workspaces.find(workspaceId);
assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider);
if (provider) {
await provider.publish(workspaceId, isPublish);
this.workspaces.updateWorkspaceInfo(workspaceId, { isPublish });
}
}
public async inviteMember(id: string, email: string) {
const workspaceInfo = this.workspaces.getWorkspace(id);
const workspaceInfo = this._workspaces.find(id);
assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider);
if (provider) {
@@ -221,7 +235,7 @@ export class DataCenter {
* @param {number} permissionId permission id
*/
public async removeMember(workspaceId: string, permissionId: number) {
const workspaceInfo = this.workspaces.getWorkspace(workspaceId);
const workspaceInfo = this._workspaces.find(workspaceId);
assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider);
if (provider) {
@@ -235,13 +249,11 @@ export class DataCenter {
*/
public async closeCurrentWorkspace() {
assert(this.currentWorkspace?.room, 'No workspace to close');
const currentWorkspace = this.workspaces.getWorkspace(
this.currentWorkspace.room
);
const currentWorkspace = this._workspaces.find(this.currentWorkspace.room);
assert(currentWorkspace, 'Workspace not found');
const provider = this.providerMap.get(currentWorkspace.provider);
assert(provider, 'Provider not found');
await provider.close(currentWorkspace.id);
await provider.closeWorkspace(currentWorkspace.id);
}
private async _transWorkspaceProvider(
@@ -249,7 +261,7 @@ export class DataCenter {
providerId: string
) {
assert(workspace.room, 'No workspace id');
const workspaceInfo = this.workspaces.getWorkspace(workspace.room);
const workspaceInfo = this._workspaces.find(workspace.room);
assert(workspaceInfo, 'Workspace not found');
if (workspaceInfo.provider === providerId) {
this._logger('Workspace provider is same');
@@ -270,8 +282,7 @@ export class DataCenter {
);
applyUpdate(newWorkspace.doc, encodeStateAsUpdate(workspace.doc));
assert(newWorkspace, 'Create workspace failed');
await currentProvider.delete(workspace.room);
this.workspaces.refreshWorkspaces();
await currentProvider.deleteWorkspace(workspace.room);
}
/**

View File

@@ -119,9 +119,8 @@ export class AffineProvider extends BaseProvider {
return Promise.resolve(null);
}
});
await (
await Promise.all(workspaceInstances)
).forEach((workspace, i) => {
(await Promise.all(workspaceInstances)).forEach((workspace, i) => {
if (workspace) {
workspaces[i] = {
...workspaces[i],
@@ -159,6 +158,11 @@ export class AffineProvider extends BaseProvider {
}
}
});
workspaces.forEach(workspace => {
this._workspaces.add(workspace);
});
return workspaces;
}
@@ -185,17 +189,19 @@ export class AffineProvider extends BaseProvider {
return this._user;
}
public override async delete(id: string): Promise<void> {
await this.close(id);
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.delete(w.room);
await this.deleteWorkspace(w.room);
this._workspaces.remove(w.room);
} catch (e) {
this._logger('has a problem of delete workspace ', e);
}
@@ -204,14 +210,14 @@ export class AffineProvider extends BaseProvider {
this._workspacesCache.clear();
}
public override async close(id: string) {
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 leave(id: string): Promise<void> {
public override async leaveWorkspace(id: string): Promise<void> {
await leaveWorkspace({ id });
}
@@ -251,6 +257,19 @@ export class AffineProvider extends BaseProvider {
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;
}

View File

@@ -1,10 +1,10 @@
import { BlobStorage, Workspace } from '@blocksuite/store';
import { Logger, User, Workspace as WS, WorkspaceMeta } from '../types';
import { Workspaces } from '../workspaces';
import type { WorkspacesScope } from '../workspaces';
export class BaseProvider {
public readonly id: string = 'base';
protected _workspaces!: Workspaces;
protected _workspaces!: WorkspacesScope;
protected _logger!: Logger;
protected _blobs!: BlobStorage;
@@ -13,7 +13,7 @@ export class BaseProvider {
workspaces,
}: {
logger: Logger;
workspaces: Workspaces;
workspaces: WorkspacesScope;
}) {
this._logger = logger;
this._workspaces = workspaces;
@@ -83,7 +83,7 @@ export class BaseProvider {
* delete workspace include all data
* @param id workspace id
*/
public async delete(id: string): Promise<void> {
public async deleteWorkspace(id: string): Promise<void> {
id;
return;
}
@@ -92,7 +92,7 @@ export class BaseProvider {
* leave workspace by workspace id
* @param id workspace id
*/
public async leave(id: string): Promise<void> {
public async leaveWorkspace(id: string): Promise<void> {
id;
return;
}
@@ -101,7 +101,7 @@ export class BaseProvider {
* close db link and websocket connection and other resources
* @param id workspace id
*/
public async close(id: string) {
public async closeWorkspace(id: string) {
id;
return;
}

View File

@@ -1,7 +1,7 @@
import { BaseProvider } from '../base';
import { varStorage as storage } from 'lib0/storage';
import { Workspace as WS, WorkspaceMeta } from '../../types';
import { Workspace } from '@blocksuite/store';
import { Workspace, uuidv4 } from '@blocksuite/store';
import { IndexedDBProvider } from '../indexeddb';
import assert from 'assert';
import { getDefaultHeadImgBlob } from '../../utils';
@@ -11,11 +11,10 @@ const WORKSPACE_KEY = 'workspaces';
export class LocalProvider extends BaseProvider {
public id = 'local';
private _idbMap: Map<string, IndexedDBProvider> = new Map();
private _workspacesList: WS[] = [];
constructor() {
super();
this._workspacesList = [];
this.loadWorkspaces();
}
private _storeWorkspaces(workspaces: WS[]) {
@@ -46,6 +45,9 @@ export class LocalProvider extends BaseProvider {
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`);
}
@@ -53,12 +55,12 @@ export class LocalProvider extends BaseProvider {
return Promise.resolve(workspaces);
}
public override async delete(id: string): Promise<void> {
const index = this._workspacesList.findIndex(ws => ws.id === id);
if (index !== -1) {
public override async deleteWorkspace(id: string): Promise<void> {
const workspace = this._workspaces.get(id);
if (workspace) {
IndexedDBProvider.delete(id);
this._workspacesList.splice(index, 1);
this._storeWorkspaces(this._workspacesList);
this._workspaces.remove(id);
this._storeWorkspaces(this._workspaces.list());
} else {
this._logger(`Failed to delete workspace ${id}`);
}
@@ -68,15 +70,8 @@ export class LocalProvider extends BaseProvider {
id: string,
meta: Partial<WorkspaceMeta>
) {
const index = this._workspacesList.findIndex(ws => ws.id === id);
if (index !== -1) {
const workspace = this._workspacesList[index];
meta.name && (workspace.name = meta.name);
meta.avatar && (workspace.avatar = meta.avatar);
this._storeWorkspaces(this._workspacesList);
} else {
this._logger(`Failed to update workspace ${id}`);
}
this._workspaces.update(id, meta);
this._storeWorkspaces(this._workspaces.list());
}
public override async createWorkspace(
@@ -88,13 +83,27 @@ export class LocalProvider extends BaseProvider {
const blob = await getDefaultHeadImgBlob(meta.name);
meta.avatar = (await this.setBlob(blob)) || '';
}
const workspaceInfos = this._workspaces.addLocalWorkspace(meta.name);
this._logger('Creating affine workspace');
const workspace = new Workspace({ room: workspaceInfos.id });
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._storeWorkspaces([...this._workspacesList, workspaceInfos]);
this._initWorkspaceDb(workspace);
this._workspaces.add(workspaceInfo);
this._storeWorkspaces(this._workspaces.list());
return workspace;
}
@@ -102,5 +111,6 @@ export class LocalProvider extends BaseProvider {
const workspaces = await this.loadWorkspaces();
workspaces.forEach(ws => IndexedDBProvider.delete(ws.id));
this._storeWorkspaces([]);
this._workspaces.clear();
}
}

View File

@@ -1 +1,2 @@
export { Workspaces } from './workspaces';
export type { WorkspacesScope, WorkspacesChangeEvent } from './workspaces';

View File

@@ -1,101 +1,127 @@
import { Workspace as WS } from '../types';
import { Observable } from 'lib0/observable';
import { uuidv4 } from '@blocksuite/store';
import { DataCenter } from '../datacenter';
import type { Workspace, WorkspaceMeta } from '../types';
export class Workspaces extends Observable<string> {
private _workspaces: WS[];
private readonly _dc: DataCenter;
export interface WorkspacesScope {
get: (workspaceId: string) => Workspace | undefined;
list: () => Workspace[];
add: (workspace: Workspace) => void;
remove: (workspaceId: string) => boolean;
clear: () => void;
update: (workspaceId: string, workspaceMeta: Partial<WorkspaceMeta>) => void;
}
constructor(dc: DataCenter) {
super();
this._workspaces = [];
this._dc = dc;
export interface WorkspacesChangeEvent {
added?: Workspace;
deleted?: Workspace;
updated?: Workspace;
}
export class Workspaces extends Observable<'change'> {
private _workspacesMap = new Map<string, Workspace>();
get workspaces(): Workspace[] {
return Object.values(this._workspacesMap);
}
public init() {
this._loadWorkspaces();
find(workspaceId: string) {
return this._workspacesMap.get(workspaceId);
}
get workspaces() {
return this._workspaces;
}
createScope(): WorkspacesScope {
const scopedWorkspaceIds = new Set<string>();
/**
* emit when workspaces changed
* @param {(workspace: WS[]) => void} cb
*/
onWorkspacesChange(cb: (workspace: WS[]) => void) {
this.on('change', cb);
}
const get = (workspaceId: string) => {
if (!scopedWorkspaceIds.has(workspaceId)) {
return;
}
return this._workspacesMap.get(workspaceId);
};
private async _loadWorkspaces() {
const providers = this._dc.providers;
let workspaces: WS[] = [];
providers.forEach(async p => {
const pWorkspaces = await p.loadWorkspaces();
workspaces = [...workspaces, ...pWorkspaces];
this._updateWorkspaces([...workspaces, ...pWorkspaces]);
});
}
const add = (workspace: Workspace) => {
if (this._workspacesMap.has(workspace.id)) {
throw new Error(`Duplicate workspace id.`);
}
this._workspacesMap.set(workspace.id, workspace);
scopedWorkspaceIds.add(workspace.id);
/**
* focus load all workspaces list
*/
public async refreshWorkspaces() {
this._loadWorkspaces();
}
this.emit('change', [
{
added: workspace,
} as WorkspacesChangeEvent,
]);
};
private _updateWorkspaces(workspaces: WS[]) {
this._workspaces = workspaces;
this.emit('change', this._workspaces);
}
const remove = (workspaceId: string) => {
if (!scopedWorkspaceIds.has(workspaceId)) {
return true;
}
const workspace = this._workspacesMap.get(workspaceId);
if (workspace) {
const ret = this._workspacesMap.delete(workspaceId);
// If deletion failed, return.
if (!ret) {
return ret;
}
scopedWorkspaceIds.delete(workspaceId);
this.emit('change', [
{
deleted: workspace,
} as WorkspacesChangeEvent,
]);
}
return true;
};
const clear = () => {
scopedWorkspaceIds.forEach(id => {
remove(id);
});
};
const update = (
workspaceId: string,
workspaceMeta: Partial<WorkspaceMeta>
) => {
if (!scopedWorkspaceIds.has(workspaceId)) {
return true;
}
const workspace = this._workspacesMap.get(workspaceId);
if (!workspace) {
return true;
}
this._workspacesMap.set(workspaceId, { ...workspace, ...workspaceMeta });
this.emit('change', [
{
updated: this._workspacesMap.get(workspaceId),
} as WorkspacesChangeEvent,
]);
};
// TODO: need to optimize
const list = () => {
const workspaces: Workspace[] = [];
scopedWorkspaceIds.forEach(id => {
const workspace = this._workspacesMap.get(id);
if (workspace) {
workspaces.push(workspace);
}
});
return workspaces;
};
private _getDefaultWorkspace(name: string): WS {
return {
name,
id: uuidv4(),
isPublish: false,
avatar: '',
owner: undefined,
isLocal: true,
memberCount: 1,
provider: 'local',
get,
list,
add,
remove,
clear,
update,
};
}
/** add a local workspaces */
public addLocalWorkspace(name: string) {
const workspace = this._getDefaultWorkspace(name);
this._updateWorkspaces([...this._workspaces, workspace]);
return workspace;
}
/** delete a workspaces by id */
public delete(id: string) {
const index = this._workspaces.findIndex(w => w.id === id);
if (index >= 0) {
this._workspaces.splice(index, 1);
this._updateWorkspaces(this._workspaces);
}
}
/** get workspace info by id */
public getWorkspace(id: string) {
return this._workspaces.find(w => w.id === id);
}
/** check if workspace exists */
public hasWorkspace(id: string) {
return this._workspaces.some(w => w.id === id);
}
public updateWorkspaceInfo(id: string, info: Partial<WS>) {
const index = this._workspaces.findIndex(w => w.id === id);
if (index >= 0) {
this._workspaces[index] = { ...this._workspaces[index], ...info };
this._updateWorkspaces(this._workspaces);
}
}
}

View File

@@ -0,0 +1,101 @@
import { Workspace as WS } from '../types';
import { Observable } from 'lib0/observable';
import { uuidv4 } from '@blocksuite/store';
import { DataCenter } from '../datacenter';
export class Workspaces extends Observable<string> {
private _workspaces: WS[];
private readonly _dc: DataCenter;
constructor(dc: DataCenter) {
super();
this._workspaces = [];
this._dc = dc;
}
public init() {
this._loadWorkspaces();
}
get workspaces() {
return this._workspaces;
}
/**
* emit when workspaces changed
* @param {(workspace: WS[]) => void} cb
*/
onWorkspacesChange(cb: (workspace: WS[]) => void) {
this.on('change', cb);
}
private async _loadWorkspaces() {
const providers = this._dc.providers;
let workspaces: WS[] = [];
providers.forEach(async p => {
const pWorkspaces = await p.loadWorkspaces();
workspaces = [...workspaces, ...pWorkspaces];
this._updateWorkspaces([...workspaces, ...pWorkspaces]);
});
}
/**
* focus load all workspaces list
*/
public async refreshWorkspaces() {
this._loadWorkspaces();
}
private _updateWorkspaces(workspaces: WS[]) {
this._workspaces = workspaces;
this.emit('change', this._workspaces);
}
private _getDefaultWorkspace(name: string): WS {
return {
name,
id: uuidv4(),
isPublish: false,
avatar: '',
owner: undefined,
isLocal: true,
memberCount: 1,
provider: 'local',
};
}
/** add a local workspaces */
public addLocalWorkspace(name: string) {
const workspace = this._getDefaultWorkspace(name);
this._updateWorkspaces([...this._workspaces, workspace]);
return workspace;
}
/** delete a workspaces by id */
public delete(id: string) {
const index = this._workspaces.findIndex(w => w.id === id);
if (index >= 0) {
this._workspaces.splice(index, 1);
this._updateWorkspaces(this._workspaces);
}
}
/** get workspace info by id */
public getWorkspace(id: string) {
return this._workspaces.find(w => w.id === id);
}
/** check if workspace exists */
public hasWorkspace(id: string) {
return this._workspaces.some(w => w.id === id);
}
public updateWorkspaceInfo(id: string, info: Partial<WS>) {
const index = this._workspaces.findIndex(w => w.id === id);
if (index >= 0) {
this._workspaces[index] = { ...this._workspaces[index], ...info };
this._updateWorkspaces(this._workspaces);
}
}
}