diff --git a/blocksuite/affine/all/package.json b/blocksuite/affine/all/package.json index a14b639d57c39..d0cfb95a57f10 100644 --- a/blocksuite/affine/all/package.json +++ b/blocksuite/affine/all/package.json @@ -18,7 +18,8 @@ "@blocksuite/global": "workspace:*", "@blocksuite/inline": "workspace:*", "@blocksuite/presets": "workspace:*", - "@blocksuite/store": "workspace:*" + "@blocksuite/store": "workspace:*", + "@blocksuite/sync": "workspace:*" }, "exports": { ".": "./src/index.ts", @@ -37,7 +38,8 @@ "./inline/types": "./src/inline/types.ts", "./presets": "./src/presets/index.ts", "./blocks": "./src/blocks/index.ts", - "./blocks/schemas": "./src/blocks/schemas.ts" + "./blocks/schemas": "./src/blocks/schemas.ts", + "./sync": "./src/sync/index.ts" }, "typesVersions": { "*": { @@ -88,6 +90,9 @@ ], "blocks/schemas": [ "dist/blocks/schemas.d.ts" + ], + "sync": [ + "dist/sync/index.d.ts" ] } }, diff --git a/blocksuite/affine/all/src/sync/index.ts b/blocksuite/affine/all/src/sync/index.ts new file mode 100644 index 0000000000000..415641e2a6450 --- /dev/null +++ b/blocksuite/affine/all/src/sync/index.ts @@ -0,0 +1 @@ +export * from '@blocksuite/sync'; diff --git a/blocksuite/framework/store/src/store/collection.ts b/blocksuite/framework/store/src/store/collection.ts index f71ca30b27382..2b89845e0afa3 100644 --- a/blocksuite/framework/store/src/store/collection.ts +++ b/blocksuite/framework/store/src/store/collection.ts @@ -70,10 +70,6 @@ const FLAGS_PRESET = { readonly: {}, } satisfies BlockSuiteFlags; -export interface StackItem { - meta: Map<'cursor-location' | 'selection-state', unknown>; -} - export class DocCollection implements Workspace { protected readonly _schema: Schema; diff --git a/blocksuite/framework/store/src/store/index.ts b/blocksuite/framework/store/src/store/index.ts index 8024bbd343dee..b7ab3da9b484c 100644 --- a/blocksuite/framework/store/src/store/index.ts +++ b/blocksuite/framework/store/src/store/index.ts @@ -3,5 +3,5 @@ export { DocCollection } from './collection.js'; export type * from './doc/block-collection.js'; export * from './doc/index.js'; export * from './id.js'; -export type * from './meta.js'; +export * from './meta.js'; export * from './workspace.js'; diff --git a/blocksuite/framework/store/src/store/workspace.ts b/blocksuite/framework/store/src/store/workspace.ts index a75e775f652fe..f5b64bfeeb369 100644 --- a/blocksuite/framework/store/src/store/workspace.ts +++ b/blocksuite/framework/store/src/store/workspace.ts @@ -84,3 +84,7 @@ export interface Workspace { dispose(): void; } + +export interface StackItem { + meta: Map<'cursor-location' | 'selection-state', unknown>; +} diff --git a/blocksuite/framework/tsconfig.json b/blocksuite/framework/tsconfig.json index dae3abee89242..ff7f2caac1b04 100644 --- a/blocksuite/framework/tsconfig.json +++ b/blocksuite/framework/tsconfig.json @@ -13,6 +13,9 @@ }, { "path": "./store" + }, + { + "path": "./sync" } ] } diff --git a/packages/frontend/core/src/blocksuite/presets/_common/utils/markdown-utils.ts b/packages/frontend/core/src/blocksuite/presets/_common/utils/markdown-utils.ts index bbdae671eb98b..f9dfb803c93a8 100644 --- a/packages/frontend/core/src/blocksuite/presets/_common/utils/markdown-utils.ts +++ b/packages/frontend/core/src/blocksuite/presets/_common/utils/markdown-utils.ts @@ -1,3 +1,4 @@ +import { WorkspaceImpl } from '@affine/core/modules/workspace/impl/workspace'; import type { EditorHost, TextRangePoint, @@ -14,7 +15,7 @@ import { } from '@blocksuite/affine/blocks'; import type { ServiceProvider } from '@blocksuite/affine/global/di'; import type { JobMiddleware, Schema } from '@blocksuite/affine/store'; -import { DocCollection, Job } from '@blocksuite/affine/store'; +import { Job } from '@blocksuite/affine/store'; import { assertExists } from '@blocksuite/global/utils'; import type { BlockModel, @@ -207,7 +208,7 @@ export async function markDownToDoc( additionalMiddlewares?: JobMiddleware[] ) { // Should not create a new doc in the original collection - const collection = new DocCollection({ + const collection = new WorkspaceImpl({ schema, }); collection.meta.initialize(); diff --git a/packages/frontend/core/src/blocksuite/presets/ai/messages/slides-renderer.ts b/packages/frontend/core/src/blocksuite/presets/ai/messages/slides-renderer.ts index 708ff7aa039ef..815ebfff2026d 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/messages/slides-renderer.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/messages/slides-renderer.ts @@ -1,3 +1,4 @@ +import { WorkspaceImpl } from '@affine/core/modules/workspace/impl/workspace'; import { BlockStdScope, type EditorHost } from '@blocksuite/affine/block-std'; import { type AffineAIPanelWidgetConfig, @@ -6,7 +7,7 @@ import { import { AffineSchemas } from '@blocksuite/affine/blocks/schemas'; import { WithDisposable } from '@blocksuite/affine/global/utils'; import type { Doc } from '@blocksuite/affine/store'; -import { DocCollection, Schema } from '@blocksuite/affine/store'; +import { Schema } from '@blocksuite/affine/store'; import { css, html, LitElement, nothing } from 'lit'; import { property, query } from 'lit/decorators.js'; import { createRef, type Ref, ref } from 'lit/directives/ref.js'; @@ -54,7 +55,7 @@ export class AISlidesRenderer extends WithDisposable(LitElement) { private _doc!: Doc; - private _docCollection: DocCollection | null = null; + private _docCollection: WorkspaceImpl | null = null; @query('editor-host') private accessor _editorHost!: EditorHost; @@ -220,7 +221,7 @@ export class AISlidesRenderer extends WithDisposable(LitElement) { super.connectedCallback(); const schema = new Schema().register(AffineSchemas); - const collection = new DocCollection({ + const collection = new WorkspaceImpl({ schema, id: 'SLIDES_PREVIEW', }); diff --git a/packages/frontend/core/src/blocksuite/presets/ai/mini-mindmap/mindmap-preview.ts b/packages/frontend/core/src/blocksuite/presets/ai/mini-mindmap/mindmap-preview.ts index fe9249616fa24..2835d4fb0c39a 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/mini-mindmap/mindmap-preview.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/mini-mindmap/mindmap-preview.ts @@ -1,3 +1,4 @@ +import { WorkspaceImpl } from '@affine/core/modules/workspace/impl/workspace.js'; import { BlockStdScope, type EditorHost } from '@blocksuite/affine/block-std'; import { MarkdownAdapter, @@ -13,7 +14,6 @@ import type { ServiceProvider } from '@blocksuite/affine/global/di'; import { WithDisposable } from '@blocksuite/affine/global/utils'; import { type Doc, - DocCollection, type DocCollectionOptions, IdGeneratorType, Job, @@ -109,7 +109,7 @@ export class MiniMindmapPreview extends WithDisposable(LitElement) { awarenessSources: [], }; - const collection = new DocCollection(options); + const collection = new WorkspaceImpl(options); collection.meta.initialize(); collection.start(); diff --git a/packages/frontend/core/src/components/affine/page-history-modal/data.ts b/packages/frontend/core/src/components/affine/page-history-modal/data.ts index d763d4b6b0c79..68913b3fbcb59 100644 --- a/packages/frontend/core/src/components/affine/page-history-modal/data.ts +++ b/packages/frontend/core/src/components/affine/page-history-modal/data.ts @@ -2,12 +2,13 @@ import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite- import { useDocCollectionPage } from '@affine/core/components/hooks/use-block-suite-workspace-page'; import { FetchService, GraphQLService } from '@affine/core/modules/cloud'; import { getAFFiNEWorkspaceSchema } from '@affine/core/modules/workspace'; +import { WorkspaceImpl } from '@affine/core/modules/workspace/impl/workspace'; import { DebugLogger } from '@affine/debug'; import type { ListHistoryQuery } from '@affine/graphql'; import { listHistoryQuery, recoverDocMutation } from '@affine/graphql'; import { i18nTime } from '@affine/i18n'; import { assertEquals } from '@blocksuite/affine/global/utils'; -import { DocCollection, type Workspace } from '@blocksuite/affine/store'; +import type { Workspace } from '@blocksuite/affine/store'; import { useService } from '@toeverything/infra'; import { useEffect, useMemo } from 'react'; import useSWRImmutable from 'swr/immutable'; @@ -114,11 +115,9 @@ const getOrCreateShellWorkspace = ( fetchService, graphQLService ); - docCollection = new DocCollection({ + docCollection = new WorkspaceImpl({ id: workspaceId, - blobSources: { - main: blobStorage, - }, + blobSource: blobStorage, schema: getAFFiNEWorkspaceSchema(), }); docCollectionMap.set(workspaceId, docCollection); diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/index.ts b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/index.ts index 01d12d4630255..a1476043c2bd3 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/index.ts +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/docs/index.ts @@ -1,16 +1,17 @@ +import { WorkspaceImpl } from '@affine/core/modules/workspace/impl/workspace'; import { AffineSchemas } from '@blocksuite/affine/blocks'; import type { Doc, DocSnapshot } from '@blocksuite/affine/store'; -import { DocCollection, Job, Schema } from '@blocksuite/affine/store'; +import { Job, Schema } from '@blocksuite/affine/store'; const getCollection = (() => { - let collection: DocCollection | null = null; + let collection: WorkspaceImpl | null = null; return async function () { if (collection) { return collection; } const schema = new Schema(); schema.register(AffineSchemas); - collection = new DocCollection({ schema }); + collection = new WorkspaceImpl({ schema }); collection.meta.initialize(); return collection; }; diff --git a/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts b/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts index 5a5b3048abdcb..645903c5e993a 100644 --- a/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts +++ b/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts @@ -14,7 +14,6 @@ import { import { Container } from '@blocksuite/affine/global/di'; import { createYProxy, - DocCollection, type DraftModel, Job, type JobMiddleware, @@ -35,6 +34,7 @@ import { } from 'yjs'; import { getAFFiNEWorkspaceSchema } from '../../workspace/global-schema'; +import { WorkspaceImpl } from '../../workspace/impl/workspace'; import type { BlockIndexSchema, DocIndexSchema } from '../schema'; import type { WorkerIngoingMessage, @@ -118,7 +118,7 @@ const bookmarkFlavours = new Set([ 'affine:embed-loom', ]); -const markdownPreviewDocCollection = new DocCollection({ +const markdownPreviewDocCollection = new WorkspaceImpl({ id: 'indexer', schema: blocksuiteSchema, }); diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts index f047a31bb488c..0631612d3f669 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts @@ -5,7 +5,6 @@ import { getWorkspaceInfoQuery, getWorkspacesQuery, } from '@affine/graphql'; -import { DocCollection } from '@blocksuite/affine/store'; import { type BlobStorage, catchErrorInto, @@ -20,7 +19,6 @@ import { Service, } from '@toeverything/infra'; import { isEqual } from 'lodash-es'; -import { nanoid } from 'nanoid'; import { EMPTY, map, mergeMap, Observable, switchMap } from 'rxjs'; import { encodeStateAsUpdate } from 'yjs'; @@ -43,6 +41,7 @@ import { type WorkspaceMetadata, type WorkspaceProfileInfo, } from '../../workspace'; +import { WorkspaceImpl } from '../../workspace/impl/workspace'; import type { WorkspaceEngineStorageProvider } from '../providers/engine'; import { BroadcastChannelAwarenessConnection } from './engine/awareness-broadcast-channel'; import { CloudAwarenessConnection } from './engine/awareness-cloud'; @@ -101,7 +100,7 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider { async createWorkspace( initial: ( - docCollection: DocCollection, + docCollection: WorkspaceImpl, blobStorage: BlobStorage, docStorage: DocStorage ) => Promise @@ -117,13 +116,10 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider { const blobStorage = this.storageProvider.getBlobStorage(workspaceId); const docStorage = this.storageProvider.getDocStorage(workspaceId); - const docCollection = new DocCollection({ + const docCollection = new WorkspaceImpl({ id: workspaceId, - idGenerator: () => nanoid(), schema: getAFFiNEWorkspaceSchema(), - blobSources: { - main: blobStorage, - }, + blobSource: blobStorage, }); try { diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/local.ts b/packages/frontend/core/src/modules/workspace-engine/impls/local.ts index 45c60b0f201a1..48a8de388ffb2 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/local.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/local.ts @@ -1,5 +1,4 @@ import { DebugLogger } from '@affine/debug'; -import { DocCollection } from '@blocksuite/affine/store'; import type { BlobStorage, DocStorage, @@ -20,6 +19,7 @@ import { type WorkspaceMetadata, type WorkspaceProfileInfo, } from '../../workspace'; +import { WorkspaceImpl } from '../../workspace/impl/workspace'; import type { WorkspaceEngineStorageProvider } from '../providers/engine'; import { BroadcastChannelAwarenessConnection } from './engine/awareness-broadcast-channel'; import { StaticBlobStorage } from './engine/blob-static'; @@ -79,7 +79,7 @@ class LocalWorkspaceFlavourProvider implements WorkspaceFlavourProvider { } async createWorkspace( initial: ( - docCollection: DocCollection, + docCollection: WorkspaceImpl, blobStorage: BlobStorage, docStorage: DocStorage ) => Promise @@ -90,11 +90,10 @@ class LocalWorkspaceFlavourProvider implements WorkspaceFlavourProvider { const blobStorage = this.storageProvider.getBlobStorage(id); const docStorage = this.storageProvider.getDocStorage(id); - const docCollection = new DocCollection({ + const docCollection = new WorkspaceImpl({ id: id, - idGenerator: () => nanoid(), schema: getAFFiNEWorkspaceSchema(), - blobSources: { main: blobStorage }, + blobSource: blobStorage, }); try { diff --git a/packages/frontend/core/src/modules/workspace/entities/workspace.ts b/packages/frontend/core/src/modules/workspace/entities/workspace.ts index edbc6684c957d..d96a2e5343541 100644 --- a/packages/frontend/core/src/modules/workspace/entities/workspace.ts +++ b/packages/frontend/core/src/modules/workspace/entities/workspace.ts @@ -1,14 +1,11 @@ -import { - DocCollection, - type Workspace as BSWorkspace, -} from '@blocksuite/affine/store'; +import type { Workspace as WorkspaceInterface } from '@blocksuite/affine/store'; import { Entity, LiveData } from '@toeverything/infra'; -import { nanoid } from 'nanoid'; import { Observable } from 'rxjs'; import type { Awareness } from 'y-protocols/awareness.js'; import { WorkspaceDBService } from '../../db'; import { getAFFiNEWorkspaceSchema } from '../global-schema'; +import { WorkspaceImpl } from '../impl/workspace'; import type { WorkspaceScope } from '../scopes/workspace'; import { WorkspaceEngineService } from '../services/engine'; @@ -25,16 +22,13 @@ export class Workspace extends Entity { readonly flavour = this.meta.flavour; - _docCollection: BSWorkspace | null = null; + _docCollection: WorkspaceInterface | null = null; get docCollection() { if (!this._docCollection) { - this._docCollection = new DocCollection({ + this._docCollection = new WorkspaceImpl({ id: this.openOptions.metadata.id, - blobSources: { - main: this.engine.blob, - }, - idGenerator: () => nanoid(), + blobSource: this.engine.blob, schema: getAFFiNEWorkspaceSchema(), }); this._docCollection.slots.docCreated.on(id => { diff --git a/packages/frontend/core/src/modules/workspace/impl/workspace.ts b/packages/frontend/core/src/modules/workspace/impl/workspace.ts new file mode 100644 index 0000000000000..115e4550bf36a --- /dev/null +++ b/packages/frontend/core/src/modules/workspace/impl/workspace.ts @@ -0,0 +1,234 @@ +import { + BlockSuiteError, + ErrorCode, +} from '@blocksuite/affine/global/exceptions'; +import type { BlockSuiteFlags } from '@blocksuite/affine/global/types'; +import { NoopLogger, Slot } from '@blocksuite/affine/global/utils'; +import { + AwarenessStore, + BlockCollection, + BlockSuiteDoc, + type CreateDocOptions, + type Doc, + DocCollectionMeta, + type GetDocOptions, + type IdGenerator, + nanoid, + type Schema, + type Workspace, +} from '@blocksuite/affine/store'; +import { + AwarenessEngine, + BlobEngine, + type BlobSource, + DocEngine, + MemoryBlobSource, + NoopDocSource, +} from '@blocksuite/affine/sync'; +import { Awareness } from 'y-protocols/awareness.js'; + +type WorkspaceOptions = { + id?: string; + schema: Schema; + blobSource?: BlobSource; +}; + +const FLAGS_PRESET = { + enable_synced_doc_block: false, + enable_pie_menu: false, + enable_database_number_formatting: false, + enable_database_attachment_note: false, + enable_database_full_width: false, + enable_block_query: false, + enable_lasso_tool: false, + enable_edgeless_text: true, + enable_ai_onboarding: false, + enable_ai_chat_block: false, + enable_color_picker: false, + enable_mind_map_import: false, + enable_advanced_block_visibility: false, + enable_shape_shadow_blur: false, + enable_mobile_keyboard_toolbar: false, + enable_mobile_linked_doc_menu: false, + readonly: {}, +} satisfies BlockSuiteFlags; + +export class WorkspaceImpl implements Workspace { + protected readonly _schema: Schema; + + readonly awarenessStore: AwarenessStore; + + readonly awarenessSync: AwarenessEngine; + + readonly blobSync: BlobEngine; + + readonly blockCollections = new Map(); + + readonly doc: BlockSuiteDoc; + + readonly docSync: DocEngine; + + readonly id: string; + + readonly idGenerator: IdGenerator; + + meta: DocCollectionMeta; + + slots = { + docListUpdated: new Slot(), + docRemoved: new Slot(), + docCreated: new Slot(), + }; + + get docs() { + return this.blockCollections; + } + + get schema() { + return this._schema; + } + + constructor({ id, schema, blobSource }: WorkspaceOptions) { + this._schema = schema; + + this.id = id || ''; + this.doc = new BlockSuiteDoc({ guid: id }); + this.awarenessStore = new AwarenessStore(new Awareness(this.doc), { + ...FLAGS_PRESET, + readonly: {}, + }); + + blobSource = blobSource ?? new MemoryBlobSource(); + const docSource = new NoopDocSource(); + const logger = new NoopLogger(); + + this.awarenessSync = new AwarenessEngine(this.awarenessStore.awareness, []); + this.docSync = new DocEngine(this.doc, docSource, [], logger); + this.blobSync = new BlobEngine(blobSource, [], logger); + + this.idGenerator = nanoid; + + this.meta = new DocCollectionMeta(this.doc); + this._bindDocMetaEvents(); + } + + private _bindDocMetaEvents() { + this.meta.docMetaAdded.on(docId => { + const doc = new BlockCollection({ + id: docId, + collection: this, + doc: this.doc, + awarenessStore: this.awarenessStore, + idGenerator: this.idGenerator, + }); + this.blockCollections.set(doc.id, doc); + }); + + this.meta.docMetaUpdated.on(() => this.slots.docListUpdated.emit()); + + this.meta.docMetaRemoved.on(id => { + const space = this.getBlockCollection(id); + if (!space) return; + this.blockCollections.delete(id); + space.remove(); + this.slots.docRemoved.emit(id); + }); + } + + private _hasDoc(docId: string) { + return this.docs.has(docId); + } + + /** + * Verify that all data has been successfully saved to the primary storage. + * Return true if the data transfer is complete and it is secure to terminate the synchronization operation. + */ + canGracefulStop() { + this.docSync.canGracefulStop(); + } + + /** + * By default, only an empty doc will be created. + * If the `init` parameter is passed, a `surface`, `note`, and `paragraph` block + * will be created in the doc simultaneously. + */ + createDoc(options: CreateDocOptions = {}) { + const { id: docId = this.idGenerator(), query, readonly } = options; + if (this._hasDoc(docId)) { + throw new BlockSuiteError( + ErrorCode.DocCollectionError, + 'doc already exists' + ); + } + + this.meta.addDocMeta({ + id: docId, + title: '', + createDate: Date.now(), + tags: [], + }); + this.slots.docCreated.emit(docId); + return this.getDoc(docId, { query, readonly }) as Doc; + } + + dispose() { + this.awarenessStore.destroy(); + } + + /** + * Terminate the data sync process forcefully, which may cause data loss. + * It is advised to invoke `canGracefulStop` before calling this method. + */ + forceStop() { + this.docSync.forceStop(); + this.blobSync.stop(); + this.awarenessSync.disconnect(); + } + + getBlockCollection(docId: string): BlockCollection | null { + const space = this.docs.get(docId) as BlockCollection | undefined; + return space ?? null; + } + + getDoc(docId: string, options?: GetDocOptions): Doc | null { + const collection = this.getBlockCollection(docId); + return collection?.getDoc(options) ?? null; + } + + removeDoc(docId: string) { + const docMeta = this.meta.getDocMeta(docId); + if (!docMeta) { + throw new BlockSuiteError( + ErrorCode.DocCollectionError, + `doc meta not found: ${docId}` + ); + } + + const blockCollection = this.getBlockCollection(docId); + if (!blockCollection) return; + + blockCollection.dispose(); + this.meta.removeDocMeta(docId); + this.blockCollections.delete(docId); + } + + /** + * Start the data sync process + */ + start() { + this.docSync.start(); + this.blobSync.start(); + this.awarenessSync.connect(); + } + + /** + * Wait for all data has been successfully saved to the primary storage. + */ + waitForGracefulStop(abort?: AbortSignal) { + return this.docSync.waitForGracefulStop(abort); + } + + waitForSynced() { + return this.docSync.waitForSynced(); + } +} diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index ba18062f8a792..92f5d0ee21385 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -11,6 +11,7 @@ export const PackageList = [ 'blocksuite/framework/inline', 'blocksuite/presets', 'blocksuite/framework/store', + 'blocksuite/framework/sync', ], }, { diff --git a/yarn.lock b/yarn.lock index e0c7958276fef..7f539757030a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3804,6 +3804,7 @@ __metadata: "@blocksuite/inline": "workspace:*" "@blocksuite/presets": "workspace:*" "@blocksuite/store": "workspace:*" + "@blocksuite/sync": "workspace:*" languageName: unknown linkType: soft