import { DebugLogger } from '@affine/debug'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { assertEquals } from '@blocksuite/global/utils'; import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import { fixWorkspaceVersion } from '@toeverything/infra/blocksuite'; import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import type { BlobStorage } from '.'; import type { WorkspaceFactory } from './factory'; import { LOCAL_WORKSPACE_LOCAL_STORAGE_KEY } from './impl/local/consts'; import type { WorkspaceList } from './list'; import type { WorkspaceMetadata } from './metadata'; import { WorkspacePool } from './pool'; import type { Workspace } from './workspace'; const logger = new DebugLogger('affine:workspace-manager'); /** * # `WorkspaceManager` * * This class acts as the central hub for managing various aspects of workspaces. * It is structured as follows: * * ``` * ┌───────────┐ * │ Workspace │ * │ Manager │ * └─────┬─────┘ * ┌─────────────┼─────────────┐ * ┌───┴───┐ ┌───┴───┐ ┌─────┴─────┐ * │ List │ │ Pool │ │ Factories │ * └───────┘ └───────┘ └───────────┘ * ``` * * Manage every about workspace * * # List * * The `WorkspaceList` component stores metadata for all workspaces, also include workspace avatar and custom name. * * # Factories * * This class contains a collection of `WorkspaceFactory`, * We utilize `metadata.flavour` to identify the appropriate factory for opening a workspace. * Once opened, workspaces are stored in the `WorkspacePool`. * * # Pool * * The `WorkspacePool` use reference counting to manage active workspaces. * Calling `use()` to create a reference to the workspace. Calling `release()` to release the reference. * When the reference count is 0, it will close the workspace. * */ export class WorkspaceManager { pool: WorkspacePool = new WorkspacePool(); constructor( public list: WorkspaceList, public factories: WorkspaceFactory[] ) {} /** * get workspace reference by metadata. * * You basically don't need to call this function directly, use the react hook `useWorkspace(metadata)` instead. * * @returns the workspace reference and a release function, don't forget to call release function when you don't * need the workspace anymore. */ use(metadata: WorkspaceMetadata): { workspace: Workspace; release: () => void; } { const exist = this.pool.get(metadata.id); if (exist) { return exist; } const workspace = this.open(metadata); const ref = this.pool.put(workspace); return ref; } createWorkspace( flavour: WorkspaceFlavour, initial: ( workspace: BlockSuiteWorkspace, blobStorage: BlobStorage ) => Promise ): Promise { logger.info(`create workspace [${flavour}]`); return this.list.create(flavour, initial); } /** * delete workspace by metadata, same as `WorkspaceList.deleteWorkspace` */ async deleteWorkspace(metadata: WorkspaceMetadata) { await this.list.delete(metadata); } /** * helper function to transform local workspace to cloud workspace */ async transformLocalToCloud(local: Workspace): Promise { assertEquals(local.flavour, WorkspaceFlavour.LOCAL); await local.engine.sync.waitForSynced(); const newId = await this.list.create( WorkspaceFlavour.AFFINE_CLOUD, async (ws, bs) => { applyUpdate(ws.doc, encodeStateAsUpdate(local.blockSuiteWorkspace.doc)); for (const subdoc of local.blockSuiteWorkspace.doc.getSubdocs()) { for (const newSubdoc of ws.doc.getSubdocs()) { if (newSubdoc.guid === subdoc.guid) { applyUpdate(newSubdoc, encodeStateAsUpdate(subdoc)); } } } const blobList = await local.engine.blob.list(); for (const blobKey of blobList) { const blob = await local.engine.blob.get(blobKey); if (blob) { await bs.set(blobKey, blob); } } } ); await this.list.delete(local.meta); return { id: newId, flavour: WorkspaceFlavour.AFFINE_CLOUD, }; } /** * helper function to get blob without open workspace, its be used for download workspace avatars. */ getWorkspaceBlob(metadata: WorkspaceMetadata, blobKey: string) { const factory = this.factories.find(x => x.name === metadata.flavour); if (!factory) { throw new Error(`Unknown workspace flavour: ${metadata.flavour}`); } return factory.getWorkspaceBlob(metadata.id, blobKey); } /** * a hack for directly add local workspace to workspace list * Used after copying sqlite database file to appdata folder */ _addLocalWorkspace(id: string) { const allWorkspaceIDs: string[] = JSON.parse( localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]' ); allWorkspaceIDs.push(id); localStorage.setItem( LOCAL_WORKSPACE_LOCAL_STORAGE_KEY, JSON.stringify(allWorkspaceIDs) ); } private open(metadata: WorkspaceMetadata) { logger.info(`open workspace [${metadata.flavour}] ${metadata.id} `); const factory = this.factories.find(x => x.name === metadata.flavour); if (!factory) { throw new Error(`Unknown workspace flavour: ${metadata.flavour}`); } const workspace = factory.openWorkspace(metadata); // sync information with workspace list, when workspace's avatar and name changed, information will be updated this.list.getInformation(metadata).syncWithWorkspace(workspace); // apply compatibility fix fixWorkspaceVersion(workspace.blockSuiteWorkspace.doc); return workspace; } }