refactor(core): move block collection to affine and implement as doc (#9514)

This commit is contained in:
Saul-Mirone
2025-01-04 06:28:54 +00:00
parent 4cb186def2
commit dcf4993265
39 changed files with 595 additions and 192 deletions

View File

@@ -7,6 +7,7 @@ import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { COLLECTION_VERSION, PAGE_VERSION } from '../consts.js';
import type { BlockModel, Blocks, BlockSchemaType } from '../index.js';
import { Schema } from '../index.js';
import { Text } from '../reactive/text.js';
import type { DocMeta } from '../store/workspace.js';
import { TestWorkspace } from '../test/test-workspace.js';
import { createAutoIncrementIdGenerator } from '../utils/id-generator.js';
@@ -166,7 +167,7 @@ describe('basic', () => {
doc.load(() => {
const rootId = doc.addBlock('affine:page', {
title: new doc.Text(),
title: new Text(),
});
expect(rootAddedCallback).toBeCalledTimes(1);
@@ -186,7 +187,7 @@ describe('basic', () => {
});
doc.load(() => {
doc.addBlock('affine:page', {
title: new doc.Text(),
title: new Text(),
});
});
{
@@ -245,7 +246,7 @@ describe('addBlock', () => {
it('can add single model', () => {
const doc = createTestDoc();
doc.addBlock('affine:page', {
title: new doc.Text(),
title: new Text(),
});
assert.deepEqual(serializCollection(doc.rootDoc).spaces[spaceId].blocks, {
@@ -264,7 +265,7 @@ describe('addBlock', () => {
it('can add model with props', () => {
const doc = createTestDoc();
doc.addBlock('affine:page', { title: new doc.Text('hello') });
doc.addBlock('affine:page', { title: new Text('hello') });
assert.deepEqual(serializCollection(doc.rootDoc).spaces[spaceId].blocks, {
'0': {
@@ -283,7 +284,7 @@ describe('addBlock', () => {
it('can add multi models', () => {
const doc = createTestDoc();
const rootId = doc.addBlock('affine:page', {
title: new doc.Text(),
title: new Text(),
});
const noteId = doc.addBlock('affine:note', {}, rootId);
doc.addBlock('affine:paragraph', {}, noteId);
@@ -344,7 +345,7 @@ describe('addBlock', () => {
queueMicrotask(() =>
doc.addBlock('affine:page', {
title: new doc.Text(),
title: new Text(),
})
);
const blockId = await waitOnce(doc.slots.rootAdded);
@@ -389,7 +390,7 @@ describe('addBlock', () => {
assert.equal(collection.docs.size, 2);
doc0.addBlock('affine:page', {
title: new doc0.Text(),
title: new Text(),
});
collection.removeDoc(doc0.id);

View File

@@ -5,15 +5,15 @@ import { signal } from '@preact/signals-core';
import type { BlockModel, Schema } from '../../schema/index.js';
import type { DraftModel } from '../../transformer/index.js';
import { syncBlockProps } from '../../utils/utils.js';
import type { BlockProps, Doc } from '../workspace.js';
import type { BlockOptions } from './block/index.js';
import { Block } from './block/index.js';
import type { BlockCollection, BlockProps } from './block-collection.js';
import { DocCRUD } from './crud.js';
import { type Query, runQuery } from './query.js';
type DocOptions = {
schema: Schema;
blockCollection: BlockCollection;
blockCollection: Doc;
readonly?: boolean;
query?: Query;
};
@@ -23,7 +23,7 @@ export class Blocks {
runQuery(this._query, block);
};
protected readonly _blockCollection: BlockCollection;
protected readonly _blockCollection: Doc;
protected readonly _blocks = signal<Record<string, Block>>({});
@@ -40,7 +40,7 @@ export class Blocks {
protected readonly _schema: Schema;
readonly slots: BlockCollection['slots'] & {
readonly slots: Doc['slots'] & {
/** This is always triggered after `doc.load` is called. */
ready: Slot;
/**
@@ -179,10 +179,6 @@ export class Blocks {
return this._blockCollection.collection;
}
get generateBlockId() {
return this._blockCollection.generateBlockId.bind(this._blockCollection);
}
get history() {
return this._blockCollection.history;
}
@@ -250,10 +246,6 @@ export class Blocks {
return this._blockCollection.spaceDoc;
}
get Text() {
return this._blockCollection.Text;
}
get transact() {
return this._blockCollection.transact.bind(this._blockCollection);
}
@@ -432,7 +424,7 @@ export class Blocks {
);
}
const id = blockProps.id ?? this._blockCollection.generateBlockId();
const id = blockProps.id ?? this._blockCollection.collection.idGenerator();
this.transact(() => {
this._crud.addBlock(

View File

@@ -1,5 +1,4 @@
export * from './block/index.js';
export * from './block-collection.js';
export * from './consts.js';
export * from './doc.js';
export * from './query.js';

View File

@@ -1,4 +1,3 @@
export type * from './doc/block-collection.js';
export * from './doc/index.js';
export * from './meta.js';
export * from './workspace.js';

View File

@@ -1,12 +1,14 @@
import type { Slot } from '@blocksuite/global/utils';
import type { BlobEngine, DocEngine } from '@blocksuite/sync';
import type * as Y from 'yjs';
import type { BlockModel } from '../schema/base.js';
import type { Schema } from '../schema/schema.js';
import type { IdGenerator } from '../utils/id-generator.js';
import type { AwarenessStore } from '../yjs/awareness.js';
import type { BlockSuiteDoc } from '../yjs/doc.js';
import type { YBlock } from './doc/block/types.js';
import type { Blocks } from './doc/doc.js';
import type { BlockCollection } from './doc/index.js';
import type { Query } from './doc/query.js';
export type Tag = {
@@ -70,7 +72,7 @@ export interface Workspace {
get schema(): Schema;
get doc(): BlockSuiteDoc;
get docs(): Map<string, BlockCollection>;
get docs(): Map<string, Doc>;
slots: {
docListUpdated: Slot;
@@ -85,6 +87,77 @@ export interface Workspace {
dispose(): void;
}
export interface Doc {
readonly id: string;
get meta(): DocMeta | undefined;
get schema(): Schema;
remove(): void;
load(initFn?: () => void): void;
get ready(): boolean;
dispose(): void;
slots: {
historyUpdated: Slot;
yBlockUpdated: Slot<
| {
type: 'add';
id: string;
}
| {
type: 'delete';
id: string;
}
>;
};
get canRedo(): boolean;
get canUndo(): boolean;
undo(): void;
redo(): void;
resetHistory(): void;
transact(fn: () => void, shouldTransact?: boolean): void;
withoutTransact(fn: () => void): void;
captureSync(): void;
clear(): void;
getDoc(options?: GetDocOptions): Blocks;
clearQuery(query: Query, readonly?: boolean): void;
get history(): Y.UndoManager;
get loaded(): boolean;
get readonly(): boolean;
get awarenessStore(): AwarenessStore;
get collection(): Workspace;
get rootDoc(): BlockSuiteDoc;
get spaceDoc(): Y.Doc;
get yBlocks(): Y.Map<YBlock>;
}
export interface StackItem {
meta: Map<'selection-state', unknown>;
}
export type YBlocks = Y.Map<YBlock>;
/** JSON-serializable properties of a block */
export type BlockSysProps = {
id: string;
flavour: string;
children?: BlockModel[];
};
export type BlockProps = BlockSysProps & Record<string, unknown>;
declare global {
namespace BlockSuite {
interface BlockModels {}
type Flavour = string & keyof BlockModels;
type ModelProps<Model> = Partial<
Model extends BlockModel<infer U> ? U : never
>;
}
}

View File

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

View File

@@ -1,36 +1,21 @@
import { type Disposable, Slot } from '@blocksuite/global/utils';
import { signal } from '@preact/signals-core';
import { uuidv4 } from 'lib0/random.js';
import * as Y from 'yjs';
import { Text } from '../../reactive/text.js';
import type { BlockModel } from '../../schema/base.js';
import type { IdGenerator } from '../../utils/id-generator.js';
import type { AwarenessStore, BlockSuiteDoc } from '../../yjs/index.js';
import type { GetDocOptions, Workspace } from '../workspace.js';
import { Blocks } from './doc.js';
import type { YBlock } from './index.js';
import type { Query } from './query.js';
export type YBlocks = Y.Map<YBlock>;
/** JSON-serializable properties of a block */
export type BlockSysProps = {
id: string;
flavour: string;
children?: BlockModel[];
};
export type BlockProps = BlockSysProps & Record<string, unknown>;
import { Blocks } from '../store/doc/doc.js';
import type { YBlock } from '../store/doc/index.js';
import type { Query } from '../store/doc/query.js';
import type { Doc, GetDocOptions, Workspace } from '../store/workspace.js';
import type { AwarenessStore, BlockSuiteDoc } from '../yjs/index.js';
type DocOptions = {
id: string;
collection: Workspace;
doc: BlockSuiteDoc;
awarenessStore: AwarenessStore;
idGenerator?: IdGenerator;
};
export class BlockCollection {
export class TestDoc implements Doc {
private _awarenessUpdateDisposable: Disposable | null = null;
private readonly _canRedo$ = signal(false);
@@ -57,8 +42,6 @@ export class BlockCollection {
this.slots.historyUpdated.emit();
};
private readonly _idGenerator: IdGenerator;
private readonly _initSubDoc = () => {
let subDoc = this.rootDoc.spaces.get(this.id);
if (!subDoc) {
@@ -184,7 +167,7 @@ export class BlockCollection {
return this.collection.meta.getDocMeta(this.id);
}
get readonly() {
get readonly(): boolean {
return this.awarenessStore.isReadonly(this);
}
@@ -200,21 +183,11 @@ export class BlockCollection {
return this._ySpaceDoc;
}
get Text() {
return Text;
}
get yBlocks() {
return this._yBlocks;
}
constructor({
id,
collection,
doc,
awarenessStore,
idGenerator = uuidv4,
}: DocOptions) {
constructor({ id, collection, doc, awarenessStore }: DocOptions) {
this.id = id;
this.rootDoc = doc;
this.awarenessStore = awarenessStore;
@@ -223,7 +196,6 @@ export class BlockCollection {
this._yBlocks = this._ySpaceDoc.getMap('blocks');
this._collection = collection;
this._idGenerator = idGenerator;
}
private _getReadonlyKey(readonly?: boolean): 'true' | 'false' | 'undefined' {
@@ -295,7 +267,7 @@ export class BlockCollection {
this._docMap[readonlyKey].delete(JSON.stringify(query));
}
destroy() {
private _destroy() {
this._ySpaceDoc.destroy();
this._onLoadSlot.dispose();
this._loaded = false;
@@ -311,10 +283,6 @@ export class BlockCollection {
}
}
generateBlockId() {
return this._idGenerator();
}
getDoc({ readonly, query }: GetDocOptions = {}) {
const readonlyKey = this._getReadonlyKey(readonly);
@@ -375,8 +343,16 @@ export class BlockCollection {
this._history.redo();
}
undo() {
if (this.readonly) {
console.error('cannot modify data in readonly mode');
return;
}
this._history.undo();
}
remove() {
this.destroy();
this._destroy();
this.rootDoc.spaces.delete(this.id);
}
@@ -403,30 +379,9 @@ export class BlockCollection {
);
}
// Handle all the events that happen at _any_ level (potentially deep inside the structure).
undo() {
if (this.readonly) {
console.error('cannot modify data in readonly mode');
return;
}
this._history.undo();
}
withoutTransact(callback: () => void) {
this._shouldTransact = false;
callback();
this._shouldTransact = true;
}
}
declare global {
namespace BlockSuite {
interface BlockModels {}
type Flavour = string & keyof BlockModels;
type ModelProps<Model> = Partial<
Model extends BlockModel<infer U> ? U : never
>;
}
}

View File

@@ -17,7 +17,6 @@ import { Awareness } from 'y-protocols/awareness.js';
import type { Schema } from '../schema/index.js';
import {
BlockCollection,
type Blocks,
type CreateDocOptions,
DocCollectionMeta,
@@ -30,6 +29,7 @@ import {
BlockSuiteDoc,
type RawAwarenessState,
} from '../yjs/index.js';
import { TestDoc } from './test-doc.js';
export type DocCollectionOptions = {
schema: Schema;
@@ -80,7 +80,7 @@ export class TestWorkspace implements Workspace {
readonly blobSync: BlobEngine;
readonly blockCollections = new Map<string, BlockCollection>();
readonly blockCollections = new Map<string, TestDoc>();
readonly doc: BlockSuiteDoc;
@@ -154,12 +154,11 @@ export class TestWorkspace implements Workspace {
private _bindDocMetaEvents() {
this.meta.docMetaAdded.on(docId => {
const doc = new BlockCollection({
const doc = new TestDoc({
id: docId,
collection: this,
doc: this.doc,
awarenessStore: this.awarenessStore,
idGenerator: this.idGenerator,
});
this.blockCollections.set(doc.id, doc);
});
@@ -225,8 +224,8 @@ export class TestWorkspace implements Workspace {
this.awarenessSync.disconnect();
}
getBlockCollection(docId: string): BlockCollection | null {
const space = this.docs.get(docId) as BlockCollection | undefined;
getBlockCollection(docId: string): TestDoc | null {
const space = this.docs.get(docId) as TestDoc | undefined;
return space ?? null;
}

View File

@@ -5,7 +5,7 @@ import { native2Y } from '../reactive/index.js';
import type { BlockModel, BlockSchema } from '../schema/base.js';
import { internalPrimitives } from '../schema/base.js';
import type { YBlock } from '../store/doc/block/index.js';
import type { BlockProps } from '../store/doc/block-collection.js';
import type { BlockProps } from '../store/workspace.js';
export function syncBlockProps(
schema: z.infer<typeof BlockSchema>,

View File

@@ -5,7 +5,7 @@ import clonedeep from 'lodash.clonedeep';
import merge from 'lodash.merge';
import type { Awareness as YAwareness } from 'y-protocols/awareness.js';
import type { BlockCollection } from '../store/index.js';
import type { Doc } from '../store/index.js';
export interface UserInfo {
name: string;
@@ -115,7 +115,7 @@ export class AwarenessStore<Flags extends BlockSuiteFlags = BlockSuiteFlags> {
return this.awareness.getStates();
}
isReadonly(blockCollection: BlockCollection): boolean {
isReadonly(blockCollection: Doc): boolean {
const rd = this.getFlag('readonly');
if (rd && typeof rd === 'object') {
return Boolean((rd as Record<string, boolean>)[blockCollection.id]);
@@ -137,7 +137,7 @@ export class AwarenessStore<Flags extends BlockSuiteFlags = BlockSuiteFlags> {
});
}
setReadonly(blockCollection: BlockCollection, value: boolean): void {
setReadonly(blockCollection: Doc, value: boolean): void {
const flags = this.getFlag('readonly') ?? {};
this.setFlag('readonly', {
...flags,