feat(editor): improve api for store, and add docs (#10941)

This commit is contained in:
Saul-Mirone
2025-03-17 16:30:59 +00:00
parent b0aa2c90fd
commit 3de7d85eea
47 changed files with 1212 additions and 210 deletions

View File

@@ -82,7 +82,7 @@ describe('DatabaseManager', () => {
noteBlockId
);
const databaseModel = doc.getBlockById(
const databaseModel = doc.getModelById(
databaseBlockId
) as DatabaseBlockModel;
db = databaseModel;
@@ -187,7 +187,7 @@ describe('DatabaseManager', () => {
addProperty(db, 'end', column);
updateCell(db, modelId, cell);
const model = doc.getBlockById(modelId);
const model = doc.getModelById(modelId);
expect(model).not.toBeNull();

View File

@@ -466,7 +466,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
}
rowMove(rowId: string, position: InsertToPosition): void {
const model = this.doc.getBlockById(rowId);
const model = this.doc.getModelById(rowId);
if (model) {
const index = insertPositionToIndex(position, this._model.children);
const target = this._model.children[index];

View File

@@ -68,7 +68,7 @@ export async function uploadBlobForImage(
} finally {
setImageUploaded(blockId);
const imageModel = doc.getBlockById(blockId) as ImageBlockModel | null;
const imageModel = doc.getModelById(blockId) as ImageBlockModel | null;
if (sourceId && imageModel) {
const props: Partial<ImageBlockProps> = {
sourceId,

View File

@@ -84,7 +84,7 @@ export const updateBlockType: Command<
if (flavour !== 'affine:code') return;
const id = mergeToCodeModel(blockModels);
if (!id) return;
const model = doc.getBlockById(id);
const model = doc.getModelById(id);
if (!model) return;
asyncSetInlineRange(host, model, {
index: model.text?.length ?? 0,
@@ -115,7 +115,7 @@ export const updateBlockType: Command<
nextSiblingId = doc.addBlock('affine:paragraph', {}, parent);
}
focusTextModel(host.std, nextSiblingId);
const newModel = doc.getBlockById(id);
const newModel = doc.getModelById(id);
if (!newModel) {
return next({ updatedBlocks: [] });
}
@@ -217,7 +217,7 @@ export const updateBlockType: Command<
if (!newId) {
return;
}
const newModel = doc.getBlockById(newId);
const newModel = doc.getModelById(newId);
if (newModel) {
newModels.push(newModel);
}

View File

@@ -1091,7 +1091,7 @@ export class EdgelessClipboardController extends PageClipboard {
const newSelected = [
...canvasElementIds,
...blockIds.filter(id => {
return isTopLevelBlock(this.doc.getBlockById(id));
return isTopLevelBlock(this.doc.getModelById(id));
}),
];

View File

@@ -380,7 +380,7 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
this.edgeless
);
} else {
const model = doc.getBlockById(id);
const model = doc.getModelById(id);
if (!model) {
return;
}

View File

@@ -182,7 +182,7 @@ export class NoteSlicer extends WidgetComponent<
doc.root?.id
);
doc.moveBlocks(resetBlocks, doc.getBlockById(newNoteId) as NoteBlockModel);
doc.moveBlocks(resetBlocks, doc.getModelById(newNoteId) as NoteBlockModel);
this._activeSlicerIndex = 0;
this._selection.set({

View File

@@ -188,7 +188,7 @@ export class TemplateJob {
if (isMergeBlock) {
this._mergeProps(
json,
this.model.doc.getBlockById(
this.model.doc.getModelById(
this._getMergeBlockId(json)
) as BlockModel
);

View File

@@ -191,7 +191,7 @@ export class PageRootBlockComponent extends BlockComponent<
const { doc } = this;
const noteId = doc.addBlock('affine:note', {}, doc.root?.id);
return doc.getBlockById(noteId) as NoteBlockModel;
return doc.getModelById(noteId) as NoteBlockModel;
}
private _getDefaultNoteBlock() {
@@ -262,7 +262,7 @@ export class PageRootBlockComponent extends BlockComponent<
);
if (!sel) return;
let model: BlockModel | null = null;
let current = this.doc.getBlockById(sel.blockId);
let current = this.doc.getModelById(sel.blockId);
while (current && !model) {
if (current.flavour === 'affine:note') {
model = current;
@@ -280,7 +280,7 @@ export class PageRootBlockComponent extends BlockComponent<
}
return;
}
const notes = this.doc.getBlockByFlavour('affine:note');
const notes = this.doc.getModelsByFlavour('affine:note');
const index = notes.indexOf(prevNote);
if (index !== 0) return;

View File

@@ -281,7 +281,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
flavour: 'affine:paragraph',
},
]);
const model = this.doc.getBlockById(paragraphId);
const model = this.doc.getModelById(paragraphId);
if (!model) return;
requestConnectedFrame(() => {

View File

@@ -129,7 +129,7 @@ export class EdgelessCRUDExtension extends Extension {
return;
}
const block = this.std.store.getBlockById(id);
const block = this.std.store.getModelById(id);
if (block) {
const key = getLastPropsKey(block.flavour, {
...block.yBlock.toJSON(),
@@ -145,7 +145,7 @@ export class EdgelessCRUDExtension extends Extension {
if (!surface) {
return null;
}
const el = surface.getElementById(id) ?? this.std.store.getBlockById(id);
const el = surface.getElementById(id) ?? this.std.store.getModelById(id);
return el as GfxModel | null;
}

View File

@@ -602,7 +602,7 @@ function getNotesInFrameBound(
): NoteBlockModel[] {
const bound = Bound.deserialize(frame.xywh);
return (doc.getBlockByFlavour('affine:note') as NoteBlockModel[]).filter(
return (doc.getModelsByFlavour('affine:note') as NoteBlockModel[]).filter(
ele => {
const xywh = Bound.deserialize(ele.xywh);

View File

@@ -32,7 +32,7 @@ export function mindmap(
const { connector, outdated } = result;
const elementGetter = (id: string) =>
model.surface.getElementById(id) ??
(model.surface.doc.getBlockById(id) as GfxModel);
(model.surface.doc.getModelById(id) as GfxModel);
if (outdated) {
ConnectorPathGenerator.updatePath(connector, null, elementGetter);

View File

@@ -25,7 +25,7 @@ export class SurfaceBlockService extends BlockService {
const disposable = this.doc.slots.blockUpdated.subscribe(payload => {
if (payload.flavour === 'affine:surface') {
disposable.unsubscribe();
const surface = this.doc.getBlockById(
const surface = this.doc.getModelById(
payload.id
) as SurfaceBlockModel | null;
if (!surface) return;

View File

@@ -100,7 +100,7 @@ export function addNote(
noteId
);
if (options.collapse && height > NOTE_MIN_HEIGHT) {
const note = doc.getBlockById(noteId) as NoteBlockModel;
const note = doc.getModelById(noteId) as NoteBlockModel;
doc.updateBlock(note, () => {
note.props.edgeless.collapse = true;
note.props.edgeless.collapsedHeight = height;

View File

@@ -8,9 +8,9 @@ export const connectorWatcher: SurfaceMiddleware = (
surface: SurfaceBlockModel
) => {
const hasElementById = (id: string) =>
surface.hasElementById(id) || surface.doc.hasBlockById(id);
surface.hasElementById(id) || surface.doc.hasBlock(id);
const elementGetter = (id: string) =>
surface.getElementById(id) ?? (surface.doc.getBlockById(id) as GfxModel);
surface.getElementById(id) ?? (surface.doc.getModelById(id) as GfxModel);
const updateConnectorPath = (connector: ConnectorElementModel) => {
if (
((connector.source?.id && hasElementById(connector.source.id)) ||

View File

@@ -78,7 +78,7 @@ export class FrameBlockModel
for (const key of this.childIds) {
const element =
this.surface.getElementById(key) ||
(this.surface.doc.getBlockById(key) as GfxBlockElementModel);
(this.surface.doc.getModelById(key) as GfxBlockElementModel);
element && elements.push(element);
}

View File

@@ -15,7 +15,7 @@ export class RootBlockModel extends BlockModel<RootBlockProps> {
const createdSubscription = this.created.subscribe(() => {
createdSubscription.unsubscribe();
this.doc.slots.rootAdded.subscribe(id => {
const model = this.doc.getBlockById(id);
const model = this.doc.getModelById(id);
if (model instanceof RootBlockModel) {
const newDocMeta = this.doc.workspace.meta.getDocMeta(model.doc.id);
if (

View File

@@ -45,7 +45,7 @@ export function calcDropTarget(
*/
allowSublist: boolean = true
): DropTarget | null {
const schema = model.doc.getSchemaByFlavour('affine:database');
const schema = model.doc.schema.get('affine:database');
const children = schema?.model.children ?? [];
let shouldAppendToDatabase = true;

View File

@@ -62,7 +62,7 @@ export class EdgelessWatcher {
};
private readonly _showDragHandle = async () => {
const surfaceModel = this.widget.doc.getBlockByFlavour('affine:surface');
const surfaceModel = this.widget.doc.getModelsByFlavour('affine:surface');
const surface = this.widget.std.view.getBlock(
surfaceModel[0]!.id
) as SurfaceBlockComponent;

View File

@@ -10,3 +10,7 @@
- [Extension](classes/Extension.md)
- [StoreExtension](classes/StoreExtension.md)
## Store
- [Store](classes/Store.md)

View File

@@ -0,0 +1,786 @@
[**@blocksuite/store**](../../../@blocksuite/store/README.md)
***
[BlockSuite API Documentation](../../../README.md) / [@blocksuite/store](../README.md) / Store
# Class: Store
Core store class that manages blocks and their lifecycle in BlockSuite
## Remarks
The Store class is responsible for managing the lifecycle of blocks, handling transactions,
and maintaining the block tree structure.
A store is a piece of data created from one or a part of a Y.Doc.
## Block CRUD
### updateBlock()
> **updateBlock**: \<`T`\>(`model`, `props`) => `void`(`model`, `callback`) => `void`
Updates a block's properties or executes a callback in a transaction
#### Type Parameters
##### T
`T` *extends* `Partial`\<`BlockProps`\>
#### Parameters
##### model
`string` | `BlockModel`\<`object`\>
##### props
`T`
#### Returns
`void`
#### Parameters
##### model
`string` | `BlockModel`\<`object`\>
##### callback
() => `void`
#### Returns
`void`
#### Param
The block model or block ID to update
#### Param
Either a callback function to execute or properties to update
#### Throws
When the block is not found or schema validation fails
***
### blockSize
#### Get Signature
> **get** **blockSize**(): `number`
Get the number of blocks in the store
##### Returns
`number`
***
### addBlock()
> **addBlock**(`flavour`, `blockProps`, `parent`?, `parentIndex`?): `string`
Creates and adds a new block to the store
#### Parameters
##### flavour
`string`
The block's flavour (type)
##### blockProps
`Partial`\<`BlockSysProps` & `Record`\<`string`, `unknown`\> & `Omit`\<`BlockProps`, `"flavour"`\>\> = `{}`
Optional properties for the new block
##### parent?
Optional parent block or parent block ID
`null` | `string` | `BlockModel`\<`object`\>
##### parentIndex?
`number`
Optional index position in parent's children
#### Returns
`string`
The ID of the newly created block
#### Throws
When store is in readonly mode
***
### addBlocks()
> **addBlocks**(`blocks`, `parent`?, `parentIndex`?): `string`[]
Add multiple blocks to the store
#### Parameters
##### blocks
`object`[]
Array of blocks to add
##### parent?
Optional parent block or parent block ID
`null` | `string` | `BlockModel`\<`object`\>
##### parentIndex?
`number`
Optional index position in parent's children
#### Returns
`string`[]
Array of IDs of the newly created blocks
***
### addSiblingBlocks()
> **addSiblingBlocks**(`targetModel`, `props`, `place`): `string`[]
Add sibling blocks to the store
#### Parameters
##### targetModel
`BlockModel`
The target block model
##### props
`Partial`\<`BlockProps`\>[]
Array of block properties
##### place
Optional position to place the new blocks ('after' or 'before')
`"after"` | `"before"`
#### Returns
`string`[]
Array of IDs of the newly created blocks
***
### deleteBlock()
> **deleteBlock**(`model`, `options`): `void`
Delete a block from the store
#### Parameters
##### model
The block model or block ID to delete
`string` | `BlockModel`\<`object`\>
##### options
Optional options for the deletion
###### bringChildrenTo?
`BlockModel`\<`object`\>
Optional block model to bring children to
###### deleteChildren?
`boolean`
Optional flag to delete children
#### Returns
`void`
***
### getAllModels()
> **getAllModels**(): `BlockModel`\<`object`\>[]
Get all models in the store
#### Returns
`BlockModel`\<`object`\>[]
Array of all models
***
### getBlock()
> **getBlock**(`id`): `undefined` \| `Block`
Gets a block by its ID
#### Parameters
##### id
`string`
The block's ID
#### Returns
`undefined` \| `Block`
The block instance if found, undefined otherwise
***
### getBlock$()
> **getBlock$**(`id`): `undefined` \| `Block`
Gets a block by its ID
#### Parameters
##### id
`string`
The block's ID
#### Returns
`undefined` \| `Block`
The block instance in signal if found, undefined otherwise
***
### getBlocksByFlavour()
> **getBlocksByFlavour**(`blockFlavour`): `Block`[]
Gets all blocks of specified flavour(s)
#### Parameters
##### blockFlavour
Single flavour or array of flavours to filter by
`string` | `string`[]
#### Returns
`Block`[]
Array of matching blocks
***
### getModelById()
> **getModelById**\<`Model`\>(`id`): `null` \| `Model`
Get a model by its ID
#### Type Parameters
##### Model
`Model` *extends* `BlockModel`\<`object`\> = `BlockModel`\<`object`\>
#### Parameters
##### id
`string`
The model's ID
#### Returns
`null` \| `Model`
The model instance if found, null otherwise
***
### getModelsByFlavour()
> **getModelsByFlavour**(`blockFlavour`): `BlockModel`\<`object`\>[]
Get all models of specified flavour(s)
#### Parameters
##### blockFlavour
Single flavour or array of flavours to filter by
`string` | `string`[]
#### Returns
`BlockModel`\<`object`\>[]
Array of matching models
***
### getNext()
> **getNext**(`block`): `null` \| `BlockModel`\<`object`\>
Get the next sibling block of a given block
#### Parameters
##### block
Block model or block ID to find next sibling for
`string` | `BlockModel`\<`object`\>
#### Returns
`null` \| `BlockModel`\<`object`\>
The next sibling block model if found, null otherwise
***
### getNexts()
> **getNexts**(`block`): `BlockModel`\<`object`\>[]
Get all next sibling blocks of a given block
#### Parameters
##### block
Block model or block ID to find next siblings for
`string` | `BlockModel`\<`object`\>
#### Returns
`BlockModel`\<`object`\>[]
Array of next sibling blocks if found, empty array otherwise
***
### getParent()
> **getParent**(`target`): `null` \| `BlockModel`\<`object`\>
Gets the parent block of a given block
#### Parameters
##### target
Block model or block ID to find parent for
`string` | `BlockModel`\<`object`\>
#### Returns
`null` \| `BlockModel`\<`object`\>
The parent block model if found, null otherwise
***
### getPrev()
> **getPrev**(`block`): `null` \| `BlockModel`\<`object`\>
Get the previous sibling block of a given block
#### Parameters
##### block
Block model or block ID to find previous sibling for
`string` | `BlockModel`\<`object`\>
#### Returns
`null` \| `BlockModel`\<`object`\>
The previous sibling block model if found, null otherwise
***
### getPrevs()
> **getPrevs**(`block`): `BlockModel`\<`object`\>[]
Get all previous sibling blocks of a given block
#### Parameters
##### block
Block model or block ID to find previous siblings for
`string` | `BlockModel`\<`object`\>
#### Returns
`BlockModel`\<`object`\>[]
Array of previous sibling blocks if found, empty array otherwise
***
### hasBlock()
> **hasBlock**(`id`): `boolean`
Check if a block exists by its ID
#### Parameters
##### id
`string`
The block's ID
#### Returns
`boolean`
True if the block exists, false otherwise
***
### moveBlocks()
> **moveBlocks**(`blocksToMove`, `newParent`, `targetSibling`, `shouldInsertBeforeSibling`): `void`
Move blocks to a new parent block
#### Parameters
##### blocksToMove
`BlockModel`\<`object`\>[]
Array of block models to move
##### newParent
`BlockModel`
The new parent block model
##### targetSibling
Optional target sibling block model
`null` | `BlockModel`\<`object`\>
##### shouldInsertBeforeSibling
`boolean` = `true`
Optional flag to insert before sibling
#### Returns
`void`
## Store Lifecycle
### disposableGroup
> **disposableGroup**: `DisposableGroup`
Group of disposable resources managed by the store
***
### slots
> `readonly` **slots**: `object` & `object`
Slots for receiving events from the store.
#### Type declaration
##### historyUpdated
> **historyUpdated**: `Subject`\<`void`\>
This fires when the doc history is updated.
##### yBlockUpdated
> **yBlockUpdated**: `Subject`\<\{ `id`: `string`; `isLocal`: `boolean`; `type`: `"add"`; \} \| \{ `id`: `string`; `isLocal`: `boolean`; `type`: `"delete"`; \}\>
This fires when the doc yBlock is updated.
#### Type declaration
##### blockUpdated
> **blockUpdated**: `Subject`\<`BlockUpdatedPayload`\>
This fires when a block is updated via API call or has just been updated from existing ydoc.
##### ready
> **ready**: `Subject`\<`void`\>
This is always triggered after `doc.load` is called.
##### rootAdded
> **rootAdded**: `Subject`\<`string`\>
This fires when the root block is added via API call or has just been initialized from existing ydoc.
useful for internal block UI components to start subscribing following up events.
Note that at this moment, the whole block tree may not be fully initialized yet.
##### rootDeleted
> **rootDeleted**: `Subject`\<`string`\>
This fires when the root block is deleted via API call or has just been removed from existing ydoc.
***
### dispose()
> **dispose**(): `void`
Disposes the store and releases all resources
#### Returns
`void`
***
### load()
> **load**(`initFn`?): `Store`
Initializes and loads the store
#### Parameters
##### initFn?
() => `void`
Optional initialization function
#### Returns
`Store`
The store instance
## Transformer
### getTransformer()
> **getTransformer**(`middlewares`): `Transformer`
Creates a new transformer instance for the store
#### Parameters
##### middlewares
`TransformerMiddleware`[] = `[]`
Optional array of transformer middlewares
#### Returns
`Transformer`
A new Transformer instance
## Other
### awarenessStore
#### Get Signature
> **get** **awarenessStore**(): `AwarenessStore`
Get the AwarenessStore instance for current store
##### Returns
`AwarenessStore`
***
### blobSync
#### Get Signature
> **get** **blobSync**(): `BlobEngine`
Get the BlobEngine instance for current store.
##### Returns
`BlobEngine`
***
### doc
#### Get Signature
> **get** **doc**(): `Doc`
Get the Doc instance for current store.
##### Returns
`Doc`
***
### get
#### Get Signature
> **get** **get**(): \<`T`\>(`identifier`, `options`?) => `T`
Get an extension instance from the store
##### Example
```ts
const extension = store.get(SomeExtension);
```
##### Returns
`Function`
The extension instance
###### Type Parameters
###### T
`T`
###### Parameters
###### identifier
`GeneralServiceIdentifier`\<`T`\>
###### options?
`ResolveOptions`
###### Returns
`T`
***
### getOptional
#### Get Signature
> **get** **getOptional**(): \<`T`\>(`identifier`, `options`?) => `null` \| `T`
Optional get an extension instance from the store.
The major difference between `get` and `getOptional` is that `getOptional` will not throw an error if the extension is not found.
##### Example
```ts
const extension = store.getOptional(SomeExtension);
```
##### Returns
`Function`
The extension instance
###### Type Parameters
###### T
`T`
###### Parameters
###### identifier
`GeneralServiceIdentifier`\<`T`\>
###### options?
`ResolveOptions`
###### Returns
`null` \| `T`
***
### provider
#### Get Signature
> **get** **provider**(): `ServiceProvider`
Get the di provider for current store.
##### Returns
`ServiceProvider`

View File

@@ -10,7 +10,8 @@
"excludeProtected": true,
"excludeExternals": true,
"externalPattern": ["node_modules/**/*"],
"disableSources": true
"disableSources": true,
"categoryOrder": ["*", "Other"]
},
"readme": "none",
"plugin": ["typedoc-plugin-markdown"],

View File

@@ -492,7 +492,7 @@ export class LayerManager extends GfxExtension {
private _reset() {
const elements = (
this._doc
.getStore()
.getAllModels()
.filter(
model =>
model instanceof GfxBlockElementModel &&
@@ -798,7 +798,7 @@ export class LayerManager extends GfxExtension {
this._disposable.add(
store.slots.blockUpdated.subscribe(payload => {
if (payload.type === 'add') {
const block = store.getBlockById(payload.id)!;
const block = store.getModelById(payload.id)!;
if (
block instanceof GfxBlockElementModel &&
@@ -810,7 +810,7 @@ export class LayerManager extends GfxExtension {
}
}
if (payload.type === 'update') {
const block = store.getBlockById(payload.id)!;
const block = store.getModelById(payload.id)!;
if (
(payload.props.key === 'index' ||
@@ -825,7 +825,7 @@ export class LayerManager extends GfxExtension {
}
}
if (payload.type === 'delete') {
const block = store.getBlockById(payload.id);
const block = store.getModelById(payload.id);
if (block instanceof GfxBlockElementModel) {
this.delete(block as GfxBlockElementModel);

View File

@@ -396,7 +396,7 @@ export abstract class GfxGroupLikeElementModel<
for (const key of this.childIds) {
const element =
this.surface.getElementById(key) ||
(this.surface.doc.getBlockById(key) as GfxBlockElementModel);
(this.surface.doc.getModelById(key) as GfxBlockElementModel);
element && elements.push(element);
}

View File

@@ -316,7 +316,7 @@ export class GfxSelectionManager extends GfxExtension {
}
const { blocks = [], elements = [] } = groupBy(selection.elements, id => {
return this.std.store.getBlockById(id) ? 'blocks' : 'elements';
return this.std.store.getModelById(id) ? 'blocks' : 'elements';
});
let instances: (SurfaceSelection | CursorSelection)[] = [];

View File

@@ -236,7 +236,7 @@ export class RangeBinding {
return;
}
const model = this.host.doc.getBlockById(textSelection.blockId);
const model = this.host.doc.getModelById(textSelection.blockId);
// If the model is not found, the selection maybe in another editor
if (!model) return;

View File

@@ -109,7 +109,7 @@ export class BlockComponent<
if (this._model) {
return this._model;
}
const model = this.doc.getBlockById<Model>(this.blockId);
const model = this.doc.getModelById<Model>(this.blockId);
if (!model) {
throw new BlockSuiteError(
ErrorCode.MissingViewModelError,

View File

@@ -347,7 +347,7 @@ describe('addBlock', () => {
})
);
const blockId = await waitOnce(doc.slots.rootAdded);
const block = doc.getBlockById(blockId) as BlockModel;
const block = doc.getModelById(blockId) as BlockModel;
assert.equal(block.flavour, 'affine:page');
});
@@ -512,7 +512,7 @@ describe('deleteBlock', () => {
},
});
const deletedModel = doc.getBlockById('1') as BlockModel;
const deletedModel = doc.getModelById('1') as BlockModel;
doc.deleteBlock(deletedModel);
assert.deepEqual(serializCollection(doc.rootDoc).spaces[spaceId].blocks, {
@@ -581,8 +581,8 @@ describe('deleteBlock', () => {
},
});
const deletedModel = doc.getBlockById('2') as BlockModel;
const deletedModelParent = doc.getBlockById('1') as BlockModel;
const deletedModel = doc.getModelById('2') as BlockModel;
const deletedModelParent = doc.getModelById('1') as BlockModel;
doc.deleteBlock(deletedModel, {
bringChildrenTo: deletedModelParent,
});
@@ -693,8 +693,8 @@ describe('deleteBlock', () => {
},
});
const deletedModel = doc.getBlockById('2') as BlockModel;
const moveToModel = doc.getBlockById('3') as BlockModel;
const deletedModel = doc.getModelById('2') as BlockModel;
const moveToModel = doc.getModelById('3') as BlockModel;
doc.deleteBlock(deletedModel, {
bringChildrenTo: moveToModel,
});
@@ -820,11 +820,11 @@ describe('getBlock', () => {
doc.addBlock('affine:paragraph', {}, noteId);
doc.addBlock('affine:paragraph', {}, noteId);
const text = doc.getBlockById('3') as BlockModel;
const text = doc.getModelById('3') as BlockModel;
assert.equal(text.flavour, 'affine:paragraph');
assert.equal(rootModel.children[0].children.indexOf(text), 1);
const invalid = doc.getBlockById('😅');
const invalid = doc.getModelById('😅');
assert.equal(invalid, null);
});

View File

@@ -24,7 +24,13 @@ export interface Doc {
dispose(): void;
slots: {
/**
* This fires when the doc history is updated.
*/
historyUpdated: Subject<void>;
/**
* This fires when the doc yBlock is updated.
*/
yBlockUpdated: Subject<
| {
type: 'add';

View File

@@ -61,9 +61,24 @@ export type BlockUpdatedPayload =
const internalExtensions = [StoreSelectionExtension];
/**
* Core store class that manages blocks and their lifecycle in BlockSuite
* @remarks
* The Store class is responsible for managing the lifecycle of blocks, handling transactions,
* and maintaining the block tree structure.
* A store is a piece of data created from one or a part of a Y.Doc.
*
* @category Store
*/
export class Store {
/** @internal */
readonly userExtensions: ExtensionType[];
/**
* Group of disposable resources managed by the store
*
* @category Store Lifecycle
*/
disposableGroup = new DisposableGroup();
private readonly _provider: ServiceProvider;
@@ -91,6 +106,11 @@ export class Store {
private readonly _schema: Schema;
/**
* Slots for receiving events from the store.
*
* @category Store Lifecycle
*/
readonly slots: Doc['slots'] & {
/** This is always triggered after `doc.load` is called. */
ready: Subject<void>;
@@ -100,106 +120,60 @@ export class Store {
* Note that at this moment, the whole block tree may not be fully initialized yet.
*/
rootAdded: Subject<string>;
/**
* This fires when the root block is deleted via API call or has just been removed from existing ydoc.
*/
rootDeleted: Subject<string>;
/**
* This fires when a block is updated via API call or has just been updated from existing ydoc.
*/
blockUpdated: Subject<BlockUpdatedPayload>;
};
updateBlock: {
<T extends Partial<BlockProps>>(model: BlockModel | string, props: T): void;
(model: BlockModel | string, callback: () => void): void;
} = (
modelOrId: BlockModel | string,
callBackOrProps: (() => void) | Partial<BlockProps>
) => {
if (this.readonly) {
console.error('cannot modify data in readonly mode');
return;
}
const isCallback = typeof callBackOrProps === 'function';
const model =
typeof modelOrId === 'string'
? this.getBlock(modelOrId)?.model
: modelOrId;
if (!model) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
`updating block: ${modelOrId} not found`
);
}
if (!isCallback) {
const parent = this.getParent(model);
this.schema.validate(
model.flavour,
parent?.flavour,
callBackOrProps.children?.map(child => child.flavour)
);
}
const yBlock = this._yBlocks.get(model.id);
if (!yBlock) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
`updating block: ${model.id} not found`
);
}
const block = this.getBlock(model.id);
if (!block) return;
this.transact(() => {
if (isCallback) {
callBackOrProps();
this._runQuery(block);
return;
}
if (callBackOrProps.children) {
this._crud.updateBlockChildren(
model.id,
callBackOrProps.children.map(child => child.id)
);
}
const schema = this.schema.flavourSchemaMap.get(model.flavour);
if (!schema) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
`schema for flavour: ${model.flavour} not found`
);
}
syncBlockProps(schema, model, yBlock, callBackOrProps);
this._runQuery(block);
return;
});
};
private get _yBlocks() {
return this._doc.yBlocks;
}
/**
* Get the {@link AwarenessStore} instance for current store
*/
get awarenessStore() {
return this._doc.awarenessStore;
}
/**
* Get the di provider for current store.
*/
get provider() {
return this._provider;
}
/**
* Get the {@link BlobEngine} instance for current store.
*/
get blobSync() {
return this.workspace.blobSync;
}
/**
* Get the {@link Doc} instance for current store.
*/
get doc() {
return this._doc;
}
/**
* @internal
*/
get blocks() {
return this._blocks;
}
/**
* Get the number of blocks in the store
*
* @category Block CRUD
*/
get blockSize() {
return Object.values(this._blocks.peek()).length;
}
@@ -488,6 +462,17 @@ export class Store {
}
}
/**
* Creates and adds a new block to the store
* @param flavour - The block's flavour (type)
* @param blockProps - Optional properties for the new block
* @param parent - Optional parent block or parent block ID
* @param parentIndex - Optional index position in parent's children
* @returns The ID of the newly created block
* @throws {BlockSuiteError} When store is in readonly mode
*
* @category Block CRUD
*/
addBlock(
flavour: string,
blockProps: Partial<BlockProps & Omit<BlockProps, 'flavour'>> = {},
@@ -516,6 +501,15 @@ export class Store {
return id;
}
/**
* Add multiple blocks to the store
* @param blocks - Array of blocks to add
* @param parent - Optional parent block or parent block ID
* @param parentIndex - Optional index position in parent's children
* @returns Array of IDs of the newly created blocks
*
* @category Block CRUD
*/
addBlocks(
blocks: Array<{
flavour: string;
@@ -539,6 +533,15 @@ export class Store {
return ids;
}
/**
* Add sibling blocks to the store
* @param targetModel - The target block model
* @param props - Array of block properties
* @param place - Optional position to place the new blocks ('after' or 'before')
* @returns Array of IDs of the newly created blocks
*
* @category Block CRUD
*/
addSiblingBlocks(
targetModel: BlockModel,
props: Array<Partial<BlockProps>>,
@@ -576,6 +579,95 @@ export class Store {
return this.addBlocks(blocks, parent.id, insertIndex);
}
/**
* Updates a block's properties or executes a callback in a transaction
* @param modelOrId - The block model or block ID to update
* @param callBackOrProps - Either a callback function to execute or properties to update
* @throws {BlockSuiteError} When the block is not found or schema validation fails
*
* @category Block CRUD
*/
updateBlock: {
<T extends Partial<BlockProps>>(model: BlockModel | string, props: T): void;
(model: BlockModel | string, callback: () => void): void;
} = (
modelOrId: BlockModel | string,
callBackOrProps: (() => void) | Partial<BlockProps>
) => {
if (this.readonly) {
console.error('cannot modify data in readonly mode');
return;
}
const isCallback = typeof callBackOrProps === 'function';
const model =
typeof modelOrId === 'string'
? this.getBlock(modelOrId)?.model
: modelOrId;
if (!model) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
`updating block: ${modelOrId} not found`
);
}
if (!isCallback) {
const parent = this.getParent(model);
this.schema.validate(
model.flavour,
parent?.flavour,
callBackOrProps.children?.map(child => child.flavour)
);
}
const yBlock = this._yBlocks.get(model.id);
if (!yBlock) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
`updating block: ${model.id} not found`
);
}
const block = this.getBlock(model.id);
if (!block) return;
this.transact(() => {
if (isCallback) {
callBackOrProps();
this._runQuery(block);
return;
}
if (callBackOrProps.children) {
this._crud.updateBlockChildren(
model.id,
callBackOrProps.children.map(child => child.id)
);
}
const schema = this.schema.flavourSchemaMap.get(model.flavour);
if (!schema) {
throw new BlockSuiteError(
ErrorCode.ModelCRUDError,
`schema for flavour: ${model.flavour} not found`
);
}
syncBlockProps(schema, model, yBlock, callBackOrProps);
this._runQuery(block);
return;
});
};
/**
* Delete a block from the store
* @param model - The block model or block ID to delete
* @param options - Optional options for the deletion
* @param options.bringChildrenTo - Optional block model to bring children to
* @param options.deleteChildren - Optional flag to delete children
*
* @category Block CRUD
*/
deleteBlock(
model: BlockModel | string,
options: {
@@ -610,49 +702,49 @@ export class Store {
});
}
dispose() {
this._provider.getAll(StoreExtensionIdentifier).forEach(ext => {
ext.disposed();
});
this.slots.ready.complete();
this.slots.rootAdded.complete();
this.slots.rootDeleted.complete();
this.slots.blockUpdated.complete();
this.disposableGroup.dispose();
this._isDisposed = true;
}
/**
* Gets a block by its ID
* @param id - The block's ID
* @returns The block instance if found, undefined otherwise
*
* @category Block CRUD
*/
getBlock(id: string): Block | undefined {
return this._blocks.peek()[id];
}
/**
* Gets a block by its ID
* @param id - The block's ID
* @returns The block instance in signal if found, undefined otherwise
*
* @category Block CRUD
*/
getBlock$(id: string): Block | undefined {
return this._blocks.value[id];
}
/**
* @deprecated
* Use `getBlocksByFlavour` instead.
* Get a model by its ID
* @param id - The model's ID
* @returns The model instance if found, null otherwise
*
* @category Block CRUD
*/
getBlockByFlavour(blockFlavour: string | string[]) {
return this.getBlocksByFlavour(blockFlavour).map(x => x.model);
}
/**
* @deprecated
* Use `getBlock` instead.
*/
getBlockById<Model extends BlockModel = BlockModel>(
getModelById<Model extends BlockModel = BlockModel>(
id: string
): Model | null {
return (this.getBlock(id)?.model ?? null) as Model | null;
}
getStore() {
return Object.values(this._blocks.peek()).map(block => block.model);
}
getBlocksByFlavour(blockFlavour: string | string[]) {
/**
* Gets all blocks of specified flavour(s)
* @param blockFlavour - Single flavour or array of flavours to filter by
* @returns Array of matching blocks
*
* @category Block CRUD
*/
getBlocksByFlavour(blockFlavour: string | string[]): Block[] {
const flavours =
typeof blockFlavour === 'string' ? [blockFlavour] : blockFlavour;
@@ -661,21 +753,34 @@ export class Store {
);
}
getNext(block: BlockModel | string) {
return this._getSiblings(
block,
(parent, index) => parent.children[index + 1] ?? null
);
/**
* Get all models in the store
* @returns Array of all models
*
* @category Block CRUD
*/
getAllModels() {
return Object.values(this._blocks.peek()).map(block => block.model);
}
getNexts(block: BlockModel | string) {
return (
this._getSiblings(block, (parent, index) =>
parent.children.slice(index + 1)
) ?? []
);
/**
* Get all models of specified flavour(s)
* @param blockFlavour - Single flavour or array of flavours to filter by
* @returns Array of matching models
*
* @category Block CRUD
*/
getModelsByFlavour(blockFlavour: string | string[]): BlockModel[] {
return this.getBlocksByFlavour(blockFlavour).map(x => x.model);
}
/**
* Gets the parent block of a given block
* @param target - Block model or block ID to find parent for
* @returns The parent block model if found, null otherwise
*
* @category Block CRUD
*/
getParent(target: BlockModel | string): BlockModel | null {
const targetId = typeof target === 'string' ? target : target.id;
const parentId = this._crud.getParent(targetId);
@@ -687,6 +792,13 @@ export class Store {
return parent.model;
}
/**
* Get the previous sibling block of a given block
* @param block - Block model or block ID to find previous sibling for
* @returns The previous sibling block model if found, null otherwise
*
* @category Block CRUD
*/
getPrev(block: BlockModel | string) {
return this._getSiblings(
block,
@@ -694,6 +806,13 @@ export class Store {
);
}
/**
* Get all previous sibling blocks of a given block
* @param block - Block model or block ID to find previous siblings for
* @returns Array of previous sibling blocks if found, empty array otherwise
*
* @category Block CRUD
*/
getPrevs(block: BlockModel | string) {
return (
this._getSiblings(block, (parent, index) =>
@@ -702,38 +821,55 @@ export class Store {
);
}
getSchemaByFlavour(flavour: string) {
return this._schema.flavourSchemaMap.get(flavour);
/**
* Get the next sibling block of a given block
* @param block - Block model or block ID to find next sibling for
* @returns The next sibling block model if found, null otherwise
*
* @category Block CRUD
*/
getNext(block: BlockModel | string) {
return this._getSiblings(
block,
(parent, index) => parent.children[index + 1] ?? null
);
}
/**
* Get all next sibling blocks of a given block
* @param block - Block model or block ID to find next siblings for
* @returns Array of next sibling blocks if found, empty array otherwise
*
* @category Block CRUD
*/
getNexts(block: BlockModel | string) {
return (
this._getSiblings(block, (parent, index) =>
parent.children.slice(index + 1)
) ?? []
);
}
/**
* Check if a block exists by its ID
* @param id - The block's ID
* @returns True if the block exists, false otherwise
*
* @category Block CRUD
*/
hasBlock(id: string) {
return id in this._blocks.peek();
}
/**
* @deprecated
* Use `hasBlock` instead.
* Move blocks to a new parent block
* @param blocksToMove - Array of block models to move
* @param newParent - The new parent block model
* @param targetSibling - Optional target sibling block model
* @param shouldInsertBeforeSibling - Optional flag to insert before sibling
*
* @category Block CRUD
*/
hasBlockById(id: string) {
return this.hasBlock(id);
}
load(initFn?: () => void) {
if (this._isDisposed) {
this.disposableGroup = new DisposableGroup();
this._subscribeToSlots();
this._isDisposed = false;
}
this._doc.load(initFn);
this._provider.getAll(StoreExtensionIdentifier).forEach(ext => {
ext.loaded();
});
this.slots.ready.next();
this.slots.rootAdded.next(this.root?.id ?? '');
return this;
}
moveBlocks(
blocksToMove: BlockModel[],
newParent: BlockModel,
@@ -755,14 +891,13 @@ export class Store {
});
}
get get() {
return this.provider.get.bind(this.provider);
}
get getOptional() {
return this.provider.getOptional.bind(this.provider);
}
/**
* Creates a new transformer instance for the store
* @param middlewares - Optional array of transformer middlewares
* @returns A new Transformer instance
*
* @category Transformer
*/
getTransformer(middlewares: TransformerMiddleware[] = []) {
return new Transformer({
schema: this.schema,
@@ -775,4 +910,72 @@ export class Store {
middlewares,
});
}
/**
* Get an extension instance from the store
* @returns The extension instance
*
* @example
* ```ts
* const extension = store.get(SomeExtension);
* ```
*/
get get() {
return this.provider.get.bind(this.provider);
}
/**
* Optional get an extension instance from the store.
* The major difference between `get` and `getOptional` is that `getOptional` will not throw an error if the extension is not found.
*
* @returns The extension instance
*
* @example
* ```ts
* const extension = store.getOptional(SomeExtension);
* ```
*/
get getOptional() {
return this.provider.getOptional.bind(this.provider);
}
/**
* Initializes and loads the store
* @param initFn - Optional initialization function
* @returns The store instance
*
* @category Store Lifecycle
*/
load(initFn?: () => void) {
if (this._isDisposed) {
this.disposableGroup = new DisposableGroup();
this._subscribeToSlots();
this._isDisposed = false;
}
this._doc.load(initFn);
this._provider.getAll(StoreExtensionIdentifier).forEach(ext => {
ext.loaded();
});
this.slots.ready.next();
this.slots.rootAdded.next(this.root?.id ?? '');
return this;
}
/**
* Disposes the store and releases all resources
*
* @category Store Lifecycle
*/
dispose() {
this._provider.getAll(StoreExtensionIdentifier).forEach(ext => {
ext.disposed();
});
this.slots.ready.complete();
this.slots.rootAdded.complete();
this.slots.rootDeleted.complete();
this.slots.blockUpdated.complete();
this.disposableGroup.dispose();
this._isDisposed = true;
}
}

View File

@@ -293,7 +293,7 @@ export class Transformer {
}
const contentBlocks = blockTree.children
.map(tree => doc.getBlockById(tree.draft.id))
.map(tree => doc.getModelById(tree.draft.id))
.filter((x): x is BlockModel => x !== null)
.map(model => toDraftModel(model));

View File

@@ -14,7 +14,9 @@ let model: SurfaceBlockModel;
beforeEach(async () => {
const cleanup = await setupEditor('edgeless');
const models = doc.getBlockByFlavour('affine:surface') as SurfaceBlockModel[];
const models = doc.getModelsByFlavour(
'affine:surface'
) as SurfaceBlockModel[];
model = models[0];

View File

@@ -51,7 +51,7 @@ const snapshotTest = async (snapshotUrl: string, elementsCount: number) => {
editor.doc = newDoc;
await wait();
const surface = newDoc.getBlockByFlavour(
const surface = newDoc.getModelsByFlavour(
'affine:surface'
)[0] as SurfaceBlockModel;
const surfaceElements = [...surface['_elementModels']].map(

View File

@@ -8,7 +8,7 @@ import type { Store } from '@blocksuite/store';
import type { TestAffineEditorContainer } from '../../index.js';
export function getSurface(doc: Store, editor: TestAffineEditorContainer) {
const surfaceModel = doc.getBlockByFlavour('affine:surface');
const surfaceModel = doc.getModelsByFlavour('affine:surface');
return editor.host!.view.getBlock(
surfaceModel[0]!.id

View File

@@ -489,7 +489,7 @@ export class StarterDebugMenu extends ShadowlessElement {
);
for (const doc of docs) {
if (doc) {
const noteBlock = window.doc.getBlockByFlavour('affine:note');
const noteBlock = window.doc.getModelsByFlavour('affine:note');
window.doc.addBlock(
'affine:paragraph',
{

View File

@@ -27,7 +27,7 @@ export const database: InitFn = (collection: Workspace, id: string) => {
// Add note block inside root block
const noteId = doc.addBlock('affine:note', {}, rootId);
const pId = doc.addBlock('affine:paragraph', {}, noteId);
const model = doc.getBlockById(pId);
const model = doc.getModelById(pId);
if (!model) {
throw new Error('model is not found');
}
@@ -40,7 +40,7 @@ export const database: InitFn = (collection: Workspace, id: string) => {
},
noteId
);
const database = doc.getBlockById(databaseId) as DatabaseBlockModel;
const database = doc.getModelById(databaseId) as DatabaseBlockModel;
const datasource = new DatabaseBlockDataSource(database);
datasource.viewManager.viewAdd('table');
database.props.title = new Text(title);