diff --git a/blocksuite/affine/block-data-view/src/data-source.ts b/blocksuite/affine/block-data-view/src/data-source.ts index 566bf80bd5..19d1d51d54 100644 --- a/blocksuite/affine/block-data-view/src/data-source.ts +++ b/blocksuite/affine/block-data-view/src/data-source.ts @@ -167,7 +167,7 @@ export class BlockQueryDataSource extends DataSourceBase { type ?? propertyPresets.multiSelectPropertyConfig.type ].create(this.newColumnName()); - const id = doc.generateBlockId(); + const id = doc.collection.idGenerator(); if (this.block.columns.some(v => v.id === id)) { return id; } diff --git a/blocksuite/affine/block-data-view/src/data-view-model.ts b/blocksuite/affine/block-data-view/src/data-view-model.ts index 24007a3ecc..d18135084e 100644 --- a/blocksuite/affine/block-data-view/src/data-view-model.ts +++ b/blocksuite/affine/block-data-view/src/data-view-model.ts @@ -33,7 +33,7 @@ export class DataViewBlockModel extends BlockModel { } duplicateView(id: string): string { - const newId = this.doc.generateBlockId(); + const newId = this.doc.collection.idGenerator(); this.doc.transact(() => { const index = this.views.findIndex(v => v.id === id); const view = this.views[index]; diff --git a/blocksuite/affine/block-database/src/data-source.ts b/blocksuite/affine/block-database/src/data-source.ts index 8e2ae60794..79e2f50f28 100644 --- a/blocksuite/affine/block-database/src/data-source.ts +++ b/blocksuite/affine/block-database/src/data-source.ts @@ -448,7 +448,7 @@ export const databaseViewInitTemplate = ( const rowId = model.doc.addBlock( 'affine:paragraph', { - text: new model.doc.Text(`Task ${i + 1}`), + text: new Text(`Task ${i + 1}`), }, model.id ); diff --git a/blocksuite/affine/block-database/src/utils/block-utils.ts b/blocksuite/affine/block-database/src/utils/block-utils.ts index d332c8bbab..bc889769bd 100644 --- a/blocksuite/affine/block-database/src/utils/block-utils.ts +++ b/blocksuite/affine/block-database/src/utils/block-utils.ts @@ -19,7 +19,7 @@ export function addProperty( id?: string; } ): string { - const id = column.id ?? model.doc.generateBlockId(); + const id = column.id ?? model.doc.collection.idGenerator(); if (model.columns.some(v => v.id === id)) { return id; } @@ -101,7 +101,7 @@ export function deleteView(model: DatabaseBlockModel, id: string) { } export function duplicateView(model: DatabaseBlockModel, id: string): string { - const newId = model.doc.generateBlockId(); + const newId = model.doc.collection.idGenerator(); model.doc.transact(() => { const index = model.views.findIndex(v => v.id === id); const view = model.views[index]; diff --git a/blocksuite/affine/block-embed/src/common/render-linked-doc.ts b/blocksuite/affine/block-embed/src/common/render-linked-doc.ts index b4f0dc6058..dd47c03e4f 100644 --- a/blocksuite/affine/block-embed/src/common/render-linked-doc.ts +++ b/blocksuite/affine/block-embed/src/common/render-linked-doc.ts @@ -17,6 +17,7 @@ import { type DraftModel, type Query, Slice, + Text, } from '@blocksuite/store'; import { render, type TemplateResult } from 'lit'; @@ -407,7 +408,7 @@ export function createLinkedDocFromSlice( const linkedDoc = doc.collection.createDoc({}); linkedDoc.load(() => { const rootId = linkedDoc.addBlock('affine:page', { - title: new doc.Text(docTitle), + title: new Text(docTitle), }); linkedDoc.addBlock('affine:surface', {}, rootId); const noteId = linkedDoc.addBlock('affine:note', {}, rootId); diff --git a/blocksuite/affine/block-embed/src/embed-linked-doc-block/embed-linked-doc-block.ts b/blocksuite/affine/block-embed/src/embed-linked-doc-block/embed-linked-doc-block.ts index 0ecf25d3e3..8cc68c0708 100644 --- a/blocksuite/affine/block-embed/src/embed-linked-doc-block/embed-linked-doc-block.ts +++ b/blocksuite/affine/block-embed/src/embed-linked-doc-block/embed-linked-doc-block.ts @@ -22,6 +22,7 @@ import { referenceToNode, } from '@blocksuite/affine-shared/utils'; import { Bound, throttle } from '@blocksuite/global/utils'; +import { Text } from '@blocksuite/store'; import { computed } from '@preact/signals-core'; import { html, nothing } from 'lit'; import { property, queryAsync, state } from 'lit/decorators.js'; @@ -171,7 +172,7 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent { doc = createTestDoc(); rootId = doc.addBlock('affine:page', { - title: new doc.Text('database test'), + title: new Text('database test'), }); noteBlockId = doc.addBlock('affine:note', {}, rootId); @@ -114,14 +114,14 @@ describe('DatabaseManager', () => { p1 = doc.addBlock( 'affine:paragraph', { - text: new doc.Text('text1'), + text: new Text('text1'), }, databaseBlockId ); p2 = doc.addBlock( 'affine:paragraph', { - text: new doc.Text('text2'), + text: new Text('text2'), }, databaseBlockId ); @@ -173,7 +173,7 @@ describe('DatabaseManager', () => { const modelId = doc.addBlock( 'affine:paragraph', { - text: new doc.Text('paragraph'), + text: new Text('paragraph'), }, noteBlockId ); @@ -201,7 +201,7 @@ describe('DatabaseManager', () => { const newRowId = doc.addBlock( 'affine:paragraph', { - text: new doc.Text('text3'), + text: new Text('text3'), }, databaseBlockId ); diff --git a/blocksuite/blocks/src/root-block/widgets/element-toolbar/more-menu/render-linked-doc.ts b/blocksuite/blocks/src/root-block/widgets/element-toolbar/more-menu/render-linked-doc.ts index 1bc5e4d02c..3c59d1bfda 100644 --- a/blocksuite/blocks/src/root-block/widgets/element-toolbar/more-menu/render-linked-doc.ts +++ b/blocksuite/blocks/src/root-block/widgets/element-toolbar/more-menu/render-linked-doc.ts @@ -5,7 +5,7 @@ import { DocModeProvider } from '@blocksuite/affine-shared/services'; import { getBlockProps } from '@blocksuite/affine-shared/utils'; import type { EditorHost } from '@blocksuite/block-std'; import { GfxBlockElementModel } from '@blocksuite/block-std/gfx'; -import { type BlockModel, type Blocks } from '@blocksuite/store'; +import { type BlockModel, type Blocks, Text } from '@blocksuite/store'; import { getElementProps, @@ -43,7 +43,7 @@ export function createLinkedDocFromNote( const linkedDoc = doc.collection.createDoc({}); linkedDoc.load(() => { const rootId = linkedDoc.addBlock('affine:page', { - title: new doc.Text(docTitle), + title: new Text(docTitle), }); linkedDoc.addBlock('affine:surface', {}, rootId); const blockProps = getBlockProps(note); @@ -74,7 +74,7 @@ export function createLinkedDocFromEdgelessElements( const linkedDoc = host.doc.collection.createDoc({}); linkedDoc.load(() => { const rootId = linkedDoc.addBlock('affine:page', { - title: new host.doc.Text(docTitle), + title: new Text(docTitle), }); const surfaceId = linkedDoc.addBlock('affine:surface', {}, rootId); const surface = getSurfaceBlock(linkedDoc); diff --git a/blocksuite/blocks/src/root-block/widgets/embed-card-toolbar/embed-card-toolbar.ts b/blocksuite/blocks/src/root-block/widgets/embed-card-toolbar/embed-card-toolbar.ts index f604b4e7c0..5acdfc1d95 100644 --- a/blocksuite/blocks/src/root-block/widgets/embed-card-toolbar/embed-card-toolbar.ts +++ b/blocksuite/blocks/src/root-block/widgets/embed-card-toolbar/embed-card-toolbar.ts @@ -53,7 +53,7 @@ import { } from '@blocksuite/affine-shared/services'; import { getHostName, referenceToNode } from '@blocksuite/affine-shared/utils'; import { type BlockStdScope, WidgetComponent } from '@blocksuite/block-std'; -import { type BlockModel } from '@blocksuite/store'; +import { type BlockModel, Text } from '@blocksuite/store'; import { autoUpdate, computePosition, flip, offset } from '@floating-ui/dom'; import { html, nothing, type TemplateResult } from 'lit'; import { query, state } from 'lit/decorators.js'; @@ -619,7 +619,7 @@ export class EmbedCardToolbar extends WidgetComponent< const insert = title || caption || url; yText.insert(0, insert); yText.format(0, insert.length, { link: url }); - const text = new doc.Text(yText); + const text = new Text(yText); doc.addBlock( 'affine:paragraph', { diff --git a/blocksuite/blocks/src/root-block/widgets/slash-menu/config.ts b/blocksuite/blocks/src/root-block/widgets/slash-menu/config.ts index 929b343d01..84499fb5c4 100644 --- a/blocksuite/blocks/src/root-block/widgets/slash-menu/config.ts +++ b/blocksuite/blocks/src/root-block/widgets/slash-menu/config.ts @@ -709,9 +709,7 @@ export const defaultSlashMenuConfig: SlashMenuConfig = { model.flavour as never, { type: (model as ParagraphBlockModel).type, - text: new rootComponent.doc.Text( - model.text.toDelta() as DeltaInsert[] - ), + text: new Text(model.text.toDelta() as DeltaInsert[]), // @ts-expect-error FIXME: ts error checked: model.checked, }, diff --git a/blocksuite/framework/store/src/__tests__/collection.unit.spec.ts b/blocksuite/framework/store/src/__tests__/collection.unit.spec.ts index fea9304cf9..772acc47cf 100644 --- a/blocksuite/framework/store/src/__tests__/collection.unit.spec.ts +++ b/blocksuite/framework/store/src/__tests__/collection.unit.spec.ts @@ -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); diff --git a/blocksuite/framework/store/src/store/doc/doc.ts b/blocksuite/framework/store/src/store/doc/doc.ts index 163070e6ba..d3a0c02a25 100644 --- a/blocksuite/framework/store/src/store/doc/doc.ts +++ b/blocksuite/framework/store/src/store/doc/doc.ts @@ -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>({}); @@ -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( diff --git a/blocksuite/framework/store/src/store/doc/index.ts b/blocksuite/framework/store/src/store/doc/index.ts index 23a3433563..4b1647494a 100644 --- a/blocksuite/framework/store/src/store/doc/index.ts +++ b/blocksuite/framework/store/src/store/doc/index.ts @@ -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'; diff --git a/blocksuite/framework/store/src/store/index.ts b/blocksuite/framework/store/src/store/index.ts index 6afb07b43c..f3b3850ff8 100644 --- a/blocksuite/framework/store/src/store/index.ts +++ b/blocksuite/framework/store/src/store/index.ts @@ -1,4 +1,3 @@ -export type * from './doc/block-collection.js'; 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 312e7aeb28..209f31d7d3 100644 --- a/blocksuite/framework/store/src/store/workspace.ts +++ b/blocksuite/framework/store/src/store/workspace.ts @@ -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; + get docs(): Map; 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; +} + export interface StackItem { meta: Map<'selection-state', unknown>; } + +export type YBlocks = Y.Map; + +/** JSON-serializable properties of a block */ +export type BlockSysProps = { + id: string; + flavour: string; + children?: BlockModel[]; +}; +export type BlockProps = BlockSysProps & Record; + +declare global { + namespace BlockSuite { + interface BlockModels {} + + type Flavour = string & keyof BlockModels; + + type ModelProps = Partial< + Model extends BlockModel ? U : never + >; + } +} diff --git a/blocksuite/framework/store/src/test/index.ts b/blocksuite/framework/store/src/test/index.ts index 5d61ea21bf..8747323132 100644 --- a/blocksuite/framework/store/src/test/index.ts +++ b/blocksuite/framework/store/src/test/index.ts @@ -1,2 +1,3 @@ export { createAutoIncrementIdGenerator } from '../utils/id-generator.js'; +export * from './test-doc.js'; export * from './test-workspace.js'; diff --git a/blocksuite/framework/store/src/store/doc/block-collection.ts b/blocksuite/framework/store/src/test/test-doc.ts similarity index 85% rename from blocksuite/framework/store/src/store/doc/block-collection.ts rename to blocksuite/framework/store/src/test/test-doc.ts index 1000b2f912..cc45e05242 100644 --- a/blocksuite/framework/store/src/store/doc/block-collection.ts +++ b/blocksuite/framework/store/src/test/test-doc.ts @@ -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; - -/** JSON-serializable properties of a block */ -export type BlockSysProps = { - id: string; - flavour: string; - children?: BlockModel[]; -}; -export type BlockProps = BlockSysProps & Record; +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 = Partial< - Model extends BlockModel ? U : never - >; - } -} diff --git a/blocksuite/framework/store/src/test/test-workspace.ts b/blocksuite/framework/store/src/test/test-workspace.ts index 598e9ea614..47a91a9e7a 100644 --- a/blocksuite/framework/store/src/test/test-workspace.ts +++ b/blocksuite/framework/store/src/test/test-workspace.ts @@ -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(); + readonly blockCollections = new Map(); 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; } diff --git a/blocksuite/framework/store/src/utils/utils.ts b/blocksuite/framework/store/src/utils/utils.ts index ac08dcf203..1490f94c2a 100644 --- a/blocksuite/framework/store/src/utils/utils.ts +++ b/blocksuite/framework/store/src/utils/utils.ts @@ -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, diff --git a/blocksuite/framework/store/src/yjs/awareness.ts b/blocksuite/framework/store/src/yjs/awareness.ts index 59662e2e43..09e9d60241 100644 --- a/blocksuite/framework/store/src/yjs/awareness.ts +++ b/blocksuite/framework/store/src/yjs/awareness.ts @@ -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 { 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)[blockCollection.id]); @@ -137,7 +137,7 @@ export class AwarenessStore { }); } - setReadonly(blockCollection: BlockCollection, value: boolean): void { + setReadonly(blockCollection: Doc, value: boolean): void { const flags = this.getFlag('readonly') ?? {}; this.setFlag('readonly', { ...flags, diff --git a/blocksuite/presets/src/__tests__/utils/setup.ts b/blocksuite/presets/src/__tests__/utils/setup.ts index 6280aa145c..104dfd5ef1 100644 --- a/blocksuite/presets/src/__tests__/utils/setup.ts +++ b/blocksuite/presets/src/__tests__/utils/setup.ts @@ -1,5 +1,5 @@ import { effects as blocksEffects } from '@blocksuite/blocks/effects'; -import type { BlockCollection, Blocks, Job } from '@blocksuite/store'; +import type { Blocks, Job } from '@blocksuite/store'; import { effects } from '../../effects.js'; @@ -57,9 +57,7 @@ function initCollection(collection: TestWorkspace) { async function createEditor(collection: TestWorkspace, mode: DocMode = 'page') { const app = document.createElement('div'); - const blockCollection = collection.docs.values().next().value as - | BlockCollection - | undefined; + const blockCollection = collection.docs.values().next().value; assertExists(blockCollection, 'Need to create a doc first'); const doc = blockCollection.getDoc(); const editor = new AffineEditorContainer(); diff --git a/blocksuite/tests-legacy/basic.spec.ts b/blocksuite/tests-legacy/basic.spec.ts index eb089a68cf..62f5bae988 100644 --- a/blocksuite/tests-legacy/basic.spec.ts +++ b/blocksuite/tests-legacy/basic.spec.ts @@ -64,11 +64,11 @@ test(scoped`basic init with external text`, async ({ page }) => { await page.evaluate(() => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text('hello'), + title: new window.$blocksuite.store.Text('hello'), }); const note = doc.addBlock('affine:note', {}, rootId); - const text = new doc.Text('world'); + const text = new window.$blocksuite.store.Text('world'); doc.addBlock('affine:paragraph', { text }, note); const delta = [ @@ -78,7 +78,7 @@ test(scoped`basic init with external text`, async ({ page }) => { doc.addBlock( 'affine:paragraph', { - text: new doc.Text(delta as DeltaInsert[]), + text: new window.$blocksuite.store.Text(delta as DeltaInsert[]), }, note ); diff --git a/blocksuite/tests-legacy/drag.spec.ts b/blocksuite/tests-legacy/drag.spec.ts index 345c137e27..fe69220320 100644 --- a/blocksuite/tests-legacy/drag.spec.ts +++ b/blocksuite/tests-legacy/drag.spec.ts @@ -628,7 +628,7 @@ test.fixme( const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); doc.addBlock('affine:surface', {}, rootId); @@ -637,7 +637,7 @@ test.fixme( doc.addBlock( 'affine:paragraph', { - text: new doc.Text(text), + text: new window.$blocksuite.store.Text(text), }, noteId ); diff --git a/blocksuite/tests-legacy/edgeless/edgeless-text.spec.ts b/blocksuite/tests-legacy/edgeless/edgeless-text.spec.ts index 85f4aa164f..75a5713bc2 100644 --- a/blocksuite/tests-legacy/edgeless/edgeless-text.spec.ts +++ b/blocksuite/tests-legacy/edgeless/edgeless-text.spec.ts @@ -554,7 +554,7 @@ test('press backspace at the start of first line when edgeless text exist', asyn await page.evaluate(() => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); doc.addBlock('affine:surface', {}, rootId); doc.addBlock('affine:note', {}, rootId); diff --git a/blocksuite/tests-legacy/embed-synced-doc.spec.ts b/blocksuite/tests-legacy/embed-synced-doc.spec.ts index ab8c4e828a..547fe0382c 100644 --- a/blocksuite/tests-legacy/embed-synced-doc.spec.ts +++ b/blocksuite/tests-legacy/embed-synced-doc.spec.ts @@ -140,7 +140,7 @@ test.describe('Embed synced doc', () => { await page.evaluate(() => { const { doc, collection } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const noteId = doc.addBlock('affine:note', {}, rootId); @@ -149,14 +149,14 @@ test.describe('Embed synced doc', () => { const doc2 = collection.createDoc({ id: 'doc2' }); doc2.load(); const rootId2 = doc2.addBlock('affine:page', { - title: new doc.Text('Doc 2'), + title: new window.$blocksuite.store.Text('Doc 2'), }); const noteId2 = doc2.addBlock('affine:note', {}, rootId2); doc2.addBlock( 'affine:paragraph', { - text: new doc.Text('Hello from Doc 2'), + text: new window.$blocksuite.store.Text('Hello from Doc 2'), }, noteId2 ); @@ -164,14 +164,14 @@ test.describe('Embed synced doc', () => { const doc3 = collection.createDoc({ id: 'doc3' }); doc3.load(); const rootId3 = doc3.addBlock('affine:page', { - title: new doc.Text('Doc 3'), + title: new window.$blocksuite.store.Text('Doc 3'), }); const noteId3 = doc3.addBlock('affine:note', {}, rootId3); doc3.addBlock( 'affine:paragraph', { - text: new doc.Text('Hello from Doc 3'), + text: new window.$blocksuite.store.Text('Hello from Doc 3'), }, noteId3 ); @@ -232,7 +232,7 @@ test.describe('Embed synced doc', () => { const databaseId = doc2.addBlock( 'affine:database', { - title: new doc2.Text('Database 1'), + title: new window.$blocksuite.store.Text('Database 1'), }, noteId ); diff --git a/blocksuite/tests-legacy/format-bar.spec.ts b/blocksuite/tests-legacy/format-bar.spec.ts index 8165f4766b..7c65b4b7a5 100644 --- a/blocksuite/tests-legacy/format-bar.spec.ts +++ b/blocksuite/tests-legacy/format-bar.spec.ts @@ -453,10 +453,10 @@ test('should format quick bar position correct at the start of second line', asy await page.evaluate(() => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const note = doc.addBlock('affine:note', {}, rootId); - const text = new doc.Text('a'.repeat(100)); + const text = new window.$blocksuite.store.Text('a'.repeat(100)); const paragraphId = doc.addBlock('affine:paragraph', { text }, note); return paragraphId; }); @@ -715,7 +715,7 @@ test('should format bar style active correctly', async ({ page }) => { await page.evaluate(() => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const note = doc.addBlock('affine:note', {}, rootId); const delta = [ @@ -723,7 +723,7 @@ test('should format bar style active correctly', async ({ page }) => { { insert: '2', attributes: { bold: true, underline: true } }, { insert: '3', attributes: { bold: true, code: true } }, ]; - const text = new doc.Text(delta as DeltaInsert[]); + const text = new window.$blocksuite.store.Text(delta as DeltaInsert[]); doc.addBlock('affine:paragraph', { text }, note); }); @@ -869,7 +869,7 @@ test('should update the format quick bar state when there is a change in keyboar await page.evaluate(() => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const note = doc.addBlock('affine:note', {}, rootId); const delta = [ @@ -877,7 +877,7 @@ test('should update the format quick bar state when there is a change in keyboar { insert: '2', attributes: { bold: true } }, { insert: '3', attributes: { bold: false } }, ]; - const text = new doc.Text(delta as DeltaInsert[]); + const text = new window.$blocksuite.store.Text(delta as DeltaInsert[]); doc.addBlock('affine:paragraph', { text }, note); }); await focusTitle(page); diff --git a/blocksuite/tests-legacy/link.spec.ts b/blocksuite/tests-legacy/link.spec.ts index 54741227a6..0682198b16 100644 --- a/blocksuite/tests-legacy/link.spec.ts +++ b/blocksuite/tests-legacy/link.spec.ts @@ -121,11 +121,11 @@ async function createLinkBlock(page: Page, str: string, link: string) { ([str, link]) => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text('title'), + title: new window.$blocksuite.store.Text('title'), }); const noteId = doc.addBlock('affine:note', {}, rootId); - const text = new doc.Text([ + const text = new window.$blocksuite.store.Text([ { insert: 'Hello' }, { insert: str, attributes: { link } }, ]); diff --git a/blocksuite/tests-legacy/paragraph.spec.ts b/blocksuite/tests-legacy/paragraph.spec.ts index ad95559f05..1b30980863 100644 --- a/blocksuite/tests-legacy/paragraph.spec.ts +++ b/blocksuite/tests-legacy/paragraph.spec.ts @@ -825,20 +825,20 @@ test('press arrow down should move caret to the start of line', async ({ await page.evaluate(() => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const note = doc.addBlock('affine:note', {}, rootId); doc.addBlock( 'affine:paragraph', { - text: new doc.Text('0'.repeat(100)), + text: new window.$blocksuite.store.Text('0'.repeat(100)), }, note ); doc.addBlock( 'affine:paragraph', { - text: new doc.Text('1'), + text: new window.$blocksuite.store.Text('1'), }, note ); @@ -860,7 +860,7 @@ test('press arrow up in the second line should move caret to the first line', as await page.evaluate(() => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const note = doc.addBlock('affine:note', {}, rootId); const delta = Array.from({ length: 150 }, (_, i) => { @@ -868,7 +868,7 @@ test('press arrow up in the second line should move caret to the first line', as ? { insert: 'i', attributes: { italic: true } } : { insert: 'b', attributes: { bold: true } }; }) as DeltaInsert[]; - const text = new doc.Text(delta); + const text = new window.$blocksuite.store.Text(delta); doc.addBlock('affine:paragraph', { text }, note); doc.addBlock('affine:paragraph', {}, note); }); @@ -912,7 +912,7 @@ test('press arrow down in indent line should not move caret to the start of line await page.evaluate(() => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const note = doc.addBlock('affine:note', {}, rootId); const p1 = doc.addBlock('affine:paragraph', {}, note); @@ -921,7 +921,7 @@ test('press arrow down in indent line should not move caret to the start of line doc.addBlock( 'affine:paragraph', { - text: new doc.Text('0'), + text: new window.$blocksuite.store.Text('0'), }, note ); @@ -1003,20 +1003,22 @@ test.describe('press ArrowDown when cursor is at the last line of a block', () = await page.evaluate(() => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const note = doc.addBlock('affine:note', {}, rootId); doc.addBlock( 'affine:paragraph', { - text: new doc.Text('This is the 2nd last block.'), + text: new window.$blocksuite.store.Text( + 'This is the 2nd last block.' + ), }, note ); doc.addBlock( 'affine:paragraph', { - text: new doc.Text('This is the last block.'), + text: new window.$blocksuite.store.Text('This is the last block.'), }, note ); @@ -1172,7 +1174,7 @@ test('delete at the start of paragraph (multiple notes)', async ({ page }) => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); doc.addBlock('affine:surface', {}, rootId); @@ -1181,7 +1183,7 @@ test('delete at the start of paragraph (multiple notes)', async ({ page }) => { doc.addBlock( 'affine:paragraph', { - text: new doc.Text(text), + text: new window.$blocksuite.store.Text(text), }, noteId ); diff --git a/blocksuite/tests-legacy/utils/actions/misc.ts b/blocksuite/tests-legacy/utils/actions/misc.ts index 3774b221a6..cd8f001d1f 100644 --- a/blocksuite/tests-legacy/utils/actions/misc.ts +++ b/blocksuite/tests-legacy/utils/actions/misc.ts @@ -413,7 +413,7 @@ export async function enterPlaygroundWithList( ({ contents, type }: { contents: string[]; type: ListType }) => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const noteId = doc.addBlock('affine:note', {}, rootId); // eslint-disable-next-line @typescript-eslint/prefer-for-of @@ -421,7 +421,7 @@ export async function enterPlaygroundWithList( doc.addBlock( 'affine:list', contents.length > 0 - ? { text: new doc.Text(contents[i]), type } + ? { text: new window.$blocksuite.store.Text(contents[i]), type } : { type }, noteId ); @@ -439,7 +439,7 @@ export async function initEmptyParagraphState(page: Page, rootId?: string) { doc.captureSync(); if (!rootId) { rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); } @@ -464,7 +464,7 @@ export async function initMultipleNoteWithParagraphState( doc.captureSync(); if (!rootId) { rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); } @@ -490,7 +490,7 @@ export async function initEmptyEdgelessState(page: Page) { const ids = await page.evaluate(() => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); doc.addBlock('affine:surface', {}, rootId); const noteId = doc.addBlock('affine:note', {}, rootId); @@ -509,14 +509,14 @@ export async function initEmptyDatabaseState(page: Page, rootId?: string) { doc.captureSync(); if (!rootId) { rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); } const noteId = doc.addBlock('affine:note', {}, rootId); const databaseId = doc.addBlock( 'affine:database', { - title: new doc.Text('Database 1'), + title: new window.$blocksuite.store.Text('Database 1'), }, noteId ); @@ -553,14 +553,14 @@ export async function initKanbanViewState( doc.captureSync(); if (!rootId) { rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); } const noteId = doc.addBlock('affine:note', {}, rootId); const databaseId = doc.addBlock( 'affine:database', { - title: new doc.Text('Database 1'), + title: new window.$blocksuite.store.Text('Database 1'), }, noteId ); @@ -572,7 +572,7 @@ export async function initKanbanViewState( const rowIds = config.rows.map(rowText => { const rowId = doc.addBlock( 'affine:paragraph', - { type: 'text', text: new doc.Text(rowText) }, + { type: 'text', text: new window.$blocksuite.store.Text(rowText) }, databaseId ); return rowId; @@ -590,7 +590,7 @@ export async function initKanbanViewState( columnId, value: column.type === 'rich-text' - ? new doc.Text(value as string) + ? new window.$blocksuite.store.Text(value as string) : value, }); } @@ -619,14 +619,14 @@ export async function initEmptyDatabaseWithParagraphState( doc.captureSync(); if (!rootId) { rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); } const noteId = doc.addBlock('affine:note', {}, rootId); const databaseId = doc.addBlock( 'affine:database', { - title: new doc.Text('Database 1'), + title: new window.$blocksuite.store.Text('Database 1'), }, noteId ); @@ -1326,7 +1326,7 @@ export async function initImageState(page: Page, prependParagraph = false) { await page.evaluate(async prepend => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const noteId = doc.addBlock('affine:note', {}, rootId); diff --git a/blocksuite/tests-legacy/utils/declare-test-window.ts b/blocksuite/tests-legacy/utils/declare-test-window.ts index 84e37465ac..3bb7dde95d 100644 --- a/blocksuite/tests-legacy/utils/declare-test-window.ts +++ b/blocksuite/tests-legacy/utils/declare-test-window.ts @@ -6,7 +6,7 @@ import type { } from '@blocksuite/block-std'; import type { AffineEditorContainer } from '@blocksuite/presets'; import type { StarterDebugMenu } from '@playground/apps/_common/components/starter-debug-menu.js'; -import type { BlockModel, Doc, Job, Workspace } from '@store/index.js'; +import type { BlockModel, Blocks, Job, Workspace } from '@store/index.js'; declare global { interface Window { @@ -38,7 +38,7 @@ declare global { }; collection: Workspace; blockSchema: Record; - doc: Doc; + doc: Blocks; debugMenu: StarterDebugMenu; editor: AffineEditorContainer; host: EditorHost; diff --git a/blocksuite/tests-legacy/zero-width.spec.ts b/blocksuite/tests-legacy/zero-width.spec.ts index d386969d58..4f1a80e6dc 100644 --- a/blocksuite/tests-legacy/zero-width.spec.ts +++ b/blocksuite/tests-legacy/zero-width.spec.ts @@ -44,7 +44,7 @@ test( await page.evaluate(() => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const note = doc.addBlock('affine:note', {}, rootId); doc.addBlock('affine:code', {}, note); @@ -71,7 +71,7 @@ test( async ({ bookMarkUrl, embedUrl }) => { const { doc } = window; const rootId = doc.addBlock('affine:page', { - title: new doc.Text(), + title: new window.$blocksuite.store.Text(), }); const note = doc.addBlock('affine:note', {}, rootId); doc.addBlock('affine:code', {}, note); diff --git a/packages/frontend/core/src/components/hooks/affine/use-reference-link-helper.ts b/packages/frontend/core/src/components/hooks/affine/use-reference-link-helper.ts index d696c54a36..98ad23f14f 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-reference-link-helper.ts +++ b/packages/frontend/core/src/components/hooks/affine/use-reference-link-helper.ts @@ -1,5 +1,5 @@ import type { DeltaInsert } from '@blocksuite/affine/inline'; -import type { Workspace } from '@blocksuite/affine/store'; +import { Text, type Workspace } from '@blocksuite/affine/store'; import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import { useCallback } from 'react'; @@ -10,7 +10,7 @@ export function useReferenceLinkHelper(docCollection: Workspace) { if (!page) { return; } - const text = new page.Text([ + const text = new Text([ { insert: ' ', attributes: { diff --git a/packages/frontend/core/src/components/page-list/__tests__/use-block-suite-page-preview.spec.ts b/packages/frontend/core/src/components/page-list/__tests__/use-block-suite-page-preview.spec.ts index 12b6c27e79..b7b2cabc53 100644 --- a/packages/frontend/core/src/components/page-list/__tests__/use-block-suite-page-preview.spec.ts +++ b/packages/frontend/core/src/components/page-list/__tests__/use-block-suite-page-preview.spec.ts @@ -5,7 +5,7 @@ import 'fake-indexeddb/auto'; import { AffineSchemas } from '@blocksuite/affine/blocks/schemas'; import { assertExists } from '@blocksuite/affine/global/utils'; -import type { Blocks } from '@blocksuite/affine/store'; +import { type Blocks, Text } from '@blocksuite/affine/store'; import { Schema } from '@blocksuite/store'; import { TestWorkspace } from '@blocksuite/store/test'; import { renderHook } from '@testing-library/react'; @@ -27,7 +27,7 @@ beforeEach(async () => { expect(page).not.toBeNull(); assertExists(page); const pageBlockId = page.addBlock('affine:page', { - title: new page.Text(''), + title: new Text(''), }); const frameId = page.addBlock('affine:note', {}, pageBlockId); page.addBlock('affine:paragraph', {}, frameId); @@ -41,7 +41,7 @@ describe('useBlockSuitePagePreview', () => { const id = page.addBlock( 'affine:paragraph', { - text: new page.Text('Hello, world!'), + text: new Text('Hello, world!'), }, page.getBlockByFlavour('affine:note')[0].id ); @@ -58,7 +58,7 @@ describe('useBlockSuitePagePreview', () => { page.addBlock( 'affine:paragraph', { - text: new page.Text('First block!'), + text: new Text('First block!'), }, page.getBlockByFlavour('affine:note')[0].id, 0 diff --git a/packages/frontend/core/src/modules/doc/services/docs.ts b/packages/frontend/core/src/modules/doc/services/docs.ts index 2e4087c365..1feb8086d4 100644 --- a/packages/frontend/core/src/modules/doc/services/docs.ts +++ b/packages/frontend/core/src/modules/doc/services/docs.ts @@ -2,6 +2,7 @@ import { DebugLogger } from '@affine/debug'; import { Unreachable } from '@affine/env/constant'; import type { DocMode } from '@blocksuite/affine/blocks'; import type { DeltaInsert } from '@blocksuite/affine/inline'; +import { Text } from '@blocksuite/affine/store'; import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import { LiveData, ObjectPool, Service } from '@toeverything/infra'; import { omitBy } from 'lodash-es'; @@ -121,7 +122,7 @@ export class DocsService extends Service { const { doc, release } = this.open(targetDocId); doc.setPriorityLoad(10); await doc.waitForSyncReady(); - const text = new doc.blockSuiteDoc.Text([ + const text = new Text([ { insert: ' ', attributes: { diff --git a/packages/frontend/core/src/modules/workspace/impl/doc.ts b/packages/frontend/core/src/modules/workspace/impl/doc.ts new file mode 100644 index 0000000000..039d8bddc3 --- /dev/null +++ b/packages/frontend/core/src/modules/workspace/impl/doc.ts @@ -0,0 +1,383 @@ +import { type Disposable, Slot } from '@blocksuite/affine/global/utils'; +import { + type AwarenessStore, + Blocks, + type BlockSuiteDoc, + type Doc, + type GetDocOptions, + type Query, + type Workspace, + type YBlock, +} from '@blocksuite/affine/store'; +import { signal } from '@preact/signals-core'; +import * as Y from 'yjs'; + +type DocOptions = { + id: string; + collection: Workspace; + doc: BlockSuiteDoc; + awarenessStore: AwarenessStore; +}; + +export class DocImpl implements Doc { + private _awarenessUpdateDisposable: Disposable | null = null; + + private readonly _canRedo = signal(false); + + private readonly _canUndo = signal(false); + + private readonly _collection: Workspace; + + private readonly _docMap = { + undefined: new Map(), + true: new Map(), + false: new Map(), + }; + + // doc/space container. + private readonly _handleYEvents = (events: Y.YEvent[]) => { + events.forEach(event => this._handleYEvent(event)); + }; + + private _history!: Y.UndoManager; + + private readonly _historyObserver = () => { + this._updateCanUndoRedoSignals(); + this.slots.historyUpdated.emit(); + }; + + private readonly _initSubDoc = () => { + let subDoc = this.rootDoc.spaces.get(this.id); + if (!subDoc) { + subDoc = new Y.Doc({ + guid: this.id, + }); + this.rootDoc.spaces.set(this.id, subDoc); + this._loaded = true; + this._onLoadSlot.emit(); + } else { + this._loaded = false; + this.rootDoc.on('subdocs', this._onSubdocEvent); + } + + return subDoc; + }; + + private _loaded!: boolean; + + private readonly _onLoadSlot = new Slot(); + + private readonly _onSubdocEvent = ({ + loaded, + }: { + loaded: Set; + }): void => { + const result = Array.from(loaded).find( + doc => doc.guid === this._ySpaceDoc.guid + ); + if (!result) { + return; + } + this.rootDoc.off('subdocs', this._onSubdocEvent); + this._loaded = true; + this._onLoadSlot.emit(); + }; + + /** Indicate whether the block tree is ready */ + private _ready = false; + + private _shouldTransact = true; + + private readonly _updateCanUndoRedoSignals = () => { + const canRedo = this.readonly ? false : this._history.canRedo(); + const canUndo = this.readonly ? false : this._history.canUndo(); + if (this._canRedo.peek() !== canRedo) { + this._canRedo.value = canRedo; + } + if (this._canUndo.peek() !== canUndo) { + this._canUndo.value = canUndo; + } + }; + + protected readonly _yBlocks: Y.Map; + + /** + * @internal Used for convenient access to the underlying Yjs map, + * can be used interchangeably with ySpace + */ + protected readonly _ySpaceDoc: Y.Doc; + + readonly awarenessStore: AwarenessStore; + + readonly id: string; + + readonly rootDoc: BlockSuiteDoc; + + readonly slots = { + historyUpdated: new Slot(), + yBlockUpdated: new Slot< + | { + type: 'add'; + id: string; + } + | { + type: 'delete'; + id: string; + } + >(), + }; + + get blobSync() { + return this.collection.blobSync; + } + + get canRedo() { + return this._canRedo.peek(); + } + + get canUndo() { + return this._canUndo.peek(); + } + + get collection() { + return this._collection; + } + + get docSync() { + return this.collection.docSync; + } + + get history() { + return this._history; + } + + get isEmpty() { + return this._yBlocks.size === 0; + } + + get loaded() { + return this._loaded; + } + + get meta() { + return this.collection.meta.getDocMeta(this.id); + } + + get readonly(): boolean { + return this.awarenessStore.isReadonly(this); + } + + get ready() { + return this._ready; + } + + get schema() { + return this.collection.schema; + } + + get spaceDoc() { + return this._ySpaceDoc; + } + + get yBlocks() { + return this._yBlocks; + } + + constructor({ id, collection, doc, awarenessStore }: DocOptions) { + this.id = id; + this.rootDoc = doc; + this.awarenessStore = awarenessStore; + + this._ySpaceDoc = this._initSubDoc(); + + this._yBlocks = this._ySpaceDoc.getMap('blocks'); + this._collection = collection; + } + + private _getReadonlyKey(readonly?: boolean): 'true' | 'false' | 'undefined' { + return (readonly?.toString() as 'true' | 'false') ?? 'undefined'; + } + + private _handleVersion() { + // Initialization from empty yDoc, indicating that the document is new. + if (!this.collection.meta.hasVersion) { + this.collection.meta.writeVersion(this.collection); + } + } + + private _handleYBlockAdd(id: string) { + this.slots.yBlockUpdated.emit({ type: 'add', id }); + } + + private _handleYBlockDelete(id: string) { + this.slots.yBlockUpdated.emit({ type: 'delete', id }); + } + + private _handleYEvent(event: Y.YEvent>) { + // event on top-level block store + if (event.target !== this._yBlocks) { + return; + } + event.keys.forEach((value, id) => { + try { + if (value.action === 'add') { + this._handleYBlockAdd(id); + return; + } + if (value.action === 'delete') { + this._handleYBlockDelete(id); + return; + } + } catch (e) { + console.error('An error occurred while handling Yjs event:'); + console.error(e); + } + }); + } + + private _initYBlocks() { + const { _yBlocks } = this; + _yBlocks.observeDeep(this._handleYEvents); + this._history = new Y.UndoManager([_yBlocks], { + trackedOrigins: new Set([this._ySpaceDoc.clientID]), + }); + + this._history.on('stack-cleared', this._historyObserver); + this._history.on('stack-item-added', this._historyObserver); + this._history.on('stack-item-popped', this._historyObserver); + this._history.on('stack-item-updated', this._historyObserver); + } + + /** Capture current operations to undo stack synchronously. */ + captureSync() { + this._history.stopCapturing(); + } + + clear() { + this._yBlocks.clear(); + } + + clearQuery(query: Query, readonly?: boolean) { + const readonlyKey = this._getReadonlyKey(readonly); + + this._docMap[readonlyKey].delete(JSON.stringify(query)); + } + + private _destroy() { + this._ySpaceDoc.destroy(); + this._onLoadSlot.dispose(); + this._loaded = false; + } + + dispose() { + this.slots.historyUpdated.dispose(); + this._awarenessUpdateDisposable?.dispose(); + + if (this.ready) { + this._yBlocks.unobserveDeep(this._handleYEvents); + this._yBlocks.clear(); + } + } + + getDoc({ readonly, query }: GetDocOptions = {}) { + const readonlyKey = this._getReadonlyKey(readonly); + + const key = JSON.stringify(query); + + if (this._docMap[readonlyKey].has(key)) { + return this._docMap[readonlyKey].get(key) as Blocks; + } + + const doc = new Blocks({ + blockCollection: this, + schema: this.collection.schema, + readonly, + query, + }); + + this._docMap[readonlyKey].set(key, doc); + + return doc; + } + + load(initFn?: () => void): this { + if (this.ready) { + return this; + } + + this._ySpaceDoc.load(); + + if ((this.collection.meta.docs?.length ?? 0) <= 1) { + this._handleVersion(); + } + + this._initYBlocks(); + + this._yBlocks.forEach((_, id) => { + this._handleYBlockAdd(id); + }); + + this._awarenessUpdateDisposable = this.awarenessStore.slots.update.on( + () => { + // change readonly state will affect the undo/redo state + this._updateCanUndoRedoSignals(); + } + ); + + initFn?.(); + + this._ready = true; + + return this; + } + + redo() { + if (this.readonly) { + console.error('cannot modify data in readonly mode'); + return; + } + this._history.redo(); + } + + undo() { + if (this.readonly) { + console.error('cannot modify data in readonly mode'); + return; + } + this._history.undo(); + } + + remove() { + this._destroy(); + this.rootDoc.spaces.delete(this.id); + } + + resetHistory() { + this._history.clear(); + } + + /** + * If `shouldTransact` is `false`, the transaction will not be push to the history stack. + */ + transact(fn: () => void, shouldTransact: boolean = this._shouldTransact) { + this._ySpaceDoc.transact( + () => { + try { + fn(); + } catch (e) { + console.error( + `An error occurred while Y.doc ${this._ySpaceDoc.guid} transacting:` + ); + console.error(e); + } + }, + shouldTransact ? this.rootDoc.clientID : null + ); + } + + withoutTransact(callback: () => void) { + this._shouldTransact = false; + callback(); + this._shouldTransact = true; + } +} diff --git a/packages/frontend/core/src/modules/workspace/impl/workspace.ts b/packages/frontend/core/src/modules/workspace/impl/workspace.ts index ac71bcc97f..69dfad5ca2 100644 --- a/packages/frontend/core/src/modules/workspace/impl/workspace.ts +++ b/packages/frontend/core/src/modules/workspace/impl/workspace.ts @@ -6,10 +6,10 @@ import type { BlockSuiteFlags } from '@blocksuite/affine/global/types'; import { NoopLogger, Slot } from '@blocksuite/affine/global/utils'; import { AwarenessStore, - BlockCollection, type Blocks, BlockSuiteDoc, type CreateDocOptions, + type Doc, DocCollectionMeta, type GetDocOptions, type IdGenerator, @@ -27,6 +27,8 @@ import { } from '@blocksuite/affine/sync'; import { Awareness } from 'y-protocols/awareness.js'; +import { DocImpl } from './doc'; + type WorkspaceOptions = { id?: string; schema: Schema; @@ -62,7 +64,7 @@ export class WorkspaceImpl implements Workspace { readonly blobSync: BlobEngine; - readonly blockCollections = new Map(); + readonly blockCollections = new Map(); readonly doc: BlockSuiteDoc; @@ -114,12 +116,11 @@ export class WorkspaceImpl implements Workspace { private _bindDocMetaEvents() { this.meta.docMetaAdded.on(docId => { - const doc = new BlockCollection({ + const doc = new DocImpl({ id: docId, collection: this, doc: this.doc, awarenessStore: this.awarenessStore, - idGenerator: this.idGenerator, }); this.blockCollections.set(doc.id, doc); }); @@ -185,8 +186,8 @@ export class WorkspaceImpl implements Workspace { this.awarenessSync.disconnect(); } - getBlockCollection(docId: string): BlockCollection | null { - const space = this.docs.get(docId) as BlockCollection | undefined; + getBlockCollection(docId: string): Doc | null { + const space = this.docs.get(docId) as Doc | undefined; return space ?? null; }