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

@@ -1,3 +1,2 @@
export * from './doc/index.js'; export * from './doc/index.js';
export * from './meta.js';
export * from './workspace.js'; export * from './workspace.js';

View File

@@ -54,13 +54,12 @@ export interface WorkspaceMeta {
get name(): string | undefined; get name(): string | undefined;
setName(name: string): void; setName(name: string): void;
commonFieldsUpdated: Slot;
hasVersion: boolean; hasVersion: boolean;
writeVersion(workspace: Workspace): void; writeVersion(workspace: Workspace): void;
get docs(): unknown[] | undefined; get docs(): unknown[] | undefined;
initialize(): void; initialize(): void;
commonFieldsUpdated: Slot;
docMetaAdded: Slot<string>; docMetaAdded: Slot<string>;
docMetaRemoved: Slot<string>; docMetaRemoved: Slot<string>;
docMetaUpdated: Slot; docMetaUpdated: Slot;

View File

@@ -1,3 +1,4 @@
export { createAutoIncrementIdGenerator } from '../utils/id-generator.js'; export { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
export * from './test-doc.js'; export * from './test-doc.js';
export * from './test-meta.js';
export * from './test-workspace.js'; export * from './test-workspace.js';

View File

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

View File

@@ -20,7 +20,6 @@ import type { Schema } from '../schema/index.js';
import { import {
type Blocks, type Blocks,
type CreateBlocksOptions, type CreateBlocksOptions,
DocCollectionMeta,
type GetBlocksOptions, type GetBlocksOptions,
type Workspace, type Workspace,
type WorkspaceMeta, type WorkspaceMeta,
@@ -28,6 +27,7 @@ import {
import { type IdGenerator, nanoid } from '../utils/id-generator.js'; import { type IdGenerator, nanoid } from '../utils/id-generator.js';
import { AwarenessStore, type RawAwarenessState } from '../yjs/index.js'; import { AwarenessStore, type RawAwarenessState } from '../yjs/index.js';
import { TestDoc } from './test-doc.js'; import { TestDoc } from './test-doc.js';
import { TestMeta } from './test-meta.js';
export type DocCollectionOptions = { export type DocCollectionOptions = {
schema: Schema; schema: Schema;
@@ -146,7 +146,7 @@ export class TestWorkspace implements Workspace {
this.idGenerator = idGenerator ?? nanoid; this.idGenerator = idGenerator ?? nanoid;
this.meta = new DocCollectionMeta(this.doc); this.meta = new TestMeta(this.doc);
this._bindDocMetaEvents(); this._bindDocMetaEvents();
} }

View File

@@ -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 type * as Y from 'yjs';
import { COLLECTION_VERSION, PAGE_VERSION } from '../consts.js'; const COLLECTION_VERSION = 2;
import { createYProxy } from '../reactive/proxy.js'; const PAGE_VERSION = 2;
import type {
DocMeta,
DocsPropertiesMeta,
Workspace,
WorkspaceMeta,
} from './workspace.js';
export type DocCollectionMetaState = { type MetaState = {
pages?: unknown[]; pages?: unknown[];
properties?: DocsPropertiesMeta; properties?: DocsPropertiesMeta;
workspaceVersion?: number; workspaceVersion?: number;
@@ -20,7 +21,12 @@ export type DocCollectionMetaState = {
avatar?: string; 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 = ( private readonly _handleDocCollectionMetaEvents = (
events: Y.YEvent<Y.Array<unknown> | Y.Text | Y.Map<unknown>>[] 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>(); 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() { get avatar() {
return this._proxy.avatar; return this._proxy.avatar;
} }
get blockVersions() { setAvatar(avatar: string) {
return this._proxy.blockVersions; this._doc.transact(() => {
} this._proxy.avatar = avatar;
}, this._doc.clientID);
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() { get name() {
return this._proxy.name; return this._proxy.name;
} }
get pageVersion() { setName(name: string) {
return this._proxy.pageVersion; this._doc.transact(() => {
this._proxy.name = name;
}, this._doc.clientID);
} }
get properties(): DocsPropertiesMeta { get properties(): DocsPropertiesMeta {
@@ -108,7 +86,38 @@ export class DocCollectionMeta implements WorkspaceMeta {
return meta; 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; return this._proxy.workspaceVersion;
} }
@@ -117,10 +126,8 @@ export class DocCollectionMeta implements WorkspaceMeta {
} }
constructor(doc: Y.Doc) { constructor(doc: Y.Doc) {
this.doc = doc; this._doc = doc;
const map = doc.getMap(this.id) as Y.Map< const map = doc.getMap(this._id) as Y.Map<MetaState[keyof MetaState]>;
DocCollectionMetaState[keyof DocCollectionMetaState]
>;
this._yMap = map; this._yMap = map;
this._proxy = createYProxy(map); this._proxy = createYProxy(map);
this._yMap.observeDeep(this._handleDocCollectionMetaEvents); this._yMap.observeDeep(this._handleDocCollectionMetaEvents);
@@ -155,7 +162,7 @@ export class DocCollectionMeta implements WorkspaceMeta {
} }
addDocMeta(doc: DocMeta, index?: number) { addDocMeta(doc: DocMeta, index?: number) {
this.doc.transact(() => { this._doc.transact(() => {
if (!this.docs) { if (!this.docs) {
return; return;
} }
@@ -165,7 +172,7 @@ export class DocCollectionMeta implements WorkspaceMeta {
} else { } else {
docs.splice(index, 0, doc); docs.splice(index, 0, doc);
} }
}, this.doc.clientID); }, this._doc.clientID);
} }
getDocMeta(id: string) { getDocMeta(id: string) {
@@ -189,25 +196,19 @@ export class DocCollectionMeta implements WorkspaceMeta {
if (index === -1) { if (index === -1) {
return; return;
} }
this.doc.transact(() => { this._doc.transact(() => {
if (!this.docs) { if (!this.docs) {
return; return;
} }
this.docs.splice(index, 1); this.docs.splice(index, 1);
}, this.doc.clientID); }, this._doc.clientID);
}
setAvatar(avatar: string) {
this.doc.transact(() => {
this._proxy.avatar = avatar;
}, this.doc.clientID);
} }
setDocMeta(id: string, props: Partial<DocMeta>) { setDocMeta(id: string, props: Partial<DocMeta>) {
const docs = (this.docs as DocMeta[]) ?? []; const docs = (this.docs as DocMeta[]) ?? [];
const index = docs.findIndex((doc: DocMeta) => id === doc.id); const index = docs.findIndex((doc: DocMeta) => id === doc.id);
this.doc.transact(() => { this._doc.transact(() => {
if (!this.docs) { if (!this.docs) {
return; return;
} }
@@ -217,18 +218,7 @@ export class DocCollectionMeta implements WorkspaceMeta {
Object.entries(props).forEach(([key, value]) => { Object.entries(props).forEach(([key, value]) => {
doc[key] = value; doc[key] = value;
}); });
}, this.doc.clientID); }, 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();
} }
/** /**

View File

@@ -9,7 +9,6 @@ import {
type Blocks, type Blocks,
type CreateBlocksOptions, type CreateBlocksOptions,
type Doc, type Doc,
DocCollectionMeta,
type GetBlocksOptions, type GetBlocksOptions,
type IdGenerator, type IdGenerator,
nanoid, nanoid,
@@ -29,6 +28,7 @@ import { Awareness } from 'y-protocols/awareness.js';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { DocImpl } from './doc'; import { DocImpl } from './doc';
import { WorkspaceMetaImpl } from './meta';
type WorkspaceOptions = { type WorkspaceOptions = {
id?: string; id?: string;
@@ -111,7 +111,7 @@ export class WorkspaceImpl implements Workspace {
this.idGenerator = nanoid; this.idGenerator = nanoid;
this.meta = new DocCollectionMeta(this.doc); this.meta = new WorkspaceMetaImpl(this.doc);
this._bindDocMetaEvents(); this._bindDocMetaEvents();
} }