diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index 8350013952..e4fe394c71 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -1,13 +1,16 @@ -import { WorkspaceMetaCollection } from './workspace-meta-collection.js'; -import type { WorkspaceMetaCollectionChangeEvent } from './workspace-meta-collection'; +import { WorkspaceUnitCollection } from './workspace-unit-collection.js'; +import type { WorkspaceUnitCollectionChangeEvent } from './workspace-unit-collection'; import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; -import { BaseProvider } from './provider/base'; +import type { + BaseProvider, + CreateWorkspaceInfoParams, + UpdateWorkspaceMetaParams, +} from './provider/base'; import { LocalProvider } from './provider/local/local'; import { AffineProvider } from './provider'; -import type { Message, WorkspaceMeta } from './types'; +import type { Message } from './types'; import assert from 'assert'; import { getLogger } from './logger'; -import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { createBlocksuiteWorkspace } from './utils/index.js'; import { MessageCenter } from './message'; @@ -16,7 +19,7 @@ import { MessageCenter } from './message'; * @classdesc Data center is made for managing different providers for business */ export class DataCenter { - private readonly _workspaceMetaCollection = new WorkspaceMetaCollection(); + private readonly _workspaceUnitCollection = new WorkspaceUnitCollection(); private readonly _logger = getLogger('dc'); private _workspaceInstances: Map = new Map(); private _messageCenter = new MessageCenter(); @@ -35,7 +38,7 @@ export class DataCenter { const getInitParams = () => { return { logger: dc._logger, - workspaces: dc._workspaceMetaCollection.createScope(), + workspaces: dc._workspaceUnitCollection.createScope(), messageCenter: dc._messageCenter, }; }; @@ -43,6 +46,10 @@ export class DataCenter { dc.registerProvider(new LocalProvider(getInitParams())); dc.registerProvider(new AffineProvider(getInitParams())); + for (const provider of dc.providerMap.values()) { + await provider.loadWorkspaces(); + } + return dc; } @@ -68,7 +75,7 @@ export class DataCenter { } public get workspaces() { - return this._workspaceMetaCollection.workspaces; + return this._workspaceUnitCollection.workspaces; } public async refreshWorkspaces() { @@ -82,17 +89,15 @@ export class DataCenter { * @param {string} name workspace name * @returns {Promise} */ - public async createWorkspace(workspaceMeta: WorkspaceMeta) { + public async createWorkspace(params: CreateWorkspaceInfoParams) { assert( this._mainProvider, 'There is no provider. You should add provider first.' ); - const workspaceInfo = await this._mainProvider.createWorkspaceInfo( - workspaceMeta - ); + const workspaceMeta = await this._mainProvider.createWorkspaceInfo(params); - const workspace = createBlocksuiteWorkspace(workspaceInfo.id); + const workspace = createBlocksuiteWorkspace(workspaceMeta.id); await this._mainProvider.createWorkspace(workspace, workspaceMeta); return workspace; @@ -103,7 +108,7 @@ export class DataCenter { * @param {string} workspaceId workspace id */ public async deleteWorkspace(workspaceId: string) { - const workspaceInfo = this._workspaceMetaCollection.find(workspaceId); + const workspaceInfo = this._workspaceUnitCollection.find(workspaceId); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); assert(provider, `Workspace exists, but we couldn't find its provider.`); @@ -115,7 +120,7 @@ export class DataCenter { * @param {string} workspaceId workspace id */ private _getBlocksuiteWorkspace(workspaceId: string) { - const workspaceInfo = this._workspaceMetaCollection.find(workspaceId); + const workspaceInfo = this._workspaceUnitCollection.find(workspaceId); assert(workspaceInfo, 'Workspace not found'); return ( this._workspaceInstances.get(workspaceId) || @@ -149,7 +154,7 @@ export class DataCenter { * @returns {Promise} */ public async loadWorkspace(workspaceId: string) { - const workspaceInfo = this._workspaceMetaCollection.find(workspaceId); + const workspaceInfo = this._workspaceUnitCollection.find(workspaceId); assert(workspaceInfo, 'Workspace not found'); const currentProvider = this.providerMap.get(workspaceInfo.provider); if (currentProvider) { @@ -180,9 +185,15 @@ export class DataCenter { * @param {Function} callback callback function */ public async onWorkspacesChange( - callback: (workspaces: WorkspaceMetaCollectionChangeEvent) => void + callback: (workspaces: WorkspaceUnitCollectionChangeEvent) => void, + { immediate = true }: { immediate?: boolean } ) { - this._workspaceMetaCollection.on('change', callback); + if (immediate) { + callback({ + added: this._workspaceUnitCollection.workspaces, + }); + } + this._workspaceUnitCollection.on('change', callback); } /** @@ -191,11 +202,11 @@ export class DataCenter { * @param {BlocksuiteWorkspace} workspace workspace instance */ public async updateWorkspaceMeta( - { name, avatar }: Partial, + { name, avatar }: UpdateWorkspaceMetaParams, workspace: BlocksuiteWorkspace ) { assert(workspace?.room, 'No workspace to set meta'); - const update: Partial = {}; + const update: Partial = {}; if (name) { workspace.meta.setName(name); update.name = name; @@ -205,7 +216,7 @@ export class DataCenter { update.avatar = avatar; } // may run for change workspace meta - const workspaceInfo = this._workspaceMetaCollection.find(workspace.room); + const workspaceInfo = this._workspaceUnitCollection.find(workspace.room); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); provider?.updateWorkspaceMeta(workspace.room, update); @@ -217,7 +228,7 @@ export class DataCenter { * @param id workspace id */ public async leaveWorkspace(workspaceId: string) { - const workspaceInfo = this._workspaceMetaCollection.find(workspaceId); + const workspaceInfo = this._workspaceUnitCollection.find(workspaceId); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { @@ -227,7 +238,7 @@ export class DataCenter { } public async setWorkspacePublish(workspaceId: string, isPublish: boolean) { - const workspaceInfo = this._workspaceMetaCollection.find(workspaceId); + const workspaceInfo = this._workspaceUnitCollection.find(workspaceId); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { @@ -236,7 +247,7 @@ export class DataCenter { } public async inviteMember(id: string, email: string) { - const workspaceInfo = this._workspaceMetaCollection.find(id); + const workspaceInfo = this._workspaceUnitCollection.find(id); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { @@ -249,7 +260,7 @@ export class DataCenter { * @param {number} permissionId permission id */ public async removeMember(workspaceId: string, permissionId: number) { - const workspaceInfo = this._workspaceMetaCollection.find(workspaceId); + const workspaceInfo = this._workspaceUnitCollection.find(workspaceId); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { @@ -280,7 +291,7 @@ export class DataCenter { providerId: string ) { assert(workspace.room, 'No workspace id'); - const workspaceInfo = this._workspaceMetaCollection.find(workspace.room); + const workspaceInfo = this._workspaceUnitCollection.find(workspace.room); assert(workspaceInfo, 'Workspace not found'); if (workspaceInfo.provider === providerId) { this._logger('Workspace provider is same'); @@ -293,11 +304,12 @@ export class DataCenter { this._logger(`create ${providerId} workspace: `, workspaceInfo.name); const newWorkspaceInfo = await newProvider.createWorkspaceInfo({ name: workspaceInfo.name, - avatar: workspaceInfo.avatar, + // avatar: workspaceInfo.avatar, }); const newWorkspace = createBlocksuiteWorkspace(newWorkspaceInfo.id); // TODO optimize this function await newProvider.createWorkspace(newWorkspace, { + ...newWorkspaceInfo, name: workspaceInfo.name, avatar: workspaceInfo.avatar, }); @@ -306,7 +318,7 @@ export class DataCenter { this._logger( `update workspace data from ${workspaceInfo.provider} to ${providerId}` ); - applyUpdate(newWorkspace.doc, encodeStateAsUpdate(workspace.doc)); + await newProvider.assign(newWorkspace, workspace); assert(newWorkspace, 'Create workspace failed'); await currentProvider.deleteWorkspace(workspace.room); return newWorkspace.room; diff --git a/packages/data-center/src/index.ts b/packages/data-center/src/index.ts index bc816cb3f7..b0a5a7b373 100644 --- a/packages/data-center/src/index.ts +++ b/packages/data-center/src/index.ts @@ -27,6 +27,6 @@ export const getDataCenter = _initializeDataCenter(); export type { DataCenter }; export type { AccessTokenMessage } from './provider/affine/apis'; -export * from './types'; +export { WorkspaceUnit } from './workspace-unit'; export { getLogger } from './logger'; export * from './message'; diff --git a/packages/data-center/src/message/index.ts b/packages/data-center/src/message/index.ts index b04581df15..7d42b90d8b 100644 --- a/packages/data-center/src/message/index.ts +++ b/packages/data-center/src/message/index.ts @@ -1,2 +1,2 @@ -export { MessageCenter } from './message'; -export { MessageCode } from './code'; +export { MessageCenter } from './message.js'; +export { MessageCode } from './code.js'; diff --git a/packages/data-center/src/message/message.ts b/packages/data-center/src/message/message.ts index 34d6870211..6de9c75795 100644 --- a/packages/data-center/src/message/message.ts +++ b/packages/data-center/src/message/message.ts @@ -1,6 +1,6 @@ import { Observable } from 'lib0/observable'; import { Message } from '../types'; -import { MessageCode } from './code'; +import { MessageCode } from './code.js'; export class MessageCenter extends Observable { constructor() { diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index 478aa583d5..cbe5adef18 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -1,9 +1,13 @@ import { BaseProvider } from '../base.js'; -import type { ProviderConstructorParams } from '../base'; -import type { User, WorkspaceInfo, WorkspaceMeta } from '../../types'; +import type { + ProviderConstructorParams, + CreateWorkspaceInfoParams, + WorkspaceMeta0, +} from '../base'; +import type { User } from '../../types'; import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; import { BlockSchema } from '@blocksuite/blocks/models'; -import { applyUpdate } from 'yjs'; +import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { storage } from './storage.js'; import assert from 'assert'; import { WebsocketProvider } from './sync.js'; @@ -29,11 +33,6 @@ export class AffineProvider extends BaseProvider { constructor({ apis, ...params }: AffineProviderConstructorParams) { super(params); this._apis = apis || getApis(); - this.init().then(() => { - if (this._apis.token.isLogin) { - this.loadWorkspaces(); - } - }); } override async init() { @@ -64,6 +63,23 @@ export class AffineProvider extends BaseProvider { } } + private _getWebsocketProvider(workspace: BlocksuiteWorkspace) { + const { doc, room } = workspace; + assert(room); + assert(doc); + let ws = this._wsMap.get(room); + if (!ws) { + const wsUrl = `${ + window.location.protocol === 'https:' ? 'wss' : 'ws' + }://${window.location.host}/api/sync/`; + ws = new WebsocketProvider(wsUrl, room, doc, { + params: { token: this._apis.token.refresh }, + }); + this._wsMap.set(room, ws); + } + return ws; + } + private async _applyCloudUpdates(blocksuiteWorkspace: BlocksuiteWorkspace) { const { doc, room: workspaceId } = blocksuiteWorkspace; assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).'); @@ -78,20 +94,10 @@ export class AffineProvider extends BaseProvider { override async warpWorkspace(workspace: BlocksuiteWorkspace) { await this._applyCloudUpdates(workspace); - const { doc, room } = workspace; + const { room } = workspace; assert(room); this.linkLocal(workspace); - - let ws = this._wsMap.get(room); - if (!ws) { - const wsUrl = `${ - window.location.protocol === 'https:' ? 'wss' : 'ws' - }://${window.location.host}/api/sync/`; - ws = new WebsocketProvider(wsUrl, room, doc, { - params: { token: this._apis.token.refresh }, - }); - this._wsMap.set(room, ws); - } + const ws = this._getWebsocketProvider(workspace); // close all websocket links Array.from(this._wsMap.entries()).forEach(([id, ws]) => { if (id !== room) { @@ -115,12 +121,13 @@ export class AffineProvider extends BaseProvider { return []; } const workspacesList = await this._apis.getWorkspaces(); - const workspaces: WorkspaceInfo[] = workspacesList.map(w => { + const workspaces: WorkspaceMeta0[] = workspacesList.map(w => { return { ...w, memberCount: 0, name: '', provider: 'affine', + syncMode: 'core', }; }); const workspaceInstances = workspaces.map(({ id }) => { @@ -268,19 +275,17 @@ export class AffineProvider extends BaseProvider { } public override async createWorkspaceInfo( - meta: WorkspaceMeta - ): Promise { - const { id } = await this._apis.createWorkspace( - meta as Required - ); + meta: CreateWorkspaceInfoParams + ): Promise { + const { id } = await this._apis.createWorkspace(meta); - const workspaceInfo: WorkspaceInfo = { + const workspaceInfo: WorkspaceMeta0 = { name: meta.name, id: id, - isPublish: false, + published: false, avatar: '', owner: await this.getUserInfo(), - isLocal: true, + syncMode: 'core', memberCount: 1, provider: 'affine', }; @@ -289,7 +294,7 @@ export class AffineProvider extends BaseProvider { public override async createWorkspace( blocksuiteWorkspace: BlocksuiteWorkspace, - meta: WorkspaceMeta + meta: WorkspaceMeta0 ): Promise { const workspaceId = blocksuiteWorkspace.room; assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).'); @@ -298,13 +303,13 @@ export class AffineProvider extends BaseProvider { this._applyCloudUpdates(blocksuiteWorkspace); this.linkLocal(blocksuiteWorkspace); - const workspaceInfo: WorkspaceInfo = { + const workspaceInfo: WorkspaceMeta0 = { name: meta.name, id: workspaceId, - isPublish: false, + published: false, avatar: '', owner: undefined, - isLocal: true, + syncMode: 'core', memberCount: 1, provider: 'affine', }; @@ -335,4 +340,21 @@ export class AffineProvider extends BaseProvider { } : null; } + + public override async assign( + to: BlocksuiteWorkspace, + from: BlocksuiteWorkspace + ): Promise { + assert(to.room, 'Blocksuite Workspace without room(workspaceId).'); + const ws = this._getWebsocketProvider(to); + applyUpdate(to.doc, encodeStateAsUpdate(from.doc)); + await new Promise((resolve, reject) => { + ws.once('synced', () => { + resolve(); + }); + ws.once('lost-connection', () => reject()); + ws.once('connection-error', () => reject()); + }); + return to; + } } diff --git a/packages/data-center/src/provider/affine/apis/workspace.ts b/packages/data-center/src/provider/affine/apis/workspace.ts index 0c145928d5..8bdc25b21f 100644 --- a/packages/data-center/src/provider/affine/apis/workspace.ts +++ b/packages/data-center/src/provider/affine/apis/workspace.ts @@ -79,7 +79,6 @@ export async function getWorkspaceMembers( export interface CreateWorkspaceParams { name: string; - avatar: string; } export async function createWorkspace( diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts index f9851098df..6542b279f1 100644 --- a/packages/data-center/src/provider/base.ts +++ b/packages/data-center/src/provider/base.ts @@ -1,7 +1,8 @@ import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store'; import { MessageCenter } from '../message'; -import { Logger, User, WorkspaceInfo, WorkspaceMeta } from '../types'; -import type { WorkspaceMetaCollectionScope } from '../workspace-meta-collection'; +import { Logger, User } from '../types'; +import type { WorkspaceUnitCollectionScope } from '../workspace-unit-collection'; +import type { WorkspaceUnitCtorParams } from '../workspace-unit'; const defaultLogger = () => { return; @@ -9,13 +10,19 @@ const defaultLogger = () => { export interface ProviderConstructorParams { logger?: Logger; - workspaces: WorkspaceMetaCollectionScope; + workspaces: WorkspaceUnitCollectionScope; messageCenter: MessageCenter; } +export type WorkspaceMeta0 = WorkspaceUnitCtorParams; +export type CreateWorkspaceInfoParams = Pick; +export type UpdateWorkspaceMetaParams = Partial< + Pick +>; + export class BaseProvider { public readonly id: string = 'base'; - protected _workspaces!: WorkspaceMetaCollectionScope; + protected _workspaces!: WorkspaceUnitCollectionScope; protected _logger!: Logger; protected _messageCenter!: MessageCenter; @@ -37,8 +44,8 @@ export class BaseProvider { } public async createWorkspaceInfo( - meta: WorkspaceMeta - ): Promise { + params: CreateWorkspaceInfoParams + ): Promise { throw new Error(`provider: ${this.id} createWorkspaceInfo Not implemented`); } @@ -70,7 +77,7 @@ export class BaseProvider { /** * load workspaces **/ - public async loadWorkspaces(): Promise { + public async loadWorkspaces(): Promise { throw new Error(`provider: ${this.id} loadWorkSpace Not implemented`); } @@ -157,10 +164,10 @@ export class BaseProvider { */ public async updateWorkspaceMeta( id: string, - meta: Partial + params: UpdateWorkspaceMetaParams ): Promise { id; - meta; + params; return; } @@ -170,7 +177,7 @@ export class BaseProvider { */ public async createWorkspace( blocksuiteWorkspace: BlocksuiteWorkspace, - meta: WorkspaceMeta + meta: WorkspaceMeta0 ): Promise { return blocksuiteWorkspace; } @@ -196,4 +203,14 @@ export class BaseProvider { ): Promise { return workspace; } + + /** + * merge one workspaces to another + * @param workspace + * @returns + */ + public async assign(to: BlocksuiteWorkspace, from: BlocksuiteWorkspace) { + from; + return to; + } } diff --git a/packages/data-center/src/provider/local/indexeddb.ts b/packages/data-center/src/provider/local/indexeddb/indexeddb.ts similarity index 100% rename from packages/data-center/src/provider/local/indexeddb.ts rename to packages/data-center/src/provider/local/indexeddb/indexeddb.ts diff --git a/packages/data-center/src/provider/local/indexeddb/utils.ts b/packages/data-center/src/provider/local/indexeddb/utils.ts new file mode 100644 index 0000000000..8ca5c9b46d --- /dev/null +++ b/packages/data-center/src/provider/local/indexeddb/utils.ts @@ -0,0 +1,20 @@ +import assert from 'assert'; +import * as idb from 'lib0/indexeddb.js'; +import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; + +const { encodeStateAsUpdate } = BlocksuiteWorkspace.Y; + +export const initStore = async (blocksuiteWorkspace: BlocksuiteWorkspace) => { + const workspaceId = blocksuiteWorkspace.room; + assert(workspaceId); + await idb.deleteDB(workspaceId); + const db = await idb.openDB(workspaceId, db => + idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']]) + ); + const currState = encodeStateAsUpdate(blocksuiteWorkspace.doc); + const [updatesStore] = idb.transact(db, ['updates']); // , 'readonly') + + if (updatesStore) { + await idb.addAutoKey(updatesStore, currState); + } +}; diff --git a/packages/data-center/src/provider/local/local.spec.ts b/packages/data-center/src/provider/local/local.spec.ts index eff462ef86..e60ce31aae 100644 --- a/packages/data-center/src/provider/local/local.spec.ts +++ b/packages/data-center/src/provider/local/local.spec.ts @@ -1,13 +1,15 @@ import { test, expect } from '@playwright/test'; -import { WorkspaceMetaCollection } from '../../workspace-meta-collection.js'; +import { WorkspaceUnitCollection } from '../../workspace-unit-collection.js'; import { LocalProvider } from './local.js'; import { createBlocksuiteWorkspace } from '../../utils/index.js'; +import { MessageCenter } from '../../message/index.js'; import 'fake-indexeddb/auto'; test.describe.serial('local provider', () => { - const workspaceMetaCollection = new WorkspaceMetaCollection(); + const workspaceMetaCollection = new WorkspaceUnitCollection(); const provider = new LocalProvider({ workspaces: workspaceMetaCollection.createScope(), + messageCenter: new MessageCenter(), }); const workspaceName = 'workspace-test'; @@ -16,23 +18,20 @@ test.describe.serial('local provider', () => { test('create workspace', async () => { const workspaceInfo = await provider.createWorkspaceInfo({ name: workspaceName, - avatar: 'avatar-url-test', }); workspaceId = workspaceInfo.id; const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceId); - await provider.createWorkspace(blocksuiteWorkspace, { - name: workspaceName, - avatar: 'avatar-url-test', - }); + await provider.createWorkspace(blocksuiteWorkspace, workspaceInfo); expect(workspaceMetaCollection.workspaces.length).toEqual(1); expect(workspaceMetaCollection.workspaces[0].name).toEqual(workspaceName); }); test('workspace list cache', async () => { - const workspacesMetaCollection1 = new WorkspaceMetaCollection(); + const workspacesMetaCollection1 = new WorkspaceUnitCollection(); const provider1 = new LocalProvider({ workspaces: workspacesMetaCollection1.createScope(), + messageCenter: new MessageCenter(), }); await provider1.loadWorkspaces(); expect(workspacesMetaCollection1.workspaces.length).toEqual(1); diff --git a/packages/data-center/src/provider/local/local.ts b/packages/data-center/src/provider/local/local.ts index 89a199fb66..591a02bf00 100644 --- a/packages/data-center/src/provider/local/local.ts +++ b/packages/data-center/src/provider/local/local.ts @@ -1,9 +1,14 @@ import { BaseProvider } from '../base.js'; -import type { ProviderConstructorParams } from '../base'; +import type { + ProviderConstructorParams, + WorkspaceMeta0, + UpdateWorkspaceMetaParams, + CreateWorkspaceInfoParams, +} from '../base'; import { varStorage as storage } from 'lib0/storage'; -import { WorkspaceInfo, WorkspaceMeta } from '../../types'; import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store'; -import { IndexedDBProvider } from './indexeddb.js'; +import { IndexedDBProvider } from './indexeddb/indexeddb.js'; +import { initStore } from './indexeddb/utils.js'; import assert from 'assert'; import { setDefaultAvatar } from '../utils.js'; @@ -15,10 +20,9 @@ export class LocalProvider extends BaseProvider { constructor(params: ProviderConstructorParams) { super(params); - this.loadWorkspaces(); } - private _storeWorkspaces(workspaces: WorkspaceInfo[]) { + private _storeWorkspaces(workspaces: WorkspaceMeta0[]) { storage.setItem(WORKSPACE_KEY, JSON.stringify(workspaces)); } @@ -41,12 +45,12 @@ export class LocalProvider extends BaseProvider { return workspace; } - override loadWorkspaces(): Promise { + override loadWorkspaces(): Promise { const workspaceStr = storage.getItem(WORKSPACE_KEY); - let workspaces: WorkspaceInfo[] = []; + let workspaces: WorkspaceMeta0[] = []; if (workspaceStr) { try { - workspaces = JSON.parse(workspaceStr) as WorkspaceInfo[]; + workspaces = JSON.parse(workspaceStr) as WorkspaceMeta0[]; workspaces.forEach(workspace => { this._workspaces.add(workspace); }); @@ -70,22 +74,22 @@ export class LocalProvider extends BaseProvider { public override async updateWorkspaceMeta( id: string, - meta: Partial + meta: UpdateWorkspaceMetaParams ) { this._workspaces.update(id, meta); this._storeWorkspaces(this._workspaces.list()); } public override async createWorkspaceInfo( - meta: WorkspaceMeta - ): Promise { - const workspaceInfo: WorkspaceInfo = { + meta: CreateWorkspaceInfoParams + ): Promise { + const workspaceInfo: WorkspaceMeta0 = { name: meta.name, id: uuidv4(), - isPublish: false, + published: false, avatar: '', owner: undefined, - isLocal: true, + syncMode: 'core', memberCount: 1, provider: 'local', }; @@ -94,25 +98,16 @@ export class LocalProvider extends BaseProvider { public override async createWorkspace( blocksuiteWorkspace: BlocksuiteWorkspace, - meta: WorkspaceMeta + meta: WorkspaceMeta0 ): Promise { const workspaceId = blocksuiteWorkspace.room; assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).'); - assert(meta.name, 'Workspace name is required'); this._logger('Creating affine workspace'); - const workspaceInfo: WorkspaceInfo = { - name: meta.name, - id: workspaceId, - isPublish: false, - avatar: '', - owner: undefined, - isLocal: true, - memberCount: 1, - provider: 'local', + const workspaceInfo: WorkspaceMeta0 = { + ...meta, }; - this.linkLocal(blocksuiteWorkspace); blocksuiteWorkspace.meta.setName(meta.name); if (!meta.avatar) { @@ -120,6 +115,8 @@ export class LocalProvider extends BaseProvider { workspaceInfo.avatar = blocksuiteWorkspace.meta.avatar; } + await initStore(blocksuiteWorkspace); + this._workspaces.add(workspaceInfo); this._storeWorkspaces(this._workspaces.list()); diff --git a/packages/data-center/src/provider/utils.ts b/packages/data-center/src/provider/utils.ts index d8e5f6d058..c193719cc2 100644 --- a/packages/data-center/src/provider/utils.ts +++ b/packages/data-center/src/provider/utils.ts @@ -5,6 +5,9 @@ import { getDefaultHeadImgBlob } from '../utils/index.js'; export const setDefaultAvatar = async ( blocksuiteWorkspace: BlocksuiteWorkspace ) => { + if (typeof document === 'undefined') { + return; + } const blob = await getDefaultHeadImgBlob(blocksuiteWorkspace.meta.name); const blobStorage = await blocksuiteWorkspace.blobs; assert(blobStorage, 'No blob storage'); diff --git a/packages/data-center/src/types/index.ts b/packages/data-center/src/types/index.ts index 29e3a2f255..ea85ded82f 100644 --- a/packages/data-center/src/types/index.ts +++ b/packages/data-center/src/types/index.ts @@ -1,15 +1,15 @@ import { getLogger } from '../logger'; -export type WorkspaceInfo = { - name: string; - id: string; - isPublish?: boolean; - avatar?: string; - owner?: User; - isLocal?: boolean; - memberCount: number; - provider: string; -}; +// export type WorkspaceInfo = { +// name: string; +// id: string; +// isPublish?: boolean; +// avatar?: string; +// owner?: User; +// isLocal?: boolean; +// memberCount: number; +// provider: string; +// }; export type User = { name: string; @@ -18,7 +18,7 @@ export type User = { avatar: string; }; -export type WorkspaceMeta = Pick; +// export type WorkspaceMeta = Pick; export type Logger = ReturnType; diff --git a/packages/data-center/src/workspace-meta-collection.spec.ts b/packages/data-center/src/workspace-meta-collection.spec.ts deleted file mode 100644 index afb3eb65f5..0000000000 --- a/packages/data-center/src/workspace-meta-collection.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { WorkspaceMetaCollection } from './workspace-meta-collection.js'; -import type { WorkspaceMetaCollectionChangeEvent } from './workspace-meta-collection'; - -test.describe.serial('workspace meta collection observable', () => { - const workspaces = new WorkspaceMetaCollection(); - - const scope = workspaces.createScope(); - - test('add workspace', () => { - workspaces.once('change', (event: WorkspaceMetaCollectionChangeEvent) => { - expect(event.added?.id).toEqual('123'); - }); - scope.add({ - id: '123', - name: 'test', - memberCount: 1, - provider: '', - }); - }); - - test('list workspace', () => { - const list = scope.list(); - expect(list.length).toEqual(1); - expect(list[0].id).toEqual('123'); - }); - - test('get workspace', () => { - expect(scope.get('123')?.id).toEqual('123'); - }); - - test('update workspace', () => { - workspaces.once('change', (event: WorkspaceMetaCollectionChangeEvent) => { - expect(event.updated?.name).toEqual('demo'); - }); - scope.update('123', { name: 'demo' }); - }); - - test('get workspace form other scope', () => { - const scope1 = workspaces.createScope(); - expect(scope1.get('123')).toBeFalsy(); - }); - - test('delete workspace', () => { - workspaces.once('change', (event: WorkspaceMetaCollectionChangeEvent) => { - expect(event.deleted?.id).toEqual('123'); - }); - scope.remove('123'); - }); -}); diff --git a/packages/data-center/src/workspace-unit-collection.spec.ts b/packages/data-center/src/workspace-unit-collection.spec.ts new file mode 100644 index 0000000000..06eee95bd0 --- /dev/null +++ b/packages/data-center/src/workspace-unit-collection.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@playwright/test'; +import { WorkspaceUnitCollection } from './workspace-unit-collection.js'; +import type { WorkspaceUnitCollectionChangeEvent } from './workspace-unit-collection'; + +test.describe.serial('workspace meta collection observable', () => { + const workspaceUnitCollection = new WorkspaceUnitCollection(); + + const scope = workspaceUnitCollection.createScope(); + + test('add workspace', () => { + workspaceUnitCollection.once( + 'change', + (event: WorkspaceUnitCollectionChangeEvent) => { + expect(event.added?.id).toEqual('123'); + } + ); + scope.add({ + id: '123', + name: 'test', + memberCount: 1, + provider: '', + syncMode: 'core', + }); + }); + + test('list workspace', () => { + const list = scope.list(); + expect(list.length).toEqual(1); + expect(list[0].id).toEqual('123'); + }); + + test('get workspace', () => { + expect(scope.get('123')?.id).toEqual('123'); + }); + + test('update workspace', () => { + workspaceUnitCollection.once( + 'change', + (event: WorkspaceUnitCollectionChangeEvent) => { + expect(event.updated?.name).toEqual('demo'); + } + ); + scope.update('123', { name: 'demo' }); + }); + + test('get workspace form other scope', () => { + const scope1 = workspaceUnitCollection.createScope(); + expect(scope1.get('123')).toBeFalsy(); + }); + + test('delete workspace', () => { + workspaceUnitCollection.once( + 'change', + (event: WorkspaceUnitCollectionChangeEvent) => { + expect(event.deleted?.id).toEqual('123'); + } + ); + scope.remove('123'); + }); +}); diff --git a/packages/data-center/src/workspace-unit-collection.ts b/packages/data-center/src/workspace-unit-collection.ts new file mode 100644 index 0000000000..58770e2122 --- /dev/null +++ b/packages/data-center/src/workspace-unit-collection.ts @@ -0,0 +1,152 @@ +import { Observable } from 'lib0/observable'; +import { WorkspaceUnit } from './workspace-unit.js'; +import type { + WorkspaceUnitCtorParams, + UpdateWorkspaceUnitParams, +} from './workspace-unit'; + +export interface WorkspaceUnitCollectionScope { + get: (workspaceId: string) => WorkspaceUnit | undefined; + list: () => WorkspaceUnit[]; + add: (workspace: WorkspaceUnitCtorParams) => void; + remove: (workspaceId: string) => boolean; + clear: () => void; + update: ( + workspaceId: string, + workspaceMeta: UpdateWorkspaceUnitParams + ) => void; +} + +export interface WorkspaceUnitCollectionChangeEvent { + added?: WorkspaceUnit[]; + deleted?: WorkspaceUnit; + updated?: WorkspaceUnit; +} + +export class WorkspaceUnitCollection { + private _events = new Observable(); + private _workspaceUnitMap = new Map(); + + get workspaces(): WorkspaceUnit[] { + return Array.from(this._workspaceUnitMap.values()); + } + + public on( + type: 'change', + callback: (event: WorkspaceUnitCollectionChangeEvent) => void + ) { + this._events.on(type, callback); + } + + public once( + type: 'change', + callback: (event: WorkspaceUnitCollectionChangeEvent) => void + ) { + this._events.once(type, callback); + } + + find(workspaceId: string) { + return this._workspaceUnitMap.get(workspaceId); + } + + createScope(): WorkspaceUnitCollectionScope { + const scopedWorkspaceIds = new Set(); + + const get = (workspaceId: string) => { + if (!scopedWorkspaceIds.has(workspaceId)) { + return; + } + return this._workspaceUnitMap.get(workspaceId); + }; + + const add = (workspace: WorkspaceUnitCtorParams) => { + if (this._workspaceUnitMap.has(workspace.id)) { + throw new Error(`Duplicate workspace id.`); + } + + const workspaceUnit = new WorkspaceUnit(workspace); + this._workspaceUnitMap.set(workspace.id, workspaceUnit); + + scopedWorkspaceIds.add(workspace.id); + + this._events.emit('change', [ + { + added: [workspaceUnit], + } as WorkspaceUnitCollectionChangeEvent, + ]); + }; + + const remove = (workspaceId: string) => { + if (!scopedWorkspaceIds.has(workspaceId)) { + return true; + } + + const workspaceUnit = this._workspaceUnitMap.get(workspaceId); + if (workspaceUnit) { + const ret = this._workspaceUnitMap.delete(workspaceId); + // If deletion failed, return. + if (!ret) { + return ret; + } + + scopedWorkspaceIds.delete(workspaceId); + + this._events.emit('change', [ + { + deleted: workspaceUnit, + } as WorkspaceUnitCollectionChangeEvent, + ]); + } + return true; + }; + + const clear = () => { + scopedWorkspaceIds.forEach(id => { + remove(id); + }); + }; + + const update = ( + workspaceId: string, + workspaceMeta: UpdateWorkspaceUnitParams + ) => { + if (!scopedWorkspaceIds.has(workspaceId)) { + return true; + } + + const workspaceUnit = this._workspaceUnitMap.get(workspaceId); + if (!workspaceUnit) { + return true; + } + + workspaceUnit.update(workspaceMeta); + + this._events.emit('change', [ + { + updated: workspaceUnit, + } as WorkspaceUnitCollectionChangeEvent, + ]); + }; + + // TODO: need to optimize + const list = () => { + const workspaceUnits: WorkspaceUnit[] = []; + scopedWorkspaceIds.forEach(id => { + const workspaceUnit = this._workspaceUnitMap.get(id); + if (workspaceUnit) { + workspaceUnits.push(workspaceUnit); + } + }); + return workspaceUnits; + }; + + return { + get, + list, + add, + remove, + clear, + update, + }; + } +} diff --git a/packages/data-center/src/workspace-unit.ts b/packages/data-center/src/workspace-unit.ts new file mode 100644 index 0000000000..527e8647ef --- /dev/null +++ b/packages/data-center/src/workspace-unit.ts @@ -0,0 +1,64 @@ +import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; +import type { User } from './types'; + +export type SyncMode = 'all' | 'core'; + +export interface WorkspaceUnitCtorParams { + id: string; + name: string; + avatar?: string; + owner?: User; + published?: boolean; + memberCount: number; + provider: string; + syncMode: SyncMode; + + blocksuiteWorkspace?: BlocksuiteWorkspace; +} + +export type UpdateWorkspaceUnitParams = Partial< + Omit +>; + +export class WorkspaceUnit { + public readonly id: string; + public name!: string; + public avatar?: string; + public owner?: User; + public published?: boolean; + public memberCount!: number; + public provider!: string; + public syncMode: 'all' | 'core' = 'core'; + + private _blocksuiteWorkspace?: BlocksuiteWorkspace; + + constructor(params: WorkspaceUnitCtorParams) { + this.id = params.id; + this.update(params); + } + + get blocksuiteWorkspace() { + return this._blocksuiteWorkspace; + } + + setBlocksuiteWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace) { + if (blocksuiteWorkspace?.room !== this.id) { + throw new Error('Workspace id inconsistent.'); + } + this._blocksuiteWorkspace = blocksuiteWorkspace; + } + + update(params: UpdateWorkspaceUnitParams) { + Object.assign(this, params); + } + + get isPublish() { + console.error('Suggest changing to published'); + return this.published; + } + + get isLocal() { + console.error('Suggest changing to syncMode'); + return this.syncMode === 'all'; + } +}