From 055b63382b2ce911565cc0f2fad063e6c5c3d416 Mon Sep 17 00:00:00 2001 From: alt0 Date: Mon, 9 Jan 2023 00:41:29 +0800 Subject: [PATCH] refactor: workspace crud --- packages/data-center/src/datacenter.ts | 105 +++++----- .../data-center/src/provider/affine/affine.ts | 35 +++- packages/data-center/src/provider/base.ts | 12 +- .../data-center/src/provider/local/local.ts | 52 +++-- packages/data-center/src/workspaces/index.ts | 1 + .../data-center/src/workspaces/workspaces.ts | 196 ++++++++++-------- .../src/workspaces/workspaces.ts.bak | 101 +++++++++ 7 files changed, 335 insertions(+), 167 deletions(-) create mode 100644 packages/data-center/src/workspaces/workspaces.ts.bak diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index bce67be90..7db16c1ca 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -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 = 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} - */ - 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} */ - 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} */ 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 = {}; 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); } /** diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index 25516386d..1651cba12 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -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 { - await this.close(id); + public override async deleteWorkspace(id: string): Promise { + await this.closeWorkspace(id); IndexedDBProvider.delete(id); await deleteWorkspace({ id }); + this._workspaces.remove(id); } public override async clear(): Promise { 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 { + public override async leaveWorkspace(id: string): Promise { 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; } diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts index d1568dbbc..dbf24df43 100644 --- a/packages/data-center/src/provider/base.ts +++ b/packages/data-center/src/provider/base.ts @@ -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 { + public async deleteWorkspace(id: string): Promise { id; return; } @@ -92,7 +92,7 @@ export class BaseProvider { * leave workspace by workspace id * @param id workspace id */ - public async leave(id: string): Promise { + public async leaveWorkspace(id: string): Promise { 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; } diff --git a/packages/data-center/src/provider/local/local.ts b/packages/data-center/src/provider/local/local.ts index c741b9da0..0a383a05b 100644 --- a/packages/data-center/src/provider/local/local.ts +++ b/packages/data-center/src/provider/local/local.ts @@ -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 = 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 { - const index = this._workspacesList.findIndex(ws => ws.id === id); - if (index !== -1) { + public override async deleteWorkspace(id: string): Promise { + 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 ) { - 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(); } } diff --git a/packages/data-center/src/workspaces/index.ts b/packages/data-center/src/workspaces/index.ts index 05a26f8e2..41419a6eb 100644 --- a/packages/data-center/src/workspaces/index.ts +++ b/packages/data-center/src/workspaces/index.ts @@ -1 +1,2 @@ export { Workspaces } from './workspaces'; +export type { WorkspacesScope, WorkspacesChangeEvent } from './workspaces'; diff --git a/packages/data-center/src/workspaces/workspaces.ts b/packages/data-center/src/workspaces/workspaces.ts index f813f6b16..2d8661b2b 100644 --- a/packages/data-center/src/workspaces/workspaces.ts +++ b/packages/data-center/src/workspaces/workspaces.ts @@ -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 { - 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) => 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(); + + 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(); - /** - * 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 + ) => { + 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) { - const index = this._workspaces.findIndex(w => w.id === id); - if (index >= 0) { - this._workspaces[index] = { ...this._workspaces[index], ...info }; - this._updateWorkspaces(this._workspaces); - } - } } diff --git a/packages/data-center/src/workspaces/workspaces.ts.bak b/packages/data-center/src/workspaces/workspaces.ts.bak new file mode 100644 index 000000000..f813f6b16 --- /dev/null +++ b/packages/data-center/src/workspaces/workspaces.ts.bak @@ -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 { + 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) { + const index = this._workspaces.findIndex(w => w.id === id); + if (index >= 0) { + this._workspaces[index] = { ...this._workspaces[index], ...info }; + this._updateWorkspaces(this._workspaces); + } + } +}