From 1180e9bc15f81ce6baa21410938f1e986753115d Mon Sep 17 00:00:00 2001 From: Saul-Mirone Date: Sun, 5 Jan 2025 06:49:31 +0000 Subject: [PATCH] refactor(editor): move workspace meta to affine (#9524) --- blocksuite/framework/store/src/store/index.ts | 1 - .../framework/store/src/store/workspace.ts | 3 +- blocksuite/framework/store/src/test/index.ts | 1 + .../framework/store/src/test/test-meta.ts | 262 ++++++++++++++++++ .../store/src/test/test-workspace.ts | 4 +- .../core/src/modules/workspace/impls}/meta.ts | 148 +++++----- .../src/modules/workspace/impls/workspace.ts | 4 +- 7 files changed, 337 insertions(+), 86 deletions(-) create mode 100644 blocksuite/framework/store/src/test/test-meta.ts rename {blocksuite/framework/store/src/store => packages/frontend/core/src/modules/workspace/impls}/meta.ts (79%) diff --git a/blocksuite/framework/store/src/store/index.ts b/blocksuite/framework/store/src/store/index.ts index f3b3850ff8..2b1968a754 100644 --- a/blocksuite/framework/store/src/store/index.ts +++ b/blocksuite/framework/store/src/store/index.ts @@ -1,3 +1,2 @@ export * from './doc/index.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 59e3fdedec..abb8719170 100644 --- a/blocksuite/framework/store/src/store/workspace.ts +++ b/blocksuite/framework/store/src/store/workspace.ts @@ -54,13 +54,12 @@ export interface WorkspaceMeta { get name(): string | undefined; setName(name: string): void; - commonFieldsUpdated: Slot; - hasVersion: boolean; writeVersion(workspace: Workspace): void; get docs(): unknown[] | undefined; initialize(): void; + commonFieldsUpdated: Slot; docMetaAdded: Slot; docMetaRemoved: Slot; docMetaUpdated: Slot; diff --git a/blocksuite/framework/store/src/test/index.ts b/blocksuite/framework/store/src/test/index.ts index 8747323132..a15387a2a8 100644 --- a/blocksuite/framework/store/src/test/index.ts +++ b/blocksuite/framework/store/src/test/index.ts @@ -1,3 +1,4 @@ export { createAutoIncrementIdGenerator } from '../utils/id-generator.js'; export * from './test-doc.js'; +export * from './test-meta.js'; export * from './test-workspace.js'; diff --git a/blocksuite/framework/store/src/test/test-meta.ts b/blocksuite/framework/store/src/test/test-meta.ts new file mode 100644 index 0000000000..b0c056c78c --- /dev/null +++ b/blocksuite/framework/store/src/test/test-meta.ts @@ -0,0 +1,262 @@ +import { Slot } from '@blocksuite/global/utils'; +import type * as Y from 'yjs'; + +import { COLLECTION_VERSION, PAGE_VERSION } from '../consts.js'; +import { createYProxy } from '../reactive/proxy.js'; +import type { + DocMeta, + DocsPropertiesMeta, + Workspace, + WorkspaceMeta, +} from '../store/workspace.js'; + +export type DocCollectionMetaState = { + pages?: unknown[]; + properties?: DocsPropertiesMeta; + workspaceVersion?: number; + pageVersion?: number; + blockVersions?: Record; + name?: string; + avatar?: string; +}; + +export class TestMeta implements WorkspaceMeta { + private readonly _handleDocCollectionMetaEvents = ( + events: Y.YEvent | Y.Text | Y.Map>[] + ) => { + events.forEach(e => { + const hasKey = (k: string) => + e.target === this._yMap && e.changes.keys.has(k); + + if ( + e.target === this.yDocs || + e.target.parent === this.yDocs || + hasKey('pages') + ) { + this._handleDocMetaEvent(); + } + + if (hasKey('name') || hasKey('avatar')) { + this._handleCommonFieldsEvent(); + } + }); + }; + + private _prevDocs = new Set(); + + protected readonly _proxy: DocCollectionMetaState; + + protected readonly _yMap: Y.Map< + DocCollectionMetaState[keyof DocCollectionMetaState] + >; + + commonFieldsUpdated = new Slot(); + + readonly doc: Y.Doc; + + docMetaAdded = new Slot(); + + docMetaRemoved = new Slot(); + + docMetaUpdated = new Slot(); + + readonly id: string = 'meta'; + + get avatar() { + return this._proxy.avatar; + } + + get blockVersions() { + return this._proxy.blockVersions; + } + + get docMetas() { + if (!this._proxy.pages) { + return [] as DocMeta[]; + } + return this._proxy.pages as DocMeta[]; + } + + get docs() { + return this._proxy.pages; + } + + get hasVersion() { + if (!this.blockVersions || !this.pageVersion || !this.workspaceVersion) { + return false; + } + return Object.keys(this.blockVersions).length > 0; + } + + get name() { + return this._proxy.name; + } + + get pageVersion() { + return this._proxy.pageVersion; + } + + get properties(): DocsPropertiesMeta { + const meta = this._proxy.properties; + if (!meta) { + return { + tags: { + options: [], + }, + }; + } + return meta; + } + + get workspaceVersion() { + return this._proxy.workspaceVersion; + } + + get yDocs() { + return this._yMap.get('pages') as unknown as Y.Array; + } + + constructor(doc: Y.Doc) { + this.doc = doc; + const map = doc.getMap(this.id) as Y.Map< + DocCollectionMetaState[keyof DocCollectionMetaState] + >; + this._yMap = map; + this._proxy = createYProxy(map); + this._yMap.observeDeep(this._handleDocCollectionMetaEvents); + } + + private _handleCommonFieldsEvent() { + this.commonFieldsUpdated.emit(); + } + + private _handleDocMetaEvent() { + const { docMetas, _prevDocs } = this; + + const newDocs = new Set(); + + docMetas.forEach(docMeta => { + if (!_prevDocs.has(docMeta.id)) { + this.docMetaAdded.emit(docMeta.id); + } + newDocs.add(docMeta.id); + }); + + _prevDocs.forEach(prevDocId => { + const isRemoved = newDocs.has(prevDocId) === false; + if (isRemoved) { + this.docMetaRemoved.emit(prevDocId); + } + }); + + this._prevDocs = newDocs; + + this.docMetaUpdated.emit(); + } + + addDocMeta(doc: DocMeta, index?: number) { + this.doc.transact(() => { + if (!this.docs) { + return; + } + const docs = this.docs as unknown[]; + if (index === undefined) { + docs.push(doc); + } else { + docs.splice(index, 0, doc); + } + }, this.doc.clientID); + } + + getDocMeta(id: string) { + return this.docMetas.find(doc => doc.id === id); + } + + initialize() { + if (!this._proxy.pages) { + this._proxy.pages = []; + } + } + + removeDocMeta(id: string) { + // you cannot delete a doc if there's no doc + if (!this.docs) { + return; + } + + const docMeta = this.docMetas; + const index = docMeta.findIndex((doc: DocMeta) => id === doc.id); + if (index === -1) { + return; + } + this.doc.transact(() => { + if (!this.docs) { + return; + } + this.docs.splice(index, 1); + }, this.doc.clientID); + } + + setAvatar(avatar: string) { + this.doc.transact(() => { + this._proxy.avatar = avatar; + }, this.doc.clientID); + } + + setDocMeta(id: string, props: Partial) { + const docs = (this.docs as DocMeta[]) ?? []; + const index = docs.findIndex((doc: DocMeta) => id === doc.id); + + this.doc.transact(() => { + if (!this.docs) { + return; + } + if (index === -1) return; + + const doc = this.docs[index] as Record; + Object.entries(props).forEach(([key, value]) => { + doc[key] = value; + }); + }, this.doc.clientID); + } + + setName(name: string) { + this.doc.transact(() => { + this._proxy.name = name; + }, this.doc.clientID); + } + + setProperties(meta: DocsPropertiesMeta) { + this._proxy.properties = meta; + this.docMetaUpdated.emit(); + } + + /** + * @internal Only for doc initialization + */ + writeVersion(collection: Workspace) { + const { blockVersions, pageVersion, workspaceVersion } = this._proxy; + + if (!workspaceVersion) { + this._proxy.workspaceVersion = COLLECTION_VERSION; + } else { + console.error('Workspace version is already set'); + } + + if (!pageVersion) { + this._proxy.pageVersion = PAGE_VERSION; + } else { + console.error('Doc version is already set'); + } + + if (!blockVersions) { + const _versions: Record = {}; + collection.schema.flavourSchemaMap.forEach((schema, flavour) => { + _versions[flavour] = schema.version; + }); + this._proxy.blockVersions = _versions; + } else { + console.error('Block versions is already set'); + } + } +} diff --git a/blocksuite/framework/store/src/test/test-workspace.ts b/blocksuite/framework/store/src/test/test-workspace.ts index 2c3899c953..e4a48d3b94 100644 --- a/blocksuite/framework/store/src/test/test-workspace.ts +++ b/blocksuite/framework/store/src/test/test-workspace.ts @@ -20,7 +20,6 @@ import type { Schema } from '../schema/index.js'; import { type Blocks, type CreateBlocksOptions, - DocCollectionMeta, type GetBlocksOptions, type Workspace, type WorkspaceMeta, @@ -28,6 +27,7 @@ import { import { type IdGenerator, nanoid } from '../utils/id-generator.js'; import { AwarenessStore, type RawAwarenessState } from '../yjs/index.js'; import { TestDoc } from './test-doc.js'; +import { TestMeta } from './test-meta.js'; export type DocCollectionOptions = { schema: Schema; @@ -146,7 +146,7 @@ export class TestWorkspace implements Workspace { this.idGenerator = idGenerator ?? nanoid; - this.meta = new DocCollectionMeta(this.doc); + this.meta = new TestMeta(this.doc); this._bindDocMetaEvents(); } diff --git a/blocksuite/framework/store/src/store/meta.ts b/packages/frontend/core/src/modules/workspace/impls/meta.ts similarity index 79% rename from blocksuite/framework/store/src/store/meta.ts rename to packages/frontend/core/src/modules/workspace/impls/meta.ts index 4d51760b15..7647947b7e 100644 --- a/blocksuite/framework/store/src/store/meta.ts +++ b/packages/frontend/core/src/modules/workspace/impls/meta.ts @@ -1,16 +1,17 @@ -import { Slot } from '@blocksuite/global/utils'; +import { Slot } from '@blocksuite/affine/global/utils'; +import { + createYProxy, + type DocMeta, + type DocsPropertiesMeta, + type Workspace, + type WorkspaceMeta, +} from '@blocksuite/affine/store'; import type * as Y from 'yjs'; -import { COLLECTION_VERSION, PAGE_VERSION } from '../consts.js'; -import { createYProxy } from '../reactive/proxy.js'; -import type { - DocMeta, - DocsPropertiesMeta, - Workspace, - WorkspaceMeta, -} from './workspace.js'; +const COLLECTION_VERSION = 2; +const PAGE_VERSION = 2; -export type DocCollectionMetaState = { +type MetaState = { pages?: unknown[]; properties?: DocsPropertiesMeta; workspaceVersion?: number; @@ -20,7 +21,12 @@ export type DocCollectionMetaState = { avatar?: string; }; -export class DocCollectionMeta implements WorkspaceMeta { +export class WorkspaceMetaImpl implements WorkspaceMeta { + commonFieldsUpdated = new Slot(); + docMetaAdded = new Slot(); + docMetaRemoved = new Slot(); + docMetaUpdated = new Slot(); + private readonly _handleDocCollectionMetaEvents = ( events: Y.YEvent | Y.Text | Y.Map>[] ) => { @@ -42,58 +48,30 @@ export class DocCollectionMeta implements WorkspaceMeta { }); }; + private readonly _id: string = 'meta'; + private readonly _doc: Y.Doc; + private readonly _proxy: MetaState; + private readonly _yMap: Y.Map; private _prevDocs = new Set(); - protected readonly _proxy: DocCollectionMetaState; - - protected readonly _yMap: Y.Map< - DocCollectionMetaState[keyof DocCollectionMetaState] - >; - - commonFieldsUpdated = new Slot(); - - readonly doc: Y.Doc; - - docMetaAdded = new Slot(); - - docMetaRemoved = new Slot(); - - docMetaUpdated = new Slot(); - - readonly id: string = 'meta'; - get avatar() { return this._proxy.avatar; } - get blockVersions() { - return this._proxy.blockVersions; - } - - get docMetas() { - if (!this._proxy.pages) { - return [] as DocMeta[]; - } - return this._proxy.pages as DocMeta[]; - } - - get docs() { - return this._proxy.pages; - } - - get hasVersion() { - if (!this.blockVersions || !this.pageVersion || !this.workspaceVersion) { - return false; - } - return Object.keys(this.blockVersions).length > 0; + setAvatar(avatar: string) { + this._doc.transact(() => { + this._proxy.avatar = avatar; + }, this._doc.clientID); } get name() { return this._proxy.name; } - get pageVersion() { - return this._proxy.pageVersion; + setName(name: string) { + this._doc.transact(() => { + this._proxy.name = name; + }, this._doc.clientID); } get properties(): DocsPropertiesMeta { @@ -108,7 +86,38 @@ export class DocCollectionMeta implements WorkspaceMeta { return meta; } - get workspaceVersion() { + setProperties(meta: DocsPropertiesMeta) { + this._proxy.properties = meta; + this.docMetaUpdated.emit(); + } + + get docMetas() { + if (!this._proxy.pages) { + return [] as DocMeta[]; + } + return this._proxy.pages as DocMeta[]; + } + + get docs() { + return this._proxy.pages; + } + + get hasVersion() { + if (!this._blockVersions || !this._pageVersion || !this._workspaceVersion) { + return false; + } + return Object.keys(this._blockVersions).length > 0; + } + + private get _blockVersions() { + return this._proxy.blockVersions; + } + + private get _pageVersion() { + return this._proxy.pageVersion; + } + + private get _workspaceVersion() { return this._proxy.workspaceVersion; } @@ -117,10 +126,8 @@ export class DocCollectionMeta implements WorkspaceMeta { } constructor(doc: Y.Doc) { - this.doc = doc; - const map = doc.getMap(this.id) as Y.Map< - DocCollectionMetaState[keyof DocCollectionMetaState] - >; + this._doc = doc; + const map = doc.getMap(this._id) as Y.Map; this._yMap = map; this._proxy = createYProxy(map); this._yMap.observeDeep(this._handleDocCollectionMetaEvents); @@ -155,7 +162,7 @@ export class DocCollectionMeta implements WorkspaceMeta { } addDocMeta(doc: DocMeta, index?: number) { - this.doc.transact(() => { + this._doc.transact(() => { if (!this.docs) { return; } @@ -165,7 +172,7 @@ export class DocCollectionMeta implements WorkspaceMeta { } else { docs.splice(index, 0, doc); } - }, this.doc.clientID); + }, this._doc.clientID); } getDocMeta(id: string) { @@ -189,25 +196,19 @@ export class DocCollectionMeta implements WorkspaceMeta { if (index === -1) { return; } - this.doc.transact(() => { + this._doc.transact(() => { if (!this.docs) { return; } this.docs.splice(index, 1); - }, this.doc.clientID); - } - - setAvatar(avatar: string) { - this.doc.transact(() => { - this._proxy.avatar = avatar; - }, this.doc.clientID); + }, this._doc.clientID); } setDocMeta(id: string, props: Partial) { const docs = (this.docs as DocMeta[]) ?? []; const index = docs.findIndex((doc: DocMeta) => id === doc.id); - this.doc.transact(() => { + this._doc.transact(() => { if (!this.docs) { return; } @@ -217,18 +218,7 @@ export class DocCollectionMeta implements WorkspaceMeta { Object.entries(props).forEach(([key, value]) => { doc[key] = value; }); - }, this.doc.clientID); - } - - setName(name: string) { - this.doc.transact(() => { - this._proxy.name = name; - }, this.doc.clientID); - } - - setProperties(meta: DocsPropertiesMeta) { - this._proxy.properties = meta; - this.docMetaUpdated.emit(); + }, this._doc.clientID); } /** diff --git a/packages/frontend/core/src/modules/workspace/impls/workspace.ts b/packages/frontend/core/src/modules/workspace/impls/workspace.ts index 1b410b72c6..e633854aab 100644 --- a/packages/frontend/core/src/modules/workspace/impls/workspace.ts +++ b/packages/frontend/core/src/modules/workspace/impls/workspace.ts @@ -9,7 +9,6 @@ import { type Blocks, type CreateBlocksOptions, type Doc, - DocCollectionMeta, type GetBlocksOptions, type IdGenerator, nanoid, @@ -29,6 +28,7 @@ import { Awareness } from 'y-protocols/awareness.js'; import * as Y from 'yjs'; import { DocImpl } from './doc'; +import { WorkspaceMetaImpl } from './meta'; type WorkspaceOptions = { id?: string; @@ -111,7 +111,7 @@ export class WorkspaceImpl implements Workspace { this.idGenerator = nanoid; - this.meta = new DocCollectionMeta(this.doc); + this.meta = new WorkspaceMetaImpl(this.doc); this._bindDocMetaEvents(); }