Files
AFFiNE-Mirror/packages/frontend/workspace/src/manager.ts
T
2023-12-15 07:20:50 +00:00

186 lines
5.9 KiB
TypeScript

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<void>
): Promise<string> {
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<WorkspaceMetadata> {
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;
}
}