diff --git a/README.md b/README.md index b4d604b97142..daf9d96a5149 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,8 @@ This can be illustrated as the diagram below: In addition to extending custom blocks, here are what you can also conveniently achieve with BlockSuite: - Writing type-safe complex editing logic based on the [command](https://blocksuite.io/command.html) mechanism, similar to react hooks designed for document editing. -- Persistence of documents and compatibility with various third-party formats (such as markdown and HTML) based on block [snapshot](https://blocksuite.io/data-persistence.html#snapshot-api) and transformer. -- Incremental updates, real-time collaboration, local-first state management, and even decentralized data synchronization based on the [provider](https://blocksuite.io/data-persistence.html#provider-based-state-management) mechanism of the document. +- Persistence of documents and compatibility with various third-party formats (such as markdown and HTML) based on block [snapshot](https://blocksuite.io/data-synchronization.html#snapshot-api) and transformer. +- Incremental updates, real-time collaboration, local-first state management, and even decentralized data synchronization based on the [provider](https://blocksuite.io/data-synchronization.html#provider-based-state-management) mechanism of the document. - State scheduling across multiple documents and reusing one document in multiple editors. > 🚧 BlockSuite is currently in its early stage, with some extension capabilities still under refinement. Hope you can stay tuned, try it out, or share your feedback! diff --git a/packages/docs/.vitepress/config.ts b/packages/docs/.vitepress/config.ts index 4c1029ae1a0b..4a3c1ad83357 100644 --- a/packages/docs/.vitepress/config.ts +++ b/packages/docs/.vitepress/config.ts @@ -70,7 +70,7 @@ export default defineConfig({ }, ], }, - { text: 'Data Persistence', link: '/data-persistence' }, + { text: 'Data Synchronization', link: '/data-synchronization' }, ], }, { diff --git a/packages/docs/adapter.md b/packages/docs/adapter.md index 75e6186899cc..cc4b4fc3b48a 100644 --- a/packages/docs/adapter.md +++ b/packages/docs/adapter.md @@ -1,6 +1,6 @@ # Adapter -Adapter works as a bridge between different formats of data and the BlockSuite [`Snapshot`](./data-persistence#snapshot-api) (i.e., the JSON-serialized block tree). It enables you to import and export data from and to BlockSuite documents. +Adapter works as a bridge between different formats of data and the BlockSuite [`Snapshot`](./data-synchronization#snapshot-api) (i.e., the JSON-serialized block tree). It enables you to import and export data from and to BlockSuite documents. ## Base Adapter diff --git a/packages/docs/block-schema.md b/packages/docs/block-schema.md index f2658edbb109..25e2ff0a362a 100644 --- a/packages/docs/block-schema.md +++ b/packages/docs/block-schema.md @@ -1,9 +1,5 @@ # Block Schema -::: info -This document is preliminary and subject to refinement and updates for clarity and accuracy. -::: - In BlockSuite, all blocks should have a schema. The schema of the block describes the data structure of the block. You can use the `defineBlockSchema` function to define the schema of the block. diff --git a/packages/docs/block-service.md b/packages/docs/block-service.md index f5d0099825e2..9a8bda882e1f 100644 --- a/packages/docs/block-service.md +++ b/packages/docs/block-service.md @@ -1,9 +1,5 @@ # Block Service -::: info -This document is preliminary and subject to refinement and updates for clarity and accuracy. -::: - Each kind of block can register its own service, so as to define block-specific methods to be called during the editor lifecycle. The service is a class that extends the `BlockService` class: ```ts diff --git a/packages/docs/block-spec.md b/packages/docs/block-spec.md index fc280c6cbb11..ca42ef4de7ca 100644 --- a/packages/docs/block-spec.md +++ b/packages/docs/block-spec.md @@ -1,9 +1,5 @@ # Block Spec -::: info -This document is preliminary and subject to refinement and updates for clarity and accuracy. -::: - In BlockSuite, a `BlockSpec` defines the structure and interactive elements for a specific block type within the editor. BlockSuite editors are typically composed entirely of block specs, with the top-level UI often implemented as a dedicated block, usually of the `affine:page` type. A block spec contains the following properties: diff --git a/packages/docs/block-view.md b/packages/docs/block-view.md index 0290ca16b136..92e39b0da61c 100644 --- a/packages/docs/block-view.md +++ b/packages/docs/block-view.md @@ -1,9 +1,5 @@ # Block View -::: info -This document is preliminary and subject to refinement and updates for clarity and accuracy. -::: - In BlockSuite, blocks can be rendered by any UI framework. A block should be rendered to a DOM element, and we use `view` to represent the renderer. By default, we provide a [lit](https://lit.dev/) renderer called `@blocksuite/lit`. But it's still possible to use other UI frameworks. We'll introduce later about how to write custom block renderers. diff --git a/packages/docs/block-widgets.md b/packages/docs/block-widgets.md index 79ff220fa8d1..4eb61a7ce5fb 100644 --- a/packages/docs/block-widgets.md +++ b/packages/docs/block-widgets.md @@ -1,9 +1,5 @@ # Block Widgets -::: info -This document is preliminary and subject to refinement and updates for clarity and accuracy. -::: - In BlockSuite, widgets are components that can be used to display helper UI elements of a block. Sometimes, you want to display a menu to provide some extra information or actions for a block. As another example, it's a common practice to display a toolbar when you select a block. The widget is designed to provide this kind of functionalities. Similar to blocks, widgets also depends on UI frameworks. By default, we provide a [lit](https://lit.dev/) renderer called `@blocksuite/lit`. But it's still possible to use other UI frameworks. We'll introduce later about how to write custom block renderers. diff --git a/packages/docs/command.md b/packages/docs/command.md index 5b5f5eb709a5..47e4318fdca3 100644 --- a/packages/docs/command.md +++ b/packages/docs/command.md @@ -1,9 +1,5 @@ # Command -::: info -This document is preliminary and subject to refinement and updates for clarity and accuracy. -::: - Commands are the reusable actions for triggering state updates. Inside a command, you can query different states of the editor, or perform operations to update them. With the command API, you can define chainable commands and execute them. ## Command Chain diff --git a/packages/docs/data-persistence.md b/packages/docs/data-persistence.md deleted file mode 100644 index d1b47083ef6d..000000000000 --- a/packages/docs/data-persistence.md +++ /dev/null @@ -1,115 +0,0 @@ -# Data Persistence - -No matter the application is collaborative of not, BlockSuite offers flexible solutions for data persistence. This guide explores optimal ways to save and load documents in BlockSuite. - -## Snapshot API - -Traditionally, you make expect an `editor.load()` API that supports JSON-based serialized format. In this case, BlockSuite has the JSON snapshot format as a simple fit. - -```ts -import { Job } from '@blocksuite/store'; - -const { workspace } = page; - -// A job is required for performing the tasks -const job = new Job({ workspace }); - -// Export current page content to snapshot JSON -const json = await job.pageToSnapshot(page); - -// Import snapshot JSON to a new page -const newPage = await job.snapshotToPage(json); -``` - -BlockSuite also designs the [`Adapter`](./adapter) API based on snapshot, which handles the conversion between third-party formats like markdown or HTML, It allows for adaptive transformations of the block tree. - -::: tip -In BlockSuite [playgroud](https://try-blocksuite.vercel.app/starter/?init), You can try the "Import/Export Snapshot" feature inside the "Test Operations" menu entry. You can also use the `job` variable in browser console for quick testing. -::: - -## Provider-Based State Management - -When it comes to applications requiring real-time collaborative features, BlockSuite recommends the provider-based approach, which could be summarized as **simply connecting the document to providers, (.e.g, `WebSocketProvider`), right from the initialization of the documents**. - -This ensures that **all updates within the editor's lifecycle are encoded as binary patches and distributed via the provider**. This is not only efficient but also ensures real-time, incremental synchronization of document states, offering both simple mindset and best performance for collaborative editors. - -![pluggable-providers](./images/pluggable-providers.png) - -```ts -// IndexedDB provider from yjs community -import { IndexeddbPersistence } from 'y-indexeddb'; - -// `page.spaceDoc` is the underlying CRDT data structure. -// Here we connect the doc to the IndexedDB table named 'my-doc' -const provider = new IndexeddbPersistence('my-doc', page.spaceDoc); -``` - -## Block Tree Initialization Basics - -By default, a newly created `page` has no blocks inside. Here we clarify different ways to initialize the block tree for a `page`. - -### Creating Page from Snapshot - -When working without a provider, it's recommended to directly use the snapshot API for importing existing documents: - -```ts -const job = new Job({ workspace }); - -// Import snapshot JSON to a new page -const newPage = await job.snapshotToPage(json); -``` - -### Creating New Block Tree - -You can also use the `page.load(initCallbak)` API to programmatically construct the initial block tree. Since the "default empty state" of different BlockSuite editors may differ, it's up to editors to decide the initial block structure. This example creates a block tree for `DocEditor`: - -```ts -page.load(() => { - const rootId = page.addBlock('affine:page'); - page.addBlock('affine:surface', {}, rootId); - page.addBlock('affine:note', {}, rootId); -}); -``` - -This is exactly how the `createEmptyPage().init()` helper works under the hood ([source](https://github.com/toeverything/blocksuite/blob/master/packages/presets/src/helpers/index.ts)): - -```ts -import { AffineSchemas } from '@blocksuite/blocks/models'; -import { Schema, Workspace } from '@blocksuite/store'; - -export function createEmptyPage() { - const schema = new Schema().register(AffineSchemas); - const workspace = new Workspace({ schema }); - const page = workspace.createPage(); - - return { - page, - async init() { - await page.load(() => { - const pageBlockId = page.addBlock('affine:page', {}); - page.addBlock('affine:surface', {}, pageBlockId); - const noteId = page.addBlock('affine:note', {}, pageBlockId); - page.addBlock('affine:paragraph', {}, noteId); - }); - return page; - }, - }; -} -``` - -### Loading from Provider - -When you are using BlockSuite with providers, the application logic should distinguish between **creating** a new document and **loading** an existing one. - -Here is the rule of thumb for loading documents in a provider-based application: - -- For creating new documents, simply use `page.load(initCallback)`. -- For loading existing documents, wait by `await page.load()` after the page is connected to providers. - -```ts -// If you are opening an existing page that is connected to the provider, -// the block tree should be ready right after this line. -await page.load(); -``` - -In both cases, after the block tree is **loaded** or **created**, the [`page.slots.ready`](/api/@blocksuite/store/classes/Page.html#ready-1) slot will be triggered, indicating the completion of block tree initialization. diff --git a/packages/docs/data-synchronization.md b/packages/docs/data-synchronization.md new file mode 100644 index 000000000000..2dd9fa0f60e6 --- /dev/null +++ b/packages/docs/data-synchronization.md @@ -0,0 +1,86 @@ +# Data Synchronization + +::: info +🌐 This documentation has a [Chinese translation](https://insider.affine.pro/share/af3478a2-9c9c-4d16-864d-bffa1eb10eb6/xiObHbAC0yUb7HmX4-fjg). +::: + +This guide explores several optimal ways to synchronize (in other words, save and load) documents in BlockSuite. + +## Snapshot API + +Traditionally, you might expect a JSON-based API that works somewhat like `editor.load()`. For such scenarios, BlockSuite indeed conveniently fulfills this need through its built-in snapshot mechanism: + +```ts +import { Job } from '@blocksuite/store'; + +const { workspace } = page; + +// A job is required for performing the tasks +const job = new Job({ workspace }); + +// Export current page content to snapshot JSON +const json = await job.pageToSnapshot(page); + +// Import snapshot JSON to a new page +const newPage = await job.snapshotToPage(json); +``` + +The snapshot stores the JSON representation of the `page` block tree, preserving its nested structure. Additionally, BlockSuite has designed an [Adapter](./adapter) API on top of the snapshot to handle conversions between the block tree and third-party formats like markdown and HTML. + +## Provider-Based State Management + +Different from the classic mechanism above, BlockSuite natively supports a state management strategy that can be mentally paralleled with [React Server Component](https://www.joshwcomeau.com/react/server-components/). This allows the state of the block tree to be directly used as serializable data, streaming from the server (or local database) to the client. + +In this case, **the document data stored on the server is no longer JSON, but always a binary representation of CRDT** (similar to protobuf or RSC payload). As the block tree in BlockSuite is natively implemented by CRDT, and the CRDT data is always updated first during state updates ([see this article](./crdt-native-data-flow)), the block tree state in the BlockSuite editor is always driven entirely by CRDT data. Therefore, compared to the RSC mindset: + +``` +ui = f(data)(state) +``` + +The BlockSuite mindset is always: + +``` +ui = f(data) +``` + +This is equivalent to updating the server first every time you update a todo list item, and then updating the state with the data returned from the server. However, with the ability of CRDT that automatically resolves conflicts, this process can be reliably completed locally and synchronized with remote documents. + +In contrast, traditional editors typically only support APIs like `editor.load()`, which is more similar to a compromised `f(data)(state)` model, and has more complexity when dealing with real-time collaboration with multiple data sources. + +In BlockSuite, the data-driven synchronization strategy is implemented through providers: + +- When creating a new document, you only need to connect the `page` to a specific provider (or multiple providers) to expect the CRDT data of the block tree to be synchronized via these providers. +- Similarly, when loading an existing document, the method is to create a new empty `page` object and connect it to the corresponding provider. At this time, the block tree data will also flow in from the provider data source: + +```ts +// IndexedDB provider from Yjs community +import { IndexeddbPersistence } from 'y-indexeddb'; + +// Let's start from an empty page +const { page } = createEmptyPage(); + +// `page.spaceDoc` is the underlying CRDT data structure. +// Here we connect the doc to the IndexedDB table named 'my-doc' +const provider = new IndexeddbPersistence('my-doc', page.spaceDoc); + +// Case 1. +// If you are creating a new page, +// init here and the block will be automatically written to IndexedDB +page.load(() => { + page.addBlock('affine:page'); + // ... +}); + +// Case 2. +// If you are loading an existing page, +// simply wait here and your content will be ready +await page.load(); +``` + +In both cases, whether the document is **loaded** or **created**, the [`page.slots.ready`](/api/@blocksuite/store/classes/Page.html#ready-1) slot will be triggered, indicating that the document has been initialized. + +Furthermore, by connecting multiple providers, documents can automatically be synchronized to a variety of different backends: + +![pluggable-providers](./images/pluggable-providers.png) + +This brings great flexibility and is the pattern currently being used in [AFFiNE](https://github.com/toeverything/AFFiNE). diff --git a/packages/docs/lit.md b/packages/docs/lit.md index 5bb74c947235..d722aae19e38 100644 --- a/packages/docs/lit.md +++ b/packages/docs/lit.md @@ -3,3 +3,9 @@ Intermediate layer for adapting the block tree to the [lit](https://lit.dev/) framework component tree UI. BlockSuite uses lit as the default framework because lit components are native web components, avoiding synchronization issues between the component tree and DOM tree during complex editing. + +The [`EditorHost`](/api/@blocksuite/lit/classes/EditorHost.html) is a lit component that works as the DOM host of the block tree, and the [`BlockElement`](/api/@blocksuite/lit/classes/BlockElement.html) and [`WidgetElement`](/api/@blocksuite/lit/classes/WidgetElement.html) are standard lit components for extending UI components of block and widget. + +::: tip +Lit components extends `HTMLElement`, so all DOM-related properties are inherited. +::: diff --git a/packages/docs/overview.md b/packages/docs/overview.md index 018c593d6ad9..44a047d07ac9 100644 --- a/packages/docs/overview.md +++ b/packages/docs/overview.md @@ -62,8 +62,8 @@ This can be illustrated as the diagram below: In addition to extending custom blocks, here are what you can also conveniently achieve with BlockSuite: - Writing type-safe complex editing logic based on the [command](./command) mechanism, similar to react hooks designed for document editing. -- Persistence of documents and compatibility with various third-party formats (such as markdown and HTML) based on block [snapshot](./data-persistence#snapshot-api) and transformer. -- Incremental updates, real-time collaboration, local-first state management, and even decentralized data synchronization based on the document's [provider](./data-persistence#provider-based-state-management) mechanism. +- Persistence of documents and compatibility with various third-party formats (such as markdown and HTML) based on block [snapshot](./data-synchronization#snapshot-api) and transformer. +- Incremental updates, real-time collaboration, local-first state management, and even decentralized data synchronization based on the document's [provider](./data-synchronization#provider-based-state-management) mechanism. - State scheduling across multiple documents and reusing one document in multiple editors. To try out BlockSuite, refer to the [Quick Start](./quick-start) document and start with the preset editors in `@blocksuite/presets`. diff --git a/packages/docs/presets/doc-editor.md b/packages/docs/presets/doc-editor.md index d2291ea3395b..d4c4c9656a81 100644 --- a/packages/docs/presets/doc-editor.md +++ b/packages/docs/presets/doc-editor.md @@ -1,5 +1,9 @@ # `DocEditor` +::: info +The comprehensive API surface of this preset is still a work in progress. Please stay tuned! +::: + This editor preset is great at block-based rich text editng, with drag handle, slash menu, format toolbar and other built-in powerful widgets combined. ```ts diff --git a/packages/docs/presets/edgeless-editor.md b/packages/docs/presets/edgeless-editor.md index 5aee97490ab1..2bc09b485186 100644 --- a/packages/docs/presets/edgeless-editor.md +++ b/packages/docs/presets/edgeless-editor.md @@ -1,5 +1,9 @@ # `EdgelessEditor` +::: info +The comprehensive API surface of this preset is still a work in progress. Please stay tuned! +::: + This editor preset is great at whiteboard graphics editing capabilites. It combines canvas-based graphics rendering and DOM-based block tree editing together. This facilitates both creative graphic design and structured document editing, catering to a wide range of user needs and workflows. ```ts diff --git a/packages/docs/store.md b/packages/docs/store.md index 3cc1655e9132..83676be2c7a0 100644 --- a/packages/docs/store.md +++ b/packages/docs/store.md @@ -4,8 +4,56 @@ This package is the data layer for modeling collaborative document states. It's ## `Page` -TODO +In BlockSuite, a [`Page`](/api/@blocksuite/store/classes/Page.html) is the container for a block tree, providing essential functionalities for creating, retrieving, updating, and deleting blocks inside it. Under the hood, every page holds a Yjs [subdocument](https://docs.yjs.dev/api/subdocuments). + +Besides the block tree, the [selection](./selection) state is also stored in the [`page.awarenessStore`](/api/@blocksuite/store/classes/Page.html#awarenessstore) inside the page. This store is also built on top of the Yjs [awareness](https://docs.yjs.dev/api/about-awareness). ## `Workspace` -TODO +In BlockSuite, a [`Workspace`](/api/@blocksuite/store/classes/Workspace.html) is defined as an opt-in collection of multiple pages, providing comprehensive features for managing cross-page updates and data synchronization. You can access the workspace via the `page.workspace` getter, or you can also create a workspace manually: + +```ts +import { Workspace, Schema } from '@blocksuite/store'; + +const schema = new Schema(); + +// You can register a batch of block schemas to the workspace +schema.register(AffineSchemas); + +const workspace = new Workspace({ schema }); +``` + +Then multiple `page`s can be created under the workspace: + +```ts +const workspace = new Workspace({ schema }); + +// This is an empty page at this moment +const page = workspace.createPage(); +``` + +As an example, the `createEmptyPage` is a simple helper implemented exactly in this way ([source](https://github.com/toeverything/blocksuite/blob/master/packages/presets/src/helpers/index.ts)): + +```ts +import { AffineSchemas } from '@blocksuite/blocks/models'; +import { Schema, Workspace } from '@blocksuite/store'; + +export function createEmptyPage() { + const schema = new Schema().register(AffineSchemas); + const workspace = new Workspace({ schema }); + const page = workspace.createPage(); + + return { + page, + async init() { + await page.load(() => { + const pageBlockId = page.addBlock('affine:page', {}); + page.addBlock('affine:surface', {}, pageBlockId); + const noteId = page.addBlock('affine:note', {}, pageBlockId); + page.addBlock('affine:paragraph', {}, noteId); + }); + return page; + }, + }; +} +``` diff --git a/packages/docs/working-with-block-tree.md b/packages/docs/working-with-block-tree.md index 0e0d015b3137..6f923dcc465a 100644 --- a/packages/docs/working-with-block-tree.md +++ b/packages/docs/working-with-block-tree.md @@ -44,7 +44,7 @@ This example creates a subset of the block tree hierarchy defaultly used in `@bl ![block-nesting](./images/block-nesting.png) -As a document-centric framework, **you need to initialize a valid document structure before attaching it to editors**, which is also why it requires `init()` after `createEmptyPage()`. See [creating new block tree](./data-persistence#creating-new-block-tree) for more details. +As a document-centric framework, **you need to initialize a valid document structure before attaching it to editors**, which is also why it requires `init()` after `createEmptyPage()`. See [creating new block tree](./data-synchronization#creating-new-block-tree) for more details. ::: info The block tree hierarchy is specific to the preset editors. At the framework level, `@blocksuite/store` does **NOT** treat the "first-party" `affine:*` blocks with any special way. Feel free to add blocks from different namespaces for the block tree! @@ -134,7 +134,7 @@ For the more complex native [selection](https://developer.mozilla.org/en-US/docs ![flat-inlines](./images/flat-inlines.png) -Additionally, the entire `selection.value` object is isolated under the `clientId` scope of the current session. During collaborative editing, selection instances between different clients will be distributed in real-time (via [providers](./data-persistence#provider-based-state-management)), facilitating the implementation of UI states like remote cursors. +Additionally, the entire `selection.value` object is isolated under the `clientId` scope of the current session. During collaborative editing, selection instances between different clients will be distributed in real-time (via [providers](./data-synchronization#provider-based-state-management)), facilitating the implementation of UI states like remote cursors. For more advanced usage and details, please refer to the [`Selection`](./selection) documentation.