docs(editor): improve documentation for store class (#10949)

This commit is contained in:
Saul-Mirone
2025-03-18 07:57:58 +00:00
parent 99370573c8
commit ef00a158fc
10 changed files with 894 additions and 313 deletions

View File

@@ -51,7 +51,7 @@ export function updateXYWH(
bound.w = clamp(bound.w, NOTE_MIN_WIDTH * scale, Infinity);
bound.h = clamp(bound.h, NOTE_MIN_HEIGHT * scale, Infinity);
if (bound.h >= NOTE_MIN_HEIGHT * scale) {
updateBlock(ele, () => {
updateBlock.call(ele.doc, ele, () => {
ele.props.edgeless.collapse = true;
ele.props.edgeless.collapsedHeight = bound.h / scale;
});

View File

@@ -14,3 +14,4 @@
## Store
- [Store](classes/Store.md)
- [StoreSlots](interfaces/StoreSlots.md)

View File

@@ -16,60 +16,6 @@ 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
@@ -84,6 +30,92 @@ Get the number of blocks in the store
***
### isEmpty
#### Get Signature
> **get** **isEmpty**(): `boolean`
Check if there are no blocks in the store.
##### Returns
`boolean`
***
### isEmpty$
#### Get Signature
> **get** **isEmpty$**(): `ReadonlySignal`\<`boolean`\>
Get the signal for the empty state of the store.
##### Returns
`ReadonlySignal`\<`boolean`\>
***
### readonly
#### Get Signature
> **get** **readonly**(): `boolean`
Check if the store is readonly.
##### Returns
`boolean`
#### Set Signature
> **set** **readonly**(`value`): `void`
Set the readonly state of the store.
##### Parameters
###### value
`boolean`
##### Returns
`void`
***
### readonly$
#### Get Signature
> **get** **readonly$**(): `Signal`\<`boolean`\>
Get the signal for the readonly state of the store.
##### Returns
`Signal`\<`boolean`\>
***
### root
#### Get Signature
> **get** **root**(): `null` \| `BlockModel`\<`object`\>
Get the root block of the store.
##### Returns
`null` \| `BlockModel`\<`object`\>
***
### addBlock()
> **addBlock**(`flavour`, `blockProps`, `parent`?, `parentIndex`?): `string`
@@ -530,6 +562,331 @@ Optional flag to insert before sibling
`void`
***
### updateBlock()
> **updateBlock**(`modelOrId`, `callBackOrProps`): `void`
Updates a block's properties or executes a callback in a transaction
#### Parameters
##### modelOrId
The block model or block ID to update
`string` | `BlockModel`\<`object`\>
##### callBackOrProps
Either a callback function to execute or properties to update
`Partial`\<`BlockProps`\> | () => `void`
#### Returns
`void`
#### Throws
When the block is not found or schema validation fails
## Extension
### 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`
## History
### canRedo
#### Get Signature
> **get** **canRedo**(): `boolean`
Check if the store can redo
##### Returns
`boolean`
***
### canUndo
#### Get Signature
> **get** **canUndo**(): `boolean`
Check if the store can undo
##### Returns
`boolean`
***
### captureSync
#### Get Signature
> **get** **captureSync**(): () => `void`
Force the following history to be captured into a new stack.
##### Example
```ts
op1();
op2();
store.captureSync();
op3();
store.undo(); // undo op3
store.undo(); // undo op1, op2
```
##### Returns
`Function`
###### Returns
`void`
***
### history
#### Get Signature
> **get** **history**(): `UndoManager`
Get the Y.UndoManager instance for current store.
##### Returns
`UndoManager`
***
### redo
#### Get Signature
> **get** **redo**(): () => `void`
Redo the last undone transaction.
##### Returns
`Function`
###### Returns
`void`
***
### resetHistory
#### Get Signature
> **get** **resetHistory**(): () => `void`
Reset the history of the store.
##### Returns
`Function`
###### Returns
`void`
***
### transact
#### Get Signature
> **get** **transact**(): (`fn`, `shouldTransact`?) => `void`
Execute a transaction.
##### Example
```ts
store.transact(() => {
op1();
op2();
});
```
##### Returns
`Function`
###### Parameters
###### fn
() => `void`
###### shouldTransact?
`boolean`
###### Returns
`void`
***
### undo
#### Get Signature
> **get** **undo**(): () => `void`
Undo the last transaction.
##### Returns
`Function`
###### Returns
`void`
***
### withoutTransact
#### Get Signature
> **get** **withoutTransact**(): (`fn`) => `void`
Execute a transaction without capturing the history.
##### Example
```ts
store.withoutTransact(() => {
op1();
op2();
});
```
##### Returns
`Function`
###### Parameters
###### fn
() => `void`
###### Returns
`void`
## Store Lifecycle
### disposableGroup
@@ -542,51 +899,61 @@ Group of disposable resources managed by the store
### slots
> `readonly` **slots**: `object` & `object`
> `readonly` **slots**: [`StoreSlots`](../interfaces/StoreSlots.md)
Slots for receiving events from the store.
All events are rxjs Subjects, you can subscribe to them like this:
#### Type declaration
```ts
store.slots.ready.subscribe(() => {
console.log('store is ready');
});
```
##### historyUpdated
You can also use rxjs operators to handle the events.
> **historyUpdated**: `Subject`\<`void`\>
***
This fires when the doc history is updated.
### id
##### yBlockUpdated
#### Get Signature
> **yBlockUpdated**: `Subject`\<\{ `id`: `string`; `isLocal`: `boolean`; `type`: `"add"`; \} \| \{ `id`: `string`; `isLocal`: `boolean`; `type`: `"delete"`; \}\>
> **get** **id**(): `string`
This fires when the doc yBlock is updated.
Get the id of the store.
#### Type declaration
##### Returns
##### blockUpdated
`string`
> **blockUpdated**: `Subject`\<`BlockUpdatedPayload`\>
***
This fires when a block is updated via API call or has just been updated from existing ydoc.
### loaded
##### ready
#### Get Signature
> **ready**: `Subject`\<`void`\>
> **get** **loaded**(): `boolean`
This is always triggered after `doc.load` is called.
Check if the store is loaded.
##### rootAdded
##### Returns
> **rootAdded**: `Subject`\<`string`\>
`boolean`
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
### ready
> **rootDeleted**: `Subject`\<`string`\>
#### Get Signature
This fires when the root block is deleted via API call or has just been removed from existing ydoc.
> **get** **ready**(): `boolean`
Check if the store is ready.
Which means the Y.Doc is loaded and the root block is added.
##### Returns
`boolean`
***
@@ -688,99 +1055,28 @@ Get the Doc instance for current store.
***
### get
### schema
#### Get Signature
> **get** **get**(): \<`T`\>(`identifier`, `options`?) => `T`
> **get** **schema**(): `Schema`
Get an extension instance from the store
##### Example
```ts
const extension = store.get(SomeExtension);
```
Get the Schema instance of the store.
##### Returns
`Function`
The extension instance
###### Type Parameters
###### T
`T`
###### Parameters
###### identifier
`GeneralServiceIdentifier`\<`T`\>
###### options?
`ResolveOptions`
###### Returns
`T`
`Schema`
***
### getOptional
### workspace
#### Get Signature
> **get** **getOptional**(): \<`T`\>(`identifier`, `options`?) => `null` \| `T`
> **get** **workspace**(): `Workspace`
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);
```
Get the Workspace instance for current store.
##### 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`
`Workspace`

View File

@@ -0,0 +1,60 @@
[**@blocksuite/store**](../../../@blocksuite/store/README.md)
***
[BlockSuite API Documentation](../../../README.md) / [@blocksuite/store](../README.md) / StoreSlots
# Interface: StoreSlots
Slots for receiving events from the store.
All events are rxjs Subjects, you can subscribe to them like this:
```ts
store.slots.ready.subscribe(() => {
console.log('store is ready');
});
```
You can also use rxjs operators to handle the events.
## Properties
### blockUpdated
> **blockUpdated**: `Subject`\<`StoreBlockUpdatedPayloads`\>
***
### historyUpdated
> **historyUpdated**: `Subject`\<`void`\>
This fires when the doc history is updated.
***
### ready
> **ready**: `Subject`\<`void`\>
This fires after `doc.load` is called.
The Y.Doc is fully loaded and ready to use.
***
### 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.
In most cases, you don't need to subscribe to this event.

View File

@@ -29,20 +29,14 @@ export interface Doc {
*/
historyUpdated: Subject<void>;
/**
* @internal
* This fires when the doc yBlock is updated.
*/
yBlockUpdated: Subject<
| {
type: 'add';
id: string;
isLocal: boolean;
}
| {
type: 'delete';
id: string;
isLocal: boolean;
}
>;
yBlockUpdated: Subject<{
type: 'add' | 'delete';
id: string;
isLocal: boolean;
}>;
};
get history(): Y.UndoManager;

View File

@@ -34,30 +34,135 @@ export type StoreOptions = {
extensions?: ExtensionType[];
};
export type BlockUpdatedPayload =
| {
type: 'add';
id: string;
isLocal: boolean;
init: boolean;
flavour: string;
model: BlockModel;
}
| {
type: 'delete';
id: string;
isLocal: boolean;
flavour: string;
parent: string;
model: BlockModel;
}
| {
type: 'update';
id: string;
isLocal: boolean;
flavour: string;
props: { key: string };
};
type StoreBlockAddedPayload = {
/**
* The type of the event.
*/
type: 'add';
/**
* The id of the block.
*/
id: string;
/**
* Whether the event is triggered by local changes.
*/
isLocal: boolean;
/**
* The flavour of the block.
*/
flavour: string;
/**
* The model of the block.
*/
model: BlockModel;
/**
* @internal
* Whether the event is triggered by initialization.
* FIXME: This seems not working as expected now.
*/
init: boolean;
};
type StoreBlockDeletedPayload = {
/**
* The type of the event.
*/
type: 'delete';
/**
* The id of the block.
*/
id: string;
/**
* Whether the event is triggered by local changes.
*/
isLocal: boolean;
/**
* The flavour of the block.
*/
flavour: string;
/**
* The parent id of the block.
*/
parent: string;
/**
* The model of the block.
*/
model: BlockModel;
};
type StoreBlockUpdatedPayload = {
/**
* The type of the event.
*/
type: 'update';
/**
* The id of the block.
*/
id: string;
/**
* Whether the event is triggered by local changes.
*/
isLocal: boolean;
/**
* The flavour of the block.
*/
flavour: string;
/**
* The changed props of the block.
*/
props: { key: string };
};
type StoreBlockUpdatedPayloads =
| StoreBlockAddedPayload
| StoreBlockDeletedPayload
| StoreBlockUpdatedPayload;
/**
* Slots for receiving events from the store.
* All events are rxjs Subjects, you can subscribe to them like this:
*
* ```ts
* store.slots.ready.subscribe(() => {
* console.log('store is ready');
* });
* ```
*
* You can also use rxjs operators to handle the events.
*
* @interface
* @category Store
*/
export type StoreSlots = Doc['slots'] & {
/**
* This fires after `doc.load` is called.
* The Y.Doc is fully loaded and ready to use.
*/
ready: Subject<void>;
/**
* 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.
*/
rootAdded: Subject<string>;
/**
* This fires when the root block is deleted via API call or has just been removed from existing ydoc.
* In most cases, you don't need to subscribe to this event.
*/
rootDeleted: Subject<string>;
/**
*
* @summary
* This fires when a block is updated via API call or has just been updated from existing ydoc.
*
* The payload can have three types:
* - add: When a new block is added
* - delete: When a block is removed
* - update: When a block's properties are modified
*
*/
blockUpdated: Subject<StoreBlockUpdatedPayloads>;
};
const internalExtensions = [StoreSelectionExtension];
@@ -107,28 +212,20 @@ export class Store {
private readonly _schema: Schema;
/**
* Slots for receiving events from the store.
* Get the id of the store.
*
* @category Store Lifecycle
*/
readonly slots: Doc['slots'] & {
/** This is always triggered after `doc.load` is called. */
ready: Subject<void>;
/**
* 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.
*/
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>;
};
get id() {
return this._doc.id;
}
/**
* {@inheritDoc StoreSlots}
*
* @category Store Lifecycle
*/
readonly slots: StoreSlots;
private get _yBlocks() {
return this._doc.yBlocks;
@@ -143,6 +240,8 @@ export class Store {
/**
* Get the di provider for current store.
*
* @category Extension
*/
get provider() {
return this._provider;
@@ -178,6 +277,11 @@ export class Store {
return Object.values(this._blocks.peek()).length;
}
/**
* Check if the store can redo
*
* @category History
*/
get canRedo() {
if (this.readonly) {
return false;
@@ -185,6 +289,11 @@ export class Store {
return this._doc.canRedo;
}
/**
* Check if the store can undo
*
* @category History
*/
get canUndo() {
if (this.readonly) {
return false;
@@ -192,93 +301,11 @@ export class Store {
return this._doc.canUndo;
}
get captureSync() {
return this._doc.captureSync.bind(this._doc);
}
get clear() {
return this._doc.clear.bind(this._doc);
}
get workspace() {
return this._doc.workspace;
}
get history() {
return this._doc.history;
}
get id() {
return this._doc.id;
}
get isEmpty() {
return this._isEmpty.peek();
}
get isEmpty$() {
return this._isEmpty;
}
get loaded() {
return this._doc.loaded;
}
get meta() {
return this._doc.meta;
}
get readonly$() {
return this._readonly;
}
get readonly() {
return this._readonly.value === true;
}
set readonly(value: boolean) {
this._readonly.value = value;
}
get ready() {
return this._doc.ready;
}
get redo() {
if (this.readonly) {
return () => {
console.error('cannot undo in readonly mode');
};
}
return this._doc.redo.bind(this._doc);
}
get resetHistory() {
return this._doc.resetHistory.bind(this._doc);
}
get root() {
const rootId = this._crud.root;
if (!rootId) return null;
return this.getBlock(rootId)?.model ?? null;
}
get rootDoc() {
return this._doc.rootDoc;
}
get schema() {
return this._schema;
}
get spaceDoc() {
return this._doc.spaceDoc;
}
get transact() {
return this._doc.transact.bind(this._doc);
}
/**
* Undo the last transaction.
*
* @category History
*/
get undo() {
if (this.readonly) {
return () => {
@@ -288,12 +315,214 @@ export class Store {
return this._doc.undo.bind(this._doc);
}
/**
* Redo the last undone transaction.
*
* @category History
*/
get redo() {
if (this.readonly) {
return () => {
console.error('cannot undo in readonly mode');
};
}
return this._doc.redo.bind(this._doc);
}
/**
* Reset the history of the store.
*
* @category History
*/
get resetHistory() {
return this._doc.resetHistory.bind(this._doc);
}
/**
* Execute a transaction.
*
* @example
* ```ts
* store.transact(() => {
* op1();
* op2();
* });
* ```
*
* @category History
*/
get transact() {
return this._doc.transact.bind(this._doc);
}
/**
* Execute a transaction without capturing the history.
*
* @example
* ```ts
* store.withoutTransact(() => {
* op1();
* op2();
* });
* ```
*
* @category History
*/
get withoutTransact() {
return this._doc.withoutTransact.bind(this._doc);
}
/**
* Force the following history to be captured into a new stack.
*
* @example
* ```ts
* op1();
* op2();
* store.captureSync();
* op3();
*
* store.undo(); // undo op3
* store.undo(); // undo op1, op2
* ```
*
* @category History
*/
get captureSync() {
return this._doc.captureSync.bind(this._doc);
}
/**
* Get the {@link Workspace} instance for current store.
*/
get workspace() {
return this._doc.workspace;
}
/**
* Get the {@link Y.UndoManager} instance for current store.
*
* @category History
*/
get history() {
return this._doc.history;
}
/**
* Check if there are no blocks in the store.
*
* @category Block CRUD
*/
get isEmpty() {
return this._isEmpty.peek();
}
/**
* Get the signal for the empty state of the store.
*
* @category Block CRUD
*/
get isEmpty$() {
return this._isEmpty;
}
/**
* Check if the store is loaded.
*
* @category Store Lifecycle
*/
get loaded() {
return this._doc.loaded;
}
/**
* Get the meta data of the store.
*
* @internal
*/
get meta() {
return this._doc.meta;
}
/**
* Check if the store is readonly.
*
* @category Block CRUD
*/
get readonly() {
return this._readonly.value === true;
}
/**
* Get the signal for the readonly state of the store.
*
* @category Block CRUD
*/
get readonly$() {
return this._readonly;
}
/**
* Set the readonly state of the store.
*
* @category Block CRUD
*/
set readonly(value: boolean) {
this._readonly.value = value;
}
/**
* Check if the store is ready.
* Which means the Y.Doc is loaded and the root block is added.
*
* @category Store Lifecycle
*/
get ready() {
return this._doc.ready;
}
/**
* Get the root block of the store.
*
* @category Block CRUD
*/
get root() {
const rootId = this._crud.root;
if (!rootId) return null;
return this.getBlock(rootId)?.model ?? null;
}
/**
* @internal
* Get the root Y.Doc of sub Y.Doc.
* In the current design, store is on a sub Y.Doc, and all sub docs have the same root Y.Doc.
*/
get rootDoc() {
return this._doc.rootDoc;
}
/**
* Get the {@link Schema} instance of the store.
*/
get schema() {
return this._schema;
}
/**
* @internal
* Get the Y.Doc instance of the store.
*/
get spaceDoc() {
return this._doc.spaceDoc;
}
private _isDisposed = false;
/**
* @internal
* In most cases, you don't need to use the constructor directly.
* The store is created by the {@link Doc} instance.
*/
constructor({ doc, readonly, query, provider, extensions }: StoreOptions) {
this._doc = doc;
this.slots = {
@@ -587,13 +816,10 @@ export class Store {
*
* @category Block CRUD
*/
updateBlock: {
<T extends Partial<BlockProps>>(model: BlockModel | string, props: T): void;
(model: BlockModel | string, callback: () => void): void;
} = (
updateBlock(
modelOrId: BlockModel | string,
callBackOrProps: (() => void) | Partial<BlockProps>
) => {
) {
if (this.readonly) {
console.error('cannot modify data in readonly mode');
return;
@@ -657,7 +883,7 @@ export class Store {
this._runQuery(block);
return;
});
};
}
/**
* Delete a block from the store
@@ -919,6 +1145,8 @@ export class Store {
* ```ts
* const extension = store.get(SomeExtension);
* ```
*
* @category Extension
*/
get get() {
return this.provider.get.bind(this.provider);
@@ -934,6 +1162,8 @@ export class Store {
* ```ts
* const extension = store.getOptional(SomeExtension);
* ```
*
* @category Extension
*/
get getOptional() {
return this.provider.getOptional.bind(this.provider);

View File

@@ -4,7 +4,7 @@ import type { InitFn } from './utils.js';
export const embed: InitFn = (collection: Workspace, id: string) => {
const doc = collection.getDoc(id) ?? collection.createDoc({ id });
doc.clear();
doc.doc.clear();
doc.load(() => {
// Add root block and surface block at root level

View File

@@ -4,7 +4,7 @@ import type { InitFn } from './utils.js';
export const empty: InitFn = (collection: Workspace, id: string) => {
const doc = collection.getDoc(id) ?? collection.createDoc({ id });
doc.clear();
doc.doc.clear();
doc.load(() => {
// Add root block and surface block at root level

View File

@@ -11,9 +11,9 @@ export const linked: InitFn = (collection: Workspace, id: string) => {
const docCId = 'doc:linked-edgeless';
const docC = collection.createDoc({ id: docCId });
docA.clear();
docB.clear();
docC.clear();
docA.doc.clear();
docB.doc.clear();
docC.doc.clear();
docB.load(() => {
const rootId = docB.addBlock('affine:page', {

View File

@@ -20,9 +20,9 @@ export const synced: InitFn = (collection: Workspace, id: string) => {
const docSyncedEdgelessId = 'doc:synced-edgeless';
const docSyncedEdgeless = collection.createDoc({ id: docSyncedEdgelessId });
docMain.clear();
docSyncedPage.clear();
docSyncedEdgeless.clear();
docMain.doc.clear();
docSyncedPage.doc.clear();
docSyncedEdgeless.doc.clear();
docSyncedPage.load(() => {
// Add root block and surface block at root level