refactor(editor): move workspace meta to affine (#9524)

This commit is contained in:
Saul-Mirone
2025-01-05 06:49:31 +00:00
parent be0de6dc21
commit 1180e9bc15
7 changed files with 337 additions and 86 deletions

View File

@@ -0,0 +1,252 @@
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';
const COLLECTION_VERSION = 2;
const PAGE_VERSION = 2;
type MetaState = {
pages?: unknown[];
properties?: DocsPropertiesMeta;
workspaceVersion?: number;
pageVersion?: number;
blockVersions?: Record<string, number>;
name?: string;
avatar?: string;
};
export class WorkspaceMetaImpl implements WorkspaceMeta {
commonFieldsUpdated = new Slot();
docMetaAdded = new Slot<string>();
docMetaRemoved = new Slot<string>();
docMetaUpdated = new Slot();
private readonly _handleDocCollectionMetaEvents = (
events: Y.YEvent<Y.Array<unknown> | Y.Text | Y.Map<unknown>>[]
) => {
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 readonly _id: string = 'meta';
private readonly _doc: Y.Doc;
private readonly _proxy: MetaState;
private readonly _yMap: Y.Map<MetaState[keyof MetaState]>;
private _prevDocs = new Set<string>();
get avatar() {
return this._proxy.avatar;
}
setAvatar(avatar: string) {
this._doc.transact(() => {
this._proxy.avatar = avatar;
}, this._doc.clientID);
}
get name() {
return this._proxy.name;
}
setName(name: string) {
this._doc.transact(() => {
this._proxy.name = name;
}, this._doc.clientID);
}
get properties(): DocsPropertiesMeta {
const meta = this._proxy.properties;
if (!meta) {
return {
tags: {
options: [],
},
};
}
return meta;
}
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;
}
get yDocs() {
return this._yMap.get('pages') as unknown as Y.Array<unknown>;
}
constructor(doc: Y.Doc) {
this._doc = doc;
const map = doc.getMap(this._id) as Y.Map<MetaState[keyof MetaState]>;
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<string>();
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);
}
setDocMeta(id: string, props: Partial<DocMeta>) {
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<string, unknown>;
Object.entries(props).forEach(([key, value]) => {
doc[key] = value;
});
}, this._doc.clientID);
}
/**
* @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<string, number> = {};
collection.schema.flavourSchemaMap.forEach((schema, flavour) => {
_versions[flavour] = schema.version;
});
this._proxy.blockVersions = _versions;
} else {
console.error('Block versions is already set');
}
}
}

View File

@@ -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();
}