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
@@ -1,3 +1,2 @@
export * from './doc/index.js';
export * from './meta.js';
export * from './workspace.js';
@@ -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<string>;
docMetaRemoved: Slot<string>;
docMetaUpdated: Slot;
@@ -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';
@@ -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<string, number>;
name?: string;
avatar?: string;
};
export class TestMeta implements WorkspaceMeta {
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 _prevDocs = new Set<string>();
protected readonly _proxy: DocCollectionMetaState;
protected readonly _yMap: Y.Map<
DocCollectionMetaState[keyof DocCollectionMetaState]
>;
commonFieldsUpdated = new Slot();
readonly doc: Y.Doc;
docMetaAdded = new Slot<string>();
docMetaRemoved = new Slot<string>();
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<unknown>;
}
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<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);
}
setAvatar(avatar: string) {
this.doc.transact(() => {
this._proxy.avatar = avatar;
}, 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);
}
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<string, number> = {};
collection.schema.flavourSchemaMap.forEach((schema, flavour) => {
_versions[flavour] = schema.version;
});
this._proxy.blockVersions = _versions;
} else {
console.error('Block versions is already set');
}
}
}
@@ -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();
}
@@ -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<string>();
docMetaRemoved = new Slot<string>();
docMetaUpdated = new Slot();
private readonly _handleDocCollectionMetaEvents = (
events: Y.YEvent<Y.Array<unknown> | Y.Text | Y.Map<unknown>>[]
) => {
@@ -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<MetaState[keyof MetaState]>;
private _prevDocs = new Set<string>();
protected readonly _proxy: DocCollectionMetaState;
protected readonly _yMap: Y.Map<
DocCollectionMetaState[keyof DocCollectionMetaState]
>;
commonFieldsUpdated = new Slot();
readonly doc: Y.Doc;
docMetaAdded = new Slot<string>();
docMetaRemoved = new Slot<string>();
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<MetaState[keyof MetaState]>;
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<DocMeta>) {
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);
}
/**
@@ -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();
}